调整近似 kNN 搜索
编辑调整近似 kNN 搜索
编辑Elasticsearch 支持近似 k 最近邻搜索,用于高效地查找与查询向量最接近的 k 个向量。由于近似 kNN 搜索的工作方式与其他查询不同,因此其性能方面有一些特殊考虑。
这些建议中的许多都有助于提高搜索速度。对于近似 kNN,索引算法会在后台运行搜索以创建向量索引结构。因此,这些相同的建议也有助于提高索引速度。
减少向量内存占用
编辑默认的element_type
是 float
。但是,可以通过quantization
在索引时自动量化。量化会将所需的内存减少 4 倍、8 倍或高达 32 倍,但也会降低向量的精度并增加该字段的磁盘使用量(分别高达 25%、12.5% 或 3.125%)。磁盘使用量的增加是由于 Elasticsearch 存储了量化和未量化的向量。例如,当 int8 量化 40GB 的浮点向量时,将为量化向量存储额外的 10GB 数据。总磁盘使用量达到 50GB,但快速搜索的内存使用量将减少到 10GB。
对于dim
大于或等于 384
的 float
向量,强烈建议使用quantized
索引。
减少向量维度
编辑kNN 搜索的速度与向量维度的数量成线性关系,因为每个相似度计算都会考虑两个向量中的每个元素。在可能的情况下,最好使用维度较低的向量。一些嵌入模型具有不同的“大小”,并且提供较低和较高维度的选项。您还可以尝试使用诸如 PCA 之类的降维技术。在尝试不同的方法时,必须衡量对相关性的影响,以确保搜索质量仍然可以接受。
从 _source
中排除向量字段
编辑Elasticsearch 将在索引时传递的原始 JSON 文档存储在_source
字段中。默认情况下,搜索结果中的每次命中都包含完整的文档 _source
。当文档包含高维 dense_vector
字段时,_source
可能会很大并且加载成本很高。这可能会显著降低 kNN 搜索的速度。
reindex、update 和 update by query 操作通常需要 _source
字段。禁用字段的 _source
可能会导致这些操作出现意外行为。例如,reindex 可能实际上不包含新索引中的 dense_vector
字段。
您可以通过excludes
映射参数禁用在 _source
中存储 dense_vector
字段。这可以防止在搜索期间加载和返回大型向量,还可以减少索引大小。从 _source
中省略的向量仍然可以在 kNN 搜索中使用,因为它依赖于单独的数据结构来执行搜索。在使用excludes
参数之前,请确保查看从 _source
中省略字段的缺点。
另一种选择是使用合成 _source
。
确保数据节点有足够的内存
编辑Elasticsearch 使用 HNSW 算法进行近似 kNN 搜索。HNSW 是一种基于图的算法,只有在大多数向量数据都保存在内存中时才能高效工作。您应确保数据节点至少有足够的 RAM 来保存向量数据和索引结构。要检查向量数据的大小,可以使用分析索引磁盘使用情况 API。
以下是针对不同元素类型和量化级别的估计值
-
element_type: float
:num_vectors * num_dimensions * 4
-
element_type: float
,quantization: int8
:num_vectors * (num_dimensions + 4)
-
element_type: float
,quantization: int4
:num_vectors * (num_dimensions/2 + 4)
-
element_type: float
,quantization: bbq
:num_vectors * (num_dimensions/8 + 12)
-
element_type: byte
:num_vectors * num_dimensions
-
element_type: bit
:num_vectors * (num_dimensions/8)
如果使用 HNSW,则图也必须在内存中,要估计所需的字节数,请使用 num_vectors * 4 * HNSW.m
。 HNSW.m
的默认值为 16,因此默认值为 num_vectors * 4 * 16
。
请注意,所需的 RAM 用于文件系统缓存,这与 Java 堆是分开的。
数据节点还应为其他需要 RAM 的方式保留缓冲区。例如,您的索引可能还包括文本字段和数值,这些字段也受益于使用文件系统缓存。建议使用特定数据集运行基准测试,以确保有足够的内存来提供良好的搜索性能。您可以在此处和此处找到我们用于每晚基准测试的一些数据集和配置示例。
预热文件系统缓存
编辑如果运行 Elasticsearch 的计算机重新启动,则文件系统缓存将为空,因此操作系统需要一些时间才能将索引的热区域加载到内存中,以便搜索操作速度更快。您可以使用index.store.preload
设置显式告知操作系统应根据文件扩展名将哪些文件急切地加载到内存中。
如果文件系统缓存不足以容纳所有数据,则在太多索引或太多文件上急切地将数据加载到文件系统缓存中会使搜索速度变慢。请谨慎使用。
以下文件扩展名用于近似 kNN 搜索:每个扩展名都按量化类型细分。
-
vex
用于 HNSW 图 -
vec
用于所有未量化的向量值。这包括所有元素类型:float
、byte
和bit
。 -
veq
用于使用quantization
索引的量化向量:int4
或int8
-
veb
用于使用quantization
索引的二进制向量:bbq
-
vem
、vemf
、vemq
和vemb
用于元数据,通常很小,不考虑预加载
通常,如果您使用的是量化索引,则应仅预加载相关的量化值和 HNSW 图。预加载原始向量不是必需的,可能会适得其反。
减少索引段的数量
编辑Elasticsearch 分片由段组成,这些段是索引中的内部存储元素。对于近似 kNN 搜索,Elasticsearch 将每个段的向量值存储为单独的 HNSW 图,因此 kNN 搜索必须检查每个段。最近 kNN 搜索的并行化使得跨多个段进行搜索的速度更快,但如果段较少,kNN 搜索的速度仍然可以快数倍。默认情况下,Elasticsearch 通过后台合并过程定期将较小的段合并为较大的段。如果这还不够,您可以采取明确的步骤来减少索引段的数量。
增加最大段大小
编辑Elasticsearch 提供了许多可调整的设置来控制合并过程。一个重要的设置是 index.merge.policy.max_merged_segment
。这控制在合并过程中创建的段的最大大小。通过增加该值,您可以减少索引中的段数。默认值为 5GB
,但对于较大维度的向量来说可能太小。考虑将此值增加到 10GB
或 20GB
可以帮助减少段的数量。
在批量索引期间创建大段
编辑一种常见的模式是首先执行初始批量上传,然后使索引可用于搜索。您可以调整索引设置以鼓励 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
。