作者:来自 Elastic Valentin Crettaz

了解混合搜索、Elasticsearch 支持的混合搜索查询类型以及如何制作它们。
本文是三篇系列文章中的最后一篇,深入探讨了向量搜索(又称语义搜索)的复杂性以及它在 Elasticsearch 中的实现方式。
第一部分主要介绍嵌入(又名向量)的基础知识以及向量搜索的工作原理。
利用第一篇文章中学习到的所有向量搜索知识,第二篇文章将指导你如何在 Elasticsearch 中设置向量搜索并执行 k-NN 搜索。
在第三部分也是最后一部分中,我们将利用前两部分所学到的知识,并在此基础上深入研究如何在 Elasticsearch 中编写强大的混合搜索查询。
混合搜索的需求
在深入研究混合搜索领域之前,让我们快速回顾一下本系列第一篇文章中学到的词汇搜索和语义搜索的区别以及它们如何相互补充。
简而言之,当你可以控制结构化数据并且用户或多或少清楚他们正在搜索的内容时,词汇搜索非常有用。然而,当你需要使非结构化数据可搜索并且你的用户实际上不知道他们正在搜索什么时,语义搜索提供了很大的支持。如果有办法将两者结合起来,从而尽可能地从两者中榨取尽可能多的实质内容,那就太棒了。进入混合搜索!
从某种程度上来说,我们可以将混合搜索看作是词汇搜索和语义搜索的某种 "总和"。然而,如果操作正确,混合搜索会比仅仅将各个部分相加要好得多,产生的结果远远好于单独的词汇搜索或语义搜索。
运行混合搜索查询通常归结为发送至少一个词汇搜索查询和一个语义搜索查询的混合,然后合并两者的结果。词汇搜索结果由相似度算法进行评分,例如 BM25 或 TF-IDF,其分数范围通常无界,因为最高分数取决于倒排索引中存储的术语数量和频率。相比之下,语义搜索结果可以在封闭区间内进行评分,具体取决于所使用的相似度函数(例如,余弦相似度为 [0; 2])。
为了合并混合搜索查询的词汇和语义搜索结果,需要以保持检索到的文档的相对相关性的方式融合两个结果集,这是一个复杂的问题。幸运的是,有几种现有的方法可以利用;两个非常常见的是凸组合 (Convex Combination - CC)和倒数秩融合(**Reciprocal Rank Fusion -**RRF)。
基本上,凸组合(也称为线性组合)旨在将词汇搜索结果和语义搜索结果的归一化得分按相应的权重 和 β 进行组合,其中 0 ≤ α,β,使得:

CC 可以被看作是词汇和语义分数的加权平均值,0 到 1 之间的权重用于降低相关查询的权重,而大于 1 的权重用于提升相关查询。
然而,RRF 不需要任何分数校准或规范化,而只是根据文档在结果集中的排名对其进行评分,使用以下公式,其中 k 是一个任意常数,用于调整低排名文档的重要性:

