elasticsearch查詢之大資料集分頁查詢

無風聽海發表於2022-02-08

一、 要解決的問題

  1. search命中的記錄特別多,使用from+size分頁,直接觸發了elasticsearch的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": "shirts",
        "node": "OBkTpZcTQJ25kmlNZ6xyLg",
        "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
}
  1. 將elasticsearch作為資料庫使用,直接將max_result_window設定一個很大的值,但是資料量大了很影響查詢效能;

二、elasticsearch支援的分頁方式

elasticsearch提供了三種分頁的查詢方式,以支援不同的查詢場景;

from + size

search after

scroll

以下測試使用的是 elasticsearch 6.8

三、 from + size 分頁

from + size是使用最普遍的search分頁方案;

from: 設定要返回第一條記錄的相對位置,預設值為0;

size: 此次search返回的最大記錄數量, 預設值為10;

我們可以直接從第11條記錄開始,返回最多10條記錄;

GET my_store_index/_search
{
  "query": {
    "match": {
      "name": "bj"
    }
  },
  "from": 10,
  "size": 10
}

此種分頁方式特點

  1. 特別適合隨機獲取記錄,實現類似商用搜尋引擎的分頁功能;
  2. 由於from + size是無狀態的,搜尋的時候,每個分片都需要返回from + size條記錄,最終將所有分片的記錄進行合併再返回size條記錄;
  3. 受限實現機制,越靠後的記錄查詢效能越差,故elasticsearch預設設定max_result_window=10000,如果from + size超過這個值就會報錯;

四、search after

即使我們將max_result_window調整成一個更大的值,但是當我們命中的結果比較多的時候,使用from + size的分頁效果就會比較差;

elasticsearch提供的search after可以幫助我們解決這個問題;search after可以利用請求中包含的上一頁的資訊來幫助查詢下一頁的記錄;

起始搜尋如下,需要新增sort欄位,並使用id作為排序欄位;這裡的排序欄位需要確保每個document都是不同的,這樣才能確保排序的唯一性;

GET my_store_index/_search
{
  "_source": false, 
  "query": {
    "match": {
      "name": "bj"
    }
  },
  "size": 10,
  "sort": [
    {
      "id": {
        "order": "desc"
      }
    }
  ]
}

我們可以看到返回結果中包含了sort欄位,裡邊包含了命中記錄對應的sort的值;

{
  "took" : 1,
  "timed_out" : false,
  "_shards" : {
    "total" : 5,
    "successful" : 5,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : 25,
    "max_score" : null,
    "hits" : [
      {
        "_index" : "my_store_index",
        "_type" : "_doc",
        "_id" : "f64cf9f4-db2b-4059-bf97-315fe95f233c",
        "_score" : null,
        "sort" : [
          "f64cf9f4-db2b-4059-bf97-315fe95f233c"
        ]
      }
    ]
  }
}

我們使用上一個請求中的sort的值作為以下請求中search_after的引數,來查詢下一頁的資料;

GET my_store_index/_search
{
  "_source": false, 
  "query": {
    "match": {
      "name": "bj"
    }
  },
  "size": 10,
  "search_after":["f64cf9f4-db2b-4059-bf97-315fe95f233c"],
  "sort": [
    {
      "id": {
        "order": "desc"
      }
    }
  ]
}

此種分頁方式的特點

  1. 需要指定一個值唯一的排序欄位,通過返回記錄的sort值對應的記錄作為from的記錄;
  2. 此種分頁查詢方式也是無狀態的,分頁的時候需要依賴search_after引數作為起始點,這樣就可以直接跳過已經獲取過的記錄;
  3. 可以通過當前時間點最後一條記錄的sort值,同時通過size來控制同時獲取多頁的記錄來實現簡單的向後隨機翻頁;
  4. 需要記錄已經載入的每頁起始的sort值,可以實現前向的隨機翻頁;

我們分別使用from+size、search after查詢第二個10000條記錄,檢視兩者執行時間,可以發現search after快將近2s;

GET my_store_index/_search
{  
  "_source":["id"], 
  "query": {
    "match_phrase_prefix": {
      "deviceData.content": "us"
    }
  },
  "size": 10000,
  "from": 10000
}

{
    "took":8212,
    "timed_out":false,
    "_shards":{
        "total":5,
        "successful":5,
        "skipped":0,
        "failed":0
    },
    "hits":{
        "total":29908,
        "max_score":97.09149
    }
}
GET my_store_index/_search
{
  "_source":["id"], 
  "query": {
    "match_phrase_prefix": {
      "deviceData.content": "us"
    }
  },
  "size": 10000,
  "sort": [
    {
      "id": {
        "order": "desc"
      }
    }
  ],
  "search_after":["aa877c87-bb08-4fbd-8a51-ed4ebaa57251"]
}

{
    "took":6320,
    "timed_out":false,
    "_shards":{
        "total":5,
        "successful":5,
        "skipped":0,
        "failed":0
    },
    "hits":{
        "total":29908,
        "max_score":null
    }
}

五、scroll

elasticsearch提供的scroll可以實現一個請求返回所有命中記錄,我們可以使用類似關聯式資料庫中的遊標的方式來獲取命中的記錄; scroll並不是為了實現實時的搜尋請求,更多的是為了處理大量的資料,尤其適合從某一個index進行重新索引;

為了使用scroll,我們需要在url裡通過scroll指定elasticsearch需要保留搜尋結果的時間;

GET my_store_index/_search?scroll=1m
{
  "_source": false, 
  "query": {
    "match": {
      "name": "bj"
    }
  },
  "size": 2
}

{
  "_scroll_id" : "DnF1ZXJ5VGhlbkZldGNoBQAAAAAAAAb0Fk9Ca1RwWmNUUUoyNWttbE5aNnh5TGcAAAAAAAAG8xZPQmtUcFpjVFFKMjVrbWxOWjZ4eUxnAAAAAAAABvUWT0JrVHBaY1RRSjI1a21sTlo2eHlMZwAAAAAAAAb2Fk9Ca1RwWmNUUUoyNWttbE5aNnh5TGcAAAAAAAAG9xZPQmtUcFpjVFFKMjVrbWxOWjZ4eUxn",
  "took" : 2,
  "timed_out" : false,
  "_shards" : {
    "total" : 5,
    "successful" : 5,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : 25,
    "max_score" : 3.5134304,
    "hits" : [      
    ]
  }
}

我們使用上邊請求返回的_scroll_id來獲取下一頁的資料;

GET _search/scroll
{
  "scroll" : "1m", 
  "scroll_id" : "DnF1ZXJ5VGhlbkZldGNoBQAAAAAAAAdlFk9Ca1RwWmNUUUoyNWttbE5aNnh5TGcAAAAAAAAHZhZPQmtUcFpjVFFKMjVrbWxOWjZ4eUxnAAAAAAAAB2cWT0JrVHBaY1RRSjI1a21sTlo2eHlMZwAAAAAAAAdoFk9Ca1RwWmNUUUoyNWttbE5aNnh5TGcAAAAAAAAHaRZPQmtUcFpjVFFKMjVrbWxOWjZ4eUxn"
}

此種分頁方式的特點

  1. 首次查詢通過query string引數指定scroll的時間,elasticsearch會自動生成一個scroll並返回對應的id;
  2. 此查詢是一種有狀態的查詢,不會實時響應索引變化;
  3. 每次請求返回的記錄數通過首次請求設定的size引數控制;
  4. 返回scroll對應的全部記錄之後再查詢就返回空陣列;
  5. scroll引數控制每次請求之後elasticsearch保留scroll的時間;

相關文章