词汇搜索使用 BM25 排序算法,对于各种查询来说成本低、速度快且非常有效。但它有一个盲点:无法处理与文档没有共同标记的查询。在本文中,您将准确衡量 BM25 的不足之处。我们将使用 Elasticsearch 的排名评估 API (rank_eval),并通过添加 Jina AI 嵌入,通过 Elastic 推理服务 (EIS) 来缩小这一差距。您会看到召回分数从 0.43 提升到 0.75,并理解其原因。
什么是召回?
召回率 以 0 到 1 的范围来衡量用户真正想要的文档有多少出现在搜索结果中。如果某个查询应显示三个产品,而您的搜索结果仅有两个进入前 10 名,则该查询的得分为 recall@10 = 0.67。这是一个基于集合的指标:它并不关心相关文档在这 k 个结果中的位置。位置 10 的相关文档与位置 1 的相关文档具有同等效力。高召回率意味着您不会丢失相关结果。

该图表显示了两组文档:所有相关文档(左侧)和 BM25 实际检索到的文档(前 10 个,右侧)。只有交集部分才计入召回率,找到了 prod_1 和 prod_2,而 prod_3、prod_4 和 prod_6 则完全遗漏。结果:Recall@10 = 2/5 = 0.40。
准备工作
让我们言归正传,更好地了解召回的工作原理。本演示使用 Python。您可以在配套笔记本 (notebook.ipynb) 中跟着操作,其中每个代码块都是一个可直接运行的单元。
提供的代码使用以下内容:
- Elasticsearch 9.3+
- Python 3.10+
- 包含 Elasticsearch 凭据的
.env文件
该数据集
我们将使用包含 1,000 种产品的产品目录,涵盖鞋类、电子产品、工具等多个类别。
每份文档有四个字段:
| 字段 | 类型 |
|---|---|
| “标题” | 文本 |
| “描述” | 文本 |
| “品牌” | 关键字 |
| `类别` | 关键字 |
该数据集加载自 dataset.csv。
词汇搜索的支持和局限性
BM25 是 Elasticsearch 和大多数搜索引擎的默认排名算法。它根据查询词在文档中的出现频率对其进行评分,并根据文档长度和这些词在整个索引中的出现频率进行调整。在此基础上,您还可以获得分析器:小写规范化、词干提取和停用词消除。查询“跑步鞋”将匹配“跑步鞋”,也可能匹配“跑步”。
这对很多查询都很有效:
- “跑鞋”会立即匹配标题中包含这些确切标记的产品。
- “蓝牙扬声器”会显示便携式音频产品,因为这些词语是逐字匹配的。
搜索结果具有确定性和可解释性:文档排名靠前,是因为查询词出现在其中。调试相关性很简单。
出现问题的地方
现在,让我们针对同一目录尝试这些查询:
- “护肤流程”:在任何产品标题中都没有出现“流程”这个词。BM25 能够部分匹配“护肤”这一词,但面部精华液、身体精油和保湿霜等产品是用“维生素 C”、“视黄醇”或“提亮”等术语来描述的,这些术语与查询词都没有重叠。构成完整护肤流程的产品分散在索引中,没有任何共同的令牌将其关联起来。
- “宠物旅行配件”:这是一个用例分组,而非产品类别。宠物狗背带、宠物汽车座椅和旅行笼都与此相关,但它们的描述侧重于便携性、安全性和舒适性,而非“旅行配件”。BM25 与“宠物”大致匹配,但无法区分旅行专用产品与宠物目录中的其他产品。
这是一个召回问题。相关文档已存在于您的索引中。BM25 无法找到它们,因为用户的用词和文档中的词语匹配度不够高。
添加同义词有助于处理已知情况。但您无法枚举用户表达某种意图的所有方式。这就是向量发挥作用的地方。
为何要测量召回率
在解决问题之前,需要先对问题进行量化。
Recall@k 衡量有多少用户真正想要的文档出现在搜索结果中。正式来说:
Precision@k 衡量前 k 个结果,以及其中有多少是实际相关的:
高精度意味着您返回的结果质量较高。在电子商务领域,缺少相关产品(召回率低)通常比显示稍有瑕疵的结果(精度较低)更糟糕,因为隐藏的产品意味着销售损失。
Elasticsearch 的 rank_eval API 允许您系统地测量两者。您提供一系列查询,每个查询都有一组已评分的文档,Elasticsearch 会为您计算所有查询的指标。
设置评估
rank_eval API 需要一个评级数据集:查询与每个查询相关的文档之间的映射,以及相关性等级(0=不相关,1=相关,2=高度相关)。
在笔记本中,这是判断列表:
这种混合是有意为之:q1 是 BM25 可以很好处理的查询(产品标题中的精确标记),而 q2、q3 和 q4 是基于意图的查询,用户的意图是以概念而非具体产品关键词来表达的。
测量 BM25 基线召回率
首先,设置 Elasticsearch 客户端,并对原始文本数据建立索引:
现在为 BM25 构建 rank_eval 请求。列表中的每个请求都将会查询及其评分结合起来:
结果:
0.43 这意味着在所有四个查询中,BM25 只找到了它应该找到的文档的 43%。这种不足集中体现在基于意图的查询中:“护肤流程”漏掉了面部精华液和身体精油,因为“流程”一词从未出现在产品标题中;而“宠物旅行配件”则检索出了一些不相关的宠物产品,却遗漏了那些以便携性和安全性而非“旅行配件”来描述的宠物笼和宠物箱。
这就是我们的基准。现在我们有了一个要超越的数字。
使用 Jina 嵌入添加向量搜索
Vector search 将文档和查询编码为高维向量,这是一种由数百甚至数千个数值组成的向量,每个数值都对它所代表的数据的特定特征进行编码。意义相似的文档最终会在向量空间中靠近,即使它们没有共同的词汇。“健身器材”和“哑铃套装”会放在一起,因为这两个概念是相关的。我选择 Elasticsearch 作为我的向量数据库,是因为它支持混合搜索,让我既能理解语义,又能精确查找关键字。
步骤 1:使用 Jina 嵌入 v5 作为推理终端
如果您的集群具有 GPU 资源(在 Elastic Cloud 和 Elasticsearch 9.3+ 中可用),嵌入将在 GPU 上生成,这比 CPU 推理快得多,并消除了历史上使向量在扩展时变得昂贵的性能权衡。
为什么要特别选用 Jina 嵌入?jina-embeddings-v5-text 是一种多语言模型(支持 119 种以上语言),具有 32,000 个标记的上下文窗口,并支持特定任务的低秩自适应 (LoRA) 适配器。它适用于开箱即用的简短产品描述。点击此处了解有关 jina-embeddings-v5-text 模型的更多信息。
步骤 2:创建具有语义字段的索引
这里的关键在于 semantic_text 字段类型。这是对 dense_vector 的更高级别的抽象:您将其指向一个推理终端,Elasticsearch 会自动生成嵌入。
title 和description 上的 copy_to 属性意味着这两个字段的内容都会流入 semantic_field 进行嵌入,因此单个向量就能捕获完整的产品表示。
步骤 3:为产品编制索引
索引时,Elasticsearch 会调用每个文档的推理端点,并将生成的嵌入存储在 semantic_field 中。您无需编写任何额外代码。
混合搜索:将 BM25 与向量结合并采用 RRF
添加向量可以提高召回率,但仅使用向量可能会在精确匹配查询中失去精度;“跑鞋”仍应将逐字匹配的结果排在首位。混合搜索则保留词汇成分,以保持这种精确性。
使用倒数排序融合 (RRF) 的混合搜索可以保持两者的优点:
- BM25 可以高精度处理精确和近似精确的查询。
- 语义搜索能以高召回率处理基于意图和多语言的查询。
- RRF 将两份排名表合并为一份排名表。
RRF 公式根据每个文档在每个结果列表中的排名,为每个文档分配分数:
在两个列表中均排名靠前的文档将获得更高的综合得分。rank_constant用于控制排名较低的文档获得的权重大小。
结果:
混合搜索在 BM25 (0.43) 的基础上有了显著提升,并为“跑鞋”等精确匹配查询保留了精确度。
结果:前后结果对比
以下是所有三种方法的完整对比:
结果:
| 方法 | Recall@10 |
|---|---|
| BM25(词法) | 0.43 |
| 混合型(BM25 + 向量) | 0.75 |

按查询细分:

结论
在这篇文章中,我们看到,当用户键入精确的查询时,BM25 词汇搜索是可靠的,但当他们根据意图而非关键词进行搜索时,其召回率就会下降。借助 rank_eval,我们建立了一个可重复的基线,用真实数据来衡量这一差距。在此基础上,我们添加了一个由 Jina 嵌入提供支持的 semantic_text 字段,并再次运行了评估。结果:混合搜索将召回率从 0.43 提高到 0.75,同时保留了精确匹配查询的精确度,但实际幅度取决于您的查询组合。
该模式可扩展至本示例之外:从用户的实际查询中收集判断,以 rank_eval 作为基准运行,添加 semantic_text,然后再次进行测量。您将确切了解改进了哪些方面以及改进了多少。
后续步骤
- 深入了解召回与向量搜索:《召回与向量搜索量化》,作者:Jeff Vestal
- 添加重排序功能,以进一步提升前几条结果的精准度
- 探索 Elasticsearch 混合搜索文档
- 阅读有关
rank_evalAPI 的更多信息




