AWS Builder Center
Production RAG: Implementing Hybrid Search, RRF, and Semantic Chunking on AWS

Production RAG: Implementing Hybrid Search, RRF, and Semantic Chunking on AWS

While a basic Retrieval-Augmented Generation (RAG) setup using simple text chunking and vector search works fine for basic demo applications, it often fails in enterprise production environments. Standard vector searches struggle to find exact part numbers, product IDs, or specific names, while naive character-based text slicing often cuts sentences in half, stripping out vital context.

  1. Moving Beyond Character Limits: Semantic Chunking
    Fixed-size chunking (e.g., cutting text every 512 characters) introduces noise. If an important rule spans across two chunks, the vector embedding loses its thematic cohesion.
    Semantic Chunking uses an embedding model to evaluate relationships between sentences. It calculates the statistical semantic distance between sentence sequences and splits the document only when a sharp shift in the topic occurs.
    Implementation with AWS Lambda and Bedrock
    When an enterprise document hits an Amazon S3 bucket, an asynchronous data preparation pipeline processes it using the following logic:
    python
    import numpy as np
def calculate_cosine_distance(embedding_a, embedding_b):
"""Measures the divergence between two sentence embeddings."""
return 1 - (np.dot(embedding_a, embedding_b) / (np.linalg.norm(embedding_a) * np.linalg.norm(embedding_b)))
def split_document_semantically(sentences, embeddings, distance_threshold=0.3):
"""Groups sentences into cohesive chunks based on thematic continuity."""
chunks = []
current_chunk = []
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
for i in range(len(sentences) - 1):
current_chunk.append(sentences[i])

# Calculate semantic distance to the next sentence
distance = calculate_cosine_distance(embeddings[i], embeddings[i+1])

# If the topic changes drastically, cut the chunk here
if distance > distance_threshold:
chunks.append(" ".join(current_chunk))
current_chunk = []

if current_chunk:
current_chunk.append(sentences[-1])
chunks.append(" ".join(current_chunk))

return chunks
Use code with caution.
By utilizing amazon.titan-embed-text-v2 via Bedrock to compute these sentence-level arrays, your ingested documents are segmented into clean, contextual paragraphs.
2. Overcoming Vector Blind Spots: Hybrid Search
Vector search (k-Nearest Neighbors) is excellent at capturing abstract concepts and intent, but it is notoriously poor at finding precise tokens. For instance, querying a database for product code "X-942-B" might return documents about entirely different items if they share a similar abstract description.
Hybrid Search solves this by executing two queries simultaneously within Amazon OpenSearch Serverless:
  1. A Neural (Vector) Search: To match the overall conceptual meaning.
  2. A Keyword (Lexical) Search: Using the BM25 algorithm to look for exact keyword matches, SKU numbers, and names.
    OpenSearch Serverless Hybrid Query Structure
    You can execute both search methodologies in a single payload inside OpenSearch:
    json
    {
    "size": 5,
    "query": {
    "hybrid": {
    "queries": [
    {
    "match": {
    "text_content": "Find policy updates for server configuration X-942-B"
    }
    },
    {
    "knn": {
    "vector_field": {
    "vector": [0.123, -0.456, 0.789, "..."],
    "k": 5
    }
    }
    }
    ]
    }
    }
    }
    Use code with caution.
  3. Merging the Streams: Reciprocal Rank Fusion (RRF)
    Executing a hybrid search yields two separate lists of matching documents, each scored by completely different mathematical metrics (BM25 keyword relevance vs. Vector cosine similarity scores). You cannot simply add these scores together.
    Reciprocal Rank Fusion (RRF) normalizes the results. It completely discards the raw scores and instead looks exclusively at the relative rank (position) of a document within both individual result lists.
    The algorithm uses a constant factor (k, usually set to 60) to score each document:
    (RRF_Score(d)=\sum {m\in M}\frac{1}{k+r{m}(d)})
    Where M represents the search methods (lexical and vector), and (r_m(d)) is the rank position of document d in that specific search method.
    Programmatic RRF Ranking in Python
    python
    def reciprocal_rank_fusion(lexical_results, vector_results, k=60):
    """Merges and reranks hybrid search results using RRF scores."""
    rrf_scores = {}# Process lexical keyword rankingsfor rank, doc_id in enumerate(lexical_results):
    rrf_scores[doc_id] = rrf_scores.get(doc_id, 0) + (1.0 / (k + (rank + 1)))# Process vector rankingsfor rank, doc_id in enumerate(vector_results):
    rrf_scores[doc_id] = rrf_scores.get(doc_id, 0) + (1.0 / (k + (rank + 1)))# Sort documents by their combined RRF score descendingsorted_docs = sorted(rrf_scores.items(), key=lambda item: item[1], reverse=True)
    return sorted_docs
    Use code with caution.
    Amazon OpenSearch Serverless offers built-in, native support for RRF orchestration inside its engine pipelines. This allows you to automatically receive a single, perfectly unified list of high-relevance chunks without needing custom application-side math scripts.
    Summary Architecture for Enterprise Accuracy
    By implementing this advanced tier, the runtime flow transforms:
    ┌──> Lexical Search (BM25) ──┐
    User Query ───────┼──> Vector Search (k-NN) ──┼──> Native RRF Pipeline ──> Grounded LLM Prompt
    └──> [Embeddings Engine] ───┘
    Grounding an LLM (such as Anthropic Claude 3.5 Sonnet on Bedrock) with context derived from a semantic hybrid engine reduces model hallucinations to near zero. It balances conceptual intelligence with precise document filtering, producing an incredibly reliable interface for complex enterprise datasets.
Any opinions in this article are those of the individual author and may not reflect the opinions of AWS.
Enjoyed reading this content? Let the author know!

Your likes, comments, shares, and saves help creators reach more builders.

Loading recommendations

Loading article