基于多个kNN字段查找最相似文档的评分

了解如何通过多个kNN字段搜索文档,以及如何基于多个kNN向量字段对结果文档进行评分。

本指南介绍了如何通过多个kNN字段搜索文档,以及如何基于多个kNN向量字段对结果文档进行评分。在深入了解细节之前,让我们花几分钟时间介绍一下kNN和语义搜索,并解释kNN的机制。

Elasticsearch不仅仅是一个词汇(文本)搜索引擎。除了传统的文本匹配之外,Elasticsearch还是一个可扩展的数据存储和向量数据库,支持k近邻(kNN)搜索以及语义搜索。

Elasticsearch中的kNN搜索主要用于查找给定点在多维空间中的“最近邻”。文档表示为一组数字(向量),在搜索时,kNN功能会获取与查询向量最接近的相关文档。kNN搜索通常应用于涉及向量的场景,其中向量是通过使用深度神经网络进行“嵌入”过程从文本、图像或音频创建的。

另一方面,语义搜索是一种由自然语言处理特性驱动的搜索 - 它有助于根据意图和含义而不是仅仅根据文本匹配来搜索相关结果。

kNN机制

kNN(k近邻)会获取与给定用户查询最接近的k个文档的搜索结果,使用算法进行度量。

它的工作原理是计算向量之间的距离 - 通常是欧几里得余弦相似度。当我们使用kNN对数据集执行查询时,Elasticsearch会找到与我们的查询向量最接近的前k个条目。

在执行与数据相关的搜索活动以获取结果之前,必须使用适当的嵌入对索引进行预处理 - 嵌入是向量化数据的名称。这些字段的类型为dense_vector,包含数值数据。

让我们举个例子

如果您有一个图像数据集,并且您已使用神经网络将这些图像转换为向量,则可以使用kNN搜索查找与您的查询图像最相似的图像。如果您提供“披萨”图像的向量表示,kNN可以帮助您找到视觉上相似的其他图像,例如煎饼,也许还有意大利面 :)

kNN搜索是关于在向量空间中查找最近的数据点,因此适用于文本或图像嵌入的相似性搜索。相反,语义搜索是关于理解搜索查询中单词的含义和上下文,使其成为意图和上下文都很重要的基于文本的搜索的强大工具。

文档评分

当您有多个k近邻(kNN)字段时,基于最接近的文档对文档进行评分涉及利用Elasticsearch处理向量相似性的能力对文档进行排名。这种方法在语义搜索和推荐引擎等场景中特别有用。或者在处理多维数据并需要根据多个方面(字段)查找“最接近”或最相似项的情况下。

如何使用多个kNN字段搜索和评分文档

文本嵌入和向量字段

让我们以movies索引为例,其中包含一些文件,例如titlesynopsis等。我们将使用常见的数据类型(如text数据类型)来表示它们。除了这些普通字段之外,我们还将创建两个字段:title_vectorsynopsis_vector字段 - 顾名思义 - 它们是dense_vector数据类型字段。这意味着,数据将使用称为“文本嵌入”的过程进行向量化。

嵌入模型是一个自然语言处理神经网络,它将输入转换为数字数组。然后,向量化数据将存储在dense_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"
      }
    }
  }
}

值得注意的是,类型为texttitle字段有一个等效的向量类型字段:title_vector.predicted_value。类似地,synopsis的向量字段是synopsis_vector.predicted_value字段。此外,密集向量字段在上面的代码中作为dims提到了维度(384)。这表示模型将为每个输入字段生成384个维度。我们可以请求生成的dense_vector字段的最大维度为2048。

执行此脚本将创建一个名为movies的新索引,其中包含两个向量字段:title_vectorsynopsis_vector

索引示例文档

现在我们有了索引,我们可以索引一些电影并进行搜索。除了标题和概要字段外,文档还将包含向量字段。在索引文档之前,我们需要使用相应的向量填充文档。以下代码演示了生成向量后电影文档的示例

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模型上的推理API以获取向量化数据,如上所示(我在这里提到它作为参考,尽管我们希望改为使用推理处理器管道),以及
  • 另一种是设置和使用推理管道。

设置推理处理器

我们可以设置一个摄取管道,该管道将在相关字段上应用嵌入函数以生成向量化数据。例如,以下代码创建了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_vectorsynopsis_vector - 被提及为目标字段 - 是dense_vector字段类型。因此,它们存储由multilingual-e5-small嵌入模型生成向量化数据
  • field_map提到文档中的字段(在本例中为titlesynopsis字段)映射到模型的text_field字段
  • model_id声明用于嵌入数据的嵌入模型
  • target_field是向量化数据将复制到的字段的名称

执行上述代码将创建一个movie_embedding_pipeline摄取管道。也就是说 - 只包含titlesynopsis的文档将通过附加字段(title_vectorsynopsis_vector)进行增强,这些字段将包含内容的向量化版本。

索引文档

电影文档将包含标题和概要字段,如预期的那样 - 因此我们可以像下面这样对其进行索引。请注意,文档通过管道处理器进行增强,如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."
}
我们当然可以使用_bulkAPI一次性索引文档 - 请查看此批量API文档以获取更多详细信息。

一旦这些文档被索引,您可以通过执行搜索查询来获取电影以检查是否添加了向量化内容

GET movies/_search

这将导致电影包含两个附加字段,其中包含向量化内容,如下面的图像所示

现在我们已经索引了文档,让我们跳到使用kNN搜索功能进行搜索。

Elasticsearch中的k近邻搜索会获取与给定(查询)向量最接近的向量(文档)。Elasticsearch支持两种类型的kNN搜索

  • 近似kNN搜索
  • 蛮力(或精确)KNN搜索

虽然两种搜索都会生成结果,但蛮力搜索会以最大程度地利用资源和查询时间为代价找到准确的结果。近似kNN对于大多数搜索案例来说已经足够好了,因为它提供了接近准确的结果。

Elasticsearch为近似搜索提供knn查询,而我们应该为精确kNN搜索使用script_score查询。

让我们在电影上运行一个近似搜索,如下所示。Elasticsearch提供了一个带有query_build_vector块的kNN查询,其中包含我们的查询需求。让我们首先编写代码片段,然后讨论其组成部分

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”时,得分最高的电影是“荒野大镖客”。请注意,即使结果电影不匹配,kNN 搜索也会始终产生结果——这是 kNN 匹配的固有特性。

注意每个文档的相关性分数 (_score)——正如预期的那样,文档根据此分数排序。

使用多个 kNN 字段进行搜索

电影文档中有两个向量字段——title_vectorsynopsis_vector 字段——我们当然可以针对这两个字段进行搜索,并期望根据组合得分获得结果文档。

假设我们想在标题中搜索“Good”,但在 synopsis 字段中搜索“orders”。请记住,在之前使用“Good”进行的单个标题字段搜索中,我们检索了“荒野大镖客”电影。让我们看看在将概要的“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 查询可以接受多个搜索字段作为数组——在这里,我们提供了来自两个字段的搜索条件。答案是“义不容辞”,因为包含“order”向量的概要向量正是这部电影。

何时使用多 kNN 字段进行搜索

在一些情况下,我们可以使用多个 kNN 字段进行搜索。

  • 基于图像相似性(视觉 kNN 字段)和推文相似性(文本 kNN 字段)搜索“tweets”。
  • 基于音频特征(音频作为 kNN 字段),如节奏和韵律,以及可能标题/艺术家/流派信息(文本 kNN 字段)推荐相似的歌曲。
  • 根据用户的行为(用户交互的 kNN 字段)和电影/产品的属性(基于这些属性的 kNN 字段)推荐电影或产品。

结论

总结一下。在本文中,我们了解了 kNN 搜索的机制以及当我们有多个向量化字段时如何找到最接近的文档。

想要获得 Elastic 认证?了解下次Elasticsearch 工程师培训何时开始!

Elasticsearch 充满了新的功能,可以帮助您为您的用例构建最佳的搜索解决方案。深入了解我们的示例笔记本以了解更多信息,开始免费云试用,或立即在您的本地机器上试用 Elastic。

准备好构建最先进的搜索体验了吗?

充分先进的搜索并非一人之力所能及。Elasticsearch 由数据科学家、ML 运维工程师以及许多其他同样热衷于搜索的人员提供支持,他们与您一样热爱搜索。让我们联系起来,共同努力构建神奇的搜索体验,帮助您获得所需的结果。

亲自尝试