调整近似 kNN 搜索编辑

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

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

减少向量内存占用编辑

默认的 element_typefloat。但这可以通过 quantization 在索引时自动量化。量化将使所需的内存减少 4 倍,但它也会降低向量的精度,并增加该字段的磁盘使用量(最多 25%)。磁盘使用量的增加是由于 Elasticsearch 存储了量化后的向量和未量化后的向量。例如,当量化 40GB 的浮点向量时,将为量化后的向量存储额外的 10GB 数据。总磁盘使用量为 50GB,但快速搜索的内存使用量将减少到 10GB。

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

减少向量维数编辑

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

_source 中排除向量字段编辑

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

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

另一种选择是使用 合成 _source(如果所有索引字段都支持它)。

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

Elasticsearch 使用 HNSW 算法进行近似 kNN 搜索。HNSW 是一种基于图的算法,只有在大多数向量数据保存在内存中时才能有效地工作。您应该确保数据节点至少有足够的 RAM 来保存向量数据和索引结构。要检查向量数据的大小,您可以使用 分析索引磁盘使用量 API。作为经验法则,假设使用默认的 HNSW 选项,使用的字节数将为 num_vectors * 4 * (num_dimensions + 12)。当使用 byte element_type 时,所需的空间将更接近 num_vectors * (num_dimensions + 12)。请注意,所需的 RAM 用于文件系统缓存,它与 Java 堆是分开的。

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

预热文件系统缓存编辑

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

如果文件系统缓存不足以容纳所有数据,则在太多索引或太多文件上积极地将数据加载到文件系统缓存中会导致搜索变慢。谨慎使用。

以下文件扩展名用于近似 kNN 搜索

  • vecveq 用于向量值
  • vex 用于 HNSW 图
  • vemvemfvemq 用于元数据

减少索引段的数量编辑

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

强制合并为一个段编辑

强制合并 操作会强制执行索引合并。如果您强制合并为一个段,kNN 搜索只需要检查一个包含所有内容的 HNSW 图。强制合并 dense_vector 字段是一个昂贵的操作,可能需要相当长的时间才能完成。

我们建议只对只读索引(即不再接收写入的索引)进行强制合并。 当文档被更新或删除时,旧版本不会立即被删除,而是被软删除并标记为“墓碑”。这些软删除的文档会在定期段合并期间自动清理。但强制合并会导致生成非常大的(> 5GB)段,这些段不符合定期合并的条件。因此,软删除文档的数量可能会迅速增加,从而导致磁盘使用量增加,搜索性能下降。如果您定期对接收写入的索引进行强制合并,这也会使快照更昂贵,因为新文档无法增量备份。

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

一种常见的模式是首先执行初始批量上传,然后使索引可供搜索。您可以调整索引设置以鼓励 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,SIZE 检查当前值(以 KiB 为单位)。请参考您发行版的文档了解如何更改此值(例如,使用 udev 规则以在重启后持久化,或通过 blockdev --setra 作为临时设置)。我们建议将预读值设置为 128KiB

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