RAG Retrieval Methods: Complete Guide

Retrieval is the backbone of any Retrieval-Augmented Generation (RAG) system. The quality of the documents you surface determines the ceiling of your answer quality—no amount of prompt engineering compensates for retrieving the wrong passages. This guide covers every major retrieval approach, their trade-offs, how they combine, and practical recommendations for deploying each method in a fully sovereign, on-premise environment where no data leaves your network.

Sovereignty note: Every method and tool recommended here runs entirely on your own infrastructure. We explicitly flag cloud-only services and provide local alternatives where the canonical implementation is hosted.

1. Dense Vector Retrieval

Dense vector retrieval is the standard approach in modern RAG. You embed the user’s query and all documents into a shared high-dimensional vector space using a transformer-based embedding model, then find the nearest neighbors by cosine similarity (or dot product, depending on whether the vectors are normalized). Documents that are semantically similar to the query land close together in the space, even when they share no surface-level words.

User Query Embedding Model Query Vector ANN Search Top-K Docs

How It Works

Each document chunk is passed through an embedding model—typically a small transformer (100–400M parameters)—that outputs a fixed-length vector (384, 768, 1024, or 1536 dimensions are common). These vectors are stored in a vector index. At query time, the same model embeds the query, and an approximate nearest neighbor (ANN) algorithm finds the K stored vectors with the highest cosine similarity to the query vector.

Cosine similarity between the query vector q and document vector d is:

cosine(q, d) = (q · d) / (||q|| × ||d||)

If vectors are L2-normalized (all embeddings have unit length), cosine similarity reduces to the dot product, which is faster to compute and is the default in most vector databases.

Sovereign-Compatible Tools

Tool Type Sovereign? Notes
Qdrant Dedicated vector DB Yes Rust core, open-source (Apache 2.0). Runs in Docker on-prem. HNSW index, payload filtering, quantization support.
pgvector PostgreSQL extension Yes Adds vector types and ANN search (HNSW, IVFFlat) to PostgreSQL. Reuses your existing DB. Best when you want relational + vector in one engine.
FAISS Library (Meta) Yes C++ library with Python bindings. No server, no persistence layer—you build your own. Extremely fast, GPU-capable. Best for research and embedded use.
Milvus Dedicated vector DB Yes Open-source, scales to billions of vectors. Heavier infrastructure footprint (etcd, MinIO, Pulsar).
Chroma Embedded vector DB Yes Python-native, lightweight. Good for prototyping and small deployments.
Pinecone Managed SaaS No Cloud-only. Avoid for sovereign deployments. Listed for comparison only.

Dense Retrieval Summary

✓ Pros

  • Semantic understanding: finds related concepts even with zero word overlap.
  • Handles synonyms, paraphrasing, and natural language questions well.
  • Language-agnostic with multilingual models (e.g., e5-mistral-multilingual, bge-m3).
  • Scales to millions of documents with ANN indexes (HNSW, IVF).
  • Single model serves all queries—no per-domain tuning required for most cases.

✗ Cons

  • Misses exact matches: a search for an ID like ERR-47291 or a function name parseAuthToken() may not surface the exact passage because the embedding generalizes.
  • Requires an embedding model loaded into memory (200MB–2GB depending on model).
  • Embedding quality depends on the model—domain-specific text (medical, legal, code) may need fine-tuned or domain-specific embeddings.
  • Out-of-domain queries (text very different from training data) degrade performance.
  • Re-embedding all documents when switching models is expensive at scale.

Recommended Embedding Models (Local)

ModelDimensionsSizeBest For
bge-large-en-v1.510241.3GBGeneral English, strong MTEB scores
bge-m310242.3GBMultilingual, multi-granularity (dense + sparse + multi-vector in one)
e5-large-v210241.3GBGeneral purpose, well-documented
gte-large10241.3GBStrong on reasoning-heavy benchmarks
nomic-embed-text-v1.57680.5GBLightweight, fully open weights, long context
all-MiniLM-L6-v23840.09GBFast prototyping, low resource
# Example: dense retrieval with pgvector (sovereign, on-prem) # Assuming a table: documents(id, content, embedding vector(1024)) # 1. Store document embeddings INSERT INTO documents (content, embedding) VALUES ('The contract is void ab initio...', embedding_model('contract void text')); # 2. Query by cosine similarity SELECT id, content, 1 - (embedding <=> query_embedding) AS similarity FROM documents ORDER BY embedding <=> query_embedding -- cosine distance operator LIMIT 10; # With HNSW index for approximate search at scale: CREATE INDEX ON documents USING hnsw (embedding vector_cosine_ops);
Practical tip: Always store the original chunk text alongside the vector. You will need it for re-ranking, citation, and display. Never assume you can reconstruct text from embeddings—you cannot.

2. Sparse Retrieval (BM25 / TF-IDF)

