Elasticsearch——分页查询

默认情况下,搜索返回前10个匹配的命中率。要浏览更大的结果集,可以使用搜索API的from和size参数。from参数定义要跳过的点击数,默认为0。size参数是要返回的最大点击数。这两个参数一起定义了一页结果。

GET /_search
{
  "from": 10,
  "size": 20,
  "query": {
    "match": {
      "user.id": "kimchy"
    }
  }
}

避免使用“from”和“size”一次翻页太深或请求太多结果。搜索请求通常跨越多个碎片。每个碎片都必须将其请求的命中和之前任何页面的命中加载到内存中。对于深度页面或大型结果集,这些操作会显著增加内存和CPU使用率,从而导致性能下降或节点故障。

默认情况下,我们不能使用“from”和“size”页面浏览超过10000次点击。此限制是由index.max_ result_window索引设置设置的保护措施。如果需要翻页浏览10000次以上的点击,请改用search_after参数。

注意:Elasticsearch使用Lucene的内部文档ID作为平局破坏者。这些内部文档ID可以在同一数据的多个副本之间完全不同。当分页搜索命中时,我们可能偶尔会看到具有相同排序值的文档排序不一致。

Search after

我们可以使用search_after参数使用上一页的一组排序值检索下一页的点击。

使用search_after需要具有相同查询和排序值的多个搜索请求。如果在这些请求之间发生刷新,则结果的顺序可能会更改,从而导致跨页面的结果不一致。为了防止出现这种情况,我们可以创建一个时间点(PIT),以便在搜索过程中保留当前索引状态。

POST /my-index-000001/_pit?keep_alive=1m

The API returns a PIT ID.

{
  "id": "46ToAwMDaWR5BXV1aWQyKwZub2RlXzMAAAAAAAAAACoBYwADaWR4BXV1aWQxAgZub2RlXzEAAAAAAAAAAAEBYQADaWR5BXV1aWQyKgZub2RlXzIAAAAAAAAAAAwBYgACBXV1aWQyAAAFdXVpZDEAAQltYXRjaF9hbGw_gAAAAA=="
}

要获取结果的第一页,请提交带有排序参数的搜索请求。如果使用PIT,请在PIT.ID参数中指定PIT ID,并从请求路径中省略目标数据流或索引。

GET /_search
{
  "size": 10000,
  "query": {
    "match" : {
      "user.id" : "elkbee"
    }
  },
  "pit": {
    "id":  "46ToAwMDaWR5BXV1aWQyKwZub2RlXzMAAAAAAAAAACoBYwADaWR4BXV1aWQxAgZub2RlXzEAAAAAAAAAAAEBYQADaWR5BXV1aWQyKgZub2RlXzIAAAAAAAAAAAwBYgACBXV1aWQyAAAFdXVpZDEAAQltYXRjaF9hbGw_gAAAAA==", (1)
    "keep_alive": "1m"
  },
  "sort": [   (2)
    {"@timestamp": {"order": "asc", "format": "strict_date_optional_time_nanos", "numeric_type" : "date_nanos" }}
  ]
}

(1):PIT ID  for the search。

(2):使用_shard_doc上的隐式tiebreak对搜索的点击进行排序。

搜索响应包括每个命中的排序值数组。如果使用了PIT,则每次命中的最后一个排序值将包含一个平分符。这个称为_shard_doc的分块器会在每次使用PIT的搜索请求中自动添加。_shard_doc值是PIT中的分片索引和Lucene的内部文档ID的组合,它在每个文档中是唯一的,在PIT中是常量。我们还可以在搜索请求中明确添加分条规则,以自定义订单:

