使用 Elasticsearch 设计大规模向量搜索

探索在 Elasticsearch 中运行大规模向量搜索的成本、性能和基准测试,重点关注高保真密集向量搜索。

简介

在设计向量搜索体验时,大量可用的选项可能会让人感到不知所措。最初,管理少量向量非常简单,但随着应用程序规模的扩大,这很快就会成为瓶颈。

在本系列博文中,我们将探讨在各种数据集和用例中使用 Elasticsearch 运行大规模向量搜索的成本和性能。

我们从最大的公开可用向量数据集之一开始:Cohere/msmarco-v2-embed-english-v3。此数据集包含从 MSMARCO-passage-v2 集合 中的网页提取的 1.38 亿个段落,并使用 Cohere 的最新 embed-english-v3 模型 嵌入到 1024 个维度中。

对于此实验,我们定义了一个 可复现的 track,您可以在自己的 Elastic 部署中运行它,以帮助您衡量自己的高保真密集向量搜索体验。

它适用于实时搜索用例,其中单个搜索请求的延迟必须很低(<100 毫秒)。它使用 Rally(我们的开源工具)跨 Elasticsearch 版本进行基准测试。

在这篇文章中,我们使用我们的 浮点向量默认自动量化。这将运行向量搜索的 RAM 成本降低了 75%,而不会影响检索质量。我们还提供了在索引数十亿个维度时合并和量化的影响的见解。

我们希望此 track 能作为一个有用的基线,尤其是在您没有特定于您的用例的向量时。

关于嵌入的说明

选择满足您需求的正确模型不在本博文的范围内,但在接下来的部分中,我们将讨论压缩向量原始大小的不同技术。

套娃表示学习 (MRL)

通过在较早的维度中存储最重要的信息,像 套娃嵌入 这样的新方法可以在保持良好搜索准确性的同时缩减维度。使用此技术,某些模型的大小可以减半,并且仍然保持其在 MTEB 检索基准测试上的 90% NDCG@10。但是,并非所有模型都兼容。如果所选模型未针对套娃降维进行训练,或者其维度已达到最小值,则必须直接在向量数据库中管理维度。

幸运的是,来自 mixedbreadOpenAI 的最新模型内置了对 MRL 的支持。

对于此实验,我们选择专注于维度固定的用例(1024 个维度),玩转其他模型的维度将是以后的话题。

嵌入量化学习

模型开发人员现在通常提供具有各种权衡的模型来解决高维向量带来的开销。这些模型不是仅仅专注于降维,而是通过调整每个维度的精度来实现压缩。

通常,嵌入模型经过训练可以使用 32 位浮点数生成维度。但是,训练它们以产生精度降低的维度有助于最大程度地减少错误。开发人员通常会发布针对众所周知的精度的模型,这些精度直接与编程语言中的本机类型保持一致。

例如,int8 表示范围从 -127 到 127 的有符号整数,而 uint8 表示范围从 0 到 255 的无符号整数。二进制是最简单的形式,表示一位(0 或 1),对应于每个维度的最小可能单位。

在训练期间实施量化允许微调模型权重以最大程度地减少压缩对检索性能的影响。但是,深入研究此类模型的具体训练细节不在本博客的讨论范围内。

在下一节中,我们将介绍一种方法,如果所选模型缺少此功能,则可以应用自动量化。

自适应嵌入量化

在模型缺乏量化感知嵌入的情况下,Elasticsearch 采用了一种自适应量化方案,该方案默认为将浮点数量化为 int8。

这种通用的 int8 量化通常会导致 可忽略的 性能损失。这种量化的优势在于它对 数据漂移 的适应性。

它利用了一种动态方案,其中可以不时地重新计算量化边界以适应数据中的任何变化。

大规模基准测试

粗略估计

对于 1.383 亿个文档和 1024 维向量,存储原始浮点向量的 MSMARCO-v2 数据集的原始大小超过 520GB。使用蛮力搜索整个数据集在一个节点上需要花费数小时。

幸运的是,Elasticsearch 提供了一种称为 HNSW(分层可导航小世界图)的数据结构,旨在加速 最近邻搜索。这种结构允许快速近似最近邻搜索,但要求每个向量都必须在内存中。