Sparse retrieval is the classic keyword-based approach that predates embeddings. It relies on exact term matching: if the query contains the word “indemnification,” it finds documents containing that exact word. The dominant algorithm is BM25 (Best Matching 25), an evolution of TF-IDF that incorporates term frequency saturation and document length normalization.

The BM25 Scoring Formula

BM25(q, d) = Σt ∈ q IDF(t) × [ f(t,d) × (k1 + 1) ] / [ f(t,d) + k1 × (1 − b + b × |d| / avgdl) ]

Where f(t,d) is term frequency in the document, IDF(t) is inverse document frequency, |d| is document length, avgdl is average document length, and k1 (typically 1.2–2.0) and b (typically 0.75) are tuning parameters.

Sovereign-Compatible Tools

ToolTypeSovereign?Notes
Elasticsearch Full-text search engine Yes Mature, battle-tested BM25. Self-hostable (note: Elastic license, not pure OSI—check current terms). Scales horizontally.
OpenSearch Full-text search engine Yes Apache 2.0 licensed fork of Elasticsearch. Fully open-source. Recommended over Elasticsearch for new sovereign deployments.
PostgreSQL full-text search Built-in PG feature Yes tsvector / tsquery with GIN or GiST indexes. No separate service needed. Surprisingly capable for small-to-medium sets.
Whoosh / Tantivy / Lucene Library Yes Embed directly in your application. Tantivy (Rust) is fast and memory-efficient. No server overhead.
SPLADE Neural sparse model Yes Neural network that produces sparse vectors—combines BM25-style exactness with learned term expansion. Runs locally.

Sparse Retrieval Summary

✓ Pros

  • Exact term matching: finds specific names, product IDs, error codes, function names, legal clause numbers with high precision.
  • No embedding model needed—minimal infrastructure, low latency.
  • Transparent and debuggable: you can see exactly which terms matched and why.
  • Excellent for code, structured data, and identifier-heavy text.
  • Works well on rare terms that dense models may not handle correctly.
  • No GPU required.

✗ Cons

  • No semantic understanding: a search for “car insurance” will not find a document about “auto coverage” unless you configure synonyms.
  • Misses paraphrases and conceptual relationships.
  • Vocabulary mismatch: users phrase queries differently than documents are written.
  • Requires manual synonym lists or domain tuning for best results.
  • Performs poorly on long, natural-language questions where exact keywords are absent.
  • Language-specific: each language needs its own analyzer/stemmer.
# PostgreSQL full-text search example (sovereign, zero extra services) # Create a tsvector column and GIN index ALTER TABLE documents ADD COLUMN search_vector tsvector; UPDATE documents SET search_vector = to_tsvector('english', content); CREATE INDEX idx_search ON documents USING gin(search_vector); # Query with ranking SELECT id, content, ts_rank(search_vector, query) AS score FROM documents, plainto_tsquery('english', 'indemnification clause 4.2') AS query WHERE search_vector @@ query ORDER BY score DESC LIMIT 10;
When sparse alone fails: A user asks “What happens if the vendor is late delivering?” The contract clause uses the word “tardiness” and “delay remedies.” BM25 finds neither because there is zero word overlap. Dense retrieval handles this case effortlessly. This is why hybrid search exists.

3. Hybrid Search

Hybrid search combines dense and sparse retrieval to capture the strengths of both. It runs both retrieval pipelines in parallel, merges their results, and returns a unified ranking. This is the recommended default for production RAG—it costs slightly more compute but dramatically reduces the failure modes of either approach used alone.

User Query [Dense Retrieval] [Sparse Retrieval] Score Fusion Unified Top-K

How to Combine Dense + Sparse

Method 1: Reciprocal Rank Fusion (RRF) — Recommended

RRF is the simplest and most robust fusion method. Instead of trying to normalize incomparable score scales (cosine similarity is 0–1; BM25 is unbounded), RRF uses only the rank of each document in each result list. A document ranked #1 in dense results and #3 in sparse results gets a higher fused score than one ranked #5 and #2.

RRF(d) = Σr ∈ R 1 / (k + rankr(d))

Where R is the set of retrieval systems, rankr(d) is the rank of document d in system r’s results, and k is a constant (typically 60) that dampens the influence of very high ranks. RRF is parameter-light, robust, and requires no score calibration—making it the default in production systems.

# Reciprocal Rank Fusion implementation def reciprocal_rank_fusion(rankings: list[list[str]], k: int = 60) -> list[tuple[str, float]]: """ rankings: list of result lists, each ordered by relevance. Returns list of (doc_id, fused_score) sorted descending. """ scores = {} for ranking in rankings: for rank, doc_id in enumerate(ranking, start=1): scores[doc_id] = scores.get(doc_id, 0.0) + 1.0 / (k + rank) return sorted(scores.items(), key=lambda x: x[1], reverse=True) # Usage dense_results = ["doc_42", "doc_17", "doc_8", "doc_99", "doc_3"] sparse_results = ["doc_99", "doc_42", "doc_55", "doc_17", "doc_8"] fused = reciprocal_rank_fusion([dense_results, sparse_results]) # Result: doc_42 ranks high (good in both), doc_99 boosted by sparse #1

