elasticsearch深度分頁問題
一、深度分頁方式from + size
es 預設採用的分頁方式是 from+ size 的形式,在深度分頁的情況下,這種使用方式效率是非常低的,比如我們執行如下查詢
1 GET /student/student/_search 2 { 3 "query":{ 4 "match_all": {} 5 }, 6 "from":5000, 7 "size":10 8 }
意味著 es 需要在各個分片上匹配排序並得到5010條資料,協調節點拿到這些資料再進行排序等處理,然後結果集中取最後10條資料返回。
我們會發現這樣的深度分頁將會使得效率非常低,因為我只需要查詢10條資料,而es則需要執行from+size條資料然後處理後返回。
其次:es為了效能,限制了我們分頁的深度,es目前支援的最大的 max_result_window = 10000;也就是說我們不能分頁到10000條資料以上。
例如:
from + size <= 10000所以這個分頁深度依然能夠執行。
繼續看上圖,當size + from > 10000;es查詢失敗,並且提示
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
有關請求大資料集的更有效方法,請參閱滾動api。這個限制可以通過改變[索引]來設定。哦呵,原來es給我們提供了另外的一個API scroll。難道這個 scroll 能解決深度分頁問題?
二、深度分頁之scroll
在es中如果我們分頁要請求大資料集或者一次請求要獲取較大的資料集,scroll都是一個非常好的解決方案。
使用scroll滾動搜尋,可以先搜尋一批資料,然後下次再搜尋一批資料,以此類推,直到搜尋出全部的資料來scroll搜尋會在第一次搜尋的時候,儲存一個當時的檢視快照,之後只會基於該舊的檢視快照提供資料搜尋,如果這個期間資料變更,是不會讓使用者看到的。每次傳送scroll請求,我們還需要指定一個scroll引數,指定一個時間視窗,每次搜尋請求只要在這個時間視窗內能完成就可以了。
一個滾屏搜尋允許我們做一個初始階段搜尋並且持續批量從Elasticsearch里拉取結果直到沒有結果剩下。這有點像傳統資料庫裡的cursors(遊標)。
滾屏搜尋會及時製作快照。這個快照不會包含任何在初始階段搜尋請求後對index做的修改。它通過將舊的資料檔案儲存在手邊,所以可以保護index的樣子看起來像搜尋開始時的樣子。這樣將使得我們無法得到使用者最近的更新行為。
scroll的使用很簡單
執行如下curl,每次請求兩條。可以定製 scroll = 5m意味著該視窗過期時間為5分鐘。
1 GET /student/student/_search?scroll=5m 2 { 3 "query": { 4 "match_all": {} 5 }, 6 "size": 2 7 }
1 { 2 "_scroll_id" : "DnF1ZXJ5VGhlbkZldGNoBQAAAAAAAC0YFmllUjV1QTIyU25XMHBTck1XNHpFWUEAAAAAAAAtGRZpZVI1dUEyMlNuVzBwU3JNVzR6RVlBAAAAAAAALRsWaWVSNXVBMjJTblcwcFNyTVc0ekVZQQAAAAAAAC0aFmllUjV1QTIyU25XMHBTck1XNHpFWUEAAAAAAAAtHBZpZVI1dUEyMlNuVzBwU3JNVzR6RVlB", 3 "took" : 0, 4 "timed_out" : false, 5 "_shards" : { 6 "total" : 5, 7 "successful" : 5, 8 "skipped" : 0, 9 "failed" : 0 10 }, 11 "hits" : { 12 "total" : 6, 13 "max_score" : 1.0, 14 "hits" : [ 15 { 16 "_index" : "student", 17 "_type" : "student", 18 "_id" : "5", 19 "_score" : 1.0, 20 "_source" : { 21 "name" : "fucheng", 22 "age" : 23, 23 "class" : "2-3" 24 } 25 }, 26 { 27 "_index" : "student", 28 "_type" : "student", 29 "_id" : "2", 30 "_score" : 1.0, 31 "_source" : { 32 "name" : "xiaoming", 33 "age" : 25, 34 "class" : "2-1" 35 } 36 } 37 ] 38 } 39 }
在返回結果中,有一個很重要的
_scroll_id
在後面的請求中我們都要帶著這個 scroll_id 去請求。
現在student這個索引中共有6條資料,id分別為 1, 2, 3, 4, 5, 6。當我們使用 scroll 查詢第4次的時候,返回結果應該為kong。這時我們就知道已經結果集已經匹配完了。
繼續執行3次結果如下三圖所示。
1 GET /_search/scroll 2 { 3 "scroll":"5m", 4 "scroll_id":"DnF1ZXJ5VGhlbkZldGNoBQAAAAAAAC0YFmllUjV1QTIyU25XMHBTck1XNHpFWUEAAAAAAAAtGRZpZVI1dUEyMlNuVzBwU3JNVzR6RVlBAAAAAAAALRsWaWVSNXVBMjJTblcwcFNyTVc0ekVZQQAAAAAAAC0aFmllUjV1QTIyU25XMHBTck1XNHpFWUEAAAAAAAAtHBZpZVI1dUEyMlNuVzBwU3JNVzR6RVlB" 5 }
由結果集我們可以發現最終確實分別得到了正確的結果集,並且正確的終止了scroll。
三、search_after
from + size的分頁方式雖然是最靈活的分頁方式,但是當分頁深度達到一定程度將會產生深度分頁的問題。scroll能夠解決深度分頁的問題,但是其無法實現實時查詢,即當scroll_id生成後無法查詢到之後資料的變更,因為其底層原理是生成資料的快照。這時 search_after應運而生。其是在es-5.X之後才提供的。
search_after 是一種假分頁方式,根據上一頁的最後一條資料來確定下一頁的位置,同時在分頁請求的過程中,如果有索引資料的增刪改查,這些變更也會實時的反映到遊標上。為了找到每一頁最後一條資料,每個文件必須有一個全域性唯一值,官方推薦使用 _uid 作為全域性唯一值,但是隻要能表示其唯一性就可以。
為了演示,我們需要給上文中的student索引增加一個uid欄位表示其唯一性。
執行如下查詢:
1 GET /student/student/_search 2 { 3 "query":{ 4 "match_all": {} 5 }, 6 "size":2, 7 "sort":[ 8 { 9 "uid": "desc" 10 } 11 ] 12 }
結果集:
View Code下一次分頁,需要將上述分頁結果集的最後一條資料的值帶上。
1 GET /student/student/_search 2 { 3 "query":{ 4 "match_all": {} 5 }, 6 "size":2, 7 "search_after":[1005], 8 "sort":[ 9 { 10 "uid": "desc" 11 } 12 ] 13 }
這樣我們就使用search_after方式實現了分頁查詢。
四、三種分頁方式比較
分頁方式 | 效能 | 優點 | 缺點 | 場景 |
from + size | 低 | 靈活性好,實現簡單 | 深度分頁問題 | 資料量比較小,能容忍深度分頁問題 |
scroll | 中 | 解決了深度分頁問題 |
無法反應資料的實時性(快照版本) 維護成本高,需要維護一個 scroll_id |
海量資料的匯出(比如筆者剛遇到的將es中20w的資料匯入到excel) 需要查詢海量結果集的資料 |
search_after | 高 |
效能最好 不存在深度分頁問題 能夠反映資料的實時變更 |
實現複雜,需要有一個全域性唯一的欄位 連續分頁的實現會比較複雜,因為每一次查詢都需要上次查詢的結果 |
海量資料的分頁 |