Compare commits

...
3 Commits
10 changed files with 470 additions and 334 deletions
+91
View File
@@ -0,0 +1,91 @@
package config
import (
"fmt"
"os"
"time"
"github.com/gin-contrib/cors"
"github.com/joho/godotenv"
)
type ServerConfig struct {
Port string
}
type Neo4jConfig struct {
URI string
Username string
Password string
}
type DataConfig struct {
FilePath string
}
type CORSConfig struct {
AllowOrigins []string
AllowMethods []string
AllowHeaders []string
ExposeHeaders []string
AllowCredentials bool
MaxAge time.Duration
}
type Config struct {
Server ServerConfig
Neo4j Neo4jConfig
Data DataConfig
CORS CORSConfig
}
func Load() (*Config, error) {
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)
}
cfg := &Config{
Server: ServerConfig{
Port: getEnv("PORT", "3001"),
},
Neo4j: Neo4jConfig{
URI: os.Getenv("NEO4J_URI"),
Username: os.Getenv("NEO4J_USERNAME"),
Password: os.Getenv("NEO4J_PASSWORD"),
},
Data: DataConfig{
FilePath: getEnv("DATA_FILE", "data.json"),
},
CORS: CORSConfig{
AllowOrigins: []string{"http://localhost:5173", "http://localhost:3000"},
AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
AllowHeaders: []string{"Origin", "Content-Type", "Authorization", "Accept"},
ExposeHeaders: []string{"Content-Length", "Content-Type"},
AllowCredentials: true,
MaxAge: 12 * time.Hour,
},
}
return cfg, nil
}
func (c *CORSConfig) ToGinConfig() cors.Config {
return cors.Config{
AllowOrigins: c.AllowOrigins,
AllowMethods: c.AllowMethods,
AllowHeaders: c.AllowHeaders,
ExposeHeaders: c.ExposeHeaders,
AllowCredentials: c.AllowCredentials,
MaxAge: c.MaxAge,
}
}
func getEnv(key, defaultValue string) string {
if value := os.Getenv(key); value != "" {
return value
}
return defaultValue
}
-255
View File
@@ -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": "技术支持"
}
]
}
+5 -5
View File
@@ -8,22 +8,22 @@ import (
) )
type GraphHandler struct { type GraphHandler struct {
dataService *services.DataService service services.GraphService
} }
func NewGraphHandler(dataService *services.DataService) *GraphHandler { func NewGraphHandler(service services.GraphService) *GraphHandler {
return &GraphHandler{ return &GraphHandler{
dataService: dataService, service: service,
} }
} }
func (h *GraphHandler) GetGraphData(c *gin.Context) { func (h *GraphHandler) GetGraphData(c *gin.Context) {
data := h.dataService.GetGraphData() data := h.service.GetGraphData()
c.JSON(http.StatusOK, data) c.JSON(http.StatusOK, data)
} }
func (h *GraphHandler) GetStats(c *gin.Context) { func (h *GraphHandler) GetStats(c *gin.Context) {
stats := h.dataService.GetStats() stats := h.service.GetStats()
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
"stats": stats, "stats": stats,
"timestamp": gin.H{ "timestamp": gin.H{
+5 -5
View File
@@ -8,12 +8,12 @@ import (
) )
type NodeHandler struct { type NodeHandler struct {
dataService *services.DataService service services.GraphService
} }
func NewNodeHandler(dataService *services.DataService) *NodeHandler { func NewNodeHandler(service services.GraphService) *NodeHandler {
return &NodeHandler{ return &NodeHandler{
dataService: dataService, service: service,
} }
} }
@@ -27,7 +27,7 @@ func (h *NodeHandler) GetNodeByID(c *gin.Context) {
return return
} }
node, found := h.dataService.GetNodeByID(nodeID) node, found := h.service.GetNodeByID(nodeID)
if !found { if !found {
c.JSON(http.StatusNotFound, gin.H{ c.JSON(http.StatusNotFound, gin.H{
"error": "Not Found", "error": "Not Found",
@@ -49,7 +49,7 @@ func (h *NodeHandler) GetNeighbors(c *gin.Context) {
return return
} }
neighbors, found := h.dataService.GetNeighbors(nodeID) neighbors, found := h.service.GetNeighbors(nodeID)
if !found { if !found {
c.JSON(http.StatusNotFound, gin.H{ c.JSON(http.StatusNotFound, gin.H{
"error": "Not Found", "error": "Not Found",
+4 -4
View File
@@ -8,12 +8,12 @@ import (
) )
type SearchHandler struct { type SearchHandler struct {
dataService *services.DataService service services.GraphService
} }
func NewSearchHandler(dataService *services.DataService) *SearchHandler { func NewSearchHandler(service services.GraphService) *SearchHandler {
return &SearchHandler{ return &SearchHandler{
dataService: dataService, service: service,
} }
} }
@@ -27,7 +27,7 @@ func (h *SearchHandler) SearchNodes(c *gin.Context) {
return return
} }
results := h.dataService.SearchNodes(query) results := h.service.SearchNodes(query)
c.JSON(http.StatusOK, results) c.JSON(http.StatusOK, results)
} }
+19 -24
View File
@@ -1,42 +1,37 @@
package neo4j package main
import ( import (
"context"
"log" "log"
"net/http" "net/http"
"github.com/gin-contrib/cors" "github.com/gin-contrib/cors"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"knowledge-graph-backend/config"
"knowledge-graph-backend/handlers" "knowledge-graph-backend/handlers"
neo4jClient "knowledge-graph-backend/neo4j"
"knowledge-graph-backend/services" "knowledge-graph-backend/services"
) )
const ( func main() {
PORT = "3001" cfg, err := config.Load()
DATA_FILE = "data.json"
)
func test() {
dataService, err := services.NewDataService(DATA_FILE)
if err != nil { if err != nil {
log.Fatalf("Failed to initialize data service: %v", err) log.Fatalf("Failed to load config: %v", err)
} }
graphHandler := handlers.NewGraphHandler(dataService) driver := neo4jClient.NewDriver(cfg.Neo4j)
searchHandler := handlers.NewSearchHandler(dataService) defer driver.Close(context.Background())
nodeHandler := handlers.NewNodeHandler(dataService)
graphService := services.NewNeo4jService(driver)
graphHandler := handlers.NewGraphHandler(graphService)
searchHandler := handlers.NewSearchHandler(graphService)
nodeHandler := handlers.NewNodeHandler(graphService)
router := gin.Default() router := gin.Default()
router.Use(cors.New(cors.Config{ router.Use(cors.New(cfg.CORS.ToGinConfig()))
AllowOrigins: []string{"http://localhost:5173", "http://localhost:3000"},
AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
AllowHeaders: []string{"Origin", "Content-Type", "Authorization", "Accept"},
ExposeHeaders: []string{"Content-Length", "Content-Type"},
AllowCredentials: true,
MaxAge: 12 * 3600,
}))
router.GET("/health", func(c *gin.Context) { router.GET("/health", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
@@ -57,8 +52,8 @@ func test() {
api.GET("/nodes/:id/neighbors", nodeHandler.GetNeighbors) api.GET("/nodes/:id/neighbors", nodeHandler.GetNeighbors)
} }
log.Println("Knowledge Graph API Server running on http://localhost:" + PORT) log.Println("Knowledge Graph API Server running on http://localhost:" + cfg.Server.Port)
log.Println("Data file loaded from: " + DATA_FILE) log.Println("Data file loaded from: " + cfg.Data.FilePath)
log.Println("Available endpoints:") log.Println("Available endpoints:")
log.Println(" GET /health - Health check") log.Println(" GET /health - Health check")
log.Println(" GET /api/graph - Get all graph data") log.Println(" GET /api/graph - Get all graph data")
@@ -67,7 +62,7 @@ func test() {
log.Println(" GET /api/nodes/:id - Get node by ID") log.Println(" GET /api/nodes/:id - Get node by ID")
log.Println(" GET /api/nodes/:id/neighbors - Get node neighbors") log.Println(" GET /api/nodes/:id/neighbors - Get node neighbors")
if err := router.Run(":" + PORT); err != nil { if err := router.Run(":" + cfg.Server.Port); err != nil {
log.Fatalf("Failed to start server: %v", err) log.Fatalf("Failed to start server: %v", err)
} }
} }
+7 -29
View File
@@ -2,47 +2,25 @@ package neo4j
import ( import (
"context" "context"
"fmt"
"log"
"os"
"github.com/joho/godotenv"
"github.com/neo4j/neo4j-go-driver/v5/neo4j" "github.com/neo4j/neo4j-go-driver/v5/neo4j"
"knowledge-graph-backend/config"
) )
var ( func NewDriver(cfg config.Neo4jConfig) neo4j.DriverWithContext {
NEO4J_URI string
NEO4J_USERNAME string
NEO4J_PASSWORD string
)
func init() {
err := godotenv.Load()
if err != nil {
log.Fatal("Error loading .env file")
}
NEO4J_URI = os.Getenv("NEO4J_URI")
NEO4J_USERNAME = os.Getenv("NEO4J_USERNAME")
NEO4J_PASSWORD = os.Getenv("NEO4J_PASSWORD")
}
func doConnect() {
ctx := context.Background() ctx := context.Background()
// dbUri := "<database-uri>"
// dbUser := "<username>"
// dbPassword := "<password>"
driver, err := neo4j.NewDriverWithContext( driver, err := neo4j.NewDriverWithContext(
NEO4J_URI, cfg.URI,
neo4j.BasicAuth(NEO4J_USERNAME, NEO4J_PASSWORD, "")) neo4j.BasicAuth(cfg.Username, cfg.Password, ""))
if err != nil { if err != nil {
panic(err) panic(err)
} }
defer driver.Close(ctx)
err = driver.VerifyConnectivity(ctx) err = driver.VerifyConnectivity(ctx)
if err != nil { if err != nil {
panic(err) panic(err)
} }
fmt.Println("Connection established.")
return driver
} }
+18 -3
View File
@@ -1,7 +1,22 @@
package neo4j package neo4j
import "testing" import (
"context"
"testing"
func TestDoConnect(t *testing.T){ "knowledge-graph-backend/config"
doConnect() )
func TestNewDriver(t *testing.T) {
cnf, err := config.Load()
if err != nil {
t.Errorf("[cnf]: %v", err)
}
cfg := config.Neo4jConfig{
URI: cnf.Neo4j.URI,
Username: cnf.Neo4j.Username,
Password: cnf.Neo4j.Password,
}
driver := NewDriver(cfg)
defer driver.Close(context.Background())
} }
+301
View File
@@ -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
}
+11
View File
@@ -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
}