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

> Learn to build powerful compound boolean queries in OpenSearch using must, should, and filter clauses. Optimize your searches with this essential guide.

- Repository: [AWS Samples/sample-for-amazon-opensearch-service-tutorials-101](https://github.com/aws-samples/sample-for-amazon-opensearch-service-tutorials-101)
- Tags: how-to-guide
- Published: 2026-02-25

---

**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`](https://github.com/aws-samples/sample-for-amazon-opensearch-service-tutorials-101/blob/main/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`](https://github.com/aws-samples/sample-for-amazon-opensearch-service-tutorials-101/blob/main/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`](https://github.com/aws-samples/sample-for-amazon-opensearch-service-tutorials-101/blob/main/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.

```python

# 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`.

```python

# 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.

```python

# 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

### Example 1: Simple must Query for Text Search

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.

```python

# 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`](https://github.com/aws-samples/sample-for-amazon-opensearch-service-tutorials-101/blob/main/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.

```python

# 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`](https://github.com/aws-samples/sample-for-amazon-opensearch-service-tutorials-101/blob/main/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.

```python

# 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`](https://github.com/aws-samples/sample-for-amazon-opensearch-service-tutorials-101/blob/main/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`](https://github.com/aws-samples/sample-for-amazon-opensearch-service-tutorials-101/blob/main/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`](https://github.com/aws-samples/sample-for-amazon-opensearch-service-tutorials-101/blob/main/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.