The search API, paging & sorting
|
This section documents the current Elasticsearch 9.x line (with 8.19 as the final 8.x release) as published at the Elasticsearch documentation, which is the reference these pages are written and verified against. No specific patch version is pinned. Some capabilities (Kibana-only UIs, the ML/NLP model-management workflow, cross-cluster replication, and parts of the paid / serverless-only surface) are linked, not documented in depth. This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production, as Elasticsearch iterates quickly. This section’s bibliography lists the reference material consulted while preparing these pages. |
Every read that is not a get-by-id goes through the _search endpoint: you send a query,
Elasticsearch fans it out to the relevant shards, merges the per-shard results, and returns a fixed
response envelope. This page covers how to call _search, how to read what comes back, how to
control which fields and how many hits you get, and how to page through large result sets.
The _search endpoint
Send a search as a request body (the normal form) or as a URI query string (handy for quick checks). Both hit the same endpoint; the request body exposes the full search API.
# Request-body search: the full Query DSL is available.
GET /books/_search
{
"query": { "match": { "title": "elasticsearch" } }
}
# URI search: a compact query_string in the "q" parameter, no body.
GET /books/_search?q=title:elasticsearch&size=5
The response is always the same shape:
{
"took": 7,
"timed_out": false,
"_shards": { "total": 1, "successful": 1, "skipped": 0, "failed": 0 },
"hits": {
"total": { "value": 42, "relation": "eq" },
"max_score": 3.14,
"hits": [
{ "_index": "books", "_id": "9780", "_score": 3.14, "_source": { "title": "Elasticsearch" } }
]
}
}
-
took— milliseconds Elasticsearch spent (not counting network);timed_out— whether the optionaltimeoutwas hit and partial results returned. -
_shards— how many shards answered; a non-zerofailedmeans the result is partial.skippedcounts shards a pre-query filter (for example a date range vs. shard bounds) let Elasticsearch skip entirely. -
hits.total.valuewithrelation: "eq"is an exact count;relation: "gte"means "at least this many" — by default Elasticsearch stops counting at 10000 to save work. Settrack_total_hitstotruefor an exact count regardless of cost, or to an integer to count accurately up to that bound. -
hits.max_score— the highest_scorein the whole result set (nullwhen scoring is not computed, e.g. a pure filter or a sort other than_score). Each entry inhits.hitscarries its own_score.
GET /books/_search
{
"track_total_hits": true,
"query": { "match_all": {} }
}
For just the number, call
_count — it
returns \{ "count": N } and no hits:
GET /books/_count
{
"query": { "term": { "status": "published" } }
}
terminate_after caps how many matching documents each shard collects before stopping; the response
then carries "terminated_early": true. Use it to bound worst-case cost when an approximate answer
is acceptable.
GET /books/_search
{
"terminate_after": 1000,
"query": { "match": { "title": "guide" } }
}
Query vs. filter context
Every clause in the Query DSL runs in one of two contexts, and choosing the right one is the single most important search-performance decision.
-
Query context answers "how well does this document match?" and computes a relevance
_score. Use it for full-text clauses where ranking matters. -
Filter context answers a yes/no question, computes no score, and its result is cached in the node query cache for reuse. Use it for exact, binary conditions — status, dates, ranges, terms.
A bool query puts must / should in query context and filter / must_not in filter context:
GET /books/_search
{
"query": {
"bool": {
"must": [ { "match": { "title": "elasticsearch" } } ],
"filter": [
{ "term": { "status": "published" } },
{ "range": { "published_at": { "gte": "2020-01-01" } } }
]
}
}
}
See
Query and filter context.
For how scores combine across clauses, constant_score, dis_max and function_score, see
Compound queries & relevance.
Choosing which fields come back
By default each hit carries the full original document in _source. Trim or reshape what is
returned rather than shipping whole documents you do not need.
_source filtering
Set _source to false to omit it entirely, or to an object with includes / excludes (both
accept wildcards). This filters the stored JSON on the way out; it does not save disk.
GET /books/_search
{
"_source": { "includes": [ "title", "author.*" ], "excludes": [ "author.internal_notes" ] },
"query": { "match_all": {} }
}
# Shorthand as a URI parameter:
GET /books/_search?_source=false
fields
The fields parameter is the recommended retrieval API. Unlike raw _source, it returns values
after mapping: dates are formatted, keyword sub-fields resolve, aliases work, and
runtime fields
(computed at search time) can be requested the same way. Values always come back as arrays.
GET /books/_search
{
"query": { "match_all": {} },
"fields": [ "title", { "field": "published_at", "format": "yyyy-MM-dd" } ],
"runtime_mappings": {
"title_length": { "type": "long", "script": { "source": "emit(doc['title.keyword'].value.length())" } }
},
"_source": false
}
docvalue_fields and stored_fields
docvalue_fields reads values from columnar doc values — cheap for keyword, numeric and date
fields, but not available for analysed text. stored_fields returns fields that were individually
marked "store": true in the mapping (rare; _source usually makes this unnecessary). See
Retrieve selected fields.
GET /books/_search
{
"query": { "match_all": {} },
"docvalue_fields": [ "status", { "field": "published_at", "format": "epoch_millis" } ],
"stored_fields": [ "title" ]
}
Sorting
Add a sort array to order hits by field instead of by _score. _score and _doc (index order,
the cheapest) are also valid sort keys. Sorting on a field turns scoring off unless you also ask for
_score.
GET /books/_search
{
"query": { "match": { "title": "guide" } },
"sort": [
{ "published_at": { "order": "desc" } },
{ "rating": { "order": "desc", "missing": "_last" } },
{ "_score": { "order": "desc" } },
"_doc"
]
}
-
Multi-level: keys are applied in order, each breaking ties of the previous one.
-
missingplaces documents that lack the field (_lastor_first, default_last). -
unmapped_typelets a sort span indices where some do not map the field — without it the search fails:\{ "rating": \{ "unmapped_type": "float" } }. -
Sorting on an analysed
textfield fails; sort on itskeywordsub-field. Sorting insidenestedobjects needs anestedsort clause — see Joins & relationships.
See Sort search results.
Pagination
from / size and the result-window ceiling
from (default 0) and size (default 10) take a slice of the ranked list. This is the analogue
of OFFSET / LIMIT in SQL Queries — but with a hard
difference: every shard must build and return from + size hits for the coordinator to merge, so
cost grows with the offset. Elasticsearch refuses from + size beyond index.max_result_window
(default 10000).
GET /books/_search
{
"from": 20,
"size": 10,
"query": { "match_all": {} },
"sort": [ { "published_at": "desc" } ]
}
Raising the window works but multiplies memory use on every shard; past a few pages, switch method.
search_after + point-in-time
For deep or unbounded paging, sort on a unique, total order and pass the previous page’s sort
values as search_after. Pair it with a
point-in-time (PIT)
so every page sees the same frozen view of the index even as writes continue.
# 1. Open a PIT; it returns an "id".
POST /books/_pit?keep_alive=2m
# 2. First page: query against the PIT, sort with a tiebreaker for a total order.
GET /_search
{
"size": 100,
"query": { "match": { "title": "guide" } },
"pit": { "id": "<pit_id>", "keep_alive": "2m" },
"sort": [ { "published_at": "desc" }, { "_shard_doc": "asc" } ]
}
# 3. Next page: reuse the "sort" array of the last hit from the previous response.
GET /_search
{
"size": 100,
"query": { "match": { "title": "guide" } },
"pit": { "id": "<pit_id>", "keep_alive": "2m" },
"sort": [ { "published_at": "desc" }, { "_shard_doc": "asc" } ],
"search_after": [ 1609459200000, 42 ]
}
# 4. When done, release it.
DELETE /_pit
{ "id": "<pit_id>" }
search_after has no window limit and each page costs the same as the first. There is no cursor
state on the server beyond the PIT, so it also scales to many concurrent pagers. Details and
tiebreaker guidance are in
Paginate search results.
See Pagination: Offset vs. Keyset for how this same keyset pattern is
expressed in SQL, MongoDB, Couchbase, Solr, GraphQL and Spring Data.
scroll
The older scroll API takes a
one-time snapshot and returns a _scroll_id you feed back to pull the next batch. It holds per-search state
and segment files open on every shard for the whole scroll, which is expensive under concurrency. PIT
search_after gives the same consistent deep-paging without that cost and is the recommended choice; reserve
scroll for a single-threaded full export of an entire index.
preference and routing
preference pins a search to the same set of shard copies across requests (for example
preference=<user-session-id>), so consecutive pages stay consistent even without a PIT and benefit
from warm caches. routing sends the search to only the shard(s) a routing value maps to, cutting
fan-out when documents were indexed with that same routing. See
Search shard routing.
GET /books/_search?preference=session_9a3f&routing=author_42
{
"query": { "term": { "author_id": "author_42" } }
}
For the broader picture of running searches against your data, see Search your data. Continue with Full-text queries and Term-level queries.