Elasticsearch Top Hits Aggregation - Sample Documents Per Bucket - Syntax, Example, and Tips

The Elasticsearch top_hits aggregation is a metric aggregation that returns the top matching documents inside each parent bucket. It supports the same controls as a regular search - size, sort, _source, highlight, script_fields, and version - but applied per bucket. Use it for "the most recent N documents per category", "best-scoring hit per group", or "one sample per bucket" patterns that would otherwise need a second query.

Syntax

GET /articles/_search
{
  "size": 0,
  "aggs": {
    "by_category": {
      "terms": { "field": "category.keyword", "size": 10 },
      "aggs": {
        "latest": {
          "top_hits": {
            "size": 3,
            "sort": [{ "published_at": "desc" }],
            "_source": { "includes": ["title", "author", "published_at"] }
          }
        }
      }
    }
  }
}

top_hits must always sit inside a bucket aggregation. As a top-level aggregation it produces a single global bucket, which is rarely what you want.

Parameters

Parameter Default Description
size 3 Number of hits per bucket. Capped by index.max_inner_result_window (default 100).
from 0 Pagination offset within a bucket. Subject to the same window cap.
sort _score Any sort the search API supports - field, script, geo distance, or _score.
_source true Source filtering: false, true, or { "includes": [...], "excludes": [...] }.
highlight - Highlight configuration applied per returned hit.
script_fields - Computed fields per hit.
stored_fields - Stored fields to return per hit.
version false Include the _version field in each hit.
seq_no_primary_term false Include _seq_no and _primary_term per hit.
explain false Include scoring explanation.
track_scores false Compute _score even when sorting on a non-score field.

Examples

Most recent document per host:

"aggs": {
  "hosts": {
    "terms": { "field": "host.keyword", "size": 50 },
    "aggs": {
      "latest_event": {
        "top_hits": {
          "size": 1,
          "sort": [{ "@timestamp": "desc" }]
        }
      }
    }
  }
}

Highest-scoring 5 docs per author:

"aggs": {
  "authors": {
    "terms": { "field": "author.keyword", "size": 20 },
    "aggs": {
      "best_articles": {
        "top_hits": {
          "size": 5,
          "sort": [{ "_score": "desc" }],
          "highlight": { "fields": { "body": {} } }
        }
      }
    }
  }
}

Use top_hits as the "field collapse" alternative when collapsing is not available:

"aggs": {
  "products": {
    "terms": { "field": "product_id", "size": 100 },
    "aggs": {
      "top": {
        "top_hits": {
          "size": 1,
          "_source": { "includes": ["title", "price"] }
        }
      }
    }
  }
}

Performance Notes

top_hits fetches stored fields and source for each returned document, so cost scales with size * number_of_buckets. A terms aggregation with 1000 buckets and top_hits.size: 10 fetches 10,000 documents. That is the same cost as a regular search of size: 10000, plus the bucket grouping work.

Sorting on a non-indexed or non-doc-value field forces a per-hit script computation. Always sort on indexed fields where possible. For "field collapse" patterns - one representative document per group - the dedicated collapse parameter on a search is faster than terms + top_hits and supports inner_hits for additional documents per group.

top_hits is bound by index.max_inner_result_window (default 100). Requests for larger sizes are rejected at parse time. top_hits inside high-cardinality terms aggregations is a common cause of slow searches and heap pressure. The manual triage - matching slow-log entries to bucket counts, deciding which patterns belong in collapse instead - is the loop Pulse runs continuously.

Common Mistakes

  1. Setting size above index.max_inner_result_window (100). Raise the setting only after measuring impact.
  2. Using top_hits at the top level. It needs a parent bucket aggregation to be meaningful.
  3. Heavy _source payloads per hit when only a few fields are needed - always use _source.includes.
  4. Picking top_hits instead of collapse for one-doc-per-group rendering. collapse is purpose-built and cheaper.
  5. Sorting by a runtime field, forcing per-hit script evaluation.

Find Expensive top_hits Patterns with Pulse

Pulse is an AI DBA for Elasticsearch and OpenSearch that continuously profiles production query traffic. For top_hits aggregations specifically, Pulse:

  • Identifies top_hits nested inside high-cardinality terms aggregations where total hit-retrieval cost is size * number_of_buckets and is dominating cluster latency
  • Flags top_hits requests whose effective per-bucket fetch crosses or pushes against index.max_inner_result_window (default 100), plus heavy _source payloads where only a few fields are needed
  • Detects "one document per group" patterns that would be faster as the dedicated collapse parameter with optional inner_hits
  • Spots sorts on runtime fields or non-doc-value fields that are forcing per-hit script evaluation
  • Traces each slow top_hits aggregation back to the calling service via slow-log and APM correlation
  • Recommends concrete fixes: switch to collapse for field-collapse use cases, trim _source.includes, sort on indexed or doc-value fields, cap parent bucket size, and move exhaustive iteration to a composite aggregation with samples per page
  • Tracks latency and heap impact after the change ships

This converts the manual hit-retrieval cost triage loop into a continuous optimization workflow.

Try Pulse on your cluster.

Frequently Asked Questions

Q: Can top_hits be used without a parent bucket aggregation?
A: It can, but it produces only one global "bucket". Use the regular search API for that case. top_hits earns its place when nested in a bucket aggregation like terms or date_histogram.

Q: What is the maximum size for top_hits?
A: index.max_inner_result_window, default 100. The setting is per index and can be raised, but very large per-bucket fetches are expensive. Reconsider the query if you need much more.

Q: How does top_hits compare to field collapsing?
A: collapse on a search is purpose-built for one document per group and supports a second-level inner_hits for additional documents per group. It is faster than terms + top_hits at high cardinality. top_hits wins when you also need other bucket-level metrics in the same response.

Q: Can I highlight matched terms inside top_hits results?
A: Yes. The highlight configuration inside top_hits behaves the same as on a regular search and produces a highlight field per returned hit.

Q: Can I paginate within a bucket using top_hits from?
A: Technically yes, subject to from + size <= index.max_inner_result_window. For exhaustive iteration across buckets, a composite aggregation is usually a better fit.

Q: How does top_hits interact with nested documents?
A: Wrap top_hits in a nested aggregation to retrieve top inner documents per parent. Sort and _source operate on the nested context inside that wrapper.

Q: How do I find which top_hits aggregations are causing slow searches and heap pressure?
A: Pulse profiles Elasticsearch and OpenSearch slow logs, isolates top_hits nested inside high-cardinality terms aggregations and "one-per-group" patterns that should use collapse instead, attributes each to the calling service, and recommends concrete _source.includes, collapse, and sort-field changes.

Subscribe to the Pulse Newsletter

Get early access to new Pulse features, insightful blogs & exclusive events , webinars, and workshops.

We use cookies to provide an optimized user experience and understand our traffic. To learn more, read our use of cookies; otherwise, please choose 'Accept Cookies' to continue using our website.