如何为kNN搜索选择最佳k和num_candidates

了解选择kNN搜索中`k`和`num_candidates`参数的最佳值的策略,并通过实际示例进行说明。

如何选择kNN搜索的最佳knum_candidates

向量搜索已成为当前生成式AI/ML领域的游戏规则改变者。它允许我们根据项目的语义含义而不是简单的精确关键词匹配来查找类似的项目。

Elasticsearch的k-最近邻(kNN)算法是一种用于分类和回归任务的基础机器学习技术。随着向量搜索功能的引入,它在Elasticsearch的生态系统中占据了重要的位置。kNN基于向量搜索在Elasticsearch 8.5中引入,允许用户对密集向量字段执行高速相似性搜索。

用户可以通过利用kNN算法使用底层指定的距离度量(例如欧几里得或余弦相似度)来查找索引中“最接近”给定向量的文档。此功能标志着一个关键的进步,因为它在需要语义搜索、推荐和其他用例(例如异常检测)的应用程序中特别有用。

在Elasticsearch中引入密集向量字段和k-最近邻(kNN)搜索功能为实现超越传统文本搜索的复杂搜索功能开辟了新的视野。

本文深入探讨了选择knum_candidates参数的最佳值的策略,并使用Kibana提供了实际示例。

kNN搜索查询

Elasticsearch提供了一个用于最近邻的kNN搜索选项,类似于以下内容

POST movies/_search
{
  "knn": {
    "field": "title_vector.predicted_value",
    "query_vector_builder": {
      "text_embedding": {
        "model_id": ".multilingual-e5-small",
        "model_text": "Good Ugly"
      }
    },
    "k": 3,
    "num_candidates": 100
  },
  "_source": [
    "id",
    "title"
  ]
}

如代码片段所示,knn查询使用向量搜索获取相关查询结果(电影标题为“Good Ugly”)。搜索在多维空间中进行,生成与给定查询向量最接近的向量。

从上面的查询中,请注意两个属性:num_candidates是需要考虑的初始候选池,k是最近邻的数量。

kNN关键参数 - k和num_candidates

为了有效地利用kNN功能,需要对两个关键参数有细致的了解:k - 要检索的全局最近邻的数量,以及num_candidates - 在搜索期间每个分片考虑的候选邻的数量。

选择knum_candidates的最佳值涉及平衡精度、召回率和性能。这些参数在有效处理机器学习应用程序中常见的维数高的向量空间方面发挥着至关重要的作用。

k的最佳值很大程度上取决于具体的用例。例如,如果您正在构建一个推荐系统,较小的k(例如,10-20)可能足以提供相关的推荐。相反,对于需要聚类或异常检测功能的用例,您可能需要更大的k

请注意,较高的k值会显着增加计算和内存使用量,尤其是在大型数据集的情况下。重要的是测试不同的k值以找到结果相关性和系统资源使用之间的平衡。

K:揭示最近邻

我们可以根据需要选择k值。有时,设置较低的k值或多或少会得到您想要的结果,但例外情况是,一些结果可能无法进入最终输出。但是,设置较高的k值可能会扩大搜索结果的数量,但需要注意的是,您有时可能会收到多样化的结果。

假设您在浩瀚的推荐书库中搜索一本新书。k(也称为最近邻的数量)决定了将向您展示多少本书。可以将其视为搜索结果的内圈。让我们看看设置较低和较高的k值如何影响查询返回的书籍数量。

设置较低的K

较低的K设置优先考虑极高的精度——这意味着我们将收到少数与我们的查询向量最相似的书籍。这确保了与我们特定兴趣的高度相关性。如果您正在搜索主题或写作风格非常具体的书籍,这可能是理想的选择。

设置较高的K

使用较大的K值,我们将获取更广泛的探索结果集。请注意,结果可能不会那么专注于您的确切查询。但是,您会遇到各种潜在有趣的书籍。这种方法对于使您的阅读列表多样化并发现意想不到的珍宝可能很有价值。

