Files
sundynix-agentix/sundynix-mcp-go/internal/rag/cjk_test.go
T
Blizzard a4852c3aef 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>
2026-06-30 10:00:25 +08:00

61 lines
1.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}