摘要:Elasticsearch分頁(yè)方式有三種,分別是“from + size 淺分頁(yè)”、 “scroll” 和 “search_after”方式,本文詳細(xì)介紹一下這三種分頁(yè)的使用場(chǎng)景,elasticsearch默認(rèn)采用的分頁(yè)方式是from+size的形式,但是在深度分頁(yè)的情況下...
Elasticsearch分頁(yè)方式有三種,分別是“from + size 淺分頁(yè)”、 “scroll” 和 “search_after”方式。
一、from + size 淺分頁(yè)
"淺"分頁(yè)可以理解為簡(jiǎn)單意義上的分頁(yè),它的原理很簡(jiǎn)單,就是查詢前20條數(shù)據(jù),然后截?cái)嗲?0條,只返回10-20的數(shù)據(jù),這樣其實(shí)白白浪費(fèi)了前10條的查詢。
在這里有必要了解一下from/size的原理:
因?yàn)閑s是基于分片的,假設(shè)有5個(gè)分片,from=100,size=10。則會(huì)根據(jù)排序規(guī)則從5個(gè)分片中各取回100條數(shù)據(jù)數(shù)據(jù),然后匯總成500條數(shù)據(jù)后選擇最后面的10條數(shù)據(jù),所以越往后的分頁(yè),執(zhí)行的效率越低??傮w上會(huì)隨著from的增加,消耗時(shí)間也會(huì)增加。而且數(shù)據(jù)量越大,就越明顯!
除了效率上的問(wèn)題,還有一個(gè)無(wú)法解決的問(wèn)題是,es 目前支持最大的 skip 值是 max_result_window ,默認(rèn)為 10000 。也就是當(dāng) from + size > max_result_window 時(shí),es 將返回錯(cuò)誤,max_result_window 調(diào)大方式,治標(biāo)不治本,不建議使用。
二、scroll 深分頁(yè)
為了滿足深度分頁(yè)的場(chǎng)景,es 提供了 scroll 的方式進(jìn)行分頁(yè)讀取。原理上是對(duì)某次查詢生成一個(gè)游標(biāo) scroll_id , 后續(xù)的查詢只需要根據(jù)這個(gè)游標(biāo)去取數(shù)據(jù),直到結(jié)果集中返回的 hits 字段為空,就表示遍歷結(jié)束。scroll_id 的生成可以理解為建立了一個(gè)臨時(shí)的歷史快照,在此之后的增刪改查等操作不會(huì)影響到這個(gè)快照的結(jié)果。
scroll 類似于sql中的cursor,使用scroll,每次只能獲取一頁(yè)的內(nèi)容,然后會(huì)返回一個(gè)scroll_id。根據(jù)返回的這個(gè)scroll_id可以不斷地獲取下一頁(yè)的內(nèi)容,所以scroll并不適用于有跳頁(yè)的情景。
scroll使用過(guò)程:
先獲取第一個(gè) scroll_id,url 參數(shù)包括 /index/_type/ 和 scroll,scroll 字段指定了scroll_id 的有效生存期,以分鐘為單位,過(guò)期之后會(huì)被es自動(dòng)清理。如果文檔不需要特定排序,可以指定按照文檔創(chuàng)建的時(shí)間返回會(huì)使迭代更高效。
$client = ClientBuilder::create()->build(); $params = array( 'index' => 'product-*', '_source' => 'shopname,number,price', "scroll" => "1m", "size" => 10 ); //scroll=1m 表示設(shè)置 scroll_id 保留1分鐘可用。使用scroll必須要將from設(shè)置為0或者不寫。 // 返回結(jié)果 array(5) { ["_scroll_id"] => string(64) "cXVlcnlBbmRGZXRjaDsxOzg3OTA4NDpTQzRmWWkwQ1Q1bUlwMjc0WmdIX2ZnOzA7" ["took"] => int(1) ["timed_out"] => bool(false) ["_shards"] => array(3) { ["total"] => int(1) ["successful"] => int(1) ["failed"] => int(0) } ["hits"] => array(10) { ... } }
然后我們可以通過(guò)數(shù)據(jù)返回的 _scroll_id 讀取下一頁(yè)內(nèi)容,如果srcoll_id 的生存期很長(zhǎng),那么每次返回的 scroll_id 都是一樣的,直到該 scroll_id 過(guò)期,才會(huì)返回一個(gè)新的 scroll_id。請(qǐng)求指定的 scroll_id 時(shí)就不需要 /index/_type 等信息了。每讀取一頁(yè)都會(huì)重新設(shè)置 scroll_id 的生存時(shí)間,所以這個(gè)時(shí)間只需要滿足讀取當(dāng)前頁(yè)就可以,不需要滿足讀取所有的數(shù)據(jù)的時(shí)間,1分鐘足以。
$client = ClientBuilder::create()->build(); $res = $client->scroll(array('scroll_id' => $scroll_id, 'scroll' => '1m'));
一個(gè)完整的 es scroll深度翻頁(yè)P(yáng)HP代碼:
public function lists(){ $page = $_POST['page'] ?? 1; $size = $_POST['size'] ?? 10; $params = array( 'index' => 'yzm_users', 'scroll' => '1m', 'size' => $size ); $params['body'] = array( //查詢條件 ); $docs = $this->client->search($params); $scroll_id = $docs['_scroll_id']; if($page == 1 ){ return_json(array( 'status' => 1, 'data' => $docs['hits']['hits'] )); } $i = 1; while ($i < $page) { $response = $this->client->scroll( array( 'scroll_id' => $scroll_id, 'scroll' => '1m' ) ); if (count($response['hits']['hits']) > 0) { $scroll_id = $response['_scroll_id']; } else { break; } $i++; } return_json(array( 'status' => 1, 'data' => $response['hits']['hits'] )); }
三、search_after 深分頁(yè)
上述的 scroll search 的方式,官方的建議并不是用于實(shí)時(shí)的請(qǐng)求,因?yàn)槊恳粋€(gè) scroll_id 不僅會(huì)占用大量的資源(特別是排序的請(qǐng)求),而且是生成的歷史快照,對(duì)于數(shù)據(jù)的變更不會(huì)反映到快照上。這種方式往往用于非實(shí)時(shí)處理大量數(shù)據(jù)的情況,比如要進(jìn)行數(shù)據(jù)遷移或者索引變更之類的。那么在實(shí)時(shí)情況下如果處理深度分頁(yè)的問(wèn)題呢?es 給出了 search_after 的方式,這是在 >= 5.0 版本才提供的功能。
search_after 分頁(yè)的方式和 scroll 有一些顯著的區(qū)別,首先它是根據(jù)上一頁(yè)的最后一條數(shù)據(jù)來(lái)確定下一頁(yè)的位置,同時(shí)在分頁(yè)請(qǐng)求的過(guò)程中,如果有索引數(shù)據(jù)的增刪改查,這些變更也會(huì)實(shí)時(shí)的反映到游標(biāo)上。
為了找到每一頁(yè)最后一條數(shù)據(jù),每個(gè)文檔必須有一個(gè)全局唯一值,這種分頁(yè)方式其實(shí)和目前 moa 內(nèi)存中使用rbtree 分頁(yè)的原理一樣,官方推薦使用 _uid 作為全局唯一值,其實(shí)使用業(yè)務(wù)層的 id 也可以。
search_after使用過(guò)程:
第一頁(yè)的請(qǐng)求和正常的請(qǐng)求一樣。
$client = ClientBuilder::create()->build(); $params = array( 'index' => 'product-*', '_source' => 'shopname,number,price', 'body' => [ 'sort' => [ 'timestamp' =>['order'=>'asc'], '_id' =>['order'=>'desc'], ] ], "size" => 10 ); //search_after必須使用唯一值進(jìn)行排序才可以 // 返回結(jié)果 array(4) { ["took"] => int(1753) ["timed_out"] => bool(false) ["_shards"] => array(4) { ["total"] => int(3) ["successful"] => int(3) ["skipped"] => int(0) ["failed"] => int(0) } ["hits"] => array(3) { ["total"] => array(2) { ["value"] => int(10000) ["relation"] => string(3) "gte" } ["max_score"] => NULL ["hits"] => array(10) { [0] => array(6) { ["_index"] => string(16) "product" ["_type"] => string(7) "_doc" ["_id"] => string(20) "FHsS6XwBEqA0wm2Y5INV" ["_score"] => NULL ["_source"] => array(7) { .... } ["sort"] => array(2) { [0] => int(1635997891112) [1] => string(20) "FHsS6XwBEqA0wm2Y5INV" } } } } }
第二頁(yè)的請(qǐng)求,使用第一頁(yè)返回結(jié)果的最后一個(gè)數(shù)據(jù)的 sort 值,加上 search_after 字段來(lái)取下一頁(yè)。注意,使用 search_after 的時(shí)候要將 from參數(shù)必須被設(shè)置成 0 或 -1 (當(dāng)然你也可以不設(shè)置這個(gè)from參數(shù))。
$client = ClientBuilder::create()->build(); $params = array( 'index' => 'product-*', '_source' => 'shopname,number,price', 'body' => [ 'search_after' => [ '1635997891112', 'FHsS6XwBEqA0wm2Y5INV' ], 'sort' => [ 'timestamp' =>['order'=>'asc'], '_id' =>['order'=>'desc'], ] ], "size" => 10 ); $res = $client->search($params);
search_after 與 scroll 非常相似,同樣適用于深度分頁(yè) + 排序,因?yàn)槊恳豁?yè)的數(shù)據(jù)依賴于上一頁(yè)最后一條數(shù)據(jù),所以無(wú)法跳頁(yè)請(qǐng)求(但可以通過(guò)循環(huán)來(lái)實(shí)現(xiàn)),且返回的始終是最新的數(shù)據(jù),在分頁(yè)過(guò)程中數(shù)據(jù)的位置可能會(huì)有變更,這種分頁(yè)方式更加符合moa的業(yè)務(wù)場(chǎng)景,常用于數(shù)據(jù)導(dǎo)出等場(chǎng)景。