LUNAROPS · OPERATIONAL UPLINK 100% UPTIME 1,247d POSTS 893 JEFF.MOON@LUNAROPS.DEV UTC --:--:--

Vector Databases in Production: Pinecone, Weaviate, Qdrant, and pgvector

vector-databasesembeddingsragaipgvectorqdrantweaviatepinecone

The explosion of LLM-powered applications has made vector search a core infrastructure primitive. Semantic search, RAG (retrieval-augmented generation), recommendation systems, duplicate detection, anomaly detection — all of these reduce to the same operation: given a query vector, find the N most similar vectors in a large collection. Vector databases are purpose-built for this.

This guide explains how vector search works under the hood, when each database is the right choice, and how to run vector search in production without surprises.

What Is Vector Search and Why Does It Need a Database?

A vector embedding is a dense numerical representation of content — text, images, audio, code — typically 384 to 3072 dimensions. Semantically similar content produces similar vectors. The key operation is approximate nearest neighbor (ANN) search: given a query vector, return the K vectors in the collection that are closest in vector space.

Why not just use PostgreSQL with a float array and brute-force distance calculation?

For N=1M vectors at 1536 dimensions:

  • Brute force: compute 1.5 billion floating-point operations per query. At ~1ms, that’s ~15,000ms — 15 seconds per query.
  • With HNSW index: ~1ms per query, with 95%+ recall.

Vector databases exist to make ANN search fast through specialized indexing algorithms, while also handling the operational concerns of a database: durability, replication, filtering, and consistent reads.

Distance Metrics

The similarity between two vectors is measured by a distance function:

Cosine similarity (most common for text embeddings):

similarity = (A · B) / (||A|| × ||B||)

Measures the angle between vectors, ignoring magnitude. Range: [-1, 1]. Most text embedding models produce unit-normalized vectors, making cosine equivalent to dot product.

Dot product (fastest, used when vectors are normalized):

similarity = A · B = Σ(aᵢ × bᵢ)

When vectors are pre-normalized (unit length), dot product and cosine give identical rankings but dot product is 30-50% faster.

Euclidean distance (L2):

distance = √(Σ(aᵢ - bᵢ)²)

Measures actual spatial distance. Use when the magnitude of embeddings is meaningful (e.g., image features, some audio models).

Indexing Algorithms

The indexing algorithm determines the accuracy/speed/memory trade-off of your vector search.

HNSW (Hierarchical Navigable Small World)

HNSW is the dominant algorithm for in-memory vector search. It builds a multi-layer graph:

  • Layer 0: all vectors connected to nearest neighbors (dense graph)
  • Layer 1-N: progressively sparser graphs for long-range navigation

Query traversal: start at the top layer (few nodes, fast navigation to the right neighborhood), descend to layer 0, find exact nearest neighbors.

Key parameters:

  • M (connections per node): higher = better recall, more memory, slower build. Typical: 16–64.
  • ef_construction (build-time beam width): higher = better recall at build time, slower indexing. Typical: 100–400.
  • ef (query-time beam width): higher = better recall at query time, slower queries. Typical: 64–200.

Characteristics:

  • Query time: O(log N) — excellent
  • Memory: ~(M × 4 bytes × N) overhead on top of raw vectors
  • 1M vectors at 1536 dims, M=16: ~6GB for vectors + ~0.1GB for index
  • Recall@10: typically 95-99% with good ef settings
  • Weakness: entire index must fit in RAM

IVF (Inverted File Index)

IVF clusters vectors using k-means, then searches only the nearest clusters at query time.

Key parameters:

  • nlist: number of clusters. Typical: sqrt(N) to 4×sqrt(N).
  • nprobe: number of clusters to search at query time. Higher = better recall, slower queries.

Characteristics:

  • Supports out-of-core search (clusters stored on disk)
  • Faster indexing than HNSW
  • Recall is tunable via nprobe (10% of nlist is a common starting point)
  • Often combined with Product Quantization (IVF-PQ) to compress vectors and fit more in RAM