每当我们说k的较高或较低值时,我们的意思是实际值取决于多个因素,例如数据集的大小、可用的计算能力和其他因素。在某些情况下,k=10可能很大,但在其他情况下它也可能很小。因此,请注意此参数预期运行的环境。

num_candidates属性:幕后

虽然k决定了您看到的最终书籍数量,但num_candidates在幕后发挥着至关重要的作用。它本质上定义了每个分片的搜索空间——分片中书籍的初始池,从中识别最相关的K个邻居。当我们发出查询时,我们期望提示Elasticsearch在每个分片的前“x”个候选者中运行查询。

例如,假设我们的书籍索引包含5000本书,均匀分布在五个主分片中(即每个分片约1000本书)。当我们执行搜索时,显然为每个分片选择所有1000个文档既不可行也不正确。相反,我们将从1000个文档中挑选最多25个文档(即我们的num_candidates)。这相当于125个文档作为我们的总搜索空间(5个分片乘以每个分片25个文档)。

我们将让kNN查询知道从每个分片中选择25个文档,这个数字就是num_candidates参数。当执行kNN搜索时,“协调器”节点将请求查询发送到所有相关分片。每个分片的num_candidates个文档将构成搜索空间,并将从中提取前k个文档。例如,如果k为3,则将从每个分片的25个候选文档中选择前3个文档并返回给协调器节点。也就是说,协调器节点将从所有相关节点总共接收15个文档。然后对这15个顶级文档进行排名以获取全局前3个(k==3)文档。

此过程在以下图中进行了描述

以下是num_candidates对您的搜索意味着什么

设置较低的num_candidates

这种方法可能会限制搜索空间,可能会错过一些落在初始探索集之外的相关书籍。可以将其视为调查图书馆书架的一小部分。

设置较高的num_candidates

较高的num_candidates值会增加在我们选择的K中找到真正的最近邻的可能性。它扩展了搜索空间——也就是说——考虑了更多数量的候选者——因此导致搜索时间略有增加。因此,较高的值通常会提高准确性(因为错过相关向量的可能性降低),但以性能为代价。

平衡kNN参数的精度和性能

knum_candidates 的最佳值取决于一些因素和具体需求。如果我们优先考虑极高的精度,并希望获得更小的一组高度相关的结果,那么较低的 k 值和适中的 num_candidates 值可能是理想的选择。相反,如果探索和发现意想不到的书籍是您的目标,那么较高的 k 值和较大的 num_candidates 值可能更合适。

虽然没有严格的规则来定义 num_candidates 的“较低”或“较高”数值,但您需要根据您的数据集、计算能力和预期的精度来确定此数值。

kNN 参数实验

通过尝试不同的 K 和 num_candidates 组合,并监控搜索结果和性能,您可以微调搜索以在精度、探索和速度之间取得完美的平衡。请记住,没有放之四海而皆准的解决方案——最佳方法取决于您独特的目标和数据特征。

实际示例:使用 kNN 进行电影推荐

让我们以电影为例,创建一个手动“简单”框架,以便在搜索电影时了解 k 和 num_candidates 属性的影响。

手动框架

让我们了解如何为 kNN 搜索开发一个自定义框架来调整 knum_of_candidates 属性。

框架的机制如下

  • 在映射中创建电影索引,其中包含几个 dense_vector 字段来保存我们的向量化数据。
  • 创建一个嵌入管道,以便每个电影的标题和摘要字段都将使用 multilingual-e5-small 模型嵌入以存储向量。
  • 执行索引操作,该操作会遍历上述嵌入管道。相应的字段将被向量化。
  • 使用 kNN 功能创建搜索查询。
  • 根据需要调整 knum_candidates 选项。

让我们深入了解。

创建推理管道

