How to Build Compound Boolean Queries with must, should, and filter Clauses in OpenSearch

Compound boolean queries in OpenSearch are constructed by initializing empty lists for must and should clauses, populating them with query objects based on field types and search parameters, and assembling them into a bool query dictionary that is sent to the OpenSearch client.

The aws-samples/sample-for-amazon-opensearch-service-tutorials-101 repository demonstrates this pattern in a production-ready search Lambda. The opensearch_search.py module dynamically builds these queries based on incoming API requests, handling everything from simple text matches to complex field-based filtering.

Understanding the Three Boolean Clauses

OpenSearch bool queries combine multiple query clauses using three primary constructs. Each serves a distinct purpose in relevance scoring and result filtering.

The must Clause

The must clause acts as a required match operator. Every query placed inside a must array must match the document for it to be included in the results. These clauses contribute to the relevance score, making them ideal for mandatory search terms. In the sample code, primary text searches using multi_match are placed in must when search_type is set to "exact".

The should Clause

The should clause contains optional matches that increase a document's relevance score if they match, but do not exclude documents if they fail to match. When the search_type parameter is set to "any" in the sample Lambda, field-specific queries are appended to the should_conditions list. The code enforces at least one match by setting minimum_should_match: 1, effectively turning optional clauses into "at least one required" constraints without affecting scoring for the mandatory must clauses.

The filter Clause

The filter clause applies strict constraints without affecting relevance scoring. Filters are cached by OpenSearch, making them highly performant for exact-match constraints like status flags, category IDs, or date ranges. While the current implementation in opensearch_search.py focuses on must and should, adding a filter clause follows the identical pattern: initialize a filter_conditions list and insert it into the bool query dictionary under the "filter" key.

Step-by-Step Construction in opensearch_search.py

The search Lambda constructs compound boolean queries through a three-stage pipeline defined in artifacts/search_lambda/opensearch_search.py.

Step 1: Initialize Clause Containers

The process begins by creating empty lists to hold query objects. At lines 181-183, the code initializes must_conditions and should_conditions as empty Python lists. These containers will store the individual query clauses before final assembly.


# From opensearch_search.py, lines 181-183

must_conditions = []
should_conditions = []

Step 2: Populate must and should Lists

The Lambda then iterates through incoming search parameters to build query objects. For text searches, it constructs a multi_match query at lines 184-204, applying field boosts and fuzzy matching parameters before appending to must_conditions.

For structured field queries (text, select, or range types), the code at lines 207-240 builds appropriate query objects—match for text, term for exact select values, or range for numeric boundaries. The decision logic at lines 240-244 determines placement: if search_type equals "any", the clause goes to should_conditions; otherwise, it joins must_conditions.


# Conceptual flow from lines 184-244

if search_value:
    must_conditions.append({
        "multi_match": {
            "query": search_value,
            "fields": ["title^3", "description", "color"],
            "fuzziness": "AUTO"
        }
    })

for field in fields:
    query_obj = build_field_query(field)  # match, term, or range

    if search_type == "any":
        should_conditions.append(query_obj)
    else:
        must_conditions.append(query_obj)

Step 3: Assemble the Final Bool Query

At lines 245-258, the Lambda assembles the complete query body. It always includes the must array. When should_conditions contains items and the search type is "any", it adds the should array and sets minimum_should_match to 1, ensuring at least one optional clause matches.


# From opensearch_search.py, lines 245-258

search_body = {
    "query": {
        "bool": {
            "must": must_conditions
        }
    }
}

if search_type == "any" and should_conditions:
    search_body["query"]["bool"]["should"] = should_conditions
    search_body["query"]["bool"]["minimum_should_match"] = 1

The search_body dictionary is then passed directly to the OpenSearch client via ops_client.search(index=index_name, body=search_body).

Code Examples

When a user submits a general search term with search_type: "exact", the Lambda generates a bool query containing only a must clause with a multi_match query.


# Input payload

payload = {
    "search_value": "wireless headphones",
    "search_type": "exact",
    "fields": []
}

# Generated OpenSearch query (search_body)

{
    "query": {
        "bool": {
            "must": [
                {
                    "multi_match": {
                        "query": "wireless headphones",
                        "fields": ["title^3", "description^2", "color"],
                        "type": "best_fields",
                        "fuzziness": "AUTO"
                    }
                }
            ]
        }
    }
}

