
創作人:張超
在查詢場景中,從 Elasticsearch 中取得結果,根據不同場景,有多種不同的方式。
- 通過 from 、size 進行分頁
- 通過 scroll 拉取大量資料
- 通過 search_after 拉取大量資料
每種方式有其各自的使用場景,或者說他們是為了解決某種場景而設計的。
from + size
搜尋引擎的場景,類似 google 搜尋,翻頁操作一般是人為觸發的,并且人的行為一般不會翻頁太多,from+size 這種最經典的翻頁模式是為了解決使用者對于 TopN 的需求,使用者希望找到 TopN 個最比對的文檔。其使用方式類似 SQL 中的 LIMIT關鍵字,Elasticserach 使用 from 和 size 兩個參數來控制翻頁:
- size: 要傳回的結果數量,預設為 10
- from: 要跳過的結果數量,預設為 0
如果每頁顯示 5 條結果,下面的指令可以得到 1-3 頁的結果:
GET /_search?size=5
GET /_search?size=5&from=5
GET /_search?size=5&from=10
分頁搜尋實作方式是:
- 每個分片各自查詢的時候先建構 from+size 的優先隊列,然後将所有的文檔 ID 和排序值傳回給協調節點。
- 協調節點建立 size 為 number_of_shards * (from + size) 的優先隊列,對資料節點的傳回結果進行合并,取全局的 from+size 傳回給用戶端
這種工作模式的主要代價在于,協調節點需要等待所有分片傳回結果,然後再全局排序。是以會建立非常大的優先隊列,需要控制分頁的深度,Elasticsearch 預設最多傳回 10000 個文檔。但是有些時候,使用者需要周遊取回所有文檔,甚至可以不關心排序。在資料庫中取回全部結果可以使用遊标查詢的方式,類似的概念在 Elasticsearch 中叫做 scroll。
scroll
scroll 可以用于拉取全量資料,他的工作模式不需要像 from + size 一樣全局排序,是以沒有深分頁的代價,例如 reindex 本質上就是 scroll+bulk。使用 scroll 可以簡單的在查詢語句中添加
scroll
參數:
POST /my-index-000001/_search?scroll=1m
上面的查詢語句會傳回一個 ID,後面可以根據這個 ID 順序拉取結果:
POST /my-index-000001/_search/scroll
{
"scroll" : "1m",
"scroll_id" : "DXF1ZXJ5QW5kRmV0Y2gBAAAAAAAAAD4WYm9laVYtZndUQlNsdDcwakFMNjU1QQ=="
}
Elasticsearch 将查詢後的上下文儲存在服務端,是以用戶端可以依據這個 scroll_id 依次擷取後續的資料。
這個上下文也必然有一個生命周期,因為他會一直占用服務端資源。在這個例子中,我們設定 scroll 視窗儲存 1 分鐘的時間。1 分鐘之内,你都可以使用 scroll_id 來拉取資料。當然,你也可以在拉取完成之後根據 scroll_id 手工清理上下文。
search_after
如果說 scroll 是把上下文儲存在服務端,而 search_after 要求資料中存在一個無重複,可以用于排序的字段,需要用戶端每次傳入上次查到的最後結果,然後擷取其随後的資料。
由于随後的請求每次都是查詢出來的,如果資料發生變化,就可能出現跨頁面結果不一緻的情況,為了防止這種情況,需要在請求中加一個參數來設定目前的索引狀态保留時間。
POST /my-index-000001/_pit?keep_alive=1m
PIT 是 point in time 的簡寫,他是一個輕量級的視圖。上述請求傳回一個 ID:
{
"id": "46ToAwMDaWR5BXV1aWQyKwZub2RlXzMAAAAAAAAAACoBYwADaWR4BXV1aWQxAgZub2RlXzEAAAAAAAAAAAEBYQADaWR5BXV1aWQyKgZub2RlXzIAAAAAAAAAAAwBYgACBXV1aWQyAAAFdXVpZDEAAQltYXRjaF9hbGw_gAAAAA=="
}
随後的請求中你需要帶上他。可以通過下面的方式,擷取第一頁的結果,其中的特别之處在于,要指定進行排序的字段:
GET /_search
{
"size": 10000,
"query": {
"match" : {
"user.id" : "elkbee"
}
},
"pit": {
"id": "46ToAwMDaWR5BXV1aWQyKwZub2RlXzMAAAAAAAAAACoBYwADaWR4BXV1aWQxAgZub2RlXzEAAAAAAAAAAAEBYQADaWR5BXV1aWQyKgZub2RlXzIAAAAAAAAAAAwBYgACBXV1aWQyAAAFdXVpZDEAAQltYXRjaF9hbGw_gAAAAA==",
"keep_alive": "1m"
},
"sort": [
{"@timestamp": "asc"}
]
}
在傳回結果中,會攜帶 sort 字段的值,
{
"hits" : {
"hits" : [
{
"sort" : [
4294967298
]
}
]
}
}
你需要在下次請求的時候帶上他:
GET /_search
{
"size": 10000,
"query": ...
"pit": {
"id": ...
"keep_alive": ...
},
"sort": [
{"@timestamp": "asc"}
],
"search_after": [
4294967298
]
}
類似 scroll,pit 請求傳回的 ID 也可以手工清理。
最後我們總結一下每種分頁方式的特點:
- from+size 支援跳頁,不适合深分頁;
- scroll 不支援跳頁,适合拉取大量資料,不适合大量并發
- search_after 不支援跳頁,适合拉取大量資料
Scroll 和 search_after 都可以用于深分頁,search_after 需要提供一個主鍵字段進行排序,預設為
_shard_doc
,它是 shard index 與 Lucene 内部 ID 的組合值。在服務端儲存的上下文要比 scroll 小,目前官方推薦使用 search_after 代替 scroll。