ElasticSearch 深度分頁 (史上最全)


文章很長,而且持續更新,建議收藏起來,慢慢讀!瘋狂創客圈總目錄 博客園版 為您奉上珍貴的學習資源 :

免費贈送 :《尼恩Java面試寶典》 持續更新+ 史上最全 + 面試必備 2000頁+ 面試必備 + 大廠必備 +漲薪必備
免費贈送 經典圖書:《Java高並發核心編程(卷1)加強版》 面試必備 + 大廠必備 +漲薪必備 加尼恩免費領
免費贈送 經典圖書:《Java高並發核心編程(卷2)加強版》 面試必備 + 大廠必備 +漲薪必備 加尼恩免費領
免費贈送 經典圖書:《Java高並發核心編程(卷3)加強版》 面試必備 + 大廠必備 +漲薪必備 加尼恩免費領
免費贈送 經典圖書:尼恩Java面試寶典 最新版 面試必備 + 大廠必備 +漲薪必備 加尼恩免費領
免費贈送 資源寶庫: Java 必備 百度網盤資源大合集 價值>10000元 加尼恩領取


ElasticSearch 深度分頁 (史上最全)

社群小伙伴的呼聲

在這里插入圖片描述

三大ElasticSearch分頁方式

  • 傳統方式(from&size)

    頂部查詢,查詢10000以內的文檔
    場景:需要實時獲取頂部的部分文檔。

    eg: 例如查詢最新的訂單。

  • Scroll 滾動游標 方式

    深度分頁,用於非實時查詢場景
    eg:需要全部文檔,例如導出全部數據

  • Search After
    深度分頁,用於實時查詢場景

    注意es版本,低版本不能使用,具體見后文

說明:本文會以pdf格式持續更新,更多最新尼恩3高pdf筆記,請從下面的鏈接獲取:語雀 或者 碼雲

from&size分頁

POST movies/_search
{
  "from": 10000,
  "size": 10,
  "query": {
    "match_all": {

    }
  }
}

這是ElasticSearch最簡單的分頁查詢 , from + size10000 +10需要查詢的文檔從10000 到 10010

以上命令是會報錯的。

