作者:来自 Elastic Madhusudhan Konda
通过具有多个 kNN 字段的最接近的文档对文档进行评分
Elasticsearch 不仅仅是一个词法(文本)搜索引擎。 Elasticsearch 是多功能搜索引擎,除了传统的文本匹配之外,还支持 k 最近邻 (kNN) 搜索以及语义搜索。
Elasticsearch 中的 kNN 搜索主要用于查找多维空间中给定点的 “最近邻居”。文档被表示为一组数字(向量),在搜索时,kNN 特征会获取更接近查询向量的相关文档。 kNN 搜索通常应用于涉及向量的场景,其中向量是通过使用深度神经网络的 “嵌入” 过程从文本、图像或音频创建的。
另一方面,语义搜索是一种由自然语言处理特征驱动的搜索,它有助于根据意图和含义搜索相关结果,而不仅仅是文本匹配。
在本文中,我们的重点是搜索具有多个 kNN 字段的文档,并根据多个 kNN 向量字段对结果文档进行评分。
由于我们将在本文中详细了解 kNN 搜索,因此让我们花几分钟时间详细了解 kNN 的机制。
kNN 机制
kNN(k 最近邻居)获取的搜索结果几乎是最接近给定用户查询的 k 个文档(使用算法测量)。
它的工作方式是通过计算向量之间的距离(通常是欧几里得相似度或余弦相似度)。当我们使用 kNN 对数据集执行查询时,Elasticsearch 会找到最接近我们的查询向量的前 “k” 个条目。
在执行与搜索相关的活动以获取结果之前,必须使用适当的嵌入对索引进行预处理 —— 嵌入是向量化数据的术语。这些字段的类型是 dense_vector,用于保存数值数据。
让我们举个例子:
如果你有图像数据集,并且已使用神经网络将这些图像转换为向量,则可以使用 kNN 搜索来查找与查询图像最相似的图像。如果你提供 “披萨” 图像的向量表示,kNN 可以帮助你找到视觉上相似的其他图像,例如薄煎饼,也许还有意大利面:)
kNN 搜索是关于在向量空间中查找最近的数据点,因此适用于文本或图像嵌入的相似性搜索。相比之下,语义搜索是为了理解搜索查询中单词的含义和上下文,使其对于意图和上下文都很重要的基于文本的搜索非常强大。
为文当打分
当你有多个 k 最近邻 (kNN) 字段时,根据最接近的文档对文档进行评分需要利用 Elasticsearch 处理向量相似性的能力来对文档进行排名。这种方法在语义搜索和推荐引擎等场景中特别有用。或者我们正在处理多维数据并需要基于多个方面(字段)找到 “最接近” 或最相似的项目的情况。
文本嵌入(text embedding)及向量字段
让我们以电影(movies)索引为例,它由一些文件组成,例如标题(title)、概要(synopsis)等。我们将使用常见的数据类型(例如文本数据类型)来表示它们。除了这些普通字段之外,我们还将创建另外两个字段:title_vector 和 synopsis_vector 字段 - 顾名思义 - 它们是密集向量数据类型字段。这意味着,数据将使用称为 “文本嵌入” 的过程进行向量化。
嵌入模型是一个自然语言处理神经网络,它将输入转换为数字数组。然后,向量化数据将存储在密集向量类型字段中。数据文档可以有多个字段,包括一些用于存储向量数据的 dense_vector 字段。
因此,在下一节中,我们将混合使用普通字段和 kNN 字段来创建索引。
使用 kNN 字段创建索引
让我们创建一个名为 movies 的索引,其中包含示例电影文档。我们的文档将有多个字段,包括几个用于存储向量数据的 kNN 字段。以下片段演示了索引映射代码:
PUT /movies
{
"mappings": {
"properties": {
"title": {
"type": "text"
},
"title_vector.predicted_value": {
"type": "dense_vector",
"dims": 384
},
"synopsis": {
"type": "text"
},
"synopsis_vector.predicted_value": {
"type": "dense_vector",
"dims": 384
},
"genre": {
"type": "text"
}
}
}
}
值得注意的是,文本类型的 title 字段有一个等效的向量类型字段:title_vector.predicted_value。类似地,synopsis 的向量字段是 synopsis_vector.predicted_value 字段。此外,密集向量场的维度 (384) 在上面的代码中提到为 dims。这表明模型将为每个摄取的字段生成 384 个维度。我们可以请求在密集向量字段上生成的最大维度是 2048。
执行此脚本将创建一个名为 movies 的新索引,其中包含两个向量字段:title_vector 和 synopsis_vector。
索引示例文档
现在我们有了索引,我们可以索引一些电影并进行搜索。除了 title 和 synopsis 字段外,文档还将包含向量字段。在索引文档之前,我们需要用相应的向量填充文档。以下代码演示了生成向量后的电影文档示例:
POST /movies/_doc/1
{
"title": "The Godfather",
"title_vector": [0.1, 0.5, 3, 4,...], // vectorized data
"synopsis": "The aging patriarch of an organized crime dynasty....",
"synopsis_vector": [0.2, 0.6, 1, 0.7,...] // vectorized data
}
如你所见,需要准备好向量数据以供文档提取。有几种方法可以做到这一点:
- 一种是在 Elasticsearch 之外调用 text_embedding 模型上的 inference API 来对数据进行向量化,如上所示(我在这里提到它作为参考,尽管我们更希望使用推理处理器管道)
- 另一种是设置和使用推理管道。
设置推理处理器(inference processor)
我们可以设置一个摄取管道,该管道将嵌入函数应用于相关字段以生成矢量化数据。例如,以下代码创建 movie_embedding_pipeline 处理器,该处理器将为每个字段生成嵌入并将其添加到文档中:
PUT _ingest/pipeline/movie_embedding_pipeline
{
"processors": [
{
"inference": {
"model_id": ".multilingual-e5-small",
"target_field": "title_vector",
"field_map": { "title": "text_field" }
}
},
{
"inference": {
"model_id": ".multilingual-e5-small",
"target_field": "synopsis_vector",
"field_map": { "synopsis": "text_field" }
}
}
]
}
摄取管道可能需要一点解释:
- 两个字段 - title_vector 和 synopsis_vector - 被称为 target_field - 是 dense_vector 字段类型。因此,它们存储由 multilingual-e5-small 嵌入模型生成的向量化数据
- field_map 提到文档中的字段(在本例中为 title 和 synopsis 字段)被映射到模型的 text_field 字段
- model_id 声明用于嵌入数据的嵌入模型
- target_field 是向量化数据将被复制到的字段的名称
执行上述代码将创建一个 movie_embedding_pipeline 摄取管道。也就是说 - 仅包含 title 和 synopsis 的文档将通过附加字段(title_vector 和 synopsis_vector)进行增强,这些字段将具有内容的向量化版本。
索引文档
电影文档将由 title 和 synopsis 字段组成,正如预期的那样 - 因此我们可以按如下所示对其进行索引。请注意,文档通过 URL 中启用的管道处理器进行增强。以下代码片段显示了索引少量电影:
POST movies/_doc/?pipeline=movie_embedding_pipeline
{
"title": "The Godfather",
"synopsis": "The aging patriarch of an organized crime dynasty transfers control of his clandestine empire to his reluctant son."
}
POST movies/_doc/?pipeline=movie_embedding_pipeline
{
"title": "Avatar",
"synopsis": "A paraplegic Marine dispatched to the moon Pandora on a unique mission becomes torn between following his orders and protecting the world he feels is his home."
}
POST movies/_doc/?pipeline=movie_embedding_pipeline
{
"title": "Godzilla",
"synopsis": "The world is beset by the appearance of monstrous creatures, but one of them may be the only one who can save humanity."
}
POST movies/_doc/?pipeline=movie_embedding_pipeline
{
"title": "The Good, The Bad and The Ugly",
"synopsis": "A bounty hunting scam joins two men in an uneasy alliance against a third in a race to find a fortune in gold buried in a remote cemetery."
}
POST movies/_doc/?pipeline=movie_embedding_pipeline
{
"title": "A Few Good Men",
"synopsis": "Military lawyer Lieutenant Daniel Kaffee defends Marines accused of murder. They contend they were acting under orders."
}
我们当然可以使用 _bulk API 来一次性索引文档 - 请查看此 Bulk API 文档以了解更多详细信息。
一旦这些文档被索引,你就可以通过执行搜索查询来获取电影来检查是否添加了向量化内容:
GET movies/_search
这将导致 movies 索引具有由向量化内容组成的两个附加字段,如下图所示:
现在我们已经对文档进行了索引,让我们使用 kNN 搜索功能对这些文档进行搜索。
kNN 搜索
Elasticsearch 中的 k-最近邻搜索会获取最接近给定(查询)向量的向量(文档)。Elasticsearch 支持两种类型的 kNN 搜索:
- 近似 kNN 搜索
- 强力(或精确)KNN 搜索
虽然两种搜索都会产生结果,但强力搜索会以最大资源利用率和查询时间为代价找到准确的结果。近似 kNN 足以满足大多数搜索情况,因为它可以提供接近准确的结果。
Elasticsearch 为近似搜索提供 knn 查询,而我们应该使用 script_score 查询进行精确 kNN 搜索。
近似搜索
让我们对 movies 进行近似搜索,如下所示。Elasticsearch 提供了一个 kNN 查询,其中包含由我们的查询要求组成的 query_build_vector 块。让我们先编写代码片段,然后再讨论其组成部分:
GET movies/_search
{
"knn": {
"field": "title_vector.predicted_value",
"query_vector_builder": {
"text_embedding": {
"model_id": ".multilingual-e5-small",
"model_text": "Good"
}
},
"k": 3,
"num_candidates": 100
},
"_source": [
"id",
"title"
]
}
传统的搜索查询支持 query 功能,但是 Elasticsearch 引入了 knn 搜索功能作为查询向量的一等公民。
knn 块由我们要搜索的字段组成 - 在本例中,它是标题向量 - title_vector.predicted_value 字段。请记住,这是我们之前在映射中提到的字段的名称。
query_vector_builder 是我们需要提供查询以及嵌入查询所需的模型的地方。在本例中,我们将 multilingual-e5-small 设置为我们的模型,文本只是 “Good”。Elasticsearch 将使用文本嵌入模型 (multilingual-e5-small) 对相关查询进行向量化。然后,它将向量查询与可用的标题向量进行比较。
k 值表示需要返回多少文档作为结果。
此查询应该为我们提供前三份文档:
"hits": [
{
"_index": "movies",
"_id": "ZADvgo4BDf-WoG_MTka1",
"_score": 0.92932993,
"_source": {
"title": "The Good, The Bad and The Ugly"
}
},
{
"_index": "movies",
"_id": "uJ3wgo4BMlgFmHKKtFSp",
"_score": 0.91828954,
"_source": {
"title": "A Few Good Men"
}
},
{
"_index": "movies",
"_id": "tp15fY4BMlgFmHKK6VRV",
"_score": 0.90952975,
"_source": {
"title": "The Godfather"
}
}
]
当我们针对片名搜索 “Good” 时,得分最高的电影是《The Good, The Bad and the Ugly》。请注意,即使结果电影不匹配,kNN 搜索也总能得到结果 - 这是 kNN 匹配的固有特性。
记下每个文档的相关性分数 (_score) - 正如预期的那样,文档是根据此分数排序的。
搜索多个 kNN 字段
我们有两个向量字段 - 电影文档中的 title_vector 和 synopsis_vector 字段 - 我们当然可以针对这两个字段进行搜索,并根据组合分数期望得到结果文档。
假设我们想在 title 中搜索 “Good”,但在 synopsis 字段中搜索 “orders”。记得从之前使用 “Good” 的单个 title 字段搜索中,我们检索到了《The Good, The Bad and the Ugly》这部电影。让我们看看根据概要的 “orders” 部分作为搜索条件,将获取哪部电影。
以下代码声明了我们的多 kNN 字段搜索:
POST movies/_search
{
"knn":[
{
"field": "title_vector.predicted_value",
"query_vector_builder": {
"text_embedding": {
"model_id": ".multilingual-e5-small",
"model_text": "Good"
}
},
"k": 3,
"num_candidates": 100
},
{
"field": "synopsis_vector.predicted_value",
"query_vector_builder": {
"text_embedding": {
"model_id": ".multilingual-e5-small",
"model_text": "orders"
}
},
"k": 3,
"num_candidates": 100
}
]
}
正如你所想象的,knn 查询可以接受多个搜索字段作为数组 - 在这里我们从两个字段中提供了搜索条件。答案是 “A Few Good Men”,因为包含 “order” 向量的概要向量就是这部电影。
何时使用多 kNN 字段进行搜索
我们可以使用多个 kNN 字段进行搜索的一些情况如下:
- 根据图像相似性(视觉 kNN 字段)和推文相似性(文本 kNN 字段)搜索 “tweets”。
- 根据音频特征(音频作为 kNN 字段)推荐类似的歌曲,如节奏和韵律;可能还有标题/艺术家/流派信息(文本 kNN 字段)。
- 根据用户行为(用于用户交互的 kNN 字段)和电影/产品属性(基于这些属性的 kNN 字段)推荐电影或产品。
就这样了。在本文中,我们研究了 kNN 搜索的机制,以及当我们有多个向量化字段时如何找到最近的文档。
准备好自己尝试一下了吗?开始免费试用。
想要获得 Elastic 认证吗?了解下一次 Elasticsearch 工程师培训何时开始!
原文:Elasticsearch Scoring: Based on multiple kNN fields — Elastic Search Labs