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 }