| | |
| | | // 合并所有关键词 |
| | | allKeywords := append(append(keywords, opts.Expanded...), opts.Symptom...) |
| | | |
| | | // 长 CJK 词 bigram 展开(统一下沉):FTS5 unicode61 把连续中文当整串单 token, |
| | | // 长复合词整串 LIKE 匹配不到;ExpandCJKKeywords 拆成 bigram 滑动窗口 |
| | | // (原词保留、去重保序)。bigram 参与检索,textScore 按 ScoreExpanded 档计权 |
| | | // (与现有 Expanded 机制一致),原词仍按原词档。 |
| | | searchKeywords := ExpandCJKKeywords(allKeywords) |
| | | originalSet := make(map[string]bool, len(allKeywords)) |
| | | for _, kw := range allKeywords { |
| | | originalSet[kw] = true |
| | | } |
| | | var bigrams []string |
| | | for _, kw := range searchKeywords { |
| | | if !originalSet[kw] { |
| | | bigrams = append(bigrams, kw) |
| | | } |
| | | } |
| | | |
| | | // 1. 双通道检索(ASCII 走 FTS,CJK 走 LIKE) |
| | | candidates, err := store.KeywordSearch(allKeywords, 100) |
| | | candidates, err := store.KeywordSearch(searchKeywords, 100) |
| | | if err != nil { |
| | | return nil, err |
| | | } |
| | |
| | | for _, kw := range opts.Expanded { |
| | | score += scoreResult(r, kw, ScoreExpanded) |
| | | } |
| | | // bigram 展开词按 Expanded 档计权(与用户显式扩展词同档) |
| | | for _, kw := range bigrams { |
| | | score += scoreResult(r, kw, ScoreExpanded) |
| | | } |
| | | for _, kw := range opts.Symptom { |
| | | score += scoreResult(r, kw, ScoreSymptom) |
| | | } |
| | | textScore[r.ID] = score |
| | | } |
| | | |
| | | // 原词奖励:候选由 bigram 召回时可能并不含用户原词("无法"×10 压过 |
| | | // 含"无法开机"整词的文档)。原词真实出现在正文/标题/标签中时给强信号加分, |
| | | // 使"标题=原词"或"tags 含原词"的文档稳定压过只含泛化 bigram 的文档。 |
| | | for _, r := range candidates { |
| | | content, _, _, err := store.GetNodeContent(r.ID) |
| | | if err != nil { |
| | | continue |
| | | } |
| | | lc := strings.ToLower(content) |
| | | titleL := strings.ToLower(r.Title) |
| | | for _, kw := range keywords { |
| | | kwL := strings.ToLower(kw) |
| | | hit := strings.Contains(lc, kwL) || strings.Contains(titleL, kwL) |
| | | if !hit { |
| | | for _, t := range r.Tags { |
| | | if strings.Contains(strings.ToLower(t), kwL) { |
| | | hit = true |
| | | break |
| | | } |
| | | } |
| | | } |
| | | if hit { |
| | | textScore[r.ID] += CalcScore(kw, "content", ScoreOriginal) |
| | | } |
| | | } |
| | | } |
| | | |
| | | // 3. RWR 图质量(种子 = 候选前 20,全量加载邻接后按种子收敛) |
| | | seedIDs := make([]int64, 0, len(candidates)) |
| | | for _, r := range candidates { |
| | |
| | | score += CalcScore(keyword, "title", scoreType) |
| | | } |
| | | |
| | | // 板块匹配 |
| | | if strings.Contains(strings.ToLower(r.Section), kw) { |
| | | score += CalcScore(keyword, "tag", scoreType) |
| | | // 标签匹配(tags 字段真实参与评分;此前误用 Section,而 Section 恒为"其他") |
| | | if len(r.Tags) > 0 { |
| | | for _, t := range r.Tags { |
| | | if strings.Contains(strings.ToLower(t), kw) { |
| | | score += CalcScore(keyword, "tag", scoreType) |
| | | break |
| | | } |
| | | } |
| | | } |
| | | |
| | | // 别名匹配(命中按 title 档计权,一次命中即计) |