Method 2: Weighted Score Blending

Normalize each retriever’s scores to a 0–1 range, then take a weighted sum:

score(d) = α × normalize(dense_score(d)) + (1 − α) × normalize(sparse_score(d))

The weight α (typically 0.5–0.7) controls the balance. This method can outperform RRF when you have tuned the weights on your data, but it requires score normalization and is sensitive to the normalization method. Min-max and softmax normalization are common choices.

Method 3: Cascade (Two-Stage)

Use one retriever as a first-stage filter and the other as a refiner. For example, run sparse retrieval first to find all documents containing the exact key terms (high recall on identifiers), then run dense similarity only on that filtered set. Or invert it: use dense retrieval to find top-100 semantically relevant documents, then re-score those 100 with BM25 for exact-term precision. The cascade approach reduces the search space for the second stage, lowering latency at the cost of potentially missing documents the first stage filtered out.

Why Hybrid Is the Recommended Default

In production RAG systems, queries are unpredictable. Some users ask semantic questions (“What are the termination conditions?”), others search for exact identifiers (“Section 8.3”), and many do both in the same query. A pure-dense system fails on exact-match queries. A pure-sparse system fails on semantic queries. Hybrid search handles both cases with no additional engineering effort beyond running two retrievers and fusing results.

Empirically, hybrid search consistently outperforms either method alone on standard benchmarks (BEIR, MTEB) by 5–15% in nDCG. The compute overhead is modest—you run two lightweight retrievals in parallel instead of one—and the fusion step is trivially fast (sorting a few hundred items). For any deployment serving real users, the robustness gain is worth the cost.

Architecture tip: Run dense and sparse retrieval as separate microservices or separate threads, fuse in the application layer. This lets you tune, swap, or disable either retriever without redeploying the whole system. Qdrant and OpenSearch can run side by side with a simple fusion service in front.

4. Re-Ranking with Cross-Encoders

Re-ranking is a two-stage retrieval pattern. In stage one, a fast bi-encoder (the standard embedding model) retrieves a broad set of top-K candidates (e.g., 50–100 documents). In stage two, a more accurate but slower cross-encoder scores each query–document pair jointly and re-orders the candidates.

Query Bi-Encoder ANN (Top 100) Cross-Encoder Re-Rank (Top 10) Final Context

Bi-Encoder vs. Cross-Encoder

Property Bi-Encoder (Embedding Model) Cross-Encoder (Re-Ranker)
Input Query and document encoded separately Query and document concatenated, encoded together
Output Two vectors → similarity score Single relevance score directly
Speed Very fast (pre-computed doc vectors, one ANN lookup) Slow (must encode each pair at query time)
Accuracy Good (semantic recall) Excellent (sees full query–document interaction)
Use Case Stage 1: retrieve from millions of docs Stage 2: re-rank top 50–100 candidates
Pre-computable? Yes—document vectors stored in index No—score depends on the specific query

The cross-encoder sees both the query and document simultaneously in a single forward pass, allowing it to model fine-grained token-level interactions (attention between query tokens and document tokens). This makes it significantly more accurate than a bi-encoder, which must compress each into a single vector before comparison. The trade-off is that cross-encoders cannot pre-compute document representations—every query requires a fresh forward pass for every candidate document.

Recommended Cross-Encoder Models (Local / Sovereign)

ModelSizeNotesSovereign?
cross-encoder/ms-marco-MiniLM-L-6-v2 ~80MB Fast, lightweight, trained on MS MARCO. Good default. 384-dim backbone. Yes
cross-encoder/ms-marco-MiniLM-L-12-v2 ~120MB Slightly more accurate, still fast. Good middle ground. Yes
BAAI/bge-reranker-base ~1.1GB Strong general-purpose reranker. Supports multi-language. Yes
BAAI/bge-reranker-large ~2.6GB Higher accuracy, more memory. Recommended for high-value domains. Yes
BAAI/bge-reranker-v2-m3 ~2.3GB Multilingual, supports long documents. State of the art for open weights. Yes
jina-reranker-v2 ~1.2GB Strong on multilingual and code. Open weights available. Yes
Cohere Rerank Cloud API Excellent accuracy, but cloud-hosted. Sends queries and documents to Cohere servers. No
Sovereignty warning: Cohere Rerank, Voyage AI rerank, and OpenAI’s retrieval models are cloud APIs. They require sending your query and candidate documents to an external service. For legal, medical, or confidential business documents, this is a data sovereignty violation. Use a local cross-encoder (bge-reranker or ms-marco-MiniLM) instead. The accuracy gap is small (1–3% on most benchmarks) and shrinking as open models improve.
# Re-ranking with a local cross-encoder (sovereign) from sentence_transformers import CrossEncoder # Load a small, fast reranker reranker = CrossEncoder("cross-encoder/ms-marco-MiniLM-L-6-v2") # Stage 1: bi-encoder retrieved top-50 candidates (already done) candidates = retrieve_dense(query, top_k=50) # your ANN search # Stage 2: cross-encoder re-ranks the 50 candidates pairs = [(query, doc.text) for doc in candidates] scores = reranker.predict(pairs) # Sort by cross-encoder score, take top 10 ranked = sorted(zip(candidates, scores), key=lambda x: x[1], reverse=True) final_context = [doc for doc, score in ranked[:10]]
Latency tip: Cross-encoder re-ranking of 50–100 candidates with MiniLM takes 50–200ms on CPU, 10–50ms on a modest GPU. For most interactive RAG applications, this is well within acceptable latency budgets. If you need sub-100ms total pipeline time, reduce the candidate set to 20–30 documents.

