from size, scroll 和 search after

from size

from size是最家喻户晓的,也是最暴力的,需要查询from + size 的条数时,coordinate node就向该index的其余的shards 发送同样的请求,等汇总到(shards * (from + size))条数时在coordinate node再做一次排序,最终抽取出真正的 from 后的 size 条结果,所以from size 的源码也懒得过了,这里只是顺带提一下。实在没弄明白Elasticsearch的from size机制的必须先做功课

所以说当索引非常大时(千万级或亿级)时是无法用这个方法做深度分页的(启用routing机制可以减少这种中间态的条数,降低 OOO的风险,看是始终不是长远之计,而且性能风险摆在那里。

search after

这是Elasticsearch 5 新引入的一种分页查询机制,其实原理几乎就是和scroll一样,因此代码也是几乎一样的, 简单三句话介绍search after怎么用就是:

  • 它必须先要指定排序(因为一定要按排序记住坐标)
  • 必须从第一页开始搜起(你可以随便指定一个坐标让它返回结果,只是你不知道会在全量结果的何处)
  • 从第一页开始以后每次都带上
  • search_after=lastEmittedDocFieldValue 从而为无状态实现一个状态,说白了就是把每次固定的from size偏移变成一个确定值lastEmittedDocFieldValue,而查询则从这个偏移量开始获取size个doc(每个shard 获取size个,coordinate node最后汇总shards*size 个。

最后一点非常重要,也就是说,无论去到多少页,coordinate node向其它node发送的请求始终就是请求size个docs,是个常量,而不再是from size那样,越往后,你要请求的docs就越多,而要丢弃的垃圾结果也就越多
也就是,如果我要做非常多页的查询时,最起码search after是一个常量查询延迟和开销,并无什么副作用。
有人就会问,为啥每次提供一个search_after值就可以找到确定的那一页的内容呢,Elasticsearch 不是分布式的么,每个shard只维护一部分的离散的文档,其实这个我之前也没搞懂,自从群上一小伙扔我一干货后就秒懂了,这里也推荐大家先做做功课,看看目前一些分库分表的数据查询的方式方法:

https://mp.weixin.qq.com/s?__biz=MjM5ODYxMDA5OQ==&mid=2651959942&idx=1&sn=e9d3fe111b8a1d44335f798bbb6b9eea&chksm=bd2d075a8a5a8e4cad985b847778aa83056e22931767bb835132c04571b66d5434020fd4147f&scene=21#wechat_redirect

将查询order by time offset 0 limit 100,改写成order by time where time>0 limit 100,记录最后一个$time_max,接下来的查询order by time offset 100 limit 100,改写成order by time where time>$time_max limit 100。如此往复

scroll

上面有说到search after的总结就是如果我要做非常多页的查询时,最起码search after是一个常量查询延迟和开销,并无什么副作用,可是,就像要查询结果全量导出那样,要在短时间内不断重复同一查询成百甚至上千次,效率就显得非常低了。scroll就是把一次的查询结果缓存一定的时间,如scroll=1m则把查询结果在下一次请求上来时暂存1分钟,response比传统的返回多了一个scroll_id,下次带上这个scroll_id即可找回这个缓存的结果。这里就scroll完成的逻辑去看看源代码。

scroll的查询可以简单分成下面几步:

  1. client端向coordinate node发起类似 /{index}/_search?scroll=1m的请求
  2. coordinate node会根据参数初始化一个QueryThenFetch请求或者QueryAndFetch请求,这个步骤和其它请求无异,这里有个概念就是coordinate node会在自己的节点查一遍数据(取决于它自身是否一个data节点)再往其他节点发送一遍请求,收到结果时再提炼出最终结果,再发起一个fetch 请求取最终数据
  3. client往后会向coordinate node发起类似 _search/scroll 请求,在这个请求里会带上上次查询返回的scroll_id参数,循环这个阶段知道无结果返回
  4. client端会发起一个DELETE 操作向服务器请求查询已结束,清楚掉相关缓存

问题

  1. scroll源码的原因,在最后这段代码里,我们看到每个shard 它是通 通过lastEmittedDoc来确定游标位置的,而我们也已经知道,所有结果还需要再在coordinate node上做汇总,也就是说,这次这个shard的偏移量并不是最终的偏移量,这个shard的结果集有可能最后会全用上,又或者全用不上,因此这个lastEmittedDoc 肯定是动态set的。

在scroll 查询里,RequestID也是不变的,那么scroll_id确实就是不变的
fetch阶段发现了这个lastEmittedDoc数组,原来在coordinate node上进入了fetch阶段时会一并发送这个值给每个shard的,把这个变量一并写入构造的fetchRequest里了,而searchService里在做fetch阶段时做了一把记录

参考:
https://cloud.tencent.com/developer/column/3583/tag-125
https://cloud.tencent.com/developer/article/1134052
https://www.jianshu.com/p/91d03b16af77