ai_xiaopei
9 days ago 46a01d99b5d15120ada95754c9effbdcb576d31e
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
package search
 
import (
    "sort"
    "strings"
 
    "github.com/aisim/kb-cli/internal/index"
)
 
// SearchOptions 搜索选项
type SearchOptions struct {
    Expanded    []string // 扩展词
    Symptom     []string // 症状词
    TopN        int      // 返回前 N 条
    WithContent bool     // 返回完整文件内容
    WithLinks   bool     // 返回关联文档链接
}
 
// SearchResult 搜索结果
type SearchResult struct {
    ID      int64    `json:"id"`
    Path    string   `json:"path"`
    Title   string   `json:"title"`
    Section string   `json:"section"`
    Score   int      `json:"score"`
    Content string   `json:"content,omitempty"` // 文件内容(WithContent=true 时填充)
    Links   []string `json:"links,omitempty"`   // 关联文档路径(WithLinks=true 时填充)
}
 
// Search 执行搜索
func Search(store *index.Store, keywords []string, opts SearchOptions) ([]SearchResult, error) {
    // 合并所有关键词
    allKeywords := append(keywords, opts.Expanded...)
    allKeywords = append(allKeywords, opts.Symptom...)
 
    // FTS5 搜索
    ftsResults, err := store.FTSSearch(allKeywords, 100)
    if err != nil {
        return nil, err
    }
 
    // 评分
    scoreMap := make(map[int64]int)
    for _, r := range ftsResults {
        score := 0
 
        // 关键词匹配评分
        for _, kw := range keywords {
            score += scoreResult(r, kw, ScoreNormal)
        }
        for _, kw := range opts.Expanded {
            score += scoreResult(r, kw, ScoreExpanded)
        }
        for _, kw := range opts.Symptom {
            score += scoreResult(r, kw, ScoreSymptom)
        }
 
        scoreMap[r.ID] = score
    }
 
    // 转换为结果列表
    var results []SearchResult
    for _, r := range ftsResults {
        results = append(results, SearchResult{
            ID:      r.ID,
            Path:    r.Path,
            Title:   r.Title,
            Section: r.Section,
            Score:   scoreMap[r.ID],
        })
    }
 
    // 按分数排序
    sort.Slice(results, func(i, j int) bool {
        return results[i].Score > results[j].Score
    })
 
    // 限制返回数量
    if opts.TopN > 0 && len(results) > opts.TopN {
        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
}
 
// scoreResult 计算单个结果的得分
func scoreResult(r index.FTSResult, keyword string, scoreType ScoreType) int {
    score := 0
    kw := strings.ToLower(keyword)
 
    // 路径匹配
    if strings.Contains(strings.ToLower(r.Path), kw) {
        score += CalcScore(keyword, "path", scoreType)
    }
 
    // 标题匹配
    if strings.Contains(strings.ToLower(r.Title), kw) {
        score += CalcScore(keyword, "title", scoreType)
    }
 
    // 板块匹配
    if strings.Contains(strings.ToLower(r.Section), kw) {
        score += CalcScore(keyword, "tag", scoreType)
    }
 
    // 内容匹配(FTS 已经匹配,给基础分)
    score += CalcScore(keyword, "content", scoreType)
 
    return score
}