一次解决 Elasticsearch 两大难题: 掌握去重和深分页的最佳实践
文章目录
- 省流:ES去重取最新+分页
- `from` + `size` + `collapse`
- `search_after` + `collapse`
- `composite`
- 📢废话版:经历和踩坑
- 🎬前言
- 🧐事情并不简单
- 🙃无可奈何的决定
- 🤯坑那坑那
- 坑 1:最大`1w`条数据限制
- 坑 2:深分页的性能陷阱
- 解决方案:`search_after` 和 `scroll`
- **坑 3:分页与去重的爱恨情仇**
- 💭总结
省流:ES去重取最新+分页
from
+ size
+ collapse
GET users/_search
{"size": 2,"from": 0, // collapse去重"collapse": {"field": "csin",//将重复数据排序取最新的一条"inner_hits": {//需要获取最新数据的doc"name": "latest_doc","size": 1,"sort": [{"valDate": {"order": "desc"}}]}}
}
- 优点:
- 简单易用,无需额外逻辑
- 缺点:
- 执行进行浅分页,超过
1w
条数据后报错 - 仅支持单字段去重
- 执行进行浅分页,超过
search_after
+ collapse
GET users/_search
{"size": 2,//需要排序,根据该字段进行翻页"sort": [{"csin": {"order": "asc"}}],//查询起点"search_after": ["CSIN-18190"],"collapse": {"field": "csin","inner_hits": {//需要获取最新数据的doc"name": "latest_doc","size": 1,"sort": [{"valDate": {"order": "desc"}}]}}
}
-
优点:
- 性能优异:
search_after
避免了传统分页的性能陷阱
- 性能优异:
-
缺点:
-
仅支持单字段去重:
collapse
只能基于一个字段进行去重 -
不能聚合:无法对去重后的结果进行聚合操作
-
composite
GET /users/_search
{"size": 0,//通过csin进行分组"aggs": {"by_csin": {"composite": {"size": 10,"sources": [{"csin_field": {"terms": {"field": "csin"}}}],//分页"after": {"csin_field": "CSIN-18190"}},//取最新1"aggs": {"latest_doc": {"top_hits": {"size": 1,"sort": [{"valDate": {"order": "asc"}}]}}}}}
}
-
优点:
-
最强分页:专为聚合结果分页设计,非常稳定高效
-
支持多字段去重:
composite
可以同时对多个字段进行去重
-
-
缺点:
- 内存开销大:聚合操作通常比
collapse
更耗费内存。
- 内存开销大:聚合操作通常比
方案 | 去重方式 | 分页方式 | 聚合支持 | 多字段去重 | 性能 | 适用场景 | 主要缺点 |
---|---|---|---|---|---|---|---|
from + size + collapse | 单字段折叠去重 | 浅分页 | ❌ | ❌ | 中等 | 小数据量,简单查询 | 超过 1w 条报错,无法深分页 |
search_after + collapse | 单字段折叠去重 | 深分页(游标) | ❌ | ❌ | 高 | 大数据量分页去重 | 仅支持单字段去重,无法聚合 |
composite 聚合 | 多字段去重 | 聚合分页(after 游标) | ✅ | ✅ | 中等偏低 | 大数据量、多字段去重、聚合计算 | 内存消耗大,操作复杂,排序方向需注意 |
📢废话版:经历和踩坑
🎬前言
最近手上接到了一个新的需求,要求从A表
抽数据到B表
,给前端展示用。听到这个需求:心头一乐,这不是就是ETL
吗?写个简单的查询插入即可,简单!!!
🧐事情并不简单
三下五除二写完代码之后,自信满满的点下了运行按钮,接下来只需要抖腿等结果就行了。时间就这样一分一秒的过去:一分钟,五分钟,十分钟。。。还没有执行完,更准确的说,是还有查询完,更别说插入了。我心里咯噔一下,暗叹不妙。这个表的数据看来不少。。。。
怀着验证的心去查了下表,好好好,连count
都出不来结果,看这架势,数据量至少得破亿。顺便看了下表结构,这么大的数据量,竟然连分区都没有。这不慢才怪哩
在求爷爷告奶奶的求教一番过后,一个老兄跟我说,ES 上面也有备份,可以去那上面看看。。。不得不说,ES 就是快,在 ES 执行了下count
,基本是秒出结果,数据量有2亿!!!还是成增长态势。。。
🙃无可奈何的决定
就现在的情形,在数据库没做任何处理的情况下,一个count
都执行了十几分钟,指望数据库去做加工,那不成了厕所点灯——找死吗?这。。似乎也没得选,只能放弃数据库,拥抱 ES,虽然 ES 并非传统关系型数据库,本身并不适合做这类工作,但是他起码出的来结果啊。。。
🤯坑那坑那
在尝试 ES 的过程中并不是一帆风顺,原本在数据库中一条select
搞定的事,在 ES 会变成好几条。。。
坑 1:最大1w
条数据限制
本着知之然,知其所以然的态度,去探究了下 ES 为什么会有这个反人类的限制?发现原因其实很简单,当查询结果超过1w
条时,ES 认为你可能在进行一个 全量扫描 操作,而这种操作通常会消耗巨大的内存和 CPU 资源,可能导致集群崩溃。为了保护集群,它在默认情况下直接报错,拒绝执行。果然快是有代价的
为了绕开这个限制,就不能再使用传统的 from
和 size
分页方式,而是需要采用 深分页 的方案
坑 2:深分页的性能陷阱
解决1w
条限制最简单的方式就是不让他限制,即通过设置 index.max_result_window
解决,但是from
和 size
依然是深分页的性能黑洞
比如,想获取第10001
页、每页10
条数据时,ES 必须在每个分片上处理前100010
条文档,然后将它们全部发送到协调节点进行合并排序。这导致:
- 内存爆炸:协调节点需要将海量数据加载到内存中进行排序
- 网络风暴:分片之间传输的数据量呈线性增长
这种方式的性能瓶颈在于,它 无法跳过前面的数据,每次查询都必须从头开始计算
解决方案:search_after
和 scroll
问题虽然存在,但并不是没有解决方案,ES 提供了两个更高效的方案:
-
search_after
(推荐):一种基于 游标 的分页方式。它通过上一个结果的排序值,来确定下一个查询的起点。它无状态、性能高,是实现无限翻页的最佳选择 -
scroll
(不推荐):会创建一个 时间点快照,让你能够遍历整个结果集。但它资源消耗大,且不能进行实时查询,通常只用于 全量数据导出
坑 3:分页与去重的爱恨情仇
当需求升级为“分页”+“去重”时,事情变得更复杂。例如,要按商品ID
去重,并返回每个ID
下最新的商品。这时,会发现简单的 search_after
也不够用了。解决方案需要同时运用collapse
和composite
聚合这两种高级武器了
collapse
:用于在搜索结果返回之前,根据某个字段将文档折叠去重。它性能高,配合search_after
能完美实现高效的去重分页。composite
聚合:如果你需要对去重后的数据进行聚合计算,比如计算每个去重组的总和,那么composite
聚合是唯一的选择。它专为聚合结果分页而设计
至于怎么解决的,方案已经放在文章开头了,这里就不过多的赘述了。。。
💭总结
使用 ES 的过程中不可谓不艰难,第一次使用就遇到了这么个需求,时也命也?不过,这也加深了我对 ES 的理解,不知道大伙儿在用 ES 的过程中有遇到哪些坑呢~~