OpenSearch Aggregations Explained: Terms, Stats, Range, and Nested Stats in AWS Samples

The aws-samples/sample-for-amazon-opensearch-service-tutorials-101 repository demonstrates four core OpenSearch aggregation types—terms, stats, range, and nested_stats—implemented in Python backend logic and invoked through a React TypeScript frontend.

OpenSearch aggregations enable powerful analytics and data summarization directly within your search queries. This tutorial repository provides a complete working implementation showing how to bucket documents by keyword fields, calculate statistical metrics, group by numeric ranges, and perform nested metric calculations within parent buckets.

Types of OpenSearch Aggregations Demonstrated

The repository implements four distinct aggregation patterns in artifacts/search_lambda/opensearch_search.py, each serving different analytical purposes for the product catalog dataset.

Terms Aggregations

Terms aggregations group documents into buckets based on unique values of a keyword field, such as product categories or colors.

In opensearch_search.py (lines 141-147), the backend constructs a terms aggregation on fields like category.keyword:


# From opensearch_search.py lines 141-147

if agg['type'] == 'terms':
    agg_body = {
        "terms": {
            "field": agg['field'],
            "size": agg.get('size', 10)
        }
    }

The React frontend requests this aggregation in keyword-aggregations-page.tsx (lines 99-104) by sending a payload with type: "terms", enabling the UI to render category distribution charts and filterable bucket lists.

Stats Aggregations

Stats aggregations compute comprehensive statistical metrics—count, minimum, maximum, average, and sum—for numeric fields like pricing or inventory quantities.

The backend implementation in opensearch_search.py (lines 148-154) builds a stats aggregation:


# From opensearch_search.py lines 148-154

elif agg['type'] == 'stats':
    agg_body = {
        "stats": {
            "field": agg['field']
        }
    }

When the UI sends type: "stats" in keyword-aggregations-page.tsx (lines 111-114), OpenSearch returns a statistical summary that the frontend displays as metric cards showing average price, price ranges, and total inventory value across the product catalog.

Range Aggregations

Range aggregations divide documents into user-defined numeric buckets based on specified boundaries, ideal for creating price range filters or date histograms.

Implemented in opensearch_search.py (lines 155-162), this aggregation accepts an array of ranges:


# From opensearch_search.py lines 155-162

elif agg['type'] == 'range':
    agg_body = {
        "range": {
            "field": agg['field'],
            "ranges": agg['ranges']  # User-defined boundaries

        }
    }

The frontend configuration in keyword-aggregations-page.tsx (lines 116-124) allows users to define specific price brackets (e.g., $0-$50, $50-$100, $100+), which OpenSearch uses to bucket products accordingly for faceted navigation interfaces.

Nested Stats Aggregations

Nested stats aggregations combine bucket aggregations with metric calculations, first grouping documents by a parent field (like category) then computing metrics (like average price) within each bucket.

This advanced pattern appears in opensearch_search.py (lines 163-176):


# From opensearch_search.py lines 163-176

elif agg['type'] == 'nested_stats':
    agg_body = {
        "terms": {
            "field": agg['field'],  # Parent bucket field

            "size": agg.get('size', 10),
            "aggs": {
                "nested_metric": {
                    agg['metric_type']: {  # e.g., "avg", "sum"

                        "field": agg['metric_field']
                    }
                }
            }
        }
    }

When triggered by type: "nested_stats" from keyword-aggregations-page.tsx (lines 126-134), this aggregation enables analytical displays showing average price per category, total inventory per manufacturer, or other multi-level metrics essential for business intelligence dashboards.

How Aggregations Flow Through the Application

The repository implements a clear request-response pipeline connecting the React frontend to the OpenSearch domain via AWS Lambda.

  1. Client Request Assembly – The React page keyword-aggregations-page.tsx constructs a JSON payload containing an aggregations array with type definitions (terms, stats, range, nested_stats) and POSTs it to the /search endpoint.

  2. Lambda Handler Processing – The search_products function in opensearch_search.py receives the request, detects search_type: "aggregations", and iterates over the aggregation definitions.

  3. Aggregation DSL Construction – For each aggregation entry, the code matches the agg_type against the four supported patterns and appends the appropriate OpenSearch DSL fragment under the aggs node of the query body.

  4. OpenSearch Execution – The constructed query executes against the Amazon OpenSearch Service domain, returning bucket counts, statistical values, and nested metric results.

  5. UI Rendering – The frontend receives the aggregation results and renders them as charts, progress bars, statistical cards, and faceted filter controls.

