常见深度分页方式 from+size

es 默认采用的分页方式是 from+ size 的形式,在深度分页的情况下,这种使用方式效率是非常低的,比如

from = 5000, size=10, es 需要在各个分片上匹配排序并得到5000*10条有效数据,然后在结果集中取最后10条

数据返回,这种方式类似于mongo的 skip + size。

除了效率上的问题,还有一个无法解决的问题是,es 目前支持最大的 skip 值是 max_result_window ,默认

为 10000 。也就是当 from + size > max_result_window 时,es 将返回错误

[root@dnsserver ~]# curl -XGET 127.0.0.1:9200/custm/_settings?pretty
{
"custm" : {
"settings" : {
"index" : {
"max_result_window" : "50000",
....
}
}
}
}
最开始的时候是线上客户的es数据出现问题,当分页到几百页的时候,es 无法返回数据,此时为了恢复正常使用,我们采用了紧急规避方案,就是将 max_result_window 的值调至 50000。
[root@dnsserver ~]# curl -XPUT "127.0.0.1:9200/custm/_settings" -d
'{
"index" : {
"max_result_window" : 50000
}
}'

然后这种方式只能暂时解决问题,当es 的使用越来越多,数据量越来越大,深度分页的场景越来越复杂时,如何解决这种问题呢?

另一种分页方式 scroll

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

使用 curl 进行分页读取过程如下:

  1. 先获取第一个 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":{}}, "sort": ["_doc"]}' # 返回结果
{
"_scroll_id": "cXVlcnlBbmRGZXRjaDsxOzg3OTA4NDpTQzRmWWkwQ1Q1bUlwMjc0WmdIX2ZnOzA7",
"took": 1,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"failed": 0
},
"hits":{...}
}
  1. 后续的文档读取上一次查询返回的scroll_id 来不断的取下一页,如果srcoll_id 的生存期很长,那么每次返回的 scroll_id 都是一样的,直到该 scroll_id 过期,才会返回一个新的 scroll_id。请求指定的 scroll_id 时就不需要 /index/_type 等信息了。每读取一页都会重新设置 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": {
....
}
}
]
}
}
  1. 所有文档获取完毕之后,需要手动清理掉 scroll_id 。虽然es 会有自动清理机制,但是 srcoll_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
}
}
}
}
}

scroll + scan

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

# 2.0-beta 版本禁用 scroll 的排序,使遍历更加高效
[root@dnsserver ~]# curl '127.0.0.1:9200/order/info/_search?scroll=1m&search_type=scan' -d '{"query":{"match_all":{}}'

search_after 的方式

上述的 scroll search 的方式,官方的建议并不是用于实时的请求,因为每一个 scroll_id 不仅会占用大量的资源(特别是排序的请求),而且是生成的历史快照,对于数据的变更不会反映到快照上。这种方式往往用于非实时处理大量数据的情况,比如要进行数据迁移或者索引变更之类的。那么在实时情况下如果处理深度分页的问题呢?es 给出了 search_after 的方式,这是在 >= 5.0 版本才提供的功能。

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

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

  1. 第一页的请求和正常的请求一样,
curl -XGET 127.0.0.1:9200/order/info/_search
{
"size": 10,
"query": {
"term" : {
"did" : 519390
}
},
"sort": [
{"date": "asc"},
{"_uid": "desc"}
]
}
  1. 第二页的请求,使用第一页返回结果的最后一个数据的值,加上 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 适用于深度分页+ 排序,因为每一页的数据依赖于上一页最后一条数据,所以无法跳页请求。

且返回的始终是最新的数据,在分页过程中数据的位置可能会有变更。这种分页方式更加符合moa的业务场景。

es 库 scroll search 的实现

由于当前服务端的 es 版本还局限于 2.3 ,所以无法使用的更高效的 search_after 的方式,在某些场景中为了能取得所有的数据,只能使用 scroll 的方式代替。以下基于 scroll_search 实现的 c API:

es_cursor * co_es_scroll_search(char* esindex, char* estype,
cJSON* query, cJSON* sort, cJSON* fields, int size, char* routing);
BOOL es_scroll_cursor_next(es_cursor* cursor);
void es_cursor_destroy(es_cursor* cursor);

具体业务的使用场景如下:

// 1. 获取第一个 scroll_id 和部分数据
es_cursor *cursor = co_es_scroll_search((char*)index_name,(char*)type_name,
queryJ, sortJ, fieldJ, size , routing);
// 2. 迭代处理每一项数据,当前页的数据处理完毕之后会自动根据 scroll_id 去请求下一页,无需业务层关心
while (es_scroll_cursor_next(cursor))
{
cJSON* data = es_cursor_json(cursor); //获取一项数据
....
}
// 3. 销毁游标,同时会清除无效的 scroll_id ,无需业务层关心
es_cursor_destroy(cursor);

附:es 版本变更记录如下

2.0 -> 2.1 -> 2.2 -> 2.3 -> 2.4 -> 5.0 -> 5.1 -> 5.2 -> 5.3 -> 5.4 -> 5.5 -> 5.6 -> 6.0 -> 6.1