DiskANN

DiskANN (Microsoft Research) stores the full graph index on SSD, not RAM. Optimized for billion-scale datasets where the entire index won’t fit in memory.

Characteristics:

  • Can index 1B+ vectors on commodity hardware
  • Query latency: ~2-10ms (SSD seek time dominates)
  • Memory requirement: only the compressed vectors in RAM, full graph on SSD
  • Used by Azure Cognitive Search, Weaviate’s enterprise tier

ScaNN (Google)

Google’s Scalable Approximate Nearest Neighbor algorithm. Uses anisotropic quantization to reduce the vector space while preserving directional similarity. Excellent recall at high throughput.

pgvector: Vector Search in PostgreSQL

pgvector is a PostgreSQL extension that adds vector similarity search. If you’re already running PostgreSQL, it’s often the right first choice.

Installation and Setup

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
-- Enable the extension
CREATE EXTENSION IF NOT EXISTS vector;

-- Create a table with a vector column
CREATE TABLE documents (
    id          BIGSERIAL PRIMARY KEY,
    content     TEXT NOT NULL,
    embedding   VECTOR(1536),      -- OpenAI ada-002 dimension
    metadata    JSONB,
    created_at  TIMESTAMPTZ DEFAULT NOW()
);

-- Create an HNSW index
CREATE INDEX ON documents USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);

-- Or an IVF index (better for larger datasets where build time matters)
-- Must have data before creating IVF index
CREATE INDEX ON documents USING ivfflat (embedding vector_cosine_ops)
WITH (lists = 100);   -- lists ≈ sqrt(row_count)

Inserting and Querying

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
import psycopg2
from openai import OpenAI
import numpy as np

client = OpenAI()
conn = psycopg2.connect("postgresql://user:pass@localhost/mydb")

def embed(text: str) -> list[float]:
    response = client.embeddings.create(
        input=text,
        model="text-embedding-3-small"   # 1536 dims
    )
    return response.data[0].embedding

def index_document(content: str, metadata: dict = None):
    embedding = embed(content)
    with conn.cursor() as cur:
        cur.execute(
            """INSERT INTO documents (content, embedding, metadata)
               VALUES (%s, %s, %s)""",
            (content, embedding, psycopg2.extras.Json(metadata or {}))
        )
    conn.commit()

def search(query: str, k: int = 5, filter_sql: str = None) -> list[dict]:
    query_embedding = embed(query)
    with conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) as cur:
        # Cosine similarity search with optional metadata filter
        sql = """
            SELECT
                id,
                content,
                metadata,
                1 - (embedding <=> %s::vector) AS similarity
            FROM documents
            {filter}
            ORDER BY embedding <=> %s::vector
            LIMIT %s
        """.format(filter=f"WHERE {filter_sql}" if filter_sql else "")

        cur.execute(sql, (query_embedding, query_embedding, k))
        return cur.fetchall()

# Usage
index_document("Kubernetes is an open-source container orchestration system",
               {"source": "docs", "category": "kubernetes"})

results = search("How do I deploy containers at scale?")
for r in results:
    print(f"[{r['similarity']:.3f}] {r['content'][:80]}")

# Filtered search — only search within a category
results = search(
    "container deployment",
    filter_sql="metadata->>'category' = 'kubernetes'"
)

pgvector Performance Tuning

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
-- Set ef for query (higher = better recall, slower queries)
SET hnsw.ef_search = 100;   -- Default 40, increase for better recall

-- For IVFFlat: how many clusters to search
SET ivfflat.probes = 10;   -- Default 1! Increase for better recall

-- Check index is being used
EXPLAIN (ANALYZE, BUFFERS)
SELECT id, content, 1 - (embedding <=> '[...]'::vector) AS sim
FROM documents
ORDER BY embedding <=> '[...]'::vector
LIMIT 10;

-- Parallel query support
SET max_parallel_workers_per_gather = 4;

