混合搜索:当关键字和向量手拉手成为好朋友

周三下午的产品 demo 进行到一半,会议室里气氛尴尬得能拧出水。老李站在大屏幕前,手指僵在键盘上。

“就搜‘API-2024-03 接口变更说明’,这不过分吧?”老李的声音压着火,“我们自己的文档,标题就叫《API-2024-03 接口变更说明》,结果你们这系统给我返回什么?‘API 版本管理最佳实践’、‘2024 年 3 月产品更新概览’、‘接口设计规范 v2.1’——我要的合同变更说明呢?第四页了都没看到!”

他转向角落里的小王,保温杯差点戳到屏幕上:“这 AI 怎么连搜索都不如?”

小王不慌不忙地合上笔记本:“老李你想啊,纯向量搜索有个天生的盲区——它分不清‘API-2024-03’是一串有精确含义的编号,还是一组需要语义理解的普通词语。”

老李眉头紧锁:“编号不就几个字母加数字,怎么还理解不了?”

“在向量空间里,‘API-2024-03’被切成了 token,然后映射成一个向量。这个向量离它最近的,不是‘API-2024-03 接口变更说明’,而是那些频繁出现‘API’、‘2024’、‘03’这些词的文档——比如产品更新概览、版本管理最佳实践。那些文档虽然不精确,但从语义分布上看,‘用词’确实更像。”

老李沉默了几秒,然后吐出一句:“所以这玩意儿的‘聪明’反而害了它?”

“对。有时候,你需要的不是‘聪明’,而是精确。”


向量搜索的死穴:当精确匹配比语义更重要

小王走到白板前,画了两个圈,分别写上“语义理解”和“精确匹配”。

“纯向量搜索擅长回答‘怎么提升数据库性能’这种问题,因为问法和写法千变万化,要靠语义来捕捉。但它也有一系列死穴——”

他掰着手指头数:“第一,编号和代码。API-2024-03ERR-500SKU-88921,这些字符串的意义就是它们本身,不存在‘意思相近’的变体。向量搜索把它们当普通词语理解,反而会产生干扰。”

“第二,人名和地名。‘老李’和‘李工’可能向量相似度很高,但如果你搜的是‘老李的年假申请’,系统返回了‘李工的年假申请’,看起来没问题,但万一公司里还有个李工呢?精确匹配在这种场景下更可靠。”

“第三,否定词和逻辑词。‘没有库存’和‘有库存’,向量空间里几乎重叠,但意思完全相反。纯语义搜索根本分不清。”

老李点点头,又摇摇头:“那照你这么说,我的老本行——关键词全文搜索,难道要卷土重来?”

“不是卷土重来,是联合执政。”小王在白板上写下四个字——混合搜索。

图:混合搜索架构——两个引擎,一个结果


混合搜索怎么玩?给两个裁判一个规则

“混合搜索就是同时跑两套检索——一套向量,一套关键词——然后把结果融合成一张统一的排序列表。关键是怎么融合。你不能简单地把分数相加,因为向量相似度是 0 到 1 的余弦值,BM25 是一个没有上界的分数,直接加会被 BM25 的数值规模主导。”

老李追问:“那怎么办?”

“用得最多的两种方法:加权融合和 RRF。”

小王走到白板前画了个公式:“加权融合最简单。你把两边的分数各自归一化到 0 到 1 区间,然后按权重混合。比如你更信任关键词,就给 BM25 分配 0.7 的权重,向量分配 0.3。”

“但有时候你不知道该给多少权重。这时候 RRF——倒数排名融合,就出场了。”

“RRF 不在乎原始分数,只在乎排名。一个文档在向量搜索里排第 2,在 BM25 里排第 5,那它的 RRF 分数就是 1/(60+2) + 1/(60+5)——60 是一个平滑常数,防止排第一和第二之间差距太大。最终按 RRF 分数重新排序。这种方法不用归一化,也不用调权重,特别适合快速上线。”

老李拿出小本本:“你这公式能不能写成代码?我好理解。”

python
# RRF 融合算法:只关心排名,不关心分数
def reciprocal_rank_fusion(rankings, k=60):
    """
    rankings: 多个检索结果的排名列表,每个元素是 (doc_id, rank)
    rank 从 1 开始
    """
    scores = {}
    for ranking in rankings:
        for doc_id, rank in ranking:
            scores[doc_id] = scores.get(doc_id, 0) + 1 / (k + rank)
    # 按 RRF 分数降序排列
    return sorted(scores.items(), key=lambda x: x[1], reverse=True)

