RAG & Knowledge Systems

Advanced RAG Techniques

Improve RAG performance with hybrid search, reranking, query transformation, and other advanced retrieval strategies.

Advanced RAG Techniques

Basic RAG works well for simple cases, but production systems need advanced techniques to handle complex queries, improve accuracy, and reduce hallucinations.

The Limitations of Basic RAG

Basic RAG can fail when:

  • The query doesn't match document terminology
  • Relevant information is spread across multiple chunks
  • The user asks multi-part or complex questions
  • Important context is lost during chunking

Advanced techniques address each of these issues.

Combine semantic search (embeddings) with keyword search (BM25) for better results:

python
from langchain.retrievers import EnsembleRetriever
from langchain_community.retrievers import BM25Retriever
from langchain_chroma import Chroma

# Semantic retriever (embedding-based)
semantic_retriever = vectorstore.as_retriever(search_kwargs={"k": 5})

# Keyword retriever (BM25)
bm25_retriever = BM25Retriever.from_documents(chunks)
bm25_retriever.k = 5

# Combine both
ensemble_retriever = EnsembleRetriever(
    retrievers=[semantic_retriever, bm25_retriever],
    weights=[0.6, 0.4]  # Weight semantic higher
)

results = ensemble_retriever.invoke("error code E-4021")

When Hybrid Search Helps

Query TypeSemantic OnlyHybrid
"How to reset password?"GoodGood
"Error code E-4021"Poor (no semantic meaning)Good (keyword match)
"API rate limit"GoodBetter
"John Smith's account"PoorGood (exact name match)

Technique 2: Reranking

After initial retrieval, use a more powerful model to rerank results:

python
from openai import OpenAI

client = OpenAI()

def rerank_results(query, documents, top_k=3):
    """Use an LLM to rerank retrieved documents."""
    doc_list = "\n\n".join(
        f"[Document {i+1}]: {doc}" for i, doc in enumerate(documents)
    )

    response = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[{
            "role": "user",
            "content": f"""Given this query: "{query}"

Rank these documents by relevance (most relevant first).
Return only the document numbers in order, separated by commas.

{doc_list}

Ranking (most relevant first):"""
        }],
        temperature=0,
        max_tokens=50
    )

    # Parse the ranking
    ranking = response.choices[0].message.content.strip()
    indices = [int(x.strip()) - 1 for x in ranking.split(",")]
    return [documents[i] for i in indices[:top_k]]

Using Cohere Reranker (Purpose-Built)

python
# pip install cohere
import cohere

co = cohere.Client("your-cohere-key")

def cohere_rerank(query, documents, top_k=3):
    results = co.rerank(
        query=query,
        documents=documents,
        model="rerank-english-v3.0",
        top_n=top_k
    )
    return [documents[r.index] for r in results.results]

Technique 3: Query Transformation

Transform the user's query before retrieval to improve matches:

Query Expansion

python
def expand_query(query):
    """Generate multiple search queries from a single question."""
    response = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[{
            "role": "user",
            "content": f"""Generate 3 alternative search queries for this question.
            Each should capture a different aspect or use different terms.

            Original: "{query}"

            Return only the queries, one per line."""
        }],
        temperature=0.7,
        max_tokens=200
    )

    queries = response.choices[0].message.content.strip().split("\n")
    return [query] + queries  # Include original

# Example:
# "How do I cancel my subscription?"
# → ["How do I cancel my subscription?",
#    "Steps to unsubscribe from service",
#    "Cancel recurring payment plan",
#    "End membership account"]

HyDE (Hypothetical Document Embeddings)

Generate a hypothetical answer, then search for real documents similar to it:

python
def hyde_search(query, vectorstore):
    # Step 1: Generate a hypothetical answer
    response = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[{
            "role": "user",
            "content": f"Write a short paragraph that would answer: {query}"
        }],
        temperature=0.5,
        max_tokens=200
    )
    hypothetical_answer = response.choices[0].message.content

    # Step 2: Search using the hypothetical answer as the query
    results = vectorstore.similarity_search(hypothetical_answer, k=5)
    return results

Technique 4: Parent Document Retrieval

