Skip to main content

Vector Search at Scale: Building Semantic Search Systems

Published: July 7, 2025 Updated: June 24, 2026 Larry Qu 15 min read

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 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
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

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:

  1. Start with single-node Qdrant or Pinecone Serverless
  2. Add embedding model version pinning
  3. Implement metadata filtering with proper indexes
  4. Add cross-encoder reranking for Stage 2
  5. Monitor p99 latency and recall@10
  6. Shard across nodes when exceeding 10M vectors
  7. Implement hybrid search (dense + sparse)
  8. Add GPU indexing for billion-scale collections
  9. Set up multi-region replication for HA
  10. 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

Comments

👍 Was this article helpful?