在實際搜尋系統中,分頁是必須的。Elasticsearch 預設提供 from + size
,但這種方式在深度分頁時會變得非常慢。我們今天的目標是:
理解 from + size
的限制,改用 search_after
作為分頁方案,在 Go 端實作一個支援分頁的查詢
from + size
假設我們要查詢第 3 頁,每頁 10 筆:
curl -X POST "http://localhost:9200/books/_search?pretty" \
-H 'Content-Type: application/json' \
-d '{
"from": 20,
"size": 10,
"query": {
"match": { "title": "golang" }
}
}'
這會跳過前 20 筆,再取後 10 筆。
當分頁很深(例如跳過 10 萬筆)時,ES 仍需掃過前面所有資料,效能急速下降。
search_after
概念search_after
不是用「第幾頁」,而是用「上一次最後一筆的排序值」來接續查詢。這種方式是 cursor-based pagination,效能遠比 from + size
好。
用法:
sort
search_after
先查詢第一頁,每頁 2 筆:
curl -X POST "http://localhost:9200/books/_search?pretty" \
-H 'Content-Type: application/json' \
-d '{
"size": 2,
"sort": [
{ "published_at": "asc" },
{ "_id": "asc" }
],
"query": {
"match_all": {}
}
}'
回傳結果裡,每筆文件會帶有 sort
欄位,例如:
"sort": ["2025-01-01T12:00:00Z", "1"]
下一頁查詢就帶上它:
curl -X POST "http://localhost:9200/books/_search?pretty" \
-H 'Content-Type: application/json' \
-d '{
"size": 2,
"search_after": ["2025-01-01T12:00:00Z", "1"],
"sort": [
{ "published_at": "asc" },
{ "_id": "asc" }
],
"query": {
"match_all": {}
}
}'
在 ESSearchService
新增一個分頁版查詢:
func (es *ESSearchService) SearchPaged(ctx context.Context, query string, after []any, size int) ([]SearchResult, []any, error) {
body := fmt.Sprintf(`{
"size": %d,
"sort": [
{ "published_at": "asc" },
{ "_id": "asc" }
],
"query": {
"match": { "title": "%s" }
}`, size, query)
if after != nil {
sa, _ := json.Marshal(after)
body = body + fmt.Sprintf(`, "search_after": %s`, sa)
}
body = body + "}"
results, sorts, err := es.searchRawWithSort(ctx, body)
return results, sorts, err
}
其中 sorts
會存回最後一筆文件的 sort 值,供下一頁使用。
今天我們完成了:
from + size
的限制search_after
做 cursor-based paginationSearchPaged
,支援分頁查詢這樣,我們的 /search
API 已經可以處理大規模資料集的分頁,而不怕深度分頁效能崩壞。