-- Memory for index build
SET maintenance_work_mem = '8GB';

-- Vacuum after bulk loads to update index statistics
ANALYZE documents;

When pgvector Is the Right Choice

  • You already run PostgreSQL and don’t want another service
  • Dataset is < 5M vectors (beyond this, dedicated databases are faster)
  • You need complex SQL joins with your vector results
  • ACID transactions matter (e.g., updating document + embedding atomically)
  • Your team knows PostgreSQL operations

Qdrant: High-Performance Self-Hosted

Qdrant is a purpose-built vector database written in Rust. It has the best performance-per-dollar of the self-hosted options and excellent Kubernetes support.

Running Qdrant

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# Docker
docker run -p 6333:6333 -p 6334:6334 \
  -v $(pwd)/qdrant_storage:/qdrant/storage \
  qdrant/qdrant

# Docker Compose with persistence
cat > docker-compose.yml <<'EOF'
version: '3.8'
services:
  qdrant:
    image: qdrant/qdrant:latest
    ports:
      - "6333:6333"
      - "6334:6334"
    volumes:
      - qdrant_storage:/qdrant/storage
    environment:
      QDRANT__SERVICE__GRPC_PORT: 6334
    configs:
      - source: qdrant_config
        target: /qdrant/config/production.yaml

configs:
  qdrant_config:
    content: |
      storage:
        hnsw_index:
          m: 16
          ef_construct: 100
          full_scan_threshold: 10000

volumes:
  qdrant_storage:
EOF

docker compose up -d

Creating Collections and Indexing

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
from qdrant_client import QdrantClient
from qdrant_client.models import (
    Distance, VectorParams, PointStruct,
    Filter, FieldCondition, MatchValue, Range,
    SearchRequest, PayloadSchemaType
)
import uuid

client = QdrantClient(host="localhost", port=6333)

# Create a collection
client.create_collection(
    collection_name="documents",
    vectors_config=VectorParams(
        size=1536,
        distance=Distance.COSINE,
        # Enable quantization for 4x memory reduction
        # with ~5% recall trade-off
        on_disk=False,   # Keep in RAM for speed
    ),
    # Scalar quantization: compress float32 to int8 (4x less memory)
    quantization_config={
        "scalar": {
            "type": "int8",
            "quantile": 0.99,
            "always_ram": True,   # Keep quantized vectors in RAM
        }
    },
    optimizers_config={
        "default_segment_number": 4,   # Parallel indexing segments
    },
)

# Create payload index for fast filtering
client.create_payload_index(
    collection_name="documents",
    field_name="category",
    field_schema=PayloadSchemaType.KEYWORD,
)
client.create_payload_index(
    collection_name="documents",
    field_name="created_at",
    field_schema=PayloadSchemaType.DATETIME,
)

# Bulk insert with batching
def batch_upsert(vectors, payloads, batch_size=256):
    for i in range(0, len(vectors), batch_size):
        batch_vectors = vectors[i:i + batch_size]
        batch_payloads = payloads[i:i + batch_size]
        points = [
            PointStruct(
                id=str(uuid.uuid4()),
                vector=vec,
                payload=payload,
            )
            for vec, payload in zip(batch_vectors, batch_payloads)
        ]
        client.upsert(collection_name="documents", points=points)
        print(f"Upserted batch {i // batch_size + 1}")

# Search with filtering — Qdrant's killer feature
results = client.search(
    collection_name="documents",
    query_vector=embed("kubernetes deployment strategies"),
    query_filter=Filter(
        must=[
            FieldCondition(
                key="category",
                match=MatchValue(value="kubernetes"),
            ),
            FieldCondition(
                key="score",
                range=Range(gte=0.8),   # Only high-quality documents
            ),
        ]
    ),
    limit=10,
    with_payload=True,
    score_threshold=0.7,   # Minimum similarity score
)

for hit in results:
    print(f"[{hit.score:.3f}] {hit.payload['title']}")

