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.go | 147 +++++++++++++++++++++++++++++++++++++++++--------
1 files changed, 123 insertions(+), 24 deletions(-)
diff --git a/internal/search/engine.go b/internal/search/engine.go
index 95d7c37..a1dd52a 100644
--- a/internal/search/engine.go
+++ b/internal/search/engine.go
@@ -4,43 +4,51 @@
"sort"
"strings"
+ "github.com/aisim/kb-cli/internal/graph"
"github.com/aisim/kb-cli/internal/index"
)
// SearchOptions 搜索选项
type SearchOptions struct {
- Expanded []string // 扩展词
- Symptom []string // 症状词
- TopN int // 返回前 N 条
+ Expanded []string // 扩展词
+ Symptom []string // 症状词
+ TopN int // 返回前 N 条
+ WithContent bool // 返回完整文件内容
+ WithLinks bool // 返回关联文档链接
+ TextWeight float64 // 文本分权重(0~1,缺省 0.5;RWR 图质量权重 = 1 - TextWeight)
}
// SearchResult 搜索结果
type SearchResult struct {
- ID int64
- Path string
- Title string
- Section string
- Score int
+ ID int64 `json:"id"`
+ Path string `json:"path"`
+ Title string `json:"title"`
+ Section string `json:"section"`
+ Score int `json:"score"`
+ Status string `json:"status,omitempty"`
+ Content string `json:"content,omitempty"` // 文件内容(WithContent=true 时填充)
+ Links []string `json:"links,omitempty"` // 关联文档路径(WithLinks=true 时填充)
}
-// Search 执行搜索
+// Search 执行搜索(双信号加权:文本位置分 × TextWeight + RWR 图质量 × (1-TextWeight),
+// 草稿态/待审阅降权 0.6)
func Search(store *index.Store, keywords []string, opts SearchOptions) ([]SearchResult, error) {
// 合并所有关键词
- allKeywords := append(keywords, opts.Expanded...)
- allKeywords = append(allKeywords, opts.Symptom...)
+ allKeywords := append(append(keywords, opts.Expanded...), opts.Symptom...)
- // FTS5 搜索
- ftsResults, err := store.FTSSearch(allKeywords, 100)
+ // 1. 双通道检索(ASCII 走 FTS,CJK 走 LIKE)
+ candidates, err := store.KeywordSearch(allKeywords, 100)
if err != nil {
return nil, err
}
+ if len(candidates) == 0 {
+ return nil, nil
+ }
- // 评分
- scoreMap := make(map[int64]int)
- for _, r := range ftsResults {
- score := 0
-
- // 关键词匹配评分
+ // 2. 文本分(沿用位置加权,aliases 按 title 档计权)
+ textScore := make(map[int64]int, len(candidates))
+ for _, r := range candidates {
+ var score int
for _, kw := range keywords {
score += scoreResult(r, kw, ScoreNormal)
}
@@ -50,19 +58,71 @@
for _, kw := range opts.Symptom {
score += scoreResult(r, kw, ScoreSymptom)
}
-
- scoreMap[r.ID] = score
+ textScore[r.ID] = score
}
- // 转换为结果列表
+ // 3. RWR 图质量(种子 = 候选前 20,全量加载邻接后按种子收敛)
+ seedIDs := make([]int64, 0, len(candidates))
+ for _, r := range candidates {
+ if len(seedIDs) >= 20 {
+ break
+ }
+ seedIDs = append(seedIDs, r.ID)
+ }
+ rwrMass := map[int64]float64{}
+ if len(seedIDs) > 0 {
+ adj, err := store.LoadRWRGraph()
+ if err == nil {
+ rwrMass = graph.RWR(seedIDs, adj, 0.25)
+ }
+ }
+
+ // 4. 双信号加权:finalScore = norm(textScore)*tw + rwrMass*(1-tw)
+ tw := opts.TextWeight
+ if tw <= 0 || tw > 1 {
+ tw = 0.5
+ }
+ // 先求文本分真实 min/max,再按 (s-minT)/(maxT-minT) 归一化
+ minT, maxT := 0, 0
+ for _, s := range textScore {
+ if minT == 0 && maxT == 0 {
+ minT, maxT = s, s
+ } else {
+ if s < minT {
+ minT = s
+ }
+ if s > maxT {
+ maxT = s
+ }
+ }
+ }
+ // status 降权:草稿态或待审阅板块 ×0.6
+ statusFactor := make(map[int64]float64, len(candidates))
+ for _, r := range candidates {
+ if r.Section == "待审阅" || isDraftStatus(r.Status) {
+ statusFactor[r.ID] = 0.6
+ } else {
+ statusFactor[r.ID] = 1.0
+ }
+ }
+
var results []SearchResult
- for _, r := range ftsResults {
+ for _, r := range candidates {
+ ts := float64(textScore[r.ID])
+ if maxT > minT {
+ ts = (ts - float64(minT)) / float64(maxT - minT)
+ } else if maxT > 0 {
+ ts = 1
+ }
+ final := ts*tw + rwrMass[r.ID]*(1-tw)
+ final *= statusFactor[r.ID]
results = append(results, SearchResult{
ID: r.ID,
Path: r.Path,
Title: r.Title,
Section: r.Section,
- Score: scoreMap[r.ID],
+ Score: int(final * 100),
+ Status: r.Status,
})
}
@@ -76,9 +136,38 @@
results = results[:opts.TopN]
}
+ // 获取内容(如果请求)
+ if opts.WithContent {
+ for i := range results {
+ content, _, _, err := store.GetNodeContent(results[i].ID)
+ if err == nil {
+ results[i].Content = content
+ }
+ }
+ }
+
+ // 获取关联链接(如果请求)
+ if opts.WithLinks {
+ for i := range results {
+ links, err := store.GetNodeLinks(results[i].ID)
+ if err == nil {
+ results[i].Links = links
+ }
+ }
+ }
+
return results, nil
}
+// isDraftStatus 草稿态降权判断(草稿/待确认/跟进中 降权 0.6)
+func isDraftStatus(status string) bool {
+ switch status {
+ case "草稿", "待确认", "跟进中":
+ return true
+ }
+ return false
+}
+
// scoreResult 计算单个结果的得分
func scoreResult(r index.FTSResult, keyword string, scoreType ScoreType) int {
score := 0
@@ -99,6 +188,16 @@
score += CalcScore(keyword, "tag", scoreType)
}
+ // 别名匹配(命中按 title 档计权,一次命中即计)
+ if len(r.Aliases) > 0 {
+ for _, a := range r.Aliases {
+ if strings.Contains(strings.ToLower(a), kw) {
+ score += CalcScore(keyword, "title", scoreType)
+ break
+ }
+ }
+ }
+
// 内容匹配(FTS 已经匹配,给基础分)
score += CalcScore(keyword, "content", scoreType)
--
Gitblit v1.10.0