阿里云Elasticsearch向量引擎功能,用于处理大规模的向量数据。它结合了Elasticsearch的强大搜索功能和向量相似度计算能力,适用于推荐系统、图像检索、自然语言处理等场景。本文将介绍如何高效地使用阿里云Elasticsearch向量引擎,并为您提供一些最佳实践建议。阿里云Elasticsearch的向量引擎正在不断迭代,因此建议使用最新版本的阿里云Elasticsearch,以确保在性能、成本和用户体验方面获得最佳效果。
前提条件
已创建ES实例,如您未创建实例,请参见快速入门(创建阿里云ES 8.x的最新版本)。
机型:向量引擎建议使用turbo机型,提升向量引擎的性能。
规格:向量引擎需要大量堆外内存cache向量索引。在选择规格时,可依据下文关于内存计算的说明,评估堆外内存的使用情况,从而选择合适的数据节点规格及其数量。
操作指引
1.创建索引
创建一个适合存储向量数据的索引是首要步骤。以下是一个示例索引定义:
PUT /my_vector_index
{
"settings": {
"number_of_shards": 1,
"number_of_replicas": 1
},
"mappings": {
"properties": {
"my_vector": {
"type": "dense_vector",
"dims": 3
},
"my_text" : {
"type" : "keyword"
}
}
}
}
number_of_shards
和number_of_replicas
的设置取决于您的数据规模和性能需求。dense_vector
类型用于存储向量数据,dims
参数指定了向量的维度。dense_vector的参数很多,具体请参见Dense vector field type。
2.数据导入
您可以使用多种方式将数据导入到Elasticsearch向量索引中,例如使用Bulk API
批量导入数据。以下是一个示例:
PUT my_vector_index/_doc/1
{
"my_text" : "text1",
"my_vector" : [0.5, 10, 6]
}
PUT my_vector_index/_doc/2
{
"my_text" : "text2",
"my_vector" : [-0.5, 10, 10]
}
请确保向量数据的维度与索引定义中的维度一致。
3.向量搜索
使用Elasticsearch的向量相似度搜索功能,可以通过指定查询向量来查找最相似的文档。以下是一个示例查询:
GET my_vector_index/_search
{
"knn": {
"field": "my_vector",
"query_vector": [-5, 9, -12],
"k": 10,
"num_candidates": 100
},
"fields": [ "my_text" ]
}
参数 | 内容 |
参数 | 内容 |
| (可选)表示返回的最近邻居的数量。该值必须小于或等于 |
| (可选)每个分片需考虑的最近邻居候选数量。该参数对性能和召回率具有显著影响, |
关于k
和num_candidates
参数的说明:num_candidates
在HNSW中指的是查询的ef值,它表示在分片中收集的最临近的num_candidates
个文档,而k则是Elasticsearch最终在结果中返回的doc数量。
更多参数说明,请参见knn search API。
更多向量搜索的功能包括:
knn
可以支持filter
查询。可以支持similarity
,设置最小score
的命中doc
。可以支持nested
字段。支持同时查询多个
knn
字段。支持使用
script
进行精确knn
查询。支持使用
script
进行rescore
。完整功能请参见k-nearest neighbor (knn) search。
性能优化
Elasticsearch 使用 HNSW
算法进行近似 knn
搜索。HNSW
是一种基于图的算法,只有在大多数向量数据保存在内存中时才能有效运作。因此,您应确保数据节点具备足够的堆外内存,以存储向量数据及索引结构。在使用向量引擎时,需要关注以下性能优化要点:
设置合理参数
您可以选择设置合理的m
、ef_construction
参数,上述参数是创建索引时,dense_vector
类型的高级参数,详情请参见Dense vector field type。
HNSW
是一种近似knn
搜索方法,无法确保100%返回最相邻的数据。影响召回率的主要参数为m
和ef_construction
。
参数 | 内容 |
参数 | 内容 |
| 表示一个节点的邻居数量,默认值为16。邻居数量越多,召回率会相应提高,但这将对性能产生较大影响,并增加内存占用。如果对召回率有严格要求,可以将其设置为64或更大的值。 |
| 是在构建 |
降低内存消耗
Elasticsearch采用量化技术来降低内存占用。通过量化,向量的内存容量可以减少4倍、8倍甚至32倍。以默认的float
类型为例,一个向量的值占用4字节。如果使用int8
量化,每个值仅占用1字节;采用int4
量化时,每个值占用半个字节;而使用BBQ(Better Binary Quantization)量化,每个值仅占用1比特,8个值合计为1字节。与未量化情况相比,内存需求仅为原来的1/32。
计算向量数据所需的内存:
需要考虑向量数据的内存和HNSW
图索引的内存这两部分。在未量化或进行int8
量化时,图索引所占内存比例较小。然而,在进行bbq
量化时,图索引的内存占比将显著增加。因此,计算向量数据所使用的内存时,必须重视这部分内存的影响。
向量数据内存计算方式:
element_type: float
:num_vectors * num_dimensions * 4
element_type: float
withquantization: int8
:num_vectors * (num_dimensions + 4)
element_type: float
withquantization: int4
:num_vectors * (num_dimensions/2 + 4)
element_type: float
withquantization: bbq
:num_vectors * (num_dimensions/8 + 12)
element_type: byte
:num_vectors * num_dimensions
element_type: bit
:num_vectors * (num_dimensions/8)
如果采用flat
类型且未创建HNSW
索引,则向量数据的内存占用如前计算方式所述;如果采用HNSW
类型,则还需计算图索引的大小,以下为图索引大小的估算:
num_vectors * 4 * HNSW.m
,HNSW.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
。
向量索引内存计算
示例如下:
假设一份1000w的1024维数据,使用向量的默认值,开启int8
量化,m=16
,索引number_of_replicas
使用默认值1,则向量数据的总内存为:
2* (10000000 * (1024 + 4) + 10000000 * 4 * 16) = 20.34 GB。
如果用2个16 GB内存的数据节点存储这个索引,那么节点堆外总内存为16 / 2 * 2 = 16 GB,内存是不足以存下向量数据的。
如果用2个32 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
用于通过量化索引的量化向量:int4
或int8
。veb
用于通过量化索引的二进制向量:bbq
。vem
、vemf
、vemq
和vemb
用于元数据,通常很小,不需要预加载。
通常情况下,使用量化索引时,应仅预加载相关的量化值和 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
搜索的速度。
reindex, update, update 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
。
- 本页导读
- 前提条件
- 操作指引
- 1.创建索引
- 2.数据导入
- 3.向量搜索
- 性能优化
- 设置合理参数
- 降低内存消耗
- 堆外内存容量是否充足
- 向量索引内存计算
- 预热文件系统缓存
- 减少索引segment数量
- 在_source中排除向量字段