从磁盘加载这些向量非常昂贵,因此我们必须确保系统有足够的内存来将它们全部保存在内存中。

每个向量在 1024 个维度上,每个维度 4 个字节,因此需要 4 千字节的内存。

此外,我们需要考虑将分层可导航小世界 (HNSW) 图加载到内存中所需的内存。在图中每个节点默认设置 32 个邻居的情况下,每个向量需要额外的 128 个字节(每个邻居 4 个字节)的内存来存储图,这相当于存储向量维度内存成本的大约 3%。

确保有足够的内存来满足这些要求对于获得最佳性能至关重要。

Elastic Cloud 上,我们的向量搜索优化配置文件为 JVM(Java 虚拟机)保留了总节点内存的 25%,使每个数据节点上剩余的 75% 内存可用于系统页面缓存,向量将加载到其中。对于具有 60GB RAM 的节点,这相当于有 45GB 的页面缓存可用于向量。向量搜索优化配置文件可在所有云解决方案提供商 (CSP) AWS、AzureGCP 上使用。

为了容纳所需的 520GB 内存,我们需要 12 个节点,每个节点有 60GB RAM,总计 720GB。

在撰写本博文时,此设置可以在我们的 Cloud 环境中部署,在 AWS 上每小时的总成本为 14.44 美元:(请注意,Azure 和 GCP 环境的价格会有所不同)

通过利用自动量化到字节,我们可以将内存需求减少到 130gb,这仅仅是原始大小的四分之一。

应用相同的 25/75 内存分配规则,我们可以在 Elastic Cloud 上分配总共 180 gb 的内存。

在撰写本博文时,此优化设置在 Elastic Cloud 上每小时的总成本为 3.60 美元(请注意,Azure 和 GCP 环境的价格会有所不同)

在 Elastic Cloud 上开始 免费试用,只需选择新的向量搜索优化配置文件即可开始。

在这篇文章中,我们将探索使用我们创建的基准测试来实验大规模向量搜索性能的这种经济高效的量化方法。通过这样做,我们的目标是演示如何能够在保持高搜索准确性和效率的同时实现显着的成本节约。

基准测试配置

msmarco-v2-vector rally track 定义了将使用的 默认映射

它包括一个具有 1024 个维度的密集向量字段,使用自动 int8 量化进行索引,以及一个类型为关键字的 doc_id 字段,用于唯一标识每个段落。

在此实验中,我们测试了两种配置

  • 默认:这是基线,在 Elasticsearch 上使用默认选项使用 track。
  • 积极合并:此配置提供了一个具有不同权衡的比较点。

如前所述,Elasticsearch 中的每个分片都由段组成。段是数据的不可变分区,包含直接查找和搜索数据所需的结构。

文档索引涉及在内存中创建段,这些段会定期刷新到磁盘。

为了管理段的数量,后台进程会合并段,以使总数量保持在某个预算之下。

这种合并策略对于向量搜索至关重要,因为 HNSW 图在每个段内都是独立的。每个密集向量字段搜索都涉及在每个段中查找最近邻,这使得总成本取决于段的数量。

默认情况下,Elasticsearch 会合并大小大致相等的段,并遵循由每层允许的段数控制的分层策略。

此设置的默认值为 10,这意味着每个级别应该不超过 10 个大小相似的段。例如,如果第一层包含 50MB 的段,则第二层将包含 500MB 的段,第三层包含 5GB 的段,依此类推。

aggressive merge 配置会调整默认设置,使其更具侵略性。

  • 它将每层段数设置为 5,从而实现更积极的合并。
  • 它将最大合并段大小从 5GB 增加到 25GB,以最大化单个段中的向量数量。
  • 它将最低段大小设置为 1GB,人为地将第一层起始大小设为 1GB。

使用此配置,我们预计搜索速度更快,但索引速度会变慢。

对于此实验,我们保留了 HNSW 图的mef_constructionconfidence_interval 选项的默认设置,两种配置均相同。对这些索引参数进行实验将是另一篇博文的主题。在第一部分中,我们选择专注于更改合并和搜索参数。

