Short answer: build IVFFlat after loading data, with lists ≈ rows / 1000, and raise ivfflat.probes well above its default of 1. On 100,000 OpenAI embeddings with 316 lists, probes = 1 found only 55.0% of the true top-10; probes = 16 found 91.2%. Unless build time is your constraint, HNSW is the better default.
How IVFFlat works#
At build time, k-means picks lists centre points from the data and assigns every vector to its nearest centre. A query compares itself with all centres, then scans every vector in the probes nearest lists. Vectors near a cell boundary are the problem: their true neighbours may sit in the next cell over.
Real k-means and IVF search on 500 random 2-D points, in your browser. Drag the query to a cell boundary with probes = 1 and watch true neighbours go missing.
Creating the index#
demo=# CREATE INDEX ON items USING ivfflat (embedding vector_cosine_ops) WITH (lists = 100);
CREATE INDEX
demo=# SET ivfflat.probes = 10;
SETUnlike HNSW, IVFFlat learns from the rows present at build time. Built on an empty table, it warns — and recall collapses as rows arrive:
demo=# CREATE TABLE empty_items (id bigserial PRIMARY KEY, embedding vector(3));
CREATE TABLE
demo=# CREATE INDEX ON empty_items USING ivfflat (embedding vector_l2_ops) WITH (lists = 100);
NOTICE: ivfflat index created with little data
DETAIL: This will cause low recall.
HINT: Drop the index until the table has more data.
CREATE INDEXprobes: the recall knob#
IVFFlat recall@10 vs probes (100K rows)
pgvector's default is probes = 1. More lists means smaller lists, so each probe covers less of the table.
- 100 lists
- 316 lists
- 1000 lists
| lists | probes | Recall@10 | p50 | p95 |
|---|---|---|---|---|
| 100 | 1 | 0.619 | 2.7 ms | 7.1 ms |
| 100 | 2 | 0.763 | 4.3 ms | 9.1 ms |
| 100 | 4 | 0.850 | 7.9 ms | 14 ms |
| 100 | 8 | 0.921 | 16 ms | 27 ms |
| 100 | 16 | 0.965 | 28 ms | 41 ms |
| 100 | 32 | 0.985 | 54 ms | 75 ms |
| 100 | 64 | 0.998 | 104 ms | 131 ms |
| 316 | 1 | 0.550 | 1.4 ms | 2.4 ms |
| 316 | 2 | 0.674 | 1.9 ms | 3.2 ms |
| 316 | 4 | 0.778 | 3.1 ms | 4.9 ms |
| 316 | 8 | 0.857 | 5.5 ms | 8.8 ms |
| 316 | 16 | 0.912 | 9.9 ms | 15 ms |
| 316 | 32 | 0.953 | 19 ms | 26 ms |
| 316 | 64 | 0.978 | 35 ms | 49 ms |
| 1000 | 1 | 0.485 | 1.4 ms | 2.0 ms |
| 1000 | 2 | 0.613 | 1.5 ms | 2.0 ms |
| 1000 | 4 | 0.716 | 2.0 ms | 3.2 ms |
| 1000 | 8 | 0.797 | 2.7 ms | 4.1 ms |
| 1000 | 16 | 0.860 | 4.4 ms | 6.9 ms |
| 1000 | 32 | 0.910 | 7.4 ms | 12 ms |
| 1000 | 64 | 0.947 | 13 ms | 18 ms |
Each line is a different lists value on the same 100,000 rows. At probes = 1, more lists means fewer vectors per list and worse recall. As probes rises, every configuration converges; what changes is how much work it takes. Setting it is the same as for HNSW:
SET ivfflat.probes = 16; -- session
SET LOCAL ivfflat.probes = 32; -- one transactionChoosing lists#
We built three indexes at each size — rows / 1000, √rows, and one deliberately large — and swept probes:
| Rows | lists | Build | Size | Recall, probes 1 | Recall, probes 16 | p50, probes 16 |
|---|---|---|---|---|---|---|
| 100,000 | 100 | 9.8 s | 782 MB | 0.619 | 0.965 | 28 ms |
| 100,000 | 316 | 23 s | 784 MB | 0.550 | 0.912 | 9.9 ms |
| 100,000 | 1,000 | 2.1 min | 789 MB | 0.485 | 0.860 | 4.4 ms |
| 500,000 | 500 | 1.7 min | 3.82 GB | 0.553 | 0.913 | 30 ms |
| 500,000 | 707 | 2.3 min | 3.82 GB | 0.561 | 0.902 | 23 ms |
| 500,000 | 2,000 | 8.5 min | 3.83 GB | 0.499 | 0.856 | 13 ms |
IVFFlat recall vs latency at 500K rows
Points are probes 1 → 64. lists = rows/1000 (500) and √rows (707) are the usual rules of thumb.
- 500 lists
- 707 lists
- 2000 lists
| lists | probes | Recall@10 | p50 |
|---|---|---|---|
| 500 | 1 | 0.553 | 5.0 ms |
| 500 | 2 | 0.686 | 6.8 ms |
| 500 | 4 | 0.780 | 10 ms |
| 500 | 8 | 0.858 | 16 ms |
| 500 | 16 | 0.913 | 30 ms |
| 500 | 32 | 0.951 | 56 ms |
| 500 | 64 | 0.976 | 111 ms |
| 707 | 1 | 0.561 | 3.5 ms |
| 707 | 2 | 0.676 | 4.6 ms |
| 707 | 4 | 0.771 | 7.9 ms |
| 707 | 8 | 0.843 | 14 ms |
| 707 | 16 | 0.902 | 23 ms |
| 707 | 32 | 0.941 | 43 ms |
| 707 | 64 | 0.970 | 82 ms |
| 2000 | 1 | 0.499 | 3.8 ms |
| 2000 | 2 | 0.623 | 4.1 ms |
| 2000 | 4 | 0.722 | 5.9 ms |
| 2000 | 8 | 0.797 | 8.0 ms |
| 2000 | 16 | 0.856 | 13 ms |
| 2000 | 32 | 0.904 | 19 ms |
| 2000 | 64 | 0.942 | 32 ms |
Read the curves by latency, not by probe count. More lists need more probes for the same recall, but each probe scans a smaller list, so per millisecond the larger lists values came out ahead at both sizes: at 500K rows, 2,000 lists reached 0.942 in 32 ms while 500 lists took 56 ms for 0.951. What you pay is build time — k-means over more centres — 8.5 min versus 1.7 min. The rows / 1000 rule is a reasonable middle; go higher if query latency matters more than rebuild time.
IVFFlat vs HNSW#
Recall vs latency: HNSW and IVFFlat on 100K embeddings
Each point is one setting of ef_search (HNSW) or probes (IVFFlat). Up and to the left is better.
- HNSW (m 16, ef_construction 64)
- IVFFlat (316 lists)
| Index | Setting | Recall@10 | p50 latency |
|---|---|---|---|
| HNSW | ef_search 10 | 0.815 | 1.4 ms |
| HNSW | ef_search 20 | 0.897 | 1.9 ms |
| HNSW | ef_search 40 | 0.947 | 2.9 ms |
| HNSW | ef_search 64 | 0.970 | 3.9 ms |
| HNSW | ef_search 100 | 0.983 | 5.3 ms |
| HNSW | ef_search 200 | 0.990 | 9.1 ms |
| HNSW | ef_search 400 | 0.995 | 16 ms |
| IVFFlat | probes 1 | 0.550 | 1.4 ms |
| IVFFlat | probes 2 | 0.674 | 1.9 ms |
| IVFFlat | probes 4 | 0.778 | 3.1 ms |
| IVFFlat | probes 8 | 0.857 | 5.5 ms |
| IVFFlat | probes 16 | 0.912 | 9.9 ms |
| IVFFlat | probes 32 | 0.953 | 19 ms |
| IVFFlat | probes 64 | 0.978 | 35 ms |
Build time and index size: IVFFlat vs HNSW (100K rows)
Same table, maintenance_work_mem 4 GB, single-process builds.
| Index | Build time | Size |
|---|---|---|
| IVFFlat 100 lists | 9.8 s | 782 MB |
| IVFFlat 316 lists | 23 s | 784 MB |
| IVFFlat 1000 lists | 2.1 min | 789 MB |
| HNSW m16/efc64 | 3.4 min | 781 MB |
| IVFFlat | HNSW | |
|---|---|---|
| Recall vs latency | Lower at every latency we measured | Higher |
| Build time (100K rows) | 23 s (316 lists) | 3.4 min |
| Build memory | Small: list centres only | Whole graph |
| Index size (100K × 1,536) | 784 MB | 781 MB — both store one 6 KB vector per 8 KB page |
| Empty-table creation | No — needs data to train | Yes |
| Inserts after build | Fine, but centres go stale as data drifts | Fine |
| Distances | L2, inner product, cosine, Hamming | All six |
Choose IVFFlat for large, mostly static tables where build time or build memory is the binding constraint and you can rebuild periodically. Choose HNSW otherwise. Don't choose IVFFlat to save disk: for full-size embeddings it isn't smaller — use halfvec for that.
Iterative scans#
Since 0.8.0, IVFFlat can keep probing additional lists until a filtered query has enough rows: SET ivfflat.iterative_scan = relaxed_order; (capped by ivfflat.max_probes). Only relaxed_order is supported for IVFFlat. Details and measurements in the filtering guide.
When to rebuild#
List centres are fixed at build time. If your data distribution shifts — a new product category, a new language, a different embedding model — recall degrades even though nothing errors. Rebuild with REINDEX INDEX CONCURRENTLY after large changes, and re-derive lists from the new row count.
Frequently asked questions
How many lists should I use for IVFFlat?
The upstream rule of thumb is rows / 1000 for up to 1M rows and √rows beyond that. More lists means each probe scans fewer vectors, so you need more probes but each is cheaper: at 100K rows, 1,000 lists reached 0.860 recall in 4.4 ms (16 probes), while 100 lists needed 7.9 ms for 0.850 (4 probes). The price is build time: 2.1 min versus 9.8 s.
What should ivfflat.probes be set to?
Not 1, the default. With 316 lists on 100K rows, probes = 1 found 55.0% of the true top-10; probes = 16 found 91.2% at a median 9.9 ms. Start around √lists and measure.
Is IVFFlat or HNSW better in pgvector?
HNSW gave higher recall at lower latency in every comparison we ran, handles inserts without retraining and can be built on an empty table. IVFFlat's advantages are build time (23 s vs 3.4 min on our 100K table) and lower build memory.