5. Query Transformation

Users are bad at writing search queries. They ask vague questions, use different vocabulary than the documents, combine multiple questions into one, or phrase things in ways no embedding model expects. Query transformation rewrites the user’s query before retrieval to improve what gets found. The LLM does the rewriting; the retrieval system runs on the improved query.

5.1 Sub-Query Decomposition

Complex multi-part questions are decomposed into simpler sub-queries. Each sub-query is run independently through the retriever, and results are merged. This dramatically improves recall for questions that span multiple documents or topics.

# Example: decompose a complex question # User asks: "How does the termination clause interact with the # non-compete clause and what are the notice periods for each?" # LLM decomposes into sub-queries: sub_queries = [ "termination clause conditions and triggers", "non-compete clause restrictions and duration", "notice period for termination of agreement", "notice period for non-compete violation", ] # Retrieve for each sub-query, then merge and deduplicate all_results = [] for sq in sub_queries: all_results.extend(retrieve(sq, top_k=5)) # Deduplicate by document ID, keep highest score merged = deduplicate_and_merge(all_results)

5.2 Query Expansion (Synonym Injection)

The LLM or a thesaurus adds synonyms and related terms to the query before retrieval. This is especially helpful for sparse retrieval, which depends on exact term matching. A query for “automobile insurance” can be expanded to include “car coverage,” “vehicle policy,” and “motor protection.”

# Query expansion with an LLM expanded = llm.generate(f""" Rewrite the following search query by adding synonymous terms and related keywords. Return a single expanded query string. Original query: {user_query} Expanded query:""") # Then retrieve with the expanded query results = retrieve(expanded)

5.3 Back-Translation (Paraphrasing)

Translate the query to another language (e.g., English → French → English) using the LLM or a translation model. The round-trip translation produces a paraphrase that uses different vocabulary, which may match documents the original query missed. This is a form of data augmentation that exploits the non-determinism of translation to generate alternative phrasings.

# Back-translation for query paraphrase french = llm.translate(user_query, target="fr") paraphrase = llm.translate(french, target="en") # Retrieve with both original and paraphrase, merge results results_original = retrieve(user_query) results_paraphrase = retrieve(paraphrase) merged = merge_and_dedupe(results_original, results_paraphrase)

Query Transformation Summary

✓ Pros

  • Improves recall on vague or multi-part questions.
  • Bridges vocabulary gaps between users and documents.
  • Can be combined with any retrieval method (dense, sparse, hybrid).
  • Uses the LLM you already have—no additional models required.
  • Sub-query decomposition is particularly effective for multi-hop reasoning.

✗ Cons

  • Adds latency: each LLM call adds 200ms–2s before retrieval even begins.
  • Sub-query decomposition multiplies retrieval calls (N sub-queries = N searches).
  • LLM may generate poor or irrelevant expansions, degrading retrieval quality.
  • Back-translation can introduce errors if the translation model is weak.
  • Harder to debug—you must trace through the transformation to understand results.

6. Query Expansion with HyDE

HyDE (Hypothetical Document Embeddings) is a clever query expansion technique that leverages the fact that embedding models work better on document-length text than on short queries. Instead of embedding the user’s query directly, you ask the LLM to generate a hypothetical answer to the question, then embed that hypothetical answer and use it for retrieval.

User Query LLM Generates Hypothetical Answer Embed Hypothetical Answer ANN Search Top-K Real Docs

Why HyDE Works

Embedding models are trained on document-length text. A short query like “What are the termination penalties?” produces a sparse, low-information vector. A hypothetical answer like “The contract specifies that early termination incurs a penalty of 30% of the remaining contract value, payable within 30 days…” is rich, document-like text whose embedding lands much closer in vector space to real documents about termination penalties. The retrieval is more precise because the embedding space is being used the way it was designed to be used.

