Files
sundynix-agentix/sundynix-mcp-go/internal/rag/cjk_test.go
T
Blizzard b26fe21408 fix(rag): 检索三路不再静默吞错 —— 逐路诊断 + 一路挂不拖垮全部
排查"向量路为什么是空的"花了半小时,因为空就是空,没有任何线索。这次把
整条检索链上的静默降级一次清掉。

真 bug(不只是可观测性):
  - kb_search 与 Search() 都拿 rag.Ready() 当总闸,而 Ready() 只代表"向量路
    可用"(embedding + Milvus)。全文(bleve)与图谱(Neo4j)根本不依赖它们,却
    被一并毙掉 → "模型配置没下发"表现为"整个知识库什么都搜不到",还不报错。
    改为逐路判定,任一路可用就仍有召回。

不再吞错:
  - milvus.search 原先把 error 转成 nil,nil —— 检索失败与无召回彻底无法区分;
  - bleve.search / graph.search 出错直接回 nil,连日志都没有;
  - searchPaths 丢掉 embedding 的 error。
    三处改为如实返回,错误统一打日志。

逐路诊断(RouteDiag):每路上报 ok/empty/disabled/error + 耗时 + 原因,经
kb_search 的 diag 参数(仅试验台传,生产调用返回值不变)→ gateway → 检索
试验台。界面上现在能直接看出"这一路没配置/报错了/确实没匹配",不必翻日志。
内存兜底索引也会在 note 里点明"重启即清零"。

测试:3 组,覆盖"无 embedding 时全文仍可召回"、三种空的区分、内存索引提示。
把总闸加回去验证过第一条确实会红——测试能抓到这个回归,不是摆设。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 15:10:03 +08:00

86 lines
2.7 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) {
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, err := b.search(kb, "星云一号的总设计师是谁", 5)
if err != nil {
t.Fatalf("全文检索报错: %v", err)
}
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, err := b2.search("kp", "龙渊号探测器", 5); err != nil || 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
}