Skip to content

深度分页

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=="]
}
  1. 第一次请求返回的scroll_id在ES7.8版本中是不变的,可以一直作为参数请求直到快照过期。
  2. 因为实时性问题,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"
    }
  ]
}
  1. search_after必须先要指定sort排序(可以是多个字段),并且从第一页开始搜索,并且需要文档有唯一索引(一般是_id或自定义的唯一索引,在新增文档的时候,最好不要指定_id,让ES默认生成)。
  2. search_after不能指定from,只能请求下一页的业务场景(滚动查询),与scroll相比,两者都采用了游标的方式实现滚动查询,但相比scroll基于快照的不实时,search_after每次查询都会针对最新的文档,即在search_after查询期间,排序的顺序可能是变化的。
  3. 我们假设有2个shard,指定time为排序字段,查询20条数据,那么每个shard执行order by time where time > 0 limit 20,在协调节点汇总后取前20条数据。
  4. 第一次查询返回的索引值作为第二次查询的参数,即order by time where time > $max_time limit 20,协调节点汇总返回前20条,以此循环反复,直到没有数据返回。
  5. 假设我们存在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的新增数据(实时)。