Hybrid search combines dense semantic search with sparse keyword search (BM25-style) for better retrieval:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
from qdrant_client.models import SparseVector, SparseVectorParams, NamedSparseVector, NamedVector

# Collection with both dense and sparse vectors
client.create_collection(
    collection_name="hybrid_docs",
    vectors_config={
        "dense": VectorParams(size=1536, distance=Distance.COSINE),
    },
    sparse_vectors_config={
        "sparse": SparseVectorParams(),
    },
)

# Index a document with both vectors
from fastembed import SparseTextEmbedding, TextEmbedding

sparse_model = SparseTextEmbedding(model_name="prithivida/Splade_PP_en_v1")
dense_model = TextEmbedding(model_name="BAAI/bge-small-en-v1.5")

text = "Kubernetes horizontal pod autoscaling guide"
dense_vec = list(dense_model.embed([text]))[0].tolist()
sparse_vec = list(sparse_model.embed([text]))[0]

client.upsert(
    collection_name="hybrid_docs",
    points=[PointStruct(
        id=str(uuid.uuid4()),
        vector={
            "dense": dense_vec,
            "sparse": SparseVector(
                indices=sparse_vec.indices.tolist(),
                values=sparse_vec.values.tolist()
            ),
        },
        payload={"text": text}
    )]
)

# Hybrid query with Reciprocal Rank Fusion
from qdrant_client.models import Prefetch, FusionQuery, Fusion

results = client.query_points(
    collection_name="hybrid_docs",
    prefetch=[
        Prefetch(
            query=NamedVector(name="dense", vector=dense_query_vec),
            limit=20,
        ),
        Prefetch(
            query=NamedSparseVector(name="sparse", vector=SparseVector(
                indices=sparse_query.indices.tolist(),
                values=sparse_query.values.tolist()
            )),
            limit=20,
        ),
    ],
    query=FusionQuery(fusion=Fusion.RRF),   # Reciprocal Rank Fusion
    limit=10,
)

Weaviate: The ML-Native Database

Weaviate integrates directly with embedding model providers — it can generate embeddings for you via modules, rather than requiring pre-computed vectors.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import weaviate
import weaviate.classes as wvc

client = weaviate.connect_to_local()   # or connect_to_weaviate_cloud()

# Create a collection with automatic vectorization
client.collections.create(
    name="Documents",
    vectorizer_config=wvc.config.Configure.Vectorizer.text2vec_openai(
        model="text-embedding-3-small",
    ),
    generative_config=wvc.config.Configure.Generative.openai(
        model="gpt-4o-mini",
    ),
    properties=[
        wvc.config.Property(name="content", data_type=wvc.config.DataType.TEXT),
        wvc.config.Property(name="category", data_type=wvc.config.DataType.TEXT),
        wvc.config.Property(name="score", data_type=wvc.config.DataType.NUMBER),
    ],
)

documents = client.collections.get("Documents")

# Insert — Weaviate calls OpenAI to generate the embedding automatically
with documents.batch.dynamic() as batch:
    for doc in my_documents:
        batch.add_object({
            "content": doc["text"],
            "category": doc["category"],
            "score": doc["score"],
        })

# Semantic search
results = documents.query.near_text(
    query="container orchestration best practices",
    limit=5,
    filters=wvc.query.Filter.by_property("category").equal("kubernetes"),
    return_metadata=wvc.query.MetadataQuery(score=True, distance=True),
)

# Generative search (RAG built-in)
results = documents.generate.near_text(
    query="kubernetes networking",
    limit=3,
    grouped_task="Summarize these documents into a concise technical guide",
)
print(results.generated)   # LLM-generated summary of retrieved docs

client.close()

Pinecone: Fully Managed, No Ops

Pinecone is a fully managed vector database — no servers to run, no indexes to tune at the infrastructure level. Pay per usage.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
from pinecone import Pinecone, ServerlessSpec

pc = Pinecone(api_key="your-api-key")

