Skip to main content

Vector Search

The inverted index also indexes vector embeddings for approximate nearest neighbor (ANN) search, using an IVF (Inverted File) index or an HNSW graph. This powers semantic search, recommendations and other similarity workloads over FLOAT vectors.

IVF partitions the vectors into nlist coarse clusters (found by k-means at build time). A query first identifies the clusters closest to the query vector, then computes distances only within those, so a search touches a small fraction of the vectors instead of scanning them all. That is what makes it approximate — it trades a little recall for a large speed-up, and the number of clusters scanned (nprobe, a query-time setting) tunes that trade-off. Optional quantization (quant) compresses each stored vector to shrink the index further, at some additional recall cost that can be recovered by reranking.

Creating a vector index​

A vector column uses the ivf (...) operator class. The column must be a fixed-size FLOAT array (FLOAT[N]) — every row shares the same dimension N:

SQL
CREATE TABLE vectors (id INTEGER, emb FLOAT[3]);
CREATE INDEX l2_index ON vectors USING inverted (id, emb ivf (metric = 'l2'));

The metric is required. Everything else is optional: the index is sized automatically from the row count and, for l2, ip and cosine, quantized with sq8:

ParameterDescription
metricDistance metric: l2 (Euclidean), cosine, ip (inner product) or l1 (Manhattan)
nlistNumber of coarse clusters. Higher values narrow each cluster (faster, more precise probes) at the cost of build time. Mutually exclusive with nlist_factor
nlist_factorSizes nlist relative to the row count as round(nlist_factor * sqrt(rows)). Default 2.0. Mutually exclusive with nlist
quantVector compression: sq8 (the default for l2, ip and cosine), sq4, pq, rabitq, tq or none (the default for l1) — see Quantization below. l1 indexes are always unquantized, and rabitq needs l2 or ip
pq_mNumber of subquantizers for quant = 'pq'. Must evenly divide the vector dimension N. Defaults to a value close to a 2-dimensional subvector
rabitq_bitsExtra magnitude bits per dimension for quant = 'rabitq', 1 to 9. Default 1 (sign-only). Also spelled nb_bits
nb_bitsBits per dimension for quant = 'tq', 1 to 5. Default 3
compressionWhether the stored vectors are compressed. Default true; false stores them uncompressed, which searches faster and takes more disk

There are two ways to query a vector index: k-nearest-neighbor search (the closest k vectors) and range search (every vector within a distance threshold).

k-nearest-neighbor (kNN)​

Order by the distance to a query vector and LIMIT to the number of neighbors you want. Each distance operator computes a fixed metric — <-> is L2, <=> is cosine, <+> is L1 and <#> is inner product — so use the one matching the metric your index was built with; the optimizer then routes the query through the IVF index, and EXPLAIN shows the distance as the scan's Score with a Top: k row. With another operator the query still returns correct distances, but from a full scan: a TOP_N over a PROJECTION that computes each distance.

sql
SELECT id FROM index_name ORDER BY emb <-> $query_vector LIMIT k;
SQL
SELECT id FROM l2_index ORDER BY emb <-> [0, 0, 0]::FLOAT[3] LIMIT 2;
 id
----
  3
  2

The named distance functions l2_distance, cosine_distance, l1_distance and negative_inner_product are equivalent to the matching operator and can be used explicitly:

SQL
SELECT id FROM cos_index ORDER BY cosine_distance(emb, [1, 0, 0]::FLOAT[3]) LIMIT 2;
 id
----
  1
  3

Instead of a fixed number of neighbors, return every vector within a distance threshold (a radius) by comparing the distance in a WHERE clause:

sql
SELECT id FROM index_name WHERE emb <-> $query_vector < radius;
SQL
SELECT id FROM l2_index WHERE emb <-> [0, 0, 0]::FLOAT[3] < 100 ORDER BY id;
 id
----
  2
  3

The two forms combine: add ORDER BY emb <-> $query_vector LIMIT k to a range query to take the closest k within the radius.

Quantization​

An l2, ip or cosine index stores its codes quantized with sq8 unless quant says otherwise; an l1 index stores full-precision vectors. Quantization shrinks the index, trading some recall for size. sdb_rerank_factor wins the recall back: it re-scores a candidate pool with exact distances before picking the final k:

quantCompressionNotes
nonenoneFull-precision vectors; never reranks regardless of sdb_rerank_factor. The only choice for l1
sq8 (default)8-bit scalar quantization per dimensionGood recall/size trade-off
sq44-bit scalar quantization per dimensionSmaller than sq8, lower recall before reranking
pqProduct quantization — the vector is split into pq_m subvectors, each quantized against its own small codebookHighest compression; recall is sensitive to pq_m (must divide N)
rabitqRaBitQ binary quantization, 1 bit per dimension plus rabitq_bits − 1 extra magnitude bitsVery compact; rabitq_bits (1 to 9) trades size for recall. Needs l2 or ip
tqLow-bit quantization with nb_bits (1 to 5, default 3) bits per dimensionSize and recall grow with nb_bits

Quantization applies to l2, ip and cosine indexes; l1 indexes are always unquantized.

Quantization also speeds up the scan itself, not just the index size: quantized codes are stored inline in each cluster's postings, laid out contiguously per cluster, so a probe reads them sequentially instead of chasing full vectors elsewhere; and comparing quantized codes (a table lookup for pq, a popcount for rabitq, integer arithmetic for sq8/sq4) is cheaper than a full-precision FLOAT[N] distance. So quant is a query-latency optimization as much as a storage one — the smaller, posting-aware layout is what lets nprobe scan more clusters for the same latency budget.

HNSW​

The hnsw (...) operator class indexes the vectors as a navigable graph instead of IVF clusters. It takes the same metric and is queried the same way, with the distance operator that matches the metric:

SQL
CREATE TABLE hnsw_vectors (id INTEGER, emb FLOAT[3]);
CREATE INDEX hnsw_index ON hnsw_vectors USING inverted (id, emb hnsw (metric = 'l2'));
INSERT INTO hnsw_vectors VALUES    (1, [100, 0, 0]::FLOAT[3]),    (2, [50, 50, 50]::FLOAT[3]),    (3, [50, 25, 50]::FLOAT[3]),    (4, [50, 50, 100]::FLOAT[3]);
VACUUM (REFRESH_TABLE) hnsw_vectors;
SELECT id FROM hnsw_index ORDER BY emb <-> [0, 0, 0]::FLOAT[3] LIMIT 2;
 id
----
  3
  2
ParameterDescription
metricRequired: l2, cosine, ip or l1
quantsq8 (the default), sq4, tq or none; l1 supports only none. pq and rabitq need IVF clusters and are refused
nb_bitsBits per dimension for quant = 'tq', 1 to 5. Default 3
mGraph links per vector, at least 2. Default 32
ef_constructionCandidate list size while building, at least m. Default 200
compressionWhether the stored vectors are compressed. Default true

At query time the sdb_hnsw_ef_search setting (default 64) sets the search beam. On an index with quant = 'none' it is also the result ceiling: a value below the query's LIMIT returns fewer rows than asked for. A quantized index searches at least sdb_rerank_factor times the LIMIT candidates, so its LIMIT holds.

Column types​

A vector column must be a fixed-size FLOAT[N] array — all rows share dimension N (an unsized FLOAT[] is rejected). Unlike text and INCLUDEd columns, a vector column does not take a storage compression codec — use quant instead to control its on-disk size.

See also​