From 258e5d21f926ae97821e5ab3d525b66e0ae04270 Mon Sep 17 00:00:00 2001 From: wonder Date: Mon, 13 Apr 2026 15:21:54 +0800 Subject: [PATCH] =?UTF-8?q?Feat:=20=E5=AE=9E=E7=8E=B0=E7=9F=A5=E8=AF=86?= =?UTF-8?q?=E5=9B=BE=E8=B0=B1=E6=95=B0=E6=8D=AE=E5=BA=93=E7=9A=84=E6=A3=80?= =?UTF-8?q?=E7=B4=A2=E6=96=B9=E6=B3=95=EF=BC=8C=E5=B9=B6=E5=AF=B9=E5=A4=96?= =?UTF-8?q?=E6=8F=90=E4=BE=9B=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/config/config.go | 5 +- backend/data.json | 255 ------------------------ backend/handlers/graph_handler.go | 10 +- backend/handlers/node_handler.go | 10 +- backend/handlers/search_handler.go | 8 +- backend/main.go | 16 +- backend/services/neo4j_service.go | 301 +++++++++++++++++++++++++++++ backend/services/service.go | 11 ++ 8 files changed, 339 insertions(+), 277 deletions(-) delete mode 100644 backend/data.json create mode 100644 backend/services/neo4j_service.go create mode 100644 backend/services/service.go diff --git a/backend/config/config.go b/backend/config/config.go index ef3d70a..5c07570 100644 --- a/backend/config/config.go +++ b/backend/config/config.go @@ -40,7 +40,10 @@ type Config struct { } func Load() (*Config, error) { - if err := godotenv.Load("../config/.env"); err != nil { + var err error + err = godotenv.Load("../config/.env") + err = godotenv.Load("./config/.env") + if err != nil { return nil, fmt.Errorf("error loading .env file: %w", err) } diff --git a/backend/data.json b/backend/data.json deleted file mode 100644 index 13c2606..0000000 --- a/backend/data.json +++ /dev/null @@ -1,255 +0,0 @@ -{ - "nodes": [ - { - "id": "1", - "label": "人工智能", - "type": "概念", - "properties": { - "importance": 1, - "description": "计算机科学的一个分支,致力于创建能够执行通常需要人类智能的任务的机器", - "aliases": ["机器智能", "AI"] - } - }, - { - "id": "2", - "label": "机器学习", - "type": "概念", - "properties": { - "importance": 0.9, - "description": "人工智能的一个子领域,让计算机系统能够从数据中学习和改进" - } - }, - { - "id": "3", - "label": "深度学习", - "type": "概念", - "properties": { - "importance": 0.8, - "description": "基于人工神经网络的机器学习方法" - } - }, - { - "id": "4", - "label": "神经网络", - "type": "概念", - "properties": { - "importance": 0.7, - "description": "受人脑启发的计算模型" - } - }, - { - "id": "5", - "label": "TensorFlow", - "type": "工具", - "properties": { - "importance": 0.6, - "language": "Python, C++", - "author": "Google" - } - }, - { - "id": "6", - "label": "PyTorch", - "type": "工具", - "properties": { - "importance": 0.6, - "language": "Python", - "author": "Meta" - } - }, - { - "id": "7", - "label": "自然语言处理", - "type": "应用", - "properties": { - "importance": 0.7, - "description": "处理和理解人类语言的技术" - } - }, - { - "id": "8", - "label": "计算机视觉", - "type": "应用", - "properties": { - "importance": 0.7, - "description": "让计算机理解和解析视觉信息" - } - }, - { - "id": "9", - "label": "强化学习", - "type": "概念", - "properties": { - "importance": 0.6, - "description": "通过与环境交互来学习最优策略" - } - }, - { - "id": "10", - "label": "监督学习", - "type": "概念", - "properties": { - "importance": 0.5, - "description": "使用标记数据进行学习" - } - }, - { - "id": "11", - "label": "GPT", - "type": "应用", - "properties": { - "importance": 0.8, - "author": "OpenAI", - "description": "Generative Pre-trained Transformer" - } - }, - { - "id": "12", - "label": "CNN", - "type": "概念", - "properties": { - "importance": 0.6, - "description": "卷积神经网络,用于处理网格状数据" - } - }, - { - "id": "13", - "label": "RNN", - "type": "概念", - "properties": { - "importance": 0.6, - "description": "循环神经网络,用于处理序列数据" - } - }, - { - "id": "14", - "label": "数据挖掘", - "type": "应用", - "properties": { - "importance": 0.5, - "description": "从大量数据中发现模式和知识" - } - } - ], - "edges": [ - { - "id": "e1", - "source": "1", - "target": "2", - "label": "包含", - "type": "关系" - }, - { - "id": "e2", - "source": "2", - "target": "3", - "label": "子领域", - "type": "关系" - }, - { - "id": "e3", - "source": "3", - "target": "4", - "label": "基于", - "type": "技术依赖" - }, - { - "id": "e4", - "source": "3", - "target": "5", - "label": "框架", - "type": "工具关系" - }, - { - "id": "e5", - "source": "3", - "target": "6", - "label": "框架", - "type": "工具关系" - }, - { - "id": "e6", - "source": "1", - "target": "7", - "label": "应用", - "type": "应用领域" - }, - { - "id": "e7", - "source": "1", - "target": "8", - "label": "应用", - "type": "应用领域" - }, - { - "id": "e8", - "source": "2", - "target": "9", - "label": "包含", - "type": "关系" - }, - { - "id": "e9", - "source": "2", - "target": "10", - "label": "包含", - "type": "关系" - }, - { - "id": "e10", - "source": "7", - "target": "11", - "label": "技术实现", - "type": "技术依赖" - }, - { - "id": "e11", - "source": "3", - "target": "12", - "label": "主要架构", - "type": "技术依赖" - }, - { - "id": "e12", - "source": "3", - "target": "13", - "label": "主要架构", - "type": "技术依赖" - }, - { - "id": "e13", - "source": "8", - "target": "12", - "label": "核心技术", - "type": "技术依赖" - }, - { - "id": "e14", - "source": "7", - "target": "13", - "label": "核心技术", - "type": "技术依赖" - }, - { - "id": "e15", - "source": "2", - "target": "14", - "label": "相关技术", - "type": "相关" - }, - { - "id": "e16", - "source": "5", - "target": "12", - "label": "支持", - "type": "技术支持" - }, - { - "id": "e17", - "source": "6", - "target": "13", - "label": "支持", - "type": "技术支持" - } - ] -} \ No newline at end of file diff --git a/backend/handlers/graph_handler.go b/backend/handlers/graph_handler.go index 2718f7a..aecf476 100644 --- a/backend/handlers/graph_handler.go +++ b/backend/handlers/graph_handler.go @@ -8,22 +8,22 @@ import ( ) type GraphHandler struct { - dataService *services.DataService + service services.GraphService } -func NewGraphHandler(dataService *services.DataService) *GraphHandler { +func NewGraphHandler(service services.GraphService) *GraphHandler { return &GraphHandler{ - dataService: dataService, + service: service, } } func (h *GraphHandler) GetGraphData(c *gin.Context) { - data := h.dataService.GetGraphData() + data := h.service.GetGraphData() c.JSON(http.StatusOK, data) } func (h *GraphHandler) GetStats(c *gin.Context) { - stats := h.dataService.GetStats() + stats := h.service.GetStats() c.JSON(http.StatusOK, gin.H{ "stats": stats, "timestamp": gin.H{ diff --git a/backend/handlers/node_handler.go b/backend/handlers/node_handler.go index feb00e3..9d018c5 100644 --- a/backend/handlers/node_handler.go +++ b/backend/handlers/node_handler.go @@ -8,12 +8,12 @@ import ( ) type NodeHandler struct { - dataService *services.DataService + service services.GraphService } -func NewNodeHandler(dataService *services.DataService) *NodeHandler { +func NewNodeHandler(service services.GraphService) *NodeHandler { return &NodeHandler{ - dataService: dataService, + service: service, } } @@ -27,7 +27,7 @@ func (h *NodeHandler) GetNodeByID(c *gin.Context) { return } - node, found := h.dataService.GetNodeByID(nodeID) + node, found := h.service.GetNodeByID(nodeID) if !found { c.JSON(http.StatusNotFound, gin.H{ "error": "Not Found", @@ -49,7 +49,7 @@ func (h *NodeHandler) GetNeighbors(c *gin.Context) { return } - neighbors, found := h.dataService.GetNeighbors(nodeID) + neighbors, found := h.service.GetNeighbors(nodeID) if !found { c.JSON(http.StatusNotFound, gin.H{ "error": "Not Found", diff --git a/backend/handlers/search_handler.go b/backend/handlers/search_handler.go index b344728..debe3d9 100644 --- a/backend/handlers/search_handler.go +++ b/backend/handlers/search_handler.go @@ -8,12 +8,12 @@ import ( ) type SearchHandler struct { - dataService *services.DataService + service services.GraphService } -func NewSearchHandler(dataService *services.DataService) *SearchHandler { +func NewSearchHandler(service services.GraphService) *SearchHandler { return &SearchHandler{ - dataService: dataService, + service: service, } } @@ -27,7 +27,7 @@ func (h *SearchHandler) SearchNodes(c *gin.Context) { return } - results := h.dataService.SearchNodes(query) + results := h.service.SearchNodes(query) c.JSON(http.StatusOK, results) } diff --git a/backend/main.go b/backend/main.go index 80b827c..c1a9a86 100644 --- a/backend/main.go +++ b/backend/main.go @@ -1,6 +1,7 @@ package main import ( + "context" "log" "net/http" @@ -9,6 +10,7 @@ import ( "knowledge-graph-backend/config" "knowledge-graph-backend/handlers" + neo4jClient "knowledge-graph-backend/neo4j" "knowledge-graph-backend/services" ) @@ -18,14 +20,14 @@ func main() { log.Fatalf("Failed to load config: %v", err) } - dataService, err := services.NewDataService(cfg.Data.FilePath) - if err != nil { - log.Fatalf("Failed to initialize data service: %v", err) - } + driver := neo4jClient.NewDriver(cfg.Neo4j) + defer driver.Close(context.Background()) - graphHandler := handlers.NewGraphHandler(dataService) - searchHandler := handlers.NewSearchHandler(dataService) - nodeHandler := handlers.NewNodeHandler(dataService) + graphService := services.NewNeo4jService(driver) + + graphHandler := handlers.NewGraphHandler(graphService) + searchHandler := handlers.NewSearchHandler(graphService) + nodeHandler := handlers.NewNodeHandler(graphService) router := gin.Default() diff --git a/backend/services/neo4j_service.go b/backend/services/neo4j_service.go new file mode 100644 index 0000000..8d173c3 --- /dev/null +++ b/backend/services/neo4j_service.go @@ -0,0 +1,301 @@ +package services + +import ( + "context" + "fmt" + + "github.com/neo4j/neo4j-go-driver/v5/neo4j" + + "knowledge-graph-backend/models" +) + +type Neo4jService struct { + driver neo4j.DriverWithContext +} + +func NewNeo4jService(driver neo4j.DriverWithContext) *Neo4jService { + return &Neo4jService{driver: driver} +} + +func (s *Neo4jService) GetGraphData() models.GraphData { + ctx := context.Background() + var nodes []models.Node + var edges []models.Edge + + nodeResult, err := neo4j.ExecuteQuery(ctx, s.driver, + "MATCH (n) RETURN n", + map[string]any{}, + neo4j.EagerResultTransformer, + ) + if err != nil { + fmt.Printf("Error querying nodes: %v\n", err) + return models.GraphData{} + } + for _, record := range nodeResult.Records { + if v, ok := record.Get("n"); ok { + if n, ok := v.(neo4j.Node); ok { + nodes = append(nodes, neo4jNodeToModel(n)) + } + } + } + + edgeResult, err := neo4j.ExecuteQuery(ctx, s.driver, + "MATCH (a)-[r]->(b) RETURN a.id AS source, b.id AS target, r", + map[string]any{}, + neo4j.EagerResultTransformer, + ) + if err != nil { + fmt.Printf("Error querying edges: %v\n", err) + return models.GraphData{Nodes: nodes} + } + for _, record := range edgeResult.Records { + source, _ := record.Get("source") + target, _ := record.Get("target") + v, _ := record.Get("r") + if rel, ok := v.(neo4j.Relationship); ok { + edge := neo4jRelToModel(rel) + if s, ok := source.(string); ok { + edge.Source = s + } + if t, ok := target.(string); ok { + edge.Target = t + } + edges = append(edges, edge) + } + } + + return models.GraphData{Nodes: nodes, Edges: edges} +} + +func (s *Neo4jService) GetNodeByID(id string) (models.Node, bool) { + ctx := context.Background() + result, err := neo4j.ExecuteQuery(ctx, s.driver, + "MATCH (n {id: $id}) RETURN n", + map[string]any{"id": id}, + neo4j.EagerResultTransformer, + ) + if err != nil || len(result.Records) == 0 { + return models.Node{}, false + } + v, _ := result.Records[0].Get("n") + if n, ok := v.(neo4j.Node); ok { + return neo4jNodeToModel(n), true + } + return models.Node{}, false +} + +func (s *Neo4jService) SearchNodes(query string) []models.Node { + if query == "" { + return []models.Node{} + } + ctx := context.Background() + result, err := neo4j.ExecuteQuery(ctx, s.driver, + `MATCH (n) + WHERE toLower(n.label) CONTAINS toLower($query) + OR toLower(n.id) CONTAINS toLower($query) + OR (n.type IS NOT NULL AND toLower(n.type) CONTAINS toLower($query)) + RETURN n`, + map[string]any{"query": query}, + neo4j.EagerResultTransformer, + ) + if err != nil { + return []models.Node{} + } + var nodes []models.Node + for _, record := range result.Records { + v, _ := record.Get("n") + if n, ok := v.(neo4j.Node); ok { + nodes = append(nodes, neo4jNodeToModel(n)) + } + } + return nodes +} + +func (s *Neo4jService) GetNeighbors(nodeID string) (models.NeighborResponse, bool) { + ctx := context.Background() + + _, exists := s.GetNodeByID(nodeID) + if !exists { + return models.NeighborResponse{}, false + } + + result, err := neo4j.ExecuteQuery(ctx, s.driver, + `MATCH ({id: $id})-[r]-(m) + RETURN m, r, startNode(r).id AS source, endNode(r).id AS target`, + map[string]any{"id": nodeID}, + neo4j.EagerResultTransformer, + ) + if err != nil { + return models.NeighborResponse{Nodes: []models.Node{}, Edges: []models.Edge{}}, true + } + + neighborMap := make(map[string]models.Node) + edgeMap := make(map[string]models.Edge) + + for _, record := range result.Records { + if v, ok := record.Get("m"); ok { + if n, ok := v.(neo4j.Node); ok { + modelNode := neo4jNodeToModel(n) + neighborMap[modelNode.ID] = modelNode + } + } + + if v, ok := record.Get("r"); ok { + if rel, ok := v.(neo4j.Relationship); ok { + edge := neo4jRelToModel(rel) + if source, ok := record.Get("source"); ok { + if s, ok := source.(string); ok { + edge.Source = s + } + } + if target, ok := record.Get("target"); ok { + if t, ok := target.(string); ok { + edge.Target = t + } + } + edgeMap[edge.ID] = edge + } + } + } + + var neighborNodes []models.Node + for _, n := range neighborMap { + neighborNodes = append(neighborNodes, n) + } + + var relatedEdges []models.Edge + for _, e := range edgeMap { + relatedEdges = append(relatedEdges, e) + } + + return models.NeighborResponse{ + Nodes: neighborNodes, + Edges: relatedEdges, + }, true +} + +func (s *Neo4jService) GetStats() map[string]int { + ctx := context.Background() + result, err := neo4j.ExecuteQuery(ctx, s.driver, + `MATCH (n) + RETURN count(n) AS totalNodes, + sum(CASE WHEN n.type = '概念' THEN 1 ELSE 0 END) AS conceptNodes, + sum(CASE WHEN n.type = '工具' THEN 1 ELSE 0 END) AS toolNodes, + sum(CASE WHEN n.type = '应用' THEN 1 ELSE 0 END) AS applicationNodes`, + map[string]any{}, + neo4j.EagerResultTransformer, + ) + if err != nil || len(result.Records) == 0 { + return map[string]int{} + } + + record := result.Records[0] + totalNodes := getInt(record, "totalNodes") + conceptNodes := getInt(record, "conceptNodes") + toolNodes := getInt(record, "toolNodes") + applicationNodes := getInt(record, "applicationNodes") + + totalEdges := 0 + edgeResult, err := neo4j.ExecuteQuery(ctx, s.driver, + "MATCH ()-[r]->() RETURN count(r) AS totalEdges", + map[string]any{}, + neo4j.EagerResultTransformer, + ) + if err == nil && len(edgeResult.Records) > 0 { + totalEdges = getInt(edgeResult.Records[0], "totalEdges") + } + + return map[string]int{ + "totalNodes": totalNodes, + "totalEdges": totalEdges, + "conceptNodes": conceptNodes, + "toolNodes": toolNodes, + "applicationNodes": applicationNodes, + } +} + +func neo4jNodeToModel(n neo4j.Node) models.Node { + props := n.Props + node := models.Node{ + ID: getStr(props, "id"), + Label: getStr(props, "label"), + Type: getStr(props, "type"), + Properties: make(map[string]interface{}), + } + if v, ok := props["x"]; ok && v != nil { + node.X = getFloat64(v) + } + if v, ok := props["y"]; ok && v != nil { + node.Y = getFloat64(v) + } + if v, ok := props["style"]; ok && v != nil { + if s, ok := v.(map[string]interface{}); ok { + node.Style = s + } + } + for k, v := range props { + switch k { + case "id", "label", "type", "x", "y", "style": + default: + node.Properties[k] = v + } + } + return node +} + +func neo4jRelToModel(r neo4j.Relationship) models.Edge { + props := r.Props + edge := models.Edge{ + ID: getStr(props, "id"), + Label: getStr(props, "label"), + Type: r.Type, + Properties: make(map[string]interface{}), + } + if v, ok := props["style"]; ok && v != nil { + if s, ok := v.(map[string]interface{}); ok { + edge.Style = s + } + } + for k, v := range props { + switch k { + case "id", "label", "style": + default: + edge.Properties[k] = v + } + } + return edge +} + +func getStr(props map[string]any, key string) string { + if v, ok := props[key]; ok && v != nil { + if s, ok := v.(string); ok { + return s + } + } + return "" +} + +func getFloat64(v any) float64 { + switch val := v.(type) { + case float64: + return val + case int64: + return float64(val) + case int: + return float64(val) + } + return 0 +} + +func getInt(record *neo4j.Record, key string) int { + v, _ := record.Get(key) + switch val := v.(type) { + case int64: + return int(val) + case int: + return val + case float64: + return int(val) + } + return 0 +} diff --git a/backend/services/service.go b/backend/services/service.go new file mode 100644 index 0000000..60513be --- /dev/null +++ b/backend/services/service.go @@ -0,0 +1,11 @@ +package services + +import "knowledge-graph-backend/models" + +type GraphService interface { + GetGraphData() models.GraphData + GetNodeByID(id string) (models.Node, bool) + SearchNodes(query string) []models.Node + GetNeighbors(nodeID string) (models.NeighborResponse, bool) + GetStats() map[string]int +}