干貨 | 全方位深度解讀 Elasticsearch 分頁(yè)查詢
1、關(guān)于 Elasticsearch 分頁(yè)查詢,這幾個(gè)問(wèn)題經(jīng)常被問(wèn)到
問(wèn)題1:想請(qǐng)問(wèn)下,一次性獲取索引上的某個(gè)字段的所有值(100 萬(wàn)左右),除了把 max_result_window 調(diào)大 ,還有沒(méi)有啥方法?
問(wèn)題2:關(guān)于 es 的分頁(yè),每次拿 20 條展示在前臺(tái),然后點(diǎn)擊下一頁(yè),在查詢后面的20條數(shù)據(jù),應(yīng)該要怎么寫?
問(wèn)題3:From+size、Scroll、search_after 的本質(zhì)區(qū)別和應(yīng)用場(chǎng)景分別是什么?
2、 Elasticsearch 支持的三種分頁(yè)查詢方式
From + Size 查詢 Search After 查詢 Scroll 查詢
下面我就三種方式的聯(lián)系與區(qū)別、優(yōu)缺點(diǎn)、適用場(chǎng)景等展開(kāi)進(jìn)行解讀。
2.1 From + size 分頁(yè)查詢
2.1.1 From + size 分頁(yè)查詢定義與實(shí)戰(zhàn)案例
如下基礎(chǔ)查詢:
GET kibana_sample_data_flights/_search
默認(rèn)返回前10個(gè)匹配的匹配項(xiàng)。其中:
from:未指定,默認(rèn)值是 0,注意不是1,代表當(dāng)前頁(yè)返回?cái)?shù)據(jù)的起始值。 size:未指定,默認(rèn)值是 10,代表當(dāng)前頁(yè)返回?cái)?shù)據(jù)的條數(shù)。
如下指定條件查詢和排序:
GET kibana_sample_data_flights/_search
{
"from": 0,
"size":20,
"query": {
"match": {
"DestWeather": "Sunny"
}
},
"sort": [
{
"FlightTimeHour": {
"order": "desc"
}
}
]
}
共返回 20 條數(shù)據(jù)。
其中:from + size 兩個(gè)參數(shù)定義了結(jié)果頁(yè)面顯示數(shù)據(jù)的內(nèi)容。
2.1.2 From + size 查詢優(yōu)缺點(diǎn)及適用場(chǎng)景
From + size 查詢優(yōu)點(diǎn)
支持隨機(jī)翻頁(yè)。
From + size 查詢?nèi)秉c(diǎn)
受制于 max_result_window 設(shè)置,不能無(wú)限制翻頁(yè)。
存在深度翻頁(yè)問(wèn)題,越往后翻頁(yè)越慢。
From + size 查詢適用場(chǎng)景
第一:非常適合小型數(shù)據(jù)集或者大數(shù)據(jù)集返回 Top N(N <= 10000)結(jié)果集的業(yè)務(wù)場(chǎng)景。
第二:類似主流 PC 搜索引擎(谷歌、bing、百度、360、sogou等)支持隨機(jī)跳轉(zhuǎn)分頁(yè)的業(yè)務(wù)場(chǎng)景。

2.1.3 深度翻頁(yè)不推薦使用 From + size
Elasticsearch 會(huì)限制最大分頁(yè)數(shù),避免大數(shù)據(jù)量的召回導(dǎo)致性能低下。
Elasticsearch 的 max_result_window 默認(rèn)值是:10000。也就意味著:如果每頁(yè)有 10 條數(shù)據(jù),會(huì)最大翻頁(yè)至 1000 頁(yè)。
實(shí)際主流搜索引擎都翻不了那么多頁(yè),舉例:百度搜索“上?!?,翻到第 76 頁(yè),就無(wú)法再往下翻頁(yè)了,提示信息如下截圖所示:

如下的分頁(yè)查詢
GET kibana_sample_data_flights/_search
{
"from": 0,
"size":10001
}
GET kibana_sample_data_flights/_search
{
"from": 10001,
"size":10
}
報(bào)錯(cuò)如下:
{
"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 [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."
}
],
什么原因?超過(guò)了最大窗口的限制,index.max_result_window 默認(rèn)值為10000。
報(bào)錯(cuò)信息還同時(shí)給出了兩個(gè)解決方案:
方案一:大數(shù)據(jù)集召回?cái)?shù)據(jù)使用:scroll api。
后面會(huì)詳細(xì)講解。
方案二:調(diào)大 index.max_result_window 默認(rèn)值。
PUT kibana_sample_data_flights/_settings
{
"index.max_result_window":50000
}
官方建議:避免過(guò)度使用 from 和 size 來(lái)分頁(yè)或一次請(qǐng)求太多結(jié)果。
不推薦使用 from + size 做深度分頁(yè)查詢的核心原因:
搜索請(qǐng)求通??缭蕉鄠€(gè)分片,每個(gè)分片必須將其請(qǐng)求的命中內(nèi)容以及任何先前頁(yè)面的命中內(nèi)容加載到內(nèi)存中。
對(duì)于翻頁(yè)較深的頁(yè)面或大量結(jié)果,這些操作會(huì)顯著增加內(nèi)存和 CPU 使用率,從而導(dǎo)致性能下降或節(jié)點(diǎn)故障。
什么意思呢?
GET kibana_sample_data_flights/_search
{
"from": 10001,
"size": 10
}
共 10 條數(shù)據(jù)加載到內(nèi)存嗎?不是的!
共:10011 條數(shù)據(jù)加載到內(nèi)存,然后經(jīng)過(guò)后臺(tái)處理后返回了最后 10 條我們想要的數(shù)據(jù)。
那也就意味著,越往后翻頁(yè)(也就是深度翻頁(yè))需要加載的數(shù)據(jù)量越大,勢(shì)必會(huì)越耗費(fèi) CPU + 內(nèi)存資源,響應(yīng)也會(huì)越慢!
2.2 search_after 查詢
2.2.1 search_after 查詢定義與實(shí)戰(zhàn)案例
search_after 查詢本質(zhì):使用前一頁(yè)中的一組排序值來(lái)檢索匹配的下一頁(yè)。
前置條件:使用 search_after 要求后續(xù)的多個(gè)請(qǐng)求返回與第一次查詢相同的排序結(jié)果序列。也就是說(shuō),即便在后續(xù)翻頁(yè)的過(guò)程中,可能會(huì)有新數(shù)據(jù)寫入等操作,但這些操作不會(huì)對(duì)原有結(jié)果集構(gòu)成影響。
如何實(shí)現(xiàn)呢?
可以創(chuàng)建一個(gè)時(shí)間點(diǎn) Point In Time(PIT)保障搜索過(guò)程中保留特定事件點(diǎn)的索引狀態(tài)。
Point In Time(PIT)是 Elasticsearch 7.10 版本之后才有的新特性。
PIT的本質(zhì):存儲(chǔ)索引數(shù)據(jù)狀態(tài)的輕量級(jí)視圖。
如下示例能很好的解讀 PIT 視圖的內(nèi)涵。
# 創(chuàng)建 PIT
POST kibana_sample_data_logs/_pit?keep_alive=1m
# 獲取數(shù)據(jù)量 14074
POST kibana_sample_data_logs/_count
# 新增一條數(shù)據(jù)
POST kibana_sample_data_logs/_doc/14075
{
"test":"just testing"
}
# 數(shù)據(jù)總量為 14075
POST kibana_sample_data_logs/_count
# 查詢PIT,數(shù)據(jù)依然是14074,說(shuō)明走的是之前時(shí)間點(diǎn)的視圖的統(tǒng)計(jì)。
POST /_search
{
"track_total_hits": true,
"query": {
"match_all": {}
},
"pit": {
"id": "48myAwEXa2liYW5hX3NhbXBsZV9kYXRhX2xvZ3MWM2hGWXpxLXFSSGlfSmZIaXJWN0dxUQAWdG1TOWFMTF9UdTZHdVZDYmhoWUljZwAAAAAAAAEN3RZGOFJCMGVrZVNndTk3U1I0SG81V3R3AAEWM2hGWXpxLXFSSGlfSmZIaXJWN0dxUQAA"
}
}
有了 PIT,search_after 的后續(xù)查詢都是基于 PIT 視圖進(jìn)行,能有效保障數(shù)據(jù)的一致性。
search_after 分頁(yè)查詢可以簡(jiǎn)單概括為如下幾個(gè)步驟。
步驟 1:創(chuàng)建 PIT 視圖,這是前置條件不能省。
# Step 1: 創(chuàng)建 PIT
POST kibana_sample_data_logs/_pit?keep_alive=5m
返回結(jié)果如下:
{
"id" : "48myAwEXa2liYW5hX3NhbXBsZV9kYXRhX2xvZ3MWM2hGWXpxLXFSSGlfSmZIaXJWN0dxUQAWdG1TOWFMTF9UdTZHdVZDYmhoWUljZwAAAAAAAAEg5RZGOFJCMGVrZVNndTk3U1I0SG81V3R3AAEWM2hGWXpxLXFSSGlfSmZIaXJWN0dxUQAA"
}
keep_alive=5m,類似scroll的參數(shù),代表視圖保留時(shí)間是 5 分鐘,超過(guò) 5 分鐘執(zhí)行會(huì)報(bào)錯(cuò)如下:
"type" : "search_context_missing_exception",
"reason" : "No search context found for id [91600]"
步驟 2:創(chuàng)建基礎(chǔ)查詢語(yǔ)句,這里要設(shè)置翻頁(yè)的條件。
# Step 2: 創(chuàng)建基礎(chǔ)查詢
GET /_search
{
"size":10,
"query": {
"match" : {
"host" : "elastic"
}
},
"pit": {
"id": "48myAwEXa2liYW5hX3NhbXBsZV9kYXRhX2xvZ3MWM2hGWXpxLXFSSGlfSmZIaXJWN0dxUQAWdG1TOWFMTF9UdTZHdVZDYmhoWUljZwAAAAAAAAEg5RZGOFJCMGVrZVNndTk3U1I0SG81V3R3AAEWM2hGWXpxLXFSSGlfSmZIaXJWN0dxUQAA",
"keep_alive": "1m"
},
"sort": [
{"response.keyword": "asc"}
]
}
設(shè)置了PIT,檢索時(shí)候就不需要再指定索引。
id 是基于步驟1 返回的 id 值。
排序 sort 指的是:按照哪個(gè)關(guān)鍵字排序。
在每個(gè)返回文檔的最后,會(huì)有兩個(gè)結(jié)果值,如下所示:
"sort" : [
"200",
4
]
其中,“200”就是我們指定的排序方式:基于 {"response.keyword": "asc"} 升序排列。
而 4 代表什么含義呢?
4 代表——隱含的排序值,是基于_shard_doc 的升序排序方式。
官方文檔把這種隱含的字段叫做:tiebreaker (決勝字段),tiebreaker 等價(jià)于_shard_doc。
tiebreaker 本質(zhì)含義:每個(gè)文檔的唯一值,確保分頁(yè)不會(huì)丟失或者分頁(yè)結(jié)果數(shù)據(jù)出現(xiàn)重復(fù)(相同頁(yè)重復(fù)或跨頁(yè)重復(fù))。
步驟3:實(shí)現(xiàn)后續(xù)翻頁(yè)。
# step 3 : 開(kāi)始翻頁(yè)
GET /_search
{
"size": 10,
"query": {
"match" : {
"host" : "elastic"
}
},
"pit": {
"id": "48myAwEXa2liYW5hX3NhbXBsZV9kYXRhX2xvZ3MWM2hGWXpxLXFSSGlfSmZIaXJWN0dxUQAWdG1TOWFMTF9UdTZHdVZDYmhoWUljZwAAAAAAAAEg5RZGOFJCMGVrZVNndTk3U1I0SG81V3R3AAEWM2hGWXpxLXFSSGlfSmZIaXJWN0dxUQAA",
"keep_alive": "1m"
},
"sort": [
{"response.keyword": "asc"}
],
"search_after": [
"200",
4
]
}
后續(xù)翻頁(yè)都需要借助 search_after 指定前一頁(yè)的最后一個(gè)文檔的 sort 字段值。
如下代碼所示:
"search_after": [
"200",
4
]
顯然,search_after 查詢僅支持向后翻頁(yè)。
2.2.2 search_after 查詢優(yōu)缺點(diǎn)及適用場(chǎng)景
search_after 優(yōu)點(diǎn)
不嚴(yán)格受制于 max_result_window,可以無(wú)限制往后翻頁(yè)。
ps:不嚴(yán)格含義:?jiǎn)未握?qǐng)求值不能超過(guò) max_result_window;但總翻頁(yè)結(jié)果集可以超過(guò)。
search_after 缺點(diǎn)
只支持向后翻頁(yè),不支持隨機(jī)翻頁(yè)。
search_after 適用場(chǎng)景
類似:今日頭條分頁(yè)搜索 https://m.toutiao.com/search
不支持隨機(jī)翻頁(yè),更適合手機(jī)端應(yīng)用的場(chǎng)景。

