ForcePilot/backend/package/yuxi/knowledge/graphs/graph_utils.py
Wenjie Zhang 409df5b073 feat(kb): 新增检索配置、图种子查询、实体/三元组搜索与融合检索
- 新增 MilvusRetrievalConfig 数据类,支持向量/关键词/混合/图检索参数化配置
- 新增 query_seed_subgraph 和图检索融合逻辑
- MilvusGraphVectorStore 新增 search_entities/search_triples
- MilvusGraphService.get_status 支持构建任务状态与进度查询
- cypher 语句增加 entity_id/triple_id 属性
- 前端 KnowledgeGraphSection 和 SearchConfigModal 适配新配置
- 新增 igraph 依赖
2026-05-26 17:39:12 +08:00

151 lines
4.9 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.

"""图谱构建相关的纯函数工具集。
将数据变换逻辑从 MilvusGraphService 中抽离,
使 service 类专注于 I/O 和业务编排。
"""
from __future__ import annotations
from typing import Any
from yuxi.utils import hashstr
def normalize_entity_name(text: str) -> str:
"""统一实体名称:去首尾空白、小写化、压缩内部连续空白。"""
return " ".join(text.strip().lower().split())
def compute_entity_id(db_id: str, normalized_name: str, label: str) -> str:
return hashstr(f"{db_id}:{normalized_name}:{label}", length=32)
def compute_triple_id(
db_id: str,
source_normalized_name: str,
source_label: str,
relation_type: str,
target_normalized_name: str,
target_label: str,
) -> str:
return hashstr(
f"{db_id}:{source_normalized_name}:{source_label}:{relation_type}:{target_normalized_name}:{target_label}",
length=32,
)
def graph_entity_collection_name(db_id: str) -> str:
return f"{db_id}_entity"
def graph_triple_collection_name(db_id: str) -> str:
return f"{db_id}_triple"
def build_graph_payload(normalized_result: dict[str, Any]) -> dict[str, Any]:
"""将抽取器产出的标准化结果转换为 Neo4j 写入所需的图结构。
返回的 entities 已完成去重合并:同名同 label 的实体只保留一份,
属性attributes取并集。
"""
entities: list[dict[str, Any]] = []
entity_by_key: dict[tuple[str, str], dict[str, Any]] = {}
def add_entity(entity: dict[str, Any]) -> str:
key = (normalize_entity_name(entity["text"]), entity.get("label") or "Entity")
existing = entity_by_key.get(key)
if existing is not None:
known_attributes = {(attr["text"], attr["label"]) for attr in existing.get("attributes") or []}
for attribute in entity.get("attributes") or []:
attribute_key = (attribute["text"], attribute["label"])
if attribute_key not in known_attributes:
existing.setdefault("attributes", []).append(attribute)
known_attributes.add(attribute_key)
return existing["id"]
graph_entity = {
"id": f"e{len(entities) + 1}",
"text": entity["text"],
"label": entity.get("label") or "Entity",
"attributes": list(entity.get("attributes") or []),
}
entities.append(graph_entity)
entity_by_key[key] = graph_entity
return graph_entity["id"]
for entity in normalized_result["entities"]:
add_entity(entity)
relations = []
for relation in normalized_result["relations"]:
relations.append(
{
"source": add_entity(relation["source"]),
"target": add_entity(relation["target"]),
"text": relation["text"],
"label": relation.get("label") or "RELATED_TO",
}
)
return {"entities": entities, "relations": relations, "metadata": normalized_result["metadata"]}
# ─── Cypher 模板 ────────────────────────────────────────────────
# 将大段 Cypher 字符串集中管理,提升 write_chunk_graph 的可读性。
def cypher_merge_chunk(db_label: str) -> str:
"""MERGE Chunk 节点并写入元数据。"""
return f"""
MERGE (c:Chunk:MilvusKB:`{db_label}` {{chunk_id: $chunk_id}})
SET c.file_id = $file_id,
c.db_id = $db_id,
c.chunk_index = $chunk_index,
c.content_preview = $content_preview,
c.start_char_pos = $start_char_pos,
c.end_char_pos = $end_char_pos
"""
def cypher_merge_entity_mention(db_label: str) -> str:
"""MERGE Entity 节点并创建 Chunk → Entity 的 MENTIONS 关系。"""
return f"""
MATCH (c:Chunk:MilvusKB:`{db_label}` {{chunk_id: $chunk_id}})
MERGE (e:Entity:MilvusKB:`{db_label}` {{
db_id: $db_id,
normalized_name: $normalized_name,
label: $entity_label
}})
SET e.entity_id = $entity_id,
e.name = $name,
e.attributes = $attributes
MERGE (c)-[m:MENTIONS {{chunk_id: $chunk_id, file_id: $file_id, db_id: $db_id}}]->(e)
"""
def cypher_merge_relation(db_label: str) -> str:
"""MERGE 两个 Entity 之间的 RELATION 边。"""
return f"""
MATCH (source:Entity:MilvusKB:`{db_label}` {{
db_id: $db_id,
normalized_name: $source_name,
label: $source_label
}})
MATCH (target:Entity:MilvusKB:`{db_label}` {{
db_id: $db_id,
normalized_name: $target_name,
label: $target_label
}})
MERGE (source)-[r:RELATION {{
db_id: $db_id,
chunk_id: $chunk_id,
source_name: $source_name,
target_name: $target_name,
type: $relation_type
}}]->(target)
SET r.triple_id = $triple_id,
r.text = $text,
r.file_id = $file_id,
r.extractor_type = $extractor_type
"""