RAG & Search
meaning search
vector search

Semantic Search

Search by meaning rather than keyword match. Powered by embedding models + vector databases.

Capability notes

End-to-end semantic search in 2026 — from user query to ranked results — combines query understanding, embedding-based retrieval, reranking, and optional generative answer synthesis. Unlike keyword search (which matches tokens), semantic search matches intent: "How do I fix a leaking faucet?" retrieves plumbing repair guides even if they use the word "tap" instead of "faucet." The pipeline: (1) **Query analysis** — classify query type (factoid, navigational, comparative, procedural), expand abbreviations, resolve pronouns, identify entities. (2) **Embedding encoding** — [BGE-M3](/models/bge-m3) encodes query to 1024-dim vector. (3) **Vector search** — HNSW/IVF index returns top 100-200 candidates by cosine similarity. (4) **Reranking** — [BGE Reranker V2 M3](/models/bge-reranker-v2-m3) cross-encoder scores each candidate with full query-document attention. (5) **Optional generation** — LLM ([Llama 3.3 70B](/models/llama-3-3-70b) or [DeepSeek V4](/models/deepseek-v4)) synthesizes answer from top 3-5 documents with citations. Semantic search quality is measured by NDCG@10 (Normalized Discounted Cumulative Gain). State of the art for open-weight stacks: NDCG@10 of 0.72-0.78 on BEIR (general domain), 0.65-0.72 on domain-specific corpora (medical, legal, scientific). Closed-source (Google, Cohere) pushes 0.80-0.85. The 5-10 point gap comes primarily from: (1) larger embedding models (7B+ params) that open-weight tools rarely deploy due to cost, (2) query understanding models trained on search-click data, and (3) personalization signals unavailable in open-weight. Domain adaptation is the highest-ROI improvement. General-purpose [BGE-M3](/models/bge-m3) achieving NDCG@10 of 0.55 on medical queries can reach 0.70 after fine-tuning on 10K in-domain query-document pairs — a 27% improvement from modest data investment. Domain adaptation matters more than switching embedding models or vector databases. Reranking is the critical quality step — it's where 60-70% of the quality gain over raw vector search happens. A pipeline that skips reranking (embedding → vector search → results) achieves NDCG@10 of 0.50-0.55. Adding reranking pushes to 0.70-0.78. The reranker sees the full query-document text with cross-attention, understanding relationships that the bi-encoder embedding model missed.

If you just want to try this

Lowest-friction path to a working setup.

Build a semantic search pipeline in 3 steps, each adding measurable quality: **Step 1: Dense retrieval only (15 minutes).** Use [BGE-M3](/models/bge-m3) with [Ollama](/tools/ollama) or SentenceTransformers for embedding, and [pgvector](/tools/pgvector) for vector search. Documents → encode → store vectors. Query → encode → search by cosine similarity → top 10. This works immediately and gives you a baseline. NDCG@10 will be ~0.50-0.55 — functional but not great. **Step 2: Add BM25 hybrid (30 minutes).** Add BM25 sparse retrieval via `rank-bm25` Python library. Query both systems, fuse scores with α=0.7 for dense, (1-α) for sparse. NDCG@10 improves to ~0.58-0.63. You'll notice that technical/rare-term queries improve dramatically; general queries improve modestly. **Step 3: Add reranking (1 hour).** Add [BGE Reranker V2 M3](/models/bge-reranker-v2-m3) to score the top 100 candidates from hybrid retrieval. Rerank top 100 → top 10. NDCG@10 jumps to ~0.68-0.74 — the biggest quality leap. You'll notice that results feel "smarter" — the top document is usually the right one, whereas before it was somewhere in the top 10. ```bash pip install sentence-transformers pgvector rank-bm25 FlagEmbedding ollama pull bge-m3 ``` Don't skip to LLM answer generation too early. A search pipeline with good retrieval + reranking and no LLM is more useful than a pipeline with LLM generation and mediocre retrieval. The LLM can only generate answers from what retrieval finds — garbage in, garbage out. For non-English search, [Aya Expanse 32B](/models/aya-expanse-32b) provides balanced multilingual embeddings. [BGE-M3](/models/bge-m3) supports 100+ languages but with 10-15% lower quality on non-English queries. If >30% of your queries are non-English, use a multilingual-first embedding model.

For production deployment

