RAG & Search
dense retrieval
hybrid search

Retrieval (Dense + Hybrid)

First-stage retrieval over document corpora — dense, sparse (BM25), or hybrid. Foundation for all RAG pipelines.

Capability notes

Retrieval pipelines in 2026 operate across three axes: **dense** (embedding-based semantic similarity), **sparse** (lexical matching via BM25 and its neural variants), and **hybrid** (fusion of dense + sparse with learned weighting). Each has a distinct accuracy envelope determined by the query-document relationship. Dense retrieval uses embedding models ([BGE-M3](/models/bge-m3), GTE-Qwen2-7B, E5-Mistral-7B) to encode queries and documents into the same vector space, then scores documents by cosine similarity. Dense excels at semantic matching — "car repair" retrieves "automobile maintenance" — and achieves 85-92% recall@10 on BEIR (Benchmarking IR). Sparse retrieval (BM25, SPLADE) matches exact tokens with TF-IDF weighting. BM25 excels at rare-term matching — a query containing "CRISPR-Cas9" will find documents with that exact term even if the embedding model has never seen it in training. Sparse achieves 70-80% recall@10 on BEIR but is dramatically better (15-25% gap) for technical/medical/legal queries with domain-specific terminology. Hybrid retrieval fuses dense and sparse scores: `final_score = α × dense_score + (1-α) × sparse_score`. The α weight is tuned per-domain (α=0.7-0.8 for general queries, α=0.4-0.6 for technical queries). Hybrid achieves 90-95% recall@10 on BEIR, outperforming either approach alone by 5-10%. The cost is running two separate retrieval systems + a fusion step. MTEB (Massive Text Embedding Benchmark) scores: [BGE-M3](/models/bge-m3) leads open-weight at 64.2 overall (1024-dim). Larger dimension models (E5-Mistral-7B at 4096-dim) score 65.1 but cost 4× the storage and 2× the computation. The 64-dim vs 1024-dim tradeoff: recall@10 drops ~5% going from 1024-dim to 64-dim, but storage drops 94% and query latency drops 90%. For 10M+ document indexes, dimension reduction is mandatory. A critical operational insight: **retrieval quality is bounded by embedding model quality, not vector database choice.** Qdrant, pgvector, Weaviate, and Milvus all implement HNSW/IVF indexes with near-identical recall. The 10-20% recall gap comes from the embedding model, not the index. Invest in better embeddings before optimizing the vector database.

If you just want to try this

Lowest-friction path to a working setup.

Start with [BGE-M3](/models/bge-m3) via SentenceTransformers — the most capable open-weight embedding model at 1024 dimensions. Pair it with [pgvector](/tools/pgvector) (PostgreSQL extension) for zero-infra vector search: ```bash pip install sentence-transformers pgvector psycopg2-binary ``` ```python from sentence_transformers import SentenceTransformer import psycopg2, json model = SentenceTransformer("BAAI/bge-m3") # Create table with vector column conn = psycopg2.connect("postgresql://localhost/mydb") conn.execute("CREATE EXTENSION IF NOT EXISTS vector") conn.execute("CREATE TABLE docs (id SERIAL, text TEXT, embedding vector(1024))") # Index documents docs = ["Paris is the capital of France.", "The Eiffel Tower is in Paris.", ...] embeddings = model.encode(docs, normalize_embeddings=True) for doc, emb in zip(docs, embeddings): conn.execute("INSERT INTO docs (text, embedding) VALUES (%s, %s)", (doc, emb)) # Search query_emb = model.encode("What city has the Eiffel Tower?", normalize_embeddings=True) results = conn.execute( "SELECT text, 1 - (embedding <=> %s) AS similarity FROM docs ORDER BY embedding <=> %s LIMIT 10", (query_emb, query_emb) ).fetchall() ``` This gives you semantic search in 20 lines of Python. For 100K documents: pgvector with IVFFlat index achieves <10ms query latency on a [RTX 3060 12GB](/hardware/rtx-3060-12gb) (GPU for encoding only). For 1M+ documents: switch to HNSW index for O(log N) search. Add BM25 for hybrid retrieval: `pip install rank-bm25`. Compute BM25 scores, compute dense scores, fuse with `α × dense + (1-α) × sparse`. Start with α=0.7. Tune α by measuring recall@10 on a labeled query set — whichever α maximizes recall is correct.

For production deployment

Operator-grade recommendation.

