从数据集中获取数据时分页是绕不开的操作,一下子从数据集中获取过多的数据可能会造成系统抖动、占用带宽等问题。特别是进行全文搜索时,用户只关心相关性最高的那个几个结果,从系统中拉取过多的数据等于浪费资源。
ES提供了3种分页方式:
- from + size: 最普通、简单的分页方式,但是会产生深分页的问题
- search after: 解决深分页的问题,但只能一页页地往下翻,不支持跳到指定页数
- scroll Api: 会创建数据快照,无法检索新写入的数据,适合对结果集进行遍历的时候
这3种方式的分页操作都有其优缺点,适合不同的场合使用。今天我们就来学习这3种分页方式,但除了学习这3种分页方式外,我们还会介绍ES新引入的特性:Point In Time,看看如何使用Point In Time+search after的方式来代替scroll Api进行大量数据的导出
from +size 分页操作与深分页问题
在我们检索数据时,系统会对数据相关性算分进行排序,然后默认返回前10条数据。我们可以使用from +size来指定获取哪些数据。其使用示例如下:
# 简单的分页操作
GET books/_search
{"from": 0, # 指定开始位置"size": 10, # 指定获取文档个数"query": {"match_all": {}}
}
如上示例,使用“from”指定获取数据的开始位置,使用“size”指定获取文档带的个数
但当我们将from设置大于10000或者size设置大于10001的时候,这个查询将会报错:
# 返回结果中的部分错误信息
......
"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",
"reason" : "all shards failed",
"phase" : "query",
"grouped" : true,
......
从报错信息可以看出,我们要获取的数据集合太大了,系统拒绝了我们的请求。我们可以使用“index.max_result_window”配置项设置这个上限:
PUT books/_settings
{"index": {"max_result_window": 20000}
}
如上示例,我们设置了这个上限为20000。虽然使用这个配置有时候可以解决燃眉之急,但是这个上限设置过大的情况下会产生非常严重的问题,因为ES会存在深分页的问题
那为什么时深分页和为什么会产生深分页的问题那?
如上图,ES把数据保存在3个主分片中,当使用from=90 和size=10进行分页的时候,ES会从每个分片中分别获取100个文档,然后把这300个文档在汇聚到协调节点中进行排序,最后选出排序后的前100个文档,返回第90到99的文档
可以看到,当页数变大(发生了深分页)的时候,在每个分片中获取的数据越多,消耗的资源就越多。并且如果分片越多,汇聚到协调节点的数据越多,最终协调到协调节点的文档数为:shard_acount *(from + size)
search after
使用search after api可以避免产生深分页的问题,不过 search after 不支持跳转到指定页面,只能一页页的往下翻
使用 search after接口分为两步:
- 在sort中指定需要排序的字段,并且保证其值的唯一性(可以使用文档的id)
- 在下一次查询时,带上返回结果的最后一个文档的sort值进行访问
search after的使用示例如下:
# 第一次调用 search after
POST books/_search
{"size": 2,"query": { "match_all": {} },"sort": [{ "price": "desc" },{ "_id": "asc" }]
}# 返回结果
"hits" : [{"_id" : "6","_source" : {"book_id" : "4ee82467","price" : 20.9},"sort" : [20.9, "6"]},{"_id" : "1","_source" : {"book_id" : "4ee82462","price" : 19.9},"sort" : [19.9, "1"]}
]
如上示例,在第一次调用search after时指定了sort的值,并且sort中指定了price 倒序排序。为了保证排序的唯一性,我们指定了文档_id作为唯一值
可以看到,第一次调用的返回结果除了文档的信息外,还有sort相关的信息,在下一次调用的时候需要带上最后一个文档的sort值,示例中其值为:[19.9, “1”]。
下面的示例是第二次调用search after 接口进行翻页操作:
# 第二次调用 search after
POST books/_search
{"size": 2,"query": {"match_all": {}},"search_after":[19.9, "1"], # 设置为上次返回结果中最后一个文档的 sort 值"sort": [{ "price": "desc" },{ "_id": "asc" }]
}
如上示例,进行翻页操作的时候在search after字段中设置上一次返回结果中最后一个文档的sort值,并且保持sort的内容不变
那为啥search after不会产生深度分页的问题那?其关键就是sort中指定的唯一排序值
如上图,因为有了唯一的排序值做保证,所以每个分片只需要返回比sort中唯一值大的size个数据即可。例如,上一次的查询返回的最后一个文档的sort为a,那么这一次查询只需要在分片1,2,3中返回size个排序比a大的文档,协调节点汇总这些数据进行排序后返回size个结果给客户端
而from + size的方式因为没有唯一值,所以没法保证每个分片上的排序就是全局的排序,必须把每个分片的from+size个数据汇总到协调节点进行排序处理,导致出现了深分页的问题
因为sort的值是根据上一次请求结果来设置的,所以search after不支持跳转到指定的页数,甚至不能返回前一页,只能一页页往下翻。当我们可以结合缓存中间件,把每页返回的sort值缓存下来,实现往前翻页的功能
scroll Api
当我们相对结果集进行排序的时候,例如做全量数据导出时,可以使用scroll Api。scroll Api会创建数据快照,后续的访问将会基于这个快照来进行,所以无法检索新写入的数据。
scroll API 的使用示例如下:
# 第一次使用 scroll API
POST books/_search?scroll=10m
{"query": {"match_all": {}},"sort": { "price": "desc" }, "size": 2
}# 结果
{"_scroll_id" : "FGluY2x1ZGVfY29udGV4dF9......==","hits" : {"hits" : [{"_id" : "6","_source" : {"book_id" : "4ee82467","price" : 20.9}},......]}
}
如上示例,在第一次使用scroll Api需要初始化scroll 搜索并且创建快照,使用scroll查询参数指定本次“查询上下文(快照)”的有效时间,本示例中为10分钟。
其返回的结果中除了匹配文档的列表外还有_scroll_id, 我们需要在翻页的请求带上这个_scroll_id:
# 进行翻页
POST /_search/scroll
{"scroll" : "5m", "scroll_id" : "FGluY2x1ZGVfY29udGV4dF9......=="
}