数据截至 (上游 commit 059ecec2eeac)
图网络子系统:关系检索与主题建模
30 秒导读: 前几章里 txtai 是"把文本变向量、按相似度排序"的检索引擎。本章讲它多长出来的一只手——图网络:索引时不仅把每条记录变成一个向量,还顺手把"谁和谁相似"连成边,于是整个嵌入库变成一张图。之后你既能像走地图一样在记录间遍历(A 和 C 之间隔着谁?),也能对整张图跑 PageRank(网页排名算法,按"被重要节点引用"给节点打分)、社区发现和主题建模。
本章是 txtai 系列的第 6 章。它假设你已经知道嵌入库怎么把文本变向量、怎么排序检索(见 01-embeddings-database、04-search-and-fusion)。这里只讲"图这一面":边从哪来、图查询怎么跑、PageRank / 社区 / 主题拿来干嘛。向量化、ANN、SQL 的细节不重复,分别见 02 / 03 / 05。
1. 这是什么(零基础也能懂)
一句话定义: 图子系统让 txtai 在"向量检索"之外,额外维护一张节点=记录、边=相似关系的网络,并在这张网络上支持遍历查询、图算法和主题聚类。
它解决什么问题。 普通向量检索只回答一种问题:"跟这句话最像的 N 条是谁?"——一次打分、一个排序、结束。但很多真实需求是关系型的:
- 这两篇文档之间,靠哪些中间文档串起来?(路径)
- 整个语料里,哪些记录是"枢纽"、删了会让知识断裂?(中心性 / PageRank)
- 这堆记录能自动分成几个主题簇,每簇讲什么?(社区 / 主题建模)
这些问题"排序一个 top-N"答不了,得把记录之间的连接显式建出来。图子系统就是干这个的。
一句话直觉。 把向量索引想成"每本书的内容摘要卡",图子系统则是在卡片之间拉线:两张卡内容够像就连一根线,线的粗细 = 相似度。连完之后,这摞卡片就从"一叠卡"变成了"一张地图",可以顺着线走、可以找枢纽、可以圈出聚集区。
用起来什么样(最小示例):
# 示意,非源码:开启 graph,txtai 索引时自动建边
from txtai import Embeddings
embeddings = Embeddings(
path="sentence-transformers/all-MiniLM-L6-v2",
content=True,
functions=[{"name": "graph", "function": "graph.attribute"}],
graph={"approximate": True, "topics": {}}, # 开启图 + 主题建模
)
embeddings.index(rows) # 建向量索引的同时,自动建图
# 图查询:走 openCypher(图数据库查询语言),similar() 先做向量检索再进图
g = embeddings.search("""
MATCH P=({id: "start"})-[*1..3]->({id: "end"})
RETURN P
LIMIT 5
""", graph=True)
print(g.pagerank()) # 每个节点的重要性打分
print(g.showpath("a", "b")) # 两点间最短路径
关键点:开发者不手动连边。你照常 index(),只要配了 graph,txtai 就在建向量索引之后自动把相似度关系连成图。
2. 顶层全景(它大概怎么转)
图子系统住在 txtai/graph/ 下,对外由 Embeddings 编排类驱动。它由五个部件组成:
| 部件 | 干什么 | 在哪个文件 |
|---|---|---|
Graph(基类) | 定义全部图操作(建边、遍历、算法、主题),是抽象接口 + 通用逻辑 | graph/base.py:11 |
NetworkX | 默认后端,把图存在内存里(NetworkX 库) | graph/networkx.py:28 |
RDBMS | 把图落到关系数据库(SQL 表),支持超大图 / 持久化 | graph/rdbms.py:22 |
Query | 把 openCypher 图查询解析、执行,处理 similar() 混合子句 | graph/query.py:19 |
Topics | 用社区发现做主题建模,给每个簇起名 | graph/topics.py:9 |
GraphFactory | 按 config 里的 backend 选后端 | graph/factory.py:12 |
两条主线怎么走。 一是建图(索引时),二是用图(查询时):
┌─────────────── 建图(index 时)───────────────┐
文本记录 ──向量化/索引──▶ 向量索引就绪
│
│ ① graph.insert():每条记录 = 一个节点
▼
[ 一堆孤立节点 ]
│ ② graph.index():用"相似度搜索"当探针
│ 每个节点找自己的近邻 → 连成边
▼
[ 节点 + 相似度边的图 ] ──③ 主题建模 → 主题簇
│
┌─────────────── 用图(search 时)──────────────┐
│
图查询字符串 ──▶ isquery? ──是──▶ graphsearch():
(MATCH...RETURN) 先 index scan 解析 similar()
再交给 graph.batchsearch() 走图
│
▼
路径 / 子图 / PageRank / 社区
怎么读这张图:上半"建图"发生在 index() 里,是全自动的;下半"用图"发生在 search() 里,靠识别出这是一条图查询后分流过去。两半的接缝是同一个相似度搜索函数——建边靠它,查询里的 similar() 也靠它。
3. 核心机制一:索引时,相似度怎么变成边
这是整个子系统最核心的一步:边不是人连的,是"跑一遍相似度检索"推断出来的。
3.1 三步走:插入节点 → 推断边 → 建主题
Graph.index() 是建图总入口,只有三步(graph/base.py:468 index):
# 真实源码摘录 graph/base.py:478-486
self.resolverelations(ids) # ① 先落地"手动指定"的关系边
self.inferedges(self.scan(), search) # ② 再用 search 函数为每个节点推断相似度边
if "topics" in self.config:
self.addtopics(similarity) # ③ 最后做主题建模
在这之前,节点已经由 Graph.insert() 逐条塞进来了(graph/base.py:395):每条记录取出 text/object 字段当节点数据,id 和要复制的属性挂上去,先攒成 nodes 一次性 addnodes。此刻图里只有孤立节点、还没有边。