ElasticSearch之处理深度分页
在ES中实现分页的方法有三种,我们逐个分析一下他们的优缺点。
一、常规分页
在ES中,我们可以给查询条件加from
和size
达到分页的效果,比如:
get test_index/_doc/_search
{
"query":{
"match_all":{}
},
"from":1000,
"size":3
}
但是这种使用方式效率是非常低的,比如上面那条语句,意味着ES要在每个分片上面匹配排序并得到1003条数据,协调节点
拿到这些数据再进行排序处理,最终返回我们需要的3条数据。
其次,ES为了性能考虑,限制了分页的深度,目前ES支持最大的max_result_window = 10000
,也就是说当我们指定分页个数超过10000时,ES就不支持了
比如
get test_index/_doc/_search
{
"query":{
"match_all":{}
},
"from":9998,
"size":3
}
------------------------------
"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."
总结
缺点:常规分页性能低下,不支持深度分页,默认10000条数据后不支持查询,深度分页会给每个分片的内存造成一定程度的压力。
优点:实现起来比较简单
场景:适合数据量小、没有深度分页的场景下可以使用。
二、scroll分页
如果我们分页一次请求需要获取较大的数据集,scroll
是一个非常好的解决方案。
如果想要使用,只需要在请求中加一个参数scroll=?
?是什么呢?
其实,es的滚动搜索,是先搜索一批数据,然后下次再搜索一批数据,以此类推,直到搜索出全部数据。scroll搜索会在第一次请求的时候,保存一个当时的试图快照,之后都只会基于该旧的试图快照来提供数据搜索,如果这个快照时间内数据进行了变更,用户是看不到的。而我们添加的参数scroll=?
就是指定快照时间。
注意:ES默认限制存活的scrollId
个数为500个,超出报错。
比如:
get test_index/_doc/_search?scroll=1m
{
"query":{
"match_all":{}
},
"from":0,
"size":1
}
------------------------------
{
"_scroll_id" : "FGluY2x1ZGVfY29udGV4dF91dWlkDXF1ZXJ5QW5kRmV0Y2gBFmZrX09mWXpvUjZPV2hJVlF3b0hGY0EAAAAAAAAQsxZyT3ZKTDlLZlJIeUdkSEdONmtnb0pB",
"took" : 0,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 9,
"relation" : "eq"
},
"max_score" : 1.0,
"hits" : [
{
"_index" : "test_index",
"_type" : "_doc",
"_id" : "1",
"_score" : 1.0,
"_source" : {
"name" : {
"uid" : 1,
"name" : "程大帅",
"address" : "上海市汤臣一品"
}
}
}
]
}
}
会发现和平常的搜索查询不同,本次响应中携带了参数"_scroll_id" : "FGluY2x1ZGVfY29udGV4dF91dWlkDXF1ZXJ5QW5kRmV0Y2gBFmZrX09mWXpvUjZPV2hJVlF3b0hGY0EAAAAAAAAQsxZyT3ZKTDlLZlJIeUdkSEdONmtnb0pB"
接下来我们可以使用这个scroll_id
来进行滚动查询,因为我们上面指定了滚动的步长:1,所以我们再使用scroll命令的时候不用指定步长,会每次都给我们一条数据,直到快照内数据为空。
post _search/scroll
{
"scroll":"5m",
"scroll_id":"FGluY2x1ZGVfY29udGV4dF91dWlkDXF1ZXJ5QW5kRmV0Y2gBFmZrX09mWXpvUjZPV2hJVlF3b0hGY0EAAAAAAAAQsxZyT3ZKTDlLZlJIeUdkSEdONmtnb0pB"
}
快照内数据滚动完毕就会是下面的样子。
{
"_scroll_id" : "FGluY2x1ZGVfY29udGV4dF91dWlkDXF1ZXJ5QW5kRmV0Y2gBFmZrX09mWXpvUjZPV2hJVlF3b0hGY0EAAAAAAAAQsxZyT3ZKTDlLZlJIeUdkSEdONmtnb0pB",
"took" : 1,
"timed_out" : false,
"terminated_early" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 9,
"relation" : "eq"
},
"max_score" : 1.0,
"hits" : [ ]
}
}
总结
优点:解决了深度分页问题,性能相对来说还可以。
缺点:快照时间内数据修改,无法及时感应到。滚动查询时需要维护scroll_id。ES默认限制存活的scrollId
个数为500个,不适用开放于客户端使用。
场景:大数据量情况下,比如海量数据的导出。或者后台统计聚合任务等等。
三、search_after
上面我们介绍了常规分页和scroll滚动分页。
常规分页无法避免深度分页问题。
scroll能够解决深度分页问题但是无法实现较实时的查询。
这时候可以考虑使用第三种分页方式search_after
,这其实是一个假分页,根据上一页最后一条数据来确定下一页的开始位置。但是使用它每个文档必须要有一个全局唯一的值,比如我们的业务id
,或者官方自动生成的id:_uid
等,只要能表示其唯一性即可。
示例1:
比如我们使用官方生成的uid来进行search_after查询。
get test_index/_doc/_search
{
"query":{
"match_all":{}
},
"from":0,
"size":1,
"sort":{
"_id":{
"order":"desc"
}
}
}
----------------------
{
"took" : 0,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 7,
"relation" : "eq"
},
"max_score" : null,
"hits" : [
{
"_index" : "test_index",
"_type" : "_doc",
"_id" : "xc7BBn4B1rLCL6KXklDg",
"_score" : null,
"_source" : {
"name" : {
"id" : 6,
"name" : "程六帅",
"address" : "上海市汤臣六品"
}
},
"sort" : [
"xc7BBn4B1rLCL6KXklDg"
]
}
]
}
}
下次分页需要将上述分页结果集的最后一条数据的排序值带上。
get test_index/_doc/_search
{
"query":{
"match_all":{}
},
"from":0,
"size":1,
"search_after":["xc7BBn4B1rLCL6KXklDg"],
"sort":{
"_id":{
"order":"desc"
}
}
}
-----------------------------------------
{
"took" : 0,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 7,
"relation" : "eq"
},
"max_score" : null,
"hits" : [
{
"_index" : "test_index",
"_type" : "_doc",
"_id" : "xM7BBn4B1rLCL6KXdVCl",
"_score" : null,
"_source" : {
"name" : {
"id" : 5,
"name" : "程五帅",
"address" : "上海市汤臣五品"
}
},
"sort" : [
"xM7BBn4B1rLCL6KXdVCl"
]
}
]
}
}
示例2:
实际上ES为我们提供的uid是无序的,所以我们最好文档中要包含一个唯一键,比如我们现在对文档中的唯一键id来进行search_after。
get test_index/_doc/_search
{
"query":{
"match_all":{}
},
"from":0,
"size":1,
"sort":{
"name.id":"desc"
}
}
-----------------------
{
"took" : 0,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 7,
"relation" : "eq"
},
"max_score" : null,
"hits" : [
{
"_index" : "test_index",
"_type" : "_doc",
"_id" : "xc7BBn4B1rLCL6KXklDg",
"_score" : null,
"_source" : {
"name" : {
"id" : 6,
"name" : "程六帅",
"address" : "上海市汤臣六品"
}
},
"sort" : [
6
]
}
]
}
}
再根据返回值的sort来进行search_after的参数构建。
get test_index/_doc/_search
{
"query":{
"match_all":{}
},
"search_after":["6"],
"from":0,
"size":1,
"sort":{
"name.id":"desc"
}
}
---------------------------
{
"took" : 0,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 7,
"relation" : "eq"
},
"max_score" : null,
"hits" : [
{
"_index" : "test_index",
"_type" : "_doc",
"_id" : "xM7BBn4B1rLCL6KXdVCl",
"_score" : null,
"_source" : {
"name" : {
"id" : 5,
"name" : "程五帅",
"address" : "上海市汤臣五品"
}
},
"sort" : [
5
]
}
]
}
}
总结
优点:性能强劲,根据唯一排序键来滚动查询,不存在深度分页问题,也能够避免scroll的数据非实时性。
缺点:
- 实现较为复杂,需要文档中存在全局唯一键。
- 每一次查询都要带上上一次查询的结果。
- 无法实现跳页请求。
场景:适用于海量数据分页场景。