Store small chunks for precise search, but return larger parent chunks for context:

python
from langchain.text_splitter import RecursiveCharacterTextSplitter

# Small chunks for search (precise matching)
child_splitter = RecursiveCharacterTextSplitter(
    chunk_size=200,
    chunk_overlap=50
)

# Large chunks for context (full paragraphs)
parent_splitter = RecursiveCharacterTextSplitter(
    chunk_size=1000,
    chunk_overlap=200
)

def parent_document_search(query, documents):
    # Create parent and child chunks
    parents = parent_splitter.split_documents(documents)
    parent_map = {}

    for parent in parents:
        children = child_splitter.split_text(parent.page_content)
        for child in children:
            parent_map[child] = parent.page_content

    # Search against child chunks
    matched_children = search_similar(query, list(parent_map.keys()), top_k=3)

    # Return parent chunks
    return [parent_map[child] for child in matched_children]

Technique 5: Contextual Compression

Remove irrelevant parts from retrieved documents before sending to the LLM:

python
def compress_context(query, documents):
    """Extract only relevant parts from each document."""
    compressed = []

    for doc in documents:
        response = client.chat.completions.create(
            model="gpt-4o-mini",
            messages=[{
                "role": "user",
                "content": f"""Extract only the parts of this document that are
                relevant to answering: "{query}"

                Document:
                {doc}

                Relevant excerpt (or "NOT RELEVANT" if nothing matches):"""
            }],
            temperature=0,
            max_tokens=300
        )
        excerpt = response.choices[0].message.content
        if excerpt.strip() != "NOT RELEVANT":
            compressed.append(excerpt)

    return compressed

Technique 6: Self-RAG (Self-Reflective RAG)

The model evaluates whether retrieved documents are actually helpful:

python
def self_rag(query, retriever, llm_client):
    # Step 1: Retrieve documents
    docs = retriever.invoke(query)

    # Step 2: Check if retrieval is needed
    response = llm_client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[{
            "role": "user",
            "content": f"""Can this question be answered from these documents?

Question: {query}
Documents: {docs}

Answer YES or NO, then explain briefly."""
        }],
        temperature=0
    )

    if "NO" in response.choices[0].message.content[:10]:
        # Retrieval didn't help — answer without context or say "I don't know"
        return "I don't have enough information to answer that question."

    # Step 3: Generate answer with quality check
    answer = generate_answer(query, docs)
    return answer

Advanced RAG Architecture

User Query
    │
    ├─→ Query Transformation (expand, rewrite)
    │       │
    │       ├─→ Semantic Search ──┐
    │       └─→ Keyword Search ───┤
    │                             │
    │                    ┌────────▼────────┐
    │                    │    Reranker     │
    │                    └────────┬────────┘
    │                             │
    │                    ┌────────▼────────┐
    │                    │   Contextual    │
    │                    │  Compression    │
    │                    └────────┬────────┘
    │                             │
    └─────────────────────→ LLM Generation
                                  │
                          ┌───────▼───────┐
                          │ Self-Check /  │
                          │  Validation   │
                          └───────┬───────┘
                                  │
                              Response

Choosing Techniques

ProblemSolution
Exact terms not foundHybrid search (BM25 + semantic)
Wrong docs ranked highestReranking
Query too vagueQuery expansion / transformation
Too much irrelevant contextContextual compression
Missing broader contextParent document retrieval
Answers from bad sourcesSelf-RAG validation

Performance Benchmarks

TechniqueAccuracy ImprovementLatency Impact
Hybrid search+10-15%Minimal
Reranking+15-25%+200-500ms
Query expansion+10-20%+500ms (extra embedding calls)
HyDE+5-15%+300ms (extra LLM call)
Contextual compression+5-10%+200ms per doc

Key Takeaways

  1. Start with basic RAG, then add techniques based on failure analysis
  2. Hybrid search is the easiest win — always combine semantic + keyword
  3. Reranking gives the biggest quality boost for moderate latency cost
  4. Query transformation helps when users write ambiguous queries
  5. Measure everything — A/B test each technique against your test set

With RAG mastered, the next module covers fine-tuning — customizing models for your specific domain and style.