知识图谱(四):图查询

知识图谱 / Cypher / SPARQL / Python2026.10.0915 分钟阅读

用 Python、Cypher 和 SPARQL 查询软件依赖,以及路径查询的优化方法。

存储模型与查询任务

图存储需要保存节点或资源、关系、属性以及必要的索引。小图可以用文件和内存邻接表分析,关系数据库可以用实体表和关系表表示,专用图数据库或 RDF 存储则提供相应的查询语言。选择主要取决于数据模型、更新方式、路径查询和语义推理需求。

示例数据采用静态快照:订单服务经 checkout-sdk 2.1 依赖 parser 1.4,报表服务直接依赖 parser 1.4;两者分别归交易团队和数据团队负责。所有依赖都视为当前有效的 runtime 依赖。目标是返回两个服务、对应团队以及支持入选的路径。

完整 Python 脚本只使用标准库,包含数据、遍历和检查。Cypher 片段使用 Neo4j 5 语法,SPARQL 片段使用 1.1 标准。

python3 knowledge_graph_demo.py

图遍历与依赖路径

依赖边由使用者指向被使用者,影响分析需要从目标寻找使用者,因此先建立反向邻接表。parser 1.4 的反向邻居为 checkout-sdk 2.1 和报表服务;下一层从 checkout-sdk 2.1 到达订单服务。广度优先搜索按距离逐层处理节点,在每条边权重相同时,首次到达节点获得一条最短路径。

from collections import defaultdict, deque

def affected_services(target, dependencies, services):
    reverse = defaultdict(list)
    for user, dependency, evidence in dependencies:
        reverse[dependency].append((user, evidence))

    queue = deque([(target, [target], [])])
    seen = {target}
    result = {}
    while queue:
        current, path, evidence = queue.popleft()
        for user, source in reverse[current]:
            if user in seen:
                continue
            seen.add(user)
            next_path = [user] + path
            next_evidence = [source] + evidence
            if user in services:
                result[user] = (next_path, next_evidence)
            queue.append((user, next_path, next_evidence))
    return result

队列元素保存当前节点、当前到目标的路径和逐边证据。每次找到使用者时,把使用者和来源编号加入路径前端;若它属于服务集合,就记录为结果。seen 在入队时标记节点,避免循环依赖重复展开。例子的订单路径有两条边,报表路径有一条边,证据列表分别包含两个和一个编号。

该实现为每个服务保存一条最短见证路径,不枚举全部路径。如果一个服务从三个入口间接使用 parser,结果仍只有一条。判断是否需要核查时,一条有效见证通常足够;规划如何完全移除依赖时,则需要检查所有有关入口或依赖子图。

普通邻接表 BFS 的遍历成本为 O(V+E),但这段实现还会复制路径列表,因此总成本包含随路径长度增加的复制开销。大图可以只保存前驱和边证据,最后按需恢复路径。运行脚本前,先按时间、权限和依赖作用域筛选快照。

Cypher 数据写入

Cypher 使用节点与关系的图模式表达读写。先为 Entity.id 建立唯一约束,可以防止同一业务标识出现多个实体。示例给所有节点添加 Entity 标签,再叠加 Service、PackageVersion 或 Team;这种全局约束要求不同类型的 id 使用互不冲突的命名空间。

CREATE CONSTRAINT entity_id IF NOT EXISTS
FOR (n:Entity) REQUIRE n.id IS UNIQUE;

MERGE (o:Entity:Service {id: "svc:order-api"})
MERGE (r:Entity:Service {id: "svc:report-api"})
MERGE (c:Entity:PackageVersion {id: "pkg:checkout-sdk:2.1"})
MERGE (p:Entity:PackageVersion {id: "pkg:parser:1.4"})
MERGE (t:Entity:Team {id: "team:commerce"})
MERGE (d:Entity:Team {id: "team:data"})
MERGE (o)-[a:DEPENDS_ON]->(c)
SET a.scope = "runtime", a.evidenceId = "build-order-1"
MERGE (c)-[b:DEPENDS_ON]->(p)
SET b.scope = "runtime", b.evidenceId = "manifest-checkout-1"
MERGE (r)-[e:DEPENDS_ON]->(p)
SET e.scope = "runtime", e.evidenceId = "build-report-1"
MERGE (o)-[:OWNED_BY]->(t)
MERGE (r)-[:OWNED_BY]->(d);

MERGE 表示匹配给定模式,找不到时创建。代码先按标识分别绑定节点,再对已绑定节点之间的关系执行 MERGE,最后用 SET 设置关系属性。将会变化的展示名称放进实体匹配键,可能在改名时产生新对象,因此身份字段应保持稳定。

