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下拉滑动分页
相关推荐
hengzhepa3 小时前
ElasticSearch备考 -- Async search
大数据·学习·elasticsearch·搜索引擎·es
bubble小拾11 小时前
ElasticSearch高级功能详解与读写性能调优
大数据·elasticsearch·搜索引擎
不能放弃治疗12 小时前
重生之我们在ES顶端相遇第 18 章 - Script 使用(进阶)
elasticsearch
hengzhepa12 小时前
ElasticSearch备考 -- Search across cluster
学习·elasticsearch·搜索引擎·全文检索·es
Elastic 中国社区官方博客14 小时前
Elasticsearch:使用 LLM 实现传统搜索自动化
大数据·人工智能·elasticsearch·搜索引擎·ai·自动化·全文检索
慕雪华年15 小时前
【WSL】wsl中ubuntu无法通过useradd添加用户
linux·ubuntu·elasticsearch
Elastic 中国社区官方博客17 小时前
使用 Vertex AI Gemini 模型和 Elasticsearch Playground 快速创建 RAG 应用程序
大数据·人工智能·elasticsearch·搜索引擎·全文检索
alfiy18 小时前
Elasticsearch学习笔记(四) Elasticsearch集群安全配置一
笔记·学习·elasticsearch
alfiy19 小时前
Elasticsearch学习笔记(五)Elastic stack安全配置二
笔记·学习·elasticsearch
丶21361 天前
【大数据】Elasticsearch 实战应用总结
大数据·elasticsearch·搜索引擎