简化 kNN 搜索

Elastic 的 kNN 搜索已经发展。这篇博客解释了 kNN 搜索的简化,其中 k 和 num_candidates 现在是可选的。

在这篇博文中,我们将深入探讨我们为简化 kNN 搜索入门体验所做的努力!

向量搜索概述

通过使用新的专用knn搜索类型,Elasticsearch 中已经提供向量搜索有一段时间了,同时我们还在8.12.0版本中引入了 kNN 作为查询(更多信息请参考我们最近发布的这篇精彩的博客文章!)。

虽然每种方法的执行流程和应用有一些差异,但进行基本 kNN 检索的语法非常相似。因此,典型的knn搜索请求如下所示

GET products/_search
{
    "knn": {
     "field": "my_vector",
     "query_vector": [1, 2, 3],
     "k": 5,
     "num_candidates": 10
   }
}

前几个参数非常直观:我们指定数据存储位置 (field) 以及我们要与其进行比较的内容 (query_vector)。

另一方面,knum_candidates 参数有点模糊,需要一些理解才能对其进行微调。它们特定于我们使用的算法和数据结构,即 HNSW,并且主要用于控制我们想要执行的图探索量。

Elasticsearch 文档是关于搜索方面所有内容的绝佳资源,因此可以查看我们提供的knn 部分

k:作为顶级命中返回的最近邻数。此值必须小于num_candidates
num_candidates -> 每个分片要考虑的最近邻候选数。需要大于k,或者如果省略k则大于size,并且不能超过 10,000。Elasticsearch 从每个分片收集num_candidates个结果,然后将它们合并以找到前k个结果。增加num_candidates往往会提高最终k个结果的准确性。

但是,当您第一次遇到这样的情况时,这些值应该是什么并不那么明显,正确配置它们可能会成为一项挑战。这些值越大,我们将探索的向量就越多,但这会带来性能成本。我们再次面临着准确性和性能之间永恒的权衡。

为了使 kNN 搜索更容易和更直观地使用,我们决定使这些参数可选,以便您只需要提供要搜索的位置和内容,并且只有在真正需要时才能对其进行调整。虽然看起来这是一个相当小的变化,但这使得事情更加清晰!因此,上面的查询现在可以简单地改写为

GET products/_search
{
    "knn": {
     "field": "my_vector",
     "query_vector": [1, 2, 3]
   }
}

通过使knum_candidates可选来简化 kNN 搜索

所以我们要使knum_candidates可选。这很好!但是我们应该如何设置默认值呢?

此时有两个选择。选择看起来像一个好选项的东西,发布它,并寄希望于一切顺利,或者努力工作并进行广泛的评估,让数据来指导我们的决策。在 Elastic,我们热爱这样的挑战,并希望确保任何做出的决定都是合理的,并且有充分的理由!

在上面,我们声明 kNN 搜索的k是我们从每个分片获得的结果数,因此这里一个明显的默认值就是使用size。因此,每个分片将返回size个结果,我们将合并并对它们进行排序以找到全局前size个结果。这在 kNN 查询中也非常有效,在 kNN 查询中我们根本没有k参数,而是基于请求的size进行操作(记住,knn查询的行为与其他任何查询(如termprefix等)一样)。因此,size似乎是一个合理的默认值,可以涵盖大多数用例(或者至少在入门体验期间足够好!)。

另一方面,num_candidates则有所不同。此参数特定于HNSW算法,并控制我们将考虑的最近邻队列的大小(对于好奇的人:这等同于原始论文中的ef参数)

我们可以采取多种方法

  • 我们可以考虑每个图的大小,并提出一个函数来计算N个索引向量的适当num_candidates
  • 我们可以查看底层数据分布,并尝试估计所需的探索(也许还可以考虑 HNSW 的入口点)
  • 我们可以假设num_candidates与索引数据没有直接关系,而是与搜索请求有关,并确保我们将进行必要的探索以提供足够好的结果。

作为开始并为了保持简单,我们研究了设置相对于k(或size)的num_candidates值。因此,您实际想要检索的结果越多,我们将在每个图上执行的探索就越多,以确保我们能够摆脱局部最小值。我们将主要关注的候选者如下所示

  • num_candidates = k
  • num_candidates = 1.5 * k
  • num_candidates = 2 * k
  • num_candidates = 3 * k
  • num_candidates = 4 * k
  • num_candidates = Math.max(100, k)

值得在此处注意的是,最初检查了更多替代方案,但较高的值几乎没有带来好处,因此在接下来的博客中,我们将主要关注上述内容。

有一组num_candidates候选者(双关语!),我们现在关注k参数。我们选择同时考虑标准搜索和非常大的k值(以查看我们所做探索的实际影响)。因此,我们决定更多关注的值是

  • k = 10(用于未指定size的请求)
  • k = 20
  • k = 50
  • k = 100
  • k = 500
  • k = 1000