# HyDE: Hypothetical Document Embeddings def hyde_retrieve(user_query: str, top_k: int = 10): # Step 1: Generate a hypothetical answer with the LLM prompt = f"""Answer the following question with a detailed paragraph. Do not hedge. Write as if you are confident in the answer, even if you are not certain. Question: {user_query} Hypothetical answer:""" hypothetical_doc = llm.generate(prompt, max_tokens=200) # Step 2: Embed the hypothetical answer, NOT the query query_vector = embedding_model.encode(hypothetical_doc) # Step 3: Retrieve real documents nearest to the hypothetical results = vector_db.search(query_vector, top_k=top_k) # Step 4: Use the REAL documents as context (not the hypothetical) # The hypothetical was only for retrieval, not for the answer return results, hypothetical_doc # keep hypothetical for debugging
Important: The hypothetical document is used only for retrieval. The actual context sent to the LLM for answer generation must be the real retrieved documents. Never feed the hypothetical answer into the generation prompt—it may contain hallucinations that the LLM would then treat as factual context.

HyDE Summary

✓ Pros

  • Significantly improves retrieval for short or vague queries.
  • No training or fine-tuning required—works with any embedding model.
  • Bridges the query–document length mismatch in embedding space.
  • Particularly effective for factoid questions with specific answers.
  • Uses the LLM you already have deployed.

✗ Cons

  • Adds one LLM generation step before retrieval (200ms–3s latency).
  • If the LLM’s hypothetical answer is factually wrong, the embedding may retrieve irrelevant documents (the error compounds).
  • Less effective for queries where the user already uses precise vocabulary matching the documents.
  • Increases cost: each query now requires an LLM call before retrieval.
  • Not useful for pure keyword/ID lookups where exact matching matters.

7. Step-Back Prompting

Step-back prompting is a retrieval strategy that first asks a broader, more general question to gather background context, then asks the specific question with that context in hand. It addresses the problem where a too-specific query retrieves narrow documents that lack the surrounding context needed to fully understand or answer the question.

How It Works

Given a specific user question, the LLM generates a “step-back” question—a more general version that retrieves foundational context. Both questions are run through the retriever, and their results are merged.

# Step-back prompting example # User's specific question: specific_question = "What is the penalty for late delivery in the 2024 vendor agreement?" # LLM generates a step-back (broader) question: step_back_question = "What are all the penalty clauses in the vendor agreement?" # Retrieve for BOTH questions specific_results = retrieve(specific_question, top_k=5) broad_results = retrieve(step_back_question, top_k=5) # Merge: the broad results provide context, the specific results # provide the precise answer all_context = merge_and_dedupe(broad_results + specific_results) # Generate answer with full context answer = llm.generate(context=all_context, question=specific_question)

When Step-Back Helps

When Step-Back Doesn’t Help

Combine with sub-query decomposition: Step-back prompting and sub-query decomposition are complementary. Use step-back to get background context and sub-queries to handle multi-part questions. The merged result set provides both breadth and specificity.

8. Multi-Vector Retrieval

Single-vector retrieval embeds each document chunk into one vector. Multi-vector retrieval stores multiple representations per document—for example, a summary embedding, a full-text embedding, and embeddings of key passages. At query time, the system retrieves from multiple indexes (or multiple vector fields in one index) and merges the results. This captures different aspects of relevance that a single embedding cannot.

Common Multi-Vector Strategies

RepresentationWhat It CapturesBest Query Type
Document summary High-level topic, intent, scope Broad conceptual questions
Full document text All detail, specific facts Detailed, specific questions
Key passages / sections Important subsections, definitions Targeted lookups within a long document
Document title + metadata Document identity, type, category Filtering, navigation, classification
Hypothetical questions (DPR-style) Questions the document would answer Natural language questions (inverts the embedding direction)

The “Hypothetical Questions” Approach

An interesting multi-vector strategy: for each document, use the LLM to generate 3–5 questions that the document would answer. Embed those questions. At query time, compare the user’s query against these question embeddings. If the query is similar to a question the document answers, that document is likely relevant. This inverts the usual direction and can be very effective for Q&A corpora.

# Multi-vector retrieval with Qdrant (sovereign) # Store multiple vectors per document point from qdrant_client import QdrantClient from qdrant_client.models import PointStruct, VectorParams client = QdrantClient(host="localhost", port=6333) # Create a collection with named vectors client.create_collection( collection_name="documents_multi", vectors_config={ "summary": VectorParams(size=1024, distance="Cosine"), "full_text": VectorParams(size=1024, distance="Cosine"), "key_passages": VectorParams(size=1024, distance="Cosine"), } ) # Insert with multiple vectors per document client.upsert( collection_name="documents_multi", points=[PointStruct( id=doc_id, vector={ "summary": summary_embedding, "full_text": full_text_embedding, "key_passages": passage_embedding, }, payload={"text": doc_text, "title": doc_title} )] ) # Query: search across all vector fields, merge results results = client.search_batch( collection_name="documents_multi", requests=[ SearchRequest(vector=query_vec, using="summary", limit=20), SearchRequest(vector=query_vec, using="full_text", limit=20), SearchRequest(vector=query_vec, using="key_passages", limit=20), ] ) # Merge and deduplicate by document ID merged = merge_multi_vector_results(results)