# 示例:向量搜索排名和 BM25 排名
vector_ranks = [("doc_A", 1), ("doc_B", 2), ("doc_C", 3)]
bm25_ranks =    [("doc_C", 1), ("doc_A", 2), ("doc_D", 3)]

fused = reciprocal_rank_fusion([vector_ranks, bm25_ranks])
for doc_id, score in fused:
    print(f"{doc_id}: RRF={score:.4f}")
# doc_A 在两个排名里都很靠前,最终排第一
# doc_C 在向量排第三但在 BM25 排第一,RRF 会给它不错的分数

老李盯着代码看了好一会儿:“所以 RRF 相当于让两边投票,排得越靠前票数越多,最后按总票数决定。”

“精准。而且 RRF 的好处是,你可以随时加入第三个检索器——比如基于知识图谱的检索——直接加一套排名进去就行,不用重新设计融合逻辑。”


实战:用 Elasticsearch 搭一个混合搜索

“别光讲理论,”老李催促道,“咱们现在的系统能不能改?”

小王打开笔记本:“Elasticsearch 从 8.0 开始支持向量检索,而且可以在同一个查询里同时跑关键词和向量,天然支持混合搜索。我给你搭个架子——”

python
# 用 Elasticsearch 实现混合搜索(示意)
def hybrid_search_es(index_name, query_text, query_vector, alpha=0.5):
    es = Elasticsearch("http://localhost:9200")
    
    response = es.search(index=index_name, body={
        "query": {
            "bool": {
                "should": [
                    # 关键词搜索:BM25
                    {"match": {"content": {"query": query_text, "boost": alpha}}},
                ]
            }
        },
        "knn": {
            "field": "embedding",
            "query_vector": query_vector,
            "k": 20,
            "num_candidates": 100,
            "boost": 1 - alpha
        },
        "size": 10
    })
    return response["hits"]["hits"]

“这个查询里,match 跑 BM25,knn 跑向量,Elasticsearch 内部会自动把两个分数归一化后加权求和。alpha 控制关键词的权重,你上次那个‘API-2024-03’的问题,把 alpha 调到 0.8 就能解决。”

老李问:“那所有查询都用同样的 alpha?有的问题偏精确,有的偏模糊。”

“理想情况是自适应——先对查询做一次分类,判断它是‘精确查找’还是‘语义提问’,再动态调整 alpha。不过现阶段,你可以设一个默认值,然后在特定场景下通过元数据覆盖。”


代价:双倍索引与调参的艺术

“两个引擎一起跑,性能开销多大?”老李的运维本能上线了。

“索引翻倍。原来你只存一份向量,现在还得维护一份倒排索引。存储和写入成本增加,查询延迟也略高。但对于企业级 RAG 来说,这点开销换来的检索质量提升,完全值得。”

小王补充道:“混合搜索的调参也不复杂。你只需要掌握三条黄金法则。”

“第一,精确场景提权关键词。搜合同编号、工号、API 编号时,关键词权重应该远高于向量。”

“第二,模糊场景提权语义。搜‘怎么提升性能’、‘有没有类似案例’时,语义权重占主导。”

“第三,RRF 做保底。如果你懒得调权重,直接用 RRF,大部分场景都能跑出不错的结果。”

老李合上小本本,靠在椅背上沉默了一会儿。

“那今天下午能上线吗?”

小王看了一眼墙上的钟:“四点之前给你。”

老李站起来,走到门口又停住:“这次可别再让我在客户面前丢脸了。下不为例啊。”

他拧开保温杯,枸杞的香气飘了出来。然后他回过头,像是想起了什么:“那个 RRF 的平滑常数为什么是 60?换个数值会怎样?你顺便给我讲讲。”

小王笑了:“老李,你这是要转行搞搜索算法啊?行,四点上完线我给你画一个 k 值的对比曲线。”

午后的阳光透过会议室的百叶窗洒在白板上,那些圈圈和公式还在,像两个世界终于找到了握手的方式。而老李的保温杯里,枸杞浮浮沉沉,像是 BM25 和向量相似度在达成一种微妙的平衡。

选 Embedding 模型就像相亲:合适的才是最好的
检索不止余弦相似度:高级检索策略大揭秘