分页搜索(实时)
search_after是 ES 提供的一种高效、实时的深度分页解决方案。与传统的 from+ size分页在处理大量数据时性能急剧下降不同,也不同于主要用于离线导出的 scroll API,search_after通过使用上一页最后一个结果的排序值作为“游标”来获取下一页,从而实现高性能的深度翻页。
原理:
首次查询时,必须指定一个或多个唯一或高区分度的字段进行排序(例如 _id)。
在返回的结果中,获取最后一个文档的排序值。
下一次查询时,将这些排序值作为 search_after参数传入,ES 会直接从该“游标”之后开始检索。
优势:
高性能:避免了 from分页时对前 N 条结果的重复排序和遍历,查询时间基本恒定,与页码深度无关。
实时性:查询总是基于最新的索引数据,无需像 scroll那样维持一个数据快照上下文。
资源友好:服务端不长期占用资源,查询完成后即释放。
适用于深分页:是解决成千上万页以后数据检索的标准方法。
另外,下面的案例,分页结果可能不稳定,如果要确保分页过程的一致性和稳定性,search_after必须与 Point-in-Time (PIT) API 结合使用。PIT 会创建一个数据视图的快照,在分页期间保持索引状态不变,防止因数据变更导致翻页结果错乱或丢失,具体可以参阅此文档。
//Search_afterGET /book-index/_search{"knn": {"field": "title_vector","query_vector": [0.1, 0.2, 0.3 ...],"k": 2, // 指定要返回的结果数量"num_candidates": 2 // 限制搜索节点返回的候选结果数量},"sort": [{"id": "asc"},{"price": "asc"}]}GET /book-index/_search{"knn": {"field": "title_vector","query_vector": [0.1, 0.2, 0.3],"k": 2,"num_candidates": 2},"search_after": ["1002","20"], // 输入上一步sort的结果值"sort": [{"id": "asc"},{"price": "desc"}]}
分页搜索(离线)
Scroll API 是 ES 为一次性检索大量数据(例如全量导出、批量离线处理)而设计的深度分页机制。它的核心思想是创建一个数据快照,然后通过一个可滚动的“游标”(scroll_id)分批获取数据,直到遍历完所有结果。
核心特点与适用场景:
非实时:Scroll 在首次查询时创建索引的快照,后续翻页都基于这个快照,期间的数据变更不会影响滚动结果。
顺序遍历:主要用于从头到尾的顺序遍历,不支持跳页。
资源占用:需要在集群中维护搜索上下文直到超时,会消耗内存和计算资源。
典型场景:数据迁移、全量备份、离线分析、需要处理索引中全部或大量符合条件文档的任务。
以下是一个完整的 Scroll 翻页流程示例
步骤 1:初始化 Scroll 查询
//Search ScrollPOST book-index/_search?scroll=1m{"size": 20, // 每页文档数量"knn": {"field": "title_vector","query_vector": [0.1, 0.2, 0.3],"k": 100, // 需要覆盖您想滚动获取的总文档数"num_candidates": 500}}
scroll=5m:设置搜索上下文的存活时间为 5 分钟。这个时间需要足够您处理完一批数据并获取下一批。
size:指定每次滚动返回的文档数量(每批的大小)。
响应:除了返回第一批 hits外,还会在响应体中包含一个 _scroll_id。这是后续翻页的关键。
步骤 2:使用 scroll_id获取后续批次
使用上一步返回的 _scroll_id来获取下一批结果。
//输入上一步获取到的 scroll idPOST /_search/scroll{"scroll" : "1m","scroll_id" : "XXXXXXXXXX" // scroll_id换成上一步返回的值}
每次调用都会返回下一批 size个文档,并返回一个新的 _scroll_id(即使相同,也应使用最新的)。
重复此步骤,直到返回的 hits数组为空。
步骤 3:清理 Scroll 上下文(非常重要)
处理完成后,必须主动释放资源。
DELETE /_search/scroll{"scroll_id": "XXXXXXXXXX" // scroll_id换成上一步返回的值}