feat(index): nodes 表增加 node_type 列,查询过滤虚拟节点
| | |
| | | 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, |
| | |
| | | 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 |
| | | } |
| | | |
| | |
| | | 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 |
| | |
| | | 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(` |
| | |
| | | |
| | | // 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) |
| | | } |
| | |
| | | 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) |
| | |
| | | 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) |
| | |
| | | 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 |
| | |
| | | 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 |
| | |
| | | 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 |
| | | } |