Files
group_fqcd_jr/app/infrastructure/graph.py
T
lzf_0626 75dff088d4 feat: 接通 Neo4j 图能力(驱动实现 + 装配),并实测确认图模型标签不一致
第 2 步(图库)的地基部分。

1. 新增 app/infrastructure/graph.py:GraphDriver 协议的 neo4j 实现。
   这层此前是空的——relationship_service.py 里的 GraphDriver 只有 Protocol 声明、没有任何
   具体实现,组装层也没有装配它,因此图读服务在运行期必然降级(neo4j_unavailable)。
   这正是"Neo4j 连读适配器都没有"的根因。
   降级取向与 Milvus 侧一致:本层不吞异常(由调用方决定降级方式),但构造失败返回 None
   ——图库不可用不该让应用起不来,也不该阻塞主链路。

2. bootstrap 新增 get_relationship_service():装配图读服务。
   该服务只读,关系类型受 ALLOWED_RELATIONSHIPS 白名单约束;写入走 GraphProjectionWorker
   (由领域事件驱动),这里不提供任意写接口——避免出现绕过事件链路的直写路径。

3. 实测确认一处既有缺陷(本次不改,方案待定):投影 worker 写入的节点标签是
   :Entity {entity_id}(字符串属性),而 RelationshipService.neighbors 查询的是
   :Customer {customer_id}(整数属性),两侧从未对齐,写进去的关系读不出来。
   Neo4j 在执行读查询时直接给出三条警告佐证:未知标签 Customer、未知属性 customer_id、
   未知关系类型 PREFERS——这也说明业务图从未被真正投影过(库中只有 Neo4j 自带的
   Person/Movie 示例数据,共 5 个节点)。

验证:ruff 通过、mypy 110 文件无错;get_relationship_service() 返回 RelationshipService,
neighbors 与 paths 正常执行并返回空集(非降级),非法关系类型被白名单拒绝。
2026-09-10 21:40:00 +08:00

65 lines
2.8 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.
"""Neo4j 图驱动:把 neo4j 异步驱动封装成 `RelationshipService` 期望的 `GraphDriver` 协议。
为什么单独一层:`RelationshipService` 只依赖 `execute_query(query, **params)` 这个窄协议,
不关心底层是哪个驱动。有这一层,读服务、投影 worker、删除客户端就能共用同一套连接管理,
也便于测试注入替身。
**这层此前是空的** —— `app/service/relationship_service.py` 里的 `GraphDriver` 只有一个
Protocol 声明,没有任何具体实现,组装层也没有装配它,所以读服务在运行期必然降级
(`neo4j_unavailable`),这正是"Neo4j 连读适配器都没有"的根因。
降级语义与 Milvus 侧保持一致:本层**不吞异常**(由调用方决定降级方式),但**构造失败返回 None**
——图库不可用不该让应用起不来,也不该阻塞主链路。
"""
import logging
from collections.abc import Sequence
from typing import Any
from app.core.config import get_settings
logger = logging.getLogger(__name__)
class Neo4jGraphDriver:
"""`GraphDriver` 协议的 neo4j 实现。"""
def __init__(self, driver: Any, database: str) -> None:
self._driver = driver
self._database = database
async def execute_query(self, query: str, **parameters: Any) -> Sequence[Any]:
"""执行一条 Cypher 并返回记录列表。
查询文本由调用方提供(`RelationshipService` 与投影 worker 各自持有白名单:
前者限制关系类型,后者只接受 `ALLOWED_RELATIONSHIPS` 中的关系),本层不做校验,
只负责执行与结果整形——把校验放在拥有业务语义的地方,避免两处规则漂移。
"""
async with self._driver.session(database=self._database) as session:
result = await session.run(query, **parameters)
return list(await result.data())
async def close(self) -> None:
await self._driver.close()
def build_graph_driver() -> Neo4jGraphDriver | None:
"""按配置构造图驱动;依赖缺失或配置不全时返回 None(图能力关闭)。
与 `get_vector_memory_adapter` 同一取向:外部依赖不可用只关闭对应能力,
不让整个应用起不来。
"""
settings = get_settings()
if not settings.neo4j_uri or not settings.neo4j_password:
logger.warning("neo4j config incomplete; graph capability disabled")
return None
try:
from neo4j import AsyncGraphDatabase
except ImportError:
logger.warning("neo4j driver not installed; graph capability disabled")
return None
driver = AsyncGraphDatabase.driver(
settings.neo4j_uri, auth=(settings.neo4j_username, settings.neo4j_password)
)
return Neo4jGraphDriver(driver, settings.neo4j_database)