在运行基准测试时,必须将负载驱动程序(负责发送文档和查询)与被评估的系统(Elasticsearch 部署)分开。加载和查询数亿个密集向量需要额外的资源,如果同时运行,这些资源会干扰被评估系统的搜索和索引功能。

为了最大程度地减少系统和负载驱动程序之间的延迟,建议在与 Elastic 部署相同的云提供商区域中运行负载驱动程序,理想情况下在同一可用区中。

对于此基准测试,我们在 AWS 上配置了一个 im4gn.4xlarge 节点,该节点具有 16 个 CPU、64GB 内存和 7.5TB 磁盘,与 Elastic 部署位于同一区域。此节点负责将查询和文档发送到 Elasticsearch。通过以这种方式隔离负载驱动程序,我们确保准确测量 Elasticsearch 的性能,而不会受到额外资源需求的干扰。

我们使用以下配置运行了整个基准测试。

    "track.params": {
        "mapping_type": "vectors-only",
        "vector_index_type": "int8_hnsw",
        "number_of_shards": 4,
        "initial_indexing_bulk_indexing_clients": 12,
        "standalone_search_clients": 8
    }

initial_indexing_bulk_indexing_clients 的值为 12 表示我们将使用 12 个客户端从负载驱动程序获取数据。在 Elasticsearch 数据节点中总共有 23.9 个 vCPU,使用更多客户端发送数据可以提高并行性,并使我们能够充分利用部署中所有可用的资源。

对于搜索操作,standalone_search_clientsparallel_indexing_search_clients 的值为 8 表示我们将使用 8 个客户端从负载驱动程序并行查询 Elasticsearch。客户端的最佳数量取决于多个因素;在本实验中,我们选择了客户端数量以最大化所有 Elasticsearch 数据节点的 CPU 使用率。

为了比较结果,我们在同一部署上运行了第二个基准测试,但这次我们将参数 aggressive_merge 设置为 true。这有效地将合并策略更改为更积极的策略,使我们能够评估此配置对搜索性能和索引速度的影响。

索引性能

在 Rally 中,挑战配置了一系列计划执行和报告的操作。

每个操作负责对集群执行操作并报告结果。

对于我们的新跟踪,我们将第一个操作定义为 initial-documents-indexing,它涉及批量索引整个语料库。接下来是 wait-until-merges-finish-after-index,它在批量加载过程结束时等待后台合并完成。

此操作不使用强制合并;它只是在开始搜索评估之前等待自然合并过程完成。

下面,我们报告了这些 跟踪操作的结果,它们对应于在 Elasticsearch 中初始加载数据集。搜索操作将在下一节中报告。

使用 Elasticsearch 8.14.0,1.38 亿个向量的初始索引花费不到 5 个小时,平均速度达到每秒 8000 个文档。

请注意,瓶颈通常是嵌入的生成,此处未报告。

在结束时等待合并完成仅增加了 2 分钟。

总索引性能(8.14.0 默认 int8 HNSW 配置)

为了进行比较,在 Elasticsearch 8.13.4 上进行的相同实验需要近 6 个小时才能完成数据摄取,并且还需要额外 2 个小时才能等待合并完成。

总索引性能(8.13.4 默认 int8 HNSW 配置)

Elasticsearch 8.14.0 是第一个利用 向量搜索的原生代码的版本。在合并期间使用原生 Elasticsearch 编解码器来加速 int8 向量之间的相似性计算,从而显着减少了整体索引时间。我们目前正在探索利用此自定义编解码器进行搜索的进一步优化,敬请期待更新!

积极合并运行在不到 6 个小时内完成,平均每秒 7000 个文档。但是,它需要将近一个小时才能等待合并在结束时完成。与使用默认合并策略的运行相比,这代表速度下降了 40%。

总索引性能(8.14.0 积极合并 int8 HNSW 配置)

积极合并配置执行的额外工作可以在下面的两个图表中总结。

积极 合并配置合并了多 2.7 倍的文档以创建更大且更少的段。默认 合并配置报告从已索引的 1.38 亿个文档中合并了近 3 亿个文档。这意味着每个文档平均合并了 2.2 次。

每个节点合并的文档总数(8.14.0 默认 int8 HNSW 配置)

每个节点合并的文档总数(8.14.0 积极合并 int8 HNSW 配置)

