How to Implement Range Filters for Numeric and Date Fields in OpenSearch

Range filters in OpenSearch are implemented by constructing JSON query clauses using operators like gt, gte, lt, and lte, which the Lambda handler in aws-samples/sample-for-amazon-opensearch-service-tutorials-101 translates from HTTP request parameters into OpenSearch DSL.

The aws-samples/sample-for-amazon-opensearch-service-tutorials-101 repository provides a complete reference implementation showing how to build serverless search APIs that handle numeric and date range filtering. Understanding these patterns helps you construct efficient queries for price ranges, date intervals, and other bounded numeric data in OpenSearch.

Simple Range Filters for Single-Field Queries

The repository implements a straightforward range_filter search type designed for single-field numeric or date constraints. This approach extracts operator and value information directly from the incoming request payload.

How the Lambda Handler Processes Range Operators

In artifacts/search_lambda/opensearch_search.py (lines 68-78), the search_products function handles the range_filter type by validating the operator and converting values:


# From artifacts/search_lambda/opensearch_search.py lines 68-78

if search_type == "range_filter":
    attribute_name = body.get("attribute_name")
    attribute_value = body.get("attribute_value")
    operator = body.get("operator")  # gt, gte, lt, lte

    
    # Validate operator is a string

    if not isinstance(operator, str):
        raise ValueError("Operator must be a string")
    
    # Convert to integer for numeric fields

    value = int(attribute_value)
    
    # Build the range query

    search_body = {
        "query": {
            "range": {
                attribute_name: {
                    operator: value
                }
            }
        }
    }

The code validates that the operator is one of the supported string values (gt, gte, lt, lte), converts the attribute value to an integer for numeric comparison, and nests the operator under the field name within the OpenSearch range clause.

Example: Filtering Products by Price

When a client sends a request to filter products with prices greater than or equal to 100:

{
  "type": "range_filter",
  "attribute_name": "price",
  "attribute_value": "150",
  "operator": "gte"
}

The Lambda generates the following OpenSearch query:

{
  "size": 100,
  "query": {
    "range": {
      "price": { "gte": 150 }
    }
  }
}

Complex Multi-Field Range Filtering

For scenarios requiring multiple field constraints or more sophisticated date ranges, the repository implements a complex search handler that processes field definitions with explicit min and max boundaries.

Building Range Queries with Min/Max Values

In artifacts/search_lambda/opensearch_search.py (lines 31-38), the code handles fields where type equals "range":


# From artifacts/search_lambda/opensearch_search.py lines 31-38

for field in fields:
    field_name = field.get("name")
    field_type = field.get("type")
    field_value = field.get("value")
    
    if field_type == "range":
        range_query = {"range": {field_name: {}}}
        
        if "min" in field_value:
            range_query["range"][field_name]["gte"] = field_value["min"]
        if "max" in field_value:
            range_query["range"][field_name]["lte"] = field_value["max"]
            
        must_clauses.append(range_query)

This implementation checks for the presence of min and max keys in the field value object, mapping them to gte (greater than or equal) and lte (less than or equal) operators respectively. This approach works for both numeric fields and date strings.

Example: Numeric Rating Range

For filtering products with ratings between 3 and 5:

{
  "type": "complex_search",
  "search_type": "combined",
  "fields": [
    {
      "name": "rating",
      "type": "range",
      "value": { "min": 3, "max": 5 }
    }
  ]
}

The generated OpenSearch query fragment:

{
  "range": {
    "rating": {
      "gte": 3,
      "lte": 5
    }
  }
}

Example: Date Range Filtering

For date fields, the same min/max pattern applies using ISO date strings:

{
  "type": "complex_search",
  "search_type": "combined",
  "fields": [
    {
      "name": "release_date",
      "type": "range",
      "value": { 
        "min": "2022-01-01", 
        "max": "2022-12-31" 
      }
    }
  ]
}

This produces:

{
  "range": {
    "release_date": {
      "gte": "2022-01-01",
      "lte": "2022-12-31"
    }
  }
}

Summary

  • Simple range filters use the range_filter search type in artifacts/search_lambda/opensearch_search.py (lines 68-78), supporting gt, gte, lt, and lte operators for single-field numeric queries.
  • Complex range filters handle multi-field scenarios using the "range" field type (lines 31-38), mapping min values to gte and max values to lte for both numeric and date fields.
  • Both implementations construct standard OpenSearch DSL range queries that execute via ops_client.search(), leveraging OpenSearch's native support for numeric and date range filtering.
  • The repository demonstrates production-ready patterns for validating operator strings, converting data types, and building dynamic query clauses in Python Lambda functions.

Frequently Asked Questions

How do I filter documents by a numeric price range in OpenSearch using this sample?

Send a POST request with type: "range_filter" including attribute_name (the field), attribute_value (the number as a string), and operator (gte, lte, gt, or lt). The Lambda in artifacts/search_lambda/opensearch_search.py converts the value to an integer and builds the OpenSearch range query with your specified operator.

Can I use date strings with the range filter implementation?

Yes. While the simple range_filter type converts values to integers for numeric fields, the complex search implementation (lines 31-38) accepts date strings in ISO format (e.g., "2023-01-01") when using the "range" field type with min and max values. OpenSearch automatically handles date parsing based on your index mappings.

What is the difference between the simple range_filter and the complex range field type?

The simple range_filter (lines 68-78) is designed for single-field queries with explicit operators (gt, gte, lt, lte) and converts values to integers. The complex "range" field type (lines 31-38) supports multi-field search requests, uses min/max semantics instead of explicit operators, and preserves string values for dates or decimals without forced integer conversion.

Which operators are supported for range queries in this implementation?

The implementation supports the standard OpenSearch range operators: gt (greater than), gte (greater than or equal), lt (less than), and lte (less than or equal). In the simple filter, these are passed directly in the request. In the complex search, min maps to gte and max maps to lte automatically.

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 →