对于搜索命中的结果,分页浏览是一项基本的需求。在es内部,分页有两种实现方式。
1:通过设置查询参数from和size
size:返回结果的数量。
from:从哪一条结果返回。
比如,返回前三页,没一页10条记录,可以用下边的命令表示:
GET /_search?size=10
GET /_search?size=10&from=10
GET /_search?size=10&from=20
在这种方式下,结果集在返回之前要经过排序,查询请求往往要在所有shards上搜索,每一个shard产生自己的排序结果集,然后汇聚在一起,再统一排序,返回。前面几页返回往往没什么问题,但是随着页数的增加,问题凸显。比如我们要第1000页的数据,则每一个shard要产生top 10010的数据,然后汇聚在一起综合排序,然后返回第10001--10010条数据,其实在这一页数据的时候,大部分排序结果我们都浪费掉了。而且,随着页数的增多,代价也越来越大。
这并不是首选的分页实现方式。
2:scroll+scan的方式
scroll的方式类似于数据库中的游标,可以分页返回大量的结果集。接口也十分简单:
GET /index/type/_search?scroll=1m {"query" :{.....}}
GET /_search/scroll?scroll=1m&scroll_id=cXVlcnlUaGVuRmV0Y2g7
第一条命令用scroll参数表明了这是一个scroll类型的查询,有效时间是1分钟。
第二条命令取下一页的数据,注意scroll_id字段,这是第一条命令返回的id。依次类推,直到返回的结果集为0.
上边已经介绍过from和size的方式在深度翻页的时候效率不高,而scroll api则会跟踪那些记录已经在之前的翻页中返回过,因此能更高效的完成排序。但是,对结果集进行排序还是会有消耗。
我们来分析下日常的需求,在需要返回大量结果集的情况下,往往用户对结果集的顺序并不是特别在意,因此scroll可以结合设置type=scan来禁止排序,这样就能更高效的返回大量结果集合。
GET /index/type/_search?scroll=1m&search_type=scan {"query":{.....}}
scan scroll跟标准的scroll 有几点不同:
(1)结果没有排序,按照doc进入索引的顺序
(2)不支持聚合操作
(3)最初的查询结果的hits列表是不包含查询结果的
(4)如果设置了size参数,其计算规则并不是本次搜索返回的结果条数,而是size*shard_num。假设size=10,有10个shard,则本次搜索最多会返回100条记录。
scroll参数会告诉es在多长时间内保存这次搜索的上下文。这个时间间隔并不是所有结果返回然后失效的时间间隔,而是单页的时间间隔。在整个搜索结束或者设置的时间间隔到期后自动清除上下文。时间设置的太久意味着打开更多的资源,比如file handler等,因此可以及时清除这些上下文,调用clear接口即可。
还有一点需要注意的是:scroll并不是实时的,前面讲过是在查询发生时刻做的一次snapshot,之后的查询结果都会落在这个时间点之前,因此最近刚刚进入的数据不在检索范围之内。其实这个时间差在应用中可以忽略。