CC 和 RRF 都有其优点和缺点,如下表 1 所示:
Convex Combination | Reciprocal Rank Fusion | |
---|---|---|
优点 | 良好的权重校准使 CC 比 RRF 更有效 | 不需要任何校准,完全无人监督,也不需要知道最小/最大分数 |
缺点 | 需要对权重进行良好的校准,并且最佳权重特定于每个数据集 | 调整 k 的值并不容易,并且排名质量可能会受到结果集大小的影响 |
[表 1:CC 和 RRF 的优缺点] |
值得注意的是,并非所有人都同意这些优缺点,这取决于所做的假设和测试的数据集。一个很好的总结就是,RRF 产生的分数准确度比 CC 略低,但具有 "即插即用" 的一大优势,并且可以在无需使用标记的查询集微调权重的情况下使用。
Elastic 决定同时支持 CC 和 RRF 方法。我们将在本文后面看到如何实现这一点。如果你有兴趣了解更多有关该选择背后的理由,你可以阅读 Elastic 博客中的这篇精彩文章,还可以查看 Elastician Philipp Krenn 在 Haystack 2023 上就 RRF 发表的精彩演讲。
时间线
在 2019 年的 7.0 版本中启用了对密集向量的强力 kNN 搜索之后,Elasticsearch 于 2022 年 2 月的 8.0 版本开始支持近似最近邻 (ANN) 搜索,并在 2022 年 8 月的 8.4 版本中紧随其后提供了混合搜索支持。下面的图 1 显示了 Elasticsearch 将混合搜索推向市场的时间表:
图 1:Elasticsearch 的混合搜索时间线
Elasticsearch 中的混合搜索剖析
正如我们在上一篇文章中简要暗示的那样,Elasticsearch 中的向量搜索支持是通过利用密集向量(dense)模型(因此是 density_vector 字段类型)实现的,该模型生成的向量通常包含本质上非零的值并表示多维空间中非结构化数据的含义。
然而,密集模型并不是执行语义搜索的唯一方法。 Elasticsearch 还提供了使用稀疏向量(sparse)模型的替代方法。 Elastic 创建了一个稀疏 NLP 向量模型,称为 Elastic Learned Sparse EncoderR(简称 ELSER),这是一个域外(即不在特定域上进行训练)的稀疏向量模型,不需要任何微调。它是在约 30,000 个术语的词汇上进行预训练的,并且由于它是一个稀疏模型,所以大多数向量值(即超过 99.9%)都是零。
其工作方式非常简单。在索引时,使用推理 inference 处理器生成包含术语/权重对的稀疏向量,并将其存储在 sparse_vector 类型的字段中,该字段是 dense_vector 字段类型的稀疏对应部分。在查询时,特定的 DSL 查询(也称为 sparse_vector)会将原始查询词替换为 ELSER 模型词汇表中已知的、根据权重与原始查询词最相似的词。
稀疏还是密集?
在介绍混合搜索查询之前,我们想简单强调一下稀疏模型和密集模型之间的区别。下面的图 2 显示了各个模型如何对文本 "the quick brown fox" 进行编码。
在稀疏情况下,将原始 4 个项扩展为 30 个与它们密切或远亲相关的加权项。扩展词条的权重越高,说明其与原词条的相关性越高。由于 ELSER 词汇表包含 30,000 多个术语,这意味着代表 "the quick brown fox" 的向量具有同样多的维度,并且仅包含 ~0.1% 的非零值(即~30/30,000),因此我们称这些模型为稀疏模型。
图 2:比较稀疏和密集模型编码
在密集的情况下,"the quick brown fox" 被编码成一个更小的嵌入向量,以捕捉文本的语义含义。 384 个向量元素中的每一个都包含一个非零值,表示文本片段与每个维度之间的相似度。请注意,我们赋予维度的名称(即 is_mouse、is_brown 等)纯属虚构,其目的只是为了对值进行具体描述。
另一个重要的区别是,稀疏向量是通过倒排索引来查询的(是的,就像词汇搜索一样),而正如我们在之前的文章中看到的那样,密集向量在特定的基于图或基于聚类的数据结构中被索引,可以使用近似最近邻(ANN)算法进行搜索。
我们不会进一步讨论 ELSER 的诞生细节,但如果你有兴趣了解该模型是如何诞生的,我们建议你查看 Elastic Search Labs 的这篇文章,其中详细解释了 Elastic 开发该模型的思维过程。如果你正在考虑评估 ELSER,可能值得检查 Elastic 的相关性工作台,它展示了 ELSER 与普通 BM25 词汇搜索的比较。我们也不会在本文中深入探讨下载和部署 ELSER 模型的过程,但你可以花点时间查阅官方文档,其中很好地解释了如何执行此操作。
混合搜索支持
无论你要使用密集检索还是稀疏检索,Elastic 都为这两种模型类型提供混合搜索支持。第一种类型是 query 搜索选项中指定的词汇搜索查询和 knn 搜索选项中指定的向量搜索查询(或其数组)的混合。第二个引入了一个名为 **retriever
**的新搜索选项(在 8.14 中引入,在 8.16 中引入 GA),它还包含可以是词汇(例如,匹配)或语义(例如,sparse_vector)性质的搜索查询数组。
如果你觉得这一切有些抽象,请不要担心,因为我们将很快深入细节,展示混合搜索在实践中是如何运作的,以及它们能带来什么好处。
密集模型混合搜索
这就是我们刚才提到的第一种混合搜索类型。它基本上归结为运行词汇搜索查询与近似 k-NN 搜索混合以提高相关性。此类混合搜索查询如下所示:
POST my-index/_search
{
"size": 10,
"_source": false,
"fields": [ "price" ],
"retriever": {
"standard": {
"retrievers": [
{
"standard": {
"query": {
"match": {
"text-field": "fox"
}
}
}
},
{
"knn": {
"field": "title_vector",
"query_vector": [0.1, 3.2, 2.1],
"k": 5,
"num_candidates": 100
}
}
]
}
}
}
从上面我们可以看出,混合搜索查询只是使用标准检索器进行的词汇搜索查询(例如,匹配查询)和 knn 检索器中指定的向量搜索查询的组合 。此查询首先在全局级别检索前五个向量匹配,然后将它们与词汇匹配相结合,最后返回十个最佳匹配。向量和词汇匹配的组合方式是通过 disjunction(即逻辑或条件),其中每个文档的分数是使用凸组合( Convex Combination**)**计算的,即其向量和词汇分数的加权和,正如我们之前所见。
Elasticsearch 还支持使用 RRF 排名运行完全相同的混合查询,你只需使用 rrf 检索器即可完成此操作,如下所示:
POST my-index/_search
{
"size": 10,
"_source": false,
"fields": [ "price" ],
"retriever": {
"rrf": {
"retrievers": [
...same as above...
],
"rank_constant": 60,
"rank_window_size": 100
}
}
}
此查询的运行方式与之前的查询基本相同,只是从向量和词汇查询中检索 window_size 文档(例如,在本例中为 100),然后按 RRF 排序,而不是使用 CC 进行评分。最后,返回结果集中从 1 到 size(例如 10)排名靠前的文档。
关于这种混合查询类型最后要注意的是,RRF 排名需要商业许可证(白金版或企业版),但如果你没有,你仍然可以利用 CC 评分进行混合搜索,或者使用试用许可证,让你享受一个月的完整功能集。
稀疏模型混合搜索
用于查询稀疏模型的第二种混合搜索类型的工作方式与密集向量完全相同。下面,我们可以看到这种混合查询的样子:
POST my-index/_search
{
"_source": false,
"fields": [ "text-field" ],
"retriever": {
"rrf": {
"retrievers": [
{
"standard": {
"query": {
"match": {
"text-field": "fox"
}
}
}
},
{
"standard": {
"query": {
"sparse_vector": {
"field": "ml.tokens",
"inference_id": ".elser_model_1",
"query": "a quick brown fox jumps over a lazy dog"
}
}
}
}
]
}
}
}
在上面的查询中,我们可以看到检索器数组包含一个词汇匹配查询以及一个适用于我们之前介绍的 ELSER 稀疏模型的语义 sparse_vector 查询。
密集和稀疏模型的混合搜索
到目前为止,我们已经看到了两种不同的运行混合搜索的方法,具体取决于搜索的是密集向量空间还是稀疏向量空间。此时,你可能想知道我们是否可以在同一个索引中混合密集数据和稀疏数据,并且你会很高兴地了解到这确实是可能的。一个具体的应用可能是你需要搜索包含图像的密集向量空间和包含这些图像的文本描述的稀疏向量空间。我们 standard 准检索器与 knn 检索器结合起来,这样的查询看起来是这样的:
POST my-index/_search
{
"_source": false,
"fields": [ "text-field" ],
"retriever": {
"rrf": {
"retrievers": [
{
"knn": {
"field": "image-vector",
"query_vector": [0.1, 3.2, ..., 2.1],
"k": 5,
"num_candidates": 100
}
},
{
"standard": {
"query": {
"sparse_vector": {
"field": "ml.tokens",
"inference_id": ".elser_model_1",
"query": "a quick brown fox jumps over a lazy dog"
}
}
}
}
]
}
}
}
在上面的有效载荷中,我们可以看到 sparse_vector 查询在 ELSER 稀疏向量空间中搜索图像描述,在 knn 检索器中,向量搜索查询在密集向量空间中搜索图像嵌入(例如,用嵌入向量表示的 "brown fox")。此外,我们还利用 rrf 检索器来发挥 RRF 的作用。
你甚至可以使用另一个 standard 检索器将另一个词汇搜索查询添加到组合中,它看起来像这样:
POST my-index/_search
{
"_source": false,
"fields": [ "text-field" ],
"retriever": {
"rrf": {
"retrievers": [
{
"knn": {
"field": "image-vector",
"query_vector": [0.1, 3.2, ..., 2.1],
"k": 5,
"num_candidates": 100
}
},
{
"standard": {
"query": {
"match": {
"text-field": "brown fox"
}
}
}
},
{
"standard": {
"query": {
"sparse_vector": {
"field": "ml.tokens",
"inference_id": ".elser_model_1",
"query": "a quick brown fox jumps over a lazy dog"
}
}
}
}
]
}
}
}
上述有效负载强调了我们可以利用一切可能的方法来指定包含词汇搜索查询、向量搜索查询和语义搜索查询的混合查询。
限制
评估 ELSER 稀疏模型时要注意的主要限制是,在运行文本推理时它仅支持最多 512 个标记。因此,如果你的数据包含需要完全可搜索的较长文本摘录,则你有两个选择:a)使用另一个支持较长文本的模型,b)将你的文本分成更小的段,或者 3)如果你使用的是 8.15 或更高版本,你可以利用 semantic_text 字段类型来处理自动分块。

