向量引擎最佳实践

更新时间:
复制为 MD 格式

Elasticsearch 使用 HNSW 算法进行近似 knn 搜索。HNSW 是一种基于图的算法,只有在大多数向量数据保存在内存中时才能有效运作。因此,您应确保数据节点具备足够的堆外内存,以存储向量数据及索引结构。本文向您介绍在使用向量引擎时,需要重点关注的性能优化要点。

设置合理参数

您可以选择设置合理的mef_construction参数,上述参数是创建索引时,dense_vector类型的高级参数,详情请参见Dense vector field type

说明

HNSW是一种近似knn搜索方法,无法确保100%返回最相邻的数据。影响召回率的主要参数为mef_construction

参数

内容

m

表示一个节点的邻居数量,默认值为16。邻居数量越多,召回率会相应提高,但这将对性能产生较大影响,并增加内存占用。如果对召回率有严格要求,可以将其设置为64或更大的值。

ef_construction

是在构建HNSW图时,添加一个节点时查找的最相邻doc构建邻居,默认为100。ef_construction的值越大,召回率将随之提高。同时,该参数对性能的影响较为显著,但不会对内存占用产生影响。如果对召回率有严格的要求,可以将其设置为512或更大的值。

降低内存消耗

Elasticsearch采用量化技术来降低内存占用。通过量化,向量的内存容量可以减少4倍、8倍甚至32倍。以默认的float类型为例,一个向量的值占用4字节。如果使用int8量化,每个值仅占用1字节;采用int4量化时,每个值占用半个字节;而使用BBQ(Better Binary Quantization)量化,每个值仅占用1比特,8个值合计为1字节。与未量化情况相比,内存需求仅为原来的1/32。

计算向量数据所需的内存:

需要考虑向量数据的内存和HNSW图索引的内存这两部分。在未量化或进行int8量化时,图索引所占内存比例较小。然而,在进行bbq量化时,图索引的内存占比将显著增加。因此,计算向量数据所使用的内存时,必须重视这部分内存的影响。

向量数据内存计算方式:

  • element_type: floatnum_vectors * num_dimensions * 4

  • element_type: float with quantization: int8num_vectors * (num_dimensions + 4)

  • element_type: float with quantization: int4num_vectors * (num_dimensions/2 + 4)

  • element_type: float with quantization: bbqnum_vectors * (num_dimensions/8 + 12)

  • element_type: bytenum_vectors * num_dimensions

  • element_type: bitnum_vectors * (num_dimensions/8)

如果采用flat类型且未创建HNSW索引,则向量数据的内存占用如前计算方式所述;如果采用HNSW类型,则还需计算图索引的大小,以下为图索引大小的估算:

num_vectors * 4 * HNSW.mHNSW.m的默认值为 16,因此默认情况下为 num_vectors * 4 * 16

因此,向量数据的总内存为上述两个部分的大小之和。

另外,需要关注number_of_replicas(索引副本数量)的数量,上面计算的是一份数据的内存容量,接下来需乘以replica的数据,number_of_replicas的默认值为1,因此内存容量为一份数据的内存容量 * 2。

说明

开启量化后,索引容量将会比之前有所增加,因为Elasticsearch保留了原始向量,并同时增加了量化后的向量数据。增加的容量来源于上述向量数据内存计算的第一部分。例如,当对40 GB的浮点向量进行int8量化时,将为量化向量额外存储10 GB的数据。因此,整体磁盘使用量为50 GB,但快速搜索时的内存使用量将减少至10 GB。

堆外内存容量是否充足

在计算内存容量以及检查节点内存是否充足时,需重点关注节点的堆外内存。

获取堆外内存:一个节点要为Java的堆预留足够的内存,一个节点的堆外内存在内存小于等于64 GB的节点上一般是内存的一半,超过64 GB后,堆外默认是节点内存减去31 GB,精确的计算方式可以直接使用如下命令:

GET _nodes/stats?human&filter_path=**.os.mem.total,**.jvm.mem.heap_max

一个节点具体的堆外内存容量为:os.mem.total - jvm.mem.heap_max

向量索引内存计算

示例如下:

假设一份1000w1024维数据,使用向量的默认值,开启int8量化,m=16,索引number_of_replicas使用默认值1,则向量数据的总内存为:

2* (10000000 * (1024 + 4) + 10000000 * 4 * 16) = 20.34 GB。

如果用216 GB内存的数据节点存储这个索引,那么节点堆外总内存为16 / 2 * 2 = 16 GB,内存是不足以存下向量数据的。

如果用232 GB内存的数据节点存储这个索引,那么节点堆外总内存为32 / 2 * 2 = 32 GB,内存可以存下向量数据。

