前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Elasticsearch: 简化 K-NN 搜索的入门体验

Elasticsearch: 简化 K-NN 搜索的入门体验

原创
作者头像
点火三周
发布2024-04-22 11:33:56
2490
发布2024-04-22 11:33:56
举报
文章被收录于专栏:Elastic Stack专栏Elastic Stack专栏

在这篇博客文章中,我们将深入了解我们为使 K-NN(K-最近邻)搜索的入门体验更加轻松所做的努力!

向量搜索

Elasticsearch 已经通过新的专用 knn 搜索类型提供了一段时间的向量搜索功能,同时我们在 8.12.0 版本中也将 knn 作为查询引入(更多内容可以查看我们最近发布的这篇精彩博客文章)。

虽然每种方法的执行流程和应用场景有一些差异,但进行基本 knn 检索的语法非常相似。因此,一个典型的 knn 搜索请求看起来像这样:

代码语言:javascript
复制
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 搜索更加容易和直观,我们决定使这些参数成为可选的,这样您只需要提供您想要搜索的位置和内容,如果需要,您还可以调整它们。虽然看起来只是一个相当小的变化,但它使事情变得更加清晰!所以,上述查询现在可以简单地重写为:

代码语言:javascript
复制
GET products/\_search

{

   "knn": {

     "field": "my\_vector",

     "query\_vector": [1, 2, 3]

   }

}

使 knum\_candidates 成为可选

那么,我们希望使 knum\_candidates 成为可选的。太好了!那我们应该如何设置默认值呢?

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

正如我们刚才所说,k 对于 knn-search 是我们从每个分片获得的结果数量,所以这里一个明显默认值就是使用 size。因此,每个分片将返回 size 结果,我们将合并和排序它们以找到全局顶部 size 结果。这也与 knn-query 非常契合,因为我们根本没有 k 参数,而是根据请求的 size 进行操作(记住,knn 查询的行为就像任何其他查询,如 termprefix 等)。所以,size 看起来像是一个合理的默认值,可以涵盖大多数用例(或者至少在入门体验期间足够好!)。

另一方面,num\_candidates 是一个完全不同的东西。这个参数特定于 HNSW 算法,控制我们将要考虑的最近邻队列的大小(好奇的:这相当于原始论文中的 ef 参数)

我们可以考虑的多种方法包括:

  • 我们可以考虑到每个图的大小,并想出一个函数,计算适当的 num\_candidates 对于 N 索引向量
  • 我们可以查看底层数据分布,并尝试猜测所需的探索(也许还通过考虑 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,这是一个很棒的基准测试工具(https://github.com/elastic/rally),它已经支持运行一组查询并提取多个向量数据集的指标。

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

代码语言:javascript
复制
pip3 install esrally && esrally race --track=dense-vector

为此,我们稍微修改了赛道(即 rally 的测试场景),以包括额外的指标配置,添加了一些新的,最终得到了以下赛道集合:

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

  • 一个我们执行 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
dense_vector_multiple_segments
so_vector
so_vector_multiple_segments

Dense Vector

Dense Vector Multiple Segments

SO Vector

SO Vector Multiple Segments

glove
cohere
openai

Glove Vector

Cohere Vector

OpenAI Vector

细化到 dense_vectoropenai_vector 赛道,我们有绝对值的延迟@50th 百分位和召回率:

dense_vector
dense_vector
openai_vector
openai_vector

Dense Vector latency@50

Dense Vector recall

OpenAI latency@50

OpenAI Recall

类似地,每个场景下 HNSW 图访问节点的 99th 百分位如下(越小越好):

Dense Vector
Dense Vector Multiple Segments
SO Vector
SO Vector Multiple Segments

Dense Vector

Dense Vector Multiple Segments

SO Vector

SO Vector Multiple Segments

Glove Vector
Cohere Vector
OpenAI Vector

Glove Vector

Cohere Vector

OpenAI Vector

少即是多*

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

查看结果时,有两件事脱颖而出:

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

我们不断致力于改进多段搜索(这里可以找到一个很好的例子),所以我们期望这种权衡将不再是一个问题(这里报告的数字不包括这些改进)。

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

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

经过仔细考虑和(漫长的!)讨论,我们选择前者作为默认值,即设置 num_candidates = 1.5 * k。我们不得不进行的探索要少得多,召回率一致地超过 75% 的门槛,在大多数情况下超过 90%,这应该能够提供足够好的入门体验。

结论

我们在 Elastic 处理 knn 搜索的方式总是在不断发展,我们不断引入新功能和改进,所以这些参数和这次评估本身可能很快就会过时!我们始终保持警惕,一旦发生这种情况,我们会确保跟进并相应地调整我们的配置!

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

tuning 快乐 😃

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 向量搜索
  • 使 k 和 num\_candidates 成为可选
  • 数据
  • 指标
  • 动手实践
  • 结果
  • 少即是多*
  • 结论
相关产品与服务
Elasticsearch Service
腾讯云 Elasticsearch Service(ES)是云端全托管海量数据检索分析服务,拥有高性能自研内核,集成X-Pack。ES 支持通过自治索引、存算分离、集群巡检等特性轻松管理集群,也支持免运维、自动弹性、按需使用的 Serverless 模式。使用 ES 您可以高效构建信息检索、日志分析、运维监控等服务,它独特的向量检索还可助您构建基于语义、图像的AI深度应用。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档