package rag import "testing" // TestBleve_ChineseSearch 钉死中文全文检索:CJK 分词器让"星云一号的总设计师"能命中含 // "星云一号""总设计师"的中文块。修复前默认标准分词器把整段中文当一个 token → 永远 0 命中。 func TestBleve_ChineseSearch(t *testing.T) { t.Setenv("BLEVE_PATH", t.TempDir()+"/bleve") // 隔离落盘索引,测试互不污染 b := openBleve() defer b.close() 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)) } } } // TestBleve_PersistsAcrossReopen 钉死落盘:写入→关闭→同路径重开,数据仍在。 // 修复"NewMemOnly 进程重启即丢、三路静默退化两路"的回归——这是 P0 的核心验收。 func TestBleve_PersistsAcrossReopen(t *testing.T) { t.Setenv("BLEVE_PATH", t.TempDir()+"/bleve") b1 := openBleve() if !b1.ready() { t.Skip("bleve 不可用") } if err := b1.index("kp", "d1", []string{"持久化测试:龙渊号探测器由总师陈思远负责。"}); err != nil { t.Fatal(err) } b1.close() // 模拟进程退出(落盘刷盘) b2 := openBleve() // 模拟重启:同路径重开 defer b2.close() if hits := b2.search("kp", "龙渊号探测器", 5); len(hits) == 0 { t.Fatal("重开后应仍能检索到(落盘持久),got 0 —— 退回内存了?") } } 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 }