PropertyGraphIndex

「GAPDH knockdown 如何影响糖酵解」这类问句,两段各自和问句相似也拼不出因果链——段落相似度没有边。PropertyGraphIndex 离线从 chunk 抽出实体与关系,建成带标签和属性的图;查询时先命中种子实体,再沿边走若干跳,最后把节点挂着的原文 chunk 回填给生成模型。

段末注释PropertyGraphIndex = LlamaIndex 的属性图索引:入库用 kg_extractors 写图,查询用 sub_retrievers 走路。它不是 Microsoft GraphRAG(社区摘要产品),也不是只存向量的 VectorStoreIndex。旧类 KnowledgeGraphIndex 自 0.10.53 起已弃用。

抽三元组建属性图、sub-retriever 沿边走路、include_text 回填原文与 source_nodes(科普示意)


1. 一句话定位

维度 内容
角色 知识层的属性图索引:实体/关系 + 可选 embedding;查询走子图而非纯 top-k 段落
输入 → 输出 list[Document]PropertyGraphIndex → 路径文本 / NodeWithScore / Response
典型调用入口 from_documents()from_existing()as_retriever(sub_retrievers=...)as_query_engine()
与 LangChain / LangGraph 近邻是「图库 + 子图召回」;LangGraph 节点里只 query / retrieve,不要在图里现抽三元组

出现背景:早期 KnowledgeGraphIndex 只能存刚性三元组,抽取与检索绑死。当前对象改用标注属性图(Labeled Property Graph,LPG):节点和边都可以带类型与 key-value 属性,抽取器、图库、子检索器可拆开换。

段末注释LPG = 节点有标签(如 GENE)、边上有关系类型(如 KNOCKDOWN_AFFECTS),二者都可挂属性。比「只有 (s, p, o) 三元组、无属性」的旧 KG 索引更贴 Neo4j 一类图库。


2. 前置依赖与环境

1
2
3
pip install -U llama-index-core llama-index-llms-ollama llama-index-embeddings-ollama
ollama pull qwen3.5:9b
ollama pull nomic-embed-text
  • Python 3.10+;llama-index-core 0.14.x
  • 必须同时设 Settings.llmSettings.embed_model:抽取走 LLM,实体节点默认还要 embedding
  • 默认图库 SimplePropertyGraphStore 不支持 Cypher;生产换 Neo4j 等再考虑 TextToCypherRetriever
  • 示例用同步 query();FastAPI 用 await qe.aquery(...)
  • 对接 Neo4j:pip install llama-index-graph-stores-neo4j(本篇示例用内存店)

3. 实现逻辑

1
2
3
4
5
6
7
8
9
1. Settings.llm / Settings.embed_model 设成本地 Ollama
2. 准备 Document(稳定 doc_id + metadata)
3. PropertyGraphIndex.from_documents(docs, kg_extractors=[...])
内部:切 Node → 每个 chunk 跑 kg_extractors → 实体/关系写入 property_graph_store
→(默认)给图节点算 embedding
4. retriever.retrieve(q) 或 qe.query(q)
默认 sub-retrievers:LLMSynonymRetriever + VectorContextRetriever
5. 命中实体后沿边走 path_depth 跳;include_text=True 时附带源 chunk
6. query 再合成答案;引用仍在 response.source_nodes

字段级变形

1
2
3
4
5
6
7
Document(text="……GAPDH knockdown……糖酵解……", metadata={section:"methods"})
→ kg_extractor 抽出路径 (GAPDH, knockdown, 糖酵解表型)
→ 图节点 GAPDH / 糖酵解表型(可带 embedding);边类型 knockdown
→ retrieve("GAPDH knockdown 影响什么?")
NodeWithScore(text="路径 + 可选原文 chunk", score=…)
→ query
Response(response="……糖酵解……", source_nodes=[...])

4. 原理说明