Production retrieval operates as a tiered pipeline: query understanding → candidate generation → reranking → final results. **Query understanding.** Raw user query → query rewriting (LLM expands ambiguous queries: "best laptop" → "best laptop for programming with 16GB+ RAM and SSD storage 2026"), query decomposition (break multi-aspect queries into sub-queries: "compare Tesla and BYD financials" → ["Tesla revenue 2025", "BYD revenue 2025", "Tesla profit margin", "BYD profit margin"]), and query classification (route to domain-specific index: product queries → product index, support queries → documentation index). Query understanding adds 50-200ms latency but improves recall@10 by 15-25%. Use [Qwen 3 32B](/models/qwen-3-32b) for query rewriting — small, fast, sufficient. **Candidate generation (retrieval).** Hybrid retrieval with [BGE-M3](/models/bge-m3) (dense) + BM25 (sparse) → top K=100-200 candidates. Why K=100-200 not K=10? Because the reranker needs a large candidate pool to find the truly best documents. A K=10 candidate pool misses 25-35% of relevant documents that the reranker would have surfaced. K=200 achieves 95%+ recall. At K=200, the cost is 200 reranker calls per query — amortized by batching and caching. **Reranking.** [BGE Reranker V2 M3](/models/bge-reranker-v2-m3) (cross-encoder) scores each candidate pair (query, document) with full attention — dramatically more accurate than the bi-encoder similarity used in retrieval. Reranker improves NDCG@10 by 10-20% over raw retrieval scores. The cost: 200 × reranker latency. Optimize: (1) batch reranking — score 32-64 candidates in one forward pass, (2) two-stage reranking — lightweight ranker (monoT5-3B) reduces 200 → 50 candidates, then [BGE Reranker V2 M3](/models/bge-reranker-v2-m3) scores 50 → 10. This 2-stage approach reduces reranker cost 75% at <1% recall loss. **Index maintenance.** Vector index staleness vs cost tradeoff. HNSW index with `ef_construction=200, M=16`: building takes O(N log N) + 2× storage for the graph. For 10M vectors at 1024-dim (40 GB raw): index is ~80 GB. Rebuild schedule: if <1% of documents change daily, rebuild weekly. If 5%+ change daily, use IVFFlat (faster build, slightly lower recall) for daily rebuilds. Streaming updates (insert/delete single vectors) work on both HNSW and IVFFlat but degrade recall 1-3% after 10K+ uncommitted updates — force index rebuild after 10K mutations. **Multi-index architecture.** Separate indexes for: documents (10K-10M), chunks (100K-100M for RAG), entities (persons, organizations), and code (function snippets). Query routing by classification routes to the correct index. Multi-index retrieval (fuse results from 2-3 indexes) improves recall for cross-domain queries.

What breaks

Failure modes operators see in the wild.

- **Embedding model vocabulary gap.** Symptom: query contains domain-specific terms ("PAGCL," "PFAS remediation," "ICD-10-CM E11.65") and retrieval returns zero relevant results even though documents containing those exact terms exist in the corpus. Cause: the embedding model's tokenizer splits rare terms into subword units that lose semantic meaning during encoding. The dense embedding for "PAGCL" is noise. Mitigation: hybrid retrieval — BM25 catches exact token matches that dense misses. Domain-adaptive pre-training (DAPT) of the embedding model on the target corpus. For extreme vocabulary gap, fall back entirely to BM25 for queries containing >50% out-of-vocabulary tokens. - **Document length bias.** Symptom: short documents (tweets, titles) are systematically scored lower than long documents (articles, books) by dense retrieval, even when the short document is more relevant. Cause: dense models are trained on passage-level data (~100-200 tokens). Short documents produce embeddings with less "signal" — their vector magnitude is lower on average. Mitigation: normalize embeddings to unit length (already done by most models). Apply length-normalized scoring: `score = similarity / (1 + log(doc_length))`. For extreme length variance (tweets vs books in same index), use separate indexes per document length class with query-time fusion. - **Reranker latency bottleneck.** Symptom: retrieval (dense + sparse) completes in 10ms, but reranking 200 candidates takes 500ms — 50× the retrieval time. Users experience 500ms+ latency. Cause: cross-encoder rerankers process each query-document pair with full transformer attention — O(K) forward passes, each as expensive as encoding. Mitigation: batched reranking (32-64 pairs per batch), two-stage reranking, reranker distillation (distill cross-encoder into a bi-encoder using KL-divergence loss), or GPU with high batch throughput ([RTX 4090](/hardware/rtx-4090) handles 64-pair batches at 50ms).

Hardware guidance

