当您同时拥有独立运行的语义搜索(向量嵌入)和词法搜索(BM25)时,下一步就是将它们组合成一个统一的搜索管道。这种混合方法利用了两种方法的优势,从而提供更准确的结果。

构建统一接口
两种搜索实现共享几乎相同的 API——它们都有 add_document() 和 search() 方法。这种一致性使得将它们封装在一个单一的 Retriever 类中变得非常简单,该类负责协调这两种方法。

Retriever 充当协调者,负责:
- 接收用户的问题
- 将其转发给 VectorIndex 和 BM25Index
- 收集来自两个系统的结果
- 使用排名算法合并结果

倒数排名融合
挑战在于合并来自不同搜索方法的结果。每个系统返回的结果使用不同的评分机制,因此您不能简单地直接合并分数。相反,我们使用一种称为倒数排名融合(Reciprocal Rank Fusion,RRF)的技术。

下面通过一个实际示例来说明 RRF 的工作原理。假设您的 VectorIndex 返回的结果排名为:Section 2、Section 7、Section 6。同时,您的 BM25Index 返回:Section 6、Section 2、Section 7。

要合并这些结果,您需要创建一个组合表,显示每个文本块在两个系统中的排名:

RRF 公式为每个文档计算一个分数:
RRF_score(d) = Σ(1 / (k + rank_i(d)))其中 k 是一个常数(通常为 60,不过 1 在结果更清晰时效果也很好),rank_i(d) 是文档 d 在第 i 个排名系统中的排名。

对于每个文本块,您可以计算:
- Section 2:1.0/(1+1) + 1.0/(1+2) = 0.833
- Section 7:1.0/(1+2) + 1.0/(1+3) = 0.583
- Section 6:1.0/(1+3) + 1.0/(1+1) = 0.75
按分数排序后,最终排名为:Section 2(第一)、Section 6(第二)、Section 7(第三)。

实现
Retriever 类的实现非常简单:
class Retriever:
def __init__(self, *indexes):
self._indexes = list(indexes)
def add_document(self, document):
for index in self._indexes:
index.add_document(document)
def search(self, query_text, k=1, k_rrf=60):
# 从所有索引获取结果
all_results = []
for idx, results in enumerate(all_results):
for rank, (doc, _) in enumerate(results):
# 跨系统跟踪文档排名
# 应用 RRF 公式
# 返回合并并排序后的结果
关键的洞见在于,RRF 算法通过考虑每个文档在所有搜索系统中的表现来创建统一的排名,而不是依赖任何单一的评分方法。
测试混合方法
当使用类似"what happened with INC-2023-Q4-011?"这样的查询进行测试时,混合方法提供的结果明显优于单独使用任一方法。您不再会从纯向量搜索中得到意外的结果,而是首先获得最相关的网络安全事件报告,随后是相关的软件工程内容。

可扩展性
这种设计的精妙之处在于其模块化。由于每个搜索索引都实现了相同的接口(add_document() 和 search()),您可以轻松地向系统添加新的搜索方法。无论是不同的嵌入模型、专门的领域搜索,还是任何其他检索技术,只要遵循既定的 API,它就能无缝集成到混合管道中。

这种混合搜索方法通过将向量搜索的语义理解与词法搜索的精确关键词匹配相结合,并通过数学上严谨的 RRF 排名算法统一起来,在检索准确性方面实现了显著的提升。