kgrag/graph_search/operation_graph_search.py
2026-06-30 13:35:52 +08:00

354 lines
15 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
===========================================
操作图谱检索模块 - operation_graph_search
===========================================
功能:针对"操作"的固定图谱检索接口
检索路径:舰艇 -> 系统 -> 子系统 -> 设备 -> 操作使用 -> 操作程序 -> 操作项目
===========================================
"""
import json
import os
import logging
import time
from typing import Any, Dict, List, Optional
from dotenv import load_dotenv
from neo4j import AsyncGraphDatabase, GraphDatabase
load_dotenv()
logger = logging.getLogger(__name__)
NEO4J_CONFIG = {
"uri": os.getenv("NEO4J_URI", "bolt://192.168.0.46:57687"),
"user": os.getenv("NEO4J_USER", "neo4j"),
"password": os.getenv("NEO4J_PASSWORD", "zdht123@"),
}
class EmbeddingWrapper:
"""嵌入模型包装器"""
def embed_documents(self, texts: List[str]) -> List[List[float]]:
"""批量获取嵌入向量(一次性处理所有文本)"""
from modelsAPI.model_api import OpenaiAPI
if not texts:
return []
try:
result = OpenaiAPI.batch_embeddings(texts)
if result and isinstance(result, list):
embedding_list = [item.get('embedding') if isinstance(item, dict) else item for item in result]
if embedding_list and len(embedding_list) == len(texts):
return embedding_list
else:
logger.error(f"EmbeddingWrapper: 批量向量化返回数量不匹配,期望: {len(texts)}, 实际: {len(embedding_list) if embedding_list else 0}")
return [None] * len(texts)
else:
logger.error(f"EmbeddingWrapper: 批量向量化返回格式异常")
return [None] * len(texts)
except Exception as e:
logger.error(f"EmbeddingWrapper: 批量向量化失败: {e}", exc_info=True)
return [None] * len(texts)
async def _match_node_with_retrieval(
session,
label: str,
node_name: str,
top_k: int = 10
) -> Optional[Dict[str, Any]]:
"""
使用 NodeRetrieval 匹配节点
"""
from graph_search.node_retrieval import NodeRetrieval
uri = NEO4J_CONFIG.get("uri")
user = NEO4J_CONFIG.get("user")
password = NEO4J_CONFIG.get("password")
sync_driver = GraphDatabase.driver(uri, auth=(user, password))
try:
embeddings = EmbeddingWrapper()
node_retrieval = NodeRetrieval(sync_driver, embeddings)
route_res = [{"label": label, "entity": node_name}]
retrieved_nodes = await node_retrieval.retrieve_nodes(route_res, top_k=top_k)
if not retrieved_nodes or label not in retrieved_nodes:
logger.warning(f"NodeRetrieval 未检索到 {label} 节点: {node_name}")
return None
candidates = retrieved_nodes[label]
if not candidates:
return None
logger.info(f"NodeRetrieval 检索到 {len(candidates)}{label} 节点候选")
for i, node in enumerate(candidates):
node_data = node.get("node", {})
candidate_name = node_data.get("名称") or node_data.get("name") or node.get("name", "")
logger.info(f" 候选 {i+1}: 名称={candidate_name}, score={node.get('score', 0.0)}")
best = candidates[0]
node_data = best.get("node", {})
best_name = node_data.get("名称") or node_data.get("name") or best.get("name", "")
query = f"""
MATCH (n:{label})
WHERE n.名称 = $name OR n.name = $name
RETURN elementId(n) AS id, n.名称 AS name, labels(n) AS labels, properties(n) AS props
LIMIT 1
"""
result = await session.run(query, name=best_name)
record = await result.single()
if record:
return {
"id": record.get("id"),
"name": record.get("name"),
"labels": record.get("labels"),
"props": record.get("props", {}),
"score": best.get("score", 0.0)
}
return None
finally:
sync_driver.close()
async def operation_graph_search(
ship_number: str,
device_name: str,
operation_item: str,
) -> Dict[str, Any]:
"""
操作图谱检索接口
Args:
ship_number: 舰艇舷号
device_name: 设备名称
operation_item: 操作项目名称
Returns:
Dict: 检索结果,包含 success, xxxx.graph 等字段
"""
start_time = time.time()
if not ship_number or not str(ship_number).strip():
return {"success": False, "error": "舷号不能为空"}
if not device_name or not str(device_name).strip():
return {"success": False, "error": "设备名称不能为空"}
if not operation_item or not str(operation_item).strip():
return {"success": False, "error": "操作项目名称不能为空"}
uri = NEO4J_CONFIG.get("uri")
user = NEO4J_CONFIG.get("user")
password = NEO4J_CONFIG.get("password")
if not uri or not user or not password:
return {"success": False, "error": "Neo4j 配置不完整,请检查 NEO4J_CONFIG"}
try:
async with AsyncGraphDatabase.driver(uri, auth=(user, password)) as driver:
async with driver.session() as session:
ship_key = str(ship_number).strip()
device_key = str(device_name).strip()
operation_key = str(operation_item).strip()
# ========== 1) 匹配舰艇节点 ==========
ship_query = """
MATCH (s:舰艇)
WHERE s.舷号 = $ship_key OR s.舷号 CONTAINS $ship_key OR $ship_key CONTAINS s.舷号
RETURN elementId(s) AS id, s.名称 AS name, s.舷号 AS ship_number, labels(s) AS labels, properties(s) AS props
LIMIT 1
"""
result = await session.run(ship_query, ship_key=ship_key)
ship_record = await result.single()
if not ship_record:
return {"success": False, "error": "在图谱中未找到对应舷号的舰艇节点"}
ship_id = ship_record.get("id")
ship_name = ship_record.get("name") or ship_record.get("ship_number")
logger.info(f"匹配到舰艇节点: {ship_name}, id={ship_id}")
# ========== 2) 匹配设备节点 ==========
device_query = """
MATCH (ship:舰艇)
WHERE elementId(ship) = $ship_id
MATCH (ship)-[:包含*0..8]->(d:设备)
WHERE d.名称 CONTAINS $device_key OR $device_key CONTAINS d.名称
OR d.name CONTAINS $device_key OR $device_key CONTAINS d.name
RETURN elementId(d) AS id, d.名称 AS name, labels(d) AS labels, properties(d) AS props
LIMIT 1
"""
result = await session.run(device_query, ship_id=ship_id, device_key=device_key)
device_record = await result.single()
if not device_record:
device_node = await _match_node_with_retrieval(session, "设备", device_key)
if device_node:
check_query = """
MATCH (ship:舰艇)
WHERE elementId(ship) = $ship_id
MATCH (ship)-[:包含*0..8]->(d:设备)
WHERE elementId(d) = $device_id
RETURN true AS in_range
LIMIT 1
"""
check_result = await session.run(check_query, ship_id=ship_id, device_id=device_node["id"])
check_record = await check_result.single()
if check_record:
device_record = device_node
if not device_record:
return {"success": False, "error": "在该舰艇范围内未找到匹配的设备节点"}
device_id = device_record.get("id")
device_name_found = device_record.get("name")
logger.info(f"匹配到设备节点: {device_name_found}, id={device_id}")
# ========== 3) 匹配操作项目节点 ==========
op_item_query = """
MATCH (d:设备)
WHERE elementId(d) = $device_id
MATCH (d)-[:包含*0..8]-(real_d:设备)
MATCH (real_d)-[:设备支持操作操作使用]-(op_use:操作使用)
-[:使用]-(op_proc:操作程序)
-[:操作时使用]-(op_item:操作项目)
WHERE op_item.名称 CONTAINS $op_key OR $op_key CONTAINS op_item.名称
RETURN elementId(op_item) AS id, op_item.名称 AS name, labels(op_item) AS labels, properties(op_item) AS props
LIMIT 1
"""
result = await session.run(op_item_query, device_id=device_id, op_key=operation_key)
op_item_record = await result.single()
if not op_item_record:
logger.info(f"Cypher查询未找到操作项目尝试 NodeRetrieval...")
op_item_node = await _match_node_with_retrieval(session, "操作项目", operation_key)
if op_item_node:
logger.info(f"NodeRetrieval 找到操作项目节点: id={op_item_node.get('id')}, name={op_item_node.get('name')}")
check_query = """
MATCH (d:设备)
WHERE elementId(d) = $device_id
MATCH (d)-[:包含*0..8]-(real_d:设备)
MATCH (real_d)-[:支持操作]-(op_use:操作使用)
-[:使用]-(op_proc:操作程序)
-[:操作时使用]-(op_item:操作项目)
WHERE elementId(op_item) = $op_item_id
RETURN true AS in_range
LIMIT 1
"""
check_result = await session.run(check_query, device_id=device_id, op_item_id=op_item_node["id"])
check_record = await check_result.single()
if check_record:
logger.info(f"操作项目节点在设备路径下验证通过")
op_item_record = op_item_node
else:
logger.warning(f"操作项目节点不在设备路径下,尝试直接使用...")
op_item_record = op_item_node
if not op_item_record:
return {"success": False, "error": "未找到匹配的操作项目节点"}
op_item_id = op_item_record.get("id")
op_item_name = op_item_record.get("name")
logger.info(f"匹配到操作项目节点: {op_item_name}, id={op_item_id}")
# ========== 4) 查找经过这三个节点的链路 ==========
exclude_keys = {
"last_updated",
"created_at",
"knowledge_source",
"fulltext",
"embedding",
}
path_query = """
MATCH (ship:舰艇)
WHERE elementId(ship) = $ship_id
MATCH (device:设备)
WHERE elementId(device) = $device_id
MATCH (op_item:操作项目)
WHERE elementId(op_item) = $op_item_id
MATCH path1 = (ship)-[:包含*0..8]->(device)
MATCH path2 = (device)-[:包含*0..8]-(real_d:设备)
-[:支持操作]->(op_use:操作使用)
-[:使用]->(op_proc:操作程序)
-[:操作时使用]->(op_item)
WITH path1, path2
WITH nodes(path1) + nodes(path2) AS all_nodes,
relationships(path1) + relationships(path2) AS all_rels
RETURN all_nodes, all_rels
LIMIT 1
"""
result = await session.run(path_query, ship_id=ship_id, device_id=device_id, op_item_id=op_item_id)
path_record = await result.single()
if not path_record:
return {"success": False, "error": "未找到连接这三个节点的路径"}
# ========== 5) 格式化输出 ==========
all_nodes = path_record.get("all_nodes", [])
all_rels = path_record.get("all_rels", [])
nodes_g: List[Dict[str, Any]] = []
node_id_set = set()
for node in all_nodes:
if node is None:
continue
nid = node.element_id if hasattr(node, 'element_id') else node.id
if nid in node_id_set:
continue
node_id_set.add(nid)
props = dict(node) if hasattr(node, '__iter__') else {}
props_filtered = {k: v for k, v in props.items() if k not in exclude_keys}
name = props.get("名称") or props.get("name") or str(nid)
labels = list(node.labels) if hasattr(node, 'labels') else []
nodes_g.append({
"id": nid,
"name": name,
"labels": labels,
"properties": props_filtered,
})
links_g: List[Dict[str, Any]] = []
for rel in all_rels:
if rel is None:
continue
src = rel.start_node.element_id if hasattr(rel.start_node, 'element_id') else rel.start_node.id
tgt = rel.end_node.element_id if hasattr(rel.end_node, 'element_id') else rel.end_node.id
if src not in node_id_set or tgt not in node_id_set:
continue
links_g.append({
"id": rel.element_id if hasattr(rel, 'element_id') else rel.id,
"label": rel.type if hasattr(rel, 'type') else str(rel.type),
"source": src,
"target": tgt,
})
# ========== 6) 生成 results 文本 ==========
op_item_props = op_item_record.get("props", {})
op_info = {k: v for k, v in op_item_props.items() if k not in exclude_keys and v}
results_text = json.dumps(op_info, ensure_ascii=False) if op_info else ""
elapsed_ms = (time.time() - start_time) * 1000
logger.info(f"[时间统计] 操作图谱检索完成,耗时: {elapsed_ms:.2f}ms")
graph_item = {"nodes": nodes_g, "links": links_g, "results": results_text}
return {"success": True, "xxxx.graph": [graph_item]}
except Exception as e:
logger.error(f"操作图谱检索失败: {e}", exc_info=True)
return {"success": False, "error": f"Neo4j 查询失败: {str(e)}"}