Multi-Vector Retrieval Summary

✓ Pros

  • Captures multiple aspects of relevance per document.
  • Broad conceptual queries match summaries; specific queries match full text.
  • The hypothetical-questions variant is excellent for FAQ-style corpora.
  • Can be combined with hybrid search and re-ranking.
  • Each representation can use a different embedding model optimized for its purpose.

✗ Cons

  • Storage cost multiplies: 3 representations = 3x the vectors.
  • Ingestion is slower and more complex (generate summaries, extract key passages).
  • Query latency increases: multiple ANN searches per query.
  • Merge strategy (union, intersection, weighted) needs tuning.
  • More moving parts = more failure modes.

9. Contextual Compression

Contextual compression sits between retrieval and generation. After the retriever returns top-K documents, a compression step filters and extracts only the relevant portions before sending context to the LLM. This reduces token consumption, removes irrelevant noise that could distract the LLM, and improves answer quality by focusing the generation on only what matters.

Retrieved Docs Filter / Extract Relevant Parts Compressed Context LLM Generation

Compression Approaches

1. LLM-Based Extraction

Send each retrieved document to the LLM with the instruction: “Extract only the parts relevant to this question.” The LLM returns the relevant sentences or paragraphs. This is accurate but expensive—one LLM call per retrieved document, before the main generation call.

2. Embedding-Based Filtering

Split each retrieved document into smaller units (sentences or paragraphs). Compute the embedding similarity of each unit to the query. Keep only units above a similarity threshold. No LLM call needed—just additional embedding comparisons. Faster but less nuanced than LLM extraction.

3. Document-Level Filtering

Use a lightweight classifier or the cross-encoder re-ranker score to drop entire documents below a relevance threshold. Simple, fast, and effective when the retriever returns a mix of relevant and irrelevant documents.

Tools and Implementations

ToolApproachSovereign?Notes
LangChain ContextualCompressionRetriever Framework wrapper Yes Wraps any retriever with a compression step. Supports multiple compressor backends. Works with local LLMs (Ollama, vLLM).
LangChain LLMChainExtractor LLM extraction Yes Sends each document to the LLM for relevant-part extraction. Configure with a local LLM endpoint for sovereignty.
LangChain EmbeddingsFilter Embedding similarity filter Yes No LLM calls—filters by embedding similarity threshold. Fast and cheap.
Custom pipeline Your own logic Yes Split, score, filter. Full control over the compression strategy.
# LangChain ContextualCompressionRetriever with a local LLM from langchain.retrievers import ContextualCompressionRetriever from langchain.retrievers.document_compressors import LLMChainExtractor from langchain_community.llms import Ollama # local LLM, sovereign # Use a local model (e.g., Llama 3, Qwen, Mistral via Ollama) local_llm = Ollama(model="llama3:8b", base_url="http://localhost:11434") compressor = LLMChainExtractor(llm=local_llm) compression_retriever = ContextualCompressionRetriever( base_compressor=compressor, base_retriever=your_vector_retriever, # Qdrant, pgvector, etc. ) # Retrieve: returns only the relevant compressed parts compressed_docs = compression_retriever.get_relevant_documents(user_query)
Cost warning: LLM-based contextual compression adds one LLM call per retrieved document. If you retrieve 20 documents, that’s 20 LLM calls before the main generation. For high-throughput systems, use embedding-based filtering instead, or limit compression to the top 5–10 documents after re-ranking.

10. Citation and Source Tracking

In legal, medical, and business RAG, the answer is not enough—you must be able to trace every claim back to the specific document and passage it came from. Citation and source tracking is the practice of maintaining a chain of custody from retrieved documents through to every sentence in the generated answer.

Why Citation Matters

Citation Strategies

Strategy 1: Chunk ID Tagging (Recommended)

Assign a unique ID to every chunk at ingestion time. When the retriever returns chunks, include their IDs in the context sent to the LLM. Instruct the LLM to cite the chunk ID after each claim. This is the most reliable approach because the LLM is working with explicit, structured identifiers.

# Chunk ID tagging for citation # At ingestion: each chunk gets a stable, unique ID chunk = { "id": "contract_2024_v2_chunk_47", "text": "Section 8.3: Either party may terminate this agreement with 30 days written notice...", "source_doc": "vendor_agreement_2024.pdf", "page": 12, "section": "8.3", "char_start": 4523, "char_end": 4781, } # At generation: pass chunks with IDs in context context_for_llm = "\n\n".join([ f"[{chunk['id']}] {chunk['text']}" for chunk in retrieved_chunks ]) prompt = f"""Answer the question using ONLY the context below. After each factual claim, cite the source ID in square brackets. Context: {context_for_llm} Question: {user_query} Answer (cite sources as [chunk_id]):""" answer = llm.generate(prompt) # Example output: "The contract requires 30 days written notice # for termination [contract_2024_v2_chunk_47]."

