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
+60
View File
@@ -0,0 +1,60 @@
package rag
import "testing"
// TestBleve_ChineseSearch 钉死中文全文检索:CJK 分词器让"星云一号的总设计师"能命中含
// "星云一号""总设计师"的中文块。修复前默认标准分词器把整段中文当一个 token → 永远 0 命中。
func TestBleve_ChineseSearch(t *testing.T) {
b := openBleve()
if !b.ready() {
t.Skip("bleve 不可用")
}
const kb = "k1"
if err := b.index(kb, "d1", []string{
"星云一号卫星于2023年由长征七号发射,项目总设计师是李明华。",
"今天天气不错,适合出门散步。",
}); err != nil {
t.Fatal(err)
}
hits := b.search(kb, "星云一号的总设计师是谁", 5)
if len(hits) == 0 {
t.Fatal("中文全文检索应命中(CJK 分词),got 0 —— 分词器回归了?")
}
if !contains(hits[0].Text, "总设计师") {
t.Fatalf("最相关块应含'总设计师'got %q", hits[0].Text)
}
}
// TestQueryNgrams 钉死查询 n-gram:含"星云一号"等子串,能与更长实体名"星云一号卫星"双向匹配。
func TestQueryNgrams(t *testing.T) {
ng := queryNgrams("星云一号的总设计师是谁")
if !hasStr(ng, "星云一号") {
t.Fatalf("应含 2..8 长度子串'星云一号'got %v", ng)
}
// 长度边界:最短 2、最长 8。
for _, s := range ng {
if r := []rune(s); len(r) < 2 || len(r) > 8 {
t.Fatalf("n-gram 长度应在 [2,8]got %q(len=%d)", s, len(r))
}
}
}
func contains(s, sub string) bool {
return len(sub) == 0 || (len(s) >= len(sub) && indexOf(s, sub) >= 0)
}
func indexOf(s, sub string) int {
for i := 0; i+len(sub) <= len(s); i++ {
if s[i:i+len(sub)] == sub {
return i
}
}
return -1
}
func hasStr(ss []string, want string) bool {
for _, s := range ss {
if s == want {
return true
}
}
return false
}