ElasticSearch 深度分页怎么选择

最近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) 范围内。

对于需要深度分页的场景,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下拉滑动分页
相关推荐
斑驳竹影21 分钟前
ElasticSearch存储引擎
大数据·elasticsearch·搜索引擎
努力的布布5 小时前
Elasticsearch-模糊查询
大数据·elasticsearch·搜索引擎
m0_748237057 小时前
Monorepo pnpm 模式管理多个 web 项目
大数据·前端·elasticsearch
java1234_小锋8 小时前
ElasticSearch如何做性能优化?
大数据·elasticsearch·性能优化
LI JS@你猜啊19 小时前
Elasticsearch 集群
大数据·服务器·elasticsearch
神奇侠20241 天前
解决集群Elasticsearch 未授权访问漏洞
elasticsearch
Elastic 中国社区官方博客1 天前
如何通过 Kafka 将数据导入 Elasticsearch
大数据·数据库·分布式·elasticsearch·搜索引擎·kafka·全文检索
神奇侠20241 天前
解决单台Elasticsearch 未授权访问漏洞
elasticsearch
nece0011 天前
elasticsearch 杂记
大数据·elasticsearch·搜索引擎
开心最重要(*^▽^*)1 天前
Es搭建——单节点——Linux
大数据·elasticsearch