在实际计算堆外内存时,还需为其他索引、原文以及数据读写所产生的网络流量预留一部分内存。在生产环境中,当堆外内存不足时,通常会导致磁盘ioutil指标持续满负荷运行,并伴随大量的随机读流量。

预热文件系统缓存

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

说明

如果文件系统缓存不够大,无法容纳所有数据,则在太多索引或太多文件上急切地将数据加载到文件系统缓存中将使搜索速度变慢,因此,请谨慎使用上述方法。

使用示例:

PUT /my_vector_index
{
  "settings": {
    "number_of_shards": 1,
    "number_of_replicas": 1,
    "index.store.preload": ["vex", "veq"]
  },
  "mappings": {
    "properties": {
      "my_vector": {
        "type": "dense_vector",
        "dims": 3
      },
      "my_text" : {
        "type" : "keyword"
      }
    }
  }
}

index.store.preload具体加载哪些索引文件,请参考下面的文件扩展名称后缀说明:

以下文件扩展名称后缀适用于近似 knn 搜索:每个扩展名均根据量化类型进行细分。

  • vex是存储HNSW图结构的文件。

  • vec表示所有非量化向量值。这包括所有元素类型:浮点数、字节和位。

  • veq用于通过量化索引的量化向量:int4int8

  • veb用于通过量化索引的二进制向量:bbq

  • vemvemfvemqvemb用于元数据,通常很小,不需要预加载。

通常情况下,使用量化索引时,应仅预加载相关的量化值和 HNSW 图。预加载原始向量并无必要,且可能会产生相反的效果。

配置示例:

hnsw:"index.store.preload": ["vex", "vec"]

int8、int4:"index.store.preload": ["vex", "veq"]

bbq:"index.store.preload": ["vex", "veb"]

针对现有索引的设置方式,index.store.preload 是一个静态参数。一旦索引创建完成,便无法直接修改该参数。如果可以暂时接受索引不可用的状态,则可以采取以下步骤进行设置:首先关闭索引,索引关闭后即可进行参数设置,设置完成后再重新打开索引即可。以下是使用示例:

POST my_vector_index/_close

PUT my_vector_index/_settings
{
  "index.store.preload": ["vex", "veq"]
}

POST my_vector_index/_open

减少索引segment数量

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

1. 增加最大段大小

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

PUT my_vector_index/_settings
{
  "index.merge.policy.max_merged_segment": "10gb"
}

2. 在批量索引期间创建大段

常见的流程是首先进行初始批量上传,随后使索引可供搜索。您可以调整索引设置,以促使Elasticsearch创建更大的初始段,而不是强制进行合并。确保批量上传期间禁用搜索,并通过将其设置为 -1 来禁用index.refresh_interval。这将有效防止刷新操作,并避免产生额外的段。为 Elasticsearch 配置一个较大的索引缓冲区,以便其在刷新之前能够接收更多文档。默认情况下,indices.memory.index_buffer_size 设置为堆大小的 10%。对于像 32 GB 这样的大堆大小,这通常就足够了。为了允许使用完整的索引缓冲区,您还应该增加限制index.translog.flush_threshold_size

_source中排除向量字段

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

说明

reindexupdateupdate by query操作通常需要 _source 字段。排除字段 _source 可能会导致这些操作出现意外行为。例如,重新索引时,可能实际不包含新索引中的dense_vector 字段。

您可以通过 excludes 映射参数排除在 _source 中存储的稠密向量字段。这可以防止在搜索期间加载和返回大量原始向量数据,同时也能减少索引的大小。虽然在 _source 中排除的向量仍然可以在 knn 搜索中使用,因为该过程依赖于独立的数据结构执行搜索,但在使用 excludes 参数之前,请务必查看排除_source字段的潜在缺点,缺点见上方说明。

PUT /my_vector_index
{
  "mappings": {
    "_source": {
      "excludes": [
        "my_vector"
      ]
    },
    "properties": {
      "my_vector": {
        "type": "dense_vector",
        "dims": 3
      },
      "my_text": {
        "type": "keyword"
      }
    }
  }
}

要查看doc中的向量内容,Elasticsearch版本为8.17或以上时,可以使用:

GET my_vector_index/_search
{
  "docvalue_fields": ["my_vector"]
}

其他版本可以使用:

GET my_vector_index/_search
{
  "script_fields": {
    "vector_field": {
      "script": {
        "source" : "doc['my_vector'].vectorValue"
      }
    }
  }
}

除了在_source中排除向量字段另一种选择使用方法,请参见synthetic _source

升级机型配置

由于向量相似度计算属于计算密集型任务,对CPU性能要求较高。因此,可选择Turbo机型来获取一倍以上的性能提升。您可通过蓝绿变更来选择同规格下的Turbo机型。