From 7bcbed8c3547e55287972bd0bccc7c9e9d8ec495 Mon Sep 17 00:00:00 2001
From: ai_xiaopei <xiaopei@aisim.cn>
Date: Fri, 21 Aug 2026 19:14:11 +0800
Subject: [PATCH] feat(index): nodes 表增加 node_type 列,查询过滤虚拟节点

---
 internal/index/sqlite_test.go |   64 ++++++++++++++++++++++++++++++++
 internal/index/sqlite.go      |   36 ++++++++++++++----
 2 files changed, 92 insertions(+), 8 deletions(-)

diff --git a/internal/index/sqlite.go b/internal/index/sqlite.go
index 1501020..bf2477a 100644
--- a/internal/index/sqlite.go
+++ b/internal/index/sqlite.go
@@ -55,6 +55,7 @@
 	CREATE TABLE IF NOT EXISTS nodes (
 		id          INTEGER PRIMARY KEY AUTOINCREMENT,
 		path        TEXT NOT NULL UNIQUE,
+		node_type   TEXT NOT NULL DEFAULT 'file',
 		title       TEXT,
 		section     TEXT,
 		tags        TEXT,
@@ -83,6 +84,15 @@
 	if err != nil {
 		return fmt.Errorf("创建表失败: %w", err)
 	}
+
+	// 旧库迁移:补 node_type 列(幂等)
+	var colCount int
+	if err := s.db.QueryRow("SELECT COUNT(*) FROM pragma_table_info('nodes') WHERE name = 'node_type'").Scan(&colCount); err == nil && colCount == 0 {
+		if _, err := s.db.Exec("ALTER TABLE nodes ADD COLUMN node_type TEXT NOT NULL DEFAULT 'file'"); err != nil {
+			return fmt.Errorf("迁移 node_type 列失败: %w", err)
+		}
+	}
+
 	return nil
 }
 
