Build the mental model
By default, Elasticsearch returns search results sorted by `_score` descending, but the `sort` parameter can reorder them by a field (say, `publishedAt` descending) — `sort` only works on `keyword`, numeric, or date fields (attempting it on a `text` field causes errors or wrong ordering). Pagination navigates through pages using `from` (how many documents to skip) plus `size` (how many to return), but internally each shard sorts `from + size` documents and returns them to a coordinating node, which merges and re-sorts them — as `from` grows large (a deep page like page 10,000), the memory and CPU cost grows too, and exceeding Elasticsearch's default `index.max_result_window` (10,000) causes an outright error. `search_after` solves this problem — instead of jumping to a page number, it's a cursor-based approach that says "continue from the last document's sort values", so each shard doesn't need a full re-sort, only continuing the search from the cursor position forward. This directly parallels the Redis course's pagination lesson (offset vs cursor): `from`/`size` is like a database's `OFFSET`/`LIMIT`, and `search_after` shares the same concept as cursor-based pagination — like a social media feed's "scroll for more posts" pattern, where you never jump to a page number, only ever pass along a "continue from here" pointer.
Connect it to a real scenario
Sort the Tutorial Platform's search result list by `publishedAt` descending — for the admin UI's "page 1, page 2" navigation, `from`/`size` works fine through the first 10 pages (100 results). But a public-facing "infinite scroll" search UI can let a user scroll through hundreds of results for "redis", easily crossing the deep-pagination limit — implement `search_after` there, combining `publishedAt` with a tie-breaker of `_id` (a unique field) as two sort keys; without a tie-breaker, documents sharing the same `publishedAt` can get duplicated or skipped across page boundaries.
Try the working example
GET /tutorials/_search
{
"size": 10,
"query": { "match": { "body": "redis" } },
"sort": [
{ "publishedAt": "desc" },
{ "_id": "asc" }
],
"search_after": ["2026-08-01T00:00:00Z", "tutorial-42"]
}You get the next page of "redis" search results, continuing from the previous page's last document's sort values as a cursor.5-minute try-it
Write a `search_after` query sorted by `lessonCount` descending with `_id` ascending as a tie-breaker, and predict what happens if you run the same request with `from: 50000, size: 10` instead.
One important caution
Assuming `from`/`size` scales fine for deep pagination (page 500+) and designing a production UI around it without accounting for the `max_result_window` limit — large `from` values cause errors or high latency.
Implementing `search_after` with only one sort key (like `publishedAt` alone) and no tie-breaker field with unique values — documents sharing the same value can be duplicated or skipped across page boundaries.