Course contents

Results, sorting and pagination

Design stable result pages and choose a traversal method.

8 sectionsElasticsearch 9.4.3Not read yet

What you will learn

  • Result ordering
  • Pagination depth
  • The result contract

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.

Chapter map
  • 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.

01 / 08

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 understanding
02 / 08

Pagination 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 understanding
03 / 08

The 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=1m

How to read this result

Use the returned PIT ID for search_after requests and close it afterward.

Check your understanding
04 / 08

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

POST /course-products/_search
{
  "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.

Check your understanding
05 / 08

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

POST /products/_search
{
  "query": {
    "match_all": {}
  },
  "size": 2,
  "_source": [
    "name",
    "price"
  ],
  "sort": [
    {
      "price": "desc"
    },
    {
      "id": "asc"
    }
  ]
}
Elasticsearch response
Shortened response · JSON
{
  "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.

Apply this in exercise 16
06 / 08

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

POST /products/_search
{
  "query": {
    "match_all": {}
  },
  "size": 3,
  "_source": [
    "name",
    "price"
  ],
  "sort": [
    {
      "id": "asc"
    }
  ],
  "from": 9
}
Elasticsearch response
Shortened response · JSON
{
  "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.

Apply this in exercise 17
07 / 08

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

POST /products/_search
{
  "query": {
    "match_all": {}
  },
  "size": 2,
  "_source": [
    "name",
    "category"
  ],
  "sort": [
    {
      "id": "asc"
    }
  ]
}
Elasticsearch response
Shortened response · JSON
{
  "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.

Apply this in exercise 18
08 / 08

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

POST /products/_search
{
  "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
Shortened response · JSON
{
  "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.

Apply this in exercise 28

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.