Elasticsearch:分页搜索结果
Posted Elastic 中国社区官方博客
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Elasticsearch:分页搜索结果相关的知识,希望对你有一定的参考价值。
在我之前的文章:
我讲述了如何针对大量搜索结果进行分页的描述。随着时间点 API(Point in time API)的推出,根据 Elastic 的官方博客 “使用 Elasticsearch 时间点读取器获得随时间推移而保持一致的数据视图”,Scroll 接口将不被推荐作为对搜索结果的分页。
默认情况下,搜索会返回前 10 个匹配的匹配项。 要翻阅更大的结果集,你可以使用搜索 API 的 from 和 size 参数。 from 参数定义要跳过的命中数,默认为 0。 size 参数是要返回的最大命中数。 这两个参数共同定义了一页结果。比如:
GET /twitter/_search
{
"from": 5,
"size": 20,
"query": {
"match": {
"city": "北京"
}
}
}
避免使用 from 和 size 来分页太深或一次请求太多结果。 搜索请求通常跨越多个分片。 每个分片必须将其请求的命中和任何先前页面的命中加载到内存中。 对于深页面或大型结果集,这些操作会显着增加内存和 CPU 使用率,从而导致性能下降或节点故障。这里的原因是 index.max_result_window 的默认值是 10K,也就是说 from+size 的最大值是1万。搜索请求占用堆内存和时间与 from+size 成比例,这限制了内存。假如你想 hit 从 990 到 1000,那么每个 shard 至少需要 1000 个文档:
默认情况下,你不能使用 from 和 size 来翻阅超过 10,000 次点击。 此限制是由 index.max_result_window 索引设置设置的保护措施。 如果你需要翻阅超过 10,000 次点击,请改用 search_after 参数。
警告:Elasticsearch 使用 Lucene 的内部文档 ID 作为 tie_breaker。 这些内部文档 ID 可以在相同数据的副本之间完全不同。 当分页搜索命中时,你可能偶尔会看到具有相同排序值的文档排序不一致。
Search after
你可以使用 search_after 参数使用上一页中的一组 sort values 来检索下一页的命中。
使用 search_after 需要具有相同查询和排序值的多个搜索请求。 如果在这些请求之间发生刷新,结果的顺序可能会发生变化,从而导致跨页面的结果不一致。 为防止出现这种情况,你可以创建一个时间点 (PIT) 以保留搜索中的当前索引状态。
POST /my-index-000001/_pit?keep_alive=1m
上面的命令返回一个 PIT id:
{
"id": "46ToAwMDaWR5BXV1aWQyKwZub2RlXzMAAAAAAAAAACoBYwADaWR4BXV1aWQxAgZub2RlXzEAAAAAAAAAAAEBYQADaWR5BXV1aWQyKgZub2RlXzIAAAAAAAAAAAwBYgACBXV1aWQyAAAFdXVpZDEAAQltYXRjaF9hbGw_gAAAAA=="
}
要获取第一页结果,请提交带有排序参数的搜索请求。 如果使用 PIT,请在 pit.id 参数中指定 PIT id,并从请求路径中省略目标数据流或索引。
重要:所有 PIT 搜索请求都添加了一个名为 _shard_doc 的隐式排序 tiebreaker 字段,该字段也可以显式提供。 如果 你不能使用 PIT,我们建议你在排序中包含一个 tiebreaker 字段。 此 tiebreaker 字段应包含每个文档的唯一值。 如果你不包含 tiebreaker 字段,你的分页结果可能会丢失或重复命中。
注意:当排序顺序为 _shard_doc 且未跟踪总命中数(total hits)时,请求后搜索进行了优化,使它们更快。 如果你想遍历所有文档而不考虑顺序,这是最有效的选择。
重要:如果排序字段在某些目标数据流或索引中是 date,但在其他目标中是 date_nanos 字段,请使用 numeric_type 参数将值转换为单一分辨率,并使用 format 参数为排序字段指定日期格式。 否则,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 来进行搜索。在 sort 里,在 _shard_doc 升序上使用隐式 tiebreaker 对搜索的命中进行排序。
搜索响应包括每个命中的 sort 值数组。 如果你使用了 PIT,则将包含一个 tiebreaker 作为每个命中的最后一个排序值。 这个名为 _shard_doc 的 tiebreaker 会自动添加到使用 PIT 的每个搜索请求中。 _shard_doc 值是 PIT 中的分片索引和 Lucene 的内部文档 ID 的组合,它在每个文档中是唯一的,并且在 PIT 中是常量。 你还可以在搜索请求中显式添加 tiebreaker 以自定义顺序:
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 降序上使用显式 tiebreaker 对搜索进行排序。
{
"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
]
}
]
}
}
从上面的返回结果中,我们可以看出来一个被更新的 pit.id。在 sort 里,它定义了最近返回命中的 sort 值。上面的 4294967298 是一个 tiebreaker 值。在 pit_id 中每个文档都是唯一的。
要获得下一页结果,请使用最后一次命中的排序值(包括 tiebreaker)作为 search_after 参数重新运行先前的搜索。 如果使用 PIT,请在 pit.id 参数中使用最新的 PIT ID。 搜索的查询和排序参数必须保持不变。 如果提供,则 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.id 值
- 在 search_after 里定义的是是上一次请求最后一个命中返回的 sort 值
- 我们在这个请求中把 track_total_hits 设置为 false 来 禁用对总点击数的跟踪以加快分页速度
你可以重复此过程以获取其他页面的结果。 如果使用 PIT,你可以使用每个搜索请求的 keep_alive 参数延长 PIT 的保留期。
完成后,你应该删除 PIT。
DELETE /_pit
{
"id" : "46ToAwMDaWR5BXV1aWQyKwZub2RlXzMAAAAAAAAAACoBYwADaWR4BXV1aWQxAgZub2RlXzEAAAAAAAAAAAEBYQADaWR5BXV1aWQyKgZub2RlXzIAAAAAAAAAAAwBYgACBXV1aWQyAAAFdXVpZDEAAQltYXRjaF9hbGw_gAAAAA=="
}
参考:
【1】https://www.elastic.co/guide/en/elasticsearch/reference/current/paginate-search-results.html
以上是关于Elasticsearch:分页搜索结果的主要内容,如果未能解决你的问题,请参考以下文章
Elasticsearch聚合后将聚合结果进行分页的解决办法
ElasticSearch学习问题记录——Invalid shift value in prefixCoded bytes (is encoded value really an INT?)(代码片段