Home Database pgvector Postgres Search Scales — HNSW Guide That Wins
Intermediate 3 min · September 07, 2026
pgvector Postgres Vector Search Guide

pgvector Postgres Search Scales — HNSW Guide That Wins

A second vector database means new backups, auth, and JOIN pain.

N
Naren Founder & Principal Engineer

20+ years shipping high-throughput database systems. Everything here is grounded in real deployments.

Follow
Production
production tested
September 22, 2026
last updated
1,799
articles · all by Naren
Before you start⏱ 30 min
  • Postgres 15+ running locally or in Docker
  • Basic SQL: CREATE TABLE, indexes, EXPLAIN
  • Embeddings from any model (1536-dim examples)
 ● Production Incident 🔎 Debug Guide
Quick Answer
  • pgvector is the Postgres extension for vector similarity search: vector/halfvec/bit/sparsevec types plus HNSW and IVFFlat approximate indexes beside your relational data
  • Core query shape: ORDER BY embedding <=> query LIMIT k with operators <-> (L2), <=> (cosine), <#> (inner product) matched to index opclasses
  • Performance insight: HNSW cuts p95 vector latency ~10x versus exact scan at million-row scale, with ef_search (default 40) trading recall for speed per query
  • Production rule: filtering applies AFTER the approximate scan — a 10% filter with ef_search 40 yields ~4 matches, so add B-tree/partial indexes and enable hnsw.iterative_scan
  • Build indexes after bulk loads (not before), consider halfvec indexing to halve size, and fuse with full-text search for hybrid retrieval
  • Biggest mistake: celebrating latency wins without measuring recall@k on filtered query shapes — fast wrong answers look green
✦ Definition~90s read
What is pgvector Postgres Vector Search?

pgvector is an open-source Postgres extension for vector similarity search: typed columns (vector to 2000 dims, halfvec to 4000, bit to 64000, sparsevec), distance operators (L2, cosine, inner product, Hamming, Jaccard), and approximate HNSW/IVFFlat indexes — all inside standard Postgres with ACID, JOINs, and point-in-time recovery.

Imagine a library that files books only by title.

Its architecture stores embeddings as first-class columns and serves nearest-neighbor queries as ORDER BY distance LIMIT k. HNSW builds a multilayer graph for top-tier speed-recall balance; IVFFlat partitions into lists for faster builds. Version 0.8+ adds iterative index scans that extend filtered scans until LIMIT fills.

The trade-off is approximate semantics: indexed results differ from exact search, filtering applies post-scan, and single-node Postgres bounds total scale. Dedicated vector databases win past ~100M vectors or with exotic pre-filtering — below that, pgvector's operational simplicity dominates.

Plain-English First

Imagine a library that files books only by title. Finding books about sailing means reading every spine — that's keyword search on meanings. Vector search is the librarian who has actually read everything and points you to the right shelf by topic. pgvector hires that librarian inside your existing library (Postgres) instead of building a second building across town. Your books (data) and the librarian's topic map (embeddings) live under one roof, share one checkout system (transactions), and get backed up together. The quirks are librarian quirks: ask for sailing books by a specific author (filtered search) and they grab an armful of sailing books first, then check authors — sometimes coming back short. The fix is organizing popular authors' shelves separately (partial indexes) or telling the librarian to keep grabbing until the quota fills (iterative scans).

⚙ Browser compatibility
Latest versions — ✓ supported
ChromeFirefoxSafariEdge

RAG apps need somewhere to put embeddings. Spinning up a dedicated vector database means a new cluster, new auth, new backups, and joins that cross system boundaries. For most teams that's a second database to babysit before product-market fit.

pgvector skips all that. It's a Postgres extension storing vectors beside your relational data — same transactions, same backups, same JOINs. You'll write your first semantic query in minutes, not sprints.

But vectors have sharp edges. Approximate indexes return different results than exact scans, filters apply after the scan, and dimension limits bite the unprepared. This guide covers the setup that stays fast past a million rows.

Why Vectors Belong in Postgres Until Scale Says Otherwise

A RAG feature needs three things: store embeddings, find nearest neighbors fast, and join results with app data. A dedicated vector database handles the first two and punts the third across a network boundary — two systems, two auth models, two backup stories, and application joins in code.

pgvector keeps all three in Postgres. Vectors live in typed columns, nearest-neighbor queries are ORDER BY with distance operators, and results JOIN directly to users, products, and permissions. Transactions cover vectors and rows together; point-in-time recovery covers both.

The honest limit is scale and exoticism: hundreds of millions of vectors or custom sharding still favor dedicated stores. Below that, one database beats two on every operational axis that matters.

📊 Production Insight
Teams that start RAG in pgvector ship semantic search in days; teams that start with a new cluster spend the first sprint on auth, VPCs, and backup runbooks.
🎯 Key Takeaway
One database for vectors plus rows beats two systems on transactions, JOINs, backups, and staffing.

Store, Query, Then Index — The Safe Order

Enable the extension, store embeddings in a typed column, and query with ORDER BY plus a distance operator before building any index. Exact search is slow but perfectly correct — it becomes your recall baseline.

Only then add HNSW with the opclass matching your embeddings: vector_cosine_ops for normalized embeddings, vector_l2_ops for raw, vector_ip_ops for inner product. Index and query operators must match or the index sits idle.

Index filter columns separately from day one. That B-tree on category_id looks optional until filtered recall collapses — by then you're debugging in production.

SQL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
CREATE EXTENSION IF NOT EXISTS vector;

CREATE TABLE docs (
  id serial PRIMARY KEY,
  category_id int NOT NULL,
  content text NOT NULL,
  embedding vector(1536) NOT NULL
);

-- Exact search first (perfect recall, no index):
SELECT id, content
FROM docs
ORDER BY embedding <=> '[0.1,0.2,...]'::vector
LIMIT 5;

-- Then accelerate with HNSW (cosine):
CREATE INDEX ON docs USING hnsw (embedding vector_cosine_ops);
CREATE INDEX ON docs (category_id);  -- filters need their own index
📊 Production Insight
Baseline exact-search recall before indexing. Without the baseline you'll never know whether the fast answers are the right answers.
🎯 Key Takeaway
Exact search baselines recall; matched opclass indexes accelerate; filter columns get B-trees early.

HNSW vs IVFFlat — Indexes, Types, and Build Rules

HNSW builds a multilayer graph: excellent speed-recall balance, slow memory-hungry construction. IVFFlat partitions vectors into lists: fast builds, weaker queries. Type support spans vector (2000 dims), halfvec (4000), bit (64000), and sparsevec (1000 non-zeros).

Build-time parameters m (graph connections, default 16) and ef_construction (candidate list, default 64) trade build cost for recall. Query-time ef_search (default 40) trades speed for recall per query — raise it with SET LOCAL for hard queries without rebuilding.

Operational rules: load data first, index second; raise max_parallel_maintenance_workers for big builds; consider halfvec casts to halve index size. An index built over loaded data beats one maintained through the load, every time.

📊 Production Insight
Indexing before a million-row load turns a minutes-long COPY into an hours-long graph-maintenance slog. Load first is the highest-ROI rule in this guide.
🎯 Key Takeaway
HNSW for queries, IVFFlat for fast builds, load-then-index always, tune ef_search per shape.

Filtering, Iterative Scans, and Half-Precision at Scale

Approximate indexes scan first and filter after, so selective filters starve. The fixes compose: B-tree indexes on filter columns for exact-friendly shapes, partial HNSW indexes for hot values, partitioning for many values, and iterative scans that extend the scan until LIMIT fills.

Strict ordering keeps exact distance order; relaxed ordering allows slight disorder for better recall. Pick strict for user-facing ranking, relaxed for candidate generation feeding a reranker.

Half-precision indexing halves size with minor recall cost — the standard move past a few million vectors. Cast both index and query to halfvec or the pair won't meet.

SQL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
-- Partial index for the hot slice:
CREATE INDEX ON docs USING hnsw (embedding vector_cosine_ops)
WHERE (category_id = 42);

-- Iterative scans keep scanning until LIMIT fills (0.8+):
SET hnsw.iterative_scan = strict_order;
-- relaxed_order trades exact ordering for better recall

-- Half-precision index, half the size:
CREATE INDEX ON docs
USING hnsw ((embedding::halfvec(1536)) halfvec_cosine_ops);
SELECT * FROM docs
ORDER BY embedding::halfvec(1536) <=> '[0.1,...]'::halfvec(1536)
LIMIT 5;
⚠ Filtering is the recall killer
With ef_search 40 and a 10% filter, expect ~4 surviving candidates. Filtered recall needs its own indexes and iterative scans — the bare HNSW index is not enough.
📊 Production Insight
The marketplace incident recovered to 0.98 recall with 15ms extra p95 using exactly this trio: partial index, iterative scan, ef_search 100.
🎯 Key Takeaway
Partial indexes plus iterative scans plus halfvec casts keep filtered recall near exact.

Hybrid Search and Beyond — Fusion, Quantization, Monitoring

Semantic search shines combined with keywords: Postgres full-text search (plainto_tsquery plus ts_rank) handles exact terms while vectors handle meaning. Fuse with Reciprocal Rank Fusion or a cross-encoder reranker for results neither method reaches alone.

Subvector and binary quantization extend the range further: index leading dimensions for coarse retrieval, binary vectors with Hamming distance for massive-scale pre-filtering. Each technique trades a little recall for a lot of speed — measure, don't assume.

Monitor pg_stat_progress_create_index during builds and recall@k dashboards after. Index work is visible in Postgres natively, which is another quiet win of staying inside the database you already operate.

SQL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ALTER TABLE docs ADD COLUMN textsearch tsvector
GENERATED ALWAYS AS (to_tsvector('english', content)) STORED;
CREATE INDEX ON docs USING GIN (textsearch);

WITH semantic AS (
  SELECT id, row_number() OVER () AS rnk FROM docs
  ORDER BY embedding <=> '[0.1,0.2,...]'::vector LIMIT 20
),
keyword AS (
  SELECT id, row_number() OVER () AS rnk FROM docs
  WHERE textsearch @@ plainto_tsquery('refund policy') LIMIT 20
)
SELECT COALESCE(s.id, k.id) AS id,
  COALESCE(1.0/(60+s.rnk),0) + COALESCE(1.0/(60+k.rnk),0) AS rrf
FROM semantic s FULL JOIN keyword k ON s.id = k.id
ORDER BY rrf DESC LIMIT 5;
📊 Production Insight
Hybrid retrieval consistently outperforms pure vector search on real catalogs where exact SKUs, names, and part numbers matter as much as meaning.
🎯 Key Takeaway
Keyword plus vector fusion beats either alone; quantization extends scale; native stats keep builds visible.

When to Leave Postgres — Honest Exit Criteria

Stay in pgvector while one Postgres handles the load with headroom: JOINs stay local, transactions stay simple, and your team operates one system. Move out when vectors exceed comfortable single-node scale, when exotic filtering dominates, or when multitenant isolation needs dedicated infrastructure.

Migration paths stay open because embeddings are portable — the same vectors load into dedicated stores later. Starting in pgvector doesn't trap you; it defers the distributed-systems bill until revenue justifies it.

That deferral is the strategy: ship semantic search this sprint inside the database you already back up, and revisit the architecture when measurements — not hype — demand it.

📊 Production Insight
Most teams that leave early cite imagined scale. Most that stay too long cite measured pain. Measure recall, latency, and index build time quarterly and let numbers decide.
🎯 Key Takeaway
Leave on measured scale pain, not launch-day hype; embeddings port cleanly whenever you go.
● Production incidentPOST-MORTEMseverity: high

The 10x Speedup That Quietly Halved Recommendation Quality

Symptom
Click-through on recommendations slid 12% over three weeks with no errors, no latency change, and green dashboards. A data scientist finally A/B'd approximate versus exact search and found filtered recall at 0.6 — the index was fast and wrong.
Assumption
The team assumed an HNSW index was a magic accelerator that preserved exact-search semantics, and that filters composed with vector search the way they compose with B-trees. Nobody measured recall; dashboards tracked latency only.
Root cause
With ef_search at the default 40 and category filters matching ~10% of rows, post-scan filtering left ~4 candidates per query on average. Unfiltered queries were fast and correct; filtered ones returned whatever survived instead of the true nearest neighbors. The team celebrated the latency win and never computed recall on the filtered shapes that drove 70% of traffic.
Fix
They added a B-tree index on category_id, converted the hot-category query to a partial HNSW index, enabled hnsw.iterative_scan = strict_order, and raised ef_search to 100 for filtered queries. Recall recovered to 0.98 with p95 latency up only 15ms. Recall@k dashboards now gate every index change.
Key lesson
  • Approximate indexes change results, not just speed. Any HNSW rollout needs recall@k measurement on real query shapes, including filtered ones.
  • Latency dashboards without recall dashboards lie. A fast wrong answer looks green until customers complain.
Production debug guideFour failure patterns behind most pgvector incidents — with exact diagnostics.4 entries
Symptom · 01
Vector query ignores the HNSW index and seq-scans
Fix
Check EXPLAIN for index usage and confirm the query operator matches the index opclass (vector_cosine_ops queried with <=>). Fix: rebuild the index with the matching operator or cast the query to match. Mismatched pairs silently skip the index.
Symptom · 02
Nearest neighbors look wrong after adding the index
Fix
Compare approximate results against exact search (drop the index or set enable_indexscan off) on a labeled sample to compute recall@k. Fix: raise hnsw.ef_search (try 100), increase ef_construction at build, or switch IVFFlat to HNSW.
Symptom · 03
Filtered queries return fewer rows than LIMIT asks for
Fix
Check filter selectivity: if the WHERE clause matches a small slice, add a B-tree index on the filter column or a partial HNSW index for that value. Enable SET hnsw.iterative_scan = strict_order so scans continue until LIMIT fills.
Symptom · 04
Index builds take hours and the index dwarfs the table
Fix
Check index size versus table size and build duration logs. Fix: index halfvec casts instead of full vectors, raise max_parallel_maintenance_workers, and build after loads — never before.
pgvector HNSW vs IVFFlat vs Dedicated DBs
Featurepgvector HNSWpgvector IVFFlatDedicated vector DB
Recall/speedBest speed-recall trade-offFaster builds, weaker queriesComparable at higher ops cost
Build costSlow, memory-hungryFast, lightManaged away
FilteringPost-scan (+ iterative scans 0.8+)Post-scan, weakerNative pre-filtering
OpsYour Postgres, ACID, JOINsYour PostgresNew cluster to run
Dimensionsvector 2000, halfvec 4000Same typesOften higher limits
Best forRAG inside existing PostgresBulk-load then query100M+ vectors, exotic filters
⚙ Quick Reference
3 commands from this guide
FileCommand / CodePurpose
CREATE EXTENSION IF NOT EXISTS vector;Store, Query, Then Index
CREATE INDEX ON docs USING hnsw (embedding vector_cosine_ops)Filtering, Iterative Scans, and Half-Precision at Scale
ALTER TABLE docs ADD COLUMN textsearch tsvectorHybrid Search and Beyond

Key takeaways

1
pgvector keeps vectors in Postgres
ACID, JOINs, backups, and semantic search in one system.
2
Default to HNSW with the operator matching your embeddings; build indexes after bulk loads.
3
Filtering applies post-scan
add B-tree/partial indexes and enable iterative scans.
4
Half-precision indexing halves size; tune ef_search per query shape and measure recall@k.
5
Fuse with full-text search for hybrid retrieval that beats either method alone.

Common mistakes to avoid

4 patterns
×

Mixing distance operators between index and query

Symptom
The index never gets used or results look random. Cosine-indexed data queried with L2 silently falls back to exact scan or returns misranked rows.
Fix
Pick the operator matching your embeddings (cosine for normalized, L2 for raw, inner product for max-sim) and use the same one in queries and indexes. Document the choice next to the embedding code.
×

Creating the HNSW index before loading data

Symptom
Bulk load crawls as every insert maintains the graph, and the final index is worse than one built once over loaded data. Hours lost on million-row imports.
Fix
Build the HNSW index AFTER bulk-loading initial data, raise max_parallel_maintenance_workers for the build, and only then enable iterative scans for filtered queries.
×

Assuming one HNSW index handles filtered queries

Symptom
Filtered searches return too few rows or wrong ones: with ef_search 40 and a 10% filter, only ~4 candidates survive post-filtering. Recall collapses silently.
Fix
Create B-tree indexes on filter columns for selective filters, partial HNSW indexes for hot slices, and enable SET hnsw.iterative_scan for the rest. Measure recall per query shape.
×

Storing everything at full precision at scale

Symptom
Index size and build time balloon 2x for recall gains nobody measures. At millions of vectors the bill — disk, RAM, build hours — dwarfs any quality delta.
Fix
Store full precision for truth, index halfvec for speed: CREATE INDEX ... ((embedding::halfvec(1536)) halfvec_l2_ops) and query with the same cast. Verify recall on a sample before switching reads.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
What is pgvector and why keep vectors in Postgres?
Q02SENIOR
Why does filtering break approximate vector search, and how do you fix i...
Q03SENIOR
How do you index and tune pgvector at million-row scale?
Q01 of 03SENIOR

What is pgvector and why keep vectors in Postgres?

ANSWER
pgvector adds vector similarity search to Postgres: vector/halfvec/bit/sparsevec types, distance operators (<-> L2, <=> cosine, <#> inner product), and HNSW/IVFFlat approximate indexes. Data stays with the rest of the app — ACID, JOINs, point-in-time recovery included. Default exact search gives perfect recall; approximate indexes trade recall for speed at scale.
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
Should I use HNSW or IVFFlat?
02
What vector types and dimension limits exist?
03
Can I do hybrid keyword plus vector search?
04
Why do my filtered vector queries return too few rows?
05
How do I tune recall versus speed?
N
Naren Founder & Principal Engineer

20+ years shipping high-throughput database systems. Everything here is grounded in real deployments.

Follow
Verified
production tested
September 22, 2026
last updated
1,799
articles · all by Naren
🔥

That's Vectors. Mark it forged?

3 min read · try the examples if you haven't

Previous
Neo4j Use Cases — When to Use a Graph Database
1 / 1 · Vectors
Next
DuckDB Embedded Analytics