@@ -99,9 +109,9 @@
 	wikilinksJSON, _ := json.Marshal(n.Wikilinks)
 
 	result, err := s.db.Exec(`
-		INSERT INTO nodes (path, title, section, tags, entities, wikilinks, content_fts)
-		VALUES (?, ?, ?, ?, ?, ?, ?)
-	`, n.Path, n.Title, n.Section, string(tagsJSON), string(entitiesJSON),
+		INSERT INTO nodes (path, node_type, title, section, tags, entities, wikilinks, content_fts)
+		VALUES (?, ?, ?, ?, ?, ?, ?, ?)
+	`, n.Path, nodeTypeOf(n), n.Title, n.Section, string(tagsJSON), string(entitiesJSON),
 		string(wikilinksJSON), n.Content)
 	if err != nil {
 		return 0, err
@@ -109,6 +119,14 @@
 	return result.LastInsertId()
 }
 
+// nodeTypeOf 节点类型(空值归一为 file)
+func nodeTypeOf(n *graph.Node) string {
+	if n.NodeType == "" {
+		return "file"
+	}
+	return n.NodeType
+}
+
 // InsertEdge 插入边
 func (s *Store) InsertEdge(e *graph.Edge) error {
 	_, err := s.db.Exec(`
@@ -188,7 +206,7 @@
 
 // GetAllNodes 获取所有节点(用于 GC 检查)
 func (s *Store) GetAllNodes() ([]NodeInfo, error) {
-	rows, err := s.db.Query("SELECT id, path FROM nodes")
+	rows, err := s.db.Query("SELECT id, path FROM nodes WHERE node_type = 'file'")
 	if err != nil {
 		return nil, fmt.Errorf("查询节点失败: %w", err)
 	}
@@ -296,7 +314,8 @@
 	query := `
 		SELECT id, path, title, section, tags, entities, wikilinks, content_fts
 		FROM nodes
-		WHERE title LIKE ? OR path LIKE ? OR tags LIKE ? OR entities LIKE ?
+		WHERE (title LIKE ? OR path LIKE ? OR tags LIKE ? OR entities LIKE ?)
+		AND node_type = 'file'
 		LIMIT 20
 	`
 	rows, err := s.db.Query(query, keyword, keyword, keyword, keyword)
@@ -399,7 +418,8 @@
 	query := `
 		SELECT id, path, title, section, tags, entities
 		FROM nodes
-		WHERE title LIKE ? OR path LIKE ? OR tags LIKE ? OR entities LIKE ?
+		WHERE (title LIKE ? OR path LIKE ? OR tags LIKE ? OR entities LIKE ?)
+		AND node_type = 'file'
 		LIMIT 5
 	`
 	rows, err := s.db.Query(query, keyword, keyword, keyword, keyword)
@@ -450,7 +470,7 @@
 		rows, err := s.db.Query(`
 			SELECT id, path, title, section, tags
 			FROM nodes
-			WHERE tags LIKE ?
+			WHERE tags LIKE ? AND node_type = 'file'
 		`, tagPattern)
 		if err != nil {
 			continue
@@ -480,7 +500,7 @@
 		rows, err := s.db.Query(`
 			SELECT id, path, title, section, tags
 			FROM nodes
-			WHERE entities LIKE ?
+			WHERE entities LIKE ? AND node_type = 'file'
 		`, entityPattern)
 		if err != nil {
 			continue
diff --git a/internal/index/sqlite_test.go b/internal/index/sqlite_test.go
index 5ff1d40..e0430ee 100644
--- a/internal/index/sqlite_test.go
+++ b/internal/index/sqlite_test.go
@@ -131,3 +131,67 @@
 		t.Errorf("links count = %d, want 0", len(links))
 	}
 }
+
+func TestNodeTypeSeparation(t *testing.T) {
+	tmpDir := t.TempDir()
+	store, err := Open(filepath.Join(tmpDir, "test.db"))
+	if err != nil {
+		t.Fatalf("Open failed: %v", err)
+	}
+	defer store.Close()
+
+	// 插入文件节点 + tag 节点
+	fileID, err := store.InsertNode(&graph.Node{
+		Path: "FAQ/001-测试.md", Title: "测试文档", Section: "FAQ", NodeType: "file",
+		Tags: []string{"电磁阀"},
+	})
+	if err != nil {
+		t.Fatalf("InsertNode file failed: %v", err)
+	}
+	tagID, err := store.InsertNode(&graph.Node{
+		Path: "tag:电磁阀", Title: "电磁阀", NodeType: "tag",
+	})
+	if err != nil {
+		t.Fatalf("InsertNode tag failed: %v", err)
+	}
+
+	// tag 节点不进 FTS
+	if err := store.CreateFTS(); err != nil {
+		t.Fatalf("CreateFTS failed: %v", err)
+	}
+	if err := store.PopulateFTS(); err != nil {
+		t.Fatalf("PopulateFTS failed: %v", err)
+	}
+	res, err := store.FTSSearch([]string{"测试"}, 10)
+	if err != nil {
+		t.Fatalf("FTSSearch failed: %v", err)
+	}
+	for _, r := range res {
+		if r.ID == tagID {
+			t.Error("FTS result contains tag node")
+		}
+	}
+
+	// FindNodesByKeyword 不返回 tag 节点
+	nodes, err := store.FindNodesByKeyword("电磁阀")
+	if err != nil {
+		t.Fatalf("FindNodesByKeyword failed: %v", err)
+	}
+	for _, n := range nodes {
+		if n.ID == tagID {
+			t.Error("FindNodesByKeyword returned tag node")
+		}
+	}
+
+	// GetAllNodes 只返回 file 节点
+	all, err := store.GetAllNodes()
+	if err != nil {
+		t.Fatalf("GetAllNodes failed: %v", err)
+	}
+	for _, n := range all {
+		if n.ID == tagID {
+			t.Error("GetAllNodes returned tag node")
+		}
+	}
+	_ = fileID
+}

--
Gitblit v1.10.0