elasticsearch深度分页问题
一、深度分页方式from + size
es 默认采用的分页方式是 from+ size 的形式,在深度分页的情况下,这种使用方式效率是非常低的,比如我们执行如下查询
curl -XGET '127.0.0.1:9200/shop/shop/_search?pretty' -H 'Content-Type: application/json' -d'
{
"query":{
"match_all": {}
},
"from":5000,
"size":10
}
'
意味着 es 需要在各个分片上匹配排序并得到5010条数据,协调节点拿到这些数据再进行排序等处理,然后结果集中取最后10条数据返回。
我们会发现这样的深度分页将会使得效率非常低,因为我只需要查询10条数据,而es则需要执行from+size条数据然后处理后返回。
其次:es为了性能,限制了我们分页的深度,es目前支持的最大的 max_result_window = 10000;也就是说我们不能分页到10000条数据以上。
例如:
curl -XGET '127.0.0.1:9200/shop/shop/_search?pretty' -H 'Content-Type: application/json' -d'
{
"query":{
"match_all": {}
},
"from":10000,
"size":10
}
'
当size + from > 10000;es查询失败,并且提示
Result window is too large, from + size must be less than or equal to: [10000] but was [10010]. 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.
二、深度分页之scroll
在es中如果我们分页要请求大数据集或者一次请求要获取较大的数据集,scroll都是一个非常好的解决方案。
使用scroll滚动搜索,可以先搜索一批数据,然后下次再搜索一批数据,以此类推,直到搜索出全部的数据来scroll搜索会在第一次搜索的时候,保存一个当时的视图快照,之后只会基于该旧的视图快照提供数据搜索,如果这个期间数据变更,是不会让用户看到的。每次发送scroll请求,我们还需要指定一个scroll参数,指定一个时间窗口,每次搜索请求只要在这个时间窗口内能完成就可以了。
一个滚屏搜索允许我们做一个初始阶段搜索并且持续批量从Elasticsearch里拉取结果直到没有结果剩下。这有点像传统数据库里的cursors(游标)。
滚屏搜索会及时制作快照。这个快照不会包含任何在初始阶段搜索请求后对index做的修改。它通过将旧的数据文件保存在手边,所以可以保护index的样子看起来像搜索开始时的样子。这样将使得我们无法得到用户最近的更新行为。
scroll的使用很简单
执行如下curl,每次请求两条。可以定制 scroll = 5m意味着该窗口过期时间为5分钟。
curl -XGET '127.0.0.1:9200/shop/shop/_search?scroll=5m&pretty' -H 'Content-Type: application/json' -d'
{
"query": {
"term" : {
"branchStoreId" : "10004868"
}
},
"size":2
}
'
{
"_scroll_id" : "DnF1ZXJ5VGhlbkZldGNoBQAAAAAAABdiFlVVcW1sUmxmVFYtQk1ZbWR0dXNGamcAAAAAAAAXYxZVVXFtbFJsZlRWLUJNWW1kdHVzRmpnAAAAAAAAF2UWVVVxbWxSbGZUVi1CTVltZHR1c0ZqZwAAAAAAABdmFlVVcW1sUmxmVFYtQk1ZbWR0dXNGamcAAAAAAAAXZBZVVXFtbFJsZlRWLUJNWW1kdHVzRmpn",
"took" : 1,
"timed_out" : false,
"_shards" : {
"total" : 5,
"successful" : 5,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : 6,
"max_score" : 1.0,
"hits" : [
{
"_index" : "shop",
"_type" : "shop",
"_id" : "33995",
"_score" : 1.0,
"_source" : {
"appId" : 1,
"id" : "33995",
"shopName" : "海底捞火锅 ",
"area" : "荃湾",
"address" : "荃湾大河道100号海之恋商场地下G07-G08号舖",
"branchStoreId" : 10004868,
"longitude" : "114.11575732308106",
"latitude" : "22.365408313009407",
"collects" : "1867",
"shopImg" : "/spider/food/op/de66a56637c611eb9bf500163e019e34.jpg",
"shopType" : 0,
"categoryName" : "川菜 (四川)/火锅",
"shopGrade" : 4.5,
"price" : "201-400",
"likesReview" : 5,
"okReview" : 2,
"badReview" : 0,
"attitude" : -1,
"location" : "22.365408313009407,114.11575732308106",
"status" : "",
"score" : 0.5405438429598104,
"esId" : "33995"
}
},
{
"_index" : "shop",
"_type" : "shop",
"_id" : "64058",
"_score" : 1.0,
"_source" : {
"appId" : 1,
"id" : "64058",
"shopName" : "海底捞火锅",
"area" : "尖沙咀",
"address" : "尖沙咀金巴利道26号2楼",
"branchStoreId" : 10004868,
"longitude" : "114.1784066676926",
"latitude" : "22.29771142024199",
"collects" : "2882",
"shopImg" : "/spider/food/op/c827568c37c211eb8dda00163e019e34.jpg",
"shopType" : 0,
"categoryName" : "川菜 (四川)/火锅",
"shopGrade" : 4.5,
"price" : "201-400",
"likesReview" : 112,
"okReview" : 2,
"badReview" : 0,
"attitude" : -1,
"location" : "22.29771142024199,114.1784066676926",
"status" : "",
"score" : 0.5705653294987613,
"esId" : "64058"
}
}
]
}
}
在返回结果中,有一个很重要的 _scroll_id字段。在后面的请求中我们都要带着这个 _scroll_id 去请求。
curl -XGET '127.0.0.1:9200/_search/scroll?pretty&scroll=1m&scroll_id=DnF1ZXJ5VGhlbkZldGNoBQAAAAAAABdiFlVVcW1sUmxmVFYtQk1ZbWR0dXNGamcAAAAAAAAXYxZVVXFtbFJsZlRWLUJNWW1kdHVzRmpnAAAAAAAAF2UWVVVxbWxSbGZUVi1CTVltZHR1c0ZqZwAAAAAAABdmFlVVcW1sUmxmVFYtQk1ZbWR0dXNGamcAAAAAAAAXZBZVVXFtbFJsZlRWLUJNWW1kdHVzRmpn'
每次的查询,都把最新的scroll_id带上,直到数据查询完成为止。
所有文档获取完毕之后,需要手动清理掉 scroll_id 。虽然es 会有自动清理机制,但是 srcoll_id 的存在会耗费大量的资源来保存一份当前查询结果集快照,并且会占用文件描述符。所以用完之后要及时清理。使用 es 提供的 CLEAR_API 来删除指定的 scroll_id
- 删掉指定的多个 srcoll_id
curl -XDELETE 127.0.0.1:9200/_search/scroll -H 'Content-Type: application/json' -d '{"scroll_id" : ["DnF1ZXJ5VGhlbkZldGNoBQAAAAAAABa4FlVVcW1sUmxmVFYtQk1ZbWR0dXNGamcAAAAAAAAWuhZVVXFtbFJsZlRWLUJNWW1kdHVzRmpnAAAAAAAAFrwWVVVxbWxSbGZUVi1CTVltZHR1c0ZqZwAAAAAAABa7FlVVcW1sUmxmVFYtQk1ZbWR0dXNGamcAAAAAAAAWuRZVVXFtbFJsZlRWLUJNWW1kdHVzRmpn"]}'
- 删除掉所有索引上的 scroll_id
curl -XDELETE 127.0.0.1:9200/_search/scroll/_all
三、search_after 的方式
上述的 scroll search 的方式,官方的建议并不是用于实时的请求,因为每一个 scroll_id 不仅会占用大量的资源(特别是排序的请求),而且是生成的历史快照,对于数据的变更不会反映到快照上。这种方式往往用于非实时处理大量数据的情况,比如要进行数据迁移或者索引变更之类的。那么在实时情况下如果处理深度分页的问题呢?es 给出了 search_after 的方式,这是在 >= 5.0 版本才提供的功能。
search_after 分页的方式和 scroll 有一些显著的区别,首先它是根据上一页的最后一条数据来确定下一页的位置,同时在分页请求的过程中,如果有索引数据的增删改查,这些变更也会实时的反映到游标上。
- 第一页的请求和正常的请求一样,
curl -XGET '127.0.0.1:9200/shop/shop/_search?pretty' -H 'Content-Type: application/json' -d'
{
"size": 2,
"query": {
"term" : {
"area" : "尖沙咀"
}
},
"sort": [
{"_id": "desc"}
]
}
'
返回结果
{
"took" : 23,
"timed_out" : false,
"_shards" : {
"total" : 5,
"successful" : 5,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : 10264,
"max_score" : null,
"hits" : [
{
"_index" : "shop",
"_type" : "shop",
"_id" : "99998",
"_score" : null,
"_source" : {
"appId" : 3,
"id" : "99998",
"shopName" : "茹絲葵牛排餐廳",
"area" : "尖沙咀",
"address" : "尖沙咀麼地道66號尖沙咀中心1樓108-110號舖",
"branchStoreId" : 10001661,
"longitude" : "114.182714701441",
"latitude" : "22.294923943567408",
"collects" : "11535",
"shopImg" : "/prod/restaurant/ea25592c551af318ed45375e2176314f.jpg",
"shopType" : 0,
"categoryName" : "美國菜/扒房/酒",
"shopGrade" : 4.5,
"price" : "401-800",
"likesReview" : 64,
"okReview" : 6,
"badReview" : 2,
"attitude" : -1,
"location" : "22.294923943567408,114.182714701441",
"status" : "",
"score" : 0.5910990762455836,
"esId" : "99998"
},
"sort" : [
"99998"
]
},
{
"_index" : "shop",
"_type" : "shop",
"_id" : "99990",
"_score" : null,
"_source" : {
"appId" : 3,
"id" : "99990",
"shopName" : "鴻福堂",
"area" : "尖沙咀",
"address" : "尖沙咀港鐵尖東站28號舖",
"branchStoreId" : 10000614,
"longitude" : "114.17838477184749",
"latitude" : "22.29206829010525",
"collects" : "93",
"shopImg" : "/prod/restaurant/a110399137a18f2a629a808a3d121940.jpg",
"shopType" : 0,
"categoryName" : "粵菜 (廣東)/涼茶/龜苓膏/藥膳",
"shopGrade" : 3.5,
"price" : "50以下",
"likesReview" : 0,
"okReview" : 2,
"badReview" : 1,
"attitude" : -1,
"location" : "22.29206829010525,114.17838477184749",
"status" : "",
"score" : 0.49259720096169446,
"esId" : "99990"
},
"sort" : [
"99990"
]
}
]
}
}
第二页的请求,使用第一页返回结果的最后一个数据的sort值,传给 search_after 字段来取下一页。注意,使用 search_after 的时候要将 from 置为 0 或 -1
curl -XGET '127.0.0.1:9200/shop/shop/_search?pretty' -H 'Content-Type: application/json' -d'
{
"size": 2,
"query": {
"term" : {
"area" : "尖沙咀"
}
},
"search_after": [99905],
"sort": [
{"_id": "desc"}
]
}
'
总结:search_after 适用于深度分页 + 排序,因为每一页的数据依赖于上一页最后一条数据,所以无法跳页请求。
且返回的始终是最新的数据,在分页过程中数据的位置可能会有变更。