Elasticsearch 分頁查詢

狼爺發表於2021-04-05

前言

我們在實際工作中,有很多分頁的需求,商品分頁、訂單分頁等,在MySQL中我們可以使用limit,那麼在Elasticsearch中我們可以使用什麼呢?

ES 分頁搜尋一般有三種方案,from + size、search after、scroll api,這三種方案分別有自己的優缺點,下面將進行分別介紹。

使用的資料是kibana中的kibana_sample_data_flights

from + size

這是ES分頁中最常用的一種方式,與MySQL類似,from指定起始位置,size指定返回的文件數。

GET kibana_sample_data_flights/_search
{
  "from": 10,
  "size": 2, 
  "query": {
    "match": {
      "DestWeather": "Sunny"
    }
  },
  "sort": [
    {
      "timestamp": {
        "order": "asc"
      }
    }
  ]
}

這個例子中查詢航班中,目的地的天氣是晴朗的,並且按時間進行排序。

使用簡單,且預設的深度分頁限制是1萬,from + size 大於 10000會報錯,可以通過index.max_result_window引數進行修改。

{
  "error": {
    "root_cause": [
      {
        "type": "query_phase_execution_exception",
        "reason": "Result window is too large, from + size must be less than or equal to: [10000] but was [10001]. See the scroll api for a more efficient way to request large data sets. This limit can be set by changing the [index.max_result_window] index level setting."
      }
    ],
    "type": "search_phase_execution_exception",
    "reason": "all shards failed",
    "phase": "query",
    "grouped": true,
    "failed_shards": [
      {
        "shard": 0,
        "index": "kibana_sample_data_flights",
        "node": "YRQNOSQqS-GgSo1TSzlC8A",
        "reason": {
          "type": "query_phase_execution_exception",
          "reason": "Result window is too large, from + size must be less than or equal to: [10000] but was [10001]. See the scroll api for a more efficient way to request large data sets. This limit can be set by changing the [index.max_result_window] index level setting."
        }
      }
    ]
  },
  "status": 500
}

這種分頁方式,在分散式的環境下的深度分頁是有效能問題的,一般不建議用這種方式做深度分頁,可以用下面將要介紹的兩種方式。

理解為什麼深度分頁是有問題的,我們可以假設在一個有 5 個主分片的索引中搜尋。 當我們請求結果的第一頁(結果從 1 到 10 ),每一個分片產生前 10 的結果,並且返回給協調節點 ,協調節點對 50 個結果排序得到全部結果的前 10 個。

現在假設我們請求第 1000 頁,結果從 10001 到 10010 。所有都以相同的方式工作除了每個分片不得不產生前10010個結果以外。 然後協調節點對全部 50050 個結果排序最後丟棄掉這些結果中的 50040 個結果。

可以看到,在分散式系統中,對結果排序的成本隨分頁的深度成指數上升。

search after

search after 利用實時有遊標來幫我們解決實時滾動的問題。第一次搜尋時需要指定 sort,並且保證值是唯一的,可以通過加入 _id 保證唯一性。

GET kibana_sample_data_flights/_search
{
  "size": 2, 
  "query": {
    "match": {
      "DestWeather": "Sunny"
    }
  },
  "sort": [
    {
      "timestamp": {
        "order": "asc"
      },
      "_id": {
        "order": "desc"
      }
    }
  ]
}

在返回的結果中,最後一個文件有類似下面的資料,由於我們排序用的是兩個欄位,返回的是兩個值。

"sort" : [
  1614561419000,
  "6FxZJXgBE6QbUWetnarH"
]

第二次搜尋,帶上這個sort的資訊即可,如下

GET kibana_sample_data_flights/_search
{
  "size": 2,
  "query": {
    "match": {
      "DestWeather": "Sunny"
    }
  },
  "sort": [
    {
      "timestamp": {
        "order": "asc"
      },
      "_id": {
        "order": "desc"
      }
    }
  ],
  "search_after": [
    1614561419000,
    "6FxZJXgBE6QbUWetnarH"
  ]
}

scroll api

建立一個快照,有新的資料寫入以後,無法被查到。每次查詢後,輸入上一次的 scroll_id。目前官方已經不推薦使用這個API了,使用search_after即可。

GET kibana_sample_data_flights/_search?scroll=1m
{
  "size": 2,
  "query": {
    "match": {
      "DestWeather": "Sunny"
    }
  },
  "sort": [
    {
      "timestamp": {
        "order": "asc"
      },
      "_id": {
        "order": "desc"
      }
    }
  ]
}

在返回的資料中,有一個_scroll_id欄位,下次搜尋的時候帶上這個資料,並且使用下面的查詢語句。

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

上面的scroll指定搜尋上下文保留的時間,1m代表1分鐘,還有其他時間可以選擇,有d、h、m、s等,分別代表天、時、分鐘、秒。

搜尋上下文有過期自動刪除,但如果自己知道什麼時候該刪,可以自己手動刪除,減少資源佔用。

DELETE /_search/scroll
{
  "scroll_id" : "DXF1ZXJ5QW5kRmV0Y2gBAAAAAAAAA6UWWVJRTk9TUXFTLUdnU28xVFN6bEM4QQ=="
}

總結

from + size 的優點是簡單,缺點是在深度分頁的場景下系統開銷比較大。

search after 可以實時高效的進行分頁查詢,但是它只能做下一頁這樣的查詢場景,不能隨機的指定頁數查詢。

scroll api 方案也很高效,但是它基於快照,不能用在實時性高的業務場景,且官方已不建議使用。

參考資料

相關文章