2.3 Scroll 遍歷查詢
2.3.1 Scroll 遍歷查詢定義與實(shí)戰(zhàn)案例
相比于 From + size 和 search_after 返回一頁(yè)數(shù)據(jù),Scroll API 可用于從單個(gè)搜索請(qǐng)求中檢索大量結(jié)果(甚至所有結(jié)果),其方式與傳統(tǒng)數(shù)據(jù)庫(kù)中游標(biāo)(cursor)類似。
如果把 From + size 和 search_after 兩種請(qǐng)求看做近實(shí)時(shí)的請(qǐng)求處理方式,那么 scroll 滾動(dòng)遍歷查詢顯然是非實(shí)時(shí)的。數(shù)據(jù)量大的時(shí)候,響應(yīng)時(shí)間可能會(huì)比較長(zhǎng)。
scroll 核心執(zhí)行步驟如下:
步驟 1:指定檢索語(yǔ)句同時(shí)設(shè)置 scroll 上下文保留時(shí)間。
實(shí)際上,scroll 已默認(rèn)包含了 search_after 的PIT 的視圖或快照功能。
從 Scroll 請(qǐng)求返回的結(jié)果反映了發(fā)出初始搜索請(qǐng)求時(shí)索引的狀態(tài),類似在那一個(gè)時(shí)刻做了快照。隨后對(duì)文檔的更改(寫入、更新或刪除)只會(huì)影響以后的搜索請(qǐng)求。
POST kibana_sample_data_logs/_search?scroll=3m
{
"size": 100,
"query": {
"match": {
"host": "elastic"
}
}
}
步驟 2:向后翻頁(yè)繼續(xù)獲取數(shù)據(jù),直到?jīng)]有要返回的結(jié)果為止。
POST _search/scroll
{
"scroll" : "3m",
"scroll_id":"FGluY2x1ZGVfY29udGV4dF91dWlkDXF1ZXJ5QW5kRmV0Y2gBFkY4UkIwZWtlU2d1OTdTUjRIbzVXdHcAAAAAAAGmkBZ0bVM5YUxMX1R1Nkd1VkNiaGhZSWNn"
}
scroll_id 值是步驟 1 返回的結(jié)果值。
2.3.2 Scroll 遍歷查詢優(yōu)缺點(diǎn)及適用場(chǎng)景
scroll 查詢優(yōu)點(diǎn)
支持全量遍歷。
ps:?jiǎn)未伪闅v的 size 值也不能超過(guò) max_result_window 大小。
scroll 查詢?nèi)秉c(diǎn)
響應(yīng)時(shí)間非實(shí)時(shí)。
保留上下文需要足夠的堆內(nèi)存空間。
scroll 查詢適用場(chǎng)景
全量或數(shù)據(jù)量很大時(shí)遍歷結(jié)果數(shù)據(jù),而非分頁(yè)查詢。
官方文檔強(qiáng)調(diào):不再建議使用scroll API進(jìn)行深度分頁(yè)。如果要分頁(yè)檢索超過(guò) Top 10,000+ 結(jié)果時(shí),推薦使用:PIT + search_after。
3、小結(jié)
From+ size:需要隨機(jī)跳轉(zhuǎn)不同分頁(yè)(類似主流搜索引擎)、Top 10000 條數(shù)據(jù)之內(nèi)分頁(yè)顯示場(chǎng)景。
search_after:僅需要向后翻頁(yè)的場(chǎng)景及超過(guò)Top 10000 數(shù)據(jù)需要分頁(yè)場(chǎng)景。
Scroll:需要遍歷全量數(shù)據(jù)場(chǎng)景 。
max_result_window:調(diào)大治標(biāo)不治本,不建議調(diào)過(guò)大。
PIT:本質(zhì)是視圖。


本文說(shuō)法有不嚴(yán)謹(jǐn)?shù)牡胤?,?span style="color: rgb(255, 76, 65);">官方文檔為準(zhǔn)。
歡迎大家就自己的分頁(yè)實(shí)踐進(jìn)行留言討論。
參考:
1. https://coralogix.com/log-analytics-blog/how-to-optimize-your-elasticsearch-queries-using-pagination
2. https://www.javatpoint.com/elasticsearch-pagination
3. https://www.elastic.co/guide/en/elasticsearch/reference/7.12/paginate-search-results.html
推薦:
全網(wǎng)首發(fā)!《 Elasticsearch 最少必要知識(shí)教程 V1.0 》低調(diào)發(fā)布
從實(shí)戰(zhàn)中來(lái),到實(shí)戰(zhàn)中去——Elasticsearch 技能更快提升方法論
重磅 | 死磕 Elasticsearch 方法論認(rèn)知清單(2020年國(guó)慶更新版)
中國(guó)最大的 Elastic 非官方公眾號(hào)
點(diǎn)擊查看“閱讀原文”,和全球近 1100 位+ Elastic 愛(ài)好者(含中國(guó) 50%+ Elastic 認(rèn)證工程師)一起每日精進(jìn) ELK 技能!
