分页搜索结果
默认情况下,搜索返回前 10 个匹配的命中结果。要浏览更大的结果集,您可以使用 搜索 API 的 from 和 size 参数。from 参数定义要跳过的命中数,默认为 0。size 参数是要返回的最大命中数。这两个参数共同定义了一页结果。
GET /_search
{
"from": 5,
"size": 20,
"query": {
"match": {
"user.id": "kimchy"
}
}
}
避免使用 from 和 size 进行过深的分页或一次请求过多结果。搜索请求通常跨越多个分片。每个分片必须将其请求的命中以及任何先前页面的命中加载到内存中。对于深层分页或大型结果集,这些操作会显著增加内存和 CPU 使用率,导致性能下降或节点故障。
默认情况下,您不能使用 from 和 size 浏览超过 10,000 个命中结果。此限制是由 index.max_result_window 索引设置设置的安全机制。如果您需要浏览超过 10,000 个命中结果,请改用 search_after 参数。
Elasticsearch 使用 Lucene 的内部文档 ID 作为决胜局(tie-breaker)机制。这些内部文档 ID 在同一数据的不同副本之间可能完全不同。在对搜索命中进行分页时,您偶尔可能会发现具有相同排序值的文档排序不一致。
您可以使用 search_after 参数,通过上一页的一组 排序值 来检索下一页的命中结果。
使用 search_after 需要多个具有相同 query 和 sort 值的搜索请求。第一步是运行初始请求。以下示例按两个字段(date 和 tie_breaker_id)对结果进行排序
GET twitter/_search
{
"query": {
"match": {
"title": "elasticsearch"
}
},
"sort": [
{"date": "asc"},
{"tie_breaker_id": "asc"}
]
}
- 启用了
doc_values的_id字段的副本
搜索响应包含每个命中的 sort 值数组
{
"took" : 17,
"timed_out" : false,
"_shards" : ...,
"hits" : {
"total" : ...,
"max_score" : null,
"hits" : [
...
{
"_index" : "twitter",
"_id" : "654322",
"_score" : null,
"_source" : ...,
"sort" : [
1463538855,
"654322"
]
},
{
"_index" : "twitter",
"_id" : "654323",
"_score" : null,
"_source" : ...,
"sort" : [
1463538857,
"654323"
]
}
]
}
}
- 最后返回的命中的排序值。
要检索下一页结果,请重复该请求,获取最后一个命中的 sort 值,并将这些值插入到 search_after 数组中
GET twitter/_search
{
"query": {
"match": {
"title": "elasticsearch"
}
},
"search_after": [1463538857, "654323"],
"sort": [
{"date": "asc"},
{"tie_breaker_id": "asc"}
]
}
每次检索到新一页结果时,通过更新 search_after 数组来重复此过程。如果在这些请求之间发生了 刷新(refresh),您的结果顺序可能会改变,导致跨页面的结果不一致。为防止这种情况,您可以创建一个 时间点 (PIT) 以在整个搜索过程中保持当前的索引状态。
POST /my-index-000001/_pit?keep_alive=1m
API 返回一个 PIT ID。
{
"id": "46ToAwMDaWR5BXV1aWQyKwZub2RlXzMAAAAAAAAAACoBYwADaWR4BXV1aWQxAgZub2RlXzEAAAAAAAAAAAEBYQADaWR5BXV1aWQyKgZub2RlXzIAAAAAAAAAAAwBYgACBXV1aWQyAAAFdXVpZDEAAQltYXRjaF9hbGw_gAAAAA==",
"_shards": ...
}
要获取第一页结果,请提交带有 sort 参数的搜索请求。如果使用 PIT,请在 pit.id 参数中指定 PIT ID,并从请求路径中省略目标数据流或索引。
所有 PIT 搜索请求都会添加一个名为 _shard_doc 的隐式排序决胜局字段,该字段也可以显式提供。如果您无法使用 PIT,我们建议您在 sort 中包含一个决胜局字段。此决胜局字段应包含每个文档的唯一值。如果不包含决胜局字段,您的分页结果可能会遗漏或重复命中结果。
Search after 请求具有优化功能,当排序顺序为 _shard_doc 且不跟踪总命中数时,它们会更快。如果您想遍历所有文档而不管顺序如何,这是最高效的选项。
如果 sort 字段在某些目标数据流或索引中是 date,而在其他目标中是 date_nanos 字段,请使用 numeric_type 参数将值转换为单一分辨率,并使用 format 参数为 sort 字段指定一个 日期格式。否则,Elasticsearch 将无法在每个请求中正确解释 search after 参数。
GET /_search
{
"size": 10000,
"query": {
"match" : {
"user.id" : "elkbee"
}
},
"pit": {
"id": "46ToAwMDaWR5BXV1aWQyKwZub2RlXzMAAAAAAAAAACoBYwADaWR4BXV1aWQxAgZub2RlXzEAAAAAAAAAAAEBYQADaWR5BXV1aWQyKgZub2RlXzIAAAAAAAAAAAwBYgACBXV1aWQyAAAFdXVpZDEAAQltYXRjaF9hbGw_gAAAAA==",
"keep_alive": "1m"
},
"sort": [
{"@timestamp": {"order": "asc", "format": "strict_date_optional_time_nanos", "numeric_type" : "date_nanos" }}
]
}
- 用于搜索的 PIT ID。
- 对搜索命中进行排序,并在
_shard_doc上隐式升序决胜。
搜索响应包含每个命中的 sort 值数组。如果您使用了 PIT,决胜局字段将作为每个命中的最后一个 sort 值包含在内。这个名为 _shard_doc 的决胜局字段会自动添加到使用 PIT 的每个搜索请求中。_shard_doc 值是 PIT 内的分片索引与 Lucene 内部文档 ID 的组合,它对每个文档是唯一的,并且在 PIT 内是恒定的。您还可以在搜索请求中显式添加决胜局字段以自定义顺序
GET /_search
{
"size": 10000,
"query": {
"match" : {
"user.id" : "elkbee"
}
},
"pit": {
"id": "46ToAwMDaWR5BXV1aWQyKwZub2RlXzMAAAAAAAAAACoBYwADaWR4BXV1aWQxAgZub2RlXzEAAAAAAAAAAAEBYQADaWR5BXV1aWQyKgZub2RlXzIAAAAAAAAAAAwBYgACBXV1aWQyAAAFdXVpZDEAAQltYXRjaF9hbGw_gAAAAA==",
"keep_alive": "1m"
},
"sort": [
{"@timestamp": {"order": "asc", "format": "strict_date_optional_time_nanos"}},
{"_shard_doc": "desc"}
]
}
- 用于搜索的 PIT ID。
- 对搜索命中进行排序,并在
_shard_doc上显式降序决胜。
{
"pit_id" : "46ToAwMDaWR5BXV1aWQyKwZub2RlXzMAAAAAAAAAACoBYwADaWR4BXV1aWQxAgZub2RlXzEAAAAAAAAAAAEBYQADaWR5BXV1aWQyKgZub2RlXzIAAAAAAAAAAAwBYgACBXV1aWQyAAAFdXVpZDEAAQltYXRjaF9hbGw_gAAAAA==",
"took" : 17,
"timed_out" : false,
"_shards" : ...,
"hits" : {
"total" : ...,
"max_score" : null,
"hits" : [
...
{
"_index" : "my-index-000001",
"_id" : "FaslK3QBySSL_rrj9zM5",
"_score" : null,
"_source" : ...,
"sort" : [
"2021-05-20T05:30:04.832Z",
4294967298
]
}
]
}
}
- 更新后的时间点
id。 - 最后返回的命中的排序值。
- 决胜局值,在
pit_id内每个文档是唯一的。
要获取下一页结果,请使用上一个命中的排序值(包括决胜局字段)作为 search_after 参数重新运行先前的搜索。如果使用 PIT,请在 pit.id 参数中使用最新的 PIT ID。搜索的 query 和 sort 参数必须保持不变。如果提供了 from 参数,则必须为 0(默认值)或 -1。
GET /_search
{
"size": 10000,
"query": {
"match" : {
"user.id" : "elkbee"
}
},
"pit": {
"id": "46ToAwMDaWR5BXV1aWQyKwZub2RlXzMAAAAAAAAAACoBYwADaWR4BXV1aWQxAgZub2RlXzEAAAAAAAAAAAEBYQADaWR5BXV1aWQyKgZub2RlXzIAAAAAAAAAAAwBYgACBXV1aWQyAAAFdXVpZDEAAQltYXRjaF9hbGw_gAAAAA==",
"keep_alive": "1m"
},
"sort": [
{"@timestamp": {"order": "asc", "format": "strict_date_optional_time_nanos"}}
],
"search_after": [
"2021-05-20T05:30:04.832Z",
4294967298
],
"track_total_hits": false
}
- 先前搜索返回的 PIT ID。
- 来自先前搜索最后一个命中的排序值。
- 禁用总命中数的跟踪以加快分页速度。
您可以重复此过程以获取更多页面的结果。如果使用 PIT,您可以使用每个搜索请求的 keep_alive 参数延长 PIT 的保留期限。
完成后,您应该删除您的 PIT。
DELETE /_pit
{
"id" : "46ToAwMDaWR5BXV1aWQyKwZub2RlXzMAAAAAAAAAACoBYwADaWR4BXV1aWQxAgZub2RlXzEAAAAAAAAAAAEBYQADaWR5BXV1aWQyKgZub2RlXzIAAAAAAAAAAAwBYgACBXV1aWQyAAAFdXVpZDEAAQltYXRjaF9hbGw_gAAAAA=="
}
我们不再建议将 scroll API 用于深度分页。如果您需要在浏览超过 10,000 个命中时保持索引状态,请将 search_after 参数与时间点 (PIT) 结合使用。
虽然 search 请求仅返回单页结果,但 scroll API 可用于从单个搜索请求中检索大量结果(甚至所有结果),这与您在传统数据库上使用游标的方式非常相似。
滚动并非用于实时用户请求,而是用于处理大量数据,例如,将一个数据流或索引的内容重新索引到具有不同配置的新数据流或索引中。
某些官方支持的客户端提供了辅助工具,以协助进行滚动搜索和重新索引
- Perl
- 请参阅 Search<>ElasticsearchClient<>5_0Bulk 和 Search<>ElasticsearchClient<>5_0Scroll
- Python
- 请参阅 elasticsearch.helpers.*
- JavaScript
-
请参阅 client.helpers.*
从滚动请求返回的结果反映了最初发出 search 请求时数据流或索引的状态,就像时间点快照一样。随后对文档的更改(索引、更新或删除)只会影响后面的搜索请求。
为了使用滚动,初始搜索请求应在查询字符串中指定 scroll 参数,该参数告诉 Elasticsearch 应该将搜索上下文保持活动状态多长时间(请参阅 保持搜索上下文处于活动状态),例如 ?scroll=1m。
POST /my-index-000001/_search?scroll=1m
{
"size": 100,
"query": {
"match": {
"message": "foo"
}
}
}
上述请求的结果包含一个 _scroll_id,应将其传递给 scroll API 以检索下一批结果。
POST /_search/scroll
{
"scroll" : "1m",
"scroll_id" : "DXF1ZXJ5QW5kRmV0Y2gBAAAAAAAAAD4WYm9laVYtZndUQlNsdDcwakFMNjU1QQ=="
}
- 可以使用
GET或POST,且 URL 不应包含index名称——这应该在最初的search请求中指定。 scroll参数告诉 Elasticsearch 将搜索上下文再保持开启1m。scroll_id参数
size 参数允许您配置每批结果返回的最大命中数。对 scroll API 的每次调用都会返回下一批结果,直到没有更多结果可返回,即 hits 数组为空。
初始搜索请求和后续的每个滚动请求都会各自返回一个 _scroll_id。虽然 _scroll_id 在请求之间可能会更改,但它并不总是更改——无论如何,都应使用最近收到的 _scroll_id。
如果请求指定了聚合,则只有初始搜索响应才会包含聚合结果。
滚动请求具有优化功能,当排序顺序为 _doc 时,它们会更快。如果您希望遍历所有文档而不管其顺序如何,这是最高效的选项
GET /_search?scroll=1m
{
"sort": [
"_doc"
]
}
滚动操作会返回在初始搜索请求时匹配该搜索的所有文档。它会忽略对这些文档的任何后续更改。scroll_id 标识了一个搜索上下文,该上下文会跟踪 Elasticsearch 返回正确文档所需的一切信息。搜索上下文由初始请求创建,并由后续请求维持活动状态。
scroll 参数(传递给 search 请求和每个 scroll 请求)告诉 Elasticsearch 应该将搜索上下文保持活动状态多长时间。它的值(例如 1m,请参阅 时间单位)不需要长到足以处理所有数据——它只需长到足以处理上一批结果即可。每个 scroll 请求(带有 scroll 参数)都会设置一个新的过期时间。如果 scroll 请求没有传入 scroll 参数,则搜索上下文将在该 scroll 请求执行时被释放。
通常,后台合并进程通过将较小的段合并在一起来创建新的、更大的段,从而优化索引。一旦不再需要较小的段,它们就会被删除。此过程在滚动期间会继续进行,但开放的搜索上下文会阻止旧段被删除,因为它们仍在使用中。
保持旧段处于活动状态意味着需要更多的磁盘空间和文件句柄。请确保您已将节点配置为具有充足的空闲文件句柄。请参阅 文件描述符。
此外,如果某个段包含已删除或已更新的文档,则搜索上下文必须跟踪该段中的每个文档在初始搜索请求时是否处于活动状态。如果您的索引正在进行持续的删除或更新,并且您在上面开启了许多滚动查询,请确保您的节点具有足够的堆空间。
为防止因打开过多滚动查询而引发问题,系统不允许用户打开超出特定限制的滚动查询。默认情况下,打开的滚动查询最大数量为 500。可以使用 search.max_open_scroll_context 集群设置来更新此限制。
您可以使用 nodes stats API 检查当前打开了多少个搜索上下文
GET /_nodes/stats/indices/search
当超出 scroll 超时时间时,搜索上下文会自动被移除。然而,正如上一节中所讨论的,保持滚动查询开启是有代价的,因此一旦不再使用滚动查询,应立即使用 clear-scroll API 显式清除它们
DELETE /_search/scroll
{
"scroll_id" : "DXF1ZXJ5QW5kRmV0Y2gBAAAAAAAAAD4WYm9laVYtZndUQlNsdDcwakFMNjU1QQ=="
}
可以作为数组传递多个 scroll ID
DELETE /_search/scroll
{
"scroll_id" : [
"DXF1ZXJ5QW5kRmV0Y2gBAAAAAAAAAD4WYm9laVYtZndUQlNsdDcwakFMNjU1QQ==",
"DnF1ZXJ5VGhlbkZldGNoBQAAAAAAAAABFmtSWWRRWUJrU2o2ZExpSGJCVmQxYUEAAAAAAAAAAxZrUllkUVlCa1NqNmRMaUhiQlZkMWFBAAAAAAAAAAIWa1JZZFFZQmtTajZkTGlIYkJWZDFhQQAAAAAAAAAFFmtSWWRRWUJrU2o2ZExpSGJCVmQxYUEAAAAAAAAABBZrUllkUVlCa1NqNmRMaUhiQlZkMWFB"
]
}
可以使用 _all 参数清除所有搜索上下文
DELETE /_search/scroll/_all
scroll_id 也可以作为查询字符串参数或在请求体中传递。多个 scroll ID 可以作为逗号分隔的值传递
DELETE /_search/scroll/DXF1ZXJ5QW5kRmV0Y2gBAAAAAAAAAD4WYm9laVYtZndUQlNsdDcwakFMNjU1QQ==,DnF1ZXJ5VGhlbkZldGNoBQAAAAAAAAABFmtSWWRRWUJrU2o2ZExpSGJCVmQxYUEAAAAAAAAAAxZrUllkUVlCa1NqNmRMaUhiQlZkMWFBAAAAAAAAAAIWa1JZZFFZQmtTajZkTGlIYkJWZDFhQQAAAAAAAAAFFmtSWWRRWUJrU2o2ZExpSGJCVmQxYUEAAAAAAAAABBZrUllkUVlCa1NqNmRMaUhiQlZkMWFB
在浏览大量文档时,将搜索拆分为多个切片以独立消费这些文档可能会很有用
GET /my-index-000001/_search?scroll=1m
{
"slice": {
"id": 0,
"max": 2
},
"query": {
"match": {
"message": "foo"
}
}
}
GET /my-index-000001/_search?scroll=1m
{
"slice": {
"id": 1,
"max": 2
},
"query": {
"match": {
"message": "foo"
}
}
}
- 切片的 id
- 最大切片数
第一个请求的结果返回了属于第一个切片(id: 0)的文档,第二个请求的结果返回了属于第二个切片的文档。由于最大切片数设置为 2,因此这两个请求的结果的并集等效于不带切片的 scroll 查询的结果。默认情况下,拆分首先在分片上进行,然后使用 _id 字段在每个分片上本地进行。本地拆分遵循公式 slice(doc) = floorMod(hashCode(doc._id), max))。
每个滚动都是独立的,可以像任何滚动请求一样并行处理。
如果切片数大于分片数,则切片过滤器在初次调用时会非常慢,其复杂度为 O(N),内存成本等于每个切片 N 位(其中 N 是分片中的文档总数)。经过几次调用后,过滤器应该会被缓存,后续调用会更快,但您应限制并行执行的切片查询数量,以避免内存爆炸。
point-in-time API 支持更高效的分区策略,并且不存在此问题。如果可能,建议使用带切片的时间点搜索,而不是 scroll。
避免这种高成本的另一种方法是使用另一个字段的 doc_values 来进行切片。该字段必须具备以下属性
- 该字段是数字类型。
- 该字段已启用
doc_values - 每个文档应包含单个值。如果文档对指定字段有多个值,则使用第一个值。
- 每个文档的值应在创建文档时设置一次且永不更新。这确保了每个切片获得确定性的结果。
- 该字段的基数应该很高。这确保了每个切片获得大致相同的文档数量。
GET /my-index-000001/_search?scroll=1m
{
"slice": {
"field": "@timestamp",
"id": 0,
"max": 10
},
"query": {
"match": {
"message": "foo"
}
}
}
对于仅追加的基于时间的索引,可以安全地使用 timestamp 字段。