Vector search powers the semantic layer of modern AI applications — from RAG pipelines and recommendation engines to image similarity and anomaly detection. Unlike keyword search, which matches on exact tokens, vector search finds results based on meaning by comparing high-dimensional embeddings using distance metrics such as cosine similarity or dot product.
This guide covers the full stack: generating embeddings, choosing and operating a vector database, building a query pipeline with reranking, and scaling to production.
Vector DB Comparison (2026)
| Feature | Pinecone | Weaviate | Milvus | Qdrant |
|---|---|---|---|---|
| Managed option | Yes (Serverless) | Yes (Weaviate Cloud) | Yes (Zilliz Cloud) | Yes (Qdrant Cloud) |
| Open-source | No | Yes (BSD-3) | Yes (Apache 2.0) | Yes (Apache 2.0) |
| Hybrid search | Via sparse-dense | Native BM25 + vector | Via scalar index | Native BM25 + vector |
| Filtering | Basic metadata | Excellent | Good at billion-scale | Best (payload filtering) |
| Scale ceiling | Billions | Hundreds of millions | Billions | Hundreds of millions |
| GPU indexing | No | No | Yes | Cloud only |
| Self-host complexity | N/A (managed) | Medium (Docker) | High (K8s required) | Low (single binary) |
| Best for | Zero-ops, any scale | Hybrid search OOTB | Billion-scale, GPU | Cost-sensitive, filtering |
Selection Guide
| Requirement | Recommend |
|---|---|
| Zero infrastructure, fast setup | Pinecone Serverless |
| Best filtering recall | Qdrant |
| Native hybrid search out of box | Weaviate |
| Billion+ vectors | Milvus |
| GPU-accelerated indexing | Milvus or Qdrant Cloud |
| Lowest self-hosted cost | Qdrant |
| PostgreSQL integration | pgvector |
| Most teams building RAG in 2026 | Qdrant |
Vector Search Architecture
A production vector search system has four stages. Text (or another modality) is encoded into a dense vector by an embedding model. Those vectors are stored in a purpose-built vector database that maintains an approximate nearest-neighbor (ANN) index. At query time, the user’s question is embedded with the same model, and the ANN index returns the top-K most similar stored vectors in milliseconds.
flowchart LR
A[Raw Documents] --> B[Embedding Model]
B --> C[(Vector Database\nANN Index)]
Q[User Query] --> B2[Same Embedding Model]
B2 --> D{ANN Search\nTop-K}
C --> D
D --> E[Ranked Results]
The critical constraint is that query and document embeddings must come from the same model. Switching models requires re-embedding your entire corpus.
Embedding Model Choices
| Model | Dimensions | Best For |
|---|---|---|
text-embedding-3-small (OpenAI) |
1536 | General English text, low latency |
text-embedding-3-large (OpenAI) |
3072 | Higher accuracy, multilingual |
embed-multilingual-v3 (Cohere) |
1024 | Multilingual corpora |
BAAI/bge-base-en-v1.5 (OSS) |
768 | Self-hosted, no API cost |
BAAI/bge-m3 (OSS) |
1024 | Multilingual self-hosted |
For cost-sensitive production workloads, open-source models via sentence-transformers run comfortably on a single GPU and match OpenAI’s smaller models on most benchmarks.
Embedding Generation
The embedding layer is a thin wrapper around your chosen model. The key design decisions are batching (critical for throughput) and how you combine multiple text fields before encoding. Concatenating title and body into a single string typically outperforms embedding them separately.
#!/usr/bin/env python3
"""Embedding generation with batching support."""
from openai import OpenAI
from typing import List, Dict
class EmbeddingGenerator:
"""Generate text embeddings via OpenAI or a drop-in compatible API."""
def __init__(self, model: str = "text-embedding-3-small"):
self.client = OpenAI()
self.model = model
def embed_text(self, text: str) -> List[float]:
"""Embed a single string."""
response = self.client.embeddings.create(model=self.model, input=text)
return response.data[0].embedding
def embed_batch(self, texts: List[str], batch_size: int = 100) -> List[List[float]]:
"""Embed a list of strings in batches to stay within API limits."""
all_embeddings = []
for i in range(0, len(texts), batch_size):
batch = texts[i : i + batch_size]
response = self.client.embeddings.create(model=self.model, input=batch)
all_embeddings.extend(item.embedding for item in response.data)
return all_embeddings
def embed_documents(self, documents: List[Dict]) -> List[Dict]:
"""Embed a list of documents, concatenating title + content."""
texts = [f"{d.get('title', '')} {d.get('content', '')}" for d in documents]
embeddings = self.embed_batch(texts)
return [
{"id": d["id"], "embedding": emb, "text": texts[i], "metadata": {k: v for k, v in d.items() if k not in ("id", "content")}}
for i, (d, emb) in enumerate(zip(documents, embeddings))
]
For self-hosted deployments, swap EmbeddingGenerator for the sentence-transformers variant below. The interface is identical, so the rest of the pipeline requires no changes.
from sentence_transformers import SentenceTransformer
from typing import List
class OpenSourceEmbedder:
"""Drop-in embedder using sentence-transformers (no API key required)."""
def __init__(self, model_name: str = "BAAI/bge-base-en-v1.5"):
self.model = SentenceTransformer(model_name)
def embed_text(self, text: str) -> List[float]:
return self.model.encode(text).tolist()
def embed_batch(self, texts: List[str]) -> List[List[float]]:
return self.model.encode(texts, batch_size=64, show_progress_bar=True).tolist()
Vector Databases
Once you have embeddings, you need a store that can run ANN queries at low latency. The three most widely deployed options are Pinecone (managed SaaS), Milvus (self-hosted or cloud), and Weaviate (self-hosted or cloud, with built-in vectorization). Choose based on your ops maturity and data residency requirements.
Pinecone
Pinecone is fully managed — no infrastructure to run. You create a serverless index, upsert vectors with metadata, and query with optional metadata filters. This makes it ideal for teams that want to ship quickly without managing clusters.
#!/usr/bin/env python3
"""Pinecone vector store operations."""
from pinecone import Pinecone, ServerlessSpec
from typing import List, Dict, Optional
class PineconeVectorStore:
"""Thin wrapper around the Pinecone client."""
def __init__(self, api_key: str, index_name: str, dimension: int = 1536):
self.client = Pinecone(api_key=api_key)
self.index_name = index_name
self._ensure_index(dimension)
self.index = self.client.Index(index_name)
def _ensure_index(self, dimension: int):
if self.index_name not in self.client.list_indexes().names():
self.client.create_index(
name=self.index_name,
dimension=dimension,
metric="cosine",
spec=ServerlessSpec(cloud="aws", region="us-east-1"),
)
def upsert(self, vectors: List[Dict]):
"""Upsert a list of {id, embedding, metadata} dicts."""
self.index.upsert(
vectors=[{"id": v["id"], "values": v["embedding"], "metadata": v.get("metadata", {})} for v in vectors]
)
def search(self, query_vector: List[float], top_k: int = 10, filter_dict: Optional[Dict] = None) -> List[Dict]:
"""Return top-K results, optionally filtered by metadata."""
results = self.index.query(vector=query_vector, top_k=top_k, filter=filter_dict, include_metadata=True, include_values=False)
return [{"id": m["id"], "score": m["score"], "metadata": m.get("metadata", {})} for m in results["matches"]]
Milvus
Milvus is the go-to choice for on-premises deployments or when you need fine-grained control over indexing parameters. The example below uses IVF_FLAT, which is a good baseline; for billion-scale datasets consider HNSW or IVF_PQ.
#!/usr/bin/env python3
"""Milvus vector store with IVF_FLAT index."""
from pymilvus import connections, Collection, FieldSchema, CollectionSchema, DataType
from typing import List, Dict
class MilvusVectorStore:
"""Milvus collection wrapper."""
def __init__(self, collection_name: str, dimension: int = 1536, host: str = "localhost", port: int = 19530):
connections.connect(host=host, port=port)
self.collection = self._get_or_create(collection_name, dimension)
def _get_or_create(self, name: str, dimension: int) -> Collection:
from pymilvus import utility
if utility.has_collection(name):
return Collection(name)
schema = CollectionSchema([
FieldSchema("id", DataType.INT64, is_primary=True, auto_id=False),
FieldSchema("vector", DataType.FLOAT_VECTOR, dim=dimension),
FieldSchema("text", DataType.VARCHAR, max_length=65535),
])
col = Collection(name, schema)
col.create_index("vector", {"metric_type": "IP", "index_type": "IVF_FLAT", "params": {"nlist": 128}})
return col
def insert(self, vectors: List[Dict]):
self.collection.insert([
[v["id"] for v in vectors],
[v["embedding"] for v in vectors],
[v["text"][:65535] for v in vectors],
])
self.collection.flush()
def search(self, query_vector: List[float], top_k: int = 10) -> List[Dict]:
self.collection.load()
hits = self.collection.search(
data=[query_vector], anns_field="vector",
param={"metric_type": "IP", "params": {"nprobe": 10}},
limit=top_k, output_fields=["text"],
)
return [{"id": h.id, "score": h.distance, "text": h.entity.get("text")} for h in hits[0]]
Query Pipeline with Reranking
A common production pattern is retrieve-then-rerank: fetch a larger candidate set (top-20 or top-50) from the ANN index cheaply, then run a cross-encoder to reorder by true relevance. The cross-encoder is slower but much more accurate because it attends to both query and document together.
flowchart TD
Q[Query] --> E[Embed Query]
E --> ANN[ANN Search\ntop-20 candidates]
ANN --> CE[Cross-Encoder\nRerank]
CE --> R[Return top-5]
#!/usr/bin/env python3
"""Semantic search with optional cross-encoder reranking."""
from sentence_transformers import CrossEncoder
from typing import List, Dict
class SemanticSearchApp:
"""Combines an embedder and a vector store into a search interface."""
def __init__(self, embedder, vector_store):
self.embedder = embedder
self.store = vector_store
self._reranker = None
def index(self, documents: List[Dict]) -> int:
embedded = self.embedder.embed_documents(documents)
self.store.upsert(embedded)
return len(embedded)
def search(self, query: str, top_k: int = 5) -> List[Dict]:
return self.store.search(self.embedder.embed_text(query), top_k=top_k)
def search_with_rerank(self, query: str, candidates: int = 20, top_k: int = 5) -> List[Dict]:
"""Retrieve a wider candidate set, then rerank with a cross-encoder."""
if self._reranker is None:
self._reranker = CrossEncoder("cross-encoder/ms-marco-MiniLM-L-6-v2")
results = self.search(query, top_k=candidates)
scores = self._reranker.predict([(query, r["text"]) for r in results])
for r, score in zip(results, scores):
r["rerank_score"] = float(score)
return sorted(results, key=lambda x: x["rerank_score"], reverse=True)[:top_k]
Hybrid Search
Hybrid search combines dense vector search with sparse BM25 keyword search. It consistently outperforms pure semantic search for technical docs, product catalogs, and content with specific identifiers like model numbers or proper nouns.
class HybridSearchEngine:
"""Combine dense vector search with sparse keyword search."""
def __init__(self, dense_index, sparse_index, alpha=0.7):
this.dense = dense_index # Vector DB (semantic)
this.sparse = sparse_index # BM25 index (keyword)
this.alpha = alpha # Dense weight (0-1)
def search(self, query: str, top_k: int = 10) -> list:
dense_results = this.dense.search(query, top_k)
sparse_results = this.sparse.search(query, top_k)
# Reciprocal Rank Fusion (RRF)
scores = {}
for rank, r in enumerate(dense_results):
scores[r["id"]] = this.alpha * (1.0 / (rank + 60))
for rank, r in enumerate(sparse_results):
scores[r["id"]] = scores.get(r["id"], 0) + (1 - this.alpha) * (1.0 / (rank + 60))
return sorted(scores.items(), key=lambda x: -x[1])[:top_k]
Hybrid search typically improves retrieval accuracy by 15-25% over pure dense search on mixed-content collections.
Performance Optimization
Index Types
| Index | Search Speed | Build Speed | Memory | Best For |
|---|---|---|---|---|
| HNSW | Fastest | Slow | High | High-performance production |
| IVF_FLAT | Fast | Medium | Medium | Balance of speed and accuracy |
| IVF_PQ | Medium | Fast | Low | Memory-constrained, large scale |
| DiskANN | Medium | N/A | Very low | Billion-scale on SSDs |
Quantization for Vectors
| Technique | Memory Reduction | Accuracy Loss | Use Case |
|---|---|---|---|
| FP32 → FP16 | 50% | <0.5% | General purpose |
| FP32 → INT8 | 75% | 1-2% | High-throughput, large scale |
| Product quantization (PQ) | 90%+ | 3-5% | Billion-scale, approximate |
| Binary quantization | 97% | 5-10% | Very large scale, recall-tolerant |
Filtering Performance
| Database | Filter Type | Performance at 10M | Performance at 100M |
|---|---|---|---|
| Qdrant | Payload | Fast (~12ms p99) | Good (~25ms p99) |
| Weaviate | Class + property | Fast (~16ms p99) | Good (~30ms p99) |
| Milvus | Scalar index | Good (~18ms p99) | Good (~35ms p99) |
| Pinecone | Metadata | Good (~15ms p99) | Degrades under selective filters |
Cost Analysis
| Database | Storage (1M vectors, 768d) | Indexing | Queries (1M/month) | Monthly Total |
|---|---|---|---|---|
| Pinecone Serverless | $0.50/GB/month | Included | $5.00 | ~$15 |
| Qdrant Cloud | $0.80/GB/month | Included | $3.00 | ~$12 |
| Weaviate Cloud | $25/month starter | Included | Per-request | ~$25 |
| Milvus (self-hosted) | $0.10/GB/month (disk) | CPU cost | Included | ~$50 (infra) |
| Qdrant (self-hosted) | $0.10/GB/month (disk) | CPU cost | Included | ~$40 (infra) |
For large-scale deployments (>100M vectors), self-hosted options are significantly cheaper than managed services.
Production Architecture
Client → API Gateway → Embedding Service (BERT/BGE)
↓
Vector DB Cluster
├── Primary (writes)
├── Replica 1 (reads)
├── Replica 2 (reads)
└── Replica N (reads)
↓
Reranker (Cross-encoder)
↓
LLM (for RAG applications)
Embedding Model Comparison
| Model | Dimensions | Max Tokens | Quality (MTEB) | Cost/1M Vectors |
|---|---|---|---|---|
| OpenAI text-embedding-3-large | 3072 | 8191 | 64.6 | $130 |
| OpenAI text-embedding-3-small | 1536 | 8191 | 62.3 | $20 |
| BGE-M3 | 1024 | 8192 | 65.2 | Free (self-host) |
| Cohere embed-english-v3 | 1024 | 512 | 64.8 | $30 |
| intfloat/e5-mistral-7b-instruct | 4096 | 32768 | 66.5 | Free (self-host) |
| sentence-transformers/all-MiniLM | 384 | 256 | 56.7 | Free (self-host) |
For production, BGE-M3 offers the best quality-to-cost ratio. OpenAI text-embedding-3-small is the cheapest API option.
Filtering Strategies
| Filter Type | Use Case | Recommended DB |
|---|---|---|
| Tenant isolation | Multi-tenant SaaS | Separate collection/namespace per tenant |
| Metadata equality | Category filter | Qdrant payload, Milvus scalar index |
| Range filter | Price, date range | Qdrant range index, Milvus inverted index |
| Geo filter | Location search | Milvus geo index |
| Boolean filter | Status flags | Weaviate property filtering |
| High-cardinality (>10K) | User IDs | Separate namespace, not metadata field |
Monitoring Vector Search
| Metric | Tool | Warning | Critical |
|---|---|---|---|
| p99 search latency | Prometheus + Grafana | >100ms | >500ms |
| Recall@10 | Offline evaluation | <85% | <75% |
| Index size growth | DB metrics | >20%/week | >50%/week |
| Ingestion lag | Application logs | >1 min | >5 min |
| Cache hit rate | Redis metrics | <50% | <20% |
| Disk usage | Node exporter | >70% | >85% |
Troubleshooting
| Symptom | Cause | Solution |
|---|---|---|
| Slow queries | Wrong index type | Switch from IVF to HNSW |
| Low recall | Embedding mismatch | Verify query and index use same model |
| High memory | No quantization | Enable PQ or FP16 |
| Ingestion fails | Batch too large | Reduce batch size to 100 vectors |
| Filter slow | No index on filter field | Add scalar index for filter column |
| Cross-model inconsistency | Embedding model version change | Pin model version in config |
| OOM during build | HNSW building memory | Use IVF_PQ or disk-based index |
Scaling to Production
Single-node vector databases handle tens of millions of vectors comfortably. Beyond that, you need horizontal sharding. The simplest strategy is consistent hashing: assign each document to a shard deterministically by ID, then fan out queries to all shards and merge the results.
#!/usr/bin/env python3
"""Fan-out search across multiple vector store shards."""
from typing import List, Dict
def federated_search(query_vector: List[float], shards: list, top_k: int = 10) -> List[Dict]:
"""Query all shards in parallel and return the global top-K."""
import concurrent.futures
all_results: List[Dict] = []
with concurrent.futures.ThreadPoolExecutor() as pool:
futures = [pool.submit(shard.search, query_vector, top_k) for shard in shards]
for f in concurrent.futures.as_completed(futures):
all_results.extend(f.result())
# Deduplicate by ID and keep highest score
seen: Dict[str, Dict] = {}
for r in all_results:
if r["id"] not in seen or r["score"] > seen[r["id"]]["score"]:
seen[r["id"]] = r
return sorted(seen.values(), key=lambda x: x["score"], reverse=True)[:top_k]
Deployment Checklist
Before going to production, verify these points:
- Index warm-up — Milvus and Weaviate require
collection.load()before the first query. Add a readiness probe to your service that performs a single search. - Batch ingestion throughput — Pinecone Serverless handles ~100 upserts/s per namespace; use parallel workers for bulk loads.
- Metadata filtering cardinality — High-cardinality filters (e.g., per-user namespaces) should use separate namespaces or collections, not metadata fields, to avoid index bloat.
- Embedding model versioning — Pin the exact model version. OpenAI occasionally releases updated embedding models whose outputs are incompatible with older vectors.
- Monitoring — Track p99 search latency, recall (via offline evaluation), and index size growth. Milvus exposes Prometheus metrics out of the box.
Vector Database Configuration Examples
Pinecone Serverless
import pinecone
pc = pinecone.Pinecone(api_key="your-api-key")
# Create serverless index
pc.create_index(
name="semantic-search",
dimension=1024,
metric="cosine",
spec=pinecone.ServerlessSpec(
cloud="aws",
region="us-east-1"
)
)
index = pc.Index("semantic-search")
# Upsert vectors
index.upsert(vectors=[
("id1", [0.1, 0.2, ...], {"text": "document content", "source": "pdf"}),
("id2", [0.3, 0.4, ...], {"text": "another document", "source": "web"}),
])
# Query
results = index.query(
vector=query_embedding,
top_k=10,
filter={"source": {"$eq": "pdf"}},
include_metadata=True
)
Qdrant (Self-Hosted)
from qdrant_client import QdrantClient
from qdrant_client.http.models import Distance, VectorParams
client = QdrantClient(host="localhost", port=6333)
# Create collection with payload indexes
client.create_collection(
collection_name="documents",
vectors_config=VectorParams(size=1024, distance=Distance.COSINE),
)
client.create_payload_index(
collection_name="documents",
field_name="category",
field_type="keyword",
)
client.create_payload_index(
collection_name="documents",
field_name="created_at",
field_type="integer",
)
# Search with filtering
results = client.search(
collection_name="documents",
query_vector=query_embedding,
limit=10,
query_filter=models.Filter(
must=[
models.FieldCondition(key="category", match=models.MatchValue(value="technical")),
models.FieldCondition(key="created_at", range=models.Range(gte=1700000000)),
]
),
with_payload=True,
)
Reranking Strategies
Cross-encoder Reranking
Improve search quality by reranking top-K results with a cross-encoder model:
from sentence_transformers import CrossEncoder
class Reranker:
"""Cross-encoder reranker for improving search relevance."""
def __init__(self, model_name="cross-encoder/ms-marco-MiniLM-L-6-v2"):
this.model = CrossEncoder(model_name)
def rerank(self, query: str, candidates: list[dict], top_k: int = 10) -> list[dict]:
pairs = [(query, c["text"]) for c in candidates]
scores = this.model.predict(pairs)
for c, s in zip(candidates, scores):
c["rerank_score"] = float(s)
return sorted(candidates, key=lambda x: x["rerank_score"], reverse=True)[:top_k]
Cross-encoder reranking typically improves NDCG@10 by 10-20% over raw vector similarity.
Multi-Stage Retrieval Pipeline
Stage 1: Vector search → 100 candidates (fast, approximate)
Stage 2: Reranker → 20 candidates (accurate, pairwise cross-encoder)
Stage 3: LLM rerank → top 5 (context-aware ranking)
Data Lifecycle Management
| Vector Count | Storage | Query Strategy | Maintenance |
|---|---|---|---|
| < 1M | Single node, FP32 | Full scan or simple index | Weekly optimization |
| 1M-10M | Single node, FP16 | HNSW index | Weekly index rebuild |
| 10M-100M | 2-4 shards, INT8 | IVF_PQ or HNSW+PQ | Daily incremental, weekly full |
| 100M-1B | 8-16 shards, PQ | IVF_PQ with GPU | Hourly incremental, daily full |
| 1B+ | 32+ shards, Binary | DiskANN or scaled-out Milvus | Continuous rebuild with CDC |
RAG Pipeline Performance
| Component | Latency Budget | Optimization |
|---|---|---|
| Query embedding | 50-200ms | Use smaller model (BGE-small), cache results |
| Vector search | 10-100ms | HNSW index, optimize filter selectivity |
| Reranking | 100-500ms | Limit to top 20 candidates |
| Context assembly | 10-50ms | Pre-format templates, cache snippets |
| LLM generation | 500ms-5s | Use smaller LLM for RAG (7B vs 70B) |
Total pipeline latency target: <2s for interactive applications.
Vector Search Evaluation
| Metric | What It Measures | Good Target |
|---|---|---|
| Recall@K | % of relevant docs in top K | >90% at K=20 |
| Precision@K | % of top K that are relevant | >70% at K=10 |
| NDCG@K | Ranking quality (position-weighted) | >0.85 at K=10 |
| MRR | First relevant result position | >0.90 |
| Latency p99 | Query speed | <100ms |
| Index size | Storage efficiency | <3x raw vector size |
Quick Reference: Vector DB Commands
# Pinecone: List indexes
curl -s https://api.pinecone.io/indexes -H "Api-Key: $PINECONE_API_KEY" | jq .
# Qdrant: Create collection
curl -X PUT http://localhost:6333/collections/my_collection \
-H 'Content-Type: application/json' \
-d '{"vectors": {"size": 1024, "distance": "Cosine"}}'
# Milvus: Load collection (required before search)
python3 -c "from pymilvus import Collection; Collection('my_collection').load()"
# Weaviate: Search with hybrid
curl -X POST http://localhost:8080/v1/graphql \
-H 'Content-Type: application/json' \
-d '{"query": "{ Get { Document(hybrid: {query: \"search text\"}) { title score } } }"}'
Decision Matrix: Self-Host vs Managed Vector DB
| Factor | Self-Host | Managed |
|---|---|---|
| Monthly cost at 10M vectors | $40-100 (infra) | $15-30 |
| Monthly cost at 100M vectors | $200-500 | $150-300 |
| Engineering overhead | High (K8s, ops) | Low (API calls) |
| Latency control | Full | Limited by provider |
| Data residency | On-premise | Provider region |
| Feature velocity | Slow (manual upgrades) | Fast (provider-managed) |
| Best for | >100M vectors, data-sensitive | <100M vectors, fast time-to-market |
Related Articles
- Building Production LLM Applications
- Vector Databases: Pinecone, Milvus, Weaviate
- Hybrid Search RAG Complete Guide
Embedding Strategy by Content Type
| Content Type | Recommended Embedding Model | Chunk Size | Overlap | Normalization |
|---|---|---|---|---|
| Code | intfloat/e5-mistral-7b-instruct | 512 tokens | 128 | Yes |
| Technical docs | BGE-M3 | 512 tokens | 64 | Yes |
| News/articles | OpenAI text-embedding-3-large | 512 tokens | 128 | Yes |
| Chat/support | BGE-small-en-v1.5 | 256 tokens | 32 | Yes |
| Legal documents | Cohere embed-english-v3 | 1024 tokens | 256 | Yes |
| Scientific papers | intfloat/e5-mistral-7b-instruct | 1024 tokens | 128 | Yes |
| Product descriptions | BGE-base-en-v1.5 | 256 tokens | 0 | Yes |
Disaster Recovery
| Scenario | RPO | RTO | Strategy |
|---|---|---|---|
| Node failure | 0 | 1-5 min | Replica shards, auto-failover |
| Data corruption | 5 min | 30 min | Incremental snapshots every 5 min |
| Regional outage | 15 min | 15 min | Multi-region replication |
| Accidental deletion | 24h | 1h | Point-in-time recovery |
| Index corruption | 1h | 4h | Full index rebuild from source data |
Scaling Checklist
Scale vector search from prototype to production:
- Start with single-node Qdrant or Pinecone Serverless
- Add embedding model version pinning
- Implement metadata filtering with proper indexes
- Add cross-encoder reranking for Stage 2
- Monitor p99 latency and recall@10
- Shard across nodes when exceeding 10M vectors
- Implement hybrid search (dense + sparse)
- Add GPU indexing for billion-scale collections
- Set up multi-region replication for HA
- Automate index rebuilds and compaction
Vector Search Cost Model
def estimate_vector_search_cost(
num_vectors: int,
dimension: int = 1024,
queries_per_month: int = 1000000,
managed: bool = True
) -> dict:
"""Estimate monthly cost for vector search infrastructure."""
bytes_per_vector = dimension * 4 # FP32
raw_storage_gb = num_vectors * bytes_per_vector / (1024**3)
indexed_storage_gb = raw_storage_gb * 2.5 # HNSW overhead
if managed:
storage_cost = indexed_storage_gb * 0.50 * 30 # $0.50/GB/month
query_cost = queries_per_month * 0.000005 # $5 per 1M queries
total = storage_cost + query_cost
else:
# Self-hosted: 3x A100 GPUs for 10M+ vectors
infra_cost = 3 * 3.50 * 24 * 30 # $7,560/month
total = infra_cost
return {
"vectors": f"{num_vectors:,}",
"raw_storage_gb": round(raw_storage_gb, 1),
"indexed_storage_gb": round(indexed_storage_gb, 1),
"monthly_cost": f"${total:,.0f}",
"cost_per_query": f"${total / queries_per_month:.6f}"
}
# Example: 10M vectors, 1024d, managed
print(estimate_vector_search_cost(10_000_000))
Quick Comparison: Key Dimensions
| Dimension | Winner | Why |
|---|---|---|
| Easiest setup | Pinecone | Serverless, no ops, 5 min to first query |
| Best filtering | Qdrant | Payload indexes, native geo, high-cardinality OK |
| Hybrid search | Weaviate | Native BM25 + vector, most mature |
| Billion-scale | Milvus | GPU indexing, distributed, 42K+ GitHub stars |
| Lowest cost | Qdrant self-host | Rust-based, efficient, single binary |
| PostgreSQL integration | pgvector | No new infrastructure needed |
| Agent/LLM integration | Weaviate | MCP support, built-in vectorization |
Vector Search Maturity Model
| Level | Capabilities | Scale | Latency |
|---|---|---|---|
| 1: Basic | Single index, exact search | <1M vectors | >1s |
| 2: Optimized | HNSW index, FP16 | 1-10M | <100ms |
| 3: Production | Hybrid search, reranking, sharding | 10-100M | <50ms |
| 4: Advanced | GPU indexing, PQ, auto-scaling | 100M-1B | <20ms |
| 5: Global | Multi-region, CDC, real-time sync | 1B+ | <50ms |
Summary: Vector Search Workflow
Documents → Chunk → Embed → Index → Query → Rerank → Generate (RAG)
^ ^ ^ ^ ^ ^
512t Model DB + Hybrid Cross- LLM with
chunks BGE-M3 HNSW search encoder context
Resources
- Pinecone Documentation
- Milvus Documentation
- Qdrant Documentation
- Weaviate Documentation
- Sentence Transformers
- OpenAI Embeddings Guide
- Pinecone vs Weaviate vs Milvus vs Qdrant (2026)
- Best Vector Databases 2026: 6 Top Picks Compared
Comments