package search import ( "sort" "strings" "github.com/aisim/kb-cli/internal/index" ) // SearchOptions 搜索选项 type SearchOptions struct { Expanded []string // 扩展词 Symptom []string // 症状词 TopN int // 返回前 N 条 } // SearchResult 搜索结果 type SearchResult struct { ID int64 Path string Title string Section string Score int } // 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] } 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 }