我们将需要通过 Kibana 索引数据——这并非理想选择——但对于理解这个手动框架来说足够了。但是,每个被索引的电影都必须将其标题和摘要字段向量化,以便在我们数据上启用语义搜索。我们可以通过优雅地创建一个推理管道处理器并将其附加到我们的批量索引操作来做到这一点。

让我们创建一个推理管道

# Creating an inference pipeline processor
# The title and synopsis fields gets vectorised and stored in respective fields

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" }
      }
    }
  ]
}

如上所示,推理管道 movie_embedding_pipeline 为标题和摘要字段创建向量字段文本嵌入。它使用内置的 multilingual-e5-small 模型创建文本嵌入。

创建索引映射

我们需要创建一个映射,其中包含几个属性作为 dense_vector 字段。以下代码片段可以完成这项工作。

# Creating a movies index
# Note the vector fields
PUT movies
{
  "mappings": { 
    "properties": { 
      "title": {
        "type": "text",
        "fields": { 
          "original": {
            "type": "keyword"
          }
        }
      },
      "title_vector.predicted_value": {
        "type": "dense_vector",
        "dims": 384,
        "index": true
      },
      "synopsis": {
        "type": "text"
      },
      "synopsis_vector.predicted_value": {
        "type": "dense_vector",
        "dims": 384,
        "index": true
      },
      "actors": {
        "type": "text"
      },
      "director": {
        "type": "text"
      },
      "rating": {
        "type": "half_float"
      },
      "release_date": {
        "type": "date",
        "format": "dd-MM-yyyy"
      },
      "certificate": {
        "type": "keyword"
      },
      "genre": {
        "type": "text"
      }
    }
  }
}

执行上述命令后,我们将拥有一个新的电影索引,其中包含适当的密集向量字段,包括 title_vector.predicted_valuesynopsis_vector.predicted_value 字段,这些字段分别保存相应的向量。

在 8.10 版本之前,index 映射参数默认设置为 false。在 8.11 版本中,此参数默认设置为 true,因此无需显式指定它。

下一步是摄取数据。

索引电影

我们可以使用 _bulk 操作来索引一组电影——我正在重用为我的 Elasticsearch in Action 第二版书籍创建的数据集——它可以 在这里找到。

为完整起见,这里提供了一个使用 _bulk 操作进行摄取的代码片段。

POST _bulk?pipeline=movie_embedding_pipeline
{"index":{"_index":"movies","_id":"1"}}
{"title": "The Shawshank Redemption","synopsis": "Two imprisoned men bond over a number of years, finding solace and eventual redemption through acts of common decency.","actors": ["Tim Robbins", "Morgan Freeman", "Bob Gunton", "William Sadler"] ,"director":" Frank Darabont ","rating":"9.3","certificate":"R","genre": "Drama "}
{"index":{"_index":"movies","_id":"2"}}
{"title": "The Godfather","synopsis": "An organized crime dynasty's aging patriarch transfers control of his clandestine empire to his reluctant son.","actors": ["Marlon Brando", "Al Pacino", "James Caan", "Diane Keaton"] ,"director":" Francis Ford Coppola ","rating":"9.2","certificate":"R","genre": ["Crime", "Drama"] }
{"index":{"_index":"movies","_id":"3"}}
{"title": "The Dark Knight","synopsis": "When the menace known as the Joker wreaks havoc and chaos on the people of Gotham, Batman must accept one of the greatest psychological and physical tests of his ability to fight injustice.","actors": ["Christian Bale", "Heath Ledger", "Aaron Eckhart", "Michael Caine"] ,"director":" Christopher Nolan ","rating":"9.0","certificate":"PG-13","genre": ["Action", "Crime", "Drama"] }

确保您用完整的数据集替换脚本。

请注意,_bulk 操作后缀了管道(?pipeline=movie_embedding_pipeline),因此每部电影都会通过此管道,从而生成向量。

由于我们已经使用向量嵌入为 movies 索引添加了初始数据,所以现在是时候开始我们的实验,对 knum_candidates 属性进行微调了。

