Compare commits
No commits in common. "main" and "master" have entirely different histories.
10
.gitignore
vendored
Normal file
10
.gitignore
vendored
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
logs/
|
||||||
|
*.log
|
||||||
|
nohup.out
|
||||||
|
nohup_gpu*.out
|
||||||
|
py3/
|
||||||
|
*.egg-info/
|
||||||
|
*.pid
|
||||||
35
README.md
35
README.md
@ -1,35 +0,0 @@
|
|||||||
# Graph Service — 图数据库服务
|
|
||||||
|
|
||||||
## 概述
|
|
||||||
|
|
||||||
基于 NetworkX 的内存图数据库服务,支持节点/边的增删查、邻居查询、路径搜索和图持久化。用于知识图谱的实体关系存储。
|
|
||||||
|
|
||||||
## 后端
|
|
||||||
|
|
||||||
NetworkX (纯 Python,无需外部依赖)
|
|
||||||
|
|
||||||
## API
|
|
||||||
|
|
||||||
| 端点 | 方法 | 说明 |
|
|
||||||
|------|------|------|
|
|
||||||
| /api/status | GET | 服务状态和图统计 |
|
|
||||||
| /api/graph/add_node | POST | 添加节点 |
|
|
||||||
| /api/graph/add_edge | POST | 添加边 |
|
|
||||||
| /api/graph/neighbors | POST | 查询邻居 (支持深度) |
|
|
||||||
| /api/graph/path | POST | 最短路径查询 |
|
|
||||||
| /api/graph/query | POST | 通用图查询 |
|
|
||||||
| /api/graph/stats | POST | 图统计信息 |
|
|
||||||
| /api/graph/save | POST | 保存图到磁盘 |
|
|
||||||
| /api/graph/load | POST | 从磁盘加载图 |
|
|
||||||
|
|
||||||
## 部署
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd /data/ymq/graph-service
|
|
||||||
bash build.sh
|
|
||||||
sudo systemctl restart graph-service
|
|
||||||
```
|
|
||||||
|
|
||||||
## 端口
|
|
||||||
|
|
||||||
9092
|
|
||||||
6
ah.py
Normal file
6
ah.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
# -*- coding:utf-8 -*-
|
||||||
|
from ahserver.webapp import webapp
|
||||||
|
from init import load_graph_service
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
webapp(load_graph_service)
|
||||||
55
build.sh
Executable file
55
build.sh
Executable file
@ -0,0 +1,55 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Graph Service (NetworkX)
|
||||||
|
set -e
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
|
||||||
|
SERVICE_NAME="graph-service"
|
||||||
|
PORT=9092
|
||||||
|
PY=/data/ymq/wan22-service/py3/bin/python
|
||||||
|
action="${1:-status}"
|
||||||
|
|
||||||
|
case "$action" in
|
||||||
|
deploy|update)
|
||||||
|
echo "=== $SERVICE_NAME Deploy (CPU, port $PORT) ==="
|
||||||
|
if [ -f ah.pid ] && kill -0 $(cat ah.pid) 2>/dev/null; then
|
||||||
|
kill $(cat ah.pid) 2>/dev/null || true; sleep 2
|
||||||
|
fi
|
||||||
|
if [ -d .git ] && [ -f .git/HEAD ]; then
|
||||||
|
git pull origin master 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
mkdir -p logs files wwwroot data
|
||||||
|
export PYTHONPATH="$(pwd)"
|
||||||
|
nohup $PY ah.py > nohup.out 2>&1 &
|
||||||
|
echo $! > ah.pid
|
||||||
|
echo "Started PID $(cat ah.pid) on port $PORT"
|
||||||
|
sleep 3
|
||||||
|
if curl -s http://localhost:$PORT/api/status > /dev/null 2>&1; then
|
||||||
|
echo "Service healthy"
|
||||||
|
else
|
||||||
|
echo "WARNING: not responding, check nohup.out"
|
||||||
|
tail -20 nohup.out
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
stop)
|
||||||
|
if [ -f ah.pid ]; then
|
||||||
|
kill $(cat ah.pid) 2>/dev/null || true; rm -f ah.pid; echo "Stopped"
|
||||||
|
else echo "Not running"; fi
|
||||||
|
;;
|
||||||
|
start)
|
||||||
|
mkdir -p logs files wwwroot data
|
||||||
|
export PYTHONPATH="$(pwd)"
|
||||||
|
nohup $PY ah.py > nohup.out 2>&1 &
|
||||||
|
echo $! > ah.pid; echo "Started PID $(cat ah.pid)"
|
||||||
|
;;
|
||||||
|
status)
|
||||||
|
echo "=== $SERVICE_NAME Status ==="
|
||||||
|
if [ -f ah.pid ] && kill -0 $(cat ah.pid) 2>/dev/null; then
|
||||||
|
echo "Process: running (PID $(cat ah.pid))"
|
||||||
|
else echo "Process: not running"; fi
|
||||||
|
echo "Port: $PORT"
|
||||||
|
if curl -s --max-time 3 http://localhost:$PORT/api/status > /dev/null 2>&1; then
|
||||||
|
echo "HTTP: OK"
|
||||||
|
else echo "HTTP: not responding"; fi
|
||||||
|
;;
|
||||||
|
*) echo "Usage: $0 {deploy|update|stop|start|status}"; exit 1 ;;
|
||||||
|
esac
|
||||||
33
conf/config.json
Normal file
33
conf/config.json
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"password_key": "GraphService2026Key",
|
||||||
|
"filesroot": "$[workdir]$/files",
|
||||||
|
"logger": {
|
||||||
|
"name": "graph-service",
|
||||||
|
"levelname": "info",
|
||||||
|
"logfile": "$[workdir]$/logs/graph-service.log"
|
||||||
|
},
|
||||||
|
"website": {
|
||||||
|
"paths": [["$[workdir]$/wwwroot", ""]],
|
||||||
|
"client_max_size": 52428800,
|
||||||
|
"host": "0.0.0.0",
|
||||||
|
"port": 9092,
|
||||||
|
"coding": "utf-8",
|
||||||
|
"indexes": ["index.html"],
|
||||||
|
"startswiths": [
|
||||||
|
{"leading": "/api/status", "registerfunction": "status"},
|
||||||
|
{"leading": "/api/debug", "registerfunction": "debug"},
|
||||||
|
{"leading": "/api/graph/add_node", "registerfunction": "add_node"},
|
||||||
|
{"leading": "/api/graph/add_edge", "registerfunction": "add_edge"},
|
||||||
|
{"leading": "/api/graph/neighbors", "registerfunction": "neighbors"},
|
||||||
|
{"leading": "/api/graph/path", "registerfunction": "path"},
|
||||||
|
{"leading": "/api/graph/query", "registerfunction": "query"},
|
||||||
|
{"leading": "/api/graph/stats", "registerfunction": "stats"},
|
||||||
|
{"leading": "/api/graph/save", "registerfunction": "save"},
|
||||||
|
{"leading": "/api/graph/load", "registerfunction": "load"}
|
||||||
|
],
|
||||||
|
"processors": [
|
||||||
|
[".tmpl", "tmpl"], [".app", "app"], [".ui", "bui"],
|
||||||
|
[".dspy", "dspy"], [".md", "md"]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
7
data/default.json
Normal file
7
data/default.json
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"directed": true,
|
||||||
|
"multigraph": false,
|
||||||
|
"graph": {},
|
||||||
|
"nodes": [],
|
||||||
|
"links": []
|
||||||
|
}
|
||||||
29
data/test.json
Normal file
29
data/test.json
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"directed": true,
|
||||||
|
"multigraph": false,
|
||||||
|
"graph": {},
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"name": "张三",
|
||||||
|
"age": 30,
|
||||||
|
"type": "person",
|
||||||
|
"id": "person_1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "person_2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "company_1"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"links": [
|
||||||
|
{
|
||||||
|
"source": "person_1",
|
||||||
|
"target": "person_2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "person_2",
|
||||||
|
"target": "company_1"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
184
init.py
Normal file
184
init.py
Normal file
@ -0,0 +1,184 @@
|
|||||||
|
# -*- coding:utf-8 -*-
|
||||||
|
from traceback import format_exc
|
||||||
|
from ahserver.serverenv import ServerEnv
|
||||||
|
from appPublic.registerfunction import RegisterFunction
|
||||||
|
from appPublic.log import exception
|
||||||
|
import json
|
||||||
|
|
||||||
|
|
||||||
|
async def status_handler(request, params_kw, *args, **kwargs):
|
||||||
|
import sys, os
|
||||||
|
sys.path.insert(0, os.getcwd())
|
||||||
|
from workers.graph_engine import health_check
|
||||||
|
health = health_check()
|
||||||
|
return json.dumps({
|
||||||
|
"service": "graph-service",
|
||||||
|
"backend": health["backend"],
|
||||||
|
"graphs_loaded": health["graphs_loaded"],
|
||||||
|
"data_dir": health["data_dir"],
|
||||||
|
"endpoints": [
|
||||||
|
"/api/status", "/api/graph/add_node", "/api/graph/add_edge",
|
||||||
|
"/api/graph/neighbors", "/api/graph/path", "/api/graph/query",
|
||||||
|
"/api/graph/stats", "/api/graph/save", "/api/graph/load"
|
||||||
|
]
|
||||||
|
}, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
|
||||||
|
async def debug_handler(request, params_kw, *args, **kwargs):
|
||||||
|
"""Debug endpoint to see params_kw contents"""
|
||||||
|
return json.dumps({
|
||||||
|
"params_kw": params_kw,
|
||||||
|
"params_kw_keys": list(params_kw.keys()),
|
||||||
|
"request_method": request.method,
|
||||||
|
"request_content_type": request.content_type
|
||||||
|
}, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
|
||||||
|
async def add_node_handler(request, params_kw, *args, **kwargs):
|
||||||
|
import sys, os
|
||||||
|
sys.path.insert(0, os.getcwd())
|
||||||
|
from workers.graph_engine import add_node
|
||||||
|
try:
|
||||||
|
graph = params_kw.get("graph", "default")
|
||||||
|
node_id = params_kw.get("node_id")
|
||||||
|
attrs = params_kw.get("attrs", {})
|
||||||
|
|
||||||
|
if not node_id:
|
||||||
|
return json.dumps({"error": "node_id required", "debug": {"params_kw": params_kw}})
|
||||||
|
|
||||||
|
result = add_node(graph, node_id, **attrs)
|
||||||
|
return json.dumps({"status": "SUCCEEDED", **result}, ensure_ascii=False)
|
||||||
|
except Exception as e:
|
||||||
|
exception(f"{e}, {format_exc()}")
|
||||||
|
return json.dumps({"error": str(e)})
|
||||||
|
|
||||||
|
|
||||||
|
async def add_edge_handler(request, params_kw, *args, **kwargs):
|
||||||
|
import sys, os
|
||||||
|
sys.path.insert(0, os.getcwd())
|
||||||
|
from workers.graph_engine import add_edge
|
||||||
|
try:
|
||||||
|
graph = params_kw.get("graph", "default")
|
||||||
|
source = params_kw.get("source")
|
||||||
|
target = params_kw.get("target")
|
||||||
|
attrs = params_kw.get("attrs", {})
|
||||||
|
|
||||||
|
if not source or not target:
|
||||||
|
return json.dumps({"error": "source and target required"})
|
||||||
|
|
||||||
|
result = add_edge(graph, source, target, **attrs)
|
||||||
|
return json.dumps({"status": "SUCCEEDED", **result}, ensure_ascii=False)
|
||||||
|
except Exception as e:
|
||||||
|
exception(f"{e}, {format_exc()}")
|
||||||
|
return json.dumps({"error": str(e)})
|
||||||
|
|
||||||
|
|
||||||
|
async def neighbors_handler(request, params_kw, *args, **kwargs):
|
||||||
|
import sys, os
|
||||||
|
sys.path.insert(0, os.getcwd())
|
||||||
|
from workers.graph_engine import get_neighbors
|
||||||
|
try:
|
||||||
|
graph = params_kw.get("graph", "default")
|
||||||
|
node_id = params_kw.get("node_id")
|
||||||
|
direction = params_kw.get("direction", "both")
|
||||||
|
depth = int(params_kw.get("depth", 1))
|
||||||
|
|
||||||
|
if not node_id:
|
||||||
|
return json.dumps({"error": "node_id required"})
|
||||||
|
|
||||||
|
result = get_neighbors(graph, node_id, direction, depth)
|
||||||
|
return json.dumps({"status": "SUCCEEDED", **result}, ensure_ascii=False)
|
||||||
|
except Exception as e:
|
||||||
|
exception(f"{e}, {format_exc()}")
|
||||||
|
return json.dumps({"error": str(e)})
|
||||||
|
|
||||||
|
|
||||||
|
async def path_handler(request, params_kw, *args, **kwargs):
|
||||||
|
import sys, os
|
||||||
|
sys.path.insert(0, os.getcwd())
|
||||||
|
from workers.graph_engine import find_path
|
||||||
|
try:
|
||||||
|
graph = params_kw.get("graph", "default")
|
||||||
|
source = params_kw.get("source")
|
||||||
|
target = params_kw.get("target")
|
||||||
|
max_depth = int(params_kw.get("max_depth", 10))
|
||||||
|
|
||||||
|
if not source or not target:
|
||||||
|
return json.dumps({"error": "source and target required"})
|
||||||
|
|
||||||
|
result = find_path(graph, source, target, max_depth)
|
||||||
|
return json.dumps({"status": "SUCCEEDED", **result}, ensure_ascii=False)
|
||||||
|
except Exception as e:
|
||||||
|
exception(f"{e}, {format_exc()}")
|
||||||
|
return json.dumps({"error": str(e)})
|
||||||
|
|
||||||
|
|
||||||
|
async def query_handler(request, params_kw, *args, **kwargs):
|
||||||
|
import sys, os
|
||||||
|
sys.path.insert(0, os.getcwd())
|
||||||
|
from workers.graph_engine import query_nodes
|
||||||
|
try:
|
||||||
|
graph = params_kw.get("graph", "default")
|
||||||
|
filters = params_kw.get("filters")
|
||||||
|
limit = int(params_kw.get("limit", 100))
|
||||||
|
|
||||||
|
result = query_nodes(graph, filters, limit)
|
||||||
|
return json.dumps({"status": "SUCCEEDED", **result}, ensure_ascii=False)
|
||||||
|
except Exception as e:
|
||||||
|
exception(f"{e}, {format_exc()}")
|
||||||
|
return json.dumps({"error": str(e)})
|
||||||
|
|
||||||
|
|
||||||
|
async def stats_handler(request, params_kw, *args, **kwargs):
|
||||||
|
import sys, os
|
||||||
|
sys.path.insert(0, os.getcwd())
|
||||||
|
from workers.graph_engine import get_stats
|
||||||
|
try:
|
||||||
|
graph = params_kw.get("graph", "default")
|
||||||
|
result = get_stats(graph)
|
||||||
|
return json.dumps({"status": "SUCCEEDED", **result}, ensure_ascii=False)
|
||||||
|
except Exception as e:
|
||||||
|
exception(f"{e}, {format_exc()}")
|
||||||
|
return json.dumps({"error": str(e)})
|
||||||
|
|
||||||
|
|
||||||
|
async def save_handler(request, params_kw, *args, **kwargs):
|
||||||
|
import sys, os
|
||||||
|
sys.path.insert(0, os.getcwd())
|
||||||
|
from workers.graph_engine import save_graph
|
||||||
|
try:
|
||||||
|
graph = params_kw.get("graph", "default")
|
||||||
|
result = save_graph(graph)
|
||||||
|
return json.dumps({"status": "SUCCEEDED", **result}, ensure_ascii=False)
|
||||||
|
except Exception as e:
|
||||||
|
exception(f"{e}, {format_exc()}")
|
||||||
|
return json.dumps({"error": str(e)})
|
||||||
|
|
||||||
|
|
||||||
|
async def load_handler(request, params_kw, *args, **kwargs):
|
||||||
|
import sys, os
|
||||||
|
sys.path.insert(0, os.getcwd())
|
||||||
|
from workers.graph_engine import load_graph
|
||||||
|
try:
|
||||||
|
graph = params_kw.get("graph", "default")
|
||||||
|
result = load_graph(graph)
|
||||||
|
return json.dumps({"status": "SUCCEEDED", **result}, ensure_ascii=False)
|
||||||
|
except Exception as e:
|
||||||
|
exception(f"{e}, {format_exc()}")
|
||||||
|
return json.dumps({"error": str(e)})
|
||||||
|
|
||||||
|
|
||||||
|
def load_graph_service():
|
||||||
|
"""Register API handlers"""
|
||||||
|
env = ServerEnv()
|
||||||
|
rf = RegisterFunction()
|
||||||
|
rf.register("status", status_handler)
|
||||||
|
rf.register("debug", debug_handler)
|
||||||
|
rf.register("add_node", add_node_handler)
|
||||||
|
rf.register("add_edge", add_edge_handler)
|
||||||
|
rf.register("neighbors", neighbors_handler)
|
||||||
|
rf.register("path", path_handler)
|
||||||
|
rf.register("query", query_handler)
|
||||||
|
rf.register("stats", stats_handler)
|
||||||
|
rf.register("save", save_handler)
|
||||||
|
rf.register("load", load_handler)
|
||||||
0
workers/__init__.py
Normal file
0
workers/__init__.py
Normal file
203
workers/graph_engine.py
Normal file
203
workers/graph_engine.py
Normal file
@ -0,0 +1,203 @@
|
|||||||
|
# -*- coding:utf-8 -*-
|
||||||
|
"""NetworkX-based graph engine with persistence."""
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
import networkx as nx
|
||||||
|
from typing import Dict, List, Any, Optional
|
||||||
|
|
||||||
|
DATA_DIR = "/data/ymq/graph-service/data"
|
||||||
|
_graphs: Dict[str, nx.DiGraph] = {}
|
||||||
|
|
||||||
|
|
||||||
|
def get_graph(graph_name: str = "default") -> nx.DiGraph:
|
||||||
|
"""Get or create a graph by name."""
|
||||||
|
if graph_name not in _graphs:
|
||||||
|
# Try to load from disk
|
||||||
|
filepath = os.path.join(DATA_DIR, f"{graph_name}.json")
|
||||||
|
if os.path.exists(filepath):
|
||||||
|
with open(filepath, 'r') as f:
|
||||||
|
data = json.load(f)
|
||||||
|
_graphs[graph_name] = nx.node_link_graph(data, directed=True)
|
||||||
|
else:
|
||||||
|
_graphs[graph_name] = nx.DiGraph()
|
||||||
|
return _graphs[graph_name]
|
||||||
|
|
||||||
|
|
||||||
|
def add_node(graph_name: str, node_id: str, **attrs) -> Dict:
|
||||||
|
"""Add a node to the graph."""
|
||||||
|
g = get_graph(graph_name)
|
||||||
|
g.add_node(node_id, **attrs)
|
||||||
|
return {"status": "ok", "node_id": node_id, "graph": graph_name}
|
||||||
|
|
||||||
|
|
||||||
|
def add_edge(graph_name: str, source: str, target: str, **attrs) -> Dict:
|
||||||
|
"""Add an edge to the graph."""
|
||||||
|
g = get_graph(graph_name)
|
||||||
|
g.add_edge(source, target, **attrs)
|
||||||
|
return {"status": "ok", "source": source, "target": target, "graph": graph_name}
|
||||||
|
|
||||||
|
|
||||||
|
def get_neighbors(graph_name: str, node_id: str, direction: str = "both", depth: int = 1) -> Dict:
|
||||||
|
"""Get neighbors of a node."""
|
||||||
|
g = get_graph(graph_name)
|
||||||
|
|
||||||
|
if node_id not in g:
|
||||||
|
return {"error": f"Node {node_id} not found"}
|
||||||
|
|
||||||
|
# BFS to get neighbors up to depth
|
||||||
|
visited = set()
|
||||||
|
queue = [(node_id, 0)]
|
||||||
|
neighbors = []
|
||||||
|
|
||||||
|
while queue:
|
||||||
|
current, d = queue.pop(0)
|
||||||
|
if d > depth:
|
||||||
|
break
|
||||||
|
|
||||||
|
if d > 0: # Don't include the starting node
|
||||||
|
neighbors.append({
|
||||||
|
"node_id": current,
|
||||||
|
"depth": d,
|
||||||
|
"attrs": dict(g.nodes[current])
|
||||||
|
})
|
||||||
|
|
||||||
|
if current not in visited:
|
||||||
|
visited.add(current)
|
||||||
|
|
||||||
|
if d < depth:
|
||||||
|
# Add successors
|
||||||
|
if direction in ["both", "out"]:
|
||||||
|
for neighbor in g.successors(current):
|
||||||
|
if neighbor not in visited:
|
||||||
|
queue.append((neighbor, d + 1))
|
||||||
|
|
||||||
|
# Add predecessors
|
||||||
|
if direction in ["both", "in"]:
|
||||||
|
for neighbor in g.predecessors(current):
|
||||||
|
if neighbor not in visited:
|
||||||
|
queue.append((neighbor, d + 1))
|
||||||
|
|
||||||
|
return {"node_id": node_id, "neighbors": neighbors, "count": len(neighbors)}
|
||||||
|
|
||||||
|
|
||||||
|
def find_path(graph_name: str, source: str, target: str, max_depth: int = 10) -> Dict:
|
||||||
|
"""Find shortest path between two nodes."""
|
||||||
|
g = get_graph(graph_name)
|
||||||
|
|
||||||
|
if source not in g:
|
||||||
|
return {"error": f"Source node {source} not found"}
|
||||||
|
if target not in g:
|
||||||
|
return {"error": f"Target node {target} not found"}
|
||||||
|
|
||||||
|
try:
|
||||||
|
path = nx.shortest_path(g, source, target)
|
||||||
|
if len(path) > max_depth + 1:
|
||||||
|
return {"error": f"Path too long: {len(path)-1} > {max_depth}"}
|
||||||
|
|
||||||
|
# Get path details
|
||||||
|
path_details = []
|
||||||
|
for i in range(len(path)):
|
||||||
|
node_id = path[i]
|
||||||
|
node_data = {"node_id": node_id, "attrs": dict(g.nodes[node_id])}
|
||||||
|
|
||||||
|
if i < len(path) - 1:
|
||||||
|
# Add edge info
|
||||||
|
next_node = path[i + 1]
|
||||||
|
edge_data = dict(g.edges[node_id, next_node])
|
||||||
|
node_data["edge_to_next"] = edge_data
|
||||||
|
|
||||||
|
path_details.append(node_data)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"source": source,
|
||||||
|
"target": target,
|
||||||
|
"path": path,
|
||||||
|
"length": len(path) - 1,
|
||||||
|
"details": path_details
|
||||||
|
}
|
||||||
|
except nx.NetworkXNoPath:
|
||||||
|
return {"error": "No path found"}
|
||||||
|
|
||||||
|
|
||||||
|
def query_nodes(graph_name: str, filters: Optional[Dict] = None, limit: int = 100) -> Dict:
|
||||||
|
"""Query nodes with optional filters."""
|
||||||
|
g = get_graph(graph_name)
|
||||||
|
|
||||||
|
results = []
|
||||||
|
for node_id, attrs in g.nodes(data=True):
|
||||||
|
# Apply filters
|
||||||
|
match = True
|
||||||
|
if filters:
|
||||||
|
for key, value in filters.items():
|
||||||
|
if attrs.get(key) != value:
|
||||||
|
match = False
|
||||||
|
break
|
||||||
|
|
||||||
|
if match:
|
||||||
|
results.append({"node_id": node_id, "attrs": attrs})
|
||||||
|
if len(results) >= limit:
|
||||||
|
break
|
||||||
|
|
||||||
|
return {"nodes": results, "count": len(results)}
|
||||||
|
|
||||||
|
|
||||||
|
def get_stats(graph_name: str) -> Dict:
|
||||||
|
"""Get graph statistics."""
|
||||||
|
g = get_graph(graph_name)
|
||||||
|
|
||||||
|
stats = {
|
||||||
|
"graph": graph_name,
|
||||||
|
"nodes": g.number_of_nodes(),
|
||||||
|
"edges": g.number_of_edges(),
|
||||||
|
"density": nx.density(g),
|
||||||
|
"is_connected": nx.is_weakly_connected(g) if g.number_of_nodes() > 0 else True,
|
||||||
|
}
|
||||||
|
|
||||||
|
if g.number_of_nodes() > 0:
|
||||||
|
# Degree statistics
|
||||||
|
in_degrees = [d for _, d in g.in_degree()]
|
||||||
|
out_degrees = [d for _, d in g.out_degree()]
|
||||||
|
|
||||||
|
stats["avg_in_degree"] = sum(in_degrees) / len(in_degrees)
|
||||||
|
stats["avg_out_degree"] = sum(out_degrees) / len(out_degrees)
|
||||||
|
stats["max_in_degree"] = max(in_degrees)
|
||||||
|
stats["max_out_degree"] = max(out_degrees)
|
||||||
|
|
||||||
|
return stats
|
||||||
|
|
||||||
|
|
||||||
|
def save_graph(graph_name: str) -> Dict:
|
||||||
|
"""Save graph to disk."""
|
||||||
|
g = get_graph(graph_name)
|
||||||
|
filepath = os.path.join(DATA_DIR, f"{graph_name}.json")
|
||||||
|
|
||||||
|
data = nx.node_link_data(g)
|
||||||
|
with open(filepath, 'w') as f:
|
||||||
|
json.dump(data, f, ensure_ascii=False, indent=2)
|
||||||
|
|
||||||
|
return {"status": "ok", "filepath": filepath, "nodes": g.number_of_nodes(), "edges": g.number_of_edges()}
|
||||||
|
|
||||||
|
|
||||||
|
def load_graph(graph_name: str) -> Dict:
|
||||||
|
"""Load graph from disk."""
|
||||||
|
filepath = os.path.join(DATA_DIR, f"{graph_name}.json")
|
||||||
|
|
||||||
|
if not os.path.exists(filepath):
|
||||||
|
return {"error": f"Graph file not found: {filepath}"}
|
||||||
|
|
||||||
|
with open(filepath, 'r') as f:
|
||||||
|
data = json.load(f)
|
||||||
|
|
||||||
|
g = nx.node_link_graph(data, directed=True)
|
||||||
|
_graphs[graph_name] = g
|
||||||
|
|
||||||
|
return {"status": "ok", "nodes": g.number_of_nodes(), "edges": g.number_of_edges()}
|
||||||
|
|
||||||
|
|
||||||
|
def health_check():
|
||||||
|
"""Check service status."""
|
||||||
|
return {
|
||||||
|
"backend": "NetworkX",
|
||||||
|
"graphs_loaded": len(_graphs),
|
||||||
|
"data_dir": DATA_DIR
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user