ElasticSearch 深度分页解决方案的更多相关文章

  1. ElasticSearch 深度分页解决方案 {"index":{"number_of_replicas":0}}

    常见深度分页方式 from+size es 默认采用的分页方式是 from+ size 的形式,在深度分页的情况下,这种使用方式效率是非常低的,比如 from = 5000, size=10, es ...

  2. elasticsearch深度分页问题

    elasticsearch专栏:https://www.cnblogs.com/hello-shf/category/1550315.html 一.深度分页方式from + size es 默认采用的 ...

  3. SpringBoot 整合 Elasticsearch深度分页查询

    es 查询共有4种查询类型 QUERY_AND_FETCH: 主节点将查询请求分发到所有的分片中,各个分片按照自己的查询规则即词频文档频率进行打分排序,然后将结果返回给主节点,主节点对所有数据进行汇总 ...

  4. Elasticsearch分页解决方案

    一.命令的方式做分页 1.常见的分页方式:from+size elasticsearch默认采用的分页方式是from+size的形式,但是在深度分页的情况下,这种使用方式的效率是非常低的,比如from ...

  5. 大数据学习[16]--使用scroll实现Elasticsearch数据遍历和深度分页[转]

    题目:使用scroll实现Elasticsearch数据遍历和深度分页 作者:星爷 出处: http://lxWei.github.io/posts/%E4%BD%BF%E7%94%A8scroll% ...

  6. Elasticsearch 在分布式系统中深度分页问题

    理解为什么深度分页是有问题的,我们可以假设在一个有 5 个主分片的索引中搜索. 当我们请求结果的第一页(结果从 1 到 10 ),每一个分片产生前 10 的结果,并且返回给 协调节点 ,协调节点对 5 ...

  7. 上亿数据怎么玩深度分页?兼容MySQL + ES + MongoDB

    面试题 & 真实经历 面试题:在数据量很大的情况下,怎么实现深度分页? 大家在面试时,或者准备面试中可能会遇到上述的问题,大多的回答基本上是分库分表建索引,这是一种很标准的正确回答,但现实总是 ...

  8. Elasticsearch深度应用(下)

    Query文档搜索机制剖析 1. query then fetch(默认搜索方式) 搜索步骤如下: 发送查询到每个shard 找到所有匹配的文档,并使用本地的Term/Document Frequer ...

  9. elasticserach数据库深度分页查询的原理

    深度分页存在的问题 https://segmentfault.com/a/1190000019004316?utm_source=tag-newest 在实际应用中,分页是必不可少的,例如,前端页面展 ...

随机推荐

  1. C#替换字符串起始/结尾指定的字符串

    #region 替换字符串起始位置(开头)中指定的字符串 /// <summary> /// 替换字符串起始位置(开头)中指定的字符串 /// </summary> /// & ...

  2. SVNserver搭建和使用(二)

    上一篇介绍了VisualSVN Server和TortoiseSVN的下载,安装,汉化.这篇介绍一下怎样使用VisualSVN Server建立版本号库,以及TortoiseSVN的使用. 首先打开V ...

  3. 单点登录 SSO 的实现原理 SESSION COOKIE Memcache

    单点登录SSO(Single Sign On)说得简单点就是在一个多系统共存的环境下,用户在一处登录后,就不用在其他系统中登录,也就是用户的一次登录能得到其他所有系统的信任.单点登录在大型网站里使用得 ...

  4. 【Android归纳】Fragment生命周期-基于实验的最新总结

    如今非常多应用的开发都是基于FragmentActivity中嵌套Fragment进行开发的,所以,假设我们可以清晰地知道他们的生命周期,那么会使我们的开发变的easy. 对于Activity的生命周 ...

  5. badboy提示脚本错误解决方法

    1.输入URL,提示脚本错误 解决办法:打开IE浏览器,工具->internet选项->高级,如图所示去掉禁用脚本调试 2.badboy内置浏览器,提示脚本错误解决办法 解决办法:badb ...

  6. STEM教育是什么?

    STEM教育是什么? STEM 是Science科学.Technology技术.Engineering工程.Math数学,这4个词的开头字母的组合.所以STEM教育就是结合科学.技术.工程.数学的跨领 ...

  7. 第18章 Redis数据结构常用命令

    18-1 字符串的一些基本命令 18-1 :配置Spring关于Redis字符串的运行环境 <bean id="poolConfig" class="redis.c ...

  8. [Java] 总结1.5/1.6/1.7版本的特性

    开发过程中接触到了从jdk1.5---jdk1.7的使用,在不同的阶段,都使用过了jdk的一些新特性,操作起来更加方面啦!特此总结了下,以下是测试代码: JDK1.5新特性: 1.自动装箱与拆箱: I ...

  9. computed与methods的异同

    在vue.js中,有methods和computed两种方式来动态当作方法来用的 如下: 两种方式在这种情况下的结果是一样的 写法上的区别是computed计算属性的方式在用属性时不用加(),而met ...

  10. 关于一些UI的插件(杂)

    1.时间插件 //日期框 $('.date-picker').datepicker(); 2.checkbox 保存checkbox的值 // 组装选择的标签 var check = $(" ...