在Elasticsearch(ES)中,大数据查询可能会因为多种原因导致性能大幅下降。以下是几种常见的情况及其解决方案。
- 深分页、大排序:大量数据扫描和多分片上的多次排序会严重拖慢性能。
- 通配符查询:导致全表扫描。
- 正则表达式查询:同样导致全表扫描。
- 高基数字段聚合:高基数排序会耗尽内存和计算资源,比如按照玩家ID分组。
- 脚本查询:脚本执行在每个文档上进行,消耗CPU和内存,无法利用缓存。
- 大字段全文搜索:大字段的倒索引和存储非常耗费资源。
下面分类介绍一下我总结的解决方案。
一、 深分页、大排序
深分页的性能问题来源于ES需要扫描和排序大量数据,这期间不仅要下推到每个分片上进行扫描排序,还需要在主查询节点上召回汇总,涉及到二次排序。
1. 深分页
深分页的问题在于总记录数量的处理,可以从产品和技术两个方面进行改进。
产品方面:
- 使用虚拟滚动实现分页。
- 限制总数
track_total_hits,控制在百万以内。例如,totalCount最多记录100万,多余的直接显示为100w+。
技术方面:
- 使用
search_after查询方式代替偏移量查询,性能会好很多。但要注意尽量加入id或其他为一字段,因为after的条件必须为一才能保证结果准确性。
POST /randy_index/_search
{
"size": 10,
"query": {
"match_all": {}
},
"sort": [
{"timestamp": "asc"},
{"_id": "asc"}
]
}
POST /randy_index/_search
{
"size": 10,
"query": {
"match_all": {}
},
"sort": [
{"timestamp": "asc"},
{"_id": "asc"}
],
"search_after": [1672534800000, "2"]
}
2. 大排序
大排序指的是在大量数据上进行排序。虽然没有完美的解决方案,但可以尝试以下方法:
- 多使用filter过滤:ES执行顺序是query -> filter -> sort,前两步如果能排除掉更多的数据,sort就会处理更少的数据。
- 列存储:将keyword类型的字段不要关闭
doc_values,列存储的keyword对聚合和排序性能更好。以存储空间换性能。
二、通配符查询和正则查询
这两种查询容易导致全表扫描,解决方案包括:
- 用prefix实现:对字段创建正序和倒序两个值索引字段,用prefix来查询。
- 使用gram分词:ngram或者edge gram分词,虽然会多占用一些索引空间,但查询会更高效。
注意:模糊查询的字段不需要进行评分,应放到filter中。另外这也是一个存储空间换性能的方案。
三、高基数聚合查询
高基数聚合查询常见于数据统计场景,当然也有一些处于产品需求考虑(比如从订单详情的物化索引中聚合出订单列表)。
- 产品和技术配合:确定需求理解无误后,可以独立创建另一个粒度的索引。
- 技术手段:使用composite多桶聚合,降低查询压力;预先用cardinality判断基数大小。
四、脚本查询
脚本查询常见于字段需要二次处理或排序时,这两种场景说白了都是在数据的预处理不足。
说句题外话,大家往往认为ES就是一个全能的查询,什么样的查询只要能写出来就能实现,这种思路是不对的。 无论从软件工程化的角度出发还是从维护性和性能出发都不要把ES当做一个复杂的业务服务,要把他当成一种数据存储引擎。 灵活性有的时候还是要为性能做一些让路的。这不是说它不能做这种查询,但是如果你想让它高效,就要考虑它的实现原理和特性。
处理这种问题我的两个常用手段
- 数据预处理:新建索引,数据reindex时运行脚本,补全需要查询或排序的字段。
- 脚本优化:尽量将脚本放在filter阶段,并且只保留一个脚本。脚本尽量使用ES官方的Painless来开发脚本。
五、大字段全文搜索
这个问题其实挺有意思的,涉及到一个经验值。首先要知道什么样的文本属于大文本:
- 小文本:长度在1KB以下(约1000字符以内)。
- 中等文本:长度在1KB到10KB之间(约1000到10000字符)。
- 大文本:长度在10KB以上(约10000字符以上)。
name处理大文本的两种思路:
- 分片分段:将大文本字段拆分为多个较小的字段或段落,分别进行索引和查询。
- 语义搜索:引入embedding向量查询,放弃传统的分词搜索。