ElasticSearch深度分页

在项目中经常用的是ElasticSearch,分页也是一个非常常见的场景。对于ElasticSearch下的分页实现,一般情况下是使用自带的From +Size 来实现的。如:

GET /mydoc/_search
{
    "From":0
    "Size": 100
    "query":{
       "match_all":{
       
       }
    }
}

类似mysql 的limit语句的用法,但是效率上是有问题的,还有一个无法解决的问题是,es 目前支持最大的 skip 值是 max_result_window ,默认为 10000 。也就是当 from + size > max_result_window 时,es 将返回错误

当然可以通过修改这个设置来实现:

curl -XPUT "127.0.0.1:9200/custm/_settings" -d 
'{ 
    "index" : { 
        "max_result_window" : 50000 
    }
}'

那为什么ES会有这样的问题?原因是因为ES的分布式设计,需要再各个Date节点上拿出数据然后到coordinator 节点上去计算分页排序,因此自然有限制。那深度分页怎么做?

方法一分页方式 scroll

es 提供了 scroll 的方式进行分页读取。原理上是对某次查询生成一个游标 scroll_id , 后续的查询只需要根据这个游标去取数据,直到结果集中返回的 hits 字段为空,就表示遍历结束。scroll_id 的生成可以理解为建立了一个临时的历史快照,在此之后的增删改查等操作不会影响到这个快照的结果。

GET /mydoc/_search/scroll?scroll=1m&scroll_id=cXVlcnlBbmRGZXRjaDsxOzg4NDg2OTpTQzRmWWkwQ1Q1bUlwMjc0WmdIX2ZnOzA7'
{
    "query":{
       "match_all":{
       
       }
    }
}

返回结果中会有scroll_id
所有文档获取完毕之后,需要手动清理掉 scroll_id 。虽然es 会有自动清理机制,但是 srcoll_id 的存在会耗费大量的资源来保存一份当前查询结果集映像,并且会占用文件描述符。所以用完之后要及时清理。使用 es 提供的 CLEAR_API 来删除指定的 scroll_id

DELETE 127.0.0.1:9200/_search/scroll/_all
DELETE 127.0.0.1:9200/_search/scroll -d 
{"scroll_id" : ["cXVlcnlBbmRGZXRjaDsxOzg3OTA4NDpTQzRmWWkwQ1Q1bUlwMjc0WmdIX2ZnOzA7"]}'

scroll + scan
当 scroll 的文档不需要排序时,es 为了提高检索的效率,在 2.0 版本提供了 scroll + scan 的方式。随后又在 2.1.0 版本去掉了 scan 的使用,直接将该优化合入了 scroll 中。由于moa 线上的 es 版本是2.3 的,所以只简单提一下。使用的 scan 的方式是指定 search_type=scan

但是scroll方式其实问题也非常明显就是快照的数量过多也会引起系统资源的浪费,同时ES对快照数量是有限制的也需要修改。

PUT /_cluster/settings
{
  "persistent" : {
        "search.max_open_scroll_context": 25000
    },
    "transient": {
        "search.max_open_scroll_context": 25000
    }
}

官方文档提供的第三种方式就是Search After

search_after 分页的方式和 scroll 有一些显著的区别,首先它是根据上一页的最后一条数据来确定下一页的位置,同时在分页请求的过程中,如果有索引数据的增删改查,这些变更也会实时的反映到游标上。

为了找到每一页最后一条数据,每个文档必须有一个全局唯一值,这种分页方式其实和目前 moa 内存中使用rbtree 分页的原理一样,官方推荐使用 _uid 作为全局唯一值,其实使用业务层的 id 也可以。

GET order/info/_search
{
    "size": 10,
    "query": {
        "term" : {
            "did" : 519390
        }
    },
    "sort": [
        {"date": "asc"},
        {"_uid": "desc"}
    ]
}

第二页的请求,使用第一页返回结果的最后一个数据的值,加上 search_after 字段来取下一页。注意,使用 search_after 的时候要将 from 置为 0 或 -1

GET 127.0.0.1:9200/order/info/_search
{
    "size": 10,
    "query": {
        "term" : {
            "did" : 519390
        }
    },
    "search_after": [1463538857, "tweet#654323"],
    "sort": [
        {"date": "asc"},
        {"_uid": "desc"}
    ]
}

总结:很明显第三种方式和scorll都比较合适于app那种无限下拉那样的刷新,对于传统的基于页数的分页就是From和limit的天下了。其中ES官方其实不推荐scroll api来做分页,search after其实才是深度分页最好的实现方式。

Lokie博客
请先登录后发表评论
  • 最新评论
  • 总共0条评论
  • 本博客使用免费开源的 laravel-bjyblog v5.5.1.1 搭建 © 2014-2018 lokie.wang 版权所有 ICP证:沪ICP备18016993号
  • 联系邮箱:kitche1985@hotmail.com