深度分页
from&size
from表示查询的起始位置,size表示每页的文档数量,from=100&size=20
表示查询100-120
条数据。
ES在处理from&size的查询时,会先从每个分片上拿取数据,然后在协调节点(coordinating node)上对数据汇总排序,如果是9900-10000的数据,ES会汇总10000条文档,取9900-10000的文档数据返回。所以分页越深,ES需要的内存越多,开销越大。这也是为什么ES默认现在from+size<=10000
的原因。
http
# 修改from + size 限制数量
PUT /{index}/_settings
{
"index.max_result_window": "10001"
}
scroll
scroll查询作用于需要查询大量的文档,同时对文档的实时性要求不高的情况。使用scroll查询,第一次会生成当前查询条件的快照(快照的缓存时间由查询语句决定),后面每次翻页都基于快照的结果(有新的数据进来也不会被查询出来),滚动查询结果知道没有匹配的文档。
协调节点接受到scroll请求,会请求各个节点的数据,将符合条件的文档id汇总,打成快照缓存下来(query),当后续请求携带scroll_id时,根据这个scroll_id定位到各个节点的游标位置(fetch),最后汇总返回指定size的文档(merge)。
http
# 第一次请求
POST /product/_search/?scroll=1m
{
"from": 0, // from第一次只能为0,可以省略不写
"size": 1
}
# 请求时需要携带上一次请求返回的scroll_id
POST /_search/scroll
{
"scroll_id": "FGluY2x1ZGVfY29udGV4dF91dWlkDXF1ZXJ5QW5kRmV0Y2gBFDZjMUNNSVlCRC1nd3VJaW5wa0dWAAAAAAAAp9IWekFfNnF0SnZTd0NNQk92VUJ5MUo1QQ==",
"scroll": "1m"
}
# 删除所有scroll
DELETE /_search/scroll/_all
# 删除指定scroll
DELETE /_search/scoll
{
"scroll_id": ["xxxqdqqd==","dadwqdqw=="]
}
- 第一次请求返回的scroll_id在ES7.8版本中是不变的,可以一直作为参数请求直到快照过期。
- 因为实时性问题,scroll用于实时性不高的任务,比如后台导出大量数据等。
search_after
http
# 第一次查询
POST /product/_search
{
"query": {
"match_all": {
}
},
"from": 0, // 第一次传必须是第一页,所以可以省略
"size": 1, // 指定每次滚动查询的size
"sort": [
{
"_id":"desc", //_id是文档默认id
"id": "desc" // id是mapping中定义的业务id
}
]
}
{
...
"hits": {
...
{
"sort" : [
"EM3zM4YBD-gwuIin6Jtp",
1001
]
}
}
}
# 第二次请求需要携带search_after参数
POST /product/_search
{
"query": {
"match_all": {
}
},
"search_after": ["EM3zM4YBD-gwuIin6Jtp","1001"]
"sort": [
{
"_id":"desc",
"id": "desc"
}
]
}
- search_after必须先要指定sort排序(可以是多个字段),并且从第一页开始搜索,并且需要文档有唯一索引(一般是_id或自定义的唯一索引,在新增文档的时候,最好不要指定
_id
,让ES默认生成)。- search_after不能指定from,只能请求下一页的业务场景(滚动查询),与scroll相比,两者都采用了游标的方式实现滚动查询,但相比scroll基于快照的不实时,search_after每次查询都会针对
最新的文档
,即在search_after查询期间,排序的顺序可能是变化的。- 我们假设有2个shard,指定time为排序字段,查询20条数据,那么每个shard执行
order by time where time > 0 limit 20
,在协调节点汇总后取前20条数据。- 第一次查询返回的索引值作为第二次查询的参数,即
order by time where time > $max_time limit 20
,协调节点汇总返回前20条,以此循环反复,直到没有数据返回。- 假设我们存在id为
1,3,4
的文档数据,size=1,第一次查询我们获取到第一条数据(id=1),在我们查询第二条之前,如果查询了插入了id为2的文档数据,此时继续查询时,会查询到这条文档id为2的数据(假设文档立即入库,实际上还受到refresh_interval的影响)。
对比
分页类型 | 优点 | 缺点 |
---|---|---|
from&size | 使用简单灵活、支持跳页 | 深度分页占用大量资源,默认from+size<=10000 |
scroll | 适用非实时处理大量数据 | 基于快照,数据非实时,不能跳页 |
search_after | 参数无状态,实时查询。 | 需要指定排序和唯一字段,不能跳页。 |
http
# 验证scroll的非实时和search_after的实时查询
# 1. 新建索引和mapping
PUT /test
{
"mappings": {
"properties": {
"uuid": {
"type": "keyword"
},
"name": {
"type": "text"
}
}
}
}
# 2. 新增2条文档,uuid不连续
POST /test/_bulk
{"create": {}}
{"uuid": "1001","name": "小米手机"}
{"create": {}}
{"uuid": "1003","name": "苹果手机"}
# 3.1 执行scroll的第一步请求
POST /test/_search?scroll=1m
{
"size": 1
}
# 3.2 执行 search_after的第一步请求
POST /test/_search
{
"query": {"match_all": {}},
"size": 1,
"sort": ["uuid"]
}
# 4. 执行文档插入操作,uuid为1002
POST /test/_doc
{
"uuid": "1002",
"name": "华为手机"
}
# 5. 分别执行scroll和search_after第二步查询文档数据查看结果。
按照上述的查询流程,第五步查询时,scroll查询的是uuid为1003的数据(非实时),search_after查询的是uuid为1002的新增数据(实时)。