不止于相似度:混合搜索如何重塑 RAG 的未来
1. 引言:当“最相似”不再是“最相关”
在构建检索增强生成(RAG)系统时,我们常常陷入一个困境:如何确保检索到的上下文既“语义相关”又“关键词精确”?
想象一下这个场景:
- 当用户搜索“苹果公司发布的 M3 芯片评测”时,一个纯粹依赖向量搜索的 RAG 系统可能会返回一篇关于“苹果公司最新财报”的文章。从语义上看,这没错,两者都与“苹果公司”高度相关。但用户最关心的核心关键词——“M3 芯片”——却被忽略了。
- 反过来,当用户搜索“好用的笔记本电脑”时,一个传统的关键词搜索引擎(如 BM25)可能会因为无法理解“好用”这个主观词汇,或者因为“笔记本电脑”这个词在太多文档中出现,而返回一大堆不相关的结果。它无法领会用户寻找“高性能”、“轻薄”或“长续航”的真实意图。
这两种情况都指向了一个核心问题:单纯依赖一种搜索范式,无论是基于稠密向量的语义搜索,还是基于稀疏向量的关键词搜索,都有其局限性。
为了解决这个问题,**混合搜索(Hybrid Search)**应运而生。它并非简单的两者叠加,而是通过智能地融合两种搜索范式的结果,实现 1 + 1 > 2
的效果,正在成为构建下一代高质量 RAG 应用的关键。
2. 搜索的双引擎:从传统关键词到智能稀疏向量
要理解混合搜索,我们首先要了解它的两个核心引擎。
第一类引擎:关键词匹配 (Keyword Matching)
这类引擎的核心是找到与查询词完全匹配或高度相关的文档。
传统方法:BM25 / TF-IDF
这是经典的、基于统计的关键词搜索算法。它们通过计算词频(Term Frequency)和逆文档频率(Inverse Document Frequency)来评估一个词在一个文档中的重要性。简单来说,一个词在一个文档中出现次数越多,但在所有文档中越稀有,它的权重就越高。
- 优点: 技术成熟,计算速度快,对于包含专业术语、产品型号、人名等精确查询非常有效。
- 缺点: 无法理解同义词或上下文,存在“词汇鸿沟”问题。
现代方法:模型生成的稀疏向量 (Learned Sparse Vectors)
这是对传统关键词搜索的一次“智能升级”。像 SPLADE、BGE-M3-Sparse 这样的模型,通过深度学习来生成一个高维但大部分值为零的“稀疏向量”。
- 是什么: 这个稀疏向量的非零值代表了文档中最重要的词汇及其“概念权重”。与 BM25 纯粹基于词频不同,模型能够理解词汇在特定上下文中的重要性。
- 为什么更好: 它不仅能匹配关键词,还能进行一定程度的语义扩展。例如,模型可能知道 “cpu” 和 “processor” 是高度相关的,并会给它们赋予相似的权重。这在一定程度上弥合了“词汇鸿沟”。
第二类引擎:语义理解 (Semantic Understanding)
这类引擎的目标是理解查询背后的深层意图。
稠密向量 (Dense Vectors)
通过 Sentence Transformers 这类模型,我们可以将文本转换成一个几百维的“稠密向量”(Dense Vector)。这个向量可以被看作是文本在语义空间中的一个坐标。
- 工作原理: RAG 系统将用户的查询也转换成一个向量,然后在向量数据库中通过近似最近邻(ANN)搜索,找到与之“距离”最近的文档向量。
- 优点: 能够轻松跨越“词汇鸿沟”,理解同义词、近义词和上下文。对于模糊、口语化的查询非常有效。
- 缺点: 正如引言中的例子,它有时会因为过于关注整体语义而忽略掉关键的、决定性的词汇。
总结对比
特性 | BM25 / TF-IDF | 模型稀疏向量 (SPLADE) | 稠密向量 (Embeddings) |
---|---|---|---|
核心原理 | 词频统计 | 语言模型生成权重 | 语义空间映射 |
向量类型 | 稀疏 | 稀疏 | 稠密 |
优点 | 速度快,精确匹配 | 精确且有语义扩展 | 理解意图,处理模糊查询 |
缺点 | 词汇鸿沟,不理解语义 | 计算开销较大 | 可能忽略关键词 |
最适用场景 | 专业术语、代码搜索 | 需要精确匹配但又希望有一定语义灵活性的场景 | 问答、对话、概念搜索 |
3. 混合搜索的核心:结果融合 (Result Fusion)
当我们从稀疏和稠密两种搜索中各得到一个按相关性排序的文档列表后,如何将它们合并成一个更优的列表?这就是结果融合的艺术。
最简单的方法是加权平均,但它依赖于两种搜索返回的、不可直接比较的分数,效果往往不佳。
目前,业界最推崇的方法之一是 倒数排名融合 (Reciprocal Rank Fusion, RRF)。
- 核心思想: RRF 不关心原始的相关性分数,只关心文档在每个列表中的排名。一个文档在任何一个列表里排名越高,它的最终得分就越高。
- 计算公式:
Score(doc) = Σ (1 / (k + rank_i))
其中,rank_i
是文档在第i
个搜索结果列表中的排名,k
是一个小的平滑常数(通常设为 60),用于降低排名靠后结果的权重。 - 为什么有效: RRF 非常鲁棒且简单。它优雅地绕开了归一化不同搜索引擎得分的难题,让排名决定一切,使得两种完全不同的搜索范式可以公平地“投票”。
4. 动手实践:用 Python 实现一个简单的混合搜索
下面,我们用 rank-bm25
和 sentence-transformers
库来模拟一个混合搜索过程。
# 安装必要的库
# pip install rank-bm25 sentence-transformers scikit-learn
import numpy as np
from rank_bm25 import BM25Okapi
from sentence_transformers import SentenceTransformer
from sklearn.metrics.pairwise import cosine_similarity
# 1. 准备数据
documents = [
"Apple Inc. announced the new M3 chip, focusing on performance and efficiency.",
"The latest financial report from Apple Inc. shows strong growth in the services sector.",
"A detailed review of the MacBook Pro with M3 chip highlights its impressive speed.",
"Google's new Pixel phone features an advanced AI-powered camera.",
"How to bake the perfect apple pie from scratch.",
"Microsoft's Surface Laptop competes directly with Apple's MacBook Air."
]
# 2. 创建双索引
# 稀疏索引 (BM25)
tokenized_docs = [doc.lower().split() for doc in documents]
bm25 = BM25Okapi(tokenized_docs)
# 稠密索引 (Sentence Transformer)
model = SentenceTransformer('all-MiniLM-L6-v2')
doc_embeddings = model.encode(documents)
# 3. 执行搜索
query = "latest review of Apple's M3 chip"
tokenized_query = query.lower().split()
# BM25 搜索
bm25_scores = bm25.get_scores(tokenized_query)
# 向量搜索
query_embedding = model.encode(query)
cosine_scores = cosine_similarity([query_embedding], doc_embeddings)[0]
# 4. 实现 RRF 融合
def reciprocal_rank_fusion(search_results_list, k=60):
fused_scores = {}
for doc_scores in search_results_list:
# 对每个搜索结果列表按分数降序排序,获取排名
sorted_docs = sorted(doc_scores.items(), key=lambda item: item[1], reverse=True)
for rank, (doc_index, score) in enumerate(sorted_docs):
if doc_index not in fused_scores:
fused_scores[doc_index] = 0
fused_scores[doc_index] += 1 / (k + rank + 1) # rank 从 0 开始,所以 +1
# 按 RRF 分数重新排序
reranked_results = sorted(fused_scores.items(), key=lambda item: item[1], reverse=True)
return reranked_results
# 准备 RRF 输入
bm25_results = {i: score for i, score in enumerate(bm25_scores)}
vector_results = {i: score for i, score in enumerate(cosine_scores)}
fused_results = reciprocal_rank_fusion([bm25_results, vector_results])
# 5. 展示结果
print("--- Query ---")
print(query)
print("
--- BM25 (Keyword) Search Results ---")
for i, score in sorted(bm25_results.items(), key=lambda item: item[1], reverse=True):
print(f"Score: {score:.4f} Doc: {documents[i]}")
print("
--- Vector (Semantic) Search Results ---")
for i, score in sorted(vector_results.items(), key=lambda item: item[1], reverse=True):
print(f"Score: {score:.4f} Doc: {documents[i]}")
print("
--- Hybrid Search (RRF Fused) Results ---")
for doc_index, score in fused_results:
print(f"Score: {score:.4f} Doc: {documents[doc_index]}")
运行结果分析:
- BM25 会把包含 “M3” 和 “chip” 的文档排在最前面。
- 向量搜索 会把与 “Apple” 和 “review” 语义相关的文档排在前面,可能会包含那篇财报。
- 混合搜索 的结果则会是最好的:包含 “M3 chip” 的评测文章会因为在两个列表中都排名靠前(或至少在 BM25 中排名极高)而获得最高的 RRF 分数,从而脱颖而出。
5. RAG 的进化:为什么混合搜索是关键?
将混合搜索集成到 RAG 系统中,带来的不仅仅是检索精度的提升:
- 更高质量的上下文: LLM 获取的上下文将同时包含关键词精确和语义相关的信息,使其能够生成更全面、更准确的答案。
- 显著减少“幻觉”: 高质量、高相关的上下文是减少 LLM“凭空捏造”的根本。当模型有了坚实的信息基础,它就不需要去猜测和编造。
- 提升用户体验: 无论用户输入的是精确的技术术语还是模糊的日常问题,RAG 系统都能给出更可靠、更令人满意的回答,系统的鲁棒性和适用性大大增强。
6. 结论与展望
混合搜索并非一个复杂的概念,但它通过智能地融合稀疏和稠密两种搜索范式,精准地解决了各自的短板,让搜索结果的质量产生了质的飞跃。它不再是锦上添花,而是正在成为构建下一代强大、可靠 RAG 应用的核心组件。
展望未来,我们可能会看到更智能的、能够根据查询意图自适应调整融合权重的策略出现。但就目前而言,掌握并应用混合搜索,无疑是每一位 AI 应用开发者都应该具备的关键能力。