优化
不可否认的是,无论是稀疏向量还是密集向量,都会变得相当长,从几十维到几千维,具体取决于你使用的推理模型。此外,无论你是在仅包含几个单词的小句子还是大段文本上运行文本推理,生成的表示含义的嵌入向量将始终具有与你使用的模型中配置的维度一样多的维度。因此,这些向量会占用文档中以及磁盘上的相当多的空间。
解决此问题最明显的优化是配置索引映射以从源文档中删除向量字段(即,dense_vector 和 sparse_vector)。这样做的话,向量值仍然会被索引和可搜索,但它们不再是源文档的一部分,从而大大减小它们的大小。通过配置映射以从 _source 中排除矢量字段来实现这一点非常简单,如下面的代码所示:
PUT my-index
{
"mappings": {
"_source": {
"excludes": [
"text_embedding.predicted_value",
"ml.tokens"
]
},
"properties": {
"text": {
"type": "text"
},
"ml": {
"properties": {
"tokens": {
"type": "rank_features"
}
}
},
"text_embedding": {
"properties": {
"predicted_value": {
"type": "dense_vector",
"dims": 384,
"index": true,
"similarity": "cosine"
}
}
}
}
}
}
为了向你展示一些具体的数字,我们进行了一个快速实验。我们已使用 msmarco-passagetest2019-top1000 数据集加载了索引,该数据集是 Microsoft MARCO Passage Ranking 完整数据集的一个子集。 60 MB 的 TSV 文件包含 182,469 个文本段落。
接下来,我们创建了另一个索引,其中包含原始文本和从 Hugging Face 提供的 msmarco-MiniLM-L-12-v3 句子转换器模型生成的嵌入向量(密集)。然后,我们重复了相同的实验,但这次配置映射以从源文档中排除密集向量。
我们还使用 ELSER 稀疏模型运行了相同的测试,一次将 sparse_vector 字段存储在文档中,一次将其排除。下表 2 显示了每个结果索引的大小,其名称不言自明。我们可以看到,通过从源中排除密集想量字段,索引大小除以 3,在 rank feature 情况下除以几乎 3.5。
Index | Size (in MB) |
---|---|
index-with-dense-vector-in-source | 376 |
index-without-dense-vector-in-source | 119 |
index-with-sparse_vector-in-source | 1,300 |
index-without-sparse_vector-in-source | 387 |
诚然,你的里程可能会有所不同,这些数字仅供参考,并且在很大程度上取决于你将要索引的非结构化数据的性质和大小,以及你将选择的密集或稀疏模型。
关于此优化,最后需要注意的是,如果你决定从源中排除向量,则你将无法使用索引作为源索引重新索引到另一个索引中,因为你的嵌入向量将不再可用。但是,由于索引仍然包含原始文本数据,因此你可以使用具有 inference 处理器的原始摄取管道来重新生成嵌入向量。
让我们总结一下
在本系列关于向量搜索的最后一篇文章中,我们介绍了 Elasticsearch 支持的不同类型的混合搜索查询。一种选择是使用词汇搜索(例如,查询)和向量搜索(例如,knn)的组合;另一种方法是利用新引入的带有 sparse_vector 查询的 retriever 搜索选项。
我们首先快速回顾了一下融合词汇和语义搜索结果以提高准确性的诸多优势。在此过程中,我们回顾了两种融合词汇和语义搜索结果的不同方法,即凸组合(CC)和倒数排序融合(RRF),并研究了它们各自的优缺点。
然后,通过一些说明性示例,我们展示了 Elasticsearch 如何使用凸组合和倒数秩融合作为评分和排名方法,为稀疏和密集向量空间提供混合搜索支持。我们还简要介绍了 Elastic Learned Sparse EncoderR 模型(ELSER),这是他们首次尝试提供基于 30,000 个标记词汇表构建的域外稀疏模型。
最后,我们指出了 ELSER 模型的一个局限性,并解释了一些优化未来混合搜索实现的方法。
如果你喜欢阅读的内容,请务必查看本系列的其他部分:
使用此自定进度的搜索 AI 实践学习来亲自尝试想量搜索。你现在可以开始免费的云试用或在本地机器上试用 Elastic。