Design stable result pages and choose a traversal method.
course-products examples use the teaching mapping and product from chapter 5, “Mapping and field types”. Exercise walkthroughs below use the trainer’s separate public dataset.
- Result ordering
- Pagination depth
- The result contract
Design stable result pages and choose a traversal method.
The course targets Elasticsearch 9.4.3. Exercise examples use the public products dataset; other requests illustrate separate teaching indices and may require setup or privileges unavailable in the trainer. Responses are shortened fragments; scores may vary. Check deployment and license requirements for advanced features.
Result ordering
Field sorting changes the role of score; specify textual ordering when required. Sort mode selects a value from multi-valued fields, and missing controls absent values. Equal values need a unique doc-values tie-breaker, such as a product_id keyword. Do not treat _id as a regular sort field.
Check your understandingPagination depth
from/size suits shallow pages, but shards must collect candidates for skipped results too. search_after uses the last hit’s sort values for sequential traversal. PIT stabilizes the search view across updates, reducing gaps and duplicates. A cursor belongs to a particular query and sort; renew and close PIT resources.
Check your understandingThe result contract
_source filtering reduces response JSON without guaranteeing all source-loading costs disappear. fields and docvalue_fields provide other retrieval paths. Highlighting returns snippets, while field collapsing groups hits; hits.total is not automatically a group count. Scroll is for batch traversal rather than interactive navigation.
Use the returned PIT ID for search_after requests and close it afterward.
POST /course-products/_pit?keep_alive=1mHow to read this result
Use the returned PIT ID for search_after requests and close it afterward.
Catalog reasoning example
Two products priced at 99 have no unambiguous order under price alone. Add product_id, retain the last hit’s sort array and pass it as search_after. With PIT, reuse pit.id and consistent sorting rather than opening a new PIT for every page. A cursor is not an arbitrary jump to page 100: sequential traversal and page-number navigation are different interfaces.
Continuing by sort values
{
"size": 10,
"query": {
"match_all": {}
},
"sort": [
{
"price": "asc"
},
{
"product_id": "asc"
}
],
"search_after": [
99,
"sku-1"
]
}How to read this result
99 and sku-1 are teaching cursor values. Use the complete sort array from the final hit in real responses. This demonstrates only the cursor; add PIT and reuse its ID for consistent traversal of a changing index.
Sort by price
sort is an ordered list of keys. Elasticsearch compares the first key, then the second only when the first is equal. The example asks for the most expensive products, with id ascending to resolve equal prices. Ascending and descending orders are expressed as asc and desc.
Sort on fields with sortable indexed values, such as price, rating, id and brand.keyword. The analyzed name field is not a suitable keyword sort key in this mapping. When explicit non-score sorting is used, _score may be null because scoring is not needed; null is not evidence of a failed query.
Try a different scenario
{
"query": {
"match_all": {}
},
"size": 2,
"_source": [
"name",
"price"
],
"sort": [
{
"price": "desc"
},
{
"id": "asc"
}
]
}Elasticsearch response
{
"hits": {
"total": {
"value": 300,
"relation": "eq"
},
"hits": [
{
"_id": "10",
"_score": null,
"_source": {
"name": "Logitech Wireless Charger 10",
"price": 2499
}
},
{
"_id": "20",
"_score": null,
"_source": {
"name": "Samsung Wireless Charger 20",
"price": 2499
}
}
]
}
}How to read this result
Prices descend. If several products have the highest price, the lower id comes first. Compare sort priority rather than assuming the second key reorders the whole result.
Fetch the second page
from is a zero-based offset, not a page number. For page p with length n, use from = (p − 1) × n and size = n. The example requests page four of three items, so from is 9. Filtering and sorting occur before the offset is applied.
Use the same deterministic sort for every page. Otherwise equal values can produce overlaps or omissions between pages. This trainer caps returned hits at 100 and from + size at 500; it is designed for small educational pages rather than deep production pagination.
Try a different scenario
{
"query": {
"match_all": {}
},
"size": 3,
"_source": [
"name",
"price"
],
"sort": [
{
"id": "asc"
}
],
"from": 9
}Elasticsearch response
{
"hits": {
"total": {
"value": 300,
"relation": "eq"
},
"hits": [
{
"_id": "10",
"_score": null,
"_source": {
"name": "Logitech Wireless Charger 10",
"price": 2499
}
},
{
"_id": "11",
"_score": null,
"_source": {
"name": "Dell Wireless Headphones 11",
"price": 49
}
},
{
"_id": "12",
"_score": null,
"_source": {
"name": "apple Premium Wireless Headphones 12",
"price": 99
}
}
]
}
}How to read this result
The IDs are 10, 11 and 12, while total still describes all 300 matches. Page length and population size answer different questions.
Choose returned fields
_source filtering selects original JSON properties returned for each hit. It does not change the query, its score or the fields available for sorting. The example returns only name and category while sorting by id, demonstrating that a sort field need not be returned.
_id and _score are hit metadata, outside _source. Requesting fewer source properties therefore does not remove the hit identifier. Use original field paths rather than assuming that mapping multi-fields such as brand.keyword were separately written into _source.
Try a different scenario
{
"query": {
"match_all": {}
},
"size": 2,
"_source": [
"name",
"category"
],
"sort": [
{
"id": "asc"
}
]
}Elasticsearch response
{
"hits": {
"total": {
"value": 300,
"relation": "eq"
},
"hits": [
{
"_id": "1",
"_score": null,
"_source": {
"name": "Wireless",
"category": "Smartphones"
}
},
{
"_id": "2",
"_score": null,
"_source": {
"name": "Reference device",
"category": "Audio"
}
}
]
}
}How to read this result
Each source object has exactly name and category. The hit still has _id and _score, and the order still follows id.
A catalog page for an API
An API contract can require matching, ordering, pagination and projection at once. First restrict stock and indexed rating and exclude preorder. Then sort rating descending, price ascending, id ascending. Finally select page two of three items and only name, price and rating in _source. Each step is independently meaningful.
Filtering rating with exists before sorting removes missing and null values from the population instead of relying on default missing-value sort placement. The unique final id makes ties in rating and price reproducible. An implementation can return the right IDs yet still violate the contract by including an extra source property.
Try a different scenario
{
"query": {
"bool": {
"filter": [
{
"term": {
"in_stock": true
}
},
{
"exists": {
"field": "rating"
}
}
],
"must_not": [
{
"term": {
"status": "preorder"
}
}
]
}
},
"size": 3,
"_source": [
"name",
"price",
"rating"
],
"sort": [
{
"rating": "desc"
},
{
"price": "asc"
},
{
"id": "asc"
}
],
"from": 3
}Elasticsearch response
{
"hits": {
"total": {
"value": 86,
"relation": "eq"
},
"hits": [
{
"_id": "299",
"_score": null,
"_source": {
"name": "Dell Premium Travel Laptop 299",
"price": 1299,
"rating": 4.9
}
},
{
"_id": "38",
"_score": null,
"_source": {
"name": "Samsung Headphones Premium 38",
"price": 1000,
"rating": 4.8
}
},
{
"_id": "158",
"_score": null,
"_source": {
"name": "Samsung Headphones Premium 158",
"price": 1000,
"rating": 4.8
}
}
]
}
}How to read this result
Read total, the three ordered IDs and the exact source keys separately. Their combination defines this page contract; changing size or from changes the requested page.
Check your understanding
Answer before expanding each explanation.
1. Why is deep from expensive?
Shards collect and sort candidates including skipped hits.
2. What does search_after carry?
The previous page’s final hit sort values.
3. Why use PIT?
To keep a consistent search view during traversal.
4. Why use a tie-breaker?
To order hits unambiguously when sort values tie.
5. Does collapse make total a group count?
No, compute group counts separately.
Key takeaways
- Shards collect and sort candidates including skipped hits.
- The previous page’s final hit sort values.
- To keep a consistent search view during traversal.
- To order hits unambiguously when sort values tie.
- No, compute group counts separately.
Official reference
Ready to put this into practice?
Confirm reading to save your place. Reading and exercise progress are tracked separately in this browser using your guest cookie.