ax_rd
7 hours ago a5c01a3f115399b55f43d639c8278b8d68c6211c
internal/index/fts.go
@@ -3,9 +3,8 @@
import (
   "database/sql"
   "encoding/json"
   "strings"
   "sort"
)
// FTSResult 全文搜索结果
type FTSResult struct {
   ID      int64
@@ -15,27 +14,46 @@
   Rank    float64
}
// CreateFTS 创建 FTS5 虚拟表
// CreateFTS 创建 FTS5 虚拟表(external-content 模式,由触发器增量维护)
// 注意:external-content 模式下 FTS 列名必须与 content 表列名一致,
// 故 content 列对应 nodes 表的 content_fts 列
func (s *Store) CreateFTS() error {
   _, err := s.db.Exec(`
      CREATE VIRTUAL TABLE IF NOT EXISTS nodes_fts USING fts5(
         title,
         content,
         tags,
         entities,
         content='nodes',
         content_rowid='id'
      )
   `)
   return err
         title, content_fts, tags, aliases,
         content='nodes', content_rowid='id'
      )`)
   if err != nil {
      return err
   }
   // 触发器:nodes 的增删改同步维护 FTS
   triggers := []string{
      `CREATE TRIGGER IF NOT EXISTS nodes_fts_ai AFTER INSERT ON nodes BEGIN
         INSERT INTO nodes_fts(rowid, title, content_fts, tags, aliases)
         VALUES (new.id, new.title, new.content_fts, new.tags, new.aliases);
      END`,
      `CREATE TRIGGER IF NOT EXISTS nodes_fts_ad AFTER DELETE ON nodes BEGIN
         INSERT INTO nodes_fts(nodes_fts, rowid, title, content_fts, tags, aliases)
         VALUES ('delete', old.id, old.title, old.content_fts, old.tags, old.aliases);
      END`,
      `CREATE TRIGGER IF NOT EXISTS nodes_fts_au AFTER UPDATE ON nodes BEGIN
         INSERT INTO nodes_fts(nodes_fts, rowid, title, content_fts, tags, aliases)
         VALUES ('delete', old.id, old.title, old.content_fts, old.tags, old.aliases);
         INSERT INTO nodes_fts(rowid, title, content_fts, tags, aliases)
         VALUES (new.id, new.title, new.content_fts, new.tags, new.aliases);
      END`,
   }
   for _, t := range triggers {
      if _, err := s.db.Exec(t); err != nil {
         return err
      }
   }
   return nil
}
// PopulateFTS 填充 FTS 索引
// PopulateFTS 全量重灌 FTS(external-content 模式专用语法)
func (s *Store) PopulateFTS() error {
   _, err := s.db.Exec(`
      INSERT INTO nodes_fts(rowid, title, content, tags, entities)
      SELECT id, title, content_fts, tags, entities FROM nodes
   `)
   _, err := s.db.Exec(`INSERT INTO nodes_fts(nodes_fts) VALUES('rebuild')`)
   return err
}
@@ -45,31 +63,51 @@
      return nil, nil
   }
   // 构建 FTS5 查询
   query := strings.Join(keywords, " OR ")
   // 对每个关键词单独搜索,然后合并结果(去重)
   // 这样即使某个关键词匹配不到,其他关键词也能找到结果
   seen := make(map[int64]bool)
   var allResults []FTSResult
   rows, err := s.db.Query(`
      SELECT n.id, n.path, n.title, n.section, fts.rank
      FROM nodes_fts fts
      JOIN nodes n ON n.id = fts.rowid
      WHERE nodes_fts MATCH ?
      ORDER BY fts.rank
      LIMIT ?
   `, query, limit)
   if err != nil {
      return nil, err
   }
   defer rows.Close()
   var results []FTSResult
   for rows.Next() {
      var r FTSResult
      if err := rows.Scan(&r.ID, &r.Path, &r.Title, &r.Section, &r.Rank); err != nil {
         return nil, err
   for _, kw := range keywords {
      rows, err := s.db.Query(`
         SELECT n.id, n.path, n.title, n.section, fts.rank
         FROM nodes_fts fts
         JOIN nodes n ON n.id = fts.rowid
         WHERE nodes_fts MATCH ?
         ORDER BY fts.rank
         LIMIT ?
      `, kw, limit)
      if err != nil {
         // 单个关键词搜索失败,跳过继续
         continue
      }
      results = append(results, r)
      for rows.Next() {
         var r FTSResult
         if err := rows.Scan(&r.ID, &r.Path, &r.Title, &r.Section, &r.Rank); err != nil {
            rows.Close()
            continue
         }
         // 去重
         if !seen[r.ID] {
            seen[r.ID] = true
            allResults = append(allResults, r)
         }
      }
      rows.Close()
   }
   return results, nil
   // 按 rank 排序(FTS5 的 rank 越小越好)
   sort.Slice(allResults, func(i, j int) bool {
      return allResults[i].Rank < allResults[j].Rank
   })
   // 限制返回数量
   if limit > 0 && len(allResults) > limit {
      allResults = allResults[:limit]
   }
   return allResults, nil
}
// GetNodeContent 获取节点内容