ai_xiaopei
2026-08-21 7bcbed8c3547e55287972bd0bccc7c9e9d8ec495
feat(index): nodes 表增加 node_type 列,查询过滤虚拟节点
2 files modified
100 ■■■■■ changed files
internal/index/sqlite.go 36 ●●●● patch | view | raw | blame | history
internal/index/sqlite_test.go 64 ●●●●● patch | view | raw | blame | history
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
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
}