Strategy 2: Post-Hoc Sentence Matching

After the LLM generates the answer, match each sentence in the answer back to the retrieved chunks using embedding similarity. The closest chunk is the cited source. This does not require the LLM to cooperate with citation instructions, but it is less precise—the matching is heuristic and can be wrong for claims that synthesize multiple sources.

Strategy 3: Span-Level Highlighting

For maximum precision, track which character spans in the original document each claim maps to. This requires the LLM or a separate model to identify the exact text span, but it enables UI features like “click to see the highlighted passage.” This is the gold standard for legal and medical applications.

What to Store for Full Traceability

FieldPurpose
chunk_idUnique identifier for the chunk
source_docOriginal file name or document ID
page_numberPage in the source document (for PDFs)
section / headingSection number or heading for structural context
char_start / char_endCharacter offsets for span-level highlighting
retrieval_scoreScore from the retriever (for debugging relevance)
rerank_scoreScore from the cross-encoder (if re-ranking was used)
retrieval_methodWhich method found this chunk (dense, sparse, hybrid)
Production tip: Store the full retrieval metadata (scores, method, rank) alongside each citation. When a user questions an answer, you can show the retrieval provenance: “This claim came from vendor_agreement_2024.pdf page 12 section 8.3, retrieved via hybrid search rank #1 with rerank score 0.94.” This level of transparency is what separates a toy RAG demo from a production system.

11. Comparison Table

The following table summarizes every retrieval method across the dimensions that matter for production deployment. “Sovereign-compatible” means the method can be fully deployed on-premise with no external API calls.

Method Accuracy Latency Complexity Best For Sovereign?
Dense Vector High (semantic) Low (10–50ms ANN) Low–Medium General-purpose semantic search, natural language Q&A Yes
Sparse (BM25) Medium (exact match) Very Low (5–20ms) Low Exact term matching: IDs, names, code, legal clause numbers Yes
Hybrid Search Very High Low (20–60ms parallel) Medium Production default: mixed query types, unknown user behavior Yes
Cross-Encoder Re-Ranking Very High (top-K precision) Medium (50–200ms on CPU) Medium High-stakes domains: legal, medical, financial Yes
Query Transformation High (improves base retrieval) Medium–High (adds LLM call) Medium Vague queries, multi-part questions, vocabulary gaps Yes
HyDE High (for short queries) Medium–High (adds LLM call) Medium Factoid questions, short user queries, domain-specific corpora Yes
Step-Back Prompting High (adds context) Medium (adds retrieval + optional LLM call) Medium Questions needing background context, multi-hop reasoning Yes
Multi-Vector Very High (multi-aspect) Medium (multiple searches) High Long documents, diverse query types, high-value corpora Yes
Contextual Compression Improves precision of context High (LLM-based) / Low (embedding-based) Medium Reducing token usage, removing noise, long retrieved documents Yes
Citation Tracking N/A (traceability, not retrieval) Low (metadata passthrough) Medium Legal, medical, compliance, audit trail Yes

Latency Budget by Pipeline Stage

StageTypical LatencyBottleneck
Query embedding5–20msEmbedding model forward pass
Dense ANN search5–30msVector DB, index size
Sparse BM25 search5–20msInverted index lookup
Hybrid fusion (RRF)1–5msSorting a few hundred items
Cross-encoder re-ranking (50 docs)50–200ms (CPU), 10–50ms (GPU)Cross-encoder forward passes
Query transformation (LLM)200ms–3sLLM generation latency
HyDE (LLM generation)300ms–3sLLM generation latency
Contextual compression (LLM, 10 docs)500ms–5sN LLM calls for N documents
Final LLM generation1–10sOutput length, model size

12. Practical Recommendations

The right retrieval method depends on your data, your users, your latency budget, and your accuracy requirements. Below are concrete recommendations for common scenarios, all assuming sovereign, on-premise deployment.

Small Document Set (<1,000 docs)

Recommended: Hybrid search with pgvector + PostgreSQL full-text search.

No separate services needed—PostgreSQL handles both vectors and keywords in one engine. Add a lightweight cross-encoder (ms-marco-MiniLM-L-6-v2) for re-ranking if accuracy matters.

pgvector PostgreSQL FTS RRF fusion MiniLM reranker

Large Scale (>1M documents)

Recommended: Qdrant (dense) + OpenSearch (sparse) with RRF fusion.

Dedicated engines scale better than overloaded PostgreSQL. Use HNSW indexes in Qdrant with quantization for memory efficiency. Deploy cross-encoder re-ranking only on top-50 candidates to keep latency manageable. Consider multi-vector if query diversity is high.

