文章目录世界上并没有完美的程序,但是我们并不因此而沮丧,因为写程序就是一个不断追求完美的过程。
-侯氏工坊
分页搜索结果Search after滚动显示搜索结果
保持搜索上下文活跃清除滚动滚动切片 参考
分页搜索结果默认情况下,搜索返回前10个匹配命中。要浏览更大的一组结果,可以使用搜索API的from和size参数。from参数定义要跳过的命中次数,默认值为0。size参数是返回的最大命中数。这两个参数一起定义了一个结果页面。
GET /_search
{
"from": 5,
"size": 20,
"query": {
"match": {
"user.id": "kimchy"
}
}
}
避免使用from和sizes搜索太深的页面或一次请求太多的结果。搜索请求通常跨越多个分片。每个分片必须将其请求的点击量和之前页面的点击量加载到内存中。对于深层页面或大型结果集,这些操作可能会显著增加内存和CPU使用率,导致性能下降或节点故障。默认情况下,不能使用from和size搜索超过10,000次命中条目的页面。这个限制是由索引设置的保护措施index.max_result_window设置。如果需要浏览超过10,000个命中条目,则使用search_after参数。
Search after警告:Elasticsearch使用Lucene的内部文档id作为决定因素。这些内部文档id在相同数据的不同副本之间可能完全不同。当分页搜索命中时,您可能偶尔会看到具有相同排序值的文档排序不一致。
可以使用search_after参数,使用一组来自前一页的排序值检索下一页的命中数。使用search_after需要多个具有相同query和sort值的搜索请求。如果在这些请求之间发生刷新,结果的顺序可能会改变,导致页面之间的结果不一致。为了防止这种情况发生,您可以创建一个时间点(PIT)来保持搜索过程中的当前索引状态。
POST /my-index-000001/_pit?keep_alive=1m
API返回一个PIT ID。
{
"id": "46ToAwMDaWR5BXV1aWQyKwZub2RlXzMAAAAAAAAAACoBYwADaWR4BXV1aWQxAgZub2RlXzEAAAAAAAAAAAEBYQADaWR5BXV1aWQyKgZub2RlXzIAAAAAAAAAAAwBYgACBXV1aWQyAAAFdXVpZDEAAQltYXRjaF9hbGw_gAAAAA=="
}
要获得结果的第一页,请提交一个带有sort参数的搜索请求。如果使用PIT,请通过pit.id参数指定PIT中的PIT ID,并从请求路径中省略目标数据流或索引。
重要:所有PIT搜索请求都添加了一个名为_shard_doc的隐式排序平分字段,该字段也可以显式提供。如果您不能使用PIT,我们建议您在您的sort中包含一个平局决胜字段。这个决胜字段应该为每个文档包含一个唯一的值。如果你没有包含一个平局的字段,你的页面结果可能会错过或重复命中。
注意:当排序顺序为_shard_doc且不跟踪总命中数时,Search after 请求有优化,速度更快。如果您希望遍历所有文档,而不考虑顺序,这是最有效的选择。
注意:如果排序字段在某些目标数据流中是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" }}
]
}
搜索响应包含每个命中的sort值数组。如果使用PIT,则每个命中的最后sort值都包含一个决胜点。这个名为_shard_doc的平分机制会在每个使用PIT的搜索请求中自动添加。_shard_doc值是PIT中的shard索引和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" : "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
]
}
]
}
}
要获得下一页的结果,使用最后一次命中的排序值(包括tiebreaker)作为search_after参数,重新运行之前的搜索。如果使用PIT,请使用该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,可以使用每个搜索请求的keep_alive参数来延长PIT的保留期。当你完成时,你应该删除你的PIT。
DELETE /_pit
{
"id" : "46ToAwMDaWR5BXV1aWQyKwZub2RlXzMAAAAAAAAAACoBYwADaWR4BXV1aWQxAgZub2RlXzEAAAAAAAAAAAEBYQADaWR5BXV1aWQyKgZub2RlXzIAAAAAAAAAAAwBYgACBXV1aWQyAAAFdXVpZDEAAQltYXRjaF9hbGw_gAAAAA=="
}
滚动显示搜索结果
重要:我们不再推荐使用滚动API进行深度分页。如果在进行超过10,000次分页时需要保留索引状态,请使用带有时间点(PIT)的search_after参数。
虽然搜索请求返回一个结果“页面”,但滚动API可以用于从一个搜索请求中检索大量结果(甚至所有结果),这与在传统数据库中使用游标的方式非常相似。滚动不是为了实时的用户请求,而是为了处理大量的数据,例如,为了重新索引一个数据流的内容或索引到一个新的数据流或不同配置的索引。支持滚动和索引的客户端
一些官方支持的客户端提供了帮助滚动搜索和索引的助手:Perl
请参见Search::Elasticsearch::Client::5_0::Bulk和Search::Elasticsearch::Client::5_0::Scroll Python
参考elasticsearch.helpers.* Javascript
参考client.helpers.* 为了使用滚动,初始搜索请求应该在查询字符串中指定滚动参数,该参数告诉Elasticsearch应该保持“搜索上下文”活动多长时间(参见保持搜索上下文活动),例如?scroll=1m。
POST /my-index-000001/_search?scroll=1m
{
"size": 100,
"query": {
"match": {
"message": "foo"
}
}
}
上述请求的结果包含一个_scroll_id,它应该被传递给滚动API,以便检索下一批结果。
POST /_search/scroll
{
"scroll" : "1m",
"scroll_id" : "DXF1ZXJ5QW5kRmV0Y2gBAAAAAAAAAD4WYm9laVYtZndUQlNsdDcwakFMNjU1QQ=="
}
size参数允许您配置每批结果返回的最大命中数。每次调用滚动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集群设置。
你可以用node stats API查看有多少搜索上下文是打开的:
GET /_nodes/stats/indices/search清除滚动
当超过滚动超时时,搜索上下文将自动删除。然而,保持卷轴打开是有代价的,正如前一节所讨论的那样,当卷轴不再被使用时,应该使用clear-scroll API显式地清除卷轴:
DELETE /_search/scroll
{
"scroll_id" : "DXF1ZXJ5QW5kRmV0Y2gBAAAAAAAAAD4WYm9laVYtZndUQlNsdDcwakFMNjU1QQ=="
}
多个滚动id可以作为数组传递:
DELETE /_search/scroll
{
"scroll_id" : [
"DXF1ZXJ5QW5kRmV0Y2gBAAAAAAAAAD4WYm9laVYtZndUQlNsdDcwakFMNjU1QQ==",
"DnF1ZXJ5VGhlbkZldGNoBQAAAAAAAAABFmtSWWRRWUJrU2o2ZExpSGJCVmQxYUEAAAAAAAAAAxZrUllkUVlCa1NqNmRMaUhiQlZkMWFBAAAAAAAAAAIWa1JZZFFZQmtTajZkTGlIYkJWZDFhQQAAAAAAAAAFFmtSWWRRWUJrU2o2ZExpSGJCVmQxYUEAAAAAAAAABBZrUllkUVlCa1NqNmRMaUhiQlZkMWFB"
]
}
所有的搜索上下文都可以用_all参数清除:
DELETE /_search/scroll/_all
scroll_id也可以作为查询字符串参数传递,或者在请求体中传递。多个滚动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: 0),第二个请求返回的结果属于第二个片。由于最大切片数被设置为2,两个请求的结果的并集等于不带切片的滚动查询的结果。默认情况下,分裂首先在shard上完成,然后在每个shard上使用_id字段。本地分割遵循公式slice(doc) = floorMod(hashCode(doc._id), max))。每个滚动都是独立的,可以像任何滚动请求一样并行处理。
注意:如果片的数量大于shard的数量,那么片过滤器在第一次调用时就会非常慢,它的复杂度为O(N),每个片的内存成本为N比特,其中N是shard中文档的总数。在几次调用之后,应该缓存筛选器,随后的调用应该更快,但您应该限制并行执行的切片查询的数量,以避免内存爆炸。
时间点API支持一种更有效的分区策略,并且不会受到这个问题的影响。如果可能的话,建议使用切片的时间点搜索而不是滚动搜索。另一种避免这种高开销的方法是使用另一个字段的doc_values来进行切片。该字段必须具有以下属性:
字段为数字。doc_values在该字段上启用每个文档都应该包含一个值。如果文档的指定字段有多个值,则使用第一个值。每个文档的值应该在创建文档且不更新时设置一次。这确保了每个片都得到确定性的结果。字段的基数应该很高。这确保了每个片获得大致相同数量的文档。
GET /my-index-000001/_search?scroll=1m
{
"slice": {
"field": "@timestamp",
"id": 0,
"max": 10
},
"query": {
"match": {
"message": "foo"
}
}
}
对于只追加基于时间的索引,可以安全地使用timestamp字段。 参考
Paginate search results深入学习访问基础篇和高级篇所有栏目内容参考栏目预告