唯一约束与 MERGE 承担不同职责。MERGE 提供匹配或创建的语义,唯一约束提供并发写入时的标识限制。代码中的一条依赖只保存一个 evidenceId,多个来源或历史断言需要单独建模,以免 SET 覆盖旧证据。

Cypher 路径查询

查询先用参数定位目标版本,再寻找服务到目标的一至八跳依赖路径。关系方向与写入保持一致,all 对路径中每条边检查 runtime 作用域,最后关联团队并展开路径节点和证据编号。

MATCH (target:Entity:PackageVersion {id: $targetId})
MATCH path = (service:Service)-[:DEPENDS_ON*1..8]->(target)
WHERE all(edge IN relationships(path) WHERE edge.scope = "runtime")
MATCH (service)-[:OWNED_BY]->(team:Team)
RETURN service.id AS service, team.id AS team,
       [node IN nodes(path) | node.id] AS dependencyPath,
       [edge IN relationships(path) | edge.evidenceId] AS evidence
ORDER BY service, length(path);

将 targetId 设为 pkg:parser:1.4,示例应返回订单服务与报表服务。方括号中的表达式分别把路径映射为节点 id 列表和 evidenceId 列表;结果由此包含服务及其入选依据。

1..8 将查询限定在八跳内,更长的依赖路径会被截断。需要完整可达集合时,应调整遍历实现;保留上限时,在结果中标明截断范围。

同一服务有多条路径时会产生多行,直接 COUNT(*) 统计的是匹配结果行,而非不同服务数量。需要服务数量时应按服务标识去重;需要升级入口时则应保留路径信息。团队关系缺失还会使本例中的必选 MATCH 丢弃该服务,若要求列出归属未知的服务,应使用 OPTIONAL MATCH 并显式处理空值。

SPARQL 图模式与属性路径

SPARQL 通过图模式匹配 RDF 三元组。问号开头的名称是变量,同一变量在不同模式中表示同一绑定。例如 ?service a ex:Service 选择服务实例,?service ex:ownedBy ?team 将该服务与团队绑定。分号只是沿用主体的语法缩写,不改变匹配含义。

PREFIX ex: <https://example.org/kg/>

SELECT DISTINCT ?service ?team WHERE {
  ?service a ex:Service ;
           ex:dependsOn+ ex:parser-1-4 ;
           ex:ownedBy ?team .
}

ex:dependsOn+ 表示沿该谓词连接一次或多次;星号则允许零次。零次路径意味着起点可以就是终点,是否进入最终结果还取决于其他类型条件。SELECT DISTINCT 对最终变量组合去重。

第二篇的 RDF 示例仅录入了订单服务,所以在那份数据上查询只得到订单服务和交易团队。加入报表服务的类型、依赖和归属三元组后才会得到第二行。比较两种查询时,先录入同一份数据。

SPARQL 1.1 属性路径主要表达可达匹配,不会自动绑定任意长度路径的每个中间节点和证据。需要逐边解释时,可以采用显式断言节点、分步查询或引擎扩展。启用何种 RDF 蕴涵也会影响类型匹配,应使开发、评估和生产使用一致配置。

查询优化与正确性验证

路径查询成本主要来自起点定位和路径展开。唯一标识索引可缩小起点范围,关系类型与属性过滤可减少候选,但高连接度节点和密集环路仍可能产生大量路径。相同边数的链状图与密集互连图,查询成本可能相差很大。

EXPLAIN 查看查询计划,PROFILE 执行查询并提供统计。重点看扫描行数、路径展开和索引使用。LIMIT 限制最终返回的行数,前面的扫描与展开仍需单独优化。

案例应该观察到什么暴露的问题
目标是 parser 1.4两个服务,路径长度分别为二和一方向与多跳遍历是否正确
目标是 checkout-sdk 2.1只有订单服务是否错误跨越无关边
目标不存在无匹配,明确未找到目标是否把数据缺失解释成安全
增加循环依赖遍历终止,已有服务结果不丢失是否有循环保护
同一服务有多条路径明确返回一条见证或全部路径是否混淆服务数与路径数

下载脚本检查直接依赖、间接依赖、未知目标、空图、循环、重复边和多路径,并验证每条返回路径确由输入边组成。未知目标与已知但没有使用者的目标都可能产生空集合,应用需要额外检查目标是否存在,才能给出准确状态。完整数据库实现还应加入缺少团队、时间筛选和权限范围等业务案例。

— 本页完 —
← 返回文章列表

搜索内容

输入关键词搜索文章、笔记与随笔。