分页查询执行原理

  1. POST /my_index/my_type/_search
  2. {
  3. "query": { "match_all": {}},
  4. "from": 100,
  5. "size": 10
  6. }
  • ES 的搜索一般包括两个步骤:

    • query:确定要取哪些文档
    • fetch:取出具体的doc

      Query阶段

  • 此阶段shard发送给coordinating node的数据只返回啦doc的_id和_score

    过程

  1. Client 发送一次搜索请求,node1 接收到请求,然后,node1 创建一个大小为from + size的优先级队列用来存结果,我们管 node1 叫 coordinating node。
  2. coordinating node将请求广播到涉及到的 shards,每个 shard 在内部执行搜索请求,然后,将结果存到内部的大小同样为from + size 的优先级队列里,可以把优先级队列理解为一个包含top N结果的列表。
  3. 每个 shard 把暂存在自身优先级队列里的数据返回给 coordinating node,coordinating node 拿到各个 shards 返回的结果后对结果进行一次合并,产生一个全局的优先级队列,存到自身的优先级队列里。

    Fetch阶段

  • 需要取的数据可能在不同分片,也可能在同一分片,coordinating node 使用 「multi-get」 来避免多次去同一分片取数据,从而提高性能

    过程

  1. 取出对应区间的_id,coordinating node 发送 GET 请求到相关shards
  2. shard 根据 doc 的_id取到数据详情,然后返回给 coordinating node
  3. coordinating node 返回数据给 Client。

    存在问题

  • 随着分页深度的增加,查询成本越来越高,性能越来越差
  • 这种请求并不合理,因为通常只需要前几条数据,很多查询的数据是无用的
  • size 不能超过index.max_result_window这个参数的值,默认10000

    深度分页解决方案

    sroll

    场景

  • 无法实时搜索,所以不适合实时场景

  • 适合后台批量处理的任务

    原理

  • 类似关系型数据库的cursor,相当于维护了一份当前索引数据的快照,所有查询数据来源于这份快照

  • sroll可以分为两部分

    • 初始化:将所有符合搜索条件结果的_id缓存起来
    • 遍历时候,取出对应的_id,再去请求完整信息

      基本使用

  • 初始化

    1. // 参数 scroll,表示暂存搜索结果的时间
    2. POST /twitter/tweet/_search?scroll=1m
    3. {
    4. "size": 100,
    5. "query": {
    6. "match" : {
    7. "title" : "elasticsearch"
    8. }
    9. }
    10. }
    11. // 会返回一个_scroll_id,_scroll_id用来下次取数据用。
  • 遍历

    1. POST /_search?scroll=1m
    2. {
    3. "scroll_id":"XXXXXXXXXXXXXXXXXXXXXXX I am scroll id XXXXXXXXXXXXXXX"
    4. }
  • 优缺点

    • scroll_id会占用大量的资源(特别是排序的请求
    • 是生成的历史快照,对于数据的变更不会反映到快照上

      Sroll scan

      1. POST /my_index/my_type/_search?search_type=scan&scroll=1m&size=50
      2. {
      3. "query": { "match_all": {}}
      4. }
  • search_type:赋值为scan,表示采用 Scroll Scan 的方式遍历,同时告诉 Elasticsearch 搜索结果不需要排序。

  • scroll:同上,传时间。
  • size:与普通的 size 不同,这个 size 表示的是每个 shard 返回的 size 数,最终结果最大为 number_of_shards * size。

    「Scroll Scan与Scroll的区别」

  1. Scroll-Scan结果「没有排序」,按index顺序返回,没有排序,可以提高取数据性能。
  2. 初始化时只返回 _scroll_id,没有具体的hits结果
  3. size控制的是每个分片的返回的数据量,而不是整个请求返回的数据量。

    search after

    基本使用


    1. POST twitter/_search
    2. {
    3. "size": 10,
    4. "query": {
    5. "match" : {
    6. "title" : "es"
    7. }
    8. },
    9. "sort": [
    10. {"date": "asc"},
    11. {"_id": "desc"}
    12. ]
    13. }
  • 返回结果

    1. {
    2. "took" : 29,
    3. "timed_out" : false,
    4. "_shards" : {
    5. "total" : 1,
    6. "successful" : 1,
    7. "skipped" : 0,
    8. "failed" : 0
    9. },
    10. "hits" : {
    11. "total" : {
    12. "value" : 5,
    13. "relation" : "eq"
    14. },
    15. "max_score" : null,
    16. "hits" : [
    17. {
    18. ...
    19. },
    20. "sort" : [
    21. ...
    22. ]
    23. },
    24. {
    25. ...
    26. },
    27. "sort" : [
    28. 124648691,
    29. "624812"
    30. ]
    31. }
    32. ]
    33. }
    34. }
  • 上面的请求会为每一个文档返回一个包含sort排序值的数组。

  • 这些sort排序值可以被用于search_after参数里以便抓取下一页的数据。
  • 比如,我们可以使用最后的一个文档的sort排序值,将它传递给search_after参数:

    1. GET twitter/_search
    2. {
    3. "size": 10,
    4. "query": {
    5. "match" : {
    6. "title" : "es"
    7. }
    8. },
    9. "search_after": [124648691, "624812"],
    10. "sort": [
    11. {"date": "asc"},
    12. {"_id": "desc"}
    13. ]
    14. }
  • 若我们想接着上次读取的结果进行读取下一页数据,第二次查询在第一次查询时的语句基础上添加search_after,并指明从哪个数据后开始读取。

    基本原理

  • es维护一个实时游标,它以上一次查询的最后一条记录为游标,方便对下一页的查询,它是一个无状态的查询,因此每次查询的都是最新的数据。

  • 由于它采用记录作为游标,因此「SearchAfter要求doc中至少有一条全局唯一变量(每个文档具有一个唯一值的字段应该用作排序规范)」

    优点

  1. 无状态查询,可以防止在查询过程中,数据的变更无法及时反映到查询中。
  2. 不需要维护scroll_id,不需要维护快照,因此可以避免消耗大量的资源。

    缺点

  3. 由于无状态查询,因此在查询期间的变更可能会导致跨页面的不一值。

  4. 排序顺序可能会在执行期间发生变化,具体取决于索引的更新和删除。
  5. 至少需要制定一个唯一的不重复字段来排序。
  • 它不适用于大幅度跳页查询,或者全量导出,对第N页的跳转查询相当于对es不断重复的执行N次search after,而全量导出则是在短时间内执行大量的重复查询。
  • SEARCH_AFTER不是自由跳转到任意页面的解决方案,而是并行滚动多个查询的解决方案。