"root_cause": [
      {
        "type": "illegal_argument_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",

報錯信息,指window默認是10000。

from&size為何不能獲取10000個以上的文檔

性能

為了性能,es限制了我們分頁的深度,

es目前支持的最大的 max_result_window = 10000;

也就是說我們不能獲取10000個以上的文檔 , 當ES 分頁查詢超過一定的值(10000)后,會報錯

怎么解決這個問題,首先能想到的就是調大這個window。

PUT movies/_settings
{ 
    "index" : { 
        "max_result_window" : 20000
    }
}

但這種方法只是暫時解決問題,當數據量越來越大,分頁也越來越深,而且越會出OOM問題的。

from&size分頁為何會OOM

協調節點或者客戶端節點,需要講請求發送到所有的分片

每個分片把from + size個結果,返回給協調節點或者客戶端節點‘

協調節點或者客戶端節點進行結果合並,如果有n個分片,則查詢數據是 n * (from+size) , 如果from很大的話,會造成oom或者網絡資源的浪費。

clipboard.png

例子

如請求第20頁,Elasticsearch不得不取出所有分片上的第1頁到第20頁的所有文檔,並做排序,最終再取出from后的size條結果作最終的返回

假設你有16個分片,則需要在coordinate node彙總到 shards* (from+size)條記錄,即需要16*(20+10)記錄后做一次全局排序

所以,當索引非常非常大(千萬或億),是無法使用from + size 做深分頁的,分頁越深則越容易OOM,

即便不OOM,也很消耗CPU和內存資源

結論是:

max_result_window 越大,副本越多,越容易OOM

所以不建議去 修改 max_result_window

可以看到,在分布式系統中,對結果排序的成本隨分頁的深度成指數上升。這就是 web 搜索引擎對任何查詢都不要返回超過 10000 個結果的原因。

說明:本文會以pdf格式持續更新,更多最新尼恩3高pdf筆記,請從下面的鏈接獲取:語雀 或者 碼雲

Scroll 滾動游標分頁(查詢快照)

ES為了避免深分頁,不允許使用分頁(from&size)查詢10000條以后的數據,

因此如果要查詢第10000條以后的數據,要使用ES提供的 scroll(游標) 來查詢

Scroll 滾動游標原理:

對一次查詢生成一個游標 scroll_id , 后續的查詢只需要根據這個游標scroll_id 去取數據,直到結果集中返回的 hits 字段為空,就表示遍歷結束。

scroll_id 的生成可以理解為建立了一個臨時的歷史快照,或者可以理解為一個保存doc快照的臨時的結果文件,快照文件形成之后,原doc的增刪改查等操作不會影響到這個快照的結果。

Scroll 滾動游標可以簡單理解為:

使用scroll就是一次把要用的數據都排完了,緩存起來

在遍歷時,從這個快照里取數據,分批取出,

因此,游標可以增加性能的原因,Scroll 使用from+size還好

是因為如果做深分頁,from+size 每次搜索都必須重新排序,非常浪費資源,而且容易OOM

Scroll 滾動游標使用過程:

使用 Scroll 進行分頁讀取過程如下:

先獲取第一個 scroll_id,url 參數包括 /index/_type/ 和 scroll,

scroll 字段指定了scroll_id 的有效生存期,以分鍾為單位,過期之后es 自動清理 快照數據(臨時文件)。

如果文檔不需要特定排序,可以指定按照文檔創建的時間返回會使迭代更高效。

[root@dnsserver ~]# curl -XGET 200.200.107.232:9200/product/info/_search?pretty&scroll=2m -d 
'{"query":{"match_all":{}}, "size": 10, "sort": ["_doc"]}'

# 返回結果
{
  "_scroll_id": "cXVlcnlBbmRGZXRjaDsxOzg3OTA4NDpTQzRmWWkwQ1Q1bUlwMjc0WmdIX2ZnOzA7",
  "took": 1,
  "timed_out": false,
  "_shards": {
  "total": 1,
  "successful": 1,
  "failed": 0
  },
  "hits":{...}
}

參數scroll=2m:

表示 srcoll_id 的生存期為2分鍾。scroll就是把一次的查詢結果緩存一定的時間,如scroll=2m則把查詢結果在下一次請求上來時暫存2分鍾,response比傳統的返回多了一個scroll_id,下次帶上這個scroll_id即可找回這個緩存的結果。

后續翻頁, 通過上一次查詢返回的scroll_id 來不斷的取下一頁,請求指定的 scroll_id 時就不需要 /index/_type 等信息了。

如果srcoll_id 的生存期很長,那么每次返回的 scroll_id 都是一樣的,直到該 scroll_id 過期,才會返回一個新的 scroll_id。

每讀取一頁都會重新設置 scroll_id 的生存時間,所以這個時間只需要滿足讀取當前頁就可以,不需要滿足讀取所有的數據的時間,1 分鍾足以。

[root@dnsserver ~]# curl -XGET '200.200.107.232:9200/_search/scroll?scroll=1m&scroll_id=cXVlcnlBbmRGZXRjaDsxOzg4NDg2OTpTQzRmWWkwQ1Q1bUlwMjc0WmdIX2ZnOzA7'

#返回結果
{
    "_scroll_id": "cXVlcnlBbmRGZXRjaDsxOzk1ODg3NDpTQzRmWWkwQ1Q1bUlwMjc0WmdIX2ZnOzA7",
    "took": 106,
    "_shards": {
        "total": 1,
        "successful": 1,
        "failed": 0
    },
    "hits": {
        "total": 22424,
        "max_score": 1.0,
        "hits": [{
                "_index": "product",
                "_type": "info",
                "_id": "did-519392_pdid-2010",
                "_score": 1.0,
                "_routing": "519392",
                "_source": {
                    ....
                }
            }
        ]
    }
}

注意:

使用初始化返回的_scroll_id來進行請求,每一次請求都會繼續返回初始化中未讀完數據,並且會返回一個_scroll_id,這個_scroll_id可能會改變,因此每一次請求應該帶上上一次請求返回的_scroll_id

每次發送scroll請求時,都要再重新刷新這個scroll的開啟時間,以防不小心超時導致數據取得不完整

如果沒有數據了,就會回傳空的hits,可以用這個判斷是否遍歷完成了數據

[root@dnsserver ~]# curl -XGET '200.200.107.232:9200/_search/scroll?scroll=1m&scroll_id=cXVlcnlBbmRGZXRjaDsxOzg4NDg2OTpTQzRmWWkwQ1Q1bUlwMjc0WmdIX2ZnOzA7'

#返回結果
{
    "_scroll_id": "cXVlcnlBbmRGZXRjaDsxOzk1ODg3NDpTQzRmWWkwQ1Q1bUlwMjc0WmdIX2ZnOzA7",
    "took": 106,
    "_shards": {
        "total": 1,
        "successful": 1,
        "failed": 0
    },
    "hits": {
        "total": 22424,
        "max_score": 1.0,
        "hits": []

    }
}

scroll_id 的清理

srcoll_id 的存在會耗費大量的資源來保存一份當前查詢結果集映像,並且會占用文件描述符。

為了防止因打開太多scroll而導致的問題,不允許用戶打開滾動超過某個限制。

默認情況下,打開的滾動的最大數量為500.可以使用search.max_open_scroll_context群集設置更新此限制。

獲取有多少個scroll滾動游標

GET /_nodes/stats/indices/search

雖然es 會有自動清理機制,但是,盡量保障所有文檔獲取完畢之后,手動清理掉 scroll_id 。

使用 es 提供的 CLEAR_API 來刪除指定的 scroll_id

## 刪掉指定的多個 srcoll_id 
[root@dnsserver ~]# curl -XDELETE 127.0.0.1:9200/_search/scroll -d 
'{"scroll_id" : ["cXVlcnlBbmRGZXRjaDsxOzg3OTA4NDpTQzRmWWkwQ1Q1bUlwMjc0WmdIX2ZnOzA7"]}'

## 刪除掉所有索引上的 scroll_id 
[root@dnsserver ~]# curl -XDELETE 127.0.0.1:9200/_search/scroll/_all

## 查詢當前所有的scroll 狀態
[root@dnsserver ~]# curl -XGET 127.0.0.1:9200/_nodes/stats/indices/search?pretty
{
  "cluster_name" : "200.200.107.232",
  "nodes" : {
    "SC4fYi0CT5mIp274ZgH_fg" : {
      "timestamp" : 1514346295736,
      "name" : "200.200.107.232",
      "transport_address" : "200.200.107.232:9300",
      "host" : "200.200.107.232",
      "ip" : [ "200.200.107.232:9300", "NONE" ],
      "indices" : {
        "search" : {
          "open_contexts" : 0,
          "query_total" : 975758,
          "query_time_in_millis" : 329850,
          "query_current" : 0,
          "fetch_total" : 217069,
          "fetch_time_in_millis" : 84699,
          "fetch_current" : 0,
          "scroll_total" : 5348,
          "scroll_time_in_millis" : 92712468,
          "scroll_current" : 0
        }
      }
    }
  }
}

說明:本文會以pdf格式持續更新,更多最新尼恩3高pdf筆記,請從下面的鏈接獲取:語雀 或者 碼雲

Search After深度翻頁

上述的 scroll search 的方式,官方的建議不用於實時文檔的查詢:

  • 因為每一個 scroll_id 生成的歷史快照,對於數據的變更不會反映到快照上。

scroll 方式往往用於非實時處理大量數據的情況,比如要進行數據遷移或者索引變更之類的。

那么在實時情況下如果處理深度分頁的問題呢?

es 給出了 search_after 的方式,這是在 >= 5.0 版本才提供的功能。

search_after 分頁

search_after 分頁的方式和 scroll 有一些顯著的區別,

首先它是根據上一頁的最后一條數據來確定下一頁的位置,同時在分頁請求的過程中,如果有索引數據的增刪改查,這些變更也會實時的反映到游標上。

為了找到每一頁最后一條數據,每個文檔必須有一個全局唯一值,官方推薦使用 _id 作為全局唯一值,其實使用業務層的 id 也可以。

search_after 使用

第一頁的請求和正常的請求一樣,

curl -XGET 127.0.0.1:9200/order/info/_search
{
    "size": 10,
    "query": {
        "term" : {
            "did" : 519390
        }
    },
    "sort": [
        {"date": "asc"},
        {"_id": "desc"}
    ]
}

第二頁的請求,使用第一頁返回結果的最后一個數據的值,加上 search_after 字段來取下一頁。

注意,使用 search_after 的時候要將 from 置為 0 或 -1

curl -XGET 127.0.0.1:9200/order/info/_search
{
    "size": 10,
    "query": {
        "term" : {
            "did" : 519390
        }
    },
    "search_after": [1463538857, "tweet#654323"],
    "sort": [
        {"date": "asc"},
        {"_uid": "desc"}
    ]
}


總結:search_after 適用於深度分頁+ 排序,因為每一頁的數據依賴於上一頁最后一條數據,所以無法跳頁請求。

且返回的始終是最新的數據,在分頁過程中數據的位置可能會有變更。

Search After深度翻頁要點

每個文檔具有一個唯一值的字段應該用作排序規范的仲裁器。否則,具有相同排序值的文檔的排序順序將是未定義的。

建議的方法是使用字段_id,它肯定包含每個文檔的一個唯一值。

POST twitter/_search
{
    "size": 10,
    "query": {
        "match" : {
            "title" : "es"
        }
    },
    "sort": [
        {"date": "asc"},
        {"_id": "desc"}
    ]
}

返回的結果

{
      "took" : 29,
      "timed_out" : false,
      "_shards" : {
        "total" : 1,
        "successful" : 1,
        "skipped" : 0,
        "failed" : 0
      },
      "hits" : {
        "total" : {
          "value" : 5,
          "relation" : "eq"
        },
        "max_score" : null,
        "hits" : [
          {
            ...
            },
            "sort" : [
              ...
            ]
          },
          {
            ...
            },
            "sort" : [
              124648691,
              "624812"
            ]
          }
        ]
      }
    }

