调整近似 kNN 搜索

编辑

Elasticsearch 支持用于高效查找与查询向量最接近的 *k* 个向量的近似 k-近邻搜索。由于近似 kNN 搜索的工作方式与其他查询不同,因此其性能方面有一些特殊考虑。

许多这些建议有助于提高搜索速度。对于近似 kNN,索引算法在后台运行搜索以创建向量索引结构。因此,这些相同的建议也有助于提高索引速度。

减少向量内存占用

编辑

默认的element_typefloat。但这可以通过quantization在索引期间自动量化。量化将使所需的内存减少 4 倍、8 倍或多达 32 倍,但它也会降低向量的精度并增加字段的磁盘使用量(分别最多增加 25%、12.5% 或 3.125%)。磁盘使用量的增加是 Elasticsearch 同时存储量化向量和非量化向量的结果。例如,当 int8 量化 40GB 的浮点向量时,将为量化向量额外存储 10GB 的数据。总磁盘使用量为 50GB,但快速搜索的内存使用量将减少到 10GB。

对于 dim 大于或等于 384float 向量,强烈建议使用quantized索引。

减少向量维度

编辑

kNN 搜索的速度与向量维度的数量成线性关系,因为每次相似度计算都会考虑两个向量中的每个元素。尽可能使用维度较低的向量。一些嵌入模型具有不同“大小”的选项,既有低维度也有高维度选项。您还可以尝试使用 PCA 等降维技术。在尝试不同的方法时,务必衡量对相关性的影响,以确保搜索质量仍然可以接受。

_source 中排除向量字段

编辑

Elasticsearch 将在索引时传递的原始 JSON 文档存储在_source 字段中。默认情况下,搜索结果中的每个命中都包含完整的文档 _source。当文档包含高维 dense_vector 字段时,_source 可能相当大且加载成本很高。这可能会显著降低 kNN 搜索的速度。

重新索引更新按查询更新操作通常需要 _source 字段。禁用字段的 _source 可能会导致这些操作出现意外行为。例如,重新索引实际上可能不包含新索引中的 dense_vector 字段。

您可以通过excludes映射参数禁用在 _source 中存储 dense_vector 字段。这可以防止在搜索期间加载和返回大型向量,还可以减少索引大小。从 _source 中省略的向量仍然可以在 kNN 搜索中使用,因为它依赖于单独的数据结构来执行搜索。在使用excludes参数之前,请务必查看从 _source 中省略字段的缺点。

另一种选择是使用合成 _source

确保数据节点拥有足够的内存

编辑

Elasticsearch 使用HNSW算法进行近似 kNN 搜索。HNSW 是一种基于图的算法,只有当大多数向量数据保存在内存中时才能高效工作。您应确保数据节点至少拥有足够的 RAM 来容纳向量数据和索引结构。要检查向量数据的大小,您可以使用分析索引磁盘使用情况 API。

以下是不同元素类型和量化级别的估计值

  • element_type: floatnum_vectors * num_dimensions * 4
  • element_type: floatquantization: int8num_vectors * (num_dimensions + 4)
  • element_type: floatquantization: int4num_vectors * (num_dimensions/2 + 4)
  • element_type: floatquantization: bbqnum_vectors * (num_dimensions/8 + 12)
  • element_type: bytenum_vectors * num_dimensions
  • element_type: bitnum_vectors * (num_dimensions/8)

如果使用 HNSW,则图也必须在内存中,要估计所需的字节数,请使用 num_vectors * 4 * HNSW.mHNSW.m 的默认值为 16,因此默认值为 num_vectors * 4 * 16

请注意,所需的 RAM 用于文件系统缓存,这与 Java 堆是分开的。

数据节点还应为 RAM 的其他用途留下缓冲区。例如,您的索引可能还包含文本字段和数字,它们也受益于使用文件系统缓存。建议使用您的特定数据集运行基准测试,以确保有足够的内存来提供良好的搜索性能。您可以在此处此处找到我们用于夜间基准测试的一些数据集和配置示例。

预热文件系统缓存

编辑

如果重新启动运行 Elasticsearch 的计算机,则文件系统缓存将为空,因此操作系统需要一些时间才能将索引的热点区域加载到内存中,以便搜索操作能够快速执行。您可以根据文件扩展名明确告知操作系统应提前将哪些文件加载到内存中,方法是使用index.store.preload设置。

