调优建议
查询模块
保证ES节点有充足的内存
ES默认分配节点一半的内存给JVM(最多会分配30GB),剩余的内存用于向量索引和pagecache,节点内存不足会导致频繁触发向量索引的驱逐和加载,导致查询延迟较高。
因此需要确保数据节点有足够的内存,使向量索引常驻内存。可以通过以下方式获取节点向量数据的内存使用情况。
GET /_bpack/_knn/stats
GET /_bpack/_knn/nodeId1,nodeId2/stats // 获取指定节点nodeId1,nodeId2的内存使用情况,使用逗号进行分割
如果发现内存不足,建议参考资源评估建议进行集群容量规划。
首次查询之前把数据进行预加载
默认情况下,ES的向量索引构建完成后,不会主动加载到内存中。并且在首次查询中会触发加载,只有向量数据全部加载到内存中才能进行查询。
在首次查询前,可以通过如下指令提前预热数据,降低延迟:
GET /_bpack/_knn/warmup/my_index1, my_index2
尽量减少segment
的数量
ES中的索引的基础存储单元为segment
。在KNN算法中,每个segment
都会单独构建向量索引,因此在搜索过程中需要检索每个segment
。
如果索引中segment
的划分过于琐碎,会影响检索的速度。一般情况下,可以通过BES的定时任务,定期把琐碎的segment
合并。
当然也可以在写入结束后的业务低峰期,通过force_merge
强制执行合并,减少segment
的数量,提高检索的效率。
POST /my-index-000001/_forcemerge?max_num_segments=3
尽量避免从_source
中查询向量字段
在构建索引过程中默认会把原始JSON存储在_source
字段中,因此搜索结果中的每一次命中索引都包含完整的_source
内容。
当索引包含高维度向量字段时,_source
可能比较大,加载成本就很高,这可能会显著降低KNN搜索的速度。
尽量使用doc_value
存储数据
doc_value
和_source
存储的内容是一致的,但是doc_value
是面向列式存储的,因此会拥有更好的排序和聚合效率。
创建索引过程中,不需要特地打开 doc_value
开关,ES对于绝大多数类型的数据会自动构建 doc_value
。
对于 text 类型的数据,不会构建 doc_value 而是存储在 _source 中。
因此如果可以的话,建立索引过程中尽量使用 keyword 类型代替 text 类型。
查询过程中,可以指定查询doc_value
中字段,减少加载_source
数据,加速查询效率。
查询方式如下所示:
GET /my_index/_search
{
"query": {
"hnsw": {
"vec": {
"vector": [
0.495662,
...
-0.10869
],
"k": 10,
"ef": 200
}
}
},
"size": 10,
"_source":false, // 规避加载 _source 中的数据,加速查询效率
"docvalue_fields":[
"id",
"tag_keyword"
] // 填写需要从 doc_value 中查询的字段
}
关闭默认的自动构建mapping
开关
对于字符串类型的字段,自动构建的mapping
会默认同时构建text
和keyword
类型数据,比较浪费磁盘空间。
如果不需要对字段进行全文检索,则只需要设置keyword
类型即可。
也可以通过下面配置,设置默认构建单一类型:
PUT index
{
"mappings": {
"dynamic_templates": [
{
"strings": {
"match_mapping_type": "string",
"mapping": {
"type": "keyword"
}
}
}
]
}
}
规避使用_source
存储数据,减少查询过程中的数据加载量
在构建索引的过程中,可以通过如下方式禁用_source
字段:
PUT my-index-000001
{
"mappings": {
"_source": {
"enabled": false
}
}
}
但是禁用_source
的操作需要您深思熟虑后进行,因为禁用_source
后以下功能将无法使用
- 无法使用
update
,update_by_query
以及reindex
的API,也无法从一个集群的索引reindex
到另一个集群 - 无法使用高亮
- 不能修改索引的
mappings
和analysis
也可以对向量字段不构建_source
,只对标量字段构建
PUT my-index-000001
{
"mappings": {
"_source": {
"excludes": [
"vec", // 可以直接指定不构建_source的字段
"vec.*" // 也可以通过通配符进行模糊匹配
]
}
}
}
写入模块
合理配置shard
数量
为了使shard
分配的更加均匀,从而避免写入热点,建议把shard
数量设置成节点数量的整数倍。
shard数量设置方法如下所示:
PUT /my_index
{
"settings": {
"index": {
"number_of_shards": 3 // 索引的分片数量,创建后无法修改。
}
}
}
合理设置replica的数量
没有副本意味着节点丢失可能会导致数据丢失,因此多副本对于数据安全十分重要,以便在出现问题时可以重新加载数据。
并且适度增加副本数量,可以增大查询过程中的并行程度,从而提高查询效率。
每增加一个副本都会增加集群磁盘成本,并且副本数不能大于节点的数量。
在多副本情况下,写入过程可以先设置副本数 index.number_of_replicas 为0,增加写入的速度。
PUT /my_index/_settings
{
"number_of_replicas": 0 // 分片的副本数量,取值范围[0, 节点数-1],可随时修改
}
在写入完成后,再把副本调整到合适的数量。
合理调整refresh间隔
refresh_interal
是用来控制多久把内存里的数据刷出segment的。ES会对刷出的segment
进行合并,如果合并不过来会阻止写入。
所以把refresh_interval
调大,也可以把刷出的segment
变大,降低合并的频率,提升导入性能。
调整refresh_interval
方式如下:
PUT /my_index/_settings
{
"index.refresh_interval": "10s"
}
写入速度流程优化
使用bulk
请求写入数据
bulk
请求写入效率比单个索引请求性能更好,尽量使用bulk
请求进行数据写入。
为了知道批量请求的最佳大小,可以在具有单index的单节点集群进行上进行测试。
首先尝试一个批次包含100个document,之后测试一个批次包含200个,以此类推,在每次测试过程中将批量请求中的document数量增加一倍。
当索引构建的速度开始趋于平稳时,批量请求需要包含的document数据就达到了的最佳大小。
增加索引构建并发
当构建较大数据量的向量索引时,可能会出现build较慢的情况。 可以根据分片数和节点CPU核数,在写入数据前适当调整"bpack.knn.hnsw.index_thread_qty" 。
例如,1kw数据量,1节点2分片,节点为16核CPU,我们可以把"bpack.knn.hnsw.index_thread_qty" 设置为4-6(如果设置为8,会使CPU满载,生产环境可能有风险),可以提高构建索引效率。
调节方式如下:
PUT /_cluster/settings
{
"persistent" : {
"bpack.knn.hnsw.index_thread_qty":3
}
}
"bpack.knn.hnsw.index_thread_qty" 参数设置偏大,会导致构建时启动线程过多。在负载比较高的集群,不建议调整这个参数,以免集群满载。
如果写入和构建向量索引偏慢,可以通过临时减少集群负载(减少其他写入和查询),并调大"bpack.knn.hnsw.index_thread_qty"的方式来加快构建 ,等到构建结束,再将"bpack.knn.hnsw.index_thread_qty" 调整回1。
自动生成DocumentID
当写入索引过程中指定 _id
时,ES需要检查同一分片中是否已存在具有相同 _id
的document,这是一项成本高昂的操作,并且随着索引的增长,成本会变得更高。
如果使用场景允许,建议写入过程中不指定_id
, ES可以跳过检查,从而加快索引速度。
POST _bulk
{ "create" : { "_index" : "test1"} } // 不指定 _id
{ "create" : { "_index" : "test2", "_id" : "3" } } // 指定 _id