数据

由于没有一刀切的解决方案,我们希望使用具有不同属性的不同数据集进行测试。因此,不同的总向量数、维度,以及由不同的模型生成,因此数据分布也不同。

同时,我们有rally可用,这是一个用于基准测试 Elasticsearch 的很棒的工具(https://github.com/elastic/rally),并且支持运行大量查询并为许多向量数据集提取指标。在 Elastic,我们广泛使用 rally 并依赖它进行所有夜间基准测试,您可以在此处查看!

在 rally 上运行基准测试就像运行以下命令一样简单

pip3 install esrally && esrally race --track=dense-vector

为此,我们稍微修改了轨道(即 rally 的测试场景)以包含其他指标配置,添加了一些新的指标,最终得到以下轨道集

还值得注意的是,对于前几个数据集,我们还希望同时考虑具有一个段与多个段的情况,因此我们包含了每个数据集的 2 个变体,

  • 一个是在我们执行force_merge并拥有单个段的情况,以及
  • 一个是我们依赖底层的MergeScheduler来发挥其作用,并最终得到它认为合适的段数。

指标

对于上述每个轨道,我们计算了标准的召回率和精度指标、延迟,以及我们通过报告访问的节点来执行的图的实际探索。前几个指标针对_真实最近邻_进行评估,因为在我们的场景中,这是黄金标准数据集(记住,我们正在评估_近似_搜索的质量,而不是向量本身的质量)。nodes_visited属性最近已添加到 knn 的配置文件输出中(https://github.com/elastic/elasticsearch/pull/102032),因此,通过对轨道定义进行一些小的更改以提取所有需要的指标,我们应该就可以开始了!

基准测试

既然我们已经知道要测试什么、使用哪些数据集以及如何评估结果,那么现在是时候实际运行基准测试了!

为了拥有一个标准化的环境,我们为每个测试使用了一个干净的n2-standard-8 (8 vCPU, 4 核, 32 GB 内存)云节点。Elasticsearch 配置,以及必要的映射和所有其他所需内容,都是通过rally配置和部署的,因此对于所有类似的测试都是一致的。

上面每个数据集都执行了多次,收集了所有候选集的所有可用指标,确保没有结果是一次性的。

结果

每个指定数据集和参数组合的召回率-延迟图如下所示(越高越靠左越好,延迟以毫秒为单位测量)

缩小到**dense_vector**和**openai_vector**轨迹的延迟第50百分位数和召回率的绝对值,我们有

同样,下面可以找到每个场景中HNSW图节点访问的第99百分位数(越小越好)

少即是多*

*好吧,并非所有情况下都是如此,但是嘿 :)

看一下结果,有两件事很突出

  • 使用一个段与多个段显然会以反比例的方式影响召回率和延迟指标。使用较少的段可以降低延迟(因为我们只需要遍历更少的图,即运行更少的搜索),这很好,但它也会以相反的方式影响召回率,因为一些好的候选者可能会被遗漏(由于较少的num_candidates列表)。
  • 即使进行少量探索,我们几乎在所有情况下都能获得足够好的召回率,这很棒!

我们一直在努力改进多段搜索(这篇博文中有一个很好的例子),因此我们预计这种权衡在未来会变得不那么重要(此处报告的数字不包括这些改进)。

综合考虑所有因素,我们讨论的两个主要选项如下:

  • num_candidates = 1.5 * k - 这几乎在所有情况下都能获得足够好的召回率,并且延迟分数非常好。
  • num_candidates = Math.max(100, k) - 这尤其是在较低的k值下可以获得略好一些的召回率,但代价是增加了图的探索和延迟。

经过仔细考虑和(漫长!)的讨论,我们选择前者作为默认值,即设置num_candidates = 1.5 * k。我们只需要进行更少的探索,并且在几乎所有测试案例中召回率都超过90%(一些报告的数字也明显更高),这应该可以提供足够好的入门体验。

结论

我们在Elastic中处理kNN搜索的方式一直在发展,我们不断引入新的功能和改进,因此这些参数和整体评估很快就会过时!我们一直在关注,每当发生这种情况时,我们都会确保跟进并适当地调整我们的配置!

要记住的一件重要事情是,这些值仅作为简化入门体验和非常通用的用例的合理默认值。用户可以轻松地在自己数据集上进行实验,并根据自己的需求进行相应的调整(例如,在某些情况下,召回率可能比延迟更重要)。

调整愉快 😃

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

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

足够先进的搜索并非一人之力所能完成。Elasticsearch 由数据科学家、机器学习运维工程师和其他许多同样对搜索充满热情的人员提供支持。让我们联系并一起构建神奇的搜索体验,让您获得想要的结果。

自己试试看