# Create a serverless index (AWS us-east-1)
pc.create_index(
    name="documents",
    dimension=1536,
    metric="cosine",
    spec=ServerlessSpec(
        cloud="aws",
        region="us-east-1"
    )
)

index = pc.Index("documents")

# Upsert with namespaces (logical partitioning)
index.upsert(
    vectors=[
        {
            "id": "doc-001",
            "values": embed("some document content"),
            "metadata": {
                "category": "kubernetes",
                "source": "docs",
                "created_at": "2026-03-26",
            }
        }
    ],
    namespace="production"   # Logical partitioning within an index
)

# Query
results = index.query(
    namespace="production",
    vector=embed("container deployment"),
    top_k=10,
    filter={
        "category": {"$eq": "kubernetes"},
        "created_at": {"$gte": "2026-01-01"},
    },
    include_metadata=True,
)

for match in results.matches:
    print(f"[{match.score:.3f}] {match.metadata.get('title', match.id)}")

Choosing the Right Vector Database

pgvector Qdrant Weaviate Pinecone
Hosting Self-hosted Self-hosted / Cloud Self-hosted / Cloud Fully managed
Best scale <5M vectors 10M–1B+ vectors 10M–1B+ vectors Unlimited
Query latency 5–50ms 1–10ms 5–20ms 10–50ms
Filtering Full SQL Excellent (indexed payload) Good Good
Hybrid search Manual Built-in (sparse+dense) Built-in Built-in
Ops burden Low (you know Postgres) Medium Medium None
Cost (1M vectors) ~$0 extra Hosting costs Hosting costs ~$70-140/mo
Transactions ACID (Postgres) Eventual Eventual Eventual
Auto-vectorize No No Yes No
Best for Existing Postgres users, SQL joins, < 5M Performance-critical, self-hosted ML-native apps, auto-embed No-ops, managed, fast start

Decision guide:

  • Running Postgres already and < 5M vectors? → pgvector
  • Need maximum self-hosted performance, hybrid search, or > 10M vectors? → Qdrant
  • Want auto-embedding + built-in RAG + no embedding pipeline? → Weaviate
  • No ops team, want to focus on product, okay with managed cost? → Pinecone

Production Patterns

Chunking Strategy for RAG

How you split documents into chunks dramatically affects retrieval quality:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
from langchain.text_splitter import RecursiveCharacterTextSplitter

# Recursive splitting (respects document structure)
splitter = RecursiveCharacterTextSplitter(
    chunk_size=512,          # Characters per chunk
    chunk_overlap=64,        # Overlap to avoid losing context at boundaries
    separators=["\n\n", "\n", ". ", " ", ""],   # Split order
)

def chunk_and_index(document: str, doc_id: str, metadata: dict):
    chunks = splitter.split_text(document)
    for i, chunk in enumerate(chunks):
        embedding = embed(chunk)
        client.upsert(
            collection_name="documents",
            points=[PointStruct(
                id=f"{doc_id}-chunk-{i}",
                vector=embedding,
                payload={
                    **metadata,
                    "chunk_index": i,
                    "total_chunks": len(chunks),
                    "text": chunk,
                    "doc_id": doc_id,
                }
            )]
        )

Embedding Cache

Embedding generation is expensive and idempotent — cache aggressively:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import hashlib
import redis
import json

cache = redis.Redis(host="redis", port=6379)
CACHE_TTL = 7 * 24 * 3600   # 1 week

def embed_cached(text: str, model: str = "text-embedding-3-small") -> list[float]:
    # Deterministic cache key
    key = f"embed:{model}:{hashlib.sha256(text.encode()).hexdigest()}"

    cached = cache.get(key)
    if cached:
        return json.loads(cached)

    embedding = embed(text)   # Call OpenAI / local model
    cache.setex(key, CACHE_TTL, json.dumps(embedding))
    return embedding

Metadata Filtering Design

