Elasticsearch:加快 HNSW 图的合并速度

作者:来自 Elastic Thomas Veasey 及 Mayya Sharipova

过去,我们曾讨论过搜索多个 HNSW 图时所面临的一些挑战,以及我们是如何缓解这些问题的。当时,我们也提到了一些计划中的改进措施。本文正是这项工作的成果汇总。

你可能会问,为什么要使用多个图?这是 Lucene 中架构选择的一个副作用:不可变段(immutable segments)。正如大多数架构决策一样,它既有优点也有缺点。例如,我们最近正式发布了无服务器版本的 Elasticsearch。在这个背景下,我们从不可变段中获得了非常显著的优势,包括高效的索引复制、索引计算与查询计算的解耦,以及它们各自的自动扩展能力。对于向量量化,段合并让我们有机会更新参数,以更好地适应数据特性。在这个方向上,我们认为通过测量数据特性并重新评估索引选择,还可以带来其他优势。

本文将探讨我们为显著减少构建多个 HNSW 图的开销,特别是为了降低图合并成本所做的工作。

背景

为了保持可管理的段(segment)数量,Lucene 会定期检查是否需要合并段。这基本上是检查当前段数量是否超过了一个由基础段大小和合并策略决定的目标段数。如果超过了这个限制,Lucene 就会将若干段合并,直到约束条件不再被违反。这个过程在其他文档中已有详细描述。

Lucene 倾向于合并相似大小的段,因为这样可以实现写放大的对数增长。对于向量索引来说,写放大指的是同一个向量被插入图的次数。Lucene 通常尝试将大约 10 个段合并成一组。因此,向量大约会被插入图 1 + 9/10 × log₁₀(n/n₀) 次,其中 n 是索引中的总向量数量,n₀ 是每个基础段的预期向量数量。由于这种对数增长,即便在非常大的索引中,写放大仍能保持在个位数。然而,合并图所花费的总时间与写放大呈线性关系。

在合并 HNSW 图时,我们已经采用了一个小的优化:保留最大段的图,并将其他段的向量插入这个图。这就是上述公式中 9/10 系数的原因。接下来我们将展示,如何通过利用所有参与合并的图中的信息,显著提高这一过程的效率。

图合并

之前,我们保留了最大的图,并插入了其他图中的向量,忽略了包含它们的图。我们在下面利用的关键见解是,每个我们丢弃的图包含了它所包含的向量的相似性信息。我们希望利用这些信息来加速插入至少一部分向量。

我们专注于将一个较小的图 G_s = (V_s, E_s) 插入到一个较大的图 G_l = (V_l, E_l)  的问题,因为这是一个原子操作,我们可以用它来构建任何合并策略。

该策略是找到一个小图中顶点的子集 J \subset V_s 来插入到大图中。然后,我们利用这些顶点在小图中的连接性来加速插入剩余的顶点 V_s \setminus J 。以下我们用 N_s(u)N_l(u) 分别表示小图和大图中顶点 u 的邻居。该过程的示意如下。

我们使用下文将讨论的一个过程(第1行)来计算集合 J。然后,使用标准的 HNSW 插入过程(第2行)将 J 中的每个顶点插入到大图中。对于尚未插入的每个顶点,我们会找到其已插入的邻居及这些邻居在大图中的邻居(第 4 和第 5 行)。接着,使用以这个集合为种子的 FAST-SEARCH-LAYER 过程(第 6 行),从 HNSW 论文中的 SELECT-NEIGHBORS-HEURISTIC 中找到候选集合(第 7 行)。实际上,我们是在替换 INSERT 方法(论文中的算法1)中的 SEARCH-LAYER 过程,其他部分保持不变。最后,将刚插入的顶点加入集合 J(第8行)。

显然,为了使这个过程有效,每个 V_s \setminus J 中的顶点必须至少有一个邻居在 J 中。实际上,我们要求对于每个 u \in V_s \setminus J,都有 |J \cap N_s(u)| \geq k_u​,其中 k_u < M,即最大层连接数。我们观察到,在实际的 HNSW 图中,顶点度数的分布差异很大。下图展示了 Lucene HNSW 图底层中顶点度数的典型累计密度函数。

