Why does hits.total report 10000 with relation gte, and what does track_total_hits change?
answer
- A search does not need to count to rank
- One field says eq, the other says at least
- Skipping non-competitive blocks needs no total
- The default threshold matches another familiar 10,000
- Aggregations remove most of the saving
basics
~20 sElasticsearch stops counting matches accurately at 10,000 by default so top-k queries can skip non-competitive documents. A relation of gte means at least that many matched. track_total_hits: true forces an exact count, false skips counting entirely, and an integer sets a different threshold.
solid answer
~50 sBy default a search tracks the total number of matches only up to 10,000. Past that, `hits.total.value` stays at the threshold and `hits.total.relation` becomes `"gte"` instead of `"eq"`, meaning "at least this many". The point is speed: once the engine knows a document cannot enter the top *k*, it can skip whole blocks of postings, but an **exact** count means visiting every match, which defeats that optimization. `track_total_hits: true` restores the exact number at that cost; `false` drops the count entirely and is the cheapest option; an integer sets a custom ceiling. In practice a UI shows "10,000+" and pages with `search_after`, so the exact total is rarely needed — and when it is, a separate `_count` or a cardinality-style estimate is often the better trade. Aggregations still visit all matching documents, so the saving shrinks when the request also aggregates.
go deeper
Recall that hits.total has a value and a relation, and that gte means "at least". Do not present the capped number as an exact result count in a UI.
Explain why bounded counting exists: ranking only needs the top k, so non-competitive blocks are skipped, and an exact total forces visiting every match. Know the three forms of the setting.
Reason about which request shapes actually pay for exact totals — broad scoring queries do, aggregation-heavy ones barely — and diagnose latency regressions traced to a global track_total_hits: true.
Challenge the requirement itself. Decide whether the product needs exact counts at all, what the search API contract promises about totals, and where an approximate count or a separate _count call is the better bargain.
## What hits.total actually reports Every Elasticsearch search response contains a `hits.total` object with two members: ``` "hits": { "total": { "value": 10000, "relation": "gte" }, "hits": [ ... ] } ``` `relation` is `"eq"` when `value` is the exact number of matching documents, and `"gte"` when counting stopped early and the true number is at least `value`. Clients that print `total.value` without reading `relation` are the reason so many search UIs claim exactly 10,000 results for every broad query. ## Why counting is optional Ranked retrieval does not need a count. To return the best 20 documents, the engine keeps a priority queue of size 20 and, as soon as the queue is full, it knows the minimum score required to enter it. Lucene's block-max scoring lets it compute an upper bound on the score of an entire block of postings; if that bound is below the current threshold, the whole block is skipped without scoring a single document in it. On a common term over a large index that skips the overwhelming majority of the postings list. An exact total is incompatible with that shortcut: to know how many documents match, you must at least *visit* every match. So Elasticsearch made counting bounded by default. It counts precisely until it has seen `track_total_hits` matches — 10,000 by default — and from then on it stops caring, keeps the skipping optimization, and reports `gte`. ## The three settings - **`track_total_hits: 10000`** (default) — accurate up to 10,000, `gte` beyond. Cheap, and enough for "about N results" UI copy. - **`track_total_hits: true`** — exact count, `relation` always `"eq"`. Every matching document is visited, so a query matching tens of millions of documents pays for all of them. This is the setting people quietly turn on globally and then wonder why broad queries got slower. - **`track_total_hits: false`** — no counting at all; the response omits `hits.total`. The fastest option, appropriate for infinite scroll or a "related items" widget where nothing displays a count. - **`track_total_hits: <n>`** — a custom threshold, useful when a product genuinely needs exact counts up to, say, 1,000 and can say "1,000+" past that. ## Where the cost really lands The saving is largest for high-recall, low-*k* queries: one common term, `size: 10`, no aggregations. It shrinks or vanishes when: - the request also has **aggregations**, since bucketing must visit every matching document anyway; - the query is a pure **filter** in filter context with no scoring to skip on; - the match count is small, in which case counting them all was cheap regardless. That is why "turn on exact totals, it is free because we aggregate anyway" is sometimes true and worth saying out loud in an interview — but only for that request shape. ## Interaction with pagination A classic bug: the UI computes `pageCount = ceil(total / pageSize)` and offers page 900 because `total` said 10,000, then the request fails against `index.max_result_window`, or `total` was `gte` and the real number is millions. The two limits are unrelated — `track_total_hits` governs *counting*, `index.max_result_window` governs *depth* — but they both default to 10,000, which invites the confusion. With `search_after` paging there is no page count to compute at all; the client just follows the cursor until a page comes back short. ## Getting an exact number when you need one If a report genuinely needs the number, options in rough order of cost: 1. `GET /index/_count` with the same query — still visits every match, but returns nothing else, so no scoring, sorting or fetching. 2. `track_total_hits: true` on a request that already aggregates. 3. An approximate count from a `cardinality` aggregation on a unique field when "about right" is acceptable — it uses a sketch with a tunable `precision_threshold`. And if the interviewer asks the design question: exact result counts are a product requirement people rarely examine. "About 12,000 results" and "12,431 results" are equally useless to a searcher; what they want is better results on page one.
- Does track_total_hits: true make the search slower even when only ten hits are returned?Usually yes. Returning ten hits lets the engine skip blocks of postings that cannot beat the current tenth-best score; counting exactly forces it to visit every match instead. The slowdown is largest for broad queries over large indices and negligible when few documents match or when aggregations already visit everything.
- How should a UI render hits.total when relation is gte?As "10,000+" or "more than 10,000 results", never as a precise figure and never as the input to a page-count calculation. Reading only `value` and ignoring `relation` is the single most common client bug here, and it produces pagination controls offering pages that do not exist.
- Is track_total_hits related to index.max_result_window?No, though both default to 10,000 and are constantly confused. `track_total_hits` bounds how far the engine counts matches; `index.max_result_window` bounds how deep from + size may reach. You can count exactly and still be unable to page past 10,000, and vice versa.
saying these in an interview costs you the question
- Reading hits.total.value while ignoring the relation field
- Claiming the total is capped because only 10,000 documents can match
- Enabling exact totals globally as a harmless default
- Confusing the counting threshold with the paging window
- Thinking track_total_hits: false limits how many hits are returned