滚动查询(Scroll)

虽然搜索请求返回结果是单个“页面”,但scroll API 可用于从单个搜索请求中检索大量结果(甚至所有结果),其方式与在传统数据库使用相似。

scrolling不是用于实时用户请求,而是用于处理大量数据,例如:为了将一个索引的内容重新索引到具有不同配置的新索引中。

为了使用滚动,初始搜索请求应该在查询字符串中指定scroll参数,它告诉 Elasticsearch 它应该保持“搜索上下文”多长时间(请参阅保持搜索上下文活着),例如?scroll=1m

POST /twitter/_search?scroll=1m
{
    "size": 100,
    "query": {
        "match" : {
            "title" : "elasticsearch"
        }
    }
}

上述请求的结果包括一个 _scroll_id,它应该传递给滚动 API 以检索下一批结果。

POST /_search/scroll 
{
    "scroll" : "1m", 
    "scroll_id" : "DXF1ZXJ5QW5kRmV0Y2gBAAAAAAAAAD4WYm9laVYtZndUQlNsdDcwakFMNjU1QQ==" 
}

可以使用 GET 或 POST请求,并且 URL 不应包含索引名称 — 这是在第一次搜索请求中指定的。

scroll 参数告诉 Elasticsearch 将搜索上下文保持时间为 1m。

size 参数允许您配置每批结果返回的最大命中数。 每次调用 scroll API 都会返回下一批结果,直到没有更多结果要返回为止,即 hits 数组为空。

初始搜索请求和每个后续滚动请求都返回一个 _scroll_id。 虽然 _scroll_id 可能会在请求之间改变,但它并不总是改变 — 在任何情况下,只应使用最近收到的 _scroll_id。

如果请求指定聚合,则只有初始搜索响应将包含聚合结果。

当排序顺序为 _doc 时,滚动请求进行了优化,使它们更快。 如果您想遍历所有文档而不考虑顺序,这是最有效的选项:

GET /_search?scroll=1m
{
  "sort": [
    "_doc"
  ]
}

保持搜索上下文存活

scroll 参数(传递给搜索请求和每个滚动请求)告诉 Elasticsearch 应该保持搜索上下文多长时间。 它的值(例如 1m,参见时间单位)不需要足够长来处理所有数据 — 它只需要足够长来处理前一批结果。 每个滚动请求(带有滚动参数)都会设置一个新的到期时间。 如果滚动请求未传入滚动参数,则搜索上下文将作为该滚动请求的一部分被释放。

通常,后台合并过程通过将较小的段合并在一起以创建新的较大的段来优化索引,此时删除较小的段。 此过程在滚动期间继续,但打开的搜索上下文可防止旧段在仍在使用时被删除。 这就是 Elasticsearch 能够返回初始搜索请求的结果的方式,而不管文档的后续更改如何。

保持旧段处于活动状态意味着需要更多的文件句柄。 确保您已将节点配置为具有充足的空闲文件句柄。 请参阅文件描述符。这个就是操作系统了。

为防止因打开过多“分页”而导致的问题,您可以使用 search.max_open_scroll_context 集群设置(默认为unlimited)限制每个节点的打开分页数。

在上下文过期之后会有search_context_missing_exception的异常

可以通过nodes stats API查询有多少打开的搜索上下文:

GET /_nodes/stats/indices/search

Clear scroll API

当超过滚动超时时长,搜索上下文会自动删除。 然而,保持分页打开是有代价的,如上一节所述,因此一旦不再使用该分页,就应该使用 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 可以作为逗号分隔值传递:

curl -X DELETE "localhost:9200/_search/scroll/DXF1ZXJ5QW5kRmV0Y2gBAAAAAAAAAD4WYm9laVYtZndUQlNsdDcwakFMNjU1QQ==,DnF1ZXJ5VGhlbkZldGNoBQAAAAAAAAABFmtSWWRRWUJrU2o2ZExpSGJCVmQxYUEAAAAAAAAAAxZrUllkUVlCa1NqNmRMaUhiQlZkMWFBAAAAAAAAAAIWa1JZZFFZQmtTajZkTGlIYkJWZDFhQQAAAAAAAAAFFmtSWWRRWUJrU2o2ZExpSGJCVmQxYUEAAAAAAAAABBZrUllkUVlCa1NqNmRMaUhiQlZkMWFB?pretty"

Sliced Scroll 切片滚动

对于返回大量文档的滚动查询,可以将滚动拆分为可以独立使用的多个切片:

GET /twitter/_search?scroll=1m
{
    "slice": {
        "id": 0, 
        "max": 2 
    },
    "query": {
        "match" : {
            "title" : "elasticsearch"
        }
    }
}
GET /twitter/_search?scroll=1m
{
    "slice": {
        "id": 1,
        "max": 2
    },
    "query": {
        "match" : {
            "title" : "elasticsearch"
        }
    }
}

id:代表切片的序号,序号从0开始

max:代表切片的个数

第一个请求的结果返回属于第一个切片(id:0)的文档,第二个请求的结果返回属于第二个切片的文档。 由于切片的最大数量设置为 2,因此两个请求的结果的并集等效于没有切片的滚动查询的结果。 默认情况下,拆分首先在分片上完成,然后使用 _uid 字段在每个分片上本地完成,公式如下: slice(doc) = floorMod(hashCode(doc._uid), max) 例如,如果分片的数量为2 并且用户请求了 4 个切片,然后将切片 0 和 2 分配给第一个分片,将切片 1 和 3 分配给第二个分片。

每个滚动都是独立的,可以像任何滚动请求一样并行处理。

如果切片的数量大于分片的数量,切片过滤器在第一次调用时非常慢,它的复杂度为 O(N),内存成本等于每切片 N 位,其中 N 是文档总数 在碎片中。 几次调用后,过滤器应该被缓存,随后的调用应该更快,但你应该限制并行执行的切片查询的数量以避免内存爆炸。

为了完全避免这种成本,可以使用另一个字段的 doc_values 进行切片,但用户必须确保该字段具有以下属性:

  • 该字段是数字。
  • 在该字段上启用了doc_values
  • 每个文档中都应该包含一个值。 如果文档的指定字段有多个值,则使用第一个值。
  • 每个文档中的该字段值应该在创建文档时设置一次并且永不更新。 这确保了每个切片都能获得确定性的结果。
  • 字段的基数应该很高。 这确保每个切片获得大致相同数量的文档。
GET /twitter/_search?scroll=1m
{
    "slice": {
        "field": "date",
        "id": 0,
        "max": 10
    },
    "query": {
        "match" : {
            "title" : "elasticsearch"
        }
    }
}

对于仅附加基于时间的索引,可以安全地使用时间戳字段。

默认情况下,每次滚动允许的最大切片数限制为 1024。您可以更新index.max_slices_per_scroll索引设置以绕过此限制。

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