在下一节中,我们将分析这些配置对搜索性能的影响。

搜索评估

对于搜索操作,我们的目标是捕获两个关键指标:最大查询吞吐量和近似最近邻搜索的准确性水平。

为了实现这一点,standalone-search-knn-* 操作使用各种近似搜索参数组合评估最大搜索吞吐量。此操作涉及使用 parallel_indexing_search_clients 并行地尽快从训练集中执行 10000 个查询。这些操作旨在利用节点上的所有可用 CPU,并在所有索引和合并任务完成后执行。

为了评估每个组合的准确性,knn-recall-* 操作计算相关的召回率和 归一化折损累计增益 (nDCG)。nDCG 是根据 msmarco-passage-v2/trec-dl-2022/judged 中发布的 76 个查询计算的,使用 386000 个 qrels 注释。所有 nDCG 值的范围均为 0.0 到 1.0,其中 1.0 表示排名完美。

由于数据集的大小,生成用于计算召回率的真实结果的成本非常高。因此,我们将召回率报告限制在测试集中的 76 个查询,我们使用蛮力方法离线计算了这些查询的真实结果。

搜索配置包含三个参数

  • k:要返回的段落数。
  • num_candidates:用于限制最近邻图上搜索的队列大小。
  • num_rescore:使用完整保真度向量重新评分的段落数。

使用自动量化,使用原始浮点向量重新评分略多于 k 个向量可以显着提高召回率。

操作的命名方式是根据这三个参数进行的。例如,knn-10-100-20 表示 k=10、num_candidates=100num_rescore=20。如果省略最后一个数字,如 knn-10-100,则 num_rescore 默认为 0。

有关我们如何创建搜索请求的更多信息,请参阅 track.py 文件。

下图显示了不同召回率下预期的每秒查询数 (QPS)。例如,默认配置(橙色系列)可以实现 50 QPS,预期召回率为 0.922。

召回率与每秒查询数(Elasticsearch 8.14.0)

对于相同的召回率水平,积极合并配置的效率提高了 2 到 3 倍。由于搜索是在更大且更少的段上进行的(如上一节所示),因此预计会提高效率。

默认配置的完整结果如下表所示。

每秒查询数、延迟(以毫秒为单位)、召回率和具有不同参数组合的 NDCG@10(8.14 默认 int8 HNSW 配置)

%best 列表示此配置的实际 NDCG@10 与最佳可能的 NDCG@10 之间的差异,最佳可能的 NDCG@10 是使用离线使用蛮力计算的真实最近邻确定的。

例如,我们观察到 knn-10-20-20 配置尽管召回率@10 为 67.4%,但实现了该数据集最佳可能 NDCG 的 90%。请注意,这只是一个点结果,结果可能会因其他模型和/或数据集而异。

下表显示了积极合并配置的完整结果。

每秒查询数、延迟(以毫秒为单位)、召回率和具有不同参数组合的 NDCG@10(8.14 积极合并 int8 HNSW 配置)

使用 knn-10-500-20 搜索配置,积极合并 设置可以在 150 QPS 下实现 > 90% 的召回率。

结论

在这篇文章中,我们描述了一个新的 Rally 跟踪,旨在对 Elasticsearch 上的大规模向量搜索进行基准测试。我们探讨了运行近似最近邻搜索所涉及的各种权衡,并演示了如何在 Elasticsearch 8.14 中将成本降低了 75%,同时将索引速度提高了 50%,以满足现实的大规模向量搜索工作负载。

我们正在进行的努力集中在优化和识别增强向量搜索功能的机会上。敬请期待本系列的下一期,我们将更深入地探讨向量搜索用例的成本和效率,特别是检查 int4 和二进制压缩技术的潜力。

通过不断改进我们的方法并发布用于测试规模性能的工具,我们的目标是突破 Elasticsearch 的可能性边界,确保它仍然是大规模向量搜索功能强大且经济高效的解决方案。

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

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

足够高级的搜索并非一蹴而就。Elasticsearch 由数据科学家、机器学习运维工程师、软件工程师等等许多同样对搜索充满热情的人提供支持,就像您一样。让我们联系起来,共同构建神奇的搜索体验,帮助您获得想要的结果。

亲自试一试