一、前言
Elasticsearch 是一款分布式的搜索引擎,它提供了灵活的分页技术,而在实际应用中,往往前端需要对数据进行分页展示,但是传统的分页方式存在一定的性能瓶颈,而ES为了避免深度分页所造成的性能问题,提供了响应的解决方案。

二、什么是深度分页?
简单来说,就是搜索的特别深,比如总共有100000条数据,四个primary shard,每个shard上分了25000条数据,每页是10条数据,这个时候,你要搜索到第1000页,实际上要拿到的是10001~10010,为了获取这些深层次页面的数据,系统通常需要先查询并处理前面所有页面的数据,以确保能够正确地显示用户所请求的页面,这个过程可能导致查询范围非常广泛,从而显著降低查询效率,增加服务器负担。

三、ES为什么要避免深度分页?
Elasticsearch在进行搜索操作时,尤其是在分页查询时,应尽量避免深度分页。
1.性能问题:深度分页意味着要跳过大量的搜索结果来获取目标页的数据。这会导致高开销的跳过操作和额外的网络通信,增加搜索请求的响应时间。对于大数据集或搜索负载较重的环境,深度分页可能会导致性能下降。

2.资源消耗:深度分页需要Elasticsearch在内存中缓存大量的搜索结果,并且需要维护更长时间的游标状态。这会增加资源消耗,特别是当多个搜索请求同时进行时,会占用更多的内存和处理能力。

3.数据不稳定性:在深度分页过程中,如果有新的文档被索引或删除,可能会导致结果的不稳定性。因为每次搜索请求返回的是一个快照,而不是固定的结果集,这可能会导致在连续的深度分页查询中出现重复或遗漏的数据。

四、ES分页执行原理
1.es 默认采用的分页方式是 from+ size 的形式,from表示查询结果的起始下标,size表示从起始下标开始返回文档的个数,进行分页查询示例如下:

GET /my_book/_search
{
  "from": 100,
  "size": 10,
  "query": {
    "match": {
      "content": "java"
    }
  }
}

但在深度分页的情况下,这种使用方式效率是非常低的,比如from = 20000, size=10,首先请求可能会请求到不包含这个index的shard的node上去,这个node就是一个coordinate node,也叫协调节点,那么这个coordinate node就会将搜索请求转发到index的三个shard所在node上。es 需要在各个分片上匹配排序并得到20010条有效数据,如果是3个shard的话,那么协调节点就会拿到60030节点,然后对这些数据进行排序,相关度分数 ,在结果集中取最后10条数据返回,这种方式类似于 skip + size。

2.在ES中,搜索一般包括两个阶段,query 和 fetch 阶段,简单的理解,就是query 阶段确定要取哪些doc,fetch 阶段取出具体的 doc。

(1)Query 阶段:

  • 第一步:Client 发送查询请求到 Server 端,Node1 接收到请求然后创建一个大小为 from + size 的优先级队列用来存放结果,此时 Node1 被称为 coordinating node(协调节点);
  • coordinating node将请求广播到涉及到的 shards,每个shard在内部执行搜索请求,然后将结果存到内部的大小同样为from + size 的优先级队列里。
  • 每个 shard 将暂存的自身优先级队列里的结果返给 Node1,Node1 拿到所有 shard 返回的结果后,对结果进行一次合并,产生一个全局的优先级队列,放在 Node1 的优先级队列中。

(2)Fetch 阶段:

  • Node1 根据刚才合并后保存在优先级队列中的 from+size 条数据的 id 集合,发送请求到对应的 shard 上查询 doc 数据详情。
  • shard 根据 doc 的_id取到数据详情,然后返回给 coordinating node(Node1)。
  • coordinating node (Node1) 获取到对应的分页数据后, 返回数据给 Client。

Node1 中的优先级队列中保存了 from + size 条数据的_id,但是在 Fetch 阶段并不需要取回所有数据,只需要取回从 from 到 from + size 之间的 size 条数据详情即可,这 size 条数据可能在同一个 shard 也可能在不同的 shard,因此 Node1 使用 「multi-get」 来避免多次去同一分片取数据,从而提高性能。

五、如何解决深度分页?
1.使用scroll API进行深度分页。
Elasticsearch提供了游标机制,通过scroll API,我们可以在一次查询中获取所有满足条件的文档,然后根据需要对它们进行排序和过滤而不需要跳过和重新查询。这种方法适用于需要一次性获取大量数据的场景。

原理:
scroll api 的方式会创建一个快照,每次查询后,输入上一次的 scroll_id, 来实现 下一页 的功能。
它通过在搜索结果中建立一个保持状态的 scroll_id 来实现。当您开始滚动时,ES 会返回第一批结果,并返回一个保持状态的 ID。使用此 ID,可以执行下一个滚动请求,以检索下一批结果。此过程可以重复进行,直到所有数据都被扫描完毕为止。

(1)scroll 分页方式的优点就是减少了查询和排序的次数,避免性能损耗。缺点就是只能实现上一页、下一页的翻页功能,不兼容通过页码查询数据的跳页,同时由于其在搜索初始化阶段会生成快照,后续数据的变化无法及时体现在查询结果,因此更加适合一次性批量查询或非实时数据的分页查询。

(2)srcoll_id 的存在会耗费大量的资源来保存一份当前查询结果集映像,并且会占用文件描述符。所以用完之后要及时清理。使用 es 提供的 CLEAR_API 来删除指定的 scroll_id。

注意:ES官方不再推荐使用Scroll API 进行深度分页,如果要分页检索超过 Top 10,000+ 结果时,推荐使用带有时间点 (PIT) 的 search_after 参数。

2.使用Search After进行深度分页。
使用search_after 进行分页 相比 from & size 的方式要更加高效,而且在不断有新数据入库的时候仅仅使用 from 和 size 分页会有重复的情况,相比使用 scroll 分页,search_after 可以进行实时的查询,不过 search_after 不适合跳跃式的分页。

原理:首先它是根据上一页的最后一条数据来确定下一页的位置,同时在分页请求的过程中,如果有索引数据的增删改查,这些变更也会实时的反映到游标上。

总结:search_after 适用于深度分页+ 排序,因为每一页的数据依赖于上一页最后一条数据,所以无法跳页请求。

3.java实现es分页查询,如何在elasticsearch里面使用深度分页功能
在 Elasticsearch 中使用深度分页功能需要注意以下几点:

  • 尽量避免使用深度分页功能,因为它会增加网络和计算开销,可能导致性能问题。
  • 深度分页功能是通过设置 from 和 size 参数来实现的。from 参数表示从哪个位置开始查询,size 参数表示每页返回的文档数量。
  • Elasticsearch 默认最多只能返回 10000 条记录,如果需要查询更多的记录,需要设置
    index.max_result_window 参数。但是设置太大会占用过多的内存,影响性能。

五、小结
1.from+size 适合翻页灵活且页数不大的场景,即适合浅分页场景。
2.search_after 适合深分页,也可以来回翻页(需要业务存储每页所需的search_after)
3.scroll 可以做分页,但是只能顺序往后翻页,无法实时查询,不够灵活,所以它最适合做大量数据的导出(对顺序无要求,scan完所有数据即可)。

因此,在使用 ES 时一定要避免深度分页问题,要在跳页功能实现和 ES 性能、资源之间做一个取舍。在使用ElasticSearch进行深度分页查询时,我们需要了解其分页原理以及各种分页方案的优缺点,以便根据实际情况选择合适的方案。