上面的請求會為每一個文檔返回一個包含sort排序值的數組。

這些sort排序值可以被用於 search_after 參數里以便抓取下一頁的數據。

比如,我們可以使用最后的一個文檔的sort排序值,將它傳遞給 search_after 參數:

GET twitter/_search
{
    "size": 10,
    "query": {
        "match" : {
            "title" : "es"
        }
    },
    "search_after": [124648691, "624812"],
    "sort": [
        {"date": "asc"},
        {"_id": "desc"}
    ]
}

Search After

Search After的要點:

  • 它必須先要指定排序(因為一定要按排序記住坐標)

  • 必須從第一頁開始搜起

  • 從第一頁開始以后每次都帶上search_after=lastEmittedDocFieldValue 從而為無狀態實現一個狀態,


上面的例子中:  
  "search_after": [124648691, "624812"], 
[124648691, "624812"]為上一頁最后一個文檔的 排序字段值`lastEmittedDocFieldValue`,
  這一次查詢帶上`lastEmittedDocFieldValue`

​ 說白了就是把每次固定的from size偏移變成一個確定值lastEmittedDocFieldValue,而查詢則從這個偏移量開始獲取size個doc(每個shard 獲取size個,coordinate node最后匯總 shards*size 個。

Search After與sroll的原理基本相同:

Search After是Elasticsearch 5 新引入的一種分頁查詢機制,其實原理和scroll基本一樣,但是不緩存結果,而是重新進行分片的排序計算

總結

  • 傳統方式(from&size)

    需要實時獲取頂部的部分文檔。例如查詢最新的訂單。

  • Scroll

    用於非實時查詢

    需要全部文檔,例如導出全部數據

  • Search After

    用於實時查詢

    需要做到深度分頁

說明:本文會以pdf格式持續更新,更多最新尼恩3高pdf筆記,請從下面的鏈接獲取:語雀 或者 碼雲

參考資料

https://segmentfault.com/a/1190000020395065
https://www.bbsmax.com/A/D854aOVvdE/
https://blog.csdn.net/wangxuelei036/article/details/106659019/
https://www.bbsmax.com/A/D854aOVvdE/
https://zhuanlan.zhihu.com/p/364971667
https://www.cnblogs.com/jpfss/p/10815172.html#searchafter-的方式


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM