From b554c303193aa5d22af5f8a4b91430077113e6db Mon Sep 17 00:00:00 2001
From: ax_rd <ax_rd@aisim.cn>
Date: Thu, 03 Sep 2026 13:21:36 +0800
Subject: [PATCH] fix: search 长 CJK 词 bigram 展开统一下沉(ExpandCJKKeywords 共享 + textScore 按 Expanded 档计权 + graph query 边 provenance 填充)

---
 internal/index/sqlite.go |  149 +++++++++++++++++++++++++++++++++++++++++++++----
 1 files changed, 137 insertions(+), 12 deletions(-)

diff --git a/internal/index/sqlite.go b/internal/index/sqlite.go
index 1501020..9f8d4c5 100644
--- a/internal/index/sqlite.go
+++ b/internal/index/sqlite.go
@@ -15,7 +15,8 @@
 
 // Store SQLite 存储层
 type Store struct {
-	db *sql.DB
+	db     *sql.DB
+	dbPath string
 }
 
 // Open 打开或创建数据库
@@ -31,11 +32,15 @@
 		return nil, fmt.Errorf("打开数据库失败: %w", err)
 	}
 
-	s := &Store{db: db}
+	s := &Store{db: db, dbPath: dbPath}
 	if err := s.initTables(); err != nil {
 		db.Close()
 		return nil, err
 	}
+	if err := s.migrate(); err != nil {
+		db.Close()
+		return nil, err
+	}
 	return s, nil
 }
 
@@ -88,21 +93,24 @@
 
 // ClearData 清空数据(重建前调用)
 func (s *Store) ClearData() error {
-	_, err := s.db.Exec("DELETE FROM edges; DELETE FROM nodes;")
+	_, err := s.db.Exec("DELETE FROM edges; DELETE FROM nodes; DELETE FROM unresolved_links;")
 	return err
 }
 
-// InsertNode 插入节点
+// InsertNode 插入节点(全量重建路径用)
 func (s *Store) InsertNode(n *graph.Node) (int64, error) {
 	tagsJSON, _ := json.Marshal(n.Tags)
 	entitiesJSON, _ := json.Marshal(n.Entities)
 	wikilinksJSON, _ := json.Marshal(n.Wikilinks)
+	aliasesJSON, _ := json.Marshal(n.Aliases)
 
 	result, err := s.db.Exec(`
-		INSERT INTO nodes (path, title, section, tags, entities, wikilinks, content_fts)
-		VALUES (?, ?, ?, ?, ?, ?, ?)
+		INSERT INTO nodes (path, title, section, tags, entities, wikilinks, aliases, status,
+		               content_fts, size, mtime, content_hash)
+		VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
 	`, n.Path, n.Title, n.Section, string(tagsJSON), string(entitiesJSON),
-		string(wikilinksJSON), n.Content)
+		string(wikilinksJSON), string(aliasesJSON), n.Status, n.Content,
+		n.Size, n.Mtime, n.ContentHash)
 	if err != nil {
 		return 0, err
 	}
