天天看點

ElasticSearch分頁查詢的3個坑

ES支援的三種分頁查詢方式

  • From + Size 查詢
  • Scroll 周遊查詢
  • Search After 查詢
ElasticSearch分頁查詢的3個坑

Scroll

「說明:」

官方已經不再推薦采用

Scroll API

進行深度分頁。如果遇到超過 10000 的深度分頁,推薦采用

search_after + PIT

官方文檔位址:

https://www.elastic.co/guide/en/elasticsearch/reference/7.14/paginate-search-results.html

分布式系統中的深度分頁問題

「為什麼分布式存儲系統中對深度分頁支援都不怎麼友好呢?」

首先我們看一下分布式存儲系統中分頁查詢的過程。

下面是重點。。。

假設在一個有 4 個主分片的索引中搜尋,每頁傳回10條記錄。

當我們請求結果的第1頁(結果從 1 到 10 ),每一個分片産生前 10 的結果,并且傳回給 協調節點 ,協調節點對 40 個結果排序得到全部結果的前 10 個。

當我們請求第 99 頁(結果從 990 到 1000),需要從每個分片中擷取滿足查詢條件的前1000個結果,傳回給協調節點, 然後協調節點對全部 4000 個結果排序,擷取前10個記錄。

當請求第10000頁,每頁10條記錄,則需要先從每個分片中擷取滿足查詢條件的前100010個結果,傳回給協調節點。然後協調節點需要對全部(100010 * 分片數4)的結果進行排序,然後傳回前10個記錄。

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

這就是 web 搜尋引擎對任何查詢都不要傳回超過 10000 個結果的原因。

ElasticSearch分頁查詢的3個坑

分布式系統中的深度分頁問題

From + Size 查詢

準備資料

PUT user_index
{
  "mappings": {
     "properties": {
        "id":  {"type": "integer"},
        "name":   {"type": "keyword"}
     }
  }
}

POST user_index/_bulk
{ "create":  {  "_id": "1" }}
{ "id":1,"name":"老萬"}
{ "create":  {  "_id": "2" }}
{ "id":2,"name":"老王"}
{ "create":  {  "_id": "3" }}
{ "id":3,"name":"老劉"}
{ "create":  {  "_id": "4" }}
{ "id":4,"name":"小明"}
{ "create":  {  "_id": "5" }}
{ "id":5,"name":"小紅"}
           

複制

查詢示範

「無條件查詢」

POST user_index/_search
           

複制

預設傳回前10個比對的比對項。其中:

  • from:未指定,預設值是 0,注意不是1,代表目前頁傳回資料的起始值。
  • size:未指定,預設值是 10,代表目前頁傳回資料的條數。

「指定from+size查詢」

POST user_index/_search
{
    "from": , 
    "size": ,
    "query": {
        "match_all": {}
    },
    "sort": [
        {"id": "asc"}    
    ]
}
           

複制

max_result_window

es 預設采用的分頁方式是 from+ size 的形式,在深度分頁的情況下,這種使用方式效率是非常低的。

比如 from = 5000,size=10, es 需要在各個分片上比對排序并得到5000*10條有效資料,然後在結果集中取最後 10條資料傳回,這種方式類似于 mongo 的 skip + size。

除了效率上的問題,還有一個無法解決的問題是,es 目前支援最大的 skip 值是 「max_result_window ,預設為 10000」。也就是當 from + size > max_result_window 時,es 将傳回錯誤。

POST user_index/_search
{
    "from": 10000, 
    "size": 10,
    "query": {
        "match_all": {}
    },
    "sort": [
        {"id": "asc"}    
    ]
}
           

複制

這是ElasticSearch最簡單的分頁查詢,但以上指令是會報錯的。

報錯資訊,指window預設是10000。

"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。

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

複制

然後這種方式隻能暫時解決問題,當es 的使用越來越多,資料量越來越大,深度分頁的場景越來越複雜時,如何解決這種問題呢?

「官方建議:」

避免過度使用 from 和 size 來分頁或一次請求太多結果。

不推薦使用 from + size 做深度分頁查詢的核心原因:

  • 搜尋請求通常跨越多個分片,每個分片必須将其請求的命中内容以及任何先前頁面的命中内容加載到記憶體中。
  • 對于翻頁較深的頁面或大量結果,這些操作會顯著增加記憶體和 CPU 使用率,進而導緻性能下降或節點故障。

Search After 查詢

search_after 參數使用上一頁中的一組排序值來檢索下一頁的資料。

使用 search_after 需要具有相同查詢和排序值的多個搜尋請求。如果在這些請求之間發生重新整理,結果的順序可能會發生變化,進而導緻跨頁面的結果不一緻。為防止出現這種情況,您可以建立一個時間點 (PIT) 以保留搜尋中的目前索引狀态。

時間點

Point In Time(PIT)

保障搜尋過程中保留特定事件點的索引狀态。

「注意⚠️:」

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

Point In Time(PIT)

是 Elasticsearch 7.10 版本之後才有的新特性。

「PIT的本質:存儲索引資料狀态的輕量級視圖。」

