ElasticSearch 深度分页 (史上最全)
Posted 疯狂创客圈
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ElasticSearch 深度分页 (史上最全)相关的知识,希望对你有一定的参考价值。
文章很长,而且持续更新,建议收藏起来,慢慢读! Java 高并发 发烧友社群:疯狂创客圈(总入口) 奉上以下珍贵的学习资源:
- 免费赠送 经典图书 : 极致经典 + 社群大片好评 《 Java 高并发 三部曲 》 面试必备 + 大厂必备 + 涨薪必备
- 免费赠送 经典图书 : 《Netty Zookeeper Redis 高并发实战》 面试必备 + 大厂必备 +涨薪必备 (加尼恩领取)
- 免费赠送 经典图书 : 《SpringCloud、Nginx高并发核心编程》 面试必备 + 大厂必备 + 涨薪必备 (加尼恩领取)
- 免费赠送 资源宝库: Java 必备 百度网盘资源大合集 价值>10000元 (加尼恩领取)
推荐:入大厂 、做架构、大力提升Java 内功 的 精彩博文
入大厂 、做架构、大力提升Java 内功 必备的精彩博文 | 2021 秋招涨薪1W + 必备的精彩博文 |
---|---|
1:Redis 分布式锁 (图解-秒懂-史上最全) | 2:Zookeeper 分布式锁 (图解-秒懂-史上最全) |
3: Redis与MySQL双写一致性如何保证? (面试必备) | 4: 面试必备:秒杀超卖 解决方案 (史上最全) |
5:面试必备之:Reactor模式 | 6: 10分钟看懂, Java NIO 底层原理 |
7:TCP/IP(图解+秒懂+史上最全) | 8:Feign原理 (图解) |
9:DNS图解(秒懂 + 史上最全 + 高薪必备) | 10:CDN图解(秒懂 + 史上最全 + 高薪必备) |
11: 分布式事务( 图解 + 史上最全 + 吐血推荐 ) | 12:seata AT模式实战(图解+秒懂+史上最全) |
13:seata 源码解读(图解+秒懂+史上最全) | 14:seata TCC模式实战(图解+秒懂+史上最全) |
Java 面试题 30个专题 , 史上最全 , 面试必刷 | 阿里、京东、美团... 随意挑、横着走!!! |
---|---|
1: JVM面试题(史上最强、持续更新、吐血推荐) | 2:Java基础面试题(史上最全、持续更新、吐血推荐 |
3:架构设计面试题 (史上最全、持续更新、吐血推荐) | 4:设计模式面试题 (史上最全、持续更新、吐血推荐) |
17、分布式事务面试题 (史上最全、持续更新、吐血推荐) | 一致性协议 (史上最全) |
29、多线程面试题(史上最全) | 30、HR面经,过五关斩六将后,小心阴沟翻船! |
9.网络协议面试题(史上最全、持续更新、吐血推荐) | 更多专题, 请参见【 疯狂创客圈 高并发 总目录 】 |
SpringCloud 精彩博文 | |
---|---|
nacos 实战(史上最全) | sentinel (史上最全+入门教程) |
SpringCloud gateway (史上最全) | 更多专题, 请参见【 疯狂创客圈 高并发 总目录 】 |
ElasticSearch 深度分页 (史上最全)
社群小伙伴的呼声
三大ElasticSearch分页方式
-
传统方式(from&size)
顶部查询,查询10000以内的文档
场景:需要实时获取顶部的部分文档。eg: 例如查询最新的订单。
-
Scroll 游标方式
深度分页,用于非实时查询场景
eg:需要全部文档,例如导出全部数据 -
Search After
深度分页,用于实时查询场景注意es版本,低版本不能使用,具体见后文
from&size分页
POST movies/_search
"from": 10000,
"size": 10,
"query":
"match_all":
这是ElasticSearch最简单的分页查询 , from + size 为 10000 +10, 需要查询的文档从10000 到 10010
以上命令是会报错的。
"root_cause": [
"type": "illegal_argument_exception",
"reason": "Result window is too large, from + size must be less than or equal to: [10000] but was [10001]. See the scroll api for a more efficient way to request large data sets. This limit can be set by changing the [index.max_result_window] index level setting."
],
"type": "search_phase_execution_exception",
报错信息,指window默认是10000。
from&size为何不能获取10000个以上的文档
性能
为了性能,es限制了我们分页的深度,
es目前支持的最大的 max_result_window = 10000;
也就是说我们不能获取10000个以上的文档 , 当ES 分页查询超过一定的值(10000)后,会报错
怎么解决这个问题,首先能想到的就是调大这个window。
PUT movies/_settings
"index" :
"max_result_window" : 20000
但这种方法只是暂时解决问题,当数据量越来越大,分页也越来越深,而且越会出OOM问题的。
from&size分页为何会OOM
协调节点或者客户端节点,需要讲请求发送到所有的分片
每个分片把from + size个结果,返回给协调节点或者客户端节点‘
协调节点或者客户端节点进行结果合并,如果有n个分片,则查询数据是 n * (from+size) , 如果from很大的话,会造成oom或者网络资源的浪费。
例子
如请求第20页,Elasticsearch不得不取出所有分片上的第1页到第20页的所有文档,并做排序,最终再取出from后的size条结果作最终的返回
假设你有16个分片,则需要在coordinate node彙总到 shards* (from+size)条记录,即需要16*(20+10)记录后做一次全局排序
所以,当索引非常非常大(千万或亿),是无法使用from + size 做深分页的,分页越深则越容易OOM,
即便不OOM,也很消耗CPU和内存资源
结论是:
max_result_window 越大,副本越多,越容易OOM
所以不建议去 修改 max_result_window
可以看到,在分布式系统中,对结果排序的成本随分页的深度成指数上升。这就是 web 搜索引擎对任何查询都不要返回超过 10000 个结果的原因。
Scroll 滚动游标分页(查询快照)
ES为了避免深分页,不允许使用分页(from&size)查询10000条以后的数据,
因此如果要查询第10000条以后的数据,要使用ES提供的 scroll(游标) 来查询
Scroll 滚动游标原理:
对一次查询生成一个游标 scroll_id , 后续的查询只需要根据这个游标scroll_id 去取数据,直到结果集中返回的 hits 字段为空,就表示遍历结束。
scroll_id 的生成可以理解为建立了一个临时的历史快照,或者可以理解为一个保存doc快照的临时的结果文件,快照文件形成之后,原doc的增删改查等操作不会影响到这个快照的结果。
Scroll 滚动游标可以简单理解为:
使用scroll就是一次把要用的数据都排完了,缓存起来
在遍历时,从这个快照里取数据,分批取出,
因此,游标可以增加性能的原因,Scroll 使用from+size还好
是因为如果做深分页,from+size 每次搜索都必须重新排序,非常浪费资源,而且容易OOM
Scroll 滚动游标使用过程:
使用 Scroll 进行分页读取过程如下:
先获取第一个 scroll_id,url 参数包括 /index/_type/ 和 scroll,
scroll 字段指定了scroll_id 的有效生存期,以分钟为单位,过期之后es 自动清理 快照数据(临时文件)。
如果文档不需要特定排序,可以指定按照文档创建的时间返回会使迭代更高效。
[root@dnsserver ~]# curl -XGET 200.200.107.232:9200/product/info/_search?pretty&scroll=2m -d
\'"query":"match_all":, "size": 10, "sort": ["_doc"]\'
# 返回结果
"_scroll_id": "cXVlcnlBbmRGZXRjaDsxOzg3OTA4NDpTQzRmWWkwQ1Q1bUlwMjc0WmdIX2ZnOzA7",
"took": 1,
"timed_out": false,
"_shards":
"total": 1,
"successful": 1,
"failed": 0
,
"hits":...
参数scroll=2m:
表示 srcoll_id 的生存期为2分钟。scroll就是把一次的查询结果缓存一定的时间,如scroll=2m则把查询结果在下一次请求上来时暂存2分钟,response比传统的返回多了一个scroll_id,下次带上这个scroll_id即可找回这个缓存的结果。
后续翻页, 通过上一次查询返回的scroll_id 来不断的取下一页,请求指定的 scroll_id 时就不需要 /index/_type 等信息了。
如果srcoll_id 的生存期很长,那么每次返回的 scroll_id 都是一样的,直到该 scroll_id 过期,才会返回一个新的 scroll_id。
每读取一页都会重新设置 scroll_id 的生存时间,所以这个时间只需要满足读取当前页就可以,不需要满足读取所有的数据的时间,1 分钟足以。
[root@dnsserver ~]# curl -XGET \'200.200.107.232:9200/_search/scroll?scroll=1m&scroll_id=cXVlcnlBbmRGZXRjaDsxOzg4NDg2OTpTQzRmWWkwQ1Q1bUlwMjc0WmdIX2ZnOzA7\'
#返回结果
"_scroll_id": "cXVlcnlBbmRGZXRjaDsxOzk1ODg3NDpTQzRmWWkwQ1Q1bUlwMjc0WmdIX2ZnOzA7",
"took": 106,
"_shards":
"total": 1,
"successful": 1,
"failed": 0
,
"hits":
"total": 22424,
"max_score": 1.0,
"hits": [
"_index": "product",
"_type": "info",
"_id": "did-519392_pdid-2010",
"_score": 1.0,
"_routing": "519392",
"_source":
....
]
注意:
使用初始化返回的_scroll_id来进行请求,每一次请求都会继续返回初始化中未读完数据,并且会返回一个_scroll_id,这个_scroll_id可能会改变,因此每一次请求应该带上上一次请求返回的_scroll_id
每次发送scroll请求时,都要再重新刷新这个scroll的开启时间,以防不小心超时导致数据取得不完整
如果没有数据了,就会回传空的hits,可以用这个判断是否遍历完成了数据
[root@dnsserver ~]# curl -XGET \'200.200.107.232:9200/_search/scroll?scroll=1m&scroll_id=cXVlcnlBbmRGZXRjaDsxOzg4NDg2OTpTQzRmWWkwQ1Q1bUlwMjc0WmdIX2ZnOzA7\'
#返回结果
"_scroll_id": "cXVlcnlBbmRGZXRjaDsxOzk1ODg3NDpTQzRmWWkwQ1Q1bUlwMjc0WmdIX2ZnOzA7",
"took": 106,
"_shards":
"total": 1,
"successful": 1,
"failed": 0
,
"hits":
"total": 22424,
"max_score": 1.0,
"hits": []
scroll_id 的清理
srcoll_id 的存在会耗费大量的资源来保存一份当前查询结果集映像,并且会占用文件描述符。
为了防止因打开太多scroll而导致的问题,不允许用户打开滚动超过某个限制。
默认情况下,打开的滚动的最大数量为500.可以使用search.max_open_scroll_context群集设置更新此限制。
获取有多少个scroll滚动游标
GET /_nodes/stats/indices/search
虽然es 会有自动清理机制,但是,尽量保障所有文档获取完毕之后,手动清理掉 scroll_id 。
使用 es 提供的 CLEAR_API 来删除指定的 scroll_id
## 删掉指定的多个 srcoll_id
[root@dnsserver ~]# curl -XDELETE 127.0.0.1:9200/_search/scroll -d
\'"scroll_id" : ["cXVlcnlBbmRGZXRjaDsxOzg3OTA4NDpTQzRmWWkwQ1Q1bUlwMjc0WmdIX2ZnOzA7"]\'
## 删除掉所有索引上的 scroll_id
[root@dnsserver ~]# curl -XDELETE 127.0.0.1:9200/_search/scroll/_all
## 查询当前所有的scroll 状态
[root@dnsserver ~]# curl -XGET 127.0.0.1:9200/_nodes/stats/indices/search?pretty
"cluster_name" : "200.200.107.232",
"nodes" :
"SC4fYi0CT5mIp274ZgH_fg" :
"timestamp" : 1514346295736,
"name" : "200.200.107.232",
"transport_address" : "200.200.107.232:9300",
"host" : "200.200.107.232",
"ip" : [ "200.200.107.232:9300", "NONE" ],
"indices" :
"search" :
"open_contexts" : 0,
"query_total" : 975758,
"query_time_in_millis" : 329850,
"query_current" : 0,
"fetch_total" : 217069,
"fetch_time_in_millis" : 84699,
"fetch_current" : 0,
"scroll_total" : 5348,
"scroll_time_in_millis" : 92712468,
"scroll_current" : 0
Search After深度翻页
上述的 scroll search 的方式,官方的建议不用于实时文档的查询:
- 因为每一个 scroll_id 生成的历史快照,对于数据的变更不会反映到快照上。
scroll 方式往往用于非实时处理大量数据的情况,比如要进行数据迁移或者索引变更之类的。
那么在实时情况下如果处理深度分页的问题呢?
es 给出了 search_after 的方式,这是在 >= 5.0 版本才提供的功能。
search_after 分页
search_after 分页的方式和 scroll 有一些显著的区别,
首先它是根据上一页的最后一条数据来确定下一页的位置,同时在分页请求的过程中,如果有索引数据的增删改查,这些变更也会实时的反映到游标上。
为了找到每一页最后一条数据,每个文档必须有一个全局唯一值,官方推荐使用 _id 作为全局唯一值,其实使用业务层的 id 也可以。
search_after 使用
第一页的请求和正常的请求一样,
curl -XGET 127.0.0.1:9200/order/info/_search
"size": 10,
"query":
"term" :
"did" : 519390
,
"sort": [
"date": "asc",
"_id": "desc"
]
第二页的请求,使用第一页返回结果的最后一个数据的值,加上 search_after 字段来取下一页。
注意,使用 search_after 的时候要将 from 置为 0 或 -1
curl -XGET 127.0.0.1:9200/order/info/_search
"size": 10,
"query":
"term" :
"did" : 519390
,
"search_after": [1463538857, "tweet#654323"],
"sort": [
"date": "asc",
"_uid": "desc"
]
总结:search_after 适用于深度分页+ 排序,因为每一页的数据依赖于上一页最后一条数据,所以无法跳页请求。
且返回的始终是最新的数据,在分页过程中数据的位置可能会有变更。
Search After深度翻页要点
每个文档具有一个唯一值的字段应该用作排序规范的仲裁器。否则,具有相同排序值的文档的排序顺序将是未定义的。
建议的方法是使用字段_id,它肯定包含每个文档的一个唯一值。
POST twitter/_search
"size": 10,
"query":
"match" :
"title" : "es"
,
"sort": [
"date": "asc",
"_id": "desc"
]
返回的结果
"took" : 29,
"timed_out" : false,
"_shards" :
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
,
"hits" :
"total" :
"value" : 5,
"relation" : "eq"
,
"max_score" : null,
"hits" : [
...
,
"sort" : [
...
]
,
...
,
"sort" : [
124648691,
"624812"
]
]
上面的请求会为每一个文档返回一个包含sort排序值的数组。
这些sort排序值可以被用于 search_after 参数里以便抓取下一页的数据。
比如,我们可以使用最后的一个文档的sort排序值,将它传递给 search_after 参数:
GET twitter/_search
"size": 10,
"query":
"match" :
"title" : "es"
,
"search_after": [124648691, "624812"],
"sort": [
"date": "asc",
"_id": "desc"
]
Search After
Search After的要点:
-
它必须先要指定排序(因为一定要按排序记住坐标)
-
必须从第一页开始搜起
-
从第一页开始以后每次都带上
search_after=lastEmittedDocFieldValue
从而为无状态实现一个状态,
上面的例子中:
"search_after": [124648691, "624812"],
[124648691, "624812"]为上一页最后一个文档的 排序字段值`lastEmittedDocFieldValue`,
这一次查询带上`lastEmittedDocFieldValue`
说白了就是把每次固定的from size偏移变成一个确定值lastEmittedDocFieldValue
,而查询则从这个偏移量开始获取size个doc(每个shard 获取size个,coordinate node最后汇总 shards*size 个。
Search After与sroll的原理基本相同:
Search After是Elasticsearch 5 新引入的一种分页查询机制,其实原理和scroll基本一样,但是不缓存结果,而是重新进行分片的排序计算
总结
-
传统方式(from&size)
需要实时获取顶部的部分文档。例如查询最新的订单。
-
Scroll
用于非实时查询
需要全部文档,例如导出全部数据
-
Search After
用于实时查询
需要做到深度分页
参考资料
https://segmentfault.com/a/1190000020395065
https://www.bbsmax.com/A/D854aOVvdE/
https://blog.csdn.net/wangxuelei036/article/details/106659019/
https://www.bbsmax.com/A/D854aOVvdE/
https://zhuanlan.zhihu.com/p/364971667
https://www.cnblogs.com/jpfss/p/10815172.html#searchafter-的方式
以上是关于ElasticSearch 深度分页 (史上最全)的主要内容,如果未能解决你的问题,请参考以下文章