Post

什么是ElasticSearch的深度分页问题?如何解决?

什么是ElasticSearch的深度分页问题?如何解决?

什么是ElasticSearch的深度分页问题?如何解决?

典型回答

在Elasticsearch中进行分页查询通常使用from和size参数。当我们对Elasticsearch发起一个带有分页参数的查询(如使用from和size参数)时,ES需要遍历所有匹配的文档直到达到指定的起始点(from),然后返回从这一点开始的size个文档。

1
2
3
4
5
6
7
8
9
GET /your_index/_search
{
  "from": 20,
  "size": 10,
  "query": {
    "match_all": {}
  }
}

在这个例子中:

  • from 参数定义了要跳过的记录数。在这里,它跳过了前20条记录。
  • size 参数定义了返回的记录数量。在这里,它返回了10条记录。

from + size 的总数不能超过Elasticsearch索引的index.max_result_window设置,默认为10000。这意味着如果你设置from为9900,size为100,查询将会成功。但如果from为9900,size为101,则会失败。

ES的检索机制决定了,当进行分页查询时,Elasticsearch需要先找到并处理所有位于当前页之前的记录。例如,如果你请求第1000页的数据,并且每页显示10条记录,系统需要先处理前9990条记录,然后才能获取到你请求的那10条记录。这意味着,随着页码的增加,数据库需要处理的数据量急剧增加,导致查询效率降低。

这就是ES的深度分页的问题,深度分页需要数据库在内存中维护大量的数据,并对这些数据进行排序和处理,这会消耗大量的CPU和内存资源。随着分页深度的增加,查询响应时间会显著增加。在某些情况下,这可能导致查询超时或者系统负载过重。

所以,需要想办法解决ES的深度分页的问题。

scroll

Scroll API在Elasticsearch中的主要目的是为了能够遍历大量的数据,它通常用于数据导出或者进行大规模的数据分析。可以用于处理大量数据的深度分页问题。

1
2
3
4
5
6
7
8
GET /your_index/_search?scroll=1m
{
  "size": 10,  // 每页10条记录
  "query": {
    "match_all": {}
  }
}

如上方式初始化一个带有scroll参数的搜索请求。这个请求返回一个scroll ID,用于后续的滚动。Scroll参数指定了scroll的有效期,例如1m表示一分钟。

接下来就可以使用返回的scroll ID来获取下一批数据。每次请求也会更新scroll ID的有效期。

1
2
3
4
5
GET /_search/scroll
{
  "scroll": "1m",
  "scroll_id": "your_scroll_id"
}

我们需要重复以上操作直到到达想要的页数。比如第10页,则需要执行9次滚动操作,然后第10次请求将返回第10页的数据。

Scroll API可以解决深度分页问题,主要是因为他有以下几个特点:

  1. 避免重复排序
    • 在传统的分页方式中,每次分页请求都需要对所有匹配的数据进行排序,以确定分页的起点。Scroll避免了这种重复排序,因为它保持了一个游标。
  2. 稳定视图
    • Scroll提供了对数据的”稳定视图”。当你开始一个scroll时,Elasticsearch会保持搜索时刻的数据快照,这意味着即使数据随后被修改,返回的结果仍然是一致的。
  3. 减少资源消耗
    • 由于不需要重复排序,Scroll减少了对CPU和内存的消耗,特别是对于大数据集。

Scroll非常适合于处理需要访问大量数据但不需要快速响应的场景,如数据导出、备份或大规模数据分析。

但是,需要知道,使用Scroll API进行分页并不高效,因为你需要先获取所有前面页的数据。Scroll API主要用于遍历整个索引或大量数据,而不是用于快速访问特定页数的数据。

search_after

search_after 是 Elasticsearch 中用于实现深度分页的一种机制。与传统的分页方法(使用 from 和 size 参数)不同,search_after 允许你基于上一次查询的结果来获取下一批数据,这在处理大量数据时特别有效。

在第一次查询时,你需要定义一个排序规则。不需要指定 search_after 参数:

1
2
3
4
5
6
7
8
9
10
11
12
GET /your_index/_search
{
  "size": 10,
  "query": {
    "match_all": {}
  },
  "sort": [
    {"timestamp": "asc"},
    {"id": "asc"}
  ]
}

这个查询按 timestamp 字段排序,并在相同 timestamp 的情况下按 _id 排序。

在后续的查询中,使用上一次查询结果中最后一条记录的排序值。

1
2
3
4
5
6
7
8
9
10
11
12
13
GET /your_index/_search
{
  "size": 10,
  "query": {
    "match_all": {}
  },
  "sort": [
    {"timestamp": "asc"},
    {"id": "asc"}
  ],
  "search_after": [1609459200000, 10000]
}

在这个例子中,search_after 数组包含了 timestamp 和 _id 的值,对应于上一次查询结果的最后一条记录。

search_after 可以有效解决深度分页问题,原因如下:

  1. 避免重复处理数据:与传统的分页方式不同,search_after 不需要处理每个分页请求中所有先前页面上的数据。这大大减少了处理数据的工作量。

  2. 提高查询效率:由于不需要重复计算和跳过大量先前页面上的数据,search_after 方法能显著提高查询效率,尤其是在访问数据集靠后部分的数据时。

但是这个方案有一些局限,一方面需要有一个全局唯一的字段用来排序,另外虽然一次分页查询时不需要处理先前页面中的数据,但实际需要依赖上一个页面中的查询结果。

对比

  使用场景 实现方式 优点 缺点
传统分页 适用于小数据集和用户界面中的标准分页,如网站上的列表分页。 通过指定from(起始位置)和size(页面大小)来实现分页。 实现简单,适用于小数据集,易于理解和使用。 不适用于深度分页。当from值很大时,性能急剧下降。Elasticsearch默认限制from + size不超过10000。
scroll 适用于大规模数据的导出、备份或处理,而不是实时用户请求。 初始化一个scroll请求,然后使用返回的scroll id来连续地获取后续数据。 可以有效处理大量数据。提供了数据快照,保证了查询过程中数据的一致性。 不适合实时请求。初始化scroll会占用更多资源,因为它在后端维护了数据的状态。
search_after 适用于深度分页和大数据集的遍历。 基于上一次查询结果的排序值来获取下一批数据。 解决了深度分页的性能问题。更适合于处理大数据量,尤其是当需要顺序遍历整个数据集时。 不适用于随机页访问。需要精确的排序机制,并在每次请求中维护状态。
  • 对于小型数据集和需要随机页面访问的标准分页场景,传统的分页是最简单和最直接的选择。
  • 对于需要处理大量数据但不需要随机页面访问的场景,尤其是深度分页,search_after提供了更好的性能和更高的效率。
  • 当需要处理非常大的数据集并且对数据一致性有要求时(如数据导出或备份),Scroll API是一个更好的选择。
This post is licensed under CC BY 4.0 by the author.