Operator-grade recommendation.

Production semantic search operates as a latency-budgeted pipeline where each stage has a strict time allocation. **Latency budget (target: <200ms p95).** Query understanding: 20ms (LLM call for query rewriting — use [Qwen 3 32B](/models/qwen-3-32b) with <100 token output). Embedding encoding: 10ms ([BGE-M3](/models/bge-m3) on GPU). Vector search: 5ms (HNSW index in memory). Hybrid fusion: <1ms. Reranking: 50ms (100 candidates batched through [BGE Reranker V2 M3](/models/bge-reranker-v2-m3) on GPU). Optional generation: 50-200ms ([Qwen 3 32B](/models/qwen-3-32b) generating 100-200 token answer). Total without generation: ~86ms. With generation: ~136-286ms. The budget is tight but achievable with GPU acceleration at every stage. **Query understanding in production.** Don't run query rewriting on every query — classify first. Navigational queries ("login page," "API docs authentication") need no rewriting. Comparative queries ("iPhone 16 vs Pixel 9") need decomposition into sub-queries. Ambiguous queries ("best battery") need expansion. Classification model: fine-tuned DistilBERT (50M params, 2ms latency, 95% accuracy) routes queries to rewriting or direct search. Only 20-30% of queries need rewriting — saves 70-80% of query understanding cost. **Index freshness SLA.** Define how quickly new documents appear in search results. For news/e-commerce: <1 minute (streaming index updates). For documentation: <1 hour (batch index rebuild). For legal archives: <24 hours. Streaming updates: insert vectors into HNSW graph one at a time. After 10K+ insertions, graph quality degrades (recall drops 1-3%). Mitigation: two-phase index — "hot" index (last 24 hours, streaming updates, small) + "cold" index (bulk, rebuilt nightly). Query both and fuse results. Hot index handles freshness; cold index handles recall. **Relevance tuning.** Search quality tuning is iterative: (1) Collect labeled query-document pairs (human raters score relevance 0-4 for each query-doc pair). Need 500-1000 labeled pairs for initial tuning, 5K+ for robust tuning. (2) Compute NDCG@10 on labeled set. (3) Tune: α (dense/sparse fusion weight), reranker threshold (minimum score to include), time-decay λ, domain-specific query expansion terms. (4) A/B test — 5% of traffic gets new parameters, compare click-through rate, dwell time, and reformulation rate. (5) Deploy parameter update. **Multi-index search for complex domains.** E-commerce: product index + category index + brand index + review index. Query "waterproof running shoes under $100" → product index (shoes), review index ("waterproof" appears more in reviews than product titles), category index (running shoes category). Fuse top N from each index with learned per-index weights. Multi-index search improves recall@10 by 10-20% for complex queries.

What breaks

Failure modes operators see in the wild.

- **Vocabulary mismatch (the "tap vs faucet" problem).** Symptom: user searches "fix leaking tap" and gets zero plumbing results because your corpus uses "faucet" exclusively. Keyword search would fail here; semantic search should succeed — but only if the embedding model has seen enough paraphrases of plumbing terms. Cause: embedding models have finite paraphrase coverage. Rare domain-specific synonyms may not be in training data. Mitigation: augment queries with domain-specific synonyms at query time (maintain a synonym dictionary per domain). For critical domains, fine-tune embedding model on in-domain paraphrase pairs. Evaluate recall@10 on a synonym test set — if recall <80%, invest in synonym expansion. - **Reranker overconfidence on irrelevant documents.** Symptom: the top-ranked result after reranking is a document that uses many query keywords but answers a completely different question. Reranker scored it 0.95 because of high lexical overlap, not semantic relevance. Cause: cross-encoders can be fooled by keyword density — they attend to tokens, not concepts. Mitigation: add a "contradiction check" — an LLM verifies that the retrieved document actually answers the query. For high-stakes search (medical, legal), human-in-the-loop validation of top result. Use negative training examples (documents with high keyword overlap but wrong answer) when fine-tuning reranker. - **Entity ambiguity without resolution.** Symptom: query "Apple revenue 2025" returns results about apple fruit farming, not Apple Inc. The embedding model can't distinguish which "Apple" based on the query alone. Cause: embedding models lack entity linking — they encode the token string "Apple," not the entity Q312 (Apple Inc.). Mitigation: entity linking pre-processing — identify entities in query, resolve to Knowledge Graph IDs, boost documents containing the correct entity ID. Use Wikidata/DBpedia for entity resolution. Entity linking improves NDCG@10 by 5-10% for entity-heavy queries (company names, person names, product names).

