引言
我們使用mysql的時候經常遇到分頁查詢的場景,在mysql中使用limit
關鍵字來實現分頁。比如下面的示例。
select * from orders_history where type=8 limit 100,100;
select * from orders_history where type=8 limit 1000,100;
select * from orders_history where type=8 limit 10000,100;
ElasticsSearch(以下簡稱ES)同樣也有很多分頁查詢的場景,比如在數據量比較大的情況下,並且查詢條件比較複雜,在mysql中無法命中索引,我們往往會選擇使用ES的分頁查詢。
ES實現分頁查詢有幾種不同的方案,每種方案都有自己的優缺點,下面就帶你來分別看下。
示例中使用的數據我儘量使用kibana自帶的,這樣方便有些讀者實際測試文中的示例。
分頁的幾種方案
from/size方案
這是ES分頁最常用的一種方案,跟mysql類似,from指定查詢的起始位置,size表示從起始位置開始的文檔數量。看個例子。
GET /kibana_sample_data_ecommerce/_search
{
"from": 0,
"size" : 10,
"query": {
"bool": {
"must": [
{"match": {
"customer_first_name": "Diane"
}}
],
"filter": {
"range": {
"order_date": {
"gte": "2020-01-03"
}
}
}
}
},
"sort": [
{
"order_date": {
"order": "asc"
}
}
]
}
這個例子是查詢客戶名字帶有diane
,並且訂單時間大於2020-01-03
的訂單信息,並且查詢的結果按照時間升序。
使用起來很簡單,不過ES默認的分頁深度是10000,也就是說from+size超過10000就會報錯,我們可以試下,會報下面的錯誤:
{
"error": {
"root_cause": [
{
"type": "illegal_argument_exception",
"reason": "Result window is too large, from + size must be less than or equal to: [10000] but was [10009]. 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,
其實很多時候,業務場景很少遇到這種深度分頁的情況,一般通過頁面查詢,不會有人會翻到這麼深的頁數。
不過,如果我們的業務場景確實需要超過10000條記錄的分頁,有辦法解決嗎?當然有。ES內部是通過index.max_result_window
這個參數控制分頁深度的,我們可以針對特定的索引來修改這個值。
curl -XPUT IP:PORT/index_name/_settings -d '{ "index.max_result_window" :"100000"}'
這裏是把深度分頁的限制改成了10萬。
事實上,ES之所以有這個限制,是因爲在分佈式環境下深度分頁的查詢效率會非常低。比如我們現在查詢第from=990,size=10這樣的條件,這個在業務層就是查詢第990頁,每頁展示10條數據。
但是在ES處理的時候,會分別從每個分片上拿到1000條數據,然後在coordinating
的節點上根據查詢條件聚合出1000條記錄,最後返回其中的10條。所以分頁越深,ES處理的開銷就大,佔用內存就越大。
search after方案
有時候我們會遇到一些業務場景,需要進行很深度的分頁,但是可以不指定頁數翻頁,只要可以實時請求下一頁就行。比如一些實時滾動的場景。
ES爲這種場景提供了一種解決方案:search after。
search after利用實時有遊標來幫我們解決實時滾動的問題,簡單來說前一次查詢的結果會返回一個唯一的字符串,下次查詢帶上這個字符串,進行下一頁
的查詢。看個例子:
GET /kibana_sample_data_ecommerce/_search
{
"size" : 2,
"query": {
"bool": {
"must": [
{"match": {
"customer_first_name": "Diane"
}}
],
"filter": {
"range": {
"order_date": {
"gte": "2020-01-03"
}
}
}
}
},
"sort": [
{
"order_date": "desc",
"_id": "asc"
}
]
}
首先查詢第一頁數據,我這裏指定取回2條,條件跟上一節一樣。唯一的區別在於sort
部分我多加了id,這個是爲了在order_date
字段一樣的情況下告訴ES一個可選的排序方案。因爲search after的遊標是基於排序產生的。
注意看查詢結果的最後,有個類似下面這樣的東東:
"sort" : [
1580597280000,
"RZz1f28BdseAsPClqbyw"
]
在下一頁
的查詢中,我們帶上這個玩意,如下:
GET /kibana_sample_data_ecommerce/_search
{
"size" : 2,
"query": {
"bool": {
"must": [
{"match": {
"customer_first_name": "Diane"
}}
],
"filter": {
"range": {
"order_date": {
"gte": "2020-01-03"
}
}
}
}
},
"search_after":
[
1580597280000,
"RZz1f28BdseAsPClqbyw"
],
"sort": [
{
"order_date": "desc",
"_id": "asc"
}
]
}
就這樣一直操作就可以實現不斷的查看下一頁了。
其實仔細想想這個操作原理並不複雜,以前筆者在mysql的場景下也用過類似的方案。我們來看看上一節討論的那個問題,比如通過一直下一頁,翻到了990頁,當繼續下頁時,因爲有了排序的唯一標識,ES只需從每個分片上拿到滿足條件的10條文檔,然後基於這30條文檔最終聚合成10條結果返回即可。
很顯然,開銷小很多。
scroll api方案
還有一種查詢場景,我們需要一次性或者每次查詢大量的文檔,但是對實時性要求並不高。ES針對這種場景提供了scroll api的方案。這個方案犧牲了實時性,但是查詢效率確實非常高。
先來看個示例:
POST /kibana_sample_data_ecommerce/_search?scroll=1m
{
"size": 10,
"query": {
"match_all" : {
}
}
}
首先我們第一次查詢,會生成一個當前查詢條件結果的快照,後面的每次滾屏(或者叫翻頁)都是基於這個快照的結果,也就是即使有新的數據進來也不會別查詢到。
上面這個查詢結果會返回一個scroll_id
,拷貝過來,組成下一條查詢語句,
POST /_search/scroll
{
"scroll" : "1m",
"scroll_id" : "DXF1ZXJ5QW5kRmV0Y2gBAAAAAAAAA5AWNGpKbFNMZnVSc3loXzQwb0tJZHBtZw=="
}
以此類推,後面每次滾屏都把前一個的scroll_id
複製過來。注意到,後續請求時沒有了index信息,size信息等,這些都在初始請求中,只需要使用scroll_id和scroll兩個參數即可。
很多人對scroll
這個參數容易混淆,誤認爲是查詢的限制時間。這個理解是錯誤的。這個時間其實指的是es把本次快照的結果緩存起來的有效時間。
scroll 參數相當於告訴了 ES我們的search context
要保持多久,後面每個 scroll 請求都會設置一個新的過期時間,以確保我們可以一直進行下一頁操作。
我們繼續討論一個問題,scroll
這種方式爲什麼會比較高效?
ES的檢索分爲查詢(query)和獲取(fetch)兩個階段,query階段比較高效,只是查詢滿足條件的文檔id彙總起來。fetch階段則基於每個分片的結果在coordinating節點上進行全局排序,然後最終計算出結果。
scroll查詢的時候,在query階段把符合條件的文檔id保存在前面提到的search context
裏。 後面每次scroll分批取回只是根據scroll_id定位到遊標的位置,然後抓取size大小的結果集即可。
這裏只簡單說下。後面計劃以源碼的方式詳細分析原理。
總結
-
from/size方案的優點是簡單,缺點是在深度分頁的場景下系統開銷比較大,佔用較多內存。
-
search after基於ES內部排序好的遊標,可以實時高效的進行分頁查詢,但是它只能做
下一頁
這樣的查詢場景,不能隨機的指定頁數查詢。 -
scroll方案也很高效,但是它基於快照,不能用在實時性高的業務場景,建議用在類似報表導出,或者ES內部的reindex等場景。
參考
-
https://www.elastic.co/guide/en/elasticsearch/reference/7.1/search-request-from-size.html
-
https://www.elastic.co/guide/en/elasticsearch/reference/7.1/search-request-scroll.html
-
https://www.elastic.co/guide/cn/elasticsearch/guide/cn/_query_phase.html