我们尝试了使用固定值的 k_u​ 和将其设为顶点度数的函数这两种方法。第二种选择带来了更大的加速,并对召回率的影响较小,因此我们选择了以下公式:

请注意,|N_s(u)| 是顶点 u 在小图中的度数。设定下限为 2 意味着我们将插入所有度数小于 2 的顶点。

一个简单的计数论证表明,如果我们仔细选择 J,我们只需要直接将约 \frac{1}{5}|V_s|的顶点插入到大图 G_l 中。具体而言,我们对图的边进行着色,如果我们将它的一个端点插入到 J 中。然后我们知道,为了确保 V_s \setminus J 中的每个顶点都有至少 k_u​ 个邻居在 J 中,我们需要着色至少 \sum_{u \in V_s \setminus J} k_u 条边。此外,我们预期:

这里,E_U[|N_s(U)|] 是小图中顶点的平均度数。对于每个顶点 u \in J,我们最多会着色|N_s(u)| 条边。因此,我们预期着色的总边数最多为|J| \, E_U[|N_s(U)|]。我们希望通过仔细选择 J,能够着色接近这个数量的边。因此,为了覆盖所有的顶点,\left| J \right| 需要满足:

这意味着:

提供 SEARCH-LAYER 支配了运行时间,这表明我们可以在合并时间上实现最多 5 倍的加速。考虑到写入放大效应的对数增长,这意味着即使对于非常大的索引,我们的构建时间通常也仅仅是构建一个图时的两倍。

这种策略的风险在于我们可能会损害图的质量。最初,我们尝试了一个无操作的 FAST-SEARCH-LAYER。我们发现这会导致图质量下降,特别是在合并到单个段时,召回率作为延迟的函数会降低。然后我们探索了使用有限图搜索的各种替代方案。最终,最有效的选择是最简单的。使用 SEARCH-LAYER,但设置一个较低的 ef_construction。通过这种参数设置,我们能够在保持优良图质量的同时,减少约一半的合并时间。

计算连接集

找到一个好的连接集可以表述为一个图覆盖问题。贪心启发式算法是一种简单有效的启发式方法,用于逼近最优的图覆盖。我们采用的方法是使用贪心启发式算法,一次选择一个顶点将其加入到 J 中,使用以下定义的每个候选顶点的增益:

在这里,c(v) 表示向量 vJ 中的邻居数量,1_{\{\cdot\}} 是指示函数。增益包括我们加入到 J 中的顶点的邻居数量变化,即 \max(k_v - c(v), 0),因为通过加入一个覆盖较少的顶点,我们可以获得更多的增益。增益计算在下图中对于中央橙色节点进行了说明。

我们为每个顶点 v 维护以下状态:

  • 是否过时(stale)

  • 它的增益 \text{Gain}(v)

  • J 中相邻顶点的计数,记作 c(v)

  • 一个在 [0, 1] 范围内的随机数,用于打破平局。

计算加入集合的伪代码如下。

我们首先在第 1-5 行初始化状态。

在主循环的每次迭代中,我们首先提取最大增益的顶点(第 8 行),并在存在平局时随机打破平局。在做任何修改之前,我们需要检查该顶点的增益是否过时。特别地,每次我们将一个顶点添加到 J 中时,会影响其他顶点的增益:

  1. 由于它的所有邻居在 J 中有了额外的邻居,它们的增益可能会发生变化(第14行)。

  2. 如果它的任何邻居现在已经完全覆盖,那么它们所有邻居的增益都可能发生变化(第14-16行)。

我们以懒惰的方式重新计算增益,因此只有在我们要将一个顶点插入到 J 中时,才会重新计算该顶点的增益(第18-20行)。

请注意,我们只需要跟踪我们已添加到 J 中的顶点的总增益,以决定何时退出。此外,尽管 \text{Gain}_{\text{tot}} < \text{Gain}_{\text{exit}},至少会有一个顶点具有非零增益,因此我们始终会取得进展。

结果

我们在4个数据集上进行了实验,涵盖了我们支持的三种距离度量(欧几里得、余弦和内积),并采用了两种类型的量化方法:1)int8 —— 每个维度使用1字节的整数;2)BBQ —— 每个维度使用一个比特。