Hardware guidance

**Hobbyist ($300-$1,500).** CPU-only viable for <100K document search with dense retrieval only. Embedding encoding at query time: 5-10ms CPU. Skip reranking (adds 2-5 seconds CPU). [RTX 3060 12GB](/hardware/rtx-3060-12gb) enables reranking at 10-50 queries/sec. [RTX 4060 Ti 16GB](/hardware/rtx-4060-ti-16gb) adds VRAM headroom for concurrent embedding + reranker. [MacBook Pro 16" M4 Max](/hardware/macbook-pro-16-m4-max) 36GB runs embedding + reranker on SoC at 50-100 queries/sec. **SMB ($2,000-$4,000).** [RTX 4090](/hardware/rtx-4090) 24GB runs [BGE-M3](/models/bge-m3) embedding + [BGE Reranker V2 M3](/models/bge-reranker-v2-m3) simultaneously. Embedding throughput: 1000+ queries/sec. Reranking throughput: 200+ candidate pairs/sec. For a search service at 50 QPS with 100-candidate reranking (5000 pairs/sec), this needs ~50% GPU utilization — comfortable headroom for query understanding LLM calls. **Team serving ($6,000-$15,000).** [NVIDIA L40S](/hardware/nvidia-l40s) 48GB runs the full pipeline: query understanding ([Qwen 3 32B](/models/qwen-3-32b)) + embedding ([BGE-M3](/models/bge-m3)) + reranker ([BGE Reranker V2 M3](/models/bge-reranker-v2-m3)) on a single GPU. 48GB fits all three models with room for batch processing. Serves 100-500 QPS depending on LLM generation usage (generation is the bottleneck, not embedding/reranking). **Enterprise ($25,000+).** For search with LLM answer generation at scale: [NVIDIA H100 PCIe](/hardware/nvidia-h100-pcie) 80GB. The LLM ([Llama 3.3 70B](/models/llama-3-3-70b) or [Qwen 3 32B](/models/qwen-3-32b)) for answer synthesis dominates compute — 50-200ms for 100-200 token answers. At 100 QPS with LLM generation, one H100 handles the LLM while a separate [NVIDIA L40S](/hardware/nvidia-l40s) handles embedding + reranking. Separate serving tiers: embedding/reranking on cheaper GPUs; LLM generation on accelerated GPUs. **Storage sizing for vector search.** 1024-dim float32 vectors: 4 KB/document. Index overhead: HNSW adds 100% (8 KB total per document). 1M documents: 8 GB. 10M: 80 GB. 100M: 800 GB. RAM requirements: HNSW graph should be in memory for <5ms query latency. 100M documents at 800 GB requires a multi-node cluster (shard index by document type or time range). Practical ceiling for single-node search: ~50M documents on 512 GB RAM server.

Runtime guidance

**If building from scratch** → Python + SentenceTransformers ([BGE-M3](/models/bge-m3)) + FAISS for vector search + `rank-bm25` for sparse retrieval + FlagEmbedding ([BGE Reranker V2 M3](/models/bge-reranker-v2-m3)) for reranking. Four libraries, ~200 lines of Python. No infrastructure beyond Python. Works for <10M documents. Productionize with FastAPI. **If using a search platform** → Elasticsearch 8.x+ with dense vector support. Elasticsearch combines: BM25 (built-in), dense vectors via HNSW (built-in), hybrid search (dense + sparse fusion in one query), and filtering (metadata, dates, categories). Add reranking via a plugin or external service calling [BGE Reranker V2 M3](/models/bge-reranker-v2-m3). Elasticsearch handles distributed search across nodes — the simplest path for >10M documents. **If using a dedicated vector database** → [Weaviate](/tools/weaviate) for hybrid-native search (BM25 + dense built-in, multi-tenancy, GraphQL API). [Qdrant](/tools/qdrant) for raw performance (Rust, low-latency, high-throughput). [pgvector](/tools/pgvector) for simplicity. Choose: Weaviate if you need hybrid + multi-tenancy + filtering in one query language. Qdrant if you need maximum QPS at minimum latency. pgvector if you need simple SQL-based search without new infrastructure. **If adding LLM answer generation (RAG search)** → [Llama 3.3 70B](/models/llama-3-3-70b) via [vLLM](/tools/vllm) for production, [Ollama](/tools/ollama) for prototyping. Prompt: "Using the following documents, answer the query. Cite document IDs for each claim. If the documents don't contain the answer, say so." This prompt architecture reduces hallucination by grounding the LLM in retrieved content. Track: citation accuracy (% of claims supported by cited document), hallucination rate (% of claims not supported by any document), and answer relevance. **If serving search with low latency requirements (<100ms)** → Skip LLM generation. Return ranked list with snippets. LLM generation adds 50-200ms and is the dominant latency component. For e-commerce and documentation search, users prefer fast page loads with relevant links over slow loads with generated answers.

Setup walkthrough

  1. Install Ollama → ollama pull nomic-embed-text (~274 MB) for embeddings.
  2. pip install chromadb for the vector database.
  3. Semantic search in 15 lines:
import ollama, chromadb
client = chromadb.Client()
collection = client.create_collection("search")

# Index your documents
texts = ["How to install Docker on Ubuntu", "Python async programming guide", "Best local AI models 2026"]
for i, text in enumerate(texts):
    emb = ollama.embed(model="nomic-embed-text", input=text)["embeddings"][0]
    collection.add(documents=[text], embeddings=[emb], ids=[f"doc_{i}"])

# Search semantically
query = "containerization setup linux"
query_emb = ollama.embed(model="nomic-embed-text", input=query)["embeddings"][0]
results = collection.query(query_embeddings=[query_emb], n_results=3)
print(results["documents"])  # Finds "Docker on Ubuntu" even though "Docker" isn't in the query
  1. First search in <50ms. The query "containerization setup linux" matches "Docker on Ubuntu" — semantic search understands meaning, not keywords.
  2. For hybrid search (semantic + keyword): ChromaDB doesn't support BM25 natively. Use LanceDB or Qdrant for production hybrid search, or combine results programmatically.

The cheap setup

Semantic search is trivially cheap. Nomic Embed Text + ChromaDB runs on any $300 laptop — indexes 100K documents in 5 minutes, searches in <50ms. For a personal knowledge base, company wiki, or documentation search: $300 is all you need. For the full answer-generation pipeline (search → retrieve → LLM generates answer), add a used GTX 1060 6 GB ($60) for the LLM. Total: ~$360. Semantic search is the highest-ROI local AI task — 10 lines of Python transforms "grep" into "Google for your own documents."

The serious setup

Any RTX GPU is overkill for semantic search alone. The embedding + vector search runs on CPU. For enterprise semantic search (10M+ documents, 100+ concurrent users, permissions-aware) use: BGE-M3 embeddings (GPU-accelerated for indexing speed, CPU for search), Qdrant (distributed vector DB with filtering), BGE Reranker V2 M3 (GPU for precision), Llama 3.1 8B (GPU for answer synthesis). Compute: RTX 3060 12 GB (~$200-250, see /hardware/rtx-3060-12gb) + Epyc/Xeon CPU server for Qdrant. Total: ~$1,500-2,500. The CPU/DB costs dominate at enterprise scale — the GPU is the cheapest component.

Common beginner mistake

The mistake: Replacing all keyword search with semantic search, then wondering why searching for "error code E5001" returns documents about "server errors" instead of the exact error code. Why it fails: Semantic search optimizes for meaning, not exactness. "E5001" is a specific error code — the embedding model sees it as a number, not a concept. It matches to documents about "errors" broadly, not the specific error. For exact IDs, error codes, version numbers, and proper nouns, keyword search is superior. The fix: Use hybrid search: BM25 (keyword) + embeddings (semantic). For queries: if the query contains codes/IDs/versions, weight BM25 higher. If the query is a natural language question, weight embeddings higher. Or: always retrieve top-50 from both and merge (reciprocal rank fusion). Semantic search is not a replacement for keyword search — it's a complement. Hybrid always beats either alone.

Recommended setup for semantic search

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 semantic search locally. Each links to a diagnose+fix walkthrough.

Before you buy

Verify your specific hardware can handle semantic search before committing money.

Hardware buying guidance for Semantic Search

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