Elasticsearch Sorting and Pagination
A search can match thousands of documents. Users never read them all at once. A good search page shows the best ten, lets the visitor reorder them by price or date, and offers a way to reach the next page. Elasticsearch supports all three needs through sorting and pagination.
Default Order
Without a sort instruction, Elasticsearch orders hits by relevance score, highest first. The score measures how well each document matches the query. Sorting overrides that behavior and orders results by the field you choose.
Sorting by a Field
Add a "sort" section next to the query. Each entry names a field and a direction, either "asc" or "desc."
GET /products/_search
{
"query": { "match": { "category": "shoes" } },
"sort": [
{ "price": { "order": "asc" } }
]
}
Before sort (by relevance) After sort (price ascending)
Trail Shoe $89 Canvas Shoe $39
Canvas Shoe $39 Court Shoe $62
Court Shoe $62 Trail Shoe $89
Sorting by Several Fields
List more than one sort entry to break ties. Elasticsearch applies the first entry, and uses later entries only when documents share the same value.
"sort": [
{ "rating": { "order": "desc" } },
{ "price": { "order": "asc" } },
"_score"
]
This sort places the highest-rated items first. Among equal ratings, cheaper items win. The relevance score settles any remaining ties.
Sorting Text Fields
Analyzed text fields cannot sort, because the field holds many separate words. Sort on a keyword field instead. Mappings often define a keyword sub-field beside each text field for this reason.
Mapping:
"name": {
"type": "text",
"fields": { "raw": { "type": "keyword" } }
}
Sort:
"sort": [ { "name.raw": { "order": "asc" } } ]
Missing Values and Arrays
Some documents lack the sort field. The "missing" option decides where they land, using "_last" or "_first." Fields that hold arrays need a "mode" setting such as "min," "max," or "avg" to pick the value used for ordering.
{ "price": { "order": "asc", "missing": "_last" } }
{ "scores": { "order": "desc", "mode": "max" } }
Basic Pagination with from and size
The "size" parameter sets how many hits each page holds. The "from" parameter sets how many hits to skip. Page 3 with ten results per page skips twenty.
Page 1: "from": 0, "size": 10
Page 2: "from": 10, "size": 10
Page 3: "from": 20, "size": 10
GET /products/_search
{
"from": 20,
"size": 10,
"query": { "match_all": {} },
"sort": [ { "price": "asc" } ]
}
The formula is from = (page number − 1) × size.
The Ten Thousand Limit
Elasticsearch refuses requests where from + size exceeds 10,000. Deep pages force every shard to build and sort a long list of candidates, and the cost climbs quickly.
Request: from 9,990, size 10 → 3 shards each prepare 10,000 hits
coordinator merges 30,000 hits
returns only 10
Cost grows with every page deeper into the results.
Raising the limit through index.max_result_window hides the problem without solving it. Consider whether users truly need page 1,000. Most visitors refine the search instead.
Deep Paging with search_after
The search_after feature continues from the last hit of the previous page. No skipping occurs, so the cost stays flat on every page. The sort must produce a unique order, so add a tiebreaker field such as a document ID or a unique code.
Page 1:
GET /products/_search
{
"size": 10,
"sort": [
{ "price": "asc" },
{ "sku": "asc" }
]
}
Last hit of page 1 shows: "sort": [ 62.0, "SKU-0412" ]
Page 2:
GET /products/_search
{
"size": 10,
"sort": [
{ "price": "asc" },
{ "sku": "asc" }
],
"search_after": [ 62.0, "SKU-0412" ]
}
Keeping a Stable View with Point in Time
New documents arrive while a user pages through results. Without protection, items can shift between pages. A point in time (PIT) freezes the view of the index for a short period.
POST /products/_pit?keep_alive=2m
→ returns { "id": "46ToAwMDaWR5..." }
GET /_search
{
"size": 10,
"pit": { "id": "46ToAwMDaWR5...", "keep_alive": "2m" },
"sort": [ { "price": "asc" }, { "sku": "asc" } ]
}
DELETE /_pit
{ "id": "46ToAwMDaWR5..." }
Close the PIT when finished to free resources. The scroll API served the same purpose in older versions, and PIT with search_after replaces it for new projects.
Choosing a Method
| Method | Best For | Limit |
|---|---|---|
| from and size | Page buttons on a website | First 10,000 hits |
| search_after | Infinite scroll, deep results | Forward movement only |
| PIT with search_after | Consistent view while data changes | Needs cleanup |
Search Page Design Tips
Visitors rarely click past the third page of results. Show ten to twenty items per page, and invest effort in ranking so the best matches appear early. Offer a short list of sort choices, such as relevance, price low to high, and newest first, and map each choice to one sort definition in the query.
Consider "load more" buttons for mobile apps. The button pairs naturally with search_after, because each tap continues from the last visible item and needs no page number math.
