首页 > ELK > elasticsearch深度分页详解-史上最全面试必备
2019
09-07

elasticsearch深度分页详解-史上最全面试必备

三大ElasticSearch分页方式

1、传统方式(from&size)

    顶部查询,查询10000以内的文档
    场景:需要实时获取顶部的部分文档。

    eg: 例如查询最新的订单。
2、Scroll 滚动游标 方式

    深度分页,用于非实时查询场景
    eg:需要全部文档,例如导出全部数据

    不过根据ES官网的描述,scroll查询是很耗性能的方式,不建议在实时查询中运用。摘抄自官网:The Scroll api is recommended for efficient deep scrolling but scroll contexts are costly and it is not recommended to use it for real time user requests.

3、Search After
    深度分页,用于实时查询场景

    search_after是ES5.0及之后版本提供的新特性,search_after有点类似scroll,但是和scroll又不一样,它提供一个活动的游标,通过上一次查询最后一条数据来进行下一次查询。


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-%E7%9A%84%E6%96%B9%E5%BC%8F

本文》有 0 条评论

留下一个回复