如下示例能很好的解讀 PIT 視圖的内涵。

#1、給索引user_index建立pit
POST /user_index/_pit?keep_alive=5m

#2、統計目前記錄數 5
POST /user_index/_count

#3、根據pit統計目前記錄數 5
GET /_search
{
  "query": {
        "match_all": {}
    },
  "pit": {
     "id":  "i6-xAwEKdXNlcl9pbmRleBZYTXdtSFRHeVJrZVhCby1OTjlHMS1nABZ0TEpMcVRuNFRxaWI4cXFTVERhOHR3AAAAAAAAIODBFmdBWEd2UmFVVGllZldNdnhPZDJmX0EBFlhNd21IVEd5UmtlWEJvLU5OOUcxLWcAAA==", 
     "keep_alive": "5m"
  },
  "sort": [
        {"id": "asc"}    
    ]
}
#4、插入一條資料
POST user_index/_bulk
{ "create":  {  "_id": "6" }}
{ "id":6,"name":"老李"}

#5、資料總量 6
POST /user_index/_count

#6、根據pit統計資料總量還是 5 ,說明是根據時間點的視圖進行統計。
GET /_search
{
  "query": {
        "match_all": {}
    },
  "pit": {
     "id":  "i6-xAwEKdXNlcl9pbmRleBZYTXdtSFRHeVJrZVhCby1OTjlHMS1nABZ0TEpMcVRuNFRxaWI4cXFTVERhOHR3AAAAAAAAIODBFmdBWEd2UmFVVGllZldNdnhPZDJmX0EBFlhNd21IVEd5UmtlWEJvLU5OOUcxLWcAAA==", 
     "keep_alive": "5m"
  },
  "sort": [
        {"id": "asc"}    
    ]
}
           

複制

有了 PIT,

search_after

的後續查詢都是基于 PIT 視圖進行,能有效保障資料的一緻性。

search_after 分頁查詢可以簡單概括為如下幾個步驟。

擷取索引的pit

POST /user_index/_pit?keep_alive=m
           

複制

根據pit首次查詢

說明:根據pit查詢的時候,不用指定索引名稱。

GET /_search
{
  "query": {
        "match_all": {}
    },
  "pit": {
     "id":  "i6-xAwEKdXNlcl9pbmRleBZYTXdtSFRHeVJrZVhCby1OTjlHMS1nABZ0TEpMcVRuNFRxaWI4cXFTVERhOHR3AAAAAAAAIODBFmdBWEd2UmFVVGllZldNdnhPZDJmX0EBFlhNd21IVEd5UmtlWEJvLU5OOUcxLWcAAA==", 
     "keep_alive": "1m"
  },
  "sort": [
        {"id": "asc"}    
    ]
}
           

複制

查詢結果:傳回的sort值為2.

hits" : [
      {
        "_index" : "user_index",
        "_type" : "_doc",
        "_id" : "",
        "_score" : null,
        "_source" : {
          "id" : 2,
          "name" : "老王"
        },
        "sort" : [
          2
        ]
      }
    ]
           

複制

根據search_after和pit進行翻頁查詢

說明:

search_after

指定為上一次查詢傳回的sort值。

要獲得下一頁結果,請使用最後一次命中的排序值(包括 tiebreaker)作為 search_after 參數重新運作先前的搜尋。如果使用 PIT,請在 pit.id 參數中使用最新的 PIT ID。搜尋的查詢和排序參數必須保持不變。如果提供,則 from 參數必須為 0(預設值)或 -1。

GET /_search
{
  "size": 1, 
  "query": {
        "match_all": {}
    },
  "pit": {
     "id":  "i6-xAwEKdXNlcl9pbmRleBZYTXdtSFRHeVJrZVhCby1OTjlHMS1nABZ0TEpMcVRuNFRxaWI4cXFTVERhOHR3AAAAAAAAIOJ7FmdBWEd2UmFVVGllZldNdnhPZDJmX0EBFlhNd21IVEd5UmtlWEJvLU5OOUcxLWcAAA==", 
     "keep_alive": "5m"
  },
  "sort": [
        {"id": "asc"}    
    ],
  "search_after": [                                
    2
  ]
}
           

複制

search_after 優缺點分析

search_after 查詢僅支援向後翻頁。

不嚴格受制于

max_result_window

,可以無限制往後翻頁。

單次請求值不能超過 max_result_window;但總翻頁結果集可以超過。

面試題思考

  1. 為什麼采用

    search_after

    查詢能解決深度分頁的問題?
  2. search_after + pit 分頁查詢過程中,PIT 視圖過期怎麼辦?
  3. search_after

    查詢,如果需要回到前幾頁怎麼辦?

Scroll 周遊查詢

ES 官方不再推薦使用

Scroll API

進行深度分頁。如果您需要在分頁超過 10000 個點選時保留索引狀态,請使用帶有時間點 (PIT) 的 search_after 參數。

相比于 From + size 和 search_after 傳回一頁資料,Scroll API 可用于從單個搜尋請求中檢索大量結果(甚至所有結果),其方式與傳統資料庫中遊标(cursor)類似。