如果文件系统缓存不够大,无法容纳所有数据,则提前将数据加载到太多索引或太多文件的文件系统缓存中会使搜索变慢。谨慎使用。

近似 kNN 搜索使用以下文件扩展名:每个扩展名都按量化类型细分。

  • vex 用于 HNSW 图
  • vec 用于所有非量化向量值。这包括所有元素类型:floatbytebit
  • veq 用于使用quantizationint4int8索引的量化向量
  • veb 用于使用quantizationbbq索引的二进制向量
  • vemvemfvemqvemb用于元数据,通常很小,无需担心预加载。

通常,如果您使用的是量化索引,则应仅预加载相关的量化值和 HNSW 图。无需预加载原始向量,这可能会适得其反。

减少索引段的数量

编辑

Elasticsearch 分片由段组成,段是索引中的内部存储元素。对于近似 kNN 搜索,Elasticsearch 将每个段的向量值存储为单独的 HNSW 图,因此 kNN 搜索必须检查每个段。最近对 kNN 搜索的并行化使其跨多个段的搜索速度大大加快,但如果段较少,kNN 搜索的速度仍然可以提高数倍。默认情况下,Elasticsearch 会定期通过后台合并过程将较小的段合并为较大的段。如果这还不够,您可以采取明确的步骤来减少索引段的数量。

增加最大段大小

编辑

Elasticsearch 提供了许多可调整的设置来控制合并过程。一个重要的设置是 index.merge.policy.max_merged_segment。这控制在合并过程中创建的段的最大大小。通过增加该值,您可以减少索引中的段数。默认值为 5GB,但这对于较大维度的向量来说可能太小了。考虑将此值增加到 10GB20GB 可以帮助减少段数。

在批量索引期间创建大型段

编辑

一种常见的模式是首先执行初始批量上传,然后使索引可用于搜索。您可以调整索引设置以鼓励 Elasticsearch 创建更大的初始段,而不是强制合并。

  • 确保在批量上传期间没有搜索,并通过将其设置为 -1 来禁用index.refresh_interval。这可以防止刷新操作并避免创建额外的段。
  • 为 Elasticsearch 提供一个大型索引缓冲区,以便它可以在刷新之前接受更多文档。默认情况下,indices.memory.index_buffer_size 设置为堆大小的 10%。对于 32GB 等大量的堆大小,这通常就足够了。为了允许使用完整的索引缓冲区,您还应增加限制index.translog.flush_threshold_size

避免在搜索期间进行大量索引

编辑

主动索引文档可能会对近似 kNN 搜索性能产生负面影响,因为索引线程会从搜索中窃取计算资源。在同时进行索引和搜索时,Elasticsearch 也会频繁刷新,这会创建多个小的段。这也会损害搜索性能,因为当段更多时,近似 kNN 搜索会更慢。

在近似 kNN 搜索过程中,尽可能避免大量索引操作。如果您需要重新索引所有数据(例如,由于向量嵌入模型发生了变化),最好将新文档重新索引到一个单独的索引中,而不是就地更新它们。这有助于避免上述减速问题,并防止由于频繁的文档更新而导致代价高昂的合并操作。

通过在 Linux 上使用适中的预读值来避免页面缓存抖动

编辑

搜索可能会导致大量的随机读取 I/O。当底层块设备具有较高的预读值时,可能会执行许多不必要的读取 I/O,尤其是在使用内存映射访问文件时(参见存储类型)。

大多数 Linux 发行版为单个普通设备使用 128KiB 的合理预读值,但是,当使用软件 RAID、LVM 或 dm-crypt 时,生成的块设备(支持 Elasticsearch 的 path.data)最终可能具有非常大的预读值(几 MiB 的范围)。这通常会导致严重的页面(文件系统)缓存抖动,从而对搜索(或更新)性能产生不利影响。

您可以使用 lsblk -o NAME,RA,MOUNTPOINT,TYPE,SIZEKiB 为单位检查当前值。请查阅您发行版的文档,了解如何更改此值(例如,使用 udev 规则使其在重新启动后保持不变,或者通过 blockdev --setra 作为临时设置)。我们建议预读值为 128KiB

blockdev 期望以 512 字节扇区为单位的值,而 lsblk 报告的值以 KiB 为单位。例如,要将 /dev/nvme0n1 的预读值临时设置为 128KiB,请指定 blockdev --setra 256 /dev/nvme0n1