混合搜索:向量 + 关键词
当向量搜素不够用时,你可以搭配BM25关键词搜素。
微信 ezpoda免费咨询:AI编程 | AI模型微调| AI私有化部署
AI模型价格对比 | AI工具导航 | ONNX模型库 | Tripo 3D | Meshy AI | ElevenLabs | KlingAI | ArtSpace | Phot.AI | InVideo
向量搜索很强大。你将查询和文档块转换为密集Embedding,测量余弦相似度,并返回最匹配的结果。它捕获语义含义——搜索"输出电压"会找到关于"Vout规格"的块,即使没有共享关键词。
但它有一个关键的盲点。
问:"第34页上R_COMP的值是多少?"
向量搜索会找到语义相似的关于补偿、电阻和组件的块。但R_COMP是一个特定的技术符号。它在像all-MiniLM-L6-v2这样的通用模型中的Embedding可能无法准确表示它——模型从未见过这个特定IC的文档。你可能会错过 exact page。
这就是BM25的用武之地。
1、两种搜索范式
┌─────────────────────────────────────────────────────────────────┐
│ 搜索范式比较 │
│ │
│ 向量搜索(FAISS) BM25关键词搜索 │
│ ───────────────────── ────────────────── │
│ │
│ 查询──► Embed──► [0.2, 0.8, ...] 查询──► 分词 │
│ │ │ │
│ 块──► Embed──► 矩阵 块──► 词频 │
│ │ │ │
│ 余弦相似度 ◄──────┘ BM25分数 ◄┘ │
│ │
│ 发现:语义相似 发现:精确术语匹配 │
│ 遗漏:稀有精确术语 遗漏:改写 │
│ │
└─────────────────────────────────────────────────────────────────┘
- BM25——智能关键词搜索
BM25(最佳匹配25)是大多数经典搜索算法(包括Elasticsearch默认)的算法。它根据三个因素对文档进行排名:
- 因素1——词频(TF)
查询词在这个块中出现多少次?更多出现=更高相关性(有递减回报)。
- 因素2——逆文档频率(IDF)
这个词在所有块中稀有吗?稀有术语获得更高权重。像"the"、"is"、"a"这样的常见词获得接近零的权重。
- 因素3——文档长度归一化
长块被惩罚,以防止它们仅因有更多文本而获胜。
BM25公式:
Score(query, doc) = Σ IDF(t) × [TF(t,d) × (k1+1)] / [TF(t,d) + k1 × (1 - b + b × |d|/avgdl)]
其中:
k1 = 1.5——控制TF饱和b = 0.75——控制长度归一化|d|——文档长度avgdl——语料库中平均文档长度
2、仅使用单一搜索的问题
让我们用真实查询示例来具体说明:
查询:"软启动期间的静态电流是多少?"
- 向量搜索:找到关于"启动行为"、"电流限制"、"上电序列"的块——好的语义匹配
- BM25:"静态"可能不会出现在附近块中——差的关键词匹配
- 获胜者:向量搜索
查询:"ISL81801 UVLO阈值是多少?"
- 向量搜索:
ISL81801是特定零件号,UVLO是特定首字母缩写。这些稀有token的Embedding可能很弱 - BM25:找到
ISL81801和UVLO的精确匹配——直接关键词命中 - 获胜者:BM25
没有一个能总是获胜。你需要两者。
3、倒数排名融合(RRF)
RRF将两个排名列表合并为一个,无需跨不同系统标准化分数。
def reciprocal_rank_fusion(rank_lists, k=60):
scores = {}
for rank_list in rank_lists:
for rank, chunk in enumerate(rank_list):
chunk_id = chunk["chunk_id"]
# RRF分数: 1 / (k + rank_position)
# k=60是防止排名1占主导的平滑常数
score = 1 / (k + rank + 1)
if chunk_id not in scores:
scores[chunk_id] = {"chunk": chunk, "score": 0}
# 从所有排名列表累积分数
scores[chunk_id]["score"] += score
# 按累积的RRF分数排序
sorted_chunks = sorted(
scores.values(),
key=lambda x: x["score"],
reverse=True
)
return [x["chunk"] for x in sorted_chunks]
为什么RRF有效?
示例:3个块,2个搜索系统(k=60):
FAISS 排名: BM25 排名:
排名1: 块A 排名1: 块B
排名2: 块B 排名2: 块A
排名3: 块C 排名3: 块D
RRF分数 (k=60):
块A: 1/(60+1) + 1/(60+2) = 0.01639 + 0.01613 = 0.03252
块B: 1/(60+2) + 1/(60+1) = 0.01613 + 0.01639 = 0.03252
块C: 1/(60+3) + 0 = 0.01587 + 0 = 0.01587
块D: 0 + 1/(60+3) = + 0.01587 = 0.01587
最终排名:
1. 块A(并列)
2. 块B(并列)
3. 块C / 块D
在两个列表中出现的块获得双倍积分。只在一个列表中的块仍然获得部分积分——它们不会被丢弃。
4、完整的混合检索流程
用户查询:"UVLO阈值电压是多少?"
│
┌──────────┴──────────┐
│ │
▼ ▼
FAISS向量搜索 BM25关键词搜索
(top_k=5) (top_k=5)
│ │
│ [第3页] │ [第3页] ←两者一致
│ [第12页] │ [第7页]
│ [第7页] │ [第15页]
│ [第19页] │ [第19页]
│ [第34页] │ [第34页] ←两者一致
│ │
└──────────┬──────────┘
│
▼
RRF融合 (k=60)
│
[第3页] ←顶部(两者都出现,高排名)
[第34页] ←强(两者,中排名)
[第7页]
[第12页]
[第19页]
[第15页]
│
▼
Cross-encoder重排器
(最终top-3选择)
在两个搜索运行之前,查询使用领域特定的同义词进行扩展:
SYNONYMS = {
"input voltage": "vin input supply voltage operating voltage",
"output voltage": "vout output regulation output range",
"switching frequency": "fsw oscillator frequency",
"efficiency": "efficiency power conversion efficiency",
}
for key, value in SYNONYMS.items():
if key in query:
query += " " + value
这为BM25提供更多匹配术语,为向量编码器提供更丰富的语义输入。
5、结束语
关键洞察:混合搜索在术语查询上比纯向量搜索提供约15%的MRR改进。BM25处理那些让Embedding模型犯错的精确缩写和零件号。
原文链接:Hybrid Search: Why Vector Search Alone Is Not Enough汇智网翻译整理,转载时请标明出处