在 ElasticSearch 中,分页是常见的需求,尤其是在处理大数据集时。然而,不同的分页方式在性能、实时性和资源开销上存在显著差异。本文将详细介绍 ElasticSearch 中常见的分页方式,包括 From/Size、Scroll API、Search After 和 Point In Time (PIT) + Search After,并提供代码示例和最佳实践建议。
From/Size
分页是最简单的分页方式,类似于传统数据库中的 LIMIT/OFFSET
。通过指定 from
(偏移量)和 size
(每页大小),可以获取指定范围内的文档。
GET /index/_search
{
"from": 10,
"size": 5,
"query": {
"match_all": {}
}
}
from
: 从第 10 条记录开始。size
: 返回 5 条记录。from
值较大时,ElasticSearch 需要遍历大量文档,内存和计算开销较高。from + size
不能超过 index.max_result_window
(默认 10,000)。虽然可以通过调整该参数突破限制,但会导致性能问题。当from的值较大时,这种基于from/size的分页方式会导致性能问题,主要原因如下:
需要遍历大量文档: 为了找到from位置开始的文档,ElasticSearch必须先排序并跳过前面的所有文档直到达到from的位置。这意味着如果from很大,比如10,000,ElasticSearch实际上需要对查询结果进行排序,并且跳过这10,000个文档来找到你想要的那一部分文档。这个过程对于系统资源是一个巨大的消耗。
内存和计算开销较高: 由于ElasticSearch需要维护一个排序队列来跟踪哪些文档应该被包括在结果集中以及哪些应该被跳过,随着from值的增加,这个队列也会变大。这就导致了更高的内存使用量。此外,排序操作本身也需要大量的CPU时间,特别是当处理的数据集非常庞大时。
影响响应速度: 由于上述原因,当from值较大时,查询的响应时间会显著增加,这对于用户体验是非常不利的,特别是在需要快速响应的场景下。
Scroll API
通过创建数据快照,使用游标(Scroll ID)逐批获取结果。它适合离线导出大量数据,但不适合实时分页。
POST /index/_search?scroll=1m
{
"size": 100,
"query": {
"match_all": {}
}
}
scroll=1m
: 设置 Scroll 上下文有效期为 1 分钟。size
: 每次返回 100 条记录。POST /_search/scroll
{
"scroll": "1m",
"scroll_id": ""
}
scroll_id
: 使用初始化时返回的 Scroll ID 获取下一批数据。DELETE /_search/scroll/<scroll_id>
Search After
基于上一页最后一条记录的排序值(Sort Value)作为起点,避免全局遍历。它适合深度分页且需要实时性的场景。
GET /index/_search
{
"size": 5,
"query": {
"match_all": {}
},
"sort": [
{"timestamp": "desc"},
{"_id": "asc"} // 确保排序唯一性
],
"search_after": [1630000000000, "doc_id_123"]
}
sort
: 指定排序字段,需确保唯一性(如结合 _id
)。search_after
: 使用上一页最后一条记录的排序值作为起点。无论是使用from/size还是search_after进行分页查询,在执行时都需要对符合条件的文档进行全局排序。然而,它们在性能表现上的差异主要体现在如何处理和返回结果集上,特别是当涉及到深度分页(即from值很大)时。
【from/size 分页的问题】
遍历开销: 当使用from/size进行分页时,ElasticSearch需要跳过from参数指定数量的文档来达到开始返回文档的位置。这意味着如果from值很大,ElasticSearch实际上需要遍历并丢弃大量的文档,直到到达正确的起始点。这个过程随着from值的增加变得越来越昂贵。
内存和计算资源消耗: 由于需要维护一个包含所有匹配文档的有序列表,以便能够正确地跳过前面的文档并返回请求的文档部分,这会导致更高的内存使用和CPU计算成本。
【search_after 的优化】
基于上一次的结果继续查询: search_after通过利用前一次查询结果中的最后一个排序值作为起点来实现分页。也就是说,它不是从头开始遍历整个结果集,而是直接从上一次查询结束的地方继续搜索。这样就避免了每次查询都要重新遍历前面所有的文档。
减少不必要的文档加载和排序: 因为search_after只需要关注从特定排序位置之后的文档,它可以更高效地获取下一页的数据而不需要重复之前的工作。这种方法特别适用于需要深入到结果集较远位置的场景,因为它减少了对于那些已经处理过的文档的不必要操作。
PIT
创建一个数据一致性视图,结合 Search After
实现稳定分页。它适合需要数据一致性的深度分页场景。
POST /index/_pit?keep_alive=1m
keep_alive=1m
: 设置 PIT 有效期为 1 分钟。GET /_search
{
"size": 5,
"query": {
"match_all": {}
},
"pit": {
"id": ""
},
"sort": [
{"@timestamp": "desc"},
{"_id": "asc"}
]
}
GET /_search
{
"size": 5,
"query": {
"match_all": {}
},
"pit": {
"id": "" ,
"keep_alive": "1m"
},
"search_after": [1630000000000, "doc_id_123"],
"sort": [
{"@timestamp": "desc"},
{"_id": "asc"}
]
}
仅使用 Search After 依赖于实时数据:
当单独使用 search_after时,每次查询都是基于最新的索引状态进行的。这意味着在进行连续的分页请求时,如果索引发生了变化(例如有新的文档被添加或现有文档被更新/删除),这些变化可能会影响结果的一致性。
潜在的数据不一致性: 由于每个 search_after查询都是独立执行的,并且基于最新快照的数据,所以在深度分页过程中可能会遇到重复文档或者错过某些文档的问题。
使用 Point In Time (PIT) + Search After 创建快照视图:
通过PIT,可以在某个时间点为您的搜索创建一个“快照”,即一个索引状态的视图。这意味着无论在此之后对索引进行了多少次修改,使用这个 PIT 进行的所有查询都会看到相同的索引状态。 提高数据一致性:结合 PIT 和search_after,可以确保在整个分页过程中保持数据的一致性。因为所有的 search_after 请求都引用同一个PIT,所以即使索引在这段时间内发生了变化,也不会影响到当前分页操作的结果集。
更精确的控制: PIT提供了更加精细的时间点控制能力,使得用户能够准确地从特定时间点开始检索数据,这对于需要严格数据一致性的应用场景非常重要。
Search After
的高效分页机制。方法 | 实时性 | 适用场景 | 性能 | 资源开销 |
---|---|---|---|---|
From/Size | 实时 | 浅分页(前1000条) | 差(深分页) | 低 |
Scroll | 非实时 | 全量数据导出 | 高 | 高 |
Search After | 实时 | 深度分页(用户界面) | 高 | 低 |
PIT + Search After | 实时 | 数据一致性深度分页 | 高 | 中 |
Search After
或 PIT + Search After
处理深度分页。index.max_result_window
使用 From/Size
处理深分页。Scroll API
仅用于离线任务(如数据迁移),完成后及时清理资源。