实验 1:int8 量化

从基准到候选方案(提出的改进)的平均加速比为:

  • 索引时间加速:1.28×

  • 强制合并加速:1.72×

这对应于以下运行时间的划分:

为了完整性,以下是各项时间数据:

  • quora-E5-small; 522931 文档;384 维度;余弦度量
    基准:索引时间:112.41s,强制合并:113.81s
    候选:索引时间:81.55s,强制合并:70.87s

  • cohere-wikipedia-v2; 100万文档;768 维度;余弦度量
    基准:索引时间:158.1s,强制合并:425.20s
    候选:索引时间:122.95s,强制合并:239.28s

  • gist; 960 维度,100万文档;欧几里得度量
    基准:索引时间:141.82s,强制合并:536.07s
    候选:索引时间:119.26s,强制合并:279.05s

  • cohere-wikipedia-v3; 100万文档;1024 维度;内积度量
    基准:索引时间:211.86s,强制合并:654.97s
    候选:索引时间:168.22s,强制合并:414.12s

下面是与基准进行对比的召回率与延迟图,比较了候选方案(虚线)与基准在两种检索深度下的表现:recall@10 和 recall@100,分别针对具有多个分段的索引(即索引所有向量后默认合并策略的最终结果)以及强制合并为单个分段的索引。曲线越高且越靠左表示性能越好,这意味着在较低的延迟下获得更高的召回率。

从下面的图表可以看出,在多个分段索引上,候选方案在 Cohere v3 数据集上的表现更好,对于其他所有数据集,曲线稍差但几乎可比。当合并为单个分段后,所有数据集的召回曲线几乎相同。

实验 2:BBQ 量化

从基准到候选方案的平均加速比为:

  • 索引时间加速:1.33×

  • 强制合并加速:1.34×

以下是具体的细分:

为完整性考虑,以下是时间数据:

  • quora-E5-small; 522931 个文档; 384 维度; 余弦度量
    基准:索引时间:70.71秒,强制合并:59.38秒
    候选方案:索引时间:58.25秒,强制合并:40.15秒

  • cohere-wikipedia-v2; 100 万个文档; 768 维度; 余弦度量
    基准:索引时间:203.08秒,强制合并:107.27秒
    候选方案:索引时间:142.27秒,强制合并:85.68秒

  • gist; 960 维度,100 万个文档; 欧氏度量
    基准:索引时间:110.35秒,强制合并:323.66秒
    候选方案:索引时间:105.52秒,强制合并:202.20秒

  • cohere-wikipedia-v3; 100 万个文档; 1024 维度; 内积度量
    基准:索引时间:313.43秒,强制合并:165.98秒
    候选方案:索引时间:190.63秒,强制合并:159.95秒

从多个段索引的图表可以看到,候选方案在几乎所有数据集上表现更好,除了 cohere v2 数据集,基准略优。对于单个段索引,所有数据集的召回曲线几乎相同。

总的来说,我们的实验涵盖了不同的数据集特征、索引设置和检索场景。实验结果表明,我们能够在保持强大的图质量和搜索性能的同时,实现显著的索引和合并加速,这些结果在各种测试场景中表现一致。

结论

本文讨论的算法将在即将发布的 Lucene 10.2 版本中提供,并将在基于该版本的 Elasticsearch 发布中可用。用户将在这些新版本中受益于改进的合并性能和缩短的索引构建时间。这个变化是我们不断努力使 Lucene 和 Elasticsearch 成为一个快速高效的向量搜索数据库的一部分。

自己亲自体验向量搜索,通过这款自定进度的搜索 AI 实践学习。你可以开始免费的云试用或立即在本地机器上尝试 Elastic。

原文:Speeding up merging of HNSW graphs - Elasticsearch Labs

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/77394.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

人事|人事管理系统|基于Springboot+vue的人事管理系统设计与实现(源码+数据库+文档)