Scroll API 原理上是對某次查詢生成一個遊标 scroll_id, 後續的查詢隻需要根據這個遊标去取資料,直到結果集中傳回的 hits 字段為空,就表示周遊結束。scroll_id 的生成可以了解為建立了一個臨時的曆史快照,在此之後的增删改查等操作不會影響到這個快照的結果。

所有文檔擷取完畢之後,需要手動清理掉 scroll_id。雖然es 會有自動清理機制,但是 srcoll_id 的存在會耗費大量的資源來儲存一份目前查詢結果集映像,并且會占用檔案描述符。是以用完之後要及時清理。使用 es 提供的 CLEAR_API 來删除指定的 scroll_id

首次查詢,并擷取_scroll_id

POST /user_index/_search?scroll=1m
{
  "size": 1,
  "query": {
        "match_all": {}
    }
}
           

複制

傳回結果:

{
  "_scroll_id" : "FGluY2x1ZGVfY29udGV4dF91dWlkDXF1ZXJ5QW5kRmV0Y2gBFmdBWEd2UmFVVGllZldNdnhPZDJmX0EAAAAAACDlQBZ0TEpMcVRuNFRxaWI4cXFTVERhOHR3",
  "took" : ,
  "timed_out" : false,
  "_shards" : {
    "total" : ,
    "successful" : ,
    "skipped" : ,
    "failed" : 
  },
  "hits" : {
    "total" : {
      "value" : ,
      "relation" : "eq"
    },
    "max_score" : 1.0,
    "hits" : [
      {
        "_index" : "user_index",
        "_type" : "_doc",
        "_id" : "1",
        "_score" : 1.0,
        "_source" : {
          "id" : ,
          "name" : "老萬"
        }
      }
    ]
  }
}
           

複制

根據scroll_id周遊資料

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

複制

删除遊标scroll

DELETE /_search/scroll
{
    "scroll_id" : "FGluY2x1ZGVfY29udGV4dF91dWlkDXF1ZXJ5QW5kRmV0Y2gBFmdBWEd2UmFVVGllZldNdnhPZDJmX0EAAAAAACDlKxZ0TEpMcVRuNFRxaWI4cXFTVERhOHR3"
}
           

複制

優缺點

scroll查詢的相應資料是非實時的,這點和 PIT 視圖比較類似,如果周遊過程中插入新的資料,是查詢不到的。

并且保留上下文需要足夠的堆記憶體空間。

适用場景

全量或資料量很大時周遊結果資料,而非分頁查詢。

「官方文檔強調:」

不再建議使用scroll API進行深度分頁。如果要分頁檢索超過 Top 10000+ 結果時,推薦使用:PIT + search_after。

業務層面優化

很多時候,技術解決不了的問題,可以通過業務層面變通下來解決!

比如,針對分頁場景,我們可以采用如下優化方案。

增加預設的篩選條件

通過盡可能的增加預設的篩選條件,如:時間周期和最低評分,減少滿足條件的資料量,避免出現深度分頁的情況。

采用滾動增量顯示

典型場景比如手機上面浏覽微網誌,可以一直往下滾動加載。

示例:

如下清單展示中,取消了分頁按鈕,通過滾動條增量加載資料。

ElasticSearch分頁查詢的3個坑

滾動分頁

小範圍跳頁

通過對分頁元件的設計,禁止使用者直接跳轉到非常大的頁碼中。比如直接跳轉到最後一頁這種操作。

示例:google搜尋的小範圍跳頁。

ElasticSearch分頁查詢的3個坑

谷歌搜尋小範圍跳頁

總結

分布式存儲引擎的深度分頁目前沒有完美的解決方案。

比如針對百度、google這種全文檢索的查詢,通過From+ size傳回Top 10000 條資料完全能滿足使用需求,末尾查詢評分非常低的結果一般參考意義都不大。

  • From+ size:需要随機跳轉不同分頁(類似主流搜尋引擎)、Top 10000 條資料之内分頁顯示場景。
  • search_after:僅需要向後翻頁的場景及超過Top 10000 資料需要分頁場景。
  • Scroll:需要周遊全量資料場景 。
  • max_result_window:調大治标不治本,不建議調過大。
  • PIT:本質是視圖。
ElasticSearch分頁查詢的3個坑

分布式存儲引擎的深度分頁目前沒有完美的解決方案

ElasticSearch分頁查詢的3個坑

百度搜尋分頁

百度搜尋的分頁最多隻能到 76 頁,不管你搜尋的結果比對了多少内容,隻能翻到第 76 頁,而且也隻能小範圍跳頁。

ElasticSearch分頁查詢的3個坑

搜尋引擎都不能無限的翻頁下去

ElasticSearch分頁查詢的3個坑

es深度分頁問題

ElasticSearch分頁查詢的3個坑

淘寶搜尋隻有100頁

分布式存儲引擎的搜尋,有天然的缺陷存在,沒有完美的方案。當存在技術解決不了的問題,那就從産品層面解決它。