GET /_search
{
  "size": 10000,
  "query": {
    "match" : {
      "user.id" : "elkbee"
    }
  },
  "pit": {
    "id":  "46ToAwMDaWR5BXV1aWQyKwZub2RlXzMAAAAAAAAAACoBYwADaWR4BXV1aWQxAgZub2RlXzEAAAAAAAAAAAEBYQADaWR5BXV1aWQyKgZub2RlXzIAAAAAAAAAAAwBYgACBXV1aWQyAAAFdXVpZDEAAQltYXRjaF9hbGw_gAAAAA==",  (1)
    "keep_alive": "1m"
  },
  "sort": [ (2)
    {"@timestamp": {"order": "asc", "format": "strict_date_optional_time_nanos"}},
    {"_shard_doc": "desc"}
  ]
}

(1):PIT ID  for the search。

(2):使用_shard_doc上的显式分界点对搜索的点击进行排序。

{
  "pit_id" : "46ToAwMDaWR5BXV1aWQyKwZub2RlXzMAAAAAAAAAACoBYwADaWR4BXV1aWQxAgZub2RlXzEAAAAAAAAAAAEBYQADaWR5BXV1aWQyKgZub2RlXzIAAAAAAAAAAAwBYgACBXV1aWQyAAAFdXVpZDEAAQltYXRjaF9hbGw_gAAAAA==", (1)
  "took" : 17,
  "timed_out" : false,
  "_shards" : ...,
  "hits" : {
    "total" : ...,
    "max_score" : null,
    "hits" : [
      ...
      {
        "_index" : "my-index-000001",
        "_id" : "FaslK3QBySSL_rrj9zM5",
        "_score" : null,
        "_source" : ...,
        "sort" : [               (2)                 
          "2021-05-20T05:30:04.832Z",
          4294967298             (3)              
        ]
      }
    ]
  }
}

(1):Updated id from the point in time。

(2):对上次返回的命中的值进行排序。

(3):tiebreaker值,在pit_id内每个文档都是唯一的。

要获得下一页的结果,请使用最后一次点击的排序值(包括分界符)作为search_after参数重新运行上一次搜索。如果使用PID,请在PIT.ID参数中使用最新的PID.id。搜索的查询和排序参数必须保持不变。如果提供,from参数必须为0(默认值)或-1。

GET /_search
{
  "size": 10000,
  "query": {
    "match" : {
      "user.id" : "elkbee"
    }
  },
  "pit": {
    "id":  "46ToAwMDaWR5BXV1aWQyKwZub2RlXzMAAAAAAAAAACoBYwADaWR4BXV1aWQxAgZub2RlXzEAAAAAAAAAAAEBYQADaWR5BXV1aWQyKgZub2RlXzIAAAAAAAAAAAwBYgACBXV1aWQyAAAFdXVpZDEAAQltYXRjaF9hbGw_gAAAAA==", (1)
    "keep_alive": "1m"
  },
  "sort": [
    {"@timestamp": {"order": "asc", "format": "strict_date_optional_time_nanos"}}
  ],
  "search_after": [            (2)                           
    "2021-05-20T05:30:04.832Z",
    4294967298
  ],
  "track_total_hits": false    (3)                        
}

(1):上次搜索返回的PIT ID。

(2):对上一次搜索中的值进行排序。

(3):禁用对总点击数的跟踪以加快分页。

我们可以重复此过程以获得更多页面的结果。如果使用PIT,可以使用每个搜索请求的keep_alive参数延长PIT的保留期。

当完成后,应该删除PIT。

DELETE /_pit
{
    "id" : "46ToAwMDaWR5BXV1aWQyKwZub2RlXzMAAAAAAAAAACoBYwADaWR4BXV1aWQxAgZub2RlXzEAAAAAAAAAAAEBYQADaWR5BXV1aWQyKgZub2RlXzIAAAAAAAAAAAwBYgACBXV1aWQyAAAFdXVpZDEAAQltYXRjaF9hbGw_gAAAAA=="
}

Scroll search results

官方不再建议使用scroll API进行深度分页。如果在分页超过10000次点击时需要保留索引状态,请使用带有时间点(PIT)的search_after参数。

当搜索请求返回单个“page”的结果时,scroll API可用于从单个搜索请求中检索大量结果(甚至所有结果),其方式与在传统数据库上使用光标的方式大致相同。

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

滚动请求返回的结果反映了在发出初始搜索请求时数据流或索引的状态,如时间快照。文档的后续更改(索引、更新或删除)只会影响以后的搜索请求。

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

POST /my-index-000001/_search?scroll=1m
{
  "size": 100,
  "query": {
    "match": {
      "message": "foo"
    }
  }
}

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

POST /_search/scroll                (1)                                               
{
  "scroll" : "1m",                  (2)                                             
  "scroll_id" : "DXF1ZXJ5QW5kRmV0Y2gBAAAAAAAAAD4WYm9laVYtZndUQlNsdDcwakFMNjU1QQ==" (3)
}

(1):可以使用GET或POST,URL不应包含索引名 — 这是在原始搜索请求中指定的。

(2):scroll参数告诉Elasticsearch将搜索上下文在保持1m。

(3):scroll_id参数

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

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

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

滚动请求具有优化功能,使其在排序顺序为_doc时更快。如果希望迭代所有文档,而不考虑顺序,这是最有效的选项:

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

Keeping the search context alive

滚动条返回初始搜索请求时与搜索匹配的所有文档。它将忽略对这些文档的任何后续更改。scroll_id标识一个搜索上下文,用于跟踪Elasticsearch返回正确文档所需的所有内容。搜索上下文由初始请求创建,并由后续请求保持活动状态。

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

通常,后台合并过程通过将较小的段合并在一起以创建新的较大段来优化索引。一旦不再需要较小的段,它们将被删除。此过程在滚动期间继续,但打开的搜索上下文阻止删除旧段,因为它们仍在使用中。

此外,如果segment 包含已删除或更新的文档,则搜索上下文必须跟踪segment 中的每个文档在初始搜索请求时是否处于活动状态。如果索引上有许多打开的滚动条,并且这些滚动条需要不断删除或更新,请确保节点有足够的堆空间。

为了防止由于打开的滚动条过多而导致的问题,不允许用户打开超过一定限制的滚动条。默认情况下,打开的最大数为500。可以使用search.max_open_scroll_context 集群设置更新此限制。

我们可以使用nodes stats API检查打开了多少搜索上下文:

GET /_nodes/stats/indices/search

Clear scroll 

超过滚动超时时,搜索上下文将自动删除。但是,如前一节所述,保持滚动打开是有成本的,因此一旦不再使用滚动,应使用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

Sliced scroll

在对大量文档进行分页时,将搜索分为多个部分以独立使用它们可能会有所帮助:

GET /my-index-000001/_search?scroll=1m
{
  "slice": {
    "id": 0,      (1)                
    "max": 2      (2)                
  },
  "query": {
    "match": {
      "message": "foo"
    }
  }
}
GET /my-index-000001/_search?scroll=1m
{
  "slice": {
    "id": 1,
    "max": 2
  },
  "query": {
    "match": {
      "message": "foo"
    }
  }
}

(1)The id of the slice

(2)最大切片数

第一个请求返回的结果是属于第一个切片(id:0)的文档,第二个请求返回的结果是属于第二个切片的文档。由于最大切片数设置为2,因此两个请求的结果的并集相当于没有切片的滚动查询的结果。默认情况下,首先在碎片上进行分割,然后使用_id字段在每个碎片上进行本地分割。局部拆分遵循公式slice(doc)=floorMod(hashCode(doc._id),max))。

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

point-in-time(时间点) API支持更高效的分区策略,并且不会出现此问题。如果可能,建议使用带有切片的时间点搜索,而不是滚动(scroll)。

上一篇:【智能车】简述逐飞TC246定时器


下一篇:基于RT1170 使能PIT定时功能 (七)