This corresponds to the logic in opensearch_search.py at lines 184-204 and 246-250.

Example 2: Combining should Clauses for Optional Matches

When search_type is set to "any", field-level constraints become optional clauses that boost relevance rather than filter results. The Lambda places these in the should array and enforces a minimum match requirement.


# Input payload

payload = {
    "search_value": "",
    "search_type": "any",
    "fields": [
        {"name": "color", "type": "text", "value": "red"},
        {"name": "price", "type": "range", "value": {"min": 50, "max": 200}}
    ]
}

# Generated OpenSearch query

{
    "query": {
        "bool": {
            "must": [],
            "should": [
                {"match": {"color": {"query": "red"}}},
                {"range": {"price": {"gte": 50, "lte": 200}}}
            ],
            "minimum_should_match": 1
        }
    }
}

This pattern appears in opensearch_search.py at lines 239-258, where the code checks if search_type == "any" to determine whether to append to should_conditions instead of must_conditions.

Example 3: Adding filter Clauses for Performance

While the current implementation focuses on must and should, extending the Lambda to support filter clauses follows the same architectural pattern. Filters apply strict constraints without scoring overhead and benefit from OpenSearch caching.


# Extension to opensearch_search.py (conceptual)

filter_conditions = []

# Add status filter (exact match, no scoring)

filter_conditions.append({"term": {"status.keyword": "active"}})

# Add inventory filter (range, no scoring)

filter_conditions.append({"range": {"inventory": {"gt": 0}}})

# Assemble final query with filter

search_body = {
    "query": {
        "bool": {
            "must": must_conditions,
            "should": should_conditions,
            "filter": filter_conditions,
            "minimum_should_match": 1 if should_conditions else 0
        }
    }
}

Inserting this logic at lines 245-251 of opensearch_search.py would enable high-performance filtering for categorical or numeric constraints while preserving the relevance-scoring behavior of must and should clauses.

Summary

  • Compound boolean queries in OpenSearch combine must, should, and filter clauses to balance mandatory constraints, optional relevance boosting, and high-performance filtering.
  • The must clause requires all contained queries to match and affects scoring, used in opensearch_search.py for mandatory text searches via multi_match.
  • The should clause increases relevance for matching documents without requiring them; when search_type is "any", the Lambda places field queries here and sets minimum_should_match: 1 to ensure at least one matches.
  • The filter clause (extensible pattern) applies binary yes/no constraints without scoring overhead, ideal for status flags or range limits, and can be added to the existing Lambda logic following the same list-initialization pattern at lines 181-183 and assembly pattern at lines 245-258.

Frequently Asked Questions

What is the difference between must and should in OpenSearch bool queries?

The must clause acts as an AND operator where every query must match the document for it to be included in results, and it contributes to the relevance score. The should clause acts as an OR operator for relevance purposes—matching documents get a score boost, but non-matching documents are not excluded unless minimum_should_match is specified. In the sample Lambda, must holds the primary multi_match query while should holds optional field constraints when search_type is "any".

When should I use a filter clause instead of must?

Use filter when you need strict binary inclusion or exclusion based on exact values, ranges, or categorical data where relevance scoring is irrelevant. Filters are cached by OpenSearch, making them significantly faster than must for repeated constraints like status: "active" or price > 100. The must clause is preferable when the constraint should influence the relevance score, such as matching a search term in a title field.

How does the search Lambda handle the minimum_should_match parameter?

The Lambda sets minimum_should_match: 1 when the search_type is "any" and the should_conditions list contains items. This parameter ensures that while individual should clauses are treated as optional for scoring purposes, at least one of them must match for the document to be returned. This effectively converts the should array from a pure relevance booster into a flexible OR constraint without requiring all conditions to match, as implemented at lines 254-257 of opensearch_search.py.

Can I combine must, should, and filter in a single query?

Yes, OpenSearch bool queries can simultaneously contain must, should, filter, and must_not arrays. The sample Lambda demonstrates must and should combination, and you can extend it to include filter by initializing a filter_conditions list alongside must_conditions at line 181, populating it with term or range queries, and inserting it into the bool dictionary at line 248. Documents must satisfy all must and filter clauses, match at least the minimum_should_match threshold of should clauses, and match none of the must_not clauses to be returned.

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 →