由于我们的电影索引中包含向量数据,我们将使用近似 k 近邻 (kNN) 搜索。例如,要推荐与具有父子情感的电影类似的电影(“父子”作为搜索查询),我们将使用 kNN 搜索来查找最近的邻居。

POST movies/_search
{
  "_source": ["title"], 
  "knn": {
    "field": "title_vector.predicted_value",
    "query_vector_builder": {
      "text_embedding": {
        "model_id": ".multilingual-e5-small",
        "model_text": "Father and son"
      }
    },
    "k": 5,
    "num_candidates": 10
  }
}

在给定的示例中,查询利用了顶级 kNN 搜索选项参数,该参数直接专注于查找最接近给定查询向量的文档。此搜索与 knn 查询位于顶层相比,查询位于顶层的一个关键区别在于,在前一种情况下,查询向量将由机器学习模型即时生成。

加粗的部分在技术上不正确。即时向量生成只能通过使用 query_vector_builder 而不是 query_vector 来实现,在其中您传入向量(在 ES 外部计算),但顶级 knn 搜索选项和 knn 搜索查询都提供了此功能。

该脚本根据我们的搜索查询(使用 query_vector_builder 块构建)获取相关结果。我们使用随机的 knum_candidates 值,分别设置为 5 和 10。

kNN 查询属性

上述查询具有一组属性,这些属性将构成 kNN 查询。有关这些属性的以下信息将帮助您更好地理解查询。

field 属性指定索引中包含文档向量表示的字段。在本例中,title_vector.predicted_value 是存储文档向量的字段。

query_vector_builder 属性是示例与更简单的 kNN 查询显著不同的地方。此配置不是提供静态查询向量,而是使用文本嵌入模型动态生成查询向量。该模型将一段文本(示例中的“父子”)转换为表示其语义含义的向量。

text_embedding 指示将使用文本嵌入模型生成查询向量。

model_id 是要使用的预训练机器学习模型的标识符,在本例中为 .multilingual-e5-small 模型。

model_text 属性是要由指定模型转换为向量的文本输入。这里,它是“父子”这两个词,模型将对其进行语义解释以查找类似的电影标题。

k 是要检索的最近邻的数量——也就是说,它决定了根据查询向量返回多少个最相似的文档。

num_candidates 属性是每个分片中作为潜在匹配的候选文档的更广泛集合,以确保最终结果尽可能准确。

kNN 结果

执行 kNN 基本搜索脚本应该会得到前 5 个结果——为了简洁起见,我只提供电影列表。

# The results should get you a set of 5 movies as shown in the list below:

"title": "The Godfather"
"title": "The Godfather: Part II"
"title": "Pulp Fiction"
"title": "12 Angry Men"
"title": "Life Is Beautiful"

正如您所料,“教父”(两部)是父子之间情感纽带的一部分,而“低俗小说”不应该出现在结果中(尽管查询是关于“纽带”的——“低俗小说”完全是关于几个人之间的纽带)。

现在我们已经设置了一个基本框架,我们可以根据需要调整参数并推断出近似设置。在我们调整设置之前,让我们了解一下 k 属性的最佳设置。

选择最佳 K 值

在 k 近邻 (kNN) 算法中选择最佳的 k 值对于在我们的数据集上获得最佳性能并最大程度地减少错误至关重要。但是,没有放之四海而皆准的答案,因为最佳的 k 值可能取决于一些因素,例如我们数据的具体情况以及我们试图预测的内容。

要选择最佳的 k 值,必须创建一个包含多种策略和考虑因素的自定义框架。

  • k = 1:第一步尝试使用 k=1 运行搜索查询。确保您在每次运行时更改输入查询。该查询可能会为您提供不可靠的结果,因为更改输入查询会导致随着时间的推移返回不正确的结果。这会导致一种名为“过拟合”的机器学习模式,其中模型过分依赖于直接邻域中的特定数据点。因此,模型难以推广到看不见的示例。
  • k = 5:使用 k=5 运行搜索查询并检查预测结果。搜索查询的稳定性应该得到理想的改善,并且您应该能够获得足够可靠的预测。

