京东面试题:ElasticSearch 深度分页解决方案
Scroll
======
?
Scroll 遍历数据
?
我们可以把 scroll 理解为关系型数据库里的 cursor,因此,scroll 并不适合用来做实时搜索,而更适合用于后台批处理任务,比如群发。
这个分页的用法,「不是为了实时查询数据」,而是为了**「一次性查询大量的数据(甚至是全部的数据」**)。
因为这个 scroll 相当于维护了一份当前索引段的快照信息,这个快照信息是你执行这个 scroll 查询时的快照。在这个查询后的任何新索引进来的数据,都不会在这个快照中查询到。
但是它相对于 from 和 size,不是查询所有数据然后剔除不要的部分,而是记录一个读取的位置,保证下一次快速继续读取。
不考虑排序的时候,可以结合 SearchType.SCAN 使用。
scroll 可以分为初始化和遍历两部,初始化时将**「所有符合搜索条件的搜索结果缓存起来(注意,这里只是缓存的 doc_id,而并不是真的缓存了所有的文档数据,取数据是在 fetch 阶段完成的)」**,可以想象成快照。
在遍历时,从这个快照里取数据,也就是说,在初始化后,对索引插入、删除、更新数据都不会影响遍历结果。
「基本使用」
POST /twitter/tweet/_search?scroll=1m
{
"size": 100,
"query": {
"match" : {
"title" : "elasticsearch"
}
}
}
初始化指明 index 和 type,然后,加上参数 scroll,表示暂存搜索结果的时间,其它就像一个普通的 search 请求一样。
会返回一个_scroll_id,_scroll_id 用来下次取数据用。
「遍历」
POST /_search?scroll=1m
{
"scroll_id":"XXXXXXXXXXXXXXXXXXXXXXX I am scroll id XXXXXXXXXXXXXXX"
}
这里的 scroll_id 即 上一次遍历取回的_scroll_id 或者是初始化返回的_scroll_id,同样的,需要带 scroll 参数。
重复这一步骤,直到返回的数据为空,即遍历完成。
「注意,每次都要传参数 scroll,刷新搜索结果的缓存时间」。另外,「不需要指定 index 和 type」。
设置 scroll 的时候,需要使搜索结果缓存到下一次遍历完成,「同时,也不能太长,毕竟空间有限。」
「优缺点」
缺点:
「scroll_id 会占用大量的资源(特别是排序的请求)」
同样的,scroll 后接超时时间,频繁的发起 scroll 请求,会出现一些列问题。
「是生成的历史快照,对于数据的变更不会反映到快照上。」
「优点:」
适用于非实时处理大量数据的情况,比如要进行数据迁移或者索引变更之类的。
Scroll Scan
===========
ES 提供了 scroll scan 方式进一步提高遍历性能,但是 scroll scan 不支持排序,因此 scroll scan 适合不需要排序的场景
「基本使用」
Scroll Scan 的遍历与普通 Scroll 一样,初始化存在一点差别。
POST /my_index/my_type/_search?search_type=scan&scroll=1m&size=50
{
"query": { "match_all": {}}
}
需要指明参数:
search_type:赋值为 scan,表示采用 Scroll Scan 的方式遍历,同时告诉 Elasticsearch 搜索结果不需要排序。
scroll:同上,传时间。
size:与普通的 size 不同,这个 size 表示的是每个 shard 返回的 size 数,最终结果最大为?number_of_shards * size。
「Scroll Scan 与 Scroll 的区别」
Scroll-Scan 结果**「没有排序」**,按 index 顺序返回,没有排序,可以提高取数据性能。
初始化时只返回?_scroll_id,没有具体的 hits 结果
size 控制的是每个分片的返回的数据量,而不是整个请求返回的数据量。
Sliced Scroll
=============
如果你数据量很大,用 Scroll 遍历数据那确实是接受不了,现在 Scroll 接口可以并发来进行数据遍历了。
每个 Scroll 请求,可以分成多个 Slice 请求,可以理解为切片,各 Slice 独立并行,比用 Scroll 遍历要快很多倍。
POST /index/type/_search?scroll=1m
{
"query": { "match_all": {}},
"slice": {
"id": 0,
"max": 5
}
}
POST ip:port/index/type/_search?scroll=1m
{
"query": { "match_all": {}},
"slice": {
"id": 1,
"max": 5
}
}
上边的示例可以单独请求两块数据,最终五块数据合并的结果与直接 scroll scan 相同。
其中 max 是分块数,id 是第几块。
?
官方文档中建议 max 的值不要超过 shard 的数量,否则可能会导致内存爆炸。
?
Search After
============
Search_after 是 ES 5 新引入的一种分页查询机制,其原理几乎就是和 scroll 一样,因此代码也几乎是一样的。
「基本使用:」
第一步:
POST twitter/_search
{
"size": 10,
"query": {
"match" : {
"title" : "es"
}
},
"sort": [
{"date": "asc"},
{"_id": "desc"}
]
}
返回出的结果信息 :
{
"took" : 29,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 5,
"relation" : "eq"
},
"max_score" : null,
"hits" : [
{
...
},
"sort" : [
...
]
},
{
...
},
"sort" : [
124648691,
"624812"
]
}
]
}
}
上面的请求会为每一个文档返回一个包含 sort 排序值的数组。
这些 sort 排序值可以被用于 search_ 《一线大厂 Java 面试题解析+后端开发学习笔记+最新架构讲解视频+实战项目源码讲义》无偿开源 威信搜索公众号【编程进阶路】 after 参数里以便抓取下一页的数据。
比如,我们可以使用最后的一个文档的 sort 排序值,将它传递给 search_after 参数:
GET twitter/_search
{
"size": 10,
"query": {
"match" : {
"title" : "es"
}
},
"search_after": [124648691, "624812"],
"sort": [
{"date": "asc"},
{"_id": "desc"}
]
}
若我们想接着上次读取的结果进行读取下一页数据,第二次查询在第一次查询时的语句基础上添加 search_after,并指明从哪个数据后开始读取。
「基本原理」
es 维护一个实时游标,它以上一次查询的最后一条记录为游标,方便对下一页的查询,它是一个无状态的查询,因此每次查询的都是最新的数据。
由于它采用记录作为游标,因此**「SearchAfter 要求 doc 中至少有一条全局唯一变量(每个文档具有一个唯一值的字段应该用作排序规范)」**
「优缺点」
「优点:」
无状态查询,可以防止在查询过程中,数据的变更无法及时反映到查询中。
不需要维护 scroll_id,不需要维护快照,因此可以避免消耗大量的资源。
「缺点:」
由于无状态查询,因此在查询期间的变更可能会导致跨页面的不一值。
排序顺序可能会在执行期间发生变化,具体取决于索引的更新和删除。
至少需要制定一个唯一的不重复字段来排序。
它不适用于大幅度跳页查询,或者全量导出,对第 N 页的跳转查询相当于对 es 不断重复的执行 N 次 search after,而全量导出则是在短时间内执行大量的重复查询。
SEARCH_AFTER 不是自由跳转到任意页面的解决方案,而是并行滚动多个查询的解决方案。
总结
==
分页方式性能优点缺点场景 from + size 低灵活性好,实现简单深度分页问题数据量比较小,能容忍深度分页问题 scroll 中解决了深度分页问题无法反应数据的实时性(快照版本)维护成本高,需要维护一个 scroll_id 海量数据的导出需要查询海量结果集的数据 search_after 高性能最好不存在深度分页问题能够反映数据的实时变更实现复杂,需要有一个全局唯一的字段连续分页的实现会比较复杂,因为每一次查询都需要上次查询的结果,它不适用于大幅度跳页查询海量数据的分页
ES7 版本变更
=======
在 7.*版本中,ES 官方不再推荐使用 Scroll 方法来进行深分页,而是推荐使用带 PIT 的 search_after 来进行查询;
从 7.*版本开始,您可以使用 SEARCH_AFTER 参数通过上一页中的一组排序值检索下一页命中。
使用 SEARCH_AFTER 需要多个具有相同查询和排序值的搜索请求。
如果这些请求之间发生刷新,则结果的顺序可能会更改,从而导致页面之间的结果不一致。
为防止出现这种情况,您可以创建一个时间点(PIT)来在搜索过程中保留当前索引状态。
POST /my-index-000001/_pit?keep_alive=1m
返回一个 PIT ID:
{
"id": "46ToAwMDaWR5BXV1aWQyKwZub2RlXzMAAAAAAAAAACoBYwADaWR4BXV1aWQxAgZub2RlXzEAAAAAAAAAAAEBYQADaWR5BXV1aWQyKgZub2RlXzIAAAAAAAAAAAwBYgACBXV1aWQyAAAFdXVpZDEAAQltYXRjaF9hbGw_gAAAAA=="
}
评论