Implementing OpenSearch Aggregations in Python

The backend implementation in opensearch_search.py demonstrates a clean pattern for dynamically building aggregation queries based on frontend configuration:

def build_aggregation_query(aggregations):
    """
    Constructs OpenSearch aggregation DSL from frontend configuration.
    Based on implementation in opensearch_search.py lines 141-176.
    """
    aggs_body = {}
    
    for agg in aggregations:
        agg_name = agg['name']
        agg_type = agg['type']
        
        if agg_type == 'terms':
            aggs_body[agg_name] = {
                "terms": {
                    "field": agg['field'],
                    "size": agg.get('size', 10)
                }
            }
        elif agg_type == 'stats':
            aggs_body[agg_name] = {
                "stats": {
                    "field": agg['field']
                }
            }
        elif agg_type == 'range':
            aggs_body[agg_name] = {
                "range": {
                    "field": agg['field'],
                    "ranges": agg['ranges']
                }
            }
        elif agg_type == 'nested_stats':
            aggs_body[agg_name] = {
                "terms": {
                    "field": agg['field'],
                    "size": agg.get('size', 10),
                    "aggs": {
                        "nested_metric": {
                            agg['metric_type']: {
                                "field": agg['metric_field']
                            }
                        }
                    }
                }
            }
    
    return {"aggs": aggs_body}

This pattern allows the frontend to request complex analytical queries without hardcoding aggregation logic, enabling dynamic faceted search and business intelligence visualizations.

Summary

The aws-samples/sample-for-amazon-opensearch-service-tutorials-101 repository demonstrates four essential OpenSearch aggregations for building analytical search applications:

  • Terms aggregations bucket documents by unique keyword values (e.g., categories, colors) using opensearch_search.py lines 141-147
  • Stats aggregations calculate comprehensive statistics (count, min, max, avg, sum) on numeric fields via lines 148-154
  • Range aggregations create user-defined numeric buckets (e.g., price ranges) implemented in lines 155-162
  • Nested stats aggregations combine parent buckets with sub-metrics (e.g., average price per category) using lines 163-176

These aggregations flow from the React frontend (keyword-aggregations-page.tsx) through the Lambda backend to Amazon OpenSearch Service, enabling faceted navigation and statistical dashboards.

Frequently Asked Questions

What is the difference between terms and stats aggregations in OpenSearch?

Terms aggregations group documents into buckets based on unique values of a keyword field (like product categories or brands), while stats aggregations compute mathematical statistics (count, minimum, maximum, average, and sum) across numeric fields (like prices or quantities). The sample repository implements both in opensearch_search.py, with terms handling categorical faceting and stats powering metric dashboards.

How do nested stats aggregations work in the sample repository?

Nested stats aggregations create a two-level analytical query that first buckets documents by a parent field (using a terms aggregation) and then calculates a metric (like avg, sum, or max) within each bucket. In opensearch_search.py (lines 163-176), the code constructs a terms aggregation with a nested aggs clause containing the metric, enabling calculations like "average price per category" or "total inventory per manufacturer."

Can I customize the range boundaries for bucket aggregations?

Yes, the range aggregation implementation in the sample repository accepts user-defined boundaries through the frontend configuration. In keyword-aggregations-page.tsx (lines 116-124), the UI sends a ranges array specifying bucket boundaries (e.g., [{"to": 50}, {"from": 50, "to": 100}, {"from": 100}]), which opensearch_search.py (lines 155-162) passes directly to the OpenSearch DSL range aggregation.

What file handles the aggregation logic in the backend?

The aggregation logic resides in artifacts/search_lambda/opensearch_search.py, specifically within the search_products function (lines 141-176). This Python module receives aggregation requests from the frontend, constructs the appropriate OpenSearch DSL queries for terms, stats, range, and nested_stats aggregations, and returns the processed results to the React UI.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →