Free lesson · LLMOps Engineering
Deploy pgvector and build embedding ingestion pipeline with operational monitoring
You will deploy pgvector and build an embedding ingestion pipeline with operational instrumentation. Deploy PostgreSQL with pgvector extension in your vCluster. Create embedding tables with proper indexing (HNSW for approximate nearest neighbor). Build EmbeddingIngestionPipeline: reads documents from a source queue, chunks documents using configurable strategy, calls embedding model via LiteLLM, and stores vectors in pgvector. Instrument the pipeline: embedding_pipeline_throughput_docs_per_second, embedding_pipeline_latency_seconds{stage} (chunking, embedding, storage), embedding_pipeline_errors_total{error_type}, embedding_pipeline_queue_depth. Implement pipeline health checks: verify embedding model is accessible, verify pgvector is writable, verify queue is being drained.
Course: GenAI Operations · Chapter 38 · Embedding Pipeline Ops
Free to read — no subscription required.
Introduction
When you run embedding pipelines in production, the gap between "it works locally" and "it runs reliably at scale" appears fast: writes fail silently, duplicate chunks accumulate, and there is no signal when throughput degrades. pgvector closes that gap by bringing vector similarity search into PostgreSQL, where you can enforce deduplication constraints, index embeddings for fast retrieval, and attach Prometheus metrics to the same database your application already trusts. By the end of this lesson, you'll have deployed pgvector on Kubernetes with an HNSW index configured for cosine similarity and a Helm-driven deployment script that bootstraps the full schema on first startup.
Key Terminology
- pgvector — A PostgreSQL extension that adds a native
vectorcolumn type and vector similarity operators, enabling approximate nearest-neighbor search inside the same database that stores your application data; activated per-database withCREATE EXTENSION IF NOT EXISTS vector. - HNSW index — Hierarchical Navigable Small World index, a graph-based approximate nearest-neighbor structure created with
USING hnsw; them=16parameter controls graph connectivity andef_construction=200controls build-time recall quality, together determining the throughput/accuracy trade-off for cosine similarity queries. - chunk-level deduplication — A database-enforced guarantee that each
(document_id, chunk_index)pair appears at most once in theembeddingstable, implemented via aUNIQUE(document_id, chunk_index)constraint that prevents silent accumulation of duplicate embedding rows across pipeline reruns. - init ConfigMap — A Kubernetes
ConfigMap(herepgvector-init) that holds SQL bootstrapping scripts mounted into the PostgreSQL pod; the Bitnami chart'sprimary.initdb.scriptsConfigMaphook executes these scripts automatically on first boot, so schema creation is declarative and version-controlled rather than a manual step. - idempotent Helm deployment — The
helm upgrade --installinvocation pattern used indeploy_pgvector()that either installs a new release or updates an existing one without error, making repeated script executions safe and enabling the deployment to serve as a reliable CI step or operational runbook. - Prometheus ServiceMonitor — A Kubernetes custom resource enabled via
metrics.serviceMonitor.enabled=truein the Helm values that registers the pgvector pod as a scrape target, routing throughput and query-latency metrics into the cluster's monitoring stack for operational visibility.
Concepts
Why PostgreSQL Instead of a Dedicated Vector Store
The core architectural decision in this lesson is running vector similarity search inside PostgreSQL via pgvector rather than deploying a standalone vector database. The motivation is operational: your application already trusts PostgreSQL for transactional data, and adding a vector column to an existing table means you can join embeddings with metadata, enforce foreign keys, and apply familiar backup and access-control policies — all without a second stateful system to operate.
The trade-off is real. A dedicated vector store can scale similarity search horizontally with less ceremony. But for embedding pipelines that are already PostgreSQL-native — where document_id lives in a relational row, where updated_at drives staleness queries, and where you want UNIQUE constraints to prevent duplicate ingestion — pgvector keeps the operational surface small. A single database handles storage, deduplication, and retrieval.
HNSW Indexes and the Recall/Throughput Trade-off
Vector similarity search is an approximate problem. An exhaustive scan over millions of 1536-dimensional vectors is prohibitively slow, so pgvector builds a Hierarchical Navigable Small World graph over the embedding space. When a query arrives, the HNSW algorithm traverses this graph, skipping most of the space and returning approximate nearest neighbors in milliseconds.
Two parameters control the quality of that approximation at index-build time. m=16 sets how many bidirectional links each node maintains in the graph — higher values improve recall and query-time navigation at the cost of memory and slower inserts. ef_construction=200 controls how many candidates the builder considers when placing each new node — larger values produce a higher-quality graph but slow down ingestion. The values chosen here (m=16, ef_construction=200) are a conventional starting point for high-recall cosine similarity over 1536-dimensional OpenAI-compatible embeddings. Production tuning requires measuring recall against your actual query distribution (see Code Walkthrough for the exact CREATE INDEX statement).
The vector_cosine_ops operator class tells pgvector to optimize the graph for cosine distance specifically. Using a different distance metric at query time after building with vector_cosine_ops returns meaningless results, so the index operator class and the query operator must match.
Declarative Schema Bootstrap and Idempotent Deployment
A recurring failure mode for stateful infrastructure is schema drift: the database the application expects diverges from what is actually deployed, and the discrepancy only surfaces at runtime. This lesson closes that gap with two interlocking patterns.
First, the init ConfigMap externalizes the entire schema — extension, table, constraints, and indexes — as a SQL file under version control. The Bitnami PostgreSQL chart mounts this ConfigMap and executes it on first pod startup, meaning a fresh deployment always arrives in the correct state without a manual migration step.
Second, deploy_pgvector() uses helm upgrade --install, which is idempotent by construction: running it against a cluster that already has the release applies any changed values and exits cleanly; running it against a fresh cluster installs from scratch. The --wait --timeout 300s flags turn the call into a synchronous gate — the script only returns after the PostgreSQL pod has passed its readiness probe, so any downstream pipeline step that runs immediately afterward can assume the database is accepting connections. Together these two patterns mean the deployment script is also a safe runbook for operators: run it to install, run it again to reconfigure, and the cluster converges to the declared state either way (see Code Walkthrough for the full helm_args list).
Code Walkthrough
Now that you understand how HNSW indexes and chunk-level deduplication work together in pgvector, the following two artifacts wire those concepts into a runnable Kubernetes deployment: a ConfigMap that bootstraps the schema on first startup, and a Python helper that drives the Helm installation idempotently.
The pgvector-init ConfigMap holds the SQL script that PostgreSQL executes automatically when the pod first boots. It enables the vector extension, creates the embeddings table with a vector(1536) column sized for OpenAI-compatible models, and enforces chunk-level deduplication through a UNIQUE(document_id, chunk_index) constraint. The HNSW index is tuned with m=16 and ef_construction=200 for high-recall cosine similarity search, and two supporting B-tree indexes on document_id and updated_at accelerate the document lookups and staleness queries used by the reprocessing logic.
Code snippetyaml
1# helm-values/pgvector-init-configmap.yaml 2apiVersion: v1 3kind: ConfigMap 4metadata: 5 name: pgvector-init 6 namespace: embedding-ops 7data: 8 init-pgvector.sql: | 9 CREATE EXTENSION IF NOT EXISTS vector; 10 CREATE TABLE embeddings ( 11 id UUID PRIMARY KEY DEFAULT gen_random_uuid(), 12 document_id TEXT NOT NULL, 13 chunk_index INTEGER NOT NULL, 14 content TEXT NOT NULL, 15 embedding vector(1536), 16 metadata JSONB DEFAULT '{}', 17 created_at TIMESTAMPTZ DEFAULT NOW(), 18 updated_at TIMESTAMPTZ DEFAULT NOW(), 19 source_modified_at TIMESTAMPTZ, 20 UNIQUE(document_id, chunk_index) 21 ); 22 CREATE INDEX idx_embeddings_hnsw 23 ON embeddings 24 USING hnsw (embedding vector_cosine_ops) 25 WITH (m = 16, ef_construction = 200); 26 CREATE INDEX idx_embeddings_document_id ON embeddings (document_id); 27 CREATE INDEX idx_embeddings_updated_at ON embeddings (updated_at);
With the schema ConfigMap in place, deploy_pgvector drives the Helm installation. It targets the Bitnami PostgreSQL OCI chart, overrides the default image with ankane/pgvector (which ships the extension pre-compiled against PostgreSQL 16), sets memory and CPU requests appropriate for HNSW in-memory workloads, mounts the init ConfigMap so the schema runs automatically on first boot, and enables the Prometheus ServiceMonitor so throughput and query-latency metrics flow into the cluster's monitoring stack. Using helm upgrade --install makes the call idempotent — re-running it on an already-deployed release applies value changes without creating a duplicate release. The --wait --timeout 300s flags block the script until the PostgreSQL pod passes its readiness probe, so subsequent pipeline steps can safely assume the database is accepting connections.
Code snippetpython
1# scripts/deploy_pgvector.py 2import subprocess 3import sys 4 5def deploy_pgvector(namespace: str = "embedding-ops") -> None: 6 """Deploy PostgreSQL with pgvector extension via Helm.""" 7 helm_args = [ 8 "helm", "upgrade", "--install", "pgvector", 9 "oci://registry-1.docker.io/bitnamicharts/postgresql", 10 "--namespace", namespace, 11 "--create-namespace", 12 "--set", "image.repository=ankane/pgvector", 13 "--set", "image.tag=v0.7.4-pg16", 14 "--set", "primary.persistence.size=50Gi", 15 "--set", "primary.resources.requests.memory=2Gi", 16 "--set", "primary.resources.requests.cpu=1000m", 17 "--set", "primary.initdb.scriptsConfigMap=pgvector-init", 18 "--set", "auth.postgresPassword=changeme", 19 "--set", "metrics.enabled=true", 20 "--set", "metrics.serviceMonitor.enabled=true", 21 "--wait", "--timeout", "300s", 22 ] 23 result = subprocess.run(helm_args, capture_output=True, text=True) 24 if result.returncode != 0: 25 print(f"Helm deploy failed: {result.stderr}", file=sys.stderr) 26 sys.exit(1) 27 print("pgvector deployed successfully") 28 29if __name__ == "__main__": 30 deploy_pgvector()
Confirm that the deployment succeeded by running kubectl get pods -n embedding-ops until pgvector-postgresql-0 shows Running, then connecting to the database and executing SELECT extname FROM pg_extension WHERE extname = 'vector'; — a single returned row confirms the extension is active and the schema bootstrap ran cleanly.
Do's and Don'ts
Having walked through the material above, the following Do's and Don'ts distill it into practice.
Do's
- ✓Do use
helm upgrade --installwith--wait --timeout 300s— the idempotent upgrade path means re-runningdeploy_pgvectoron an already-deployed release applies value changes without creating a duplicate, and the wait flags block downstream pipeline steps untilpgvector-postgresql-0passes its readiness probe so you never race against an unready database. - ✓Do set
UNIQUE(document_id, chunk_index)on theembeddingstable — this constraint is what prevents silent duplicate accumulation when chunks are reprocessed; without it, repeated ingestion runs stack redundant rows that corrupt similarity search results and inflate index size invisibly. - ✓Do enable
metrics.enabled=trueandmetrics.serviceMonitor.enabled=truein the Helm values — these flags wire the PostgreSQL exporter into the cluster's Prometheus stack at deploy time, giving you the throughput and query-latency signals you need to detect pipeline degradation before failures become user-visible.
Don'ts
- ✗Don't swap out
ankane/pgvectorfor the default Bitnami PostgreSQL image — the default image does not ship thevectorextension pre-compiled, so theCREATE EXTENSION IF NOT EXISTS vectorline in the init ConfigMap will fail silently and theembeddingstable will never be created, leaving downstream pipeline steps with no schema to write to. - ✗Don't remove or reduce the HNSW
m=16, ef_construction=200parameters without understanding the recall trade-off — lowering these values speeds up index builds but reduces recall for cosine similarity queries; the B-tree indexes ondocument_idandupdated_atare separate and do not compensate for a degraded HNSW configuration. - ✗Don't skip verifying the extension with
SELECT extname FROM pg_extension WHERE extname = 'vector';after deployment — the init ConfigMap only runs on first boot, so a pod that restarted against an already-initialized volume may appear healthy while the schema bootstrap never executed; the single-row query is the only reliable confirmation that the extension is active and all three indexes exist.
This lesson is free to read. Its hands-on lab — real code, in a cloud IDE — is part of the LLMOps Engineering subscription.
From · cancel anytime · Already a subscriber? Sign in →
More free lessons in GenAI Operations
- Ch 34Deploy Redis Semantic Cache and Measure Hit Rate vs Cost Savings
- Ch 34Compare Provider Caching Strategies for OpenAI, Anthropic, and Google
- Ch 38Deploy pgvector and build embedding ingestion pipeline with operational monitoringYou are here
- Ch 39Implement pgvector index maintenance with VACUUM and reindexing schedules
- Ch 39Deploy Qdrant and compare operational characteristics with pgvector
- Ch 41Compare retrieval quality across embedding models with Cohere Rerank
- Ch 43Build completeness checks for embedding coverage and knowledge graph gaps