在这篇博客文章中,我们将深入了解我们为使 K-NN(K-最近邻)搜索的入门体验更加轻松所做的努力!
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
)。
另一方面,k
和 num\_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]
}
}
k
和 num\_candidates
成为可选那么,我们希望使 k
和 num\_candidates
成为可选的。太好了!那我们应该如何设置默认值呢?
目前有两种选择。选择一个看起来不错的选项,发布它,然后希望最好的事情发生,或者做艰苦的工作,进行广泛的评估,让数据驱动我们的决策。在 Elastic,我们喜欢这样的挑战,并希望确保我们采取的任何决定都是有理由的,并且是出于好的原因!
正如我们刚才所说,k
对于 knn-search 是我们从每个分片获得的结果数量,所以这里一个明显默认值就是使用 size
。因此,每个分片将返回 size
结果,我们将合并和排序它们以找到全局顶部 size
结果。这也与 knn-query 非常契合,因为我们根本没有 k
参数,而是根据请求的 size
进行操作(记住,knn 查询的行为就像任何其他查询,如 term
、prefix
等)。所以,size
看起来像是一个合理的默认值,可以涵盖大多数用例(或者至少在入门体验期间足够好!)。
另一方面,num\_candidates
是一个完全不同的东西。这个参数特定于 HNSW
算法,控制我们将要考虑的最近邻队列的大小(好奇的:这相当于原始论文中的 ef
参数)
我们可以考虑的多种方法包括:
num\_candidates
对于 N
索引向量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 基准测试就像运行以下命令一样简单:
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 仓库创建的新赛道还值得一提的是,对于前几个数据集,我们还想考虑拥有一个与多个段的情况,因此我们包含了每种的两个变体,
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 |
Glove Vector | Cohere Vector | OpenAI Vector |
细化到 dense_vector 和 openai_vector 赛道,我们有绝对值的延迟@50th 百分位和召回率:
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 |
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 删除。