pgvector · PostgreSQL · Vector search

Tune pgvector HNSW for Recall, Latency, Filtering, and Memory

Use an exact-search baseline and a repeatable benchmark to tune pgvector HNSW construction, query, filtering, and memory parameters.

Published
Updated
Code examples
Illustrative
Reading time
5 minutes

Tune HNSW against measured recall and tail latency, not a copied configuration. The reliable process is to preserve an exact-search baseline, build indexes with controlled parameters, sweep query settings on fixed queries, and repeat the experiment with the filters and concurrency found in production.

This article specifies that experiment for pgvector 0.8.2. It intentionally reports no universal “best” setting because dataset size, dimensionality, hardware, data distribution, filters, writes, and latency targets all affect the answer.

Understand the parameters you are trading

HNSW builds a multilayer proximity graph. In pgvector, m controls the maximum connections per layer and ef_construction controls the candidate list used during index construction. Higher values can improve the graph at the cost of build time and memory. At query time, hnsw.ef_search controls the candidate list; increasing it generally provides more opportunity to recover neighbors but does more work. Exact meanings and defaults are documented in the project’s HNSW section.

Keep these control planes separate:

PhasePrimary settingsMeasure
Buildm, ef_construction, workers, maintenance memoryDuration, peak memory, index bytes, WAL
Queryhnsw.ef_searchRecall, p50/p95/p99 latency, CPU
Filtered queryiterative mode, max tuples, scan memoryReturned count, recall, visited work, latency
Storagevector, halfvec, binary quantizationIndex bytes, recall after reranking, latency

Changing the vector representation changes the experiment. pgvector documents halfvec as two bytes per dimension rather than four for vector, and supports expression indexes for binary quantization. Smaller storage is valuable only if the retained quality meets the application’s evaluation threshold. See pgvector vector types and quantization.

Freeze a benchmark manifest

A result is not reproducible unless the manifest records:

  • PostgreSQL and pgvector versions;
  • CPU, memory, storage, operating system, and relevant database settings;
  • row count, dimensions, data type, distance, and vector normalization;
  • index DDL and build settings;
  • query-set hash and filter distribution;
  • concurrency, warm-up, run length, and cache-state policy; and
  • exact definitions of recall and latency percentiles.

Use production-shaped vectors where permitted. Synthetic independent vectors can hide clustering, near-duplicates, tenant skew, and filter selectivity that dominate real behavior.

Split query vectors from indexed rows when that matches production. If every query is copied from the corpus, the self-match can make retrieval look easier than the real task.

For each query, record the top k IDs from an exact scan. pgvector recommends disabling index scans inside a transaction when monitoring approximate recall:

BEGIN;
SET LOCAL enable_indexscan = off;

SELECT id
FROM items
WHERE tenant_id = $1
ORDER BY embedding <=> $2::vector, id
LIMIT $3;

ROLLBACK;

That pattern appears in the project’s monitoring guidance. Inspect EXPLAIN (ANALYZE, BUFFERS) to confirm the plan rather than assuming a setting produced the intended path.

For one query, define set recall at k as:

| approximate_top_k ∩ exact_top_k | / k

Aggregate the distribution, not only its mean. Report a low percentile or worst cohort, and slice by tenant size and filter selectivity. A global average can hide one customer class with poor recall.

Sweep one dimension at a time

Start with a small matrix:

  1. Pick two or three plausible build configurations.
  2. Build each index from the same table snapshot.
  3. Sweep several ef_search values on the same ordered query set.
  4. Run enough iterations to stabilize tail latency.
  5. Plot recall against p95 and p99 latency rather than ranking a single number.

Set query parameters locally so pooled connections do not retain experimental state:

BEGIN;
SET LOCAL hnsw.ef_search = 100;

SELECT id
FROM items
ORDER BY embedding <=> $1::vector, id
LIMIT 10;

COMMIT;

Choose the least expensive point that clears a declared recall floor with operating headroom. If the requirement is “never omit an eligible result,” an approximate index is the wrong semantic contract; use exact search or redesign the candidate set.

Test filters as a separate workload

In pgvector approximate search, filtering occurs after index scanning. A selective predicate can therefore leave fewer than k results. Iterative scans, introduced in pgvector 0.8.0, can continue until enough matching tuples are found or a limit is reached. Strict ordering preserves exact distance order; relaxed ordering can offer better recall but may need a materialized CTE to restore strict presentation order. The behavior and settings are documented in Iterative Index Scans.

Benchmark these alternatives per filter cohort:

  • an exact scan after a B-tree filter;
  • a partial HNSW index for a small, stable category set;
  • partitioning for an appropriate high-level key;
  • strict iterative HNSW; and
  • relaxed iterative HNSW followed by reranking.

Record queries that return fewer than k separately from recall. Returning three perfect results for a requested ten is not equivalent to returning ten with lower overlap.

Include build and maintenance cost

Query performance is only one part of ownership. Capture index build time, peak resident memory, final index size, write throughput, replica lag, backup impact, and vacuum behavior. pgvector notes that HNSW builds are faster when the graph fits in maintenance_work_mem, while warning not to set that value high enough to exhaust server memory. It also recommends creating production indexes concurrently to avoid blocking writes and describes reindexing before vacuum as a way to speed vacuuming large HNSW indexes. See pgvector performance guidance.

Test the failure conditions too: aborted concurrent builds, low disk space, replica replay, bulk ingestion, deleted rows, and index rebuilds. A configuration that wins a read-only benchmark may be operationally unacceptable.

Avoid common benchmark errors

  • Do not compare approximate results without exact ground truth.
  • Do not warm one configuration while measuring another cold.
  • Do not change corpus, embeddings, and index parameters in the same comparison.
  • Do not report only average latency.
  • Do not omit failed or short result sets.
  • Do not tune on the final evaluation queries.
  • Do not infer production behavior from unfiltered single-client queries.
  • Do not force a particular plan without explaining why the planner rejected it.

Ship with a recall monitor

Sample production-shaped queries into a privacy-reviewed evaluation stream. Re-run them against exact search on a replica or bounded offline dataset and alert on recall drift by cohort. Pair this with database metrics, index sizes, query plans, and embedding-version changes.

Rebenchmark after upgrading PostgreSQL, pgvector, the embedding model, vector representation, distance function, hardware, or material data distribution. The chosen point belongs to that complete system, not to HNSW in isolation.