@@ -112,9 +120,108 @@
 // InsertEdge 插入边
 func (s *Store) InsertEdge(e *graph.Edge) error {
 	_, err := s.db.Exec(`
-		INSERT OR IGNORE INTO edges (from_node, to_node, relation, label)
-		VALUES (?, ?, ?, ?)
-	`, e.FromNode, e.ToNode, e.Relation, e.Label)
+		INSERT OR IGNORE INTO edges (from_node, to_node, relation, label, provenance)
+		VALUES (?, ?, ?, ?, ?)
+	`, e.FromNode, e.ToNode, e.Relation, e.Label, e.Provenance)
+	return err
+}
+
+// InsertUnresolved 悬空链接入表(幂等:同 from_node+link_text 不重复插)
+func (s *Store) InsertUnresolved(fromNode int64, linkText, nameTail string) error {
+	_, err := s.db.Exec(`
+		INSERT INTO unresolved_links (from_node, link_text, name_tail, status)
+		SELECT ?, ?, ?, 'pending'
+		WHERE NOT EXISTS (
+			SELECT 1 FROM unresolved_links WHERE from_node = ? AND link_text = ?)`,
+		fromNode, linkText, nameTail, fromNode, linkText)
+	return err
+}
+
+// FileStat 索引中的文件指纹
+type FileStat struct {
+	Path        string
+	Size        int64
+	Mtime       int64
+	ContentHash string
+}
+
+// GetFileStats 返回所有已索引文件的指纹
+func (s *Store) GetFileStats() (map[string]FileStat, error) {
+	rows, err := s.db.Query(`SELECT path, size, mtime, content_hash FROM nodes`)
+	if err != nil {
+		return nil, err
+	}
+	defer rows.Close()
+	m := make(map[string]FileStat)
+	for rows.Next() {
+		var st FileStat
+		var hash sql.NullString
+		if err := rows.Scan(&st.Path, &st.Size, &st.Mtime, &hash); err != nil {
+			return nil, err
+		}
+		st.ContentHash = hash.String
+		m[st.Path] = st
+	}
+	return m, rows.Err()
+}
+
+// UpsertNode 按 path 插入或更新节点(触发器自动维护 FTS)
+func (s *Store) UpsertNode(n *graph.Node, size, mtime int64, contentHash string) error {
+	tagsJSON, _ := json.Marshal(n.Tags)
+	entitiesJSON, _ := json.Marshal(n.Entities)
+	wikilinksJSON, _ := json.Marshal(n.Wikilinks)
+	aliasesJSON, _ := json.Marshal(n.Aliases)
+	_, err := s.db.Exec(`
+		INSERT INTO nodes (path, title, section, tags, entities, wikilinks, aliases, status,
+		               content_fts, size, mtime, content_hash)
+		VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+		ON CONFLICT(path) DO UPDATE SET
+			title = excluded.title,
+			section = excluded.section,
+			tags = excluded.tags,
+			entities = excluded.entities,
+			wikilinks = excluded.wikilinks,
+			aliases = excluded.aliases,
+			status = excluded.status,
+			content_fts = excluded.content_fts,
+			size = excluded.size,
+			mtime = excluded.mtime,
+			content_hash = excluded.content_hash,
+			updated_at = datetime('now')`,
+		n.Path, n.Title, n.Section, string(tagsJSON), string(entitiesJSON),
+		string(wikilinksJSON), string(aliasesJSON), n.Status, n.Content, size, mtime, contentHash)
+	return err
+}
+
+// DeleteNode 按 path 删节点(级联删边),同时清该节点相关的 unresolved_links
+func (s *Store) DeleteNode(path string) error {
+	tx, err := s.db.Begin()
+	if err != nil {
+		return err
+	}
+	var id int64
+	if err := tx.QueryRow(`SELECT id FROM nodes WHERE path = ?`, path).Scan(&id); err != nil {
+		tx.Rollback()
+		return nil // 不存在,视为成功
+	}
+	if _, err := tx.Exec(`DELETE FROM edges WHERE from_node = ? OR to_node = ?`, id, id); err != nil {
+		tx.Rollback()
+		return err
+	}
+	if _, err := tx.Exec(`DELETE FROM unresolved_links WHERE from_node = ?`, id); err != nil {
+		tx.Rollback()
+		return err
+	}
+	if _, err := tx.Exec(`DELETE FROM nodes WHERE id = ?`, id); err != nil {
+		tx.Rollback()
+		return err
+	}
+	return tx.Commit()
+}
+
+// DeleteNodeEdges 只删某节点的边(保留节点行,用于"修改"场景重建边)
+func (s *Store) DeleteNodeEdges(nodeID int64) error {
+	_, err := s.db.Exec(`DELETE FROM edges WHERE from_node = ? OR to_node = ?`, nodeID, nodeID)
 	return err
 }
 
@@ -180,6 +287,24 @@
 	return links, nil
 }
 
+// GetUnresolvedLinks 某文档的悬空链接文本列表
+func (s *Store) GetUnresolvedLinks(path string) ([]string, error) {
+	rows, err := s.db.Query(`
+		SELECT u.link_text FROM unresolved_links u
+		JOIN nodes n ON n.id = u.from_node WHERE n.path = ?`, path)
+	if err != nil {
+		return nil, err
+	}
+	defer rows.Close()
+	var links []string
+	for rows.Next() {
+		var l string
+		rows.Scan(&l)
+		links = append(links, l)
+	}
+	return links, rows.Err()
+}
+
 // NodeInfo 节点基本信息(用于 GC)
 type NodeInfo struct {
 	ID   int64
@@ -357,7 +482,7 @@
 			visited[id] = true
 
 			query := `
-				SELECT id, from_node, to_node, relation, label
+				SELECT id, from_node, to_node, relation, label, provenance
 				FROM edges
 				WHERE from_node = ?
 			`
@@ -376,7 +501,7 @@
 			for rows.Next() {
 				var e graph.Edge
 				var edgeID int64
-				if err := rows.Scan(&edgeID, &e.FromNode, &e.ToNode, &e.Relation, &e.Label); err != nil {
+				if err := rows.Scan(&edgeID, &e.FromNode, &e.ToNode, &e.Relation, &e.Label, &e.Provenance); err != nil {
 					rows.Close()
 					return nil, fmt.Errorf("扫描边失败: %w", err)
 				}

--
Gitblit v1.10.0