主轴是:入库把散文变成可走的边;查询先找种子实体,再扩子图,最后回填原文。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
1. from_documents 切出 TextNode[](transformations 或 Settings 默认 splitter)
2. 对每个 Node 依次跑 kg_extractors(默认同跑 SimpleLLMPathExtractor + ImplicitPathExtractor)
3. SimpleLLMPathExtractor:LLM 从 chunk 抽出单跳路径 (e1, rel, e2),写入图
4. ImplicitPathExtractor:把 Node.relationships(如前后 chunk)也变成图上的边——不调 LLM,也不是知识三元组
5. embed_kg_nodes=True(默认)时,图节点再走 embed_model,写入 vector_store 或图库自带向量
6. as_retriever:若未传 sub_retrievers,默认 LLMSynonymRetriever(问句 → 同义词/关键词 → 精确撞实体名)
以及(有 embedding 时)VectorContextRetriever(问句向量 → 相似图节点 → 再取相连路径)
7. 每个 sub-retriever 在命中节点后沿边走 path_depth 跳,合并成 NodeWithScore
8. include_text=True:路径节点回填其来源 chunk 正文,否则合成器只能看到三元组短句
9. as_query_engine:第 6~8 步之后把文本交给 llm;source_nodes 挂检索结果
10. 换库:property_graph_store=Neo4jPropertyGraphStore(...);已有图用 from_existing,不要再抽一遍
少了第 3 步(抽取失败/别名分裂)→ 图上没有可走的边,后面跳得再远也空
少了第 8 步 → 答案可能只有「GAPDH —knockdown→ 糖酵解」而缺剂量、品系等原文约束
少了第 9 步只 print(str(response)) → 无法证明走的是 methods 那条 chunk

SimpleLLMPathExtractor 出现在步骤 3。无预定义本体,LLM 自由命名实体和关系,覆盖广、一致性差。max_paths_per_chunk 限制每块抽出条数。

SchemaLLMPathExtractor 可替换步骤 3。用 possible_entities / possible_relations / kg_validation_schema 白名单;strict=True 时丢掉模式外三元组。领域稳定(基因、品系、方法)时优先它。本地小模型若结构化输出不稳,先退回 Simple,或把 strict=False 当建议而非硬校验。

DynamicLLMPathExtractor 介于二者:给一组允许类型作引导,但允许模型扩展新类型。

LLMSynonymRetriever / VectorContextRetriever 出现在步骤 6。前者吃 LLM 费用换别名(GAPDH / Gapdh / 甘油醛-3-磷酸脱氢酶);后者吃 embedding。两路都只是 PGRetriever 的子检索器,不是 Router,也不是段落级 BM25。

TextToCypherRetriever 仅图库支持 Cypher 时可用。任意生成的 Cypher 有注入与误删风险,生产用只读角色或改用 CypherTemplateRetriever(模板 + LLM 填参数)。

社区方案对照:LlamaIndex 官方走本对象 + Neo4jPropertyGraphStore;Microsoft GraphRAG 做社区摘要、LightRAG 做轻量图增强,API 与本类不是同一套。风险点相同:抽取噪声边会被多跳放大;实体未链接时「GAPDH」和「Gapdh」会变成两个节点。


5. 最小可运行示例

1
pip install -U llama-index-core llama-index-llms-ollama llama-index-embeddings-ollama
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
from typing import Literal

from llama_index.core import Document, PropertyGraphIndex, Settings
from llama_index.core.indices.property_graph import SchemaLLMPathExtractor
from llama_index.embeddings.ollama import OllamaEmbedding
from llama_index.llms.ollama import Ollama

Settings.llm = Ollama(model="qwen3.5:9b", request_timeout=180.0, temperature=0)
Settings.embed_model = OllamaEmbedding(
model_name="nomic-embed-text",
base_url="http://localhost:11434",
)

docs = [
Document(
text=(
"定量 PCR 以小鼠肝脏 GAPDH 为内参。"
"实验对象为 SPF 级 C57BL/6 小鼠。"
"用 siRNA knockdown GAPDH 后,糖酵解表型下降。"
),
doc_id="paper_001",
metadata={"species": "小鼠", "section": "methods"},
),
Document(
text="Python 的列表推导式用一行从可迭代对象生成列表。",
doc_id="py_001",
metadata={"species": "无关", "section": "lang"},
),
]

entities = Literal["GENE", "STRAIN", "METHOD", "PHENOTYPE"]
relations = Literal["USED_AS_CONTROL_IN", "USES_STRAIN", "KNOCKDOWN_AFFECTS"]
# 0.14 起可用 (主语类型, 关系, 宾语类型);内部会收成 {"relationships": [...]}
schema = {
"relationships": [
("GENE", "USED_AS_CONTROL_IN", "METHOD"),
("METHOD", "USES_STRAIN", "STRAIN"),
("GENE", "KNOCKDOWN_AFFECTS", "PHENOTYPE"),
]
}