您可以逐步增加 k 的值——可能以 5 或 x 为步长增加——直到找到那个最佳点,在那里您会发现输入查询的结果非常准确,错误数量较少。

您也可以使用 k 的极值,例如,选择较高的 k=50 值,如下所述。

  • k = 50:将 k 值增加到 50 并检查搜索结果。错误的结果很可能会超过实际/预期预测。当您知道自己已经触及 k 值的硬边界时,就会出现这种情况。较大的 k 值会导致一种名为“欠拟合”的机器学习特征——kNN 中的欠拟合发生在模型过于简单并且无法捕获数据中潜在模式时。

选择最佳 num_candidates

num_candidates 参数在找到搜索精度和性能之间的最佳平衡方面起着至关重要的作用。与直接影响返回的搜索结果数量的 k 不同,num_candidates 确定初始候选集的大小,从中选择最终的 k 个最近邻。如前所述,num_candidates 参数定义每个分片上将选择多少个最近邻。

调整此参数对于确保搜索过程既高效又产生高质量的结果至关重要。

  • num_candidates = 小值(例如,10):作为初步步骤,先使用 num_candidates 的低值(“低值探索”)。目的是在此阶段建立性能基线。由于候选集只是一小部分候选,因此搜索速度很快,但可能会错过相关结果——这会导致精度较差。这种情况有助于我们了解搜索质量明显下降的最低阈值。
  • num_candidates = 中值(例如,25?):将 num_candidates 增加到中等值(“中值探索”),并观察搜索质量和执行时间的变化。适量的候选者可能会通过考虑更广泛的潜在邻居来提高结果的准确性。随着候选者数量的增加,资源成本也会增加,请注意这一点。因此,请密切监控性能指标。但是,随着搜索精度的提高,增加的计算成本可能是合理的。
  • num_candidates = 步长递增:继续逐步增加 num_candidates(增量式递增探索),可能以 20 或 50 为步长(取决于数据集的大小)。评估每次递增时,额外的候选者是否会对搜索精度产生有意义的改进。将存在一个收益递减点,在该点之后,进一步增加 num_candidates 对结果质量几乎没有改善。同时,您可能已经注意到,这会占用我们的资源并显着影响性能。
  • num_candidates = 高值(例如,1000、5000):尝试使用 num_candidates 的高值来了解选择较高设置的影响的上限。由于包含了不太相关的候选者,搜索精度可能会稳定或略有下降。这可能会导致最终 k 个结果的精度降低。请注意,正如我们一直在讨论的那样,num_candidates 的高值始终会增加计算负载——从而导致更长的查询时间和潜在的资源限制。

找到最佳平衡

现在我们知道如何调整 knum_candidates 属性,以及我们的实验对不同设置的影响将如何改变搜索精度的结果。

目标是在搜索结果始终准确且处理大型候选集带来的性能开销可控的情况下找到一个最佳点。

当然,最佳值会根据我们数据的具体情况、向量的维度以及其他性能需求而有所不同。

总结

最佳 K 值在于通过实验和试错找到最佳点。您需要使用足够的邻居(K 值较低),以捕捉基本的模式,但不要太多(k 值较高),以免模型过度受噪声或不相关细节的影响。您还需要调整候选集,以便在给定的 k 值下搜索结果准确。

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

想获得 Elastic 认证?了解下一场 Elasticsearch 工程师培训 何时举行!

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

仅仅依靠个人的努力无法实现足够先进的搜索。Elasticsearch 由数据科学家、机器学习运营人员、工程师以及许多其他对搜索充满热情的人共同驱动,他们与您一样热爱搜索。让我们携手合作,构建出能为您带来理想结果的奇妙搜索体验。

亲自试一试