From 13c53904c5ddea86c077a057d5987f732e8fc974 Mon Sep 17 00:00:00 2001
From: ax_rd <ax_rd@aisim.cn>
Date: Thu, 03 Sep 2026 12:20:41 +0800
Subject: [PATCH] feat: explore 命令(字节预算 + 段落级原文直出 + 关联清单 + 悬空链接提示)
---
internal/search/engine.go | 107 +++++++++++++++++++++++++++++++++++++++++++++--------
1 files changed, 91 insertions(+), 16 deletions(-)
diff --git a/internal/search/engine.go b/internal/search/engine.go
index 16fa86d..a1dd52a 100644
--- a/internal/search/engine.go
+++ b/internal/search/engine.go
@@ -4,6 +4,7 @@
"sort"
"strings"
+ "github.com/aisim/kb-cli/internal/graph"
"github.com/aisim/kb-cli/internal/index"
)
@@ -14,6 +15,7 @@
TopN int // 返回前 N 条
WithContent bool // 返回完整文件内容
WithLinks bool // 返回关联文档链接
+ TextWeight float64 // 文本分权重(0~1,缺省 0.5;RWR 图质量权重 = 1 - TextWeight)
}
// SearchResult 搜索结果
@@ -23,28 +25,30 @@
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)
}
@@ -54,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,
})
}
@@ -103,6 +159,15 @@
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
@@ -123,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