**Hobbyist ($300-$1,500).** Retrieval splits into two hardware profiles: indexing (batch, GPU-accelerated) and querying (real-time, can be CPU). [RTX 3060 12GB](/hardware/rtx-3060-12gb) encodes ~500 docs/sec with [BGE-M3](/models/bge-m3) at 1024-dim. CPU-only (Intel i7-13700K) encodes ~60 docs/sec — 35× slower but functional for <100K document indexes. Query-time: single query encoding takes 5-10ms on CPU — acceptable for interactive search. Reranking with [BGE Reranker V2 M3](/models/bge-reranker-v2-m3) requires GPU — CPU reranking of 200 candidates takes 2-5 seconds (unacceptable). [MacBook Pro 16" M4 Max](/hardware/macbook-pro-16-m4-max) 36GB handles dense encoding + reranking on SoC via [MLX LM](/tools/mlx-lm). **SMB ($2,000-$4,000).** [RTX 4090](/hardware/rtx-4090) 24GB encodes ~1000 docs/sec. Handles index building for 10M+ documents in hours. Query-time: serves embedding encoding for 100-500 queries/sec + reranking for 10-50 queries/sec when serving both dense + reranker on the same GPU. [RTX 5090](/hardware/rtx-5090) 32GB adds memory headroom for running both the embedding model and reranker simultaneously. **Team serving ($6,000-$15,000).** [NVIDIA L40S](/hardware/nvidia-l40s) 48GB runs embedding model + reranker model simultaneously with [Text Embeddings Inference](/tools/text-embeddings-inference) (TEI). 48GB fits [BGE-M3](/models/bge-m3) FP16 (~1.1 GB) + [BGE Reranker V2 M3](/models/bge-reranker-v2-m3) FP16 (~1.1 GB) with massive headroom for batching. Serves 1000+ queries/sec for embedding, 100+ queries/sec for reranking. For 100 QPS production search with 200-candidate reranking: ~20% GPU utilization. **Enterprise ($25,000+).** [NVIDIA H100 PCIe](/hardware/nvidia-h100-pcie) 80GB. Overkill for embedding/reranking — these are small models (<2B parameters) that don't benefit from H100-class compute. The H100 is justified only if the retrieval pipeline includes LLM-based query rewriting ([Qwen 3 32B](/models/qwen-3-32b)) or LLM-based passage summarization in the same serving infrastructure. Otherwise, [NVIDIA L40S](/hardware/nvidia-l40s) is the ceiling for retrieval hardware.

Runtime guidance

**If building a dense retrieval pipeline** → [BGE-M3](/models/bge-m3) via SentenceTransformers for encoding + [Text Embeddings Inference](/tools/text-embeddings-inference) (TEI) for production serving. TEI provides: dynamic batching, token-based truncation, Flash Attention, and OpenAI-compatible API. Setup: `docker run -p 8080:80 -e MODEL_ID=BAAI/bge-m3 ghcr.io/huggingface/text-embeddings-inference`. TEI handles 500-2000 queries/sec on [RTX 4090](/hardware/rtx-4090) depending on batch size. **If building a hybrid retrieval pipeline** → BM25 via Elasticsearch or Lucene + dense via [BGE-M3](/models/bge-m3) + [Text Embeddings Inference](/tools/text-embeddings-inference). Elasticsearch 8.x has built-in dense vector support (HNSW) + BM25 in the same query — no need for separate vector DB. Query: `{ "query": { "hybrid": { "queries": [ {"match": {"text": query}}, {"knn": {"embedding": {"vector": query_emb, "k": 200}}} ] } }`. Elasticsearch's hybrid query fuses BM25 + dense scores automatically. This is the simplest path to hybrid retrieval without custom fusion code. **If using a dedicated vector database** → [Qdrant](/tools/qdrant) for pure performance (Rust, 1-5ms queries at 100M scale), [Weaviate](/tools/weaviate) for hybrid-native (BM25 + dense built-in, GraphQL API, multi-tenancy), [pgvector](/tools/pgvector) for simplicity (PostgreSQL, no additional infrastructure). Migrate from pgvector to Qdrant/Weaviate when: (1) query latency exceeds 50ms at p99, (2) index rebuilds take longer than your update SLA, (3) you need multi-tenant isolation at scale. **If reranking is the bottleneck** → [BGE Reranker V2 M3](/models/bge-reranker-v2-m3) via [Text Embeddings Inference](/tools/text-embeddings-inference) (TEI also supports reranking models). Batch 32-64 candidate pairs per forward pass. Two-stage reranking: stage 1 (lightweight ranker: monoT5-3B or distilled cross-encoder) reduces 200 → 50, stage 2 (BGE Reranker) reduces 50 → 10. Stage 1 runs on CPU; stage 2 runs on GPU. This reduces GPU reranking cost 75%. **If on Apple Silicon** → [MLX LM](/tools/mlx-lm) for embedding model encoding. MLX supports BGE-M3 natively. For production: [LM Studio](/tools/lm-studio) exposes embedding endpoint at `http://localhost:1234/v1/embeddings`. For reranking: use CPU-based reranking on Apple Silicon — the M4 Max's ANE (Neural Engine) accelerates transformer inference, achieving 50-100 candidate pairs/sec.

Setup walkthrough

  1. Install Ollama → ollama pull nomic-embed-text (~274 MB — fast embeddings).
  2. pip install chromadb (lightweight embedded vector DB).
  3. Full retrieval pipeline:
import chromadb, requests, ollama

# Index documents
client = chromadb.Client()
collection = client.create_collection("docs")
for i, doc in enumerate(documents):
    emb = ollama.embed(model="nomic-embed-text", input=doc)["embeddings"][0]
    collection.add(documents=[doc], embeddings=[emb], ids=[f"doc_{i}"])

# Retrieve (dense + sparse hybrid)
query = "How do I set up local speech-to-text?"
query_emb = ollama.embed(model="nomic-embed-text", input=query)["embeddings"][0]
results = collection.query(query_embeddings=[query_emb], n_results=20)

# Rerank with LLM
rerank_prompt = f"Query: {query}\n\nDocuments:\n"
for i, doc in enumerate(results["documents"][0]):
    rerank_prompt += f"{i+1}. {doc[:200]}\n"
rerank_prompt += "\nRank the top 5 most relevant documents by number."
ranked_docs = ollama.generate(model="llama3.1:8b", prompt=rerank_prompt)["response"]
print(ranked_docs)
  1. First retrieval in <100ms for 10K documents. End-to-end (embed + retrieve + rerank + generate answer) in 2-5 seconds.

The cheap setup

Retrieval is embarrassingly lightweight. Nomic Embed Text + ChromaDB runs on CPU at 500-1,000 documents/second for indexing, sub-100ms for retrieval on 100K documents. Any $300 laptop handles retrieval for up to 1M documents. For the full pipeline (retrieve + rerank + generate answer), add a GTX 1060 6 GB (~$60) for the LLM answer generation at 40-60 tok/s. Total: ~$360. Retrieval is the most hardware-accessible RAG component — the vector DB is the bottleneck (I/O), not the GPU.

The serious setup

Used RTX 3060 12 GB (~$200-250, see /hardware/rtx-3060-12gb) handles enterprise retrieval: BGE-M3 embeddings at 1,000+ docs/second, Qdrant for production vector DB, BGE Reranker V2 M3 for precision, Llama 3.1 8B for answer generation. For a production RAG system serving 100+ concurrent users with 10M+ documents: the retrieval layer (embedding search) runs on CPU, the reranker and generator share one GPU. Total build: ~$900-1,200. The retrieval bottleneck is almost always the vector database (index build time, query latency at scale), not the embedding model speed.

Common beginner mistake

The mistake: Embedding every single word in a document individually (word-level embeddings), or conversely, embedding entire 50-page PDFs as one giant embedding. Why it fails: Word-level embeddings → you retrieve fragments that are meaningless without surrounding context ("the revenue was" isn't helpful without the number). Document-level embeddings → you retrieve a 50-page PDF when the answer is on page 43 — the LLM has to read 49 irrelevant pages to find it. The fix: Chunk documents into semantic units. Rule of thumb: 256-512 tokens per chunk with 10-20% overlap between chunks. This gives each chunk enough context to be meaningful, while keeping it focused enough that retrieval is precise. For structured documents (legal contracts, research papers), chunk by section (each section = one chunk). The chunk size is the single most important retrieval parameter — test 3-5 different sizes on your specific documents.

Recommended setup for retrieval (dense + hybrid)

Recommended runtimes

Browse all tools for runtimes that fit this workload.

Reality check

Local AI workloads have real hardware constraints that vary by task type. VRAM ceiling decides what model fits; bandwidth decides decode speed; compute decides prefill speed. Pick the GPU tier that fits your actual workload, not the spec sheet.

Common mistakes

  • Buying for spec-sheet VRAM without modeling KV cache + activation overhead
  • Underestimating quantization quality loss below Q4
  • Skipping flash-attention support (real perf gap on long context)
  • Ignoring sustained-load thermals (laptops thermal-throttle within 30 min)

What breaks first

The errors most operators hit when running retrieval (dense + hybrid) locally. Each links to a diagnose+fix walkthrough.

Before you buy

Verify your specific hardware can handle retrieval (dense + hybrid) before committing money.

Hardware buying guidance for Retrieval (Dense + Hybrid)

RAG workflows mix embedding throughput, long-context inference, and reasonable VRAM headroom. The guides below cover the buyer decision honestly.

Specialized buyer guides
Updated 2026 roundup