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:
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:
| Parameter | Description |
|---|---|
metric | Distance metric: l2 (Euclidean), cosine, ip (inner product) or l1 (Manhattan) |
nlist | Number of coarse clusters. Higher values narrow each cluster (faster, more precise probes) at the cost of build time. Mutually exclusive with nlist_factor |
nlist_factor | Sizes nlist relative to the row count as round(nlist_factor * sqrt(rows)). Default 2.0. Mutually exclusive with nlist |
quant | Vector 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_m | Number of subquantizers for quant = 'pq'. Must evenly divide the vector dimension N. Defaults to a value close to a 2-dimensional subvector |
rabitq_bits | Extra magnitude bits per dimension for quant = 'rabitq', 1 to 9. Default 1 (sign-only). Also spelled nb_bits |
nb_bits | Bits per dimension for quant = 'tq', 1 to 5. Default 3 |
compression | Whether 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.
SELECT id FROM index_name ORDER BY emb <-> $query_vector LIMIT k;
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:
SELECT id FROM cos_index ORDER BY cosine_distance(emb, [1, 0, 0]::FLOAT[3]) LIMIT 2;id ---- 1 3
Range (radius) search
Instead of a fixed number of neighbors, return every vector within a distance threshold (a radius) by comparing the distance in a WHERE clause:
SELECT id FROM index_name WHERE emb <-> $query_vector < radius;
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:
quant | Compression | Notes |
|---|---|---|
none | none | Full-precision vectors; never reranks regardless of sdb_rerank_factor. The only choice for l1 |
sq8 (default) | 8-bit scalar quantization per dimension | Good recall/size trade-off |
sq4 | 4-bit scalar quantization per dimension | Smaller than sq8, lower recall before reranking |
pq | Product quantization — the vector is split into pq_m subvectors, each quantized against its own small codebook | Highest compression; recall is sensitive to pq_m (must divide N) |
rabitq | RaBitQ binary quantization, 1 bit per dimension plus rabitq_bits − 1 extra magnitude bits | Very compact; rabitq_bits (1 to 9) trades size for recall. Needs l2 or ip |
tq | Low-bit quantization with nb_bits (1 to 5, default 3) bits per dimension | Size 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:
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
| Parameter | Description |
|---|---|
metric | Required: l2, cosine, ip or l1 |
quant | sq8 (the default), sq4, tq or none; l1 supports only none. pq and rabitq need IVF clusters and are refused |
nb_bits | Bits per dimension for quant = 'tq', 1 to 5. Default 3 |
m | Graph links per vector, at least 2. Default 32 |
ef_construction | Candidate list size while building, at least m. Default 200 |
compression | Whether 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
- Hybrid Search — combine vector ranking with full-text or structured filters
- Inverted Index · Full-Text Search
- Vector Functions — distance functions and the
<->operator