Skip to Content
We are live but in Staging 🎉

ANN Indexes

A vector<n> column is searchable the moment it has rows: with no index, KNN runs as an exact scan — correct at any size, linear in table size. An ANN index buys latency at large row counts, and nothing else changes about the answer.

A distance you see is the true distance. An ANN or quantized index only ever selects candidates; the candidates are re-ranked at full f32 precision before they are returned. An index changes which rows you get, never the distance reported for a row you got. Quantization is a storage and recall trade — never a reported-value trade.

The type model, the distance operators and their opclasses are in Core Concepts. This page is the index itself.

Declaring one

pgvector CREATE INDEX, one column per index. The table needs a single-column primary key — integer or string. A VARCHAR key builds a surrogate dictionary index and KNN returns the real string ids, which is what lets the Qdrant / Pinecone surfaces be ANN-indexed at all; a composite key is refused with an explicit error and the column serves exact scan.

-- HNSW: high recall, larger build CREATE INDEX ON docs USING hnsw (embedding vector_cosine_ops) WITH (m = 16, ef_construction = 64); -- IVF-Flat: fast build, recall tunable by probe count CREATE INDEX ON docs USING ivfflat (embedding vector_l2_ops) WITH (lists = 100); -- quantized tiers (smaller resident footprint) CREATE INDEX ON docs USING hnsw (embedding vector_l2_ops) WITH (quantization = 'f16'); -- or 'i8' CREATE INDEX ON docs USING ivfflat (embedding vector_l2_ops) WITH (lists = 100, quantization = 'sq8'); -- register now, build in the background CREATE INDEX ON docs USING hnsw (embedding vector_l2_ops) WITH (async); SHOW VECTOR INDEXES; -- table | column | kind | metric | dim | state | built_at_version | row_count | error

The five kinds the f32 serve path offers are hnsw (the default), hnsw_sq_f16, hnsw_sq_i8, ivfflat and ivf_sq8. The quantization storage parameter is the pgvector-native spelling; the kind name is also accepted directly after USING.

An index moves building → ready, or failed with its error kept. There is no automatic retry — deliberately, to avoid rebuild storms — so re-run CREATE INDEX to retry. Unknown WITH keys, unknown opclasses and invalid (method, quantization) pairs are rejected at DDL, never silently defaulted.

Which kind to ask for

Measured against a brute-force exact-KNN oracle: recall@10 at the serve defaults, on clustered corpora that stand in for real embeddings.

CollectionAsk forWhy
under ~20 000 vectorshnsw (the default)every kind measures ≥ 0.99 here, and hnsw builds fastest with no quantization step. Don’t override the default for a small collection.
~50 000 and above, at embedding dimensionshnsw_sq_f16measured recall@10 holds ≥ 0.987 across 20k → 200k × 768, where an unquantized graph over the same rows falls to 0.434.

Two things worth knowing before reaching for a quantized kind:

  • i8 is not a cheaper f16. They tie on realistic clustered data and diverge badly on uniformly random vectors — 0.578 against 0.788 at 20 000 × 768. Prefer f16 unless you have measured your own corpus.
  • hnsw_sq_i8 on an inner-product column is refused at DDL. Inner product ranks by vector magnitude, and 8-bit quantization does not preserve magnitude across vectors, so the graph builds fragmented — 22 % of vectors unreachable at 768 dimensions, even after a full serial rebuild. The error names the fix: hnsw_sq_f16 (same memory tier, clean on this metric) or hnsw (full precision).

ivf_sq8 is roughly a quarter the code footprint of ivfflat and scores identically to it in every measured cell. IVF recall depends on how many lists a query probes — raise ivfflat.probes if your vectors are not well clustered.

The default is hnsw and has not changed. Quantization is a precision trade, so any change of default is announced as a behaviour change, never applied silently.

Beyond a single index, the engine serves large collections as segments — which is what keeps recall stable and build memory bounded as a collection grows into the millions. See Vector at Scale.

Every build proves its own completeness

The build verifies reachability, not recall:

  • Every HNSW build walks the finished graph, re-inserts anything unreachable, and declares the exact residual as a scan tail that search covers. A tail past 2 % of the corpus fails the build, so the repair pass can never quietly degrade into a brute-force scan that happens to answer correctly.
  • Every IVF build verifies partition completeness — every stored vector in exactly one posting list, nothing missing, nothing twice, nothing out of range. Exact, and O(n).

A build that cannot prove every stored vector reachable fails, leaving the index failed with its error rather than serving a quietly lossy one. That contract exists because the alternative was live: a defect that left roughly 1 in 100–200 stored vectors inserted, acknowledged, present in the index and absent from the graph — invisible to recall measurements, and caught only by instrumenting the real adjacency.

Tuning and serving

SET hnsw.ef_search = 512; -- HNSW beam width (server default 384) SET ivfflat.probes = 40; -- IVF probe count (server default ~sqrt(lists)) SET dodil.vector_consistency = eventual; -- default: strong SET dodil.vector_exact_scan = on; -- permit exact scans on large tables

Higher ef_search / probes raise recall at some latency cost; the defaults are chosen for high recall out of the box.

When the index serves, and when it doesn’t. Under the default strong consistency a KNN query is served from the index only when that is safe — the index is ready and current with no un-compacted writes pending. Otherwise the engine falls back to an exact scan and schedules a background rebuild, so correctness never depends on index freshness. eventual always uses the index and accepts a snapshot that may trail your latest writes.

Residency. Indexes are served from memory under a budget, so pin one you cannot afford to serve cold. Releasing is a latency event, not a data event: a released index does not refuse queries — the next query reloads it lazily, or serves exact until it is back.

LOAD VECTOR INDEX docs (embedding); -- eagerly load + pin (idempotent) RELEASE VECTOR INDEX docs (embedding); -- evict from RAM LOAD VECTOR INDEX docs; -- table-wide: pins every ready index

See Reservation & Hot Cache for how that budget is set.

See also