From 3e6659d64a22fe664d72f9eb83310f697ff27a94 Mon Sep 17 00:00:00 2001
From: ax_rd <ax_rd@aisim.cn>
Date: Thu, 03 Sep 2026 11:40:07 +0800
Subject: [PATCH] feat: 悬空双链自动补全(RetryUnresolved,Reconcile 末尾自动调用)+ 3 个 Task3 审查 Minor 修复

---
 internal/index/fts.go |  116 ++++++++++++++++++++++++++++++++++++++-------------------
 1 files changed, 77 insertions(+), 39 deletions(-)

diff --git a/internal/index/fts.go b/internal/index/fts.go
index 5e67985..4323985 100644
--- a/internal/index/fts.go
+++ b/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 获取节点内容

--
Gitblit v1.10.0