kg_extractor = SchemaLLMPathExtractor(
llm=Settings.llm,
possible_entities=entities,
possible_relations=relations,
kg_validation_schema=schema,
strict=True,
max_triplets_per_chunk=8,
num_workers=1,
)

index = PropertyGraphIndex.from_documents(
docs,
kg_extractors=[kg_extractor],
show_progress=True,
)

retriever = index.as_retriever(include_text=True, similarity_top_k=4)
hits = retriever.retrieve("GAPDH knockdown 影响什么表型?")
print([(h.node.get_content()[:80], h.score) for h in hits])

qe = index.as_query_engine(include_text=True, similarity_top_k=4)
resp = qe.query("GAPDH knockdown 影响什么表型?实验对象是什么品系?")
print(resp)
for n in resp.source_nodes:
print("cite", n.node.metadata, n.score)
# 预期形态:答案含糖酵解与 C57BL/6;source_nodes 能指回 methods

本地小模型若 strict=True 抽空:改 strict=False,或换成默认的 SimpleLLMPathExtractor(不传 kg_extractors 即该组合)。图已在 Neo4j 时用 PropertyGraphIndex.from_existing(property_graph_store=...),不要对同一语料重复抽取。


6. 重要配置参数

参数(API 名) 类型 / 默认值 功能说明 作用与影响 参考起点 / 常用范围 配置指导
kg_extractors list / Simple + Implicit 每个 chunk 上的抽边器 不传则自由抽 + 切分邻接边;决定图的上限 领域稳定用 Schema 先小语料看抽出的边再全库
embed_kg_nodes bool / True 是否给图节点算向量 False 则默认子检索少掉 VectorContext 路 要语义别名时保持 True Settings.embed_model 绑定
include_text bool / 常用 True 路径是否回填源 chunk False 合成器只见短三元组 问答必须 True 只要图统计可 False
similarity_top_k int / 常为 2 向量路取几个图节点 过小漏种子;过大噪声边增多 4~8 path_depth 一起拧
path_depth int / 默认 1 命中后再走几跳 跳数↑召回↑、错边放大↑ 1~2 入门 抽取脏时先降到 1
max_paths_per_chunk / max_triplets_per_chunk int / 约 10 每块最多抽几条 过大费用高、噪声边多 5~10 方法节短块够用
strict bool / Schema 常用 True 是否丢掉模式外三元组 False 当建议,图会变杂 本体已定时 True 小模型抽空再放宽
property_graph_store 图库适配器 / Simple 图存在哪 Simple 无 Cypher、单机 JSON;Neo4j 可持久与向量 生产换 Neo4j 等 密钥走环境变量,禁止写进 YAML/示例明文

7. 适用 / 不适用

维度 适用 不适用
任务形态 多跳关系、要沿「基因→干预→表型」走路 单跳事实、同义改写——VectorStoreIndex + hybrid 更便宜
语料 实体类型稳定、改动不频繁 每天大面积改稿——全量重抽成本高于重嵌
工程阶段 已能接受 LLM 抽取误差,并准备做实体归一 必须精确命中货号/基因号——加 BM25 / metadata filter,不要指望抽边

工具环、审批、会话恢复:索引仍用本对象,编排走 LangGraph。


8. 易踩坑

  1. 把它当成 Microsoft GraphRAG:本类不自动做社区摘要;只提供 LPG 构建与子检索器。
  2. 继续用 KnowledgeGraphIndex:已弃用,新代码只用 PropertyGraphIndex
  3. include_text=False 还当 RAG 问答:合成器看不到品系、剂量等原文约束。
  4. 实体未归一:GAPDH / Gapdh / 甘油醛-3-磷酸脱氢酶变成三个节点,同义词检索只能部分兜住。
  5. SimplePropertyGraphStore 上开 TextToCypherRetriever:内存店没有 Cypher。
  6. 每次启动 from_documents 重抽:LLM 抽取比 embedding 更贵;persist 或 from_existing

小结

  • PropertyGraphIndex 把文档建成 LPG:抽取器写边,子检索器走路,include_text 回填 chunk。
  • 默认抽取是自由三元组 + 切分邻接边;领域稳定改 SchemaLLMPathExtractor
  • 默认检索是同义词 + 图节点向量,不是段落 BM25。
  • 引用看 source_nodes;生产图库换 Neo4j,索引 API 保持 from_documents / from_existing

参考链接

-------------本文结束感谢您的阅读-------------