简化 KNN 检索【翻译】Simplifying kNN search

简化 KNN 检索

#转载 #大数据/ES #翻译

这篇文章是关于如何简化 k 最近邻(k-Nearest Neighbors,简称 kNN)搜索的深入探讨。以下是对全文的翻译(借助 kimi AI):


在这篇博客文章中,我们将深入探讨我们为使 kNN 搜索的入门体验更加轻松所做的努力!

向量搜索

向量搜索在 Elasticsearch 中已经可用了一段时间,通过使用新的专用 knn 搜索类型,同时我们也在 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 成为可选的

所以我们希望使 knum_candidates 成为可选的。太好了!我们应该如何设置默认值呢?

有两种选择。选择一个看起来像是个好选项的,发布它,然后希望最好的结果,或者做艰苦的工作并进行广泛的评估,让数据驱动我们的决策。在 Elastic,我们喜欢这样的挑战,并希望确保任何决策都是有理由的,并且是出于好的原因!

正如我们上面提到的,kNN 搜索的 k 是每个分片返回的结果数量,所以一个明显的默认值就是使用 size。因此,每个分片将返回 size 个结果,我们将合并并排序它们以找到全局前 size 个结果。这也与没有 k 参数的 kNN 查询很好地配合,而是根据请求的 size 操作(记住,knn 查询的行为就像任何其他查询,如 termprefix 等)。所以,size 似乎是一个合理的默认值,可以涵盖大多数用例(或者至少在入门体验中足够好!)。

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

我们可以考虑采取多种方法:

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

作为开始,并保持事情简单,我们研究了将 num_candidates 值相对于 k(或 size)进行设置。因此,您实际想要检索的结果越多,我们在每个图上执行的探索就越多,以确保我们从局部最小值中逃脱。我们将重点关注的候选选项如下:

  • 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 的测试场景),以包含额外的指标配置,添加了一些新的,最终得到了以下赛道集:

  • dense-vector(200 万文档,96 维):https://github.com/elastic/rally-tracks/tree/master/dense_vector
  • so-vector(200 万文档,768 维):https://github.com/elastic/rally-tracks/tree/master/so_vector
  • cohere-vector(300 万文档,768 维):https://github.com/elastic/rally-tracks/tree/master/cohere_vector
  • openai-vector(250 万文档,1536 维):https://github.com/elastic/rally-tracks/tree/master/openai_vector
  • Glove 200d(120 万,200 维)基于https://github.com/erikbern/ann-benchmarks repo 创建的新赛道

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

  • 一个我们执行 force_merge 并拥有单个段,
  • 一个我们依赖底层的 MergeScheduler 来做它的魔法,最终得到它认为合适的段数。

指标

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

开始实际工作

现在我们知道了我们想要测试的内容、要使用的数据集以及如何评估结果,是时候实际运行基准测试了!

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

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

结果

每个指定数据集和参数组合的召回率-延迟图可以在下面找到(越高越向左越好,延迟以毫秒为单位测量):

针对dense_vectoropenai_vector赛道的绝对延迟@50 th 百分位值和召回率值如下:

类似地,每个场景下 HNSW 图形节点访问的 99 th 百分位数可以在下面找到(越小越好):

少即是多*

*嗯,在所有情况下都不是这样,但嘿 😃

从结果来看,有两点比较突出:

一个分段与多个分段明显成反比地影响召回率和延迟指标。分段越少,延迟就越短(因为我们需要遍历的图更少,也就是需要运行的搜索更少),这固然很好,但它也会以相反的方式影响召回率,因为很有可能一些好的候选者会被遗漏(因为候选者列表数量更少)。
即使探索很少,我们也能在几乎所有情况下获得足够好的召回率,这非常好!
我们一直在努力改进多分段搜索(本文章中就有一个很好的例子),因此我们希望在今后的工作中,这种取舍将不再是一个问题(此处报告的数据不包括这些改进)。

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

Num_candidates = 1.5 * k - 这在几乎所有情况下都能达到足够好的召回率,同时获得非常好的延迟分数。
Num_candidates = Math.Max (100, k)–这种方法的召回率稍高,尤其是在 k 值较低的情况下,但代价是增加了图形探索和延迟。
经过仔细考虑和(漫长的!)讨论,我们选择了前者作为默认设置,即设置 num_candidates = 1.5 * k。在我们测试的几乎所有情况下,我们需要做的探索工作要少得多,召回率也都超过了 90%(有些报告的数字还明显更高),这应该能带来足够好的上机体验。

结论

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

需要记住的重要一点是,这些值只是简化上架体验和非常通用的使用案例的合理默认值。人们可以在自己的数据集上轻松地进行实验,并根据自己的需求进行相应的调整(例如,在某些情况下,召回可能比延迟更重要)。

调优愉快 😃

相关推荐

  1. 简化 KNN 检索翻译】Simplifying kNN search

    2024-05-11 13:30:05       12 阅读
  2. KNN 回归

    2024-05-11 13:30:05       36 阅读
  3. 全文检索&ElasticSearch简介

    2024-05-11 13:30:05       8 阅读

最近更新

  1. TCP协议是安全的吗?

    2024-05-11 13:30:05       18 阅读
  2. 阿里云服务器执行yum,一直下载docker-ce-stable失败

    2024-05-11 13:30:05       19 阅读
  3. 【Python教程】压缩PDF文件大小

    2024-05-11 13:30:05       19 阅读
  4. 通过文章id递归查询所有评论(xml)

    2024-05-11 13:30:05       20 阅读

热门阅读

  1. Milvus基本概念

    2024-05-11 13:30:05       12 阅读
  2. [排序算法]基数排序

    2024-05-11 13:30:05       10 阅读
  3. ROS——Action学习

    2024-05-11 13:30:05       9 阅读
  4. vue-video-play使用之播放hls格式视频

    2024-05-11 13:30:05       15 阅读
  5. React 学习-6-列表 & keys

    2024-05-11 13:30:05       11 阅读
  6. Android Activity.FLAG.ACTIVITY_NEW_TASK是什么

    2024-05-11 13:30:05       9 阅读
  7. sklearn框架介绍

    2024-05-11 13:30:05       10 阅读
  8. 如何打造个人IP?

    2024-05-11 13:30:05       8 阅读