The golden rule: pre-filter in the database, not in your application code. Applying metadata filters before vector search dramatically reduces the search space.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# Bad: fetch 1000 results, filter in Python
results = client.search(collection_name="docs", query_vector=vec, limit=1000)
filtered = [r for r in results if r.payload["user_id"] == current_user_id][:10]

# Good: filter in database, search smaller space
results = client.search(
    collection_name="docs",
    query_vector=vec,
    query_filter=Filter(
        must=[FieldCondition(key="user_id", match=MatchValue(value=current_user_id))]
    ),
    limit=10,
)

For Qdrant, create payload indexes on all fields used in filters — this moves filtering from O(N) scan to O(log N) lookup.

Handling Index Updates

Vectors are immutable content-wise — you delete and re-insert when content changes. For document updates:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
def update_document(doc_id: str, new_content: str, metadata: dict):
    # Delete old chunks
    client.delete(
        collection_name="documents",
        points_selector=Filter(
            must=[FieldCondition(key="doc_id", match=MatchValue(value=doc_id))]
        )
    )
    # Re-index with new content
    chunk_and_index(new_content, doc_id, metadata)

Monitoring Vector Search Quality

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
# Track retrieval quality with a test set
TEST_QUERIES = [
    ("How do I configure RBAC in Kubernetes?", ["kubernetes-rbac-guide", "k8s-security"]),
    ("What is gradient descent?", ["ml-fundamentals", "optimization-guide"]),
]

def evaluate_recall_at_k(k=10):
    hits = 0
    for query, expected_ids in TEST_QUERIES:
        results = search(query, k=k)
        retrieved_ids = {r["doc_id"] for r in results}
        if any(eid in retrieved_ids for eid in expected_ids):
            hits += 1

    recall = hits / len(TEST_QUERIES)
    print(f"Recall@{k}: {recall:.2%}")
    return recall

# Run periodically (e.g., after re-indexing or model changes)
evaluate_recall_at_k(10)

Quick Reference

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# pgvector
CREATE EXTENSION vector;
CREATE TABLE t (id BIGSERIAL, v VECTOR(1536));
CREATE INDEX ON t USING hnsw (v vector_cosine_ops) WITH (m=16, ef_construction=64);
SELECT id, 1 - (v <=> '[...]'::vector) AS sim FROM t ORDER BY v <=> '[...]'::vector LIMIT 10;

# Qdrant Python
from qdrant_client import QdrantClient
from qdrant_client.models import Distance, VectorParams, PointStruct, Filter, FieldCondition, MatchValue

client = QdrantClient(host="localhost", port=6333)
client.create_collection("docs", vectors_config=VectorParams(size=1536, distance=Distance.COSINE))
client.upsert("docs", [PointStruct(id="1", vector=[...], payload={"category": "k8s"})])
results = client.search("docs", query_vector=[...], query_filter=Filter(must=[FieldCondition(key="category", match=MatchValue(value="k8s"))]), limit=10)

# Pinecone Python
from pinecone import Pinecone
pc = Pinecone(api_key="...")
index = pc.Index("my-index")
index.upsert(vectors=[{"id": "1", "values": [...], "metadata": {"cat": "k8s"}}])
results = index.query(vector=[...], top_k=10, filter={"cat": "k8s"}, include_metadata=True)

# Key distance operators (pgvector)
<=>   cosine distance
<->   L2 (Euclidean) distance
<#>   negative inner product (dot product similarity)
<+>   L1 (Manhattan) distance

# Common embedding dimensions
text-embedding-3-small:  1536
text-embedding-3-large:  3072
BAAI/bge-small-en-v1.5:  384
nomic-embed-text:         768
all-MiniLM-L6-v2:         384

Vector databases are maturing fast. The choice that’s right for a 100K-document knowledge base (pgvector, simple and already there) is different from the right choice for a billion-document retrieval system (Qdrant with DiskANN, or a managed service). Start simple — pgvector on your existing Postgres instance handles the overwhelming majority of real-world RAG workloads. Migrate to a purpose-built database when you hit actual performance limits, not before.

Comments