From 6b58a8fd1d10191d5720b037af1137d2a747cfeb Mon Sep 17 00:00:00 2001
From: ai_xiaopei <xiaopei@aisim.cn>
Date: Sun, 26 Jul 2026 10:33:57 +0800
Subject: [PATCH] feat: add graph command for knowledge graph queries
---
internal/index/sqlite.go | 281 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 files changed, 281 insertions(+), 0 deletions(-)
diff --git a/internal/index/sqlite.go b/internal/index/sqlite.go
index db194fe..1501020 100644
--- a/internal/index/sqlite.go
+++ b/internal/index/sqlite.go
@@ -6,6 +6,7 @@
"fmt"
"os"
"path/filepath"
+ "sort"
"strings"
_ "github.com/mattn/go-sqlite3"
@@ -258,3 +259,283 @@
affected, _ := result.RowsAffected()
return int(affected), nil
}
+
+// GetRelationStats 获取关系类型分布统计
+func (s *Store) GetRelationStats() (map[string]int, error) {
+ rows, err := s.db.Query(`
+ SELECT relation, COUNT(*) as count
+ FROM edges
+ GROUP BY relation
+ ORDER BY count DESC
+ `)
+ if err != nil {
+ return nil, fmt.Errorf("查询关系统计失败: %w", err)
+ }
+ defer rows.Close()
+
+ stats := make(map[string]int)
+ for rows.Next() {
+ var relation string
+ var count int
+ if err := rows.Scan(&relation, &count); err != nil {
+ return nil, fmt.Errorf("扫描关系统计失败: %w", err)
+ }
+ stats[relation] = count
+ }
+
+ if err := rows.Err(); err != nil {
+ return nil, fmt.Errorf("遍历关系统计失败: %w", err)
+ }
+
+ return stats, nil
+}
+
+// FindNodesByKeyword 根据关键词查找节点(模糊匹配标题、路径、标签、实体)
+func (s *Store) FindNodesByKeyword(keyword string) ([]*graph.Node, error) {
+ keyword = "%" + keyword + "%"
+ 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 ?
+ LIMIT 20
+ `
+ rows, err := s.db.Query(query, keyword, keyword, keyword, keyword)
+ if err != nil {
+ return nil, fmt.Errorf("查询节点失败: %w", err)
+ }
+ defer rows.Close()
+
+ var nodes []*graph.Node
+ for rows.Next() {
+ var n graph.Node
+ var tagsJSON, entitiesJSON, wikilinksJSON string
+ var content sql.NullString
+
+ err := rows.Scan(&n.ID, &n.Path, &n.Title, &n.Section,
+ &tagsJSON, &entitiesJSON, &wikilinksJSON, &content)
+ if err != nil {
+ return nil, fmt.Errorf("扫描节点失败: %w", err)
+ }
+
+ json.Unmarshal([]byte(tagsJSON), &n.Tags)
+ json.Unmarshal([]byte(entitiesJSON), &n.Entities)
+ json.Unmarshal([]byte(wikilinksJSON), &n.Wikilinks)
+ if content.Valid {
+ n.Content = content.String
+ }
+
+ nodes = append(nodes, &n)
+ }
+
+ if err := rows.Err(); err != nil {
+ return nil, fmt.Errorf("遍历节点失败: %w", err)
+ }
+
+ return nodes, nil
+}
+
+// GetNodeEdges 获取节点的关联边(支持关系类型过滤和深度查询)
+func (s *Store) GetNodeEdges(nodeID int64, relation string, depth int) ([]*graph.Edge, error) {
+ if depth < 1 {
+ depth = 1
+ }
+ if depth > 3 {
+ depth = 3
+ }
+
+ var edges []*graph.Edge
+ visited := make(map[int64]bool)
+ currentLevel := []int64{nodeID}
+
+ for d := 0; d < depth; d++ {
+ var nextLevel []int64
+
+ for _, id := range currentLevel {
+ if visited[id] && d > 0 {
+ continue
+ }
+ visited[id] = true
+
+ query := `
+ SELECT id, from_node, to_node, relation, label
+ FROM edges
+ WHERE from_node = ?
+ `
+ args := []interface{}{id}
+
+ if relation != "" {
+ query += " AND relation = ?"
+ args = append(args, relation)
+ }
+
+ rows, err := s.db.Query(query, args...)
+ if err != nil {
+ return nil, fmt.Errorf("查询边失败: %w", err)
+ }
+
+ 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 {
+ rows.Close()
+ return nil, fmt.Errorf("扫描边失败: %w", err)
+ }
+ edges = append(edges, &e)
+ nextLevel = append(nextLevel, e.ToNode)
+ }
+ rows.Close()
+ }
+
+ currentLevel = nextLevel
+ }
+
+ return edges, nil
+}
+
+// FindRelatedNodes 查找与关键词相关的所有节点(通过共同标签、实体、wikilink)
+func (s *Store) FindRelatedNodes(keyword string, topN int) ([]RelatedNode, error) {
+ // 先找到匹配关键词的节点
+ keyword = "%" + keyword + "%"
+ query := `
+ SELECT id, path, title, section, tags, entities
+ FROM nodes
+ WHERE title LIKE ? OR path LIKE ? OR tags LIKE ? OR entities LIKE ?
+ LIMIT 5
+ `
+ rows, err := s.db.Query(query, keyword, keyword, keyword, keyword)
+ if err != nil {
+ return nil, fmt.Errorf("查询节点失败: %w", err)
+ }
+ defer rows.Close()
+
+ var seedNodes []*graph.Node
+ for rows.Next() {
+ var n graph.Node
+ var tagsJSON, entitiesJSON string
+ err := rows.Scan(&n.ID, &n.Path, &n.Title, &n.Section, &tagsJSON, &entitiesJSON)
+ if err != nil {
+ return nil, fmt.Errorf("扫描节点失败: %w", err)
+ }
+ json.Unmarshal([]byte(tagsJSON), &n.Tags)
+ json.Unmarshal([]byte(entitiesJSON), &n.Entities)
+ seedNodes = append(seedNodes, &n)
+ }
+
+ if len(seedNodes) == 0 {
+ return []RelatedNode{}, nil
+ }
+
+ // 收集种子节点的标签和实体
+ tagSet := make(map[string]bool)
+ entitySet := make(map[string]bool)
+ for _, n := range seedNodes {
+ for _, tag := range n.Tags {
+ tagSet[tag] = true
+ }
+ for _, entity := range n.Entities {
+ entitySet[entity] = true
+ }
+ }
+
+ // 查找共享标签或实体的节点
+ relevanceMap := make(map[int64]int)
+ pathMap := make(map[int64]string)
+ titleMap := make(map[int64]string)
+ sectionMap := make(map[int64]string)
+ tagsMap := make(map[int64][]string)
+
+ // 通过标签查找
+ for tag := range tagSet {
+ tagPattern := "%" + tag + "%"
+ rows, err := s.db.Query(`
+ SELECT id, path, title, section, tags
+ FROM nodes
+ WHERE tags LIKE ?
+ `, tagPattern)
+ if err != nil {
+ continue
+ }
+
+ for rows.Next() {
+ var id int64
+ var path, title, section, tagsJSON string
+ if err := rows.Scan(&id, &path, &title, §ion, &tagsJSON); err != nil {
+ rows.Close()
+ continue
+ }
+ relevanceMap[id]++
+ pathMap[id] = path
+ titleMap[id] = title
+ sectionMap[id] = section
+ var tags []string
+ json.Unmarshal([]byte(tagsJSON), &tags)
+ tagsMap[id] = tags
+ }
+ rows.Close()
+ }
+
+ // 通过实体查找
+ for entity := range entitySet {
+ entityPattern := "%" + entity + "%"
+ rows, err := s.db.Query(`
+ SELECT id, path, title, section, tags
+ FROM nodes
+ WHERE entities LIKE ?
+ `, entityPattern)
+ if err != nil {
+ continue
+ }
+
+ for rows.Next() {
+ var id int64
+ var path, title, section, tagsJSON string
+ if err := rows.Scan(&id, &path, &title, §ion, &tagsJSON); err != nil {
+ rows.Close()
+ continue
+ }
+ relevanceMap[id]++
+ pathMap[id] = path
+ titleMap[id] = title
+ sectionMap[id] = section
+ var tags []string
+ json.Unmarshal([]byte(tagsJSON), &tags)
+ tagsMap[id] = tags
+ }
+ rows.Close()
+ }
+
+ // 转换为结果列表
+ var results []RelatedNode
+ for id, relevance := range relevanceMap {
+ results = append(results, RelatedNode{
+ ID: id,
+ Path: pathMap[id],
+ Title: titleMap[id],
+ Section: sectionMap[id],
+ Tags: tagsMap[id],
+ Relevance: relevance,
+ })
+ }
+
+ // 按相关度排序
+ sort.Slice(results, func(i, j int) bool {
+ return results[i].Relevance > results[j].Relevance
+ })
+
+ // 限制返回数量
+ if topN > 0 && len(results) > topN {
+ results = results[:topN]
+ }
+
+ return results, nil
+}
+
+// RelatedNode 相关节点
+type RelatedNode struct {
+ ID int64 `json:"id"`
+ Path string `json:"path"`
+ Title string `json:"title"`
+ Section string `json:"section"`
+ Tags []string `json:"tags"`
+ Relevance int `json:"relevance"`
+}
--
Gitblit v1.9.1