人事管理系统 目录 基于Springboot的人事管理系统设计与实现 一、前言 二、系统功能设计 三、系统实现 1、管理员登录 2、员工管理 3、公告信息管理 4、公告类型管理 5、培训管理 6、培训类型管理 四、数据库设计 1、实体ER图 五、核心代码 六、论文参考 七、最新…

2.4GHz射频前端噪声系数优化架构

2.4GHz射频前端电路架构由信号处理链路、硬件模块及性能规范构成&#xff0c;其系统组成与参数要求如下&#xff1a; 一、信号发射链路‌ 数字基带信号通过DAC转换为模拟信号‌ 调制电路将信号加载至本地振荡器生成的2.4GHz载波‌ 功率放大器将信号强度提升至20-25dBm范围‌ …

开源 LLM 应用开发平台 Dify 全栈部署指南(Docker Compose 方案)

开源 LLM 应用开发平台 Dify 全栈部署指南&#xff08;Docker Compose 方案&#xff09; 一、部署环境要求与前置检查 1.1 硬件最低配置 组件要求CPU双核及以上内存4GB 及以上磁盘空间20GB 可用空间 1.2 系统兼容性验证 ✅ 官方支持系统&#xff1a; Ubuntu 20.04/22.04 L…

Trae AI 保姆级教程:从安装到调试全流程指南

Trae AI 保姆级教程&#xff1a;从安装到调试全流程指南 Trae AI 是字节跳动推出的一款 AI 原生集成开发环境(IDE)&#xff0c;专为中文开发者设计&#xff0c;集成了 Claude 3.5 和 GPT-4o 等先进 AI 模型&#xff0c;支持通过自然语言交互实现代码生成、项目构建与调试。本教…

博物馆小程序怎么做?从0到1打造数字化文化窗口

博物馆小程序怎么做&#xff1f;从0到1打造数字化文化窗口 一、行业痛点&#xff1a;传统博物馆的数字化困局 在数字化浪潮下&#xff0c;传统博物馆普遍面临三大挑战&#xff1a; ​​客流受限​​&#xff1a;线下接待能力有限&#xff0c;难以触达更广泛人群 ​​互动单一…

基于 Netty 框架的 Java TCP 服务器端实现,用于启动一个 TCP 服务器来处理客户端的连接和数据传输

代码&#xff1a; package com.example.tpson_tcp;import io.netty.bootstrap.ServerBootstrap; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelOption; import io.netty.channel.EventLoopGroup; imp…

深入解析原生鸿蒙中的 RN 日志系统:从入门到精通!

全文目录&#xff1a; 开篇语&#x1f4d6; 目录&#x1f3af; 前言&#xff1a;鸿蒙日志系统究竟有多重要&#xff1f;&#x1f6e0;️ 鸿蒙 RN 日志系统的基础结构&#x1f4dc; 1. 日志的作用⚙️ 2. 日志分类 &#x1f527; 如何在鸿蒙 RN 中使用日志系统&#x1f58b;️ 1…

算法训练营Day01(二分 双指针)

704. 二分查找 - 力扣&#xff08;LeetCode&#xff09; 关于二分查找 最重要的是要处理好边界问题&#xff0c;每次写完边界可以带入特殊值进行测试确定区间的不变量是什么&#xff1f;比如区间的左闭右闭&#xff0c;和左闭右开&#xff0c;每次二分完的新区间&#xff0c;一…

shadcn 使用步骤与注意点

目录 一、shadcn ui 二、使用流程 1.安装 2.颜色与主题 3.引用blocks 三、使用注意点 四、推荐搭配工具 五、总结 一、shadcn ui 官网&#xff1a;Build your component library - shadcn/ui 为何选择它&#xff1f;因为它是一个基于 Tailwind CSS Radix UI 的组件集…

STM32CubeMX-H7-12-IIC读写MPU6050模块(中)-MPU6050模块详解以及软件IIC驱动

前言 上一篇我们已经完成对IIC代码基本框架的编写&#xff0c;以及获取MPU6050的ID&#xff0c;接下来我们逐一分析这个模块的功能&#xff0c;并用IIC驱动 建议看完上一篇再来看这篇 MPU6050寄存器介绍 1.电源管理寄存器&#xff08;PWR_MGMT_1&#xff0c;地址&#xff1a;0…

