# How to Implement Hybrid Search Combining Lexical and Vector Queries in OpenSearch

> Discover how this sample app combines lexical and vector queries in OpenSearch for powerful hybrid search. Learn to merge Bedrock filters and Cohere vectors efficiently.

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

---

**The sample application implements hybrid search by merging Bedrock-extracted lexical filters with Cohere-generated dense vectors in a single OpenSearch hybrid query that combines bool-should clauses for term matching and k-NN for vector similarity.**

This tutorial repository demonstrates a production-ready pattern for **hybrid search** on Amazon OpenSearch Service, showing how to blend traditional keyword filtering with AI-powered semantic retrieval. The implementation lives in `aws-samples/sample-for-amazon-opensearch-service-tutorials-101` and uses AWS Lambda, Amazon Bedrock, and OpenSearch Service to process natural language queries into structured lexical constraints while simultaneously performing dense vector similarity search.

## How the Hybrid Search Workflow Works

The application processes user queries through a four-stage pipeline that bridges lexical and vector search paradigms. When the Lambda handler receives a request with `"type": "hybrid_search"`, it executes the following sequence:

### Step 1: Extract Semantic Filters with Amazon Bedrock

The function `identify_category_color_product_name()` invokes the **nova-lite-v1** foundation model via Amazon Bedrock to parse free-text queries into structured attributes. The LLM extracts `category`, `color`, and `product_type` from inputs like "red women running shoes" and returns strict JSON conforming to predefined taxonomies.

### Step 2: Generate Dense Vector Embeddings

Simultaneously, `get_embedding()` calls the **Cohere embed-english-v3** model through Bedrock to transform the raw search text into a 768-dimensional dense vector. This embedding captures semantic meaning beyond literal keyword matches.

### Step 3: Construct the Hybrid OpenSearch Query

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) (lines 401-462), the Lambda assembles a **hybrid query** structure that contains:

- A **bool-should** clause with `term` queries for each extracted attribute (`category`, `color`, `product_type`)
- A **k-NN** clause targeting the `vector_embedding` field with the 768-dimensional Cohere vector
- A **post_filter** ensuring final results satisfy all lexical constraints regardless of vector similarity scores

The query optionally routes through a custom **search pipeline** defined by `SEARCH_PIPELINE_NAME` for additional preprocessing.

## Core Implementation Details

