Nested query

Nested query #

Matches parent documents whose nested sub-documents — objects mapped with "nested": true — satisfy a query within one element at a time. It exists because a flattened object index cannot keep two field values inside the same array element together — the pitfall the next section makes concrete.

The cross-element problem #

A flattened index correlates across array elements, not within them: variants.color = blue AND variants.size = L matches a document as soon as some element is blue and some element is L — not necessarily the same one. The teaching dataset’s products makes the difference visible (each element pairs blue with L, never with M):

graph LR subgraph flat["flattened — one shared field space"] F1("variants[0] is blue") F2("variants[1] is M") F1 --> FM["matches — 10 documents
the two values may come
from different elements"] F2 --> FM end subgraph nest["nested — one element at a time"] N1("variants[0]
blue, L — no M") N2("variants[1]
red, M — no blue") N1 --> NM["no match — 0 documents"] N2 --> NM end

The flattened query above reports 10 documents — every product with some blue variant and some M variant, even when they are different elements. The nested query below evaluates each variant as a whole element and reports 0: in this dataset blue variants only ever come in size L.

Same-element matching #

When the pair lives in one element, only the nested query is guaranteed correct (blue always pairs with L in this dataset):

20 of 30 products have a blue/L variant. Any leaf composes inside the inner query:

Inner queries and minimum_should_match #

The inner query supports:

Not supported inside nested yet: terms, prefix, wildcard, fuzzy, regexp, suffix, the span family and query_string. These are rejected with an HTTP 400 naming the offending clause and the nested path — never a silent zero-hit answer. The same shapes keep working at the top level.

bool.minimum_should_match applies within one element: a parent matches when at least one element satisfies that many should clauses by itself. Clauses matched by different elements never add up — minimum_should_match: 2 over two should clauses is the same-element AND:

The same 20 blue/L products as the must example above — with minimum_should_match dropped to 1 (the default when the bool has no must/filter) it becomes the same-element OR.

Phrases compose with the same element scoping — the adjacent word pair must live in ONE element, and slop (default 0) buys holes between the words, in query order:

POST /my-collection/_search
{
  "query": {
    "nested": {
      "path": "reviews",
      "query": {
        "phrase": {
          "field": "reviews.body",
          "query": "quick fox",
          "slop": 1
        }
      }
    }
  }
}

quick fox with slop: 1 matches an element holding “quick brown fox” (one word between); at slop: 0 nothing matches unless the words are adjacent. Reversed word order ("fox quick") never matches at any slop — phrase terms keep query order.

Schema requirement #

Declare the object as nested so elements are indexed with ordinal-tagged postings (server-side collections). Both forms are accepted — they are the same schema:

PUT /my-collection
{
  "schema": {
    "properties": {
      "variants": {
        "type": "nested",
        "properties": {
          "color": { "type": "keyword" },
          "size": { "type": "keyword" }
        }
      }
    }
  }
}

The explicit form spells the parameter out ("type": "object" + "nested": true) for readers who prefer it.

Documents index variants as an array of objects; each element is addressable through its dotted sub-fields inside the nested query.

How it works #

nested rides the same block pipeline as every other query, with one extra channel — the ordinal that identifies which array element produced a posting:

  1. The inner query resolves against ordinal-tagged postings, where each entry is a (doc_id, ordinal, score) triple instead of the usual (doc_id, score).
  2. Boolean combination happens on the composite key: intersection and union match on (doc_id, ordinal) pairs, so two clauses agree only when ONE element satisfies both — never element 0’s color plus element 1’s size.
  3. A collapse producer folds the per-element matches back to unique parent ids, combining the per-element scores by score_mode.

Flat queries never touch the ordinal channel — the same block operators fall back to plain doc-id intersection — so non-nested traffic pays nothing for the feature. Documents still in the realtime layer (not yet frozen into segments) take the per-element stored-array path described under Performance characteristics below.

Performance characteristics #

Correlation is resolved in one of two ways, depending on where the matching documents live:

  • Built segments — every element’s postings carry a (doc_id, ordinal) pair, and the engine intersects / unions on that composite key. Nested queries resolve at close to ordinary bool cost.
  • Realtime (not yet frozen) documents — the inner query is evaluated against each stored array element, one document at a time. Exact and available immediately after the write, but the cost is linear in documents × elements: a nested query over 20 000 documents × 5 elements measured roughly 10–15× the latency of the equivalent flattened bool (sub-second either way). Keep realtime buffers bounded when nested queries dominate a large collection.

Scoring note: realtime-layer elements match with a constant score, so score_mode only differentiates on built segments.

Use cases #

  • Variant correlation. The products pair above — color and size must describe one SKU. Any catalog with option arrays has this shape; the flattened query overstates the match count (10 vs 0 in the teaching data).
  • Per-review constraints. “Reviews containing ‘quick fox’ AND rated

    = 4” must hold within one review, not across the array — a phrase and a range scoped to the same element.

  • Line-item queries. Orders and invoices whose line items pair a SKU with a quantity or a price band — the same-element AND, per line.
  • Explaining the match. inner_hits returns the exact elements that matched (identity, source, score), so a UI can highlight the blue/L variant instead of the whole product.
  • Sub-object nesting. nested inside nested walks multi-level arrays (variant → components) with the same element-at-a-time rule at each level.

Parameters for nested #

  • path
    (Required, string) Path of the nested object (e.g. "variants").
  • query
    (Required, object) The query applied to each inner element; a parent matches when at least one element satisfies it.
  • score_mode
    (Optional, string) How per-element scores collapse into the parent’s score: sum (default), avg, min, max, or none (constant 1.0).
  • inner_hits
    (Optional, object) When present, every top-level hit carries an inner_hits block keyed by the path: the matched ELEMENTS of that document’s nested array, each with _nested: {field, offset} identity, the element _source, and its score (default window: 3, from/size adjustable). Sub-nested requests materialize recursively inside each inner hit. An optional highlight block highlights inner hits against the inner query; multi-field sub-fields (field.vectors) fall back to the parent field’s text.
  • join — the cross-collection counterpart: membership by key against another collection, instead of correlation inside one document.
  • traverse and graph — relations between documents over a relation edge field.
  • Nested aggregation — bucket analytics over the same nested structure.
  • Children aggregation — the parent/child aggregation over joined hierarchies.
  • Relational parameters — the schema family (references, ref, routing) the cross-collection pages build on.
Calendar October 6, 2026
Edit Edit this page