LlamaIndex 检索 — 给 query 召回相关 Node
这章讲什么: 在线查询的前半段——拿到一个问题,怎么从索引里捞出最相关的一批 Node。先看最常用的向量检索(含一个容易忽略的「docstore 回填」步骤),再 看
BaseRetriever在所有检索器之上统一加的递归检索,最后看把多个检索器结果合并的多路融合(RRF)。
3.1 检索的统一外壳:BaseRetriever
所有检索器都继承 BaseRetriever,对外只暴露 retrieve(query)(base/base_retriever.py:191-227)。它做三件事:
retrieve(q):
1. 规范化 query(str → QueryBundle)
2. nodes = self._retrieve(q) # 子类实现的真正召回
3. nodes = _handle_recursive_retrieval(q, nodes) # 统一的递归展开 + 去重
return nodes
子类只需实现 _retrieve(base/base_retriever.py:262-269),其余编排(回调、事件、递归、去重)由基类包好。这是典型的模板方法模式:把可变的「怎么召回」留给子类,把不变的「召回前后怎么处理」收在基类。
3.2 向量检索:VectorIndexRetriever
最常用的 _retrieve 实现(indices/vector_store/retrievers/retriever.py)。一次召回分几步:
query_str
│ ① 嵌入 query(若该模式需要) _retrieve / _aretrieve
▼
query_embedding
│ ② 构造 VectorStoreQuery(top_k/filters/mode) _build_vector_store_query
▼
vector_store.query(...) ──▶ ids / nodes / similarities
│ ③ docstore 回填(见下) _determine_nodes_to_fetch + _insert_fetched_nodes_into_query_result
▼
NodeWithScore[] (node + similarity) _convert_nodes_to_scored_nodes
- ① 嵌入 query:
_needs_embedding()判断当前模式是否要向量(纯文本/稀疏检索就不需要),需要才把 query 文本转成向量(retriever.py:92-115)。 - ② top-k 查询:
similarity_top_k默认是DEFAULT_SIMILARITY_TOP_K(retriever.py:45),连同元数据过滤filters、模式mode(default / hybrid / sparse / text_search)一起打包成VectorStoreQuery(retriever.py:130-144)。 - ③ docstore 回填(关键细节):见 3.3。
- ④ 打分:把向量库返回的
similarities贴到每个 Node 上,封成NodeWithScore(retriever.py:212-225)。
3.3 容易忽略的「docstore 回填」
向量库有两类:存文本的(查回来直接带正文)和只存向量的(查回来只有 id)。检索器要兼容两者,于是有一步「按需回 docstore 取正本」(retriever.py:146-210、:227-246):
vector_store 查询结果
├─ 有 nodes(库存了文本):只把「非文本」节点(image/index 等)回 docstore 取完整体
└─ 只有 ids(库不存文本):全部 id 回 docstore 取 Node 正本
└─ 再按 id 拼回结果,顺序对齐
_determine_nodes_to_fetch(retriever.py:146-170)决定取哪些,_insert_fetched_nodes_into_query_result(retriever.py:172-210)把取回的正本替换/插回结果。为什么重要: 这解释了为什么「向量库存不存文本」会影响 docstore 是否必需,以及上一章里 VectorStoreIndex 为何只在「库不存文本或显式 override」时才把 Node 也写进 docstore(indices/vector_store/base.py:194-217)。
3.4 递归检索:检索结果里藏着「下一跳」
有时一个 Node 不是答案文本,而是**「去问另一个检索器/查询引擎」的指针**——这就是 IndexNode(指向 obj 或 index_id)。BaseRetriever._handle_recursive_retrieval(base/base_retriever.py:121-153)在每次召回后扫一遍结果:
对每个召回的 node:
是 IndexNode 且能解析出 obj?
├─ obj 是 Retriever → 进去再 retrieve(query)
├─ obj 是 QueryEngine→ 进去 query(query),把答案当一个 TextNode
└─ obj 是 Node/NodeWithScore → 直接展开
否则:原样保留
最后:按 node_id 去重
展开规则在 _retrieve_from_object(base/base_retriever.py:68-95)。这让你能搭多级检索:比如先检索「文档摘要」,命中的摘要节点再跳进「该文档的细粒度检索器」。去重用 seen 集合按 node_id 过滤(base/base_retriever.py:146-153)。
3.5 多路融合:QueryFusionRetriever 与 RRF
实战里常想同时用多个检索器(如向量 + 关键词 / 改写出的多个 query),再把结果合并。QueryFusionRetriever(retrievers/fusion_retriever.py)的 FUSION_MODES 枚举(fusion_retriever.py:24-30)共有四个值:reciprocal_rerank、relative_score、dist_based_score,外加一个 simple(fusion_retriever.py:30,仅按各检索器的原始分数重排、不做跨尺度归一)。真正用到「融合算法」的是前三种;simple 严格说只是重排,故下文聚焦前三种。
最经典的是 倒数排名融合(Reciprocal Rank Fusion, RRF)——一种「只看名次、不看原始分数尺度」的合并法,适合融合打分尺度不可比的检索器(向量余弦 vs BM25)。核心(retrievers/fusion_retriever.py:113-148):
# 示意,非源码:RRF 的核心,对应 _reciprocal_rerank_fusion
k = 60.0 # 论文推荐常数,抑制头部名次的过度主导
fused = {}
for result_set in all_results: # 每个检索器一份结果
for rank, nws in enumerate(sorted(result_set, key=score, reverse=True)):
fused[nws.node.hash] = fused.get(nws.node.hash, 0) + 1.0 / (rank + k)
# 按 fused 分数降序,即融合后的排名
直觉:一个 Node 在多个检索器里都排得靠前,累加的 1/(rank+k) 就高。用名次(rank)而非原始相似度,绕开了「向量分数和 BM25 分数不在一个量纲」的难题。常数 k=60 出自 Cormack 等人的 RRF 论文(代码 注释直接给了链接,fusion_retriever.py:119-122)。
另外两种模式 _relative_score_fusion(min-max 归一化后按权重加权,:150-)/ dist-based(用均值±3σ 当 min/max)适合同尺度但想加权的场景。
3.6 巧妙之处
- 模板方法收敛横切关注点:递归展开、去重、回调事件全在
BaseRetriever.retrieve里,子类只写_retrieve(base/base_retriever.py:191-269)。 - docstore 回填让「向量库存不存文本」对上层透明——同一套检索器同时兼容两类后端(
retriever.py:146-210)。 - RRF 用名次而非分数,天然适配混合检索(hybrid),不需要校准不同检索器的分数尺度(
fusion_retriever.py:113-148)。 - IndexNode 递归把「分层检索」做成数据驱动:不改检索器代码,靠在索引里放指针就能多跳(
base/base_retriever.py:121-153)。
3.7 代码地图
| 主题 | 文件 | 符号 |
|---|---|---|
| 检索基类 / 模板方法 | base/base_retriever.py | BaseRetriever.retrieve, _retrieve |
| 递归检索 | base/base_retriever.py | _handle_recursive_retrieval, _retrieve_from_object |
| 向量检索主流程 | indices/vector_store/retrievers/retriever.py | VectorIndexRetriever._get_nodes_with_embeddings |
| docstore 回填 | indices/vector_store/retrievers/retriever.py | _determine_nodes_to_fetch, _insert_fetched_nodes_into_query_result |
| 查询构造 | indices/vector_store/retrievers/retriever.py | _build_vector_store_query |
| 多路融合 | retrievers/fusion_retriever.py | QueryFusionRetriever, FUSION_MODES |
| RRF | retrievers/fusion_retriever.py | _reciprocal_rerank_fusion |
| 相对/距离融合 | retrievers/fusion_retriever.py | _relative_score_fusion |