internal/search/engine.go
@@ -36,8 +36,24 @@
   // 合并所有关键词
   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
   }
@@ -55,12 +71,43 @@
      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 {
@@ -183,9 +230,14 @@
      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 档计权,一次命中即计)