From 5a2be87f658d01bc5b3b7ecdba92ac69bf09dcf7 Mon Sep 17 00:00:00 2001
From: ai_xiaopei <xiaopei@aisim.cn>
Date: Thu, 03 Sep 2026 15:15:37 +0800
Subject: [PATCH] fix(search): 搜索准确性四连修——FTS5 MATCH 引号包裹(连字符不再被解析成 MINUS);KeywordSearch 截断按命中关键词数排序(高频 bigram 不再挤出强相关文档);原词奖励档(完整短语压过泛化 bigram);tags 字段真实参与评分(此前误用 Section)

---
 internal/search/engine.go |   60 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
 1 files changed, 56 insertions(+), 4 deletions(-)

diff --git a/internal/search/engine.go b/internal/search/engine.go
index a1dd52a..b21224d 100644
--- a/internal/search/engine.go
+++ b/internal/search/engine.go
@@ -36,8 +36,24 @@
 	// 合并所有关键词
 	allKeywords := append(append(keywords, opts.Expanded...), opts.Symptom...)
 
+	// 长 CJK 词 bigram 展开(统一下沉):FTS5 unicode61 把连续中文当整串单 token,
+	// 长复合词整串 LIKE 匹配不到;ExpandCJKKeywords 拆成 bigram 滑动窗口
+	// (原词保留、去重保序)。bigram 参与检索,textScore 按 ScoreExpanded 档计权
+	// (与现有 Expanded 机制一致),原词仍按原词档。
+	searchKeywords := ExpandCJKKeywords(allKeywords)
+	originalSet := make(map[string]bool, len(allKeywords))
+	for _, kw := range allKeywords {
+		originalSet[kw] = true
+	}
+	var bigrams []string
+	for _, kw := range searchKeywords {
+		if !originalSet[kw] {
+			bigrams = append(bigrams, kw)
+		}
+	}
+
 	// 1. 双通道检索(ASCII 走 FTS,CJK 走 LIKE)
-	candidates, err := store.KeywordSearch(allKeywords, 100)
+	candidates, err := store.KeywordSearch(searchKeywords, 100)
 	if err != nil {
 		return nil, err
 	}
@@ -55,12 +71,43 @@
 		for _, kw := range opts.Expanded {
 			score += scoreResult(r, kw, ScoreExpanded)
 		}
+		// bigram 展开词按 Expanded 档计权(与用户显式扩展词同档)
+		for _, kw := range bigrams {
+			score += scoreResult(r, kw, ScoreExpanded)
+		}
 		for _, kw := range opts.Symptom {
 			score += scoreResult(r, kw, ScoreSymptom)
 		}
 		textScore[r.ID] = score
 	}
 
+	// 原词奖励:候选由 bigram 召回时可能并不含用户原词("无法"×10 压过
+	// 含"无法开机"整词的文档)。原词真实出现在正文/标题/标签中时给强信号加分,
+	// 使"标题=原词"或"tags 含原词"的文档稳定压过只含泛化 bigram 的文档。
+	for _, r := range candidates {
+		content, _, _, err := store.GetNodeContent(r.ID)
+		if err != nil {
+			continue
+		}
+		lc := strings.ToLower(content)
+		titleL := strings.ToLower(r.Title)
+		for _, kw := range keywords {
+			kwL := strings.ToLower(kw)
+			hit := strings.Contains(lc, kwL) || strings.Contains(titleL, kwL)
+			if !hit {
+				for _, t := range r.Tags {
+					if strings.Contains(strings.ToLower(t), kwL) {
+						hit = true
+						break
+					}
+				}
+			}
+			if hit {
+				textScore[r.ID] += CalcScore(kw, "content", ScoreOriginal)
+			}
+		}
+	}
+
 	// 3. RWR 图质量(种子 = 候选前 20,全量加载邻接后按种子收敛)
 	seedIDs := make([]int64, 0, len(candidates))
 	for _, r := range candidates {
@@ -183,9 +230,14 @@
 		score += CalcScore(keyword, "title", scoreType)
 	}
 
-	// 板块匹配
-	if strings.Contains(strings.ToLower(r.Section), kw) {
-		score += CalcScore(keyword, "tag", scoreType)
+	// 标签匹配(tags 字段真实参与评分;此前误用 Section,而 Section 恒为"其他")
+	if len(r.Tags) > 0 {
+		for _, t := range r.Tags {
+			if strings.Contains(strings.ToLower(t), kw) {
+				score += CalcScore(keyword, "tag", scoreType)
+				break
+			}
+		}
 	}
 
 	// 别名匹配(命中按 title 档计权,一次命中即计)

--
Gitblit v1.10.0