量子计算模拟中的GPU加速:从量子门操作到Shor算法实现

一、量子模拟的算力困境与GPU破局 量子计算模拟面临‌指数级增长的资源需求‌&#xff1a;n个量子比特的态向量需要2^n个复数存储空间。当n>30时&#xff0c;单机内存已无法承载&#xff08;1TB需求&#xff09;。传统CPU模拟器&#xff08;如Qiskit的Aer&#xff09;在n28…

spring mvc 异常处理中@RestControllerAdvice 和 @ControllerAdvice 对比详解

RestControllerAdvice 和 ControllerAdvice 对比详解 1. 基本概念 注解等效组合核心作用ControllerAdviceComponent RequestMapping&#xff08;隐式&#xff09;定义全局控制器增强类&#xff0c;处理跨控制器的异常、数据绑定或全局响应逻辑。RestControllerAdviceControll…

JavaScript的回调函数:异步编程的基石

引言 在JavaScript的世界里&#xff0c;回调函数是一种强大而基础的编程模式&#xff0c;它是异步编程的核心概念之一。随着Web应用程序变得越来越复杂&#xff0c;理解和掌握回调函数变得尤为重要。本文将深入探讨JavaScript回调函数的概念、应用场景以及最佳实践。 什么是回…

测试用例 [软件测试 基础]

目录 测试用例 1. 概念 1.1 什么是测试用例 1.2 什么是要素 1.3 为什么需要测试用例 2. 设计测试用例的万能公式 2.1 常规思维 逆向思维 发散性思维 2.2 万能公式 3. 设计测试用例的方法 3.1 基于需求的设计方法 3.2 具体的设计方法 3.3 更多用例练习 测试用例 …

Jupyter notebook定制字体

一、生成配置文件 运行Anaconda Powershell Prompt终端&#xff0c;输入下面一行代码&#xff1a; jupyter notebook --generate-config 将生成文件“C:\Users\XXX\.jupyter\jupyter_notebook_config.py”&#xff0c;XXX为计算机账户名字。 二、修改配置文件 c.NotebookAp…

miniconda安装R语言图文教程(详细步骤)

本篇教程介绍,如何在Windows使用miniconda安装R语言。 一、创建1个conda 虚拟环境 # 创建虚拟环境 conda create -n r_env # 激活虚拟环境 conda activate r_env二、安装 R 语言 conda install -c r r-ggplot2三、运行测试 检查安装: 输入 R 进入 R 的交互式命令行,检查是…

【day1】AI软件测试学习笔记

以下为整理的 AI软件测试学习笔记&#xff0c;涵盖性能测试工具链、AI大模型应用及开发实践&#xff0c;分为四大模块&#xff1a; 一、性能测试工具链与数据分析 1. 工具链整合效果 JMeter InfluxDB Grafana JMeter压测数据存储至云端InfluxDB&#xff0c;实现分布式压测和…

WPF 资源加载问题:真是 XAML 的锅吗?

你的观察很敏锐&#xff01;确实&#xff0c;在 WPF 项目中&#xff0c;.cs 文件主要负责逻辑实现&#xff0c;而资源加载的问题通常跟 XAML&#xff08;以及它背后的 .csproj 配置&#xff09;关系更大。我会围绕这个观点&#xff0c;用 CSDN 博客风格详细解释一下 .cs、XAML …

C++17模板编程与if constexpr深度解析

一、原理深化 1.1 模板编程 1.1.1 编译器如何处理模板&#xff08;补充&#xff09; 模板的实例化机制存在两种模式&#xff1a; 隐式实例化&#xff1a;编译器在遇到模板具体使用时自动生成代码&#xff0c;可能导致多翻译单元重复实例化&#xff0c;增加编译时间。显式实…

408 计算机网络 知识点记忆(6)

前言 本文基于王道考研课程与湖科大计算机网络课程教学内容&#xff0c;系统梳理核心知识记忆点和框架&#xff0c;既为个人复习沉淀思考&#xff0c;亦希望能与同行者互助共进。&#xff08;PS&#xff1a;后续将持续迭代优化细节&#xff09; 往期内容 408 计算机网络 知识…