From a7fbbfd6f974631eadebc6effca8ce7982592786 Mon Sep 17 00:00:00 2001
From: ax_rd <ax_rd@aisim.cn>
Date: Thu, 03 Sep 2026 12:51:40 +0800
Subject: [PATCH] fix: explore 段落命中两级回退(原词→展开词)+ expandKeywords 去重 + HardBudget 接入

---
 internal/search/engine_test.go |   69 ++++++++++++++++++++++++++++++++++
 1 files changed, 69 insertions(+), 0 deletions(-)

diff --git a/internal/search/engine_test.go b/internal/search/engine_test.go
index 2cd91d3..6aeeb1e 100644
--- a/internal/search/engine_test.go
+++ b/internal/search/engine_test.go
@@ -78,6 +78,75 @@
 	}
 }
 
+func TestCJKLikeChannel(t *testing.T) {
+	// CJK LIKE 通道:多字符中文词 FTS MATCH 匹配不到(unicode61 整串 token),
+	// 必须走 LIKE 才能命中 content/title
+	tmpDir := t.TempDir()
+	dbPath := filepath.Join(tmpDir, "test.db")
+	store, err := index.Open(dbPath)
+	if err != nil {
+		t.Fatalf("Open failed: %v", err)
+	}
+	defer store.Close()
+
+	nodes := []*graph.Node{
+		{ // content 含"补气",title 不含(靠 LIKE content 通道命中)
+			Path:    "FAQ/001-a.md",
+			Title:   "设备故障排查",
+			Section: "FAQ",
+			Content: "电子秤补气失败时的排查步骤",
+		},
+		{ // title 含"补气"(title LIKE 命中,应排前)
+			Path:    "FAQ/002-b.md",
+			Title:   "电子秤补气失败",
+			Section: "FAQ",
+			Content: "补气失败的处理方法",
+		},
+		{ // 不含"补气"(不应出现在结果中)
+			Path:    "FAQ/003-c.md",
+			Title:   "阀门漏气处理",
+			Section: "FAQ",
+			Content: "阀门漏气的原因和处理",
+		},
+	}
+	for _, n := range nodes {
+		if err := store.UpsertNode(n, 100, 1, "x"); err != nil {
+			t.Fatalf("UpsertNode failed: %v", err)
+		}
+	}
+	if err := store.CreateFTS(); err != nil {
+		t.Fatalf("CreateFTS failed: %v", err)
+	}
+	if err := store.PopulateFTS(); err != nil {
+		t.Fatalf("PopulateFTS failed: %v", err)
+	}
+
+	results, err := Search(store, []string{"补气"}, SearchOptions{})
+	if err != nil {
+		t.Fatalf("Search failed: %v", err)
+	}
+	if len(results) < 2 {
+		t.Fatalf("Expected 2 results (content 命中 + title 命中), got %d", len(results))
+	}
+	// 两个含"补气"的节点都应在结果中
+	if results[0].Path != "FAQ/002-b.md" && results[1].Path != "FAQ/002-b.md" {
+		t.Error("title 含'补气'的节点应出现在结果中")
+	}
+	if results[0].Path != "FAQ/001-a.md" && results[1].Path != "FAQ/001-a.md" {
+		t.Error("content 含'补气'的节点应出现在结果中")
+	}
+	// 不含"补气"的节点不应出现
+	for _, r := range results {
+		if r.Path == "FAQ/003-c.md" {
+			t.Error("不含关键词的节点不应出现在结果中")
+		}
+	}
+	// title 命中者应排前(title 计权高于 content)
+	if results[0].Path != "FAQ/002-b.md" {
+		t.Errorf("title 命中者应排第一, got %q", results[0].Path)
+	}
+}
+
 func TestIsGenericWord(t *testing.T) {
 	tests := []struct {
 		word     string

--
Gitblit v1.10.0