fix(rag): T2.2 把"三路混合"做实 —— 中文全文/图谱两路从 0 贡献修活

基建拉起后实测发现"三路混合检索"对中文其实只有向量路在干活:
`hybrid: 向量=1 全文=0 图谱=0` —— 全文、图谱两路恒 0 贡献(典型"广而浅")。两个真因:

1. Bleve 全文:默认标准分词器不切中文 → 整段当一个 token → 中文查询永远 0 命中。
   修:text 字段用 cjk 分词器(bigram),kb/doc 用 keyword(保 TermQuery 精确过滤)。
2. 图谱检索:`$q CONTAINS a.name` 要求实体名是查询子串,而 LLM 把实体抽成"星云一号卫星"
   (带后缀),查询说"星云一号"→ CONTAINS 失败 → 0 命中。
   修:加查询字符 n-gram(2..8)双向子串匹配,`星云一号` 即可命中 `星云一号卫星`。

live 验证(同一查询):修复前 `向量=1 全文=0 图谱=0` → 修复后 `向量=1 全文=1 图谱=9`,
三路全贡献。中文混合检索从 1/3 变真 3/3。
测试:Bleve 中文检索命中 + queryNgrams 子串/长度边界(CI 安全,无需基建)。

DEPTH_ROADMAP T2.2:三路做实;检索质量度量(离线评测集 recall@k/MRR)待补。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Blizzard
2026-06-30 10:00:25 +08:00
parent 5d090304e5
commit a4852c3aef
3 changed files with 106 additions and 2 deletions
+26 -1
View File
@@ -84,11 +84,14 @@ func (g *graphStore) search(ctx context.Context, kb, query string, limit int) []
if !g.ready() || query == "" {
return nil
}
// 匹配两路:① 查询整体含实体名($q CONTAINS);② 查询的字符 n-gram 与实体名互为子串
// —— 解决"查询说'星云一号'、实体名抽成'星云一号卫星'"这类后缀错配(纯 $q CONTAINS 会漏)。
res, err := neo4j.ExecuteQuery(ctx, g.driver,
`MATCH (a:Entity {kb:$kb})-[r:REL]->(b:Entity {kb:$kb})
WHERE $q CONTAINS a.name OR $q CONTAINS b.name
OR any(ng IN $ngrams WHERE a.name CONTAINS ng OR b.name CONTAINS ng)
RETURN a.name AS s, r.type AS p, b.name AS o LIMIT $k`,
map[string]any{"kb": kb, "q": query, "k": limit},
map[string]any{"kb": kb, "q": query, "ngrams": queryNgrams(query), "k": limit},
neo4j.EagerResultTransformer, neo4j.ExecuteQueryWithDatabase("neo4j"))
if err != nil {
return nil
@@ -103,6 +106,28 @@ func (g *graphStore) search(ctx context.Context, kb, query string, limit int) []
return hits
}
// queryNgrams 生成查询的字符 n-gram(长度 2..8,去重并截断)——用于和图谱实体名做双向子串匹配,
// 解决实体名比查询里提到的更长/更短的错配(中文无词边界,按字符 n-gram 是务实做法)。
func queryNgrams(q string) []string {
r := []rune(q)
const minLen, maxLen, cap = 2, 8, 80
seen := make(map[string]bool)
out := make([]string, 0, cap)
for i := 0; i < len(r); i++ {
for l := minLen; l <= maxLen && i+l <= len(r); l++ {
s := string(r[i : i+l])
if !seen[s] {
seen[s] = true
out = append(out, s)
if len(out) >= cap {
return out
}
}
}
}
return out
}
// triples 返回某 kb 的全部三元组(供 UI 图谱可视化)。
func (g *graphStore) triples(ctx context.Context, kb string, limit int) []Triple {
if !g.ready() {