引言:混合搜索的必要性
在现代检索增强生成(RAG)和语义搜索应用中,纯粹的向量搜索(基于语义相似度)和纯粹的关键词搜索(基于词汇匹配,如BM25)都有其局限性。向量搜索可能遗漏关键词精确匹配的文档,而关键词搜索则无法捕获深层次的语义关系。
混合搜索(Hybrid Search)正是为了结合两者的优势而生。对于AI基础设施工程师而言,理解向量数据库如何原生支持这种查询结构以及如何进行结果融合(即权重分配)至关重要。
Milvus对混合搜索的原生支持
Milvus作为一个高性能的向量数据库,其核心设计支持在同一查询中结合向量搜索和结构化过滤(或关键词搜索)。
在Milvus或Zilliz的体系中,实现混合搜索通常有两种方式:
- 结构化过滤与向量搜索的结合: 利用 Milvus 的 expr 参数对非向量字段(如ID、类别、标签)进行过滤。这更侧重于布尔逻辑,而非关键词评分。
- 向量搜索与全文本搜索(FTS)的结合: 新一代的云服务或集成方案(如Zilliz Cloud或某些扩展)提供了对文本字段的索引支持,例如基于全文索引实现类似 BM25 的查询能力。
无论是哪种方式,最终都会产生两个独立的排名列表:一个是基于向量距离的排名,另一个是基于关键词/BM25得分的排名。
权重分配的核心挑战
挑战在于如何公平地融合这两个异构的得分系统:
- 向量距离(如L2或Cosine)是度量空间中的相似度,通常范围在 [0, 1] 或 [0, $\infty$)。
- BM25 得分通常是一个正数,没有固定的上限,其数值大小与集合大小和文档频率有关。
由于它们的量纲和分布差异巨大,简单地进行线性加权求和(e.g., $Score = w_{vector} \cdot VectorScore + w_{keyword} \cdot KeywordScore$)需要复杂的归一化步骤和繁琐的权重调优。
结果融合的黄金标准:Reciprocal Rank Fusion (RRF)
解决异构得分融合问题的最有效且最少参数依赖的方法是 倒数排名融合(Reciprocal Rank Fusion, RRF)。
RRF 是一种排名聚合算法,它完全忽略了原始分数,只关注文档在各个搜索结果列表中的排名。
RRF 的工作原理与公式
RRF 假设如果一个文档在多个独立搜索中的排名都很靠前,那么它应该是一个高质量的结果。RRF 的计算公式如下:
$$RRF_Score(d) = \sum_{i=1}^{N} \frac{1}{k + rank_i(d)}$$
其中:
- $d$ 是文档。
- $N$ 是搜索列表的数量(在这里 $N=2$,即向量和关键词)。
- $rank_i(d)$ 是文档 $d$ 在第 $i$ 个搜索结果列表中的排名(从1开始)。
- $k$ 是一个平滑常数(通常取 60),用于防止排名第一的文档得分过高,并给予后续排名文档一定的权重。
实践:使用 Python 实现 RRF 融合
虽然更先进的 VDB 平台在内部封装了 RRF 或类似的融合逻辑,但了解其实现原理和在客户端手动融合结果是至关重要的实操技能。
假设我们已经通过 Milvus 执行了两个独立的查询,得到了两组结果:
import pandas as pd
# 假设的向量搜索结果 (ID, 排名)
vector_results = [
{'id': 101, 'rank': 1},
{'id': 103, 'rank': 2},
{'id': 105, 'rank': 3},
{'id': 102, 'rank': 4},
]
# 假设的关键词搜索结果 (ID, 排名)
keyword_results = [
{'id': 102, 'rank': 1},
{'id': 101, 'rank': 2},
{'id': 104, 'rank': 3},
{'id': 106, 'rank': 4},
]
# RRF 平滑常数
K = 60
def calculate_rrf(results_lists, K=60):
"""根据多个排名列表计算 RRF 分数"""
# 1. 存储所有文档的累积 RRF 分数
rrf_scores = {}
# 2. 遍历每个搜索列表
for rank_list in results_lists:
for item in rank_list:
doc_id = item['id']
rank = item['rank']
# 计算该文档在该列表中的 RRF 贡献
score_contribution = 1 / (K + rank)
# 累加分数
if doc_id not in rrf_scores:
rrf_scores[doc_id] = 0
rrf_scores[doc_id] += score_contribution
# 3. 按分数降序排列结果
final_ranking = sorted(rrf_scores.items(), key=lambda item: item[1], reverse=True)
# 4. 格式化输出
df = pd.DataFrame(final_ranking, columns=['Document ID', 'RRF Score'])
df['Final Rank'] = df.index + 1
return df
# 执行 RRF 融合
final_ranking_df = calculate_rrf([vector_results, keyword_results], K=60)
print("RRF 融合结果:\n")
print(final_ranking_df.to_markdown(index=False))
输出结果(示例):
| Document ID | RRF Score | Final Rank |
|-------------|-----------|------------|
| 101 | 0.032787 | 1 |
| 102 | 0.032517 | 2 |
| 103 | 0.016393 | 3 |
| 105 | 0.016129 | 4 |
| 104 | 0.016129 | 5 |
| 106 | 0.016129 | 6 |
权重分配逻辑总结
在 RRF 机制下,权重分配的逻辑体现在排名的倒数上。文档 101(向量排名1,关键词排名2)和文档 102(向量排名4,关键词排名1)都获得了很高的综合分数,因为它们都在至少一个列表中取得了前列的成绩。RRF 的优势在于:
- 非参数化: 无需手动设置 $w_{vector}$ 和 $w_{keyword}$ 等权重。
- 抗噪性强: 避免了因为某个分数维度异常高而主导最终结果的情况。
- 高度公平: 给予在所有搜索列表中表现稳定的文档更高的综合排名。
汤不热吧