Qdrant OpenSearch bge-reranker-large HNSW + quantization

Medical RAG

Recommended: Hybrid search + cross-encoder re-ranking + citation tracking.

Accuracy is paramount. Use a domain-specific embedding model if available (or fine-tune a general one on medical literature). Re-rank with bge-reranker-large. Implement full citation tracking with chunk IDs, source document, page, and section. Use contextual compression to extract only relevant clinical passages. Never use cloud APIs—patient data must not leave the network.

Hybrid search bge-reranker-large Citation tracking Contextual compression

Legal RAG

Recommended: Hybrid search + cross-encoder re-ranking + HyDE + citation tracking.

Legal queries are often phrased as questions (good for HyDE). Exact clause numbers and section references need sparse retrieval. Long contracts benefit from multi-vector (summary + full text + key passages). Re-rank with cross-encoder. Full citation tracking with section numbers, page numbers, and character spans is non-negotiable.

Hybrid search HyDE Multi-vector Span-level citations

Code RAG

Recommended: Sparse-heavy hybrid + code-specific embeddings.

Code retrieval demands exact matching for function names, variable names, and API calls. Weight sparse retrieval higher (α = 0.3 for dense, 0.7 for sparse). Use a code-aware embedding model (jina-embeddings-v2-code or fine-tuned BGE). Consider chunking by function/class rather than fixed-size tokens. Re-rank with a code-trained cross-encoder if available.

BM25-weighted hybrid Code embeddings Function-level chunking

Multi-Lingual RAG

Recommended: Hybrid search with bge-m3 + cross-encoder re-ranking.

BGE-M3 supports 100+ languages and provides dense, sparse, and multi-vector outputs from a single model—reducing infrastructure. Use bge-reranker-v2-m3 for multilingual re-ranking. For query transformation, back-translation can help bridge cross-lingual vocabulary gaps. Ensure your sparse retriever has appropriate analyzers/stemmers for each target language.

bge-m3 bge-reranker-v2-m3 Back-translation Language-specific analyzers

High-Throughput / Low-Latency

Recommended: Dense-only with HNSW + quantization, no LLM-based steps.

Skip query transformation, HyDE, and LLM-based contextual compression—they add LLM latency. Use dense retrieval with Qdrant HNSW + scalar quantization for sub-20ms retrieval. Optional lightweight cross-encoder on top-20 candidates if you can afford 50ms. Keep the pipeline short.

Qdrant HNSW Scalar quantization No LLM pre-processing

Maximum Accuracy (Cost Agnostic)

Recommended: Full pipeline: query transformation → hybrid search → multi-vector → cross-encoder re-ranking → contextual compression → citation tracking.

Every technique in this guide, stacked. Latency will be 3–10 seconds per query, but accuracy will be the highest achievable. Suitable for expert-system deployments where correctness matters more than speed (e.g., legal research, medical diagnosis support).

Full pipeline bge-reranker-large LLM compression Full citations

Default Production Architecture

If you are building a new RAG system today and want the best balance of accuracy, latency, and simplicity, here is the recommended default:

Query Embed (bge-large) + BM25 (OpenSearch) RRF Fusion Top-50 Cross-Encoder Rerank Top-10 LLM + Citations

This pipeline delivers excellent accuracy with reasonable latency (total retrieval + re-ranking in under 300ms on commodity hardware), uses only sovereign-compatible components, and provides full citation traceability. Add query transformation or HyDE if your users tend to ask vague or short questions. Add multi-vector if your documents are long and structurally diverse.

Start simple, add complexity when needed: Begin with hybrid search + cross-encoder re-ranking. Measure accuracy and latency on your actual data. Add query transformation, HyDE, multi-vector, or contextual compression only when you identify a specific failure mode they address. Every additional stage adds latency, cost, and debugging complexity. The best pipeline is the simplest one that meets your accuracy requirements.

Conclusion

Retrieval method selection is the most impactful architectural decision in a RAG system. Dense vector retrieval handles semantics; sparse retrieval handles exact matches; hybrid search combines both. Cross-encoder re-ranking sharpens precision. Query transformation, HyDE, and step-back prompting improve what gets retrieved. Multi-vector captures multiple aspects of relevance. Contextual compression cleans up what reaches the LLM. Citation tracking ensures accountability.

For sovereign, on-premise deployments, every technique described here has a fully local implementation. The only tools to avoid are cloud-hosted retrieval and re-ranking APIs (Pinecone, Cohere, Voyage) that require sending your data to external servers. The open-source ecosystem now matches or exceeds these cloud services in quality, making fully sovereign RAG not just possible but practical.

Start with hybrid search and cross-encoder re-ranking. Add stages as your accuracy requirements demand. Measure everything. Cite everything. Build for the users who will rely on these answers in high-stakes decisions.