最近APP部门又提了新需求,文章分页要改成上滑分页。我对原来的es搜索分页方案做了重新调研评估,把es中的分页方式整理一下分享给大家。
es查询流程
1、Query阶段
如上图所示,Query 阶段大致分为 3 步:
- 第一步:Client 发送查询请求到 Server 端,Node1 接收到请求然后创建一个大小为 from + size 的优先级队列用来存放结果,此时 Node1 被称为 coordinating node(协调节点);
- 第二步:Node1 将请求广播到涉及的 shard 上,每个 shard 内部执行搜索请求,然后将执行结果存到自己内部的大小同样为 from+size 的优先级队列里;
- 第三步:每个 shard 将暂存的自身优先级队列里的结果返给 Node1,Node1 拿到所有 shard 返回的结果后,对结果进行一次合并,产生一个全局的优先级队列,存在 Node1 的优先级队列中。(如上图中,Node1 会拿到 (from + size) * 3 条数据,这些数据只包含 doc 的唯一标识_id 和用于排序的_score不包括文档内容,然后 Node1 会对这些数据合并排序,选择前 from + size 条数据存到优先级队列);
2、Feach阶段
当 Query 阶段结束后立马进入 Fetch 阶段,Fetch 阶段也分为 3 步:
- 第一步:Node1 根据刚才合并后保存在优先级队列中的 from+size 条数据的 id 集合,发送请求到对应的 shard 上查询 doc 数据详情;
- 第二步:各 shard 接收到查询请求后,查询到对应的数据详情并返回为 Node1;(Node1 中的优先级队列中保存了 from + size 条数据的_id,但是在 Fetch 阶段并不需要取回所有数据,只需要取回从 from 到 from + size 之间的 size 条数据详情即可,这 size 条数据可能在同一个 shard 也可能在不同的 shard,因此 Node1 使用 multi-get 来提高性能)
- 第三步:Node1 获取到对应的分页数据后,返回给 Client;
分页方式
上面介绍了es的查询流程,现在说一下es中的分页都有哪些
- form+size分页
- scroll分页
- search after分页
from+size分页方式
from + size 分页方式是 ES 最基本的分页方式,类似于关系型数据库中的 limit 方式。from 参数表示:分页起始位置;size 参数表示:每页获取数据条数。
java
GET abc_alias/_search
{
"query": {
"content": "chatgpt"
},
"size": 10,
"from": 100
}
在这个示例中,我们将 from 参数设置为 100,size 参数设置为 10,以便返回结果集中 [100,110) 共 10 个匹配 content 字段的文档,相当于返回第 11 页的结果集。那是不是我们可以在任何分页场景下都可以使用这种分页方式呢,答案是否定的,ES 对from+size 是有限制的,默认值为 10000
ES 官方文档给出的解释是:对于深度分页,ES 的每个 shard 都会查询 TopN(文档信息)(查询流程,注意 N=from+size)的结果,即查询 [from, from+size) 的结果实际上数据节点会查询 from+size 个结果,也就是将 [0, from) 的结果也一并查出来了,这样将会导致 CPU 和 内存的开销较大,导致系统卡顿甚至 OOM(特别是协调节点,要收集多个 shard 返回的结果,内存开销更大)。因此,from+size 常常应用于分页深度较小的场景,不适合深分页场景。es默认index.max_result_window =10000,如果超过最大数量会报错提示
例如:索引 wms_order_sku 有 1 亿数据,分 10 个 shard 存储,当一个请求的 from = 1000000, size = 10。在 Query 阶段,每个 shard 就需要返回 1000010 条数据的_id 和_score 信息,而 coordinating node 就需要接收 10 * 1000010 条数据,拿到这些数据后需要进行全局排序取到前 1000010 条数据的_id 集合保存到 coordinating node 的优先级队列中,后续在 Fetch 阶段再去获取那 10 条数据的详情返回给客户端。在 Query 阶段会在每个 shard 上均有巨大的查询量,返回给 coordinating node 时需要执行大量数据的排序操作,并且保存到优先级队列的数据量也很大,占用大量节点机器内存资源。
明明只查询 size 个结果,为什么每个 shard 偏要将 [0, from+size) 的结果都查出来呢,直接返回 [from, from+size] 的结果不就完了吗?要回答这个问题,首先要来看个简单的例子,如下:
如上图,假设有 3 个shard,每个 shard 的文档按照 value 字段大小逆序排序,查询的 from 设置为 2,size 设置为 3。那么按照 ES 的处理逻辑,每个 shard 都会返回 [0, 5) 的文档(注意不包含文档内容),协调节点将收到 15 条文档,然后对这 15 条文档按照 value 排序,取前 [2, 5) 的文档为结果。如上图的 result 即为正确结果。
那么如果每个 shard 只返回各自的 [2, 5) 的文档,结果将会如何呢?请看下图
上图表示每个 shard 返回 [2, 5) 的文档集合并后的结果。很明显,这个结果是不正确的,原因便是每个 shard 并不知道全局的排序结果,因此为了保证能够得到正确的结果,必须返回 [0, 5] 的结果集,因为它们中的任意一个都可能在全局序的 [2, 5) 范围内。
Search After分页
对于需要深度分页的场景,Elasticsearch提供了另一种分页方式search_after。他是一种更有效的分页方法,可以在不加载整个数据集的情况下快速地获取下一页数据。
search_after 是一种基于游标的分页方法,使用 search_after 查询时必须指定排序字段(可以有多个),它使用排序字段值作为游标,从而能够更快地获取下一页的数据。在进行第一次搜索时,ES 会返回第一页的结果,当需要获取下一页数据时,可以使用上一页最后一个文档的排序字段值作为游标进行搜索。通过这种方式,可以逐步遍历整个数据集而无需一次性加载所有数据。 使用 search_after 机制需要注意以下几点:
- 搜索请求必须指定排序字段,用于指定搜索结果的顺序
- 搜索第一页不必指定 search_after 参数,从第二页开始必须指定 search_after 为上一页的最后一个游标
- 游标必须是唯一的,否则可能会出现重复的数据
- sort字段必须是keyword类型,开启doc_value可用于排序
什么是doc_value?
倒排索引可以提供全文检索能力,但是无法提供对排序和数据聚合的支持。doc_values 本质上是一个序列化的列式存储结构,适用于聚合(aggregations)、排序(Sorting)、脚本(scripts access to field)等操作。默认情况下,ES几乎会为所有类型的字段存储doc_value,但是 text 等可分词字段不支持 doc values 。如果不需要对某个字段进行排序或者聚合,则可以关闭该字段的doc_value存储。
用法举例:
java
请求:
GET abc_index/_search
{
"query": {
"match": {
"title": "测试"
}
},
"sort": [
{
"publishTime": "asc"
},
{
"articleId": "desc"
}
]
}
响应:
{
"publishTime": 0,
"recommendColumn": [],
"siteId": 1,
"source": "隆众资讯",
"sourceType": 0,
"stickExpireTime": 0,
"stickTime": 0,
"subTitle": "",
"summary": "",
"title": "2023年01月28日1.28新增测试33",
"sort": [
0,
9740049
]
}
第二次查询:
GET abc_index/_search
{
"query": {
"match": {
"title": "测试"
}
},
"sort": [
{
"publishTime": "asc"
},
{
"articleId": "desc"
}
],
"search_after": [
0,
9740049
]
}
可以看到,search_after 的使用特别灵活,只要指定了游标值,便能根据游标值查询下一页文档。由于查询过程中,可能还会有数据写入,那么多次查询使用一个游标可能得到的结果不一致,如果业务有一致性需求,需要使用 point in time(PIT) 来创建一个临时的快照,查询时使用该快照保证数据一致性。
scroll分页
Scroll 查询是一种在 ES 中扫描大量数据的常用方法。它通过在搜索结果中建立一个保持状态的 scroll_id 来实现。当您开始滚动时,ES 会返回第一批结果,并返回一个保持状态的 ID。使用此 ID,可以执行下一个滚动请求,以检索下一批结果。此过程可以重复进行,直到所有数据都被扫描完毕为止。
用法
java
第一次查询要指定 scroll 参数,参数值代表 scroll 上下文的保留时长,保留时间过期后,scroll_id 将失效。
请求:
POST /abc_index/_search?scroll=1m
{
"query": {
"match": {
"keyWords": "能源"
}
}
}
响应:
{
"_scroll_id": "DnF1ZXJ5VGhlbkZldGNoCgAAAABaWjYnFnRCMFFzT3BoUkMtQl9rWWhaWDI2NlEAAAAAWj6JfhZGMVdxancyNVRyaVg2NHNObDA5dkx3AAAAAFo-iX8WRjFXcWp3MjVUcmlYNjRzTmwwOXZMdwAAAACoeYaFFmlvZFlNcU5OVE5hUVJ5UHFJVFk2R0EAAAAAWv3GaxZEMjJTYUl5dVN3ZVNUMDJQQW1sTDhnAAAAAKh5hoYWaW9kWU1xTk5UTmFRUnlQcUlUWTZHQQAAAACZD7aRFm02OE44M015UWN5U3pNQktSeEV2X3cAAAAAWlo2KBZ0QjBRc09waFJDLUJfa1loWlgyNjZRAAAAAJkPtosWbTY4TjgzTXlRY3lTek1CS1J4RXZfdwAAAABa_cZsFkQyMlNhSXl1U3dlU1QwMlBBbWxMOGc=",
"took": 378,
"timed_out": false,
"_shards": {
"total": 10,
"successful": 10,
"skipped": 0,
"failed": 0
},
"hits": {
"total": 989055,
"max_score": 1.431006,
"hits": [
{
"_index": "mysteel_oilchem_article_v6",
"_type": "mysteel_site",
"_id": "oilchem_12049713",
"_score": 1.431006,
"_source": {
"areaCodes": [
"1957"
],
"areaNames": "济源 济源市",
"author": "朱英华",
"bizType": "oilchem",
"id": "oilchem_12049713",
"informationTypes": [
"250"
],
"isRecommend": "1",
"isShow": "0",
"keyWords": "隆众资讯 能源",
"publishTime": 1583374357559,
"source": "隆众资讯",
"status": "2",
"summary": "垦利瑞和化工,柴油,柴油价格",
"summarySegs": "垦利 垦利瑞和 化工 柴油 柴油 油价 油价格 价格",
"title": "[华北]:河南顺天石化汽油报价下跌",
"titleSegs": "华北 河南 顺天 顺天石化 石化 汽油 报价 下跌 [ 华 北 ] : 河 南 顺 天 石 化 汽 油 报 价 下 跌",
"unitId": "31360"
}
}
]
}
}
第二次请求:
GET /_search/scroll
{
"scroll": "1m",
"scroll_id": "DnF1ZXJ5VGhlbkZldGNoCgAAAABaWjYnFnRCMFFzT3BoUkMtQl9rWWhaWDI2NlEAAAAAWj6JfhZGMVdxancyNVRyaVg2NHNObDA5dkx3AAAAAFo-iX8WRjFXcWp3MjVUcmlYNjRzTmwwOXZMdwAAAACoeYaFFmlvZFlNcU5OVE5hUVJ5UHFJVFk2R0EAAAAAWv3GaxZEMjJTYUl5dVN3ZVNUMDJQQW1sTDhnAAAAAKh5hoYWaW9kWU1xTk5UTmFRUnlQcUlUWTZHQQAAAACZD7aRFm02OE44M015UWN5U3pNQktSeEV2X3cAAAAAWlo2KBZ0QjBRc09waFJDLUJfa1loWlgyNjZRAAAAAJkPtosWbTY4TjgzTXlRY3lTek1CS1J4RXZfdwAAAABa_cZsFkQyMlNhSXl1U3dlU1QwMlBBbWxMOGc="
}
总结
分页方式 | 性能 | 优点 | 缺点 | 场景 |
---|---|---|---|---|
from+size | 低 | 灵活性好,实现简单,支持跳页 | 深度分页,资源消耗过多,效率越往后越慢 | 适合翻页灵活且数据量不大的场景 |
srcoll | 高 | 解决了深度分页,内部维护快照,效率高 | 无法反应数据实时性,需要足够的上下文内存空间,维护快照成本高,需要维护scroll_id,只能向后翻页,不够灵活 | 海量数据查询,深度翻页,后台数据批量处理等任务 |
search after | 高 | 解决深度分页,性能高,数据实时更新 | 实现复杂,需要全局唯一字段排序,并且每次查询都需要上次查询的结果。不支持跳页 | 深度翻页,用户实时查询需求,例如手机APP下拉滑动分页 |