The hybrid search logic resides in the `search_products()` function within [`opensearch_search.py`](https://github.com/aws-samples/sample-for-amazon-opensearch-service-tutorials-101/blob/main/opensearch_search.py). The implementation uses OpenSearch's native hybrid query syntax to execute both retrieval methods in a single API call:

- **Lexical component**: The `should_match_conditions` list contains individual `term` queries for detected attributes, with `minimum_should_match: 1` ensuring at least one lexical filter matches
- **Vector component**: The `knn` query searches the `vector_embedding` field with `k: 100` neighbors using the Cohere-generated vector
- **Post-filtering**: A separate `post_filter` bool query with `must` constraints guarantees that vector search results are filtered by the extracted lexical attributes, preventing semantic matches that violate categorical constraints

The system supports both **on_disk** and **in_memory** index modes via the `mode` parameter, allowing flexibility between cost efficiency and query latency.

## Code Examples

### Sample API Request Payload

The Lambda expects this JSON structure to trigger hybrid search processing:

```json
{
  "type": "hybrid_search",
  "attribute_value": "red women running shoes",
  "mode": "on_disk"
}

```

The `type` field distinguishes hybrid search from pure lexical or vector modes, while `mode` selects the OpenSearch index storage type.

### Python Client Implementation

Call the search endpoint via API Gateway using standard HTTP requests:

```python
import requests
import json

url = "https://<api-id>.execute-api.<region>.amazonaws.com/prod/search"
payload = {
    "type": "hybrid_search",
    "attribute_value": "red women running shoes",
    "mode": "on_disk"
}

response = requests.post(url, json=payload)
results = response.json()
print(json.dumps(results, indent=2))

```

### The Hybrid Query Structure

The Lambda constructs this OpenSearch query body in [`opensearch_search.py`](https://github.com/aws-samples/sample-for-amazon-opensearch-service-tutorials-101/blob/main/opensearch_search.py):

```python
search_body = {
    "size": 100,
    "_source": {"excludes": "vector_embedding"},
    "query": {
        "hybrid": {
            "queries": [
                {
                    "bool": {
                        "should": should_match_conditions,
                        "minimum_should_match": 1
                    }
                },
                {
                    "knn": {
                        "vector_embedding": {
                            "vector": vector_embedding,
                            "k": 100
                        }
                    }
                }
            ]
        }
    },
    "post_filter": {
        "bool": {
            "must": should_match_conditions
        }
    },
    "search_pipeline": SEARCH_PIPELINE_NAME
}

```

The `should_match_conditions` list contains term filters extracted by the LLM, while `vector_embedding` holds the Cohere-generated dense vector.

### LLM-Driven Filter Extraction Function

The semantic parsing uses carefully engineered prompts to constrain the LLM output:

```python
def identify_category_color_product_name(search_text):
    prompt = f"""Given the search text: "{search_text}",
    identify the most likely product category, color and product_name.
    Choose only from these categories: men, women, unisex.
    Choose only from these colors: red, blue, green, yellow, multicolor, orange,
    purple, pink, brown, black, white, grey, white,coral, gold, teal,
    burgundy, silver.
    Choose only from these product types: shoes, bag, apparel, accessories,
    innerwear, other
    Return strictly a json with category,color, product_type"""
    # Bedrock invocation returns: {"category":"women","color":"red","product_type":"shoes"}

    return bedrock_response

```

This function returns standardized JSON that maps directly to OpenSearch term query filters.

## Key Files and Architecture

Understanding the repository structure helps navigate the implementation:

- **[`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)**: Core Lambda handler containing `search_products()`, `get_embedding()`, and `identify_category_color_product_name()`; implements the hybrid query construction at lines 401-462
- **[`app.py`](https://github.com/aws-samples/sample-for-amazon-opensearch-service-tutorials-101/blob/main/app.py)**: Flask wrapper for local testing that simulates the Lambda execution environment
- **`search_tutorials/`**: CDK stacks provisioning the OpenSearch domain, Lambda layers, Bedrock permissions, and API Gateway endpoints
- **[`README.md`](https://github.com/aws-samples/sample-for-amazon-opensearch-service-tutorials-101/blob/main/README.md)**: Deployment instructions and environment variable configuration for `SEARCH_PIPELINE_NAME` and model ARNs

## Summary

- The sample application combines **lexical filtering** (via Bedrock LLM extraction) with **dense vector search** (via Cohere embeddings) in a single OpenSearch hybrid query
- The `hybrid` query syntax merges `bool-should` term filters with `knn` vector similarity, while `post_filter` ensures lexical constraints are strictly enforced
- Implementation resides in [`opensearch_search.py`](https://github.com/aws-samples/sample-for-amazon-opensearch-service-tutorials-101/blob/main/opensearch_search.py) with `identify_category_color_product_name()` handling semantic extraction and `get_embedding()` managing vector generation
- The architecture supports both on-disk and in-memory index configurations through the `mode` parameter
- Search pipelines allow preprocessing via `SEARCH_PIPELINE_NAME` before hybrid query execution

## Frequently Asked Questions

### What is the difference between the lexical and vector components in this hybrid search?

The **lexical component** uses traditional term matching on structured attributes (category, color, product_type) extracted by the nova-lite-v1 model, while the **vector component** performs semantic similarity search using 768-dimensional Cohere embeddings. The lexical filters constrain the result set to relevant categories, while the vector component ranks results by semantic meaning within those constraints.

### Why does the implementation use a post_filter in addition to the hybrid query?

The `post_filter` ensures that results returned by the k-NN vector search are subsequently filtered by the lexical constraints using a `bool.must` clause. This guarantees that high-scoring vector matches which violate the extracted categorical filters (e.g., returning men's shoes for a women's query) are removed from the final result set, maintaining both semantic relevance and categorical accuracy.

### Which foundation models does the sample application use for hybrid search?

The application uses **Amazon Nova Lite v1** (accessed via Amazon Bedrock) for extracting structured filters from natural language queries, and **Cohere embed-english-v3** (also via Bedrock) for generating the 768-dimensional dense vectors used in the k-NN search component. These models are invoked in the `identify_category_color_product_name()` and `get_embedding()` functions respectively.

### How does the search_pipeline parameter enhance the hybrid search?

The `search_pipeline` field (configured via the `SEARCH_PIPELINE_NAME` environment variable) allows OpenSearch to apply additional processing steps—such as synonym expansion, query normalization, or reranking—before executing the hybrid query. This enables the application to modify or enrich both the lexical and vector query components through OpenSearch's native pipeline processors without changing the Lambda code.