Vector indexes¶
Every recommender here scores a query against the whole catalog, which is exact and linear in the catalog. An index trades one of those properties away, and there are two of them, which give up different things.
index="hnsw" gives up linear: the model builds a navigable graph over its item
vectors at fit time, and recommend walks it instead of scoring everything, so a query
touches a few hundred items rather than all of them. What it gives up in return is
exactness — the answer is the best items the walk found.
index="quantized-flat" gives up exact, but only just, and keeps linear. It stores
the item vectors a second time as narrow codes, scans every candidate with those, and
then rescores the best oversample * k of them with the original vectors. The scores it
returns are therefore always exact, at any code width; the only thing quantization can
cost is a true top-k item that never made the shortlist.
from skrecsys.indexing import HNSW, QuantizedFlatIndex
from skrecsys.recommendation import AlternatingLeastSquares
rec = AlternatingLeastSquares(index=None).fit(X, y) # exact scoring (the default)
rec = AlternatingLeastSquares(index="hnsw").fit(X, y) # always index
rec = AlternatingLeastSquares(index=HNSW(m=32, ef_search=128)).fit(X, y)
rec = AlternatingLeastSquares(index=QuantizedFlatIndex(bits=4, oversample=8)).fit(X, y)
Choosing an index. On the catalogs benchmarked below, the quantized index keeps near-exact recall with exact scores, but it is slower than the exact path for every model. Use the exact path (the default) or HNSW when latency matters; see what the quantized scan costs for the numbers.
QuantizedFlatIndex takes four parameters:
bits(default8), one of1,2,4,8. Codes are bit-packed, so four bits really is half the memory of eight. Widths that are not powers of two are refused rather than rounded: a code that straddled a byte would cost a two-byte read and a pair of shifts per value.quantile(default0.0), the share of each tail clipped when the code range is chosen. A single outlier in a dimension otherwise spends the whole range on itself and leaves the bulk two levels to share; trimming puts it outside the range, where it clips — losing its magnitude but not its rank.oversample(default4), the shortlist in multiples ofk. The recall-for-latency dial, and likeef_searchthe only one that can be turned on a model that is already fitted. Narrow codes need a wider shortlist: measured on the test fixture, eight and four bits keep the exact top ten atoversample=4for most models, while one bit needs thirty-two and still reaches only 0.36 onAlternatingLeastSquares.SimpleXneeds more than the rest at every width; see what the quantized scan costs.min_index_size(default4096), as onHNSW.
Quantization is affine — value ≈ code * scale + offset — with a scale and an offset per
dimension for a dense space, and one pair over all the stored values for a sparse one,
where a catalog-sized column dimension has no per-column statistics worth keeping. The
scan never decodes: the scale is folded into the query once, so the inner loop multiplies
a query value by a code and nothing else. It also skips the norm-equalizing dimension
HNSW needs, since a scan reaches every candidate whatever its norm.
One thing it does not do is shrink a fitted model: the original vectors are kept for
the rerank, so the codes are an addition to the footprint. The index MB and
vectors MB columns below are printed side by side for that reason.
Choose an index explicitly after measuring it for your model and data. With the default index=None, recommendations score candidates exactly and build no index. index="hnsw" is honoured at any size and density, subject to the index's own query-time floor.
Everything recommend promised it still promises. It returns exactly the number of
items asked for, excludes what the user has seen, breaks ties by fitted item order, and
raises rather than padding when too few items are eligible; a fitted model still holds
nothing but numpy arrays, so it still pickles and scores anywhere. What changes is that
the items are the best ones the walk found, which is usually and not always the best
ones there are.
Two further details of the graph index. An index is skipped at query
time too, for
candidate sets below min_index_size (4096 by default), because a graph walked with few
eligible nodes needs proportionally more walking while an exact scan over a short list is
already cheap — so candidates= filters quietly stay exact and identical. And scores are
inner products, which are not a metric, so the index spends one extra
dimension equalizing the item norms; without it a model with a bias term loses about a
fifth of its recall. A build on more than one thread is not reproducible, because
concurrent insertions see different partial graphs, so a model with random_state set
builds on one thread unless n_jobs says otherwise.
Whether an index pays off depends on the model and the catalog. The benchmark harness measures both on your own data:
uv run python benchmarks/run.py run indexes --dataset movielens-100k # re-measure what changed
uv run python benchmarks/run.py run indexes --dataset amazon-books --only BM25 ALS BPR
uv run python benchmarks/run.py run indexes --index hnsw --kind sweep --no-store # look, keep nothing
MovieLens 100K, official ua split, k=10, default hyper-parameters. Regenerate with python benchmarks/run.py run indexes --dataset movielens-100k.
Measured under an earlier version of the index, and due a re-run: quantized-flat(bits=8) for SimpleX, XSimGCL; quantized-flat(bits=4) for SimpleX, XSimGCL.
What the index costs in answers
| Model | Index | dial | recall@10 | NDCG@10 | dNDCG@10 | top-1 churn | score gap@1 | dcat cov |
|---|---|---|---|---|---|---|---|---|
| SLIM | exact | - | 1.0000 | 0.3024 | +0.0000 | 0.0000 | +0.0000 | +0.0000 |
| SLIM | hnsw(m=16,efc=200) | ef=16 | 0.5900 | 0.2608 | -0.0417 | 0.3065 | +0.0474 | -0.0417 |
| SLIM | hnsw(m=16,efc=200) | ef=32 | 0.6615 | 0.2698 | -0.0326 | 0.2503 | +0.0346 | -0.0464 |
| SLIM | hnsw(m=16,efc=200) | ef=64 | 0.7195 | 0.2750 | -0.0274 | 0.2238 | +0.0297 | -0.0435 |
| SLIM | hnsw(m=16,efc=200) | ef=128 | 0.7610 | 0.2765 | -0.0259 | 0.2057 | +0.0266 | -0.0440 |
| SLIM | hnsw(m=16,efc=200) | ef=256 | 1.0000 | 0.3024 | +0.0000 | 0.0000 | +0.0000 | +0.0000 |
| SLIM | quantized-flat(bits=8) | ov=1 | 0.9948 | 0.3030 | +0.0006 | 0.0000 | +0.0000 | +0.0006 |
| SLIM | quantized-flat(bits=8) | ov=2 | 1.0000 | 0.3024 | +0.0000 | 0.0000 | +0.0000 | +0.0000 |
| SLIM | quantized-flat(bits=8) | ov=4 | 1.0000 | 0.3024 | +0.0000 | 0.0000 | +0.0000 | +0.0000 |
| SLIM | quantized-flat(bits=8) | ov=8 | 1.0000 | 0.3024 | +0.0000 | 0.0000 | +0.0000 | +0.0000 |
| SLIM | quantized-flat(bits=8) | ov=16 | 1.0000 | 0.3024 | +0.0000 | 0.0000 | +0.0000 | +0.0000 |
| SLIM | quantized-flat(bits=4) | ov=1 | 0.8979 | 0.3022 | -0.0002 | 0.0000 | +0.0000 | +0.0060 |
| SLIM | quantized-flat(bits=4) | ov=2 | 0.9988 | 0.3026 | +0.0001 | 0.0000 | +0.0000 | +0.0000 |
| SLIM | quantized-flat(bits=4) | ov=4 | 1.0000 | 0.3024 | +0.0000 | 0.0000 | +0.0000 | +0.0000 |
| SLIM | quantized-flat(bits=4) | ov=8 | 1.0000 | 0.3024 | +0.0000 | 0.0000 | +0.0000 | +0.0000 |
| SLIM | quantized-flat(bits=4) | ov=16 | 1.0000 | 0.3024 | +0.0000 | 0.0000 | +0.0000 | +0.0000 |
| EASE | exact | - | 1.0000 | 0.2767 | +0.0000 | 0.0000 | +0.0000 | +0.0000 |
| EASE | hnsw(m=16,efc=200) | ef=16 | 0.2701 | 0.1723 | -0.1045 | 0.6490 | +0.1584 | -0.1339 |
| EASE | hnsw(m=16,efc=200) | ef=32 | 0.2963 | 0.1845 | -0.0922 | 0.6161 | +0.1384 | -0.1155 |
| EASE | hnsw(m=16,efc=200) | ef=64 | 0.3486 | 0.1967 | -0.0800 | 0.5461 | +0.1145 | -0.0482 |
| EASE | hnsw(m=16,efc=200) | ef=128 | 0.4239 | 0.2121 | -0.0646 | 0.4772 | +0.0907 | +0.0196 |
| EASE | hnsw(m=16,efc=200) | ef=256 | 1.0000 | 0.2767 | +0.0000 | 0.0000 | +0.0000 | +0.0000 |
| EASE | quantized-flat(bits=8) | ov=1 | 0.9888 | 0.2774 | +0.0007 | 0.0000 | +0.0000 | +0.0030 |
| EASE | quantized-flat(bits=8) | ov=2 | 1.0000 | 0.2767 | +0.0000 | 0.0000 | +0.0000 | +0.0000 |
| EASE | quantized-flat(bits=8) | ov=4 | 1.0000 | 0.2767 | +0.0000 | 0.0000 | +0.0000 | +0.0000 |
| EASE | quantized-flat(bits=8) | ov=8 | 1.0000 | 0.2767 | +0.0000 | 0.0000 | +0.0000 | +0.0000 |
| EASE | quantized-flat(bits=8) | ov=16 | 1.0000 | 0.2767 | +0.0000 | 0.0000 | +0.0000 | +0.0000 |
| EASE | quantized-flat(bits=4) | ov=1 | 0.8456 | 0.2749 | -0.0018 | 0.0032 | +0.0004 | +0.0226 |
| EASE | quantized-flat(bits=4) | ov=2 | 0.9709 | 0.2760 | -0.0007 | 0.0011 | +0.0003 | +0.0054 |
| EASE | quantized-flat(bits=4) | ov=4 | 0.9922 | 0.2766 | -0.0001 | 0.0011 | +0.0003 | +0.0030 |
| EASE | quantized-flat(bits=4) | ov=8 | 0.9973 | 0.2769 | +0.0001 | 0.0000 | +0.0000 | +0.0012 |
| EASE | quantized-flat(bits=4) | ov=16 | 0.9995 | 0.2768 | +0.0000 | 0.0000 | +0.0000 | +0.0006 |
| RP3Beta | exact | - | 1.0000 | 0.2762 | +0.0000 | 0.0000 | +0.0000 | +0.0000 |
| RP3Beta | hnsw(m=16,efc=200) | ef=16 | 0.9199 | 0.2640 | -0.0122 | 0.0710 | +0.0186 | +0.0012 |
| RP3Beta | hnsw(m=16,efc=200) | ef=32 | 0.9512 | 0.2698 | -0.0064 | 0.0445 | +0.0097 | +0.0006 |
| RP3Beta | hnsw(m=16,efc=200) | ef=64 | 0.9745 | 0.2735 | -0.0027 | 0.0244 | +0.0052 | -0.0024 |
| RP3Beta | hnsw(m=16,efc=200) | ef=128 | 0.9914 | 0.2753 | -0.0009 | 0.0053 | +0.0006 | -0.0065 |
| RP3Beta | hnsw(m=16,efc=200) | ef=256 | 1.0000 | 0.2762 | +0.0000 | 0.0000 | +0.0000 | +0.0000 |
| RP3Beta | quantized-flat(bits=8) | ov=1 | 0.9872 | 0.2757 | -0.0005 | 0.0000 | +0.0000 | +0.0012 |
| RP3Beta | quantized-flat(bits=8) | ov=2 | 1.0000 | 0.2762 | +0.0000 | 0.0000 | +0.0000 | +0.0000 |
| RP3Beta | quantized-flat(bits=8) | ov=4 | 1.0000 | 0.2762 | +0.0000 | 0.0000 | +0.0000 | +0.0000 |
| RP3Beta | quantized-flat(bits=8) | ov=8 | 1.0000 | 0.2762 | +0.0000 | 0.0000 | +0.0000 | +0.0000 |
| RP3Beta | quantized-flat(bits=8) | ov=16 | 1.0000 | 0.2762 | +0.0000 | 0.0000 | +0.0000 | +0.0000 |
| RP3Beta | quantized-flat(bits=4) | ov=1 | 0.8349 | 0.2654 | -0.0108 | 0.0021 | +0.0002 | -0.0429 |
| RP3Beta | quantized-flat(bits=4) | ov=2 | 0.9762 | 0.2745 | -0.0017 | 0.0000 | +0.0000 | -0.0113 |
| RP3Beta | quantized-flat(bits=4) | ov=4 | 0.9983 | 0.2759 | -0.0003 | 0.0000 | +0.0000 | -0.0006 |
| RP3Beta | quantized-flat(bits=4) | ov=8 | 1.0000 | 0.2762 | +0.0000 | 0.0000 | +0.0000 | +0.0000 |
| RP3Beta | quantized-flat(bits=4) | ov=16 | 1.0000 | 0.2762 | +0.0000 | 0.0000 | +0.0000 | +0.0000 |
| BPR | exact | - | 1.0000 | 0.2800 | +0.0000 | 0.0000 | +0.0000 | +0.0000 |
| BPR | hnsw(m=16,efc=200) | ef=16 | 0.9981 | 0.2801 | +0.0000 | 0.0000 | -0.0000 | +0.0000 |
| BPR | hnsw(m=16,efc=200) | ef=32 | 0.9999 | 0.2800 | +0.0000 | 0.0000 | -0.0000 | +0.0000 |
| BPR | hnsw(m=16,efc=200) | ef=64 | 1.0000 | 0.2800 | +0.0000 | 0.0000 | -0.0000 | +0.0000 |
| BPR | hnsw(m=16,efc=200) | ef=128 | 1.0000 | 0.2800 | +0.0000 | 0.0000 | -0.0000 | +0.0000 |
| BPR | hnsw(m=16,efc=200) | ef=256 | 1.0000 | 0.2800 | +0.0000 | 0.0000 | +0.0000 | +0.0000 |
| BPR | quantized-flat(bits=8) | ov=1 | 0.9901 | 0.2796 | -0.0005 | 0.0000 | -0.0000 | +0.0024 |
| BPR | quantized-flat(bits=8) | ov=2 | 1.0000 | 0.2800 | +0.0000 | 0.0000 | -0.0000 | +0.0000 |
| BPR | quantized-flat(bits=8) | ov=4 | 1.0000 | 0.2800 | +0.0000 | 0.0000 | -0.0000 | +0.0000 |
| BPR | quantized-flat(bits=8) | ov=8 | 1.0000 | 0.2800 | +0.0000 | 0.0000 | -0.0000 | +0.0000 |
| BPR | quantized-flat(bits=8) | ov=16 | 1.0000 | 0.2800 | +0.0000 | 0.0000 | -0.0000 | +0.0000 |
| BPR | quantized-flat(bits=4) | ov=1 | 0.8821 | 0.2767 | -0.0034 | 0.0000 | -0.0000 | +0.0006 |
| BPR | quantized-flat(bits=4) | ov=2 | 0.9980 | 0.2800 | -0.0001 | 0.0000 | -0.0000 | -0.0006 |
| BPR | quantized-flat(bits=4) | ov=4 | 1.0000 | 0.2800 | +0.0000 | 0.0000 | -0.0000 | +0.0000 |
| BPR | quantized-flat(bits=4) | ov=8 | 1.0000 | 0.2800 | +0.0000 | 0.0000 | -0.0000 | +0.0000 |
| BPR | quantized-flat(bits=4) | ov=16 | 1.0000 | 0.2800 | +0.0000 | 0.0000 | -0.0000 | +0.0000 |
| BM25 | exact | - | 1.0000 | 0.2661 | +0.0000 | 0.0000 | +0.0000 | +0.0000 |
| BM25 | hnsw(m=16,efc=200) | ef=16 | 0.9180 | 0.2570 | -0.0092 | 0.0541 | +0.0221 | +0.0036 |
| BM25 | hnsw(m=16,efc=200) | ef=32 | 0.9704 | 0.2624 | -0.0037 | 0.0201 | +0.0083 | +0.0000 |
| BM25 | hnsw(m=16,efc=200) | ef=64 | 0.9937 | 0.2662 | +0.0001 | 0.0032 | +0.0005 | -0.0030 |
| BM25 | hnsw(m=16,efc=200) | ef=128 | 0.9977 | 0.2660 | -0.0002 | 0.0000 | +0.0000 | -0.0024 |
| BM25 | hnsw(m=16,efc=200) | ef=256 | 1.0000 | 0.2661 | +0.0000 | 0.0000 | +0.0000 | +0.0000 |
| BM25 | quantized-flat(bits=8) | ov=1 | 0.9984 | 0.2661 | -0.0000 | 0.0000 | +0.0000 | -0.0006 |
| BM25 | quantized-flat(bits=8) | ov=2 | 1.0000 | 0.2661 | +0.0000 | 0.0000 | +0.0000 | +0.0000 |
| BM25 | quantized-flat(bits=8) | ov=4 | 1.0000 | 0.2661 | +0.0000 | 0.0000 | +0.0000 | +0.0000 |
| BM25 | quantized-flat(bits=8) | ov=8 | 1.0000 | 0.2661 | +0.0000 | 0.0000 | +0.0000 | +0.0000 |
| BM25 | quantized-flat(bits=8) | ov=16 | 1.0000 | 0.2661 | +0.0000 | 0.0000 | +0.0000 | +0.0000 |
| BM25 | quantized-flat(bits=4) | ov=1 | 0.9778 | 0.2663 | +0.0001 | 0.0000 | +0.0000 | -0.0006 |
| BM25 | quantized-flat(bits=4) | ov=2 | 1.0000 | 0.2661 | +0.0000 | 0.0000 | +0.0000 | +0.0000 |
| BM25 | quantized-flat(bits=4) | ov=4 | 1.0000 | 0.2661 | +0.0000 | 0.0000 | +0.0000 | +0.0000 |
| BM25 | quantized-flat(bits=4) | ov=8 | 1.0000 | 0.2661 | +0.0000 | 0.0000 | +0.0000 | +0.0000 |
| BM25 | quantized-flat(bits=4) | ov=16 | 1.0000 | 0.2661 | +0.0000 | 0.0000 | +0.0000 | +0.0000 |
| ItemKNN | exact | - | 1.0000 | 0.2550 | +0.0000 | 0.0000 | +0.0000 | +0.0000 |
| ItemKNN | hnsw(m=16,efc=200) | ef=16 | 0.9909 | 0.2542 | -0.0008 | 0.0074 | +0.0019 | +0.0048 |
| ItemKNN | hnsw(m=16,efc=200) | ef=32 | 0.9990 | 0.2548 | -0.0002 | 0.0000 | +0.0000 | -0.0006 |
| ItemKNN | hnsw(m=16,efc=200) | ef=64 | 0.9998 | 0.2550 | +0.0000 | 0.0000 | +0.0000 | +0.0000 |
| ItemKNN | hnsw(m=16,efc=200) | ef=128 | 1.0000 | 0.2550 | +0.0000 | 0.0000 | +0.0000 | +0.0000 |
| ItemKNN | hnsw(m=16,efc=200) | ef=256 | 1.0000 | 0.2550 | +0.0000 | 0.0000 | +0.0000 | +0.0000 |
| ItemKNN | quantized-flat(bits=8) | ov=1 | 0.9977 | 0.2551 | +0.0001 | 0.0000 | +0.0000 | +0.0000 |
| ItemKNN | quantized-flat(bits=8) | ov=2 | 1.0000 | 0.2550 | +0.0000 | 0.0000 | +0.0000 | +0.0000 |
| ItemKNN | quantized-flat(bits=8) | ov=4 | 1.0000 | 0.2550 | +0.0000 | 0.0000 | +0.0000 | +0.0000 |
| ItemKNN | quantized-flat(bits=8) | ov=8 | 1.0000 | 0.2550 | +0.0000 | 0.0000 | +0.0000 | +0.0000 |
| ItemKNN | quantized-flat(bits=8) | ov=16 | 1.0000 | 0.2550 | +0.0000 | 0.0000 | +0.0000 | +0.0000 |
| ItemKNN | quantized-flat(bits=4) | ov=1 | 0.9600 | 0.2553 | +0.0003 | 0.0000 | +0.0000 | -0.0006 |
| ItemKNN | quantized-flat(bits=4) | ov=2 | 1.0000 | 0.2550 | +0.0000 | 0.0000 | +0.0000 | +0.0000 |
| ItemKNN | quantized-flat(bits=4) | ov=4 | 1.0000 | 0.2550 | +0.0000 | 0.0000 | +0.0000 | +0.0000 |
| ItemKNN | quantized-flat(bits=4) | ov=8 | 1.0000 | 0.2550 | +0.0000 | 0.0000 | +0.0000 | +0.0000 |
| ItemKNN | quantized-flat(bits=4) | ov=16 | 1.0000 | 0.2550 | +0.0000 | 0.0000 | +0.0000 | +0.0000 |
| ALS | exact | - | 1.0000 | 0.0422 | +0.0000 | 0.0000 | +0.0000 | +0.0000 |
| ALS | hnsw(m=16,efc=200) | ef=16 | 0.9951 | 0.0421 | -0.0001 | 0.0032 | +0.0002 | -0.0012 |
| ALS | hnsw(m=16,efc=200) | ef=32 | 0.9992 | 0.0421 | -0.0001 | 0.0000 | -0.0000 | -0.0006 |
| ALS | hnsw(m=16,efc=200) | ef=64 | 0.9999 | 0.0422 | +0.0000 | 0.0000 | -0.0000 | +0.0000 |
| ALS | hnsw(m=16,efc=200) | ef=128 | 1.0000 | 0.0422 | +0.0000 | 0.0000 | -0.0000 | +0.0000 |
| ALS | hnsw(m=16,efc=200) | ef=256 | 1.0000 | 0.0422 | +0.0000 | 0.0000 | +0.0000 | +0.0000 |
| ALS | quantized-flat(bits=8) | ov=1 | 0.9802 | 0.0423 | +0.0001 | 0.0000 | -0.0000 | +0.0000 |
| ALS | quantized-flat(bits=8) | ov=2 | 1.0000 | 0.0422 | +0.0000 | 0.0000 | -0.0000 | +0.0000 |
| ALS | quantized-flat(bits=8) | ov=4 | 1.0000 | 0.0422 | +0.0000 | 0.0000 | -0.0000 | +0.0000 |
| ALS | quantized-flat(bits=8) | ov=8 | 1.0000 | 0.0422 | +0.0000 | 0.0000 | -0.0000 | +0.0000 |
| ALS | quantized-flat(bits=8) | ov=16 | 1.0000 | 0.0422 | +0.0000 | 0.0000 | -0.0000 | +0.0000 |
| ALS | quantized-flat(bits=4) | ov=1 | 0.7913 | 0.0404 | -0.0018 | 0.0000 | -0.0000 | +0.0018 |
| ALS | quantized-flat(bits=4) | ov=2 | 0.9603 | 0.0419 | -0.0003 | 0.0000 | -0.0000 | -0.0012 |
| ALS | quantized-flat(bits=4) | ov=4 | 0.9998 | 0.0422 | +0.0000 | 0.0000 | -0.0000 | +0.0000 |
| ALS | quantized-flat(bits=4) | ov=8 | 1.0000 | 0.0422 | +0.0000 | 0.0000 | -0.0000 | +0.0000 |
| ALS | quantized-flat(bits=4) | ov=16 | 1.0000 | 0.0422 | +0.0000 | 0.0000 | -0.0000 | +0.0000 |
| SimpleX | exact | - | 1.0000 | 0.2520 | +0.0000 | 0.0000 | +0.0000 | +0.0000 |
| SimpleX | hnsw(m=16,efc=200) | ef=16 | 0.9920 | 0.2525 | +0.0005 | 0.0032 | +0.0000 | -0.0042 |
| SimpleX | hnsw(m=16,efc=200) | ef=32 | 0.9980 | 0.2521 | +0.0001 | 0.0000 | -0.0000 | -0.0024 |
| SimpleX | hnsw(m=16,efc=200) | ef=64 | 0.9998 | 0.2520 | +0.0000 | 0.0000 | -0.0000 | -0.0006 |
| SimpleX | hnsw(m=16,efc=200) | ef=128 | 0.9998 | 0.2520 | +0.0000 | 0.0000 | -0.0000 | -0.0006 |
| SimpleX | hnsw(m=16,efc=200) | ef=256 | 1.0000 | 0.2520 | +0.0000 | 0.0000 | +0.0000 | +0.0000 |
| SimpleX | quantized-flat(bits=8) | ov=1 | 0.9043 | 0.2500 | -0.0020 | 0.0201 | +0.0000 | -0.0286 |
| SimpleX | quantized-flat(bits=8) | ov=2 | 0.9587 | 0.2513 | -0.0007 | 0.0117 | +0.0000 | -0.0083 |
| SimpleX | quantized-flat(bits=8) | ov=4 | 0.9788 | 0.2519 | -0.0001 | 0.0064 | +0.0000 | -0.0024 |
| SimpleX | quantized-flat(bits=8) | ov=8 | 0.9943 | 0.2519 | -0.0001 | 0.0021 | +0.0000 | -0.0006 |
| SimpleX | quantized-flat(bits=8) | ov=16 | 0.9987 | 0.2521 | +0.0001 | 0.0000 | -0.0000 | +0.0018 |
| SimpleX | quantized-flat(bits=4) | ov=1 | 0.5542 | 0.2190 | -0.0330 | 0.2789 | +0.0006 | -0.2304 |
| SimpleX | quantized-flat(bits=4) | ov=2 | 0.7036 | 0.2317 | -0.0204 | 0.2344 | +0.0004 | -0.1786 |
| SimpleX | quantized-flat(bits=4) | ov=4 | 0.7586 | 0.2393 | -0.0127 | 0.2036 | +0.0002 | -0.1286 |
| SimpleX | quantized-flat(bits=4) | ov=8 | 0.7963 | 0.2423 | -0.0097 | 0.1729 | +0.0001 | -0.0875 |
| SimpleX | quantized-flat(bits=4) | ov=16 | 0.8396 | 0.2442 | -0.0078 | 0.1432 | +0.0001 | -0.0530 |
| XSimGCL | exact | - | 1.0000 | 0.2930 | +0.0000 | 0.0000 | +0.0000 | +0.0000 |
| XSimGCL | hnsw(m=16,efc=200) | ef=16 | 0.9943 | 0.2922 | -0.0008 | 0.0064 | +0.0004 | +0.0012 |
| XSimGCL | hnsw(m=16,efc=200) | ef=32 | 0.9981 | 0.2930 | -0.0000 | 0.0011 | +0.0000 | +0.0012 |
| XSimGCL | hnsw(m=16,efc=200) | ef=64 | 0.9995 | 0.2931 | +0.0001 | 0.0000 | -0.0000 | -0.0006 |
| XSimGCL | hnsw(m=16,efc=200) | ef=128 | 1.0000 | 0.2930 | +0.0000 | 0.0000 | -0.0000 | +0.0000 |
| XSimGCL | hnsw(m=16,efc=200) | ef=256 | 1.0000 | 0.2930 | +0.0000 | 0.0000 | +0.0000 | +0.0000 |
| XSimGCL | quantized-flat(bits=8) | ov=1 | 0.9947 | 0.2931 | +0.0001 | 0.0000 | -0.0000 | +0.0012 |
| XSimGCL | quantized-flat(bits=8) | ov=2 | 1.0000 | 0.2930 | +0.0000 | 0.0000 | -0.0000 | +0.0000 |
| XSimGCL | quantized-flat(bits=8) | ov=4 | 1.0000 | 0.2930 | +0.0000 | 0.0000 | -0.0000 | +0.0000 |
| XSimGCL | quantized-flat(bits=8) | ov=8 | 1.0000 | 0.2930 | +0.0000 | 0.0000 | -0.0000 | +0.0000 |
| XSimGCL | quantized-flat(bits=8) | ov=16 | 1.0000 | 0.2930 | +0.0000 | 0.0000 | -0.0000 | +0.0000 |
| XSimGCL | quantized-flat(bits=4) | ov=1 | 0.9226 | 0.2924 | -0.0006 | 0.0000 | -0.0000 | +0.0048 |
| XSimGCL | quantized-flat(bits=4) | ov=2 | 0.9994 | 0.2930 | +0.0000 | 0.0000 | -0.0000 | +0.0000 |
| XSimGCL | quantized-flat(bits=4) | ov=4 | 0.9999 | 0.2930 | +0.0000 | 0.0000 | -0.0000 | +0.0000 |
| XSimGCL | quantized-flat(bits=4) | ov=8 | 1.0000 | 0.2930 | +0.0000 | 0.0000 | -0.0000 | +0.0000 |
| XSimGCL | quantized-flat(bits=4) | ov=16 | 1.0000 | 0.2930 | +0.0000 | 0.0000 | -0.0000 | +0.0000 |
What it costs to build, hold and search
| Model | Index | dial | fit | build | build %fit | index MB | vectors MB | rank median | rank q95 | speedup | users/s |
|---|---|---|---|---|---|---|---|---|---|---|---|
| SLIM | exact | - | 188 ms | - | - | - | - | 1 ms | 1 ms | 1.00x | 966,291 |
| SLIM | hnsw(m=16,efc=200) | ef=16 | 188 ms | 52 ms | 27.6% | 0.2 | 0.5 | 2 ms | 2 ms | 0.57x | 549,474 |
| SLIM | hnsw(m=16,efc=200) | ef=32 | 188 ms | 52 ms | 27.6% | 0.2 | 0.5 | 2 ms | 3 ms | 0.43x | 411,599 |
| SLIM | hnsw(m=16,efc=200) | ef=64 | 188 ms | 52 ms | 27.6% | 0.2 | 0.5 | 3 ms | 5 ms | 0.31x | 300,030 |
| SLIM | hnsw(m=16,efc=200) | ef=128 | 188 ms | 52 ms | 27.6% | 0.2 | 0.5 | 5 ms | 7 ms | 0.21x | 207,000 |
| SLIM | hnsw(m=16,efc=200) | ef=256 | 188 ms | 52 ms | 27.6% | 0.2 | 0.5 | 1 ms | 1 ms | 1.03x | 997,686 |
| SLIM | quantized-flat(bits=8) | ov=1 | 188 ms | 0 ms | 0.0% | 0.0 | 0.5 | 17 ms | 18 ms | 0.06x | 54,170 |
| SLIM | quantized-flat(bits=8) | ov=2 | 188 ms | 0 ms | 0.0% | 0.0 | 0.5 | 19 ms | 20 ms | 0.05x | 48,750 |
| SLIM | quantized-flat(bits=8) | ov=4 | 188 ms | 0 ms | 0.0% | 0.0 | 0.5 | 23 ms | 23 ms | 0.04x | 41,663 |
| SLIM | quantized-flat(bits=8) | ov=8 | 188 ms | 0 ms | 0.0% | 0.0 | 0.5 | 28 ms | 28 ms | 0.03x | 33,798 |
| SLIM | quantized-flat(bits=8) | ov=16 | 188 ms | 0 ms | 0.0% | 0.0 | 0.5 | 37 ms | 37 ms | 0.03x | 25,827 |
| SLIM | quantized-flat(bits=4) | ov=1 | 188 ms | 0 ms | 0.1% | 0.0 | 0.5 | 20 ms | 20 ms | 0.05x | 47,552 |
| SLIM | quantized-flat(bits=4) | ov=2 | 188 ms | 0 ms | 0.1% | 0.0 | 0.5 | 22 ms | 22 ms | 0.04x | 43,337 |
| SLIM | quantized-flat(bits=4) | ov=4 | 188 ms | 0 ms | 0.1% | 0.0 | 0.5 | 25 ms | 25 ms | 0.04x | 37,600 |
| SLIM | quantized-flat(bits=4) | ov=8 | 188 ms | 0 ms | 0.1% | 0.0 | 0.5 | 30 ms | 31 ms | 0.03x | 31,100 |
| SLIM | quantized-flat(bits=4) | ov=16 | 188 ms | 0 ms | 0.1% | 0.0 | 0.5 | 39 ms | 39 ms | 0.03x | 24,214 |
| EASE | exact | - | 29 ms | - | - | - | - | 3 ms | 3 ms | 1.00x | 308,357 |
| EASE | hnsw(m=16,efc=200) | ef=16 | 29 ms | 402 ms | 1381.5% | 0.2 | 22.6 | 3 ms | 4 ms | 1.04x | 319,316 |
| EASE | hnsw(m=16,efc=200) | ef=32 | 29 ms | 402 ms | 1381.5% | 0.2 | 22.6 | 3 ms | 4 ms | 0.91x | 279,473 |
| EASE | hnsw(m=16,efc=200) | ef=64 | 29 ms | 402 ms | 1381.5% | 0.2 | 22.6 | 5 ms | 6 ms | 0.62x | 192,080 |
| EASE | hnsw(m=16,efc=200) | ef=128 | 29 ms | 402 ms | 1381.5% | 0.2 | 22.6 | 8 ms | 10 ms | 0.36x | 111,092 |
| EASE | hnsw(m=16,efc=200) | ef=256 | 29 ms | 402 ms | 1381.5% | 0.2 | 22.6 | 3 ms | 4 ms | 0.91x | 279,774 |
| EASE | quantized-flat(bits=8) | ov=1 | 29 ms | 9 ms | 31.8% | 2.8 | 22.6 | 70 ms | 72 ms | 0.04x | 13,392 |
| EASE | quantized-flat(bits=8) | ov=2 | 29 ms | 9 ms | 31.8% | 2.8 | 22.6 | 73 ms | 75 ms | 0.04x | 12,901 |
| EASE | quantized-flat(bits=8) | ov=4 | 29 ms | 9 ms | 31.8% | 2.8 | 22.6 | 77 ms | 79 ms | 0.04x | 12,180 |
| EASE | quantized-flat(bits=8) | ov=8 | 29 ms | 9 ms | 31.8% | 2.8 | 22.6 | 85 ms | 86 ms | 0.04x | 11,146 |
| EASE | quantized-flat(bits=8) | ov=16 | 29 ms | 9 ms | 31.8% | 2.8 | 22.6 | 98 ms | 100 ms | 0.03x | 9,634 |
| EASE | quantized-flat(bits=4) | ov=1 | 29 ms | 17 ms | 58.6% | 1.4 | 22.6 | 78 ms | 79 ms | 0.04x | 12,070 |
| EASE | quantized-flat(bits=4) | ov=2 | 29 ms | 17 ms | 58.6% | 1.4 | 22.6 | 81 ms | 82 ms | 0.04x | 11,673 |
| EASE | quantized-flat(bits=4) | ov=4 | 29 ms | 17 ms | 58.6% | 1.4 | 22.6 | 85 ms | 87 ms | 0.04x | 11,050 |
| EASE | quantized-flat(bits=4) | ov=8 | 29 ms | 17 ms | 58.6% | 1.4 | 22.6 | 93 ms | 95 ms | 0.03x | 10,131 |
| EASE | quantized-flat(bits=4) | ov=16 | 29 ms | 17 ms | 58.6% | 1.4 | 22.6 | 108 ms | 110 ms | 0.03x | 8,760 |
| RP3Beta | exact | - | 8 ms | - | - | - | - | 1 ms | 1 ms | 1.00x | 1,006,762 |
| RP3Beta | hnsw(m=16,efc=200) | ef=16 | 8 ms | 252 ms | 3193.7% | 0.2 | 1.9 | 3 ms | 4 ms | 0.36x | 365,516 |
| RP3Beta | hnsw(m=16,efc=200) | ef=32 | 8 ms | 252 ms | 3193.7% | 0.2 | 1.9 | 3 ms | 5 ms | 0.28x | 281,561 |
| RP3Beta | hnsw(m=16,efc=200) | ef=64 | 8 ms | 252 ms | 3193.7% | 0.2 | 1.9 | 5 ms | 7 ms | 0.21x | 207,764 |
| RP3Beta | hnsw(m=16,efc=200) | ef=128 | 8 ms | 252 ms | 3193.7% | 0.2 | 1.9 | 7 ms | 8 ms | 0.14x | 144,594 |
| RP3Beta | hnsw(m=16,efc=200) | ef=256 | 8 ms | 252 ms | 3193.7% | 0.2 | 1.9 | 1 ms | 1 ms | 0.99x | 992,589 |
| RP3Beta | quantized-flat(bits=8) | ov=1 | 8 ms | 0 ms | 2.7% | 0.1 | 1.9 | 59 ms | 59 ms | 0.02x | 16,035 |
| RP3Beta | quantized-flat(bits=8) | ov=2 | 8 ms | 0 ms | 2.7% | 0.1 | 1.9 | 61 ms | 63 ms | 0.02x | 15,365 |
| RP3Beta | quantized-flat(bits=8) | ov=4 | 8 ms | 0 ms | 2.7% | 0.1 | 1.9 | 65 ms | 66 ms | 0.01x | 14,477 |
| RP3Beta | quantized-flat(bits=8) | ov=8 | 8 ms | 0 ms | 2.7% | 0.1 | 1.9 | 72 ms | 72 ms | 0.01x | 13,188 |
| RP3Beta | quantized-flat(bits=8) | ov=16 | 8 ms | 0 ms | 2.7% | 0.1 | 1.9 | 83 ms | 83 ms | 0.01x | 11,390 |
| RP3Beta | quantized-flat(bits=4) | ov=1 | 8 ms | 1 ms | 7.0% | 0.1 | 1.9 | 67 ms | 68 ms | 0.01x | 14,053 |
| RP3Beta | quantized-flat(bits=4) | ov=2 | 8 ms | 1 ms | 7.0% | 0.1 | 1.9 | 70 ms | 71 ms | 0.01x | 13,535 |
| RP3Beta | quantized-flat(bits=4) | ov=4 | 8 ms | 1 ms | 7.0% | 0.1 | 1.9 | 74 ms | 74 ms | 0.01x | 12,795 |
| RP3Beta | quantized-flat(bits=4) | ov=8 | 8 ms | 1 ms | 7.0% | 0.1 | 1.9 | 80 ms | 81 ms | 0.01x | 11,747 |
| RP3Beta | quantized-flat(bits=4) | ov=16 | 8 ms | 1 ms | 7.0% | 0.1 | 1.9 | 92 ms | 92 ms | 0.01x | 10,270 |
| BPR | exact | - | 1.30 s | - | - | - | - | 1 ms | 1 ms | 1.00x | 792,229 |
| BPR | hnsw(m=16,efc=200) | ef=16 | 1.30 s | 140 ms | 10.8% | 0.2 | 0.9 | 2 ms | 2 ms | 0.71x | 561,052 |
| BPR | hnsw(m=16,efc=200) | ef=32 | 1.30 s | 140 ms | 10.8% | 0.2 | 0.9 | 2 ms | 3 ms | 0.54x | 427,075 |
| BPR | hnsw(m=16,efc=200) | ef=64 | 1.30 s | 140 ms | 10.8% | 0.2 | 0.9 | 3 ms | 4 ms | 0.40x | 317,622 |
| BPR | hnsw(m=16,efc=200) | ef=128 | 1.30 s | 140 ms | 10.8% | 0.2 | 0.9 | 4 ms | 6 ms | 0.28x | 222,727 |
| BPR | hnsw(m=16,efc=200) | ef=256 | 1.30 s | 140 ms | 10.8% | 0.2 | 0.9 | 1 ms | 1 ms | 0.99x | 782,424 |
| BPR | quantized-flat(bits=8) | ov=1 | 1.30 s | 0 ms | 0.0% | 0.1 | 0.9 | 12 ms | 12 ms | 0.10x | 79,719 |
| BPR | quantized-flat(bits=8) | ov=2 | 1.30 s | 0 ms | 0.0% | 0.1 | 0.9 | 13 ms | 13 ms | 0.09x | 72,135 |
| BPR | quantized-flat(bits=8) | ov=4 | 1.30 s | 0 ms | 0.0% | 0.1 | 0.9 | 16 ms | 16 ms | 0.08x | 60,537 |
| BPR | quantized-flat(bits=8) | ov=8 | 1.30 s | 0 ms | 0.0% | 0.1 | 0.9 | 20 ms | 20 ms | 0.06x | 48,025 |
| BPR | quantized-flat(bits=8) | ov=16 | 1.30 s | 0 ms | 0.0% | 0.1 | 0.9 | 26 ms | 26 ms | 0.05x | 35,987 |
| BPR | quantized-flat(bits=4) | ov=1 | 1.30 s | 1 ms | 0.1% | 0.1 | 0.9 | 12 ms | 12 ms | 0.10x | 77,471 |
| BPR | quantized-flat(bits=4) | ov=2 | 1.30 s | 1 ms | 0.1% | 0.1 | 0.9 | 13 ms | 14 ms | 0.09x | 70,427 |
| BPR | quantized-flat(bits=4) | ov=4 | 1.30 s | 1 ms | 0.1% | 0.1 | 0.9 | 16 ms | 16 ms | 0.08x | 59,433 |
| BPR | quantized-flat(bits=4) | ov=8 | 1.30 s | 1 ms | 0.1% | 0.1 | 0.9 | 20 ms | 20 ms | 0.06x | 47,464 |
| BPR | quantized-flat(bits=4) | ov=16 | 1.30 s | 1 ms | 0.1% | 0.1 | 0.9 | 26 ms | 27 ms | 0.05x | 35,675 |
| BM25 | exact | - | 7 ms | - | - | - | - | 1 ms | 1 ms | 1.00x | 1,426,582 |
| BM25 | hnsw(m=16,efc=200) | ef=16 | 7 ms | 38 ms | 578.5% | 0.2 | 0.6 | 2 ms | 3 ms | 0.30x | 432,478 |
| BM25 | hnsw(m=16,efc=200) | ef=32 | 7 ms | 38 ms | 578.5% | 0.2 | 0.6 | 3 ms | 4 ms | 0.23x | 331,607 |
| BM25 | hnsw(m=16,efc=200) | ef=64 | 7 ms | 38 ms | 578.5% | 0.2 | 0.6 | 4 ms | 6 ms | 0.17x | 240,102 |
| BM25 | hnsw(m=16,efc=200) | ef=128 | 7 ms | 38 ms | 578.5% | 0.2 | 0.6 | 5 ms | 8 ms | 0.12x | 172,220 |
| BM25 | hnsw(m=16,efc=200) | ef=256 | 7 ms | 38 ms | 578.5% | 0.2 | 0.6 | 1 ms | 1 ms | 1.00x | 1,423,486 |
| BM25 | quantized-flat(bits=8) | ov=1 | 7 ms | 0 ms | 1.3% | 0.0 | 0.6 | 19 ms | 19 ms | 0.04x | 50,091 |
| BM25 | quantized-flat(bits=8) | ov=2 | 7 ms | 0 ms | 1.3% | 0.0 | 0.6 | 20 ms | 21 ms | 0.03x | 46,140 |
| BM25 | quantized-flat(bits=8) | ov=4 | 7 ms | 0 ms | 1.3% | 0.0 | 0.6 | 23 ms | 24 ms | 0.03x | 40,681 |
| BM25 | quantized-flat(bits=8) | ov=8 | 7 ms | 0 ms | 1.3% | 0.0 | 0.6 | 28 ms | 28 ms | 0.02x | 34,210 |
| BM25 | quantized-flat(bits=8) | ov=16 | 7 ms | 0 ms | 1.3% | 0.0 | 0.6 | 33 ms | 33 ms | 0.02x | 28,663 |
| BM25 | quantized-flat(bits=4) | ov=1 | 7 ms | 0 ms | 2.8% | 0.0 | 0.6 | 21 ms | 21 ms | 0.03x | 44,724 |
| BM25 | quantized-flat(bits=4) | ov=2 | 7 ms | 0 ms | 2.8% | 0.0 | 0.6 | 23 ms | 23 ms | 0.03x | 41,377 |
| BM25 | quantized-flat(bits=4) | ov=4 | 7 ms | 0 ms | 2.8% | 0.0 | 0.6 | 26 ms | 26 ms | 0.03x | 36,771 |
| BM25 | quantized-flat(bits=4) | ov=8 | 7 ms | 0 ms | 2.8% | 0.0 | 0.6 | 30 ms | 30 ms | 0.02x | 31,505 |
| BM25 | quantized-flat(bits=4) | ov=16 | 7 ms | 0 ms | 2.8% | 0.0 | 0.6 | 35 ms | 35 ms | 0.02x | 26,751 |
| ItemKNN | exact | - | 6 ms | - | - | - | - | 1 ms | 1 ms | 1.00x | 881,137 |
| ItemKNN | hnsw(m=16,efc=200) | ef=16 | 6 ms | 231 ms | 4108.9% | 0.2 | 1.4 | 2 ms | 3 ms | 0.56x | 494,618 |
| ItemKNN | hnsw(m=16,efc=200) | ef=32 | 6 ms | 231 ms | 4108.9% | 0.2 | 1.4 | 2 ms | 4 ms | 0.43x | 377,675 |
| ItemKNN | hnsw(m=16,efc=200) | ef=64 | 6 ms | 231 ms | 4108.9% | 0.2 | 1.4 | 3 ms | 4 ms | 0.31x | 272,906 |
| ItemKNN | hnsw(m=16,efc=200) | ef=128 | 6 ms | 231 ms | 4108.9% | 0.2 | 1.4 | 5 ms | 8 ms | 0.21x | 183,243 |
| ItemKNN | hnsw(m=16,efc=200) | ef=256 | 6 ms | 231 ms | 4108.9% | 0.2 | 1.4 | 1 ms | 1 ms | 1.00x | 881,532 |
| ItemKNN | quantized-flat(bits=8) | ov=1 | 6 ms | 0 ms | 2.7% | 0.1 | 1.4 | 38 ms | 39 ms | 0.03x | 24,543 |
| ItemKNN | quantized-flat(bits=8) | ov=2 | 6 ms | 0 ms | 2.7% | 0.1 | 1.4 | 41 ms | 41 ms | 0.03x | 23,241 |
| ItemKNN | quantized-flat(bits=8) | ov=4 | 6 ms | 0 ms | 2.7% | 0.1 | 1.4 | 44 ms | 44 ms | 0.02x | 21,582 |
| ItemKNN | quantized-flat(bits=8) | ov=8 | 6 ms | 0 ms | 2.7% | 0.1 | 1.4 | 49 ms | 49 ms | 0.02x | 19,252 |
| ItemKNN | quantized-flat(bits=8) | ov=16 | 6 ms | 0 ms | 2.7% | 0.1 | 1.4 | 58 ms | 59 ms | 0.02x | 16,127 |
| ItemKNN | quantized-flat(bits=4) | ov=1 | 6 ms | 0 ms | 7.2% | 0.0 | 1.4 | 45 ms | 45 ms | 0.02x | 21,034 |
| ItemKNN | quantized-flat(bits=4) | ov=2 | 6 ms | 0 ms | 7.2% | 0.0 | 1.4 | 47 ms | 47 ms | 0.02x | 20,082 |
| ItemKNN | quantized-flat(bits=4) | ov=4 | 6 ms | 0 ms | 7.2% | 0.0 | 1.4 | 50 ms | 51 ms | 0.02x | 18,706 |
| ItemKNN | quantized-flat(bits=4) | ov=8 | 6 ms | 0 ms | 7.2% | 0.0 | 1.4 | 56 ms | 56 ms | 0.02x | 16,867 |
| ItemKNN | quantized-flat(bits=4) | ov=16 | 6 ms | 0 ms | 7.2% | 0.0 | 1.4 | 65 ms | 66 ms | 0.02x | 14,407 |
| ALS | exact | - | 323 ms | - | - | - | - | 1 ms | 1 ms | 1.00x | 1,456,325 |
| ALS | hnsw(m=16,efc=200) | ef=16 | 323 ms | 88 ms | 27.3% | 0.2 | 0.1 | 1 ms | 1 ms | 0.83x | 1,209,104 |
| ALS | hnsw(m=16,efc=200) | ef=32 | 323 ms | 88 ms | 27.3% | 0.2 | 0.1 | 1 ms | 1 ms | 0.63x | 917,985 |
| ALS | hnsw(m=16,efc=200) | ef=64 | 323 ms | 88 ms | 27.3% | 0.2 | 0.1 | 2 ms | 2 ms | 0.43x | 627,212 |
| ALS | hnsw(m=16,efc=200) | ef=128 | 323 ms | 88 ms | 27.3% | 0.2 | 0.1 | 2 ms | 3 ms | 0.27x | 398,153 |
| ALS | hnsw(m=16,efc=200) | ef=256 | 323 ms | 88 ms | 27.3% | 0.2 | 0.1 | 1 ms | 1 ms | 1.00x | 1,458,389 |
| ALS | quantized-flat(bits=8) | ov=1 | 323 ms | 0 ms | 0.1% | 0.0 | 0.1 | 5 ms | 5 ms | 0.12x | 179,613 |
| ALS | quantized-flat(bits=8) | ov=2 | 323 ms | 0 ms | 0.1% | 0.0 | 0.1 | 6 ms | 7 ms | 0.10x | 146,582 |
| ALS | quantized-flat(bits=8) | ov=4 | 323 ms | 0 ms | 0.1% | 0.0 | 0.1 | 9 ms | 9 ms | 0.08x | 109,260 |
| ALS | quantized-flat(bits=8) | ov=8 | 323 ms | 0 ms | 0.1% | 0.0 | 0.1 | 13 ms | 13 ms | 0.05x | 75,371 |
| ALS | quantized-flat(bits=8) | ov=16 | 323 ms | 0 ms | 0.1% | 0.0 | 0.1 | 19 ms | 20 ms | 0.03x | 48,702 |
| ALS | quantized-flat(bits=4) | ov=1 | 323 ms | 0 ms | 0.1% | 0.0 | 0.1 | 5 ms | 5 ms | 0.12x | 179,986 |
| ALS | quantized-flat(bits=4) | ov=2 | 323 ms | 0 ms | 0.1% | 0.0 | 0.1 | 6 ms | 7 ms | 0.10x | 146,318 |
| ALS | quantized-flat(bits=4) | ov=4 | 323 ms | 0 ms | 0.1% | 0.0 | 0.1 | 9 ms | 9 ms | 0.07x | 107,907 |
| ALS | quantized-flat(bits=4) | ov=8 | 323 ms | 0 ms | 0.1% | 0.0 | 0.1 | 13 ms | 13 ms | 0.05x | 75,367 |
| ALS | quantized-flat(bits=4) | ov=16 | 323 ms | 0 ms | 0.1% | 0.0 | 0.1 | 19 ms | 20 ms | 0.03x | 48,467 |
| SimpleX | exact | - | 103.64 s | - | - | - | - | 1 ms | 1 ms | 1.00x | 985,264 |
| SimpleX | hnsw(m=16,efc=200) | ef=16 | 103.64 s | 290 ms | 0.3% | 0.2 | 0.9 | 3 ms | 3 ms | 0.32x | 318,084 |
| SimpleX | hnsw(m=16,efc=200) | ef=32 | 103.64 s | 290 ms | 0.3% | 0.2 | 0.9 | 4 ms | 4 ms | 0.25x | 249,960 |
| SimpleX | hnsw(m=16,efc=200) | ef=64 | 103.64 s | 290 ms | 0.3% | 0.2 | 0.9 | 5 ms | 6 ms | 0.19x | 186,823 |
| SimpleX | hnsw(m=16,efc=200) | ef=128 | 103.64 s | 290 ms | 0.3% | 0.2 | 0.9 | 7 ms | 7 ms | 0.14x | 137,086 |
| SimpleX | hnsw(m=16,efc=200) | ef=256 | 103.64 s | 290 ms | 0.3% | 0.2 | 0.9 | 1 ms | 1 ms | 1.00x | 986,187 |
| SimpleX | quantized-flat(bits=8) | ov=1 | 103.64 s | 0 ms | 0.0% | 0.1 | 0.9 | 24 ms | 24 ms | 0.04x | 39,997 |
| SimpleX | quantized-flat(bits=8) | ov=2 | 103.64 s | 0 ms | 0.0% | 0.1 | 0.9 | 25 ms | 26 ms | 0.04x | 37,530 |
| SimpleX | quantized-flat(bits=8) | ov=4 | 103.64 s | 0 ms | 0.0% | 0.1 | 0.9 | 28 ms | 29 ms | 0.03x | 33,275 |
| SimpleX | quantized-flat(bits=8) | ov=8 | 103.64 s | 0 ms | 0.0% | 0.1 | 0.9 | 33 ms | 34 ms | 0.03x | 28,368 |
| SimpleX | quantized-flat(bits=8) | ov=16 | 103.64 s | 0 ms | 0.0% | 0.1 | 0.9 | 41 ms | 41 ms | 0.02x | 23,015 |
| SimpleX | quantized-flat(bits=4) | ov=1 | 103.64 s | 1 ms | 0.0% | 0.1 | 0.9 | 23 ms | 24 ms | 0.04x | 40,155 |
| SimpleX | quantized-flat(bits=4) | ov=2 | 103.64 s | 1 ms | 0.0% | 0.1 | 0.9 | 25 ms | 25 ms | 0.04x | 37,454 |
| SimpleX | quantized-flat(bits=4) | ov=4 | 103.64 s | 1 ms | 0.0% | 0.1 | 0.9 | 29 ms | 29 ms | 0.03x | 33,086 |
| SimpleX | quantized-flat(bits=4) | ov=8 | 103.64 s | 1 ms | 0.0% | 0.1 | 0.9 | 34 ms | 34 ms | 0.03x | 27,936 |
| SimpleX | quantized-flat(bits=4) | ov=16 | 103.64 s | 1 ms | 0.0% | 0.1 | 0.9 | 42 ms | 42 ms | 0.02x | 22,407 |
| XSimGCL | exact | - | 149.26 s | - | - | - | - | 1 ms | 1 ms | 1.00x | 982,100 |
| XSimGCL | hnsw(m=16,efc=200) | ef=16 | 149.26 s | 224 ms | 0.2% | 0.2 | 0.9 | 2 ms | 2 ms | 0.47x | 465,727 |
| XSimGCL | hnsw(m=16,efc=200) | ef=32 | 149.26 s | 224 ms | 0.2% | 0.2 | 0.9 | 3 ms | 3 ms | 0.38x | 369,002 |
| XSimGCL | hnsw(m=16,efc=200) | ef=64 | 149.26 s | 224 ms | 0.2% | 0.2 | 0.9 | 3 ms | 4 ms | 0.28x | 278,212 |
| XSimGCL | hnsw(m=16,efc=200) | ef=128 | 149.26 s | 224 ms | 0.2% | 0.2 | 0.9 | 5 ms | 5 ms | 0.20x | 197,125 |
| XSimGCL | hnsw(m=16,efc=200) | ef=256 | 149.26 s | 224 ms | 0.2% | 0.2 | 0.9 | 1 ms | 1 ms | 1.00x | 981,546 |
| XSimGCL | quantized-flat(bits=8) | ov=1 | 149.26 s | 0 ms | 0.0% | 0.1 | 0.9 | 24 ms | 24 ms | 0.04x | 39,927 |
| XSimGCL | quantized-flat(bits=8) | ov=2 | 149.26 s | 0 ms | 0.0% | 0.1 | 0.9 | 25 ms | 26 ms | 0.04x | 37,388 |
| XSimGCL | quantized-flat(bits=8) | ov=4 | 149.26 s | 0 ms | 0.0% | 0.1 | 0.9 | 28 ms | 29 ms | 0.03x | 33,454 |
| XSimGCL | quantized-flat(bits=8) | ov=8 | 149.26 s | 0 ms | 0.0% | 0.1 | 0.9 | 33 ms | 33 ms | 0.03x | 28,805 |
| XSimGCL | quantized-flat(bits=8) | ov=16 | 149.26 s | 0 ms | 0.0% | 0.1 | 0.9 | 41 ms | 41 ms | 0.02x | 23,249 |
| XSimGCL | quantized-flat(bits=4) | ov=1 | 149.26 s | 1 ms | 0.0% | 0.1 | 0.9 | 24 ms | 24 ms | 0.04x | 40,066 |
| XSimGCL | quantized-flat(bits=4) | ov=2 | 149.26 s | 1 ms | 0.0% | 0.1 | 0.9 | 25 ms | 25 ms | 0.04x | 37,465 |
| XSimGCL | quantized-flat(bits=4) | ov=4 | 149.26 s | 1 ms | 0.0% | 0.1 | 0.9 | 28 ms | 28 ms | 0.03x | 33,522 |
| XSimGCL | quantized-flat(bits=4) | ov=8 | 149.26 s | 1 ms | 0.0% | 0.1 | 0.9 | 33 ms | 33 ms | 0.03x | 28,802 |
| XSimGCL | quantized-flat(bits=4) | ov=16 | 149.26 s | 1 ms | 0.0% | 0.1 | 0.9 | 41 ms | 41 ms | 0.02x | 23,179 |
What one request costs
| Model | users/request | exact | exact per user | hnsw (ef=64) | hnsw speedup | q8 (ov=4) | q8 speedup | q4 (ov=4) | q4 speedup |
|---|---|---|---|---|---|---|---|---|---|
| SLIM | 1 | 22 us | 22 us | 139 us | 0.15x | 88 us | 0.25x | 90 us | 0.24x |
| SLIM | 10 | 83 us | 8 us | 299 us | 0.28x | 266 us | 0.31x | 303 us | 0.27x |
| SLIM | 100 | 195 us | 2 us | 570 us | 0.34x | 2.31 ms | 0.08x | 2.64 ms | 0.07x |
| EASE | 1 | 66 us | 66 us | 185 us | 0.36x | 277 us | 0.24x | 319 us | 0.21x |
| EASE | 10 | 244 us | 24 us | 347 us | 0.70x | 1.18 ms | 0.21x | 1.36 ms | 0.18x |
| EASE | 100 | 556 us | 6 us | 1.17 ms | 0.48x | 8.44 ms | 0.07x | 9.67 ms | 0.06x |
| RP3Beta | 1 | 21 us | 21 us | 177 us | 0.12x | 153 us | 0.14x | 158 us | 0.13x |
| RP3Beta | 10 | 88 us | 9 us | 373 us | 0.23x | 761 us | 0.12x | 843 us | 0.10x |
| RP3Beta | 100 | 184 us | 2 us | 795 us | 0.23x | 6.99 ms | 0.03x | 7.88 ms | 0.02x |
| BPR | 1 | 22 us | 22 us | 105 us | 0.21x | 182 us | 0.12x | 185 us | 0.12x |
| BPR | 10 | 91 us | 9 us | 263 us | 0.35x | 223 us | 0.41x | 224 us | 0.41x |
| BPR | 100 | 276 us | 3 us | 483 us | 0.57x | 1.56 ms | 0.18x | 1.71 ms | 0.16x |
| BM25 | 1 | 16 us | 16 us | 146 us | 0.11x | 91 us | 0.18x | 88 us | 0.18x |
| BM25 | 10 | 84 us | 8 us | 249 us | 0.34x | 281 us | 0.30x | 309 us | 0.27x |
| BM25 | 100 | 127 us | 1 us | 614 us | 0.21x | 2.39 ms | 0.05x | 2.68 ms | 0.05x |
| ItemKNN | 1 | 24 us | 24 us | 148 us | 0.16x | 124 us | 0.19x | 133 us | 0.18x |
| ItemKNN | 10 | 125 us | 12 us | 270 us | 0.46x | 518 us | 0.24x | 607 us | 0.21x |
| ItemKNN | 100 | 205 us | 2 us | 600 us | 0.34x | 4.67 ms | 0.04x | 5.49 ms | 0.04x |
| ALS | 1 | 14 us | 14 us | 106 us | 0.13x | 69 us | 0.20x | 70 us | 0.20x |
| ALS | 10 | 45 us | 5 us | 174 us | 0.26x | 103 us | 0.44x | 104 us | 0.44x |
| ALS | 100 | 166 us | 2 us | 314 us | 0.53x | 844 us | 0.20x | 745 us | 0.22x |
| SimpleX | 1 | 111 us | 111 us | 377 us | 0.29x | 107 us | 1.04x | 106 us | 1.05x |
| SimpleX | 10 | 289 us | 29 us | 448 us | 0.65x | 332 us | 0.87x | 335 us | 0.86x |
| SimpleX | 100 | 312 us | 3 us | 958 us | 0.33x | 2.84 ms | 0.11x | 3.08 ms | 0.10x |
| XSimGCL | 1 | 112 us | 112 us | 395 us | 0.28x | 104 us | 1.08x | 104 us | 1.07x |
| XSimGCL | 10 | 234 us | 23 us | 343 us | 0.68x | 332 us | 0.70x | 326 us | 0.72x |
| XSimGCL | 100 | 337 us | 3 us | 659 us | 0.51x | 3.08 ms | 0.11x | 2.88 ms | 0.12x |
How ranking time answers to the size of the catalog
| Model | items | users | exact rank | hnsw rank (ef=64) | hnsw speedup | q8 rank (ov=4) | q8 speedup | q4 rank (ov=4) | q4 speedup |
|---|---|---|---|---|---|---|---|---|---|
| SLIM | 84 | 936 | 0 ms | 0 ms | 1.01x | 0 ms | 1.02x | 0 ms | 1.02x |
| SLIM | 168 | 942 | 0 ms | 0 ms | 1.01x | 6 ms | 0.07x | 6 ms | 0.06x |
| SLIM | 420 | 943 | 1 ms | 1 ms | 1.01x | 13 ms | 0.05x | 14 ms | 0.04x |
| SLIM | 840 | 943 | 1 ms | 3 ms | 0.23x | 20 ms | 0.04x | 22 ms | 0.04x |
| SLIM | 1,680 | 943 | 1 ms | 3 ms | 0.31x | 23 ms | 0.04x | 25 ms | 0.04x |
| EASE | 84 | 936 | 0 ms | 0 ms | 1.01x | 0 ms | 1.00x | 0 ms | 1.01x |
| EASE | 168 | 942 | 0 ms | 0 ms | 1.02x | 6 ms | 0.07x | 6 ms | 0.06x |
| EASE | 420 | 943 | 1 ms | 1 ms | 1.01x | 16 ms | 0.06x | 17 ms | 0.05x |
| EASE | 840 | 943 | 2 ms | 4 ms | 0.36x | 36 ms | 0.04x | 39 ms | 0.04x |
| EASE | 1,680 | 943 | 3 ms | 5 ms | 0.62x | 78 ms | 0.04x | 85 ms | 0.04x |
| RP3Beta | 84 | 936 | 0 ms | 0 ms | 0.99x | 0 ms | 0.99x | 0 ms | 0.98x |
| RP3Beta | 168 | 942 | 0 ms | 0 ms | 1.00x | 10 ms | 0.05x | 11 ms | 0.04x |
| RP3Beta | 420 | 943 | 1 ms | 1 ms | 1.01x | 19 ms | 0.04x | 21 ms | 0.03x |
| RP3Beta | 840 | 943 | 1 ms | 4 ms | 0.19x | 32 ms | 0.03x | 36 ms | 0.02x |
| RP3Beta | 1,680 | 943 | 1 ms | 4 ms | 0.21x | 65 ms | 0.01x | 74 ms | 0.01x |
| BPR | 84 | 936 | 0 ms | 0 ms | 1.00x | 0 ms | 1.00x | 0 ms | 1.01x |
| BPR | 168 | 942 | 0 ms | 0 ms | 1.00x | 4 ms | 0.10x | 4 ms | 0.10x |
| BPR | 420 | 943 | 1 ms | 1 ms | 1.00x | 7 ms | 0.08x | 7 ms | 0.08x |
| BPR | 840 | 943 | 1 ms | 3 ms | 0.28x | 10 ms | 0.08x | 10 ms | 0.08x |
| BPR | 1,680 | 943 | 1 ms | 3 ms | 0.39x | 16 ms | 0.08x | 16 ms | 0.07x |
| BM25 | 84 | 936 | 0 ms | 0 ms | 1.00x | 0 ms | 1.02x | 0 ms | 1.01x |
| BM25 | 168 | 942 | 0 ms | 0 ms | 1.05x | 4 ms | 0.09x | 4 ms | 0.08x |
| BM25 | 420 | 943 | 0 ms | 0 ms | 0.99x | 8 ms | 0.06x | 9 ms | 0.05x |
| BM25 | 840 | 943 | 1 ms | 3 ms | 0.18x | 14 ms | 0.04x | 15 ms | 0.04x |
| BM25 | 1,680 | 943 | 1 ms | 4 ms | 0.18x | 23 ms | 0.03x | 26 ms | 0.03x |
| ItemKNN | 84 | 936 | 0 ms | 0 ms | 1.01x | 0 ms | 1.01x | 0 ms | 1.00x |
| ItemKNN | 168 | 942 | 0 ms | 0 ms | 1.00x | 7 ms | 0.06x | 8 ms | 0.05x |
| ItemKNN | 420 | 943 | 1 ms | 1 ms | 1.00x | 14 ms | 0.05x | 16 ms | 0.04x |
| ItemKNN | 840 | 943 | 1 ms | 3 ms | 0.27x | 25 ms | 0.04x | 28 ms | 0.03x |
| ItemKNN | 1,680 | 943 | 1 ms | 4 ms | 0.30x | 44 ms | 0.02x | 50 ms | 0.02x |
| ALS | 84 | 936 | 0 ms | 0 ms | 1.01x | 0 ms | 1.02x | 0 ms | 1.02x |
| ALS | 168 | 942 | 0 ms | 0 ms | 1.00x | 3 ms | 0.11x | 3 ms | 0.11x |
| ALS | 420 | 943 | 0 ms | 0 ms | 1.00x | 5 ms | 0.08x | 5 ms | 0.09x |
| ALS | 840 | 943 | 1 ms | 1 ms | 0.35x | 6 ms | 0.08x | 6 ms | 0.08x |
| ALS | 1,680 | 943 | 1 ms | 1 ms | 0.43x | 9 ms | 0.07x | 9 ms | 0.07x |
| SimpleX | 84 | 936 | 0 ms | 0 ms | 0.99x | 0 ms | 0.99x | 0 ms | 1.00x |
| SimpleX | 168 | 942 | 0 ms | 0 ms | 1.00x | 5 ms | 0.09x | 5 ms | 0.09x |
| SimpleX | 420 | 943 | 1 ms | 1 ms | 1.02x | 15 ms | 0.04x | 16 ms | 0.04x |
| SimpleX | 840 | 943 | 1 ms | 4 ms | 0.19x | 17 ms | 0.05x | 17 ms | 0.04x |
| SimpleX | 1,680 | 943 | 1 ms | 5 ms | 0.19x | 29 ms | 0.04x | 29 ms | 0.04x |
| XSimGCL | 84 | 936 | 0 ms | 0 ms | 1.01x | 0 ms | 1.00x | 0 ms | 1.00x |
| XSimGCL | 168 | 942 | 1 ms | 1 ms | 0.99x | 5 ms | 0.10x | 5 ms | 0.10x |
| XSimGCL | 420 | 943 | 1 ms | 1 ms | 0.99x | 10 ms | 0.06x | 10 ms | 0.06x |
| XSimGCL | 840 | 943 | 1 ms | 3 ms | 0.24x | 16 ms | 0.05x | 16 ms | 0.05x |
| XSimGCL | 1,680 | 943 | 1 ms | 3 ms | 0.28x | 28 ms | 0.03x | 29 ms | 0.03x |
Measured on Apple M4 Pro (12 usable cores), macOS-26.7-arm64-arm-64bit-Mach-O, CPython 3.14.3, numpy 2.5.3, skrecsys 0.4.0, _core built in release mode. The quality table above is deterministic and portable; the timings below are not comparable across machines or builds.
The exact row of each model is the baseline every delta in its block is measured against. recall@10 is agreement with that exact model, not with the truth, which is why dNDCG@10 is beside it: recall counts items the index missed, and dNDCG@10 says whether missing them cost anything. The dial column names the query-time width each row was measured at: ef is HNSW's candidate list and ov is the quantized scan's shortlist, in multiples of k. Those two are not comparable to each other -- they are each index's own recall-for-latency knob, and only the columns either side of them are. The quantized rows' scores are always exact, because the shortlist is rescored with the original vectors before anything is returned: their score gap@1 moves only when the shortlist missed the true best item, never because a number was dequantized. A speedup below 1.00x means the index costs more than it saves. This is expected for neighbourhood models, whose exact path is an inverted-index scan that only touches items a user's history reaches. This catalog holds 1,680 items, which is below the default min_index_size of 4,096, so a model configured with index="hnsw" would have taken the exact path here and the rows below would not exist. The benchmark lowers that floor to measure the index itself; a speedup under 1.00x on a catalog this small is the reason the default floor exists.
Amazon Books (amzn-books-l50), leave-one-out split, k=10, default hyper-parameters. Scored on a fixed random sample of 10,000 held-out users. Not run: EASE (a dense 674k x 674k item matrix does not fit in memory); SLIM (one elastic net per item over a 674k-item catalog does not finish); SimpleX (one training epoch of 7.4M interactions runs over an hour on CPU); XSimGCL (one training epoch of 7.4M interactions runs over an hour on CPU). Regenerate with python benchmarks/run.py run indexes --dataset amazon-books.
What the index costs in answers
| Model | Index | dial | recall@10 | NDCG@10 | dNDCG@10 | top-1 churn | score gap@1 | dcat cov |
|---|---|---|---|---|---|---|---|---|
| RP3Beta | exact | - | 1.0000 | 0.0247 | +0.0000 | 0.0000 | +0.0000 | +0.0000 |
| RP3Beta | hnsw(m=16,efc=200) | ef=32 | 0.0036 | 0.0006 | -0.0241 | 0.9962 | +0.9520 | -0.1196 |
| RP3Beta | hnsw(m=16,efc=200) | ef=64 | 0.0044 | 0.0006 | -0.0240 | 0.9952 | +0.9326 | -0.1190 |
| RP3Beta | hnsw(m=16,efc=200) | ef=128 | 0.0052 | 0.0007 | -0.0239 | 0.9944 | +0.9159 | -0.1183 |
| RP3Beta | quantized-flat(bits=8) | ov=1 | 0.9505 | 0.0249 | +0.0002 | 0.0000 | +0.0000 | +0.0001 |
| RP3Beta | quantized-flat(bits=8) | ov=2 | 0.9998 | 0.0247 | +0.0000 | 0.0000 | +0.0000 | -0.0000 |
| RP3Beta | quantized-flat(bits=8) | ov=4 | 1.0000 | 0.0247 | +0.0000 | 0.0000 | +0.0000 | +0.0000 |
| RP3Beta | quantized-flat(bits=8) | ov=8 | 1.0000 | 0.0247 | +0.0000 | 0.0000 | +0.0000 | +0.0000 |
| RP3Beta | quantized-flat(bits=8) | ov=16 | 1.0000 | 0.0247 | +0.0000 | 0.0000 | +0.0000 | +0.0000 |
| RP3Beta | quantized-flat(bits=4) | ov=1 | 0.6326 | 0.0224 | -0.0022 | 0.0671 | +0.0111 | -0.0014 |
| RP3Beta | quantized-flat(bits=4) | ov=2 | 0.8080 | 0.0241 | -0.0006 | 0.0217 | +0.0037 | +0.0000 |
| RP3Beta | quantized-flat(bits=4) | ov=4 | 0.8885 | 0.0248 | +0.0001 | 0.0100 | +0.0018 | +0.0002 |
| RP3Beta | quantized-flat(bits=4) | ov=8 | 0.9290 | 0.0247 | +0.0001 | 0.0053 | +0.0007 | +0.0002 |
| RP3Beta | quantized-flat(bits=4) | ov=16 | 0.9637 | 0.0247 | +0.0000 | 0.0015 | +0.0002 | +0.0002 |
| BPR | exact | - | 1.0000 | 0.0075 | +0.0000 | 0.0000 | +0.0000 | +0.0000 |
| BPR | hnsw(m=16,efc=200) | ef=32 | 0.9366 | 0.0065 | -0.0010 | 0.0718 | +0.0044 | -0.0029 |
| BPR | hnsw(m=16,efc=200) | ef=64 | 0.9607 | 0.0068 | -0.0007 | 0.0448 | +0.0031 | -0.0021 |
| BPR | hnsw(m=16,efc=200) | ef=128 | 0.9774 | 0.0068 | -0.0007 | 0.0279 | +0.0020 | -0.0014 |
| BPR | quantized-flat(bits=8) | ov=1 | 0.9615 | 0.0075 | +0.0001 | 0.0000 | +0.0000 | -0.0000 |
| BPR | quantized-flat(bits=8) | ov=2 | 1.0000 | 0.0075 | +0.0000 | 0.0000 | +0.0000 | +0.0000 |
| BPR | quantized-flat(bits=8) | ov=4 | 1.0000 | 0.0075 | +0.0000 | 0.0000 | +0.0000 | +0.0000 |
| BPR | quantized-flat(bits=8) | ov=8 | 1.0000 | 0.0075 | +0.0000 | 0.0000 | +0.0000 | +0.0000 |
| BPR | quantized-flat(bits=8) | ov=16 | 1.0000 | 0.0075 | +0.0000 | 0.0000 | +0.0000 | +0.0000 |
| BPR | quantized-flat(bits=4) | ov=1 | 0.5480 | 0.0065 | -0.0010 | 0.1543 | +0.0017 | -0.0010 |
| BPR | quantized-flat(bits=4) | ov=2 | 0.7535 | 0.0073 | -0.0002 | 0.0551 | +0.0005 | -0.0001 |
| BPR | quantized-flat(bits=4) | ov=4 | 0.9118 | 0.0074 | -0.0000 | 0.0124 | +0.0001 | -0.0000 |
| BPR | quantized-flat(bits=4) | ov=8 | 0.9857 | 0.0075 | -0.0000 | 0.0010 | +0.0000 | +0.0000 |
| BPR | quantized-flat(bits=4) | ov=16 | 0.9993 | 0.0075 | +0.0000 | 0.0000 | +0.0000 | +0.0000 |
| BM25 | exact | - | 1.0000 | 0.0466 | +0.0000 | 0.0000 | +0.0000 | +0.0000 |
| BM25 | hnsw(m=16,efc=200) | ef=32 | 0.0002 | 0.0004 | -0.0462 | 0.9997 | +0.9944 | -0.1032 |
| BM25 | hnsw(m=16,efc=200) | ef=64 | 0.0003 | 0.0004 | -0.0462 | 0.9995 | +0.9925 | -0.1032 |
| BM25 | hnsw(m=16,efc=200) | ef=128 | 0.0005 | 0.0004 | -0.0462 | 0.9992 | +0.9879 | -0.1031 |
| BM25 | quantized-flat(bits=8) | ov=1 | 0.9470 | 0.0465 | -0.0001 | 0.0000 | +0.0000 | -0.0001 |
| BM25 | quantized-flat(bits=8) | ov=2 | 0.9990 | 0.0466 | +0.0000 | 0.0000 | +0.0000 | -0.0000 |
| BM25 | quantized-flat(bits=8) | ov=4 | 1.0000 | 0.0466 | +0.0000 | 0.0000 | +0.0000 | +0.0000 |
| BM25 | quantized-flat(bits=8) | ov=8 | 1.0000 | 0.0466 | +0.0000 | 0.0000 | +0.0000 | +0.0000 |
| BM25 | quantized-flat(bits=8) | ov=16 | 1.0000 | 0.0466 | +0.0000 | 0.0000 | +0.0000 | +0.0000 |
| BM25 | quantized-flat(bits=4) | ov=1 | 0.5952 | 0.0438 | -0.0028 | 0.1072 | +0.0198 | -0.0010 |
| BM25 | quantized-flat(bits=4) | ov=2 | 0.7567 | 0.0447 | -0.0019 | 0.0634 | +0.0096 | +0.0017 |
| BM25 | quantized-flat(bits=4) | ov=4 | 0.8950 | 0.0457 | -0.0009 | 0.0225 | +0.0025 | +0.0012 |
| BM25 | quantized-flat(bits=4) | ov=8 | 0.9822 | 0.0467 | +0.0001 | 0.0021 | +0.0002 | +0.0002 |
| BM25 | quantized-flat(bits=4) | ov=16 | 0.9983 | 0.0466 | +0.0000 | 0.0000 | +0.0000 | +0.0000 |
| ItemKNN | exact | - | 1.0000 | 0.0364 | +0.0000 | 0.0000 | +0.0000 | +0.0000 |
| ItemKNN | hnsw(m=16,efc=200) | ef=32 | 0.0157 | 0.0027 | -0.0337 | 0.9774 | +0.8798 | -0.1103 |
| ItemKNN | hnsw(m=16,efc=200) | ef=64 | 0.0177 | 0.0026 | -0.0338 | 0.9746 | +0.8628 | -0.1097 |
| ItemKNN | hnsw(m=16,efc=200) | ef=128 | 0.0223 | 0.0031 | -0.0334 | 0.9689 | +0.8212 | -0.1084 |
| ItemKNN | quantized-flat(bits=8) | ov=1 | 0.9875 | 0.0364 | -0.0001 | 0.0000 | +0.0000 | +0.0000 |
| ItemKNN | quantized-flat(bits=8) | ov=2 | 0.9999 | 0.0364 | +0.0000 | 0.0000 | +0.0000 | +0.0000 |
| ItemKNN | quantized-flat(bits=8) | ov=4 | 1.0000 | 0.0364 | +0.0000 | 0.0000 | +0.0000 | +0.0000 |
| ItemKNN | quantized-flat(bits=8) | ov=8 | 1.0000 | 0.0364 | +0.0000 | 0.0000 | +0.0000 | +0.0000 |
| ItemKNN | quantized-flat(bits=8) | ov=16 | 1.0000 | 0.0364 | +0.0000 | 0.0000 | +0.0000 | +0.0000 |
| ItemKNN | quantized-flat(bits=4) | ov=1 | 0.8391 | 0.0361 | -0.0003 | 0.0030 | +0.0002 | +0.0004 |
| ItemKNN | quantized-flat(bits=4) | ov=2 | 0.9748 | 0.0365 | +0.0001 | 0.0005 | +0.0000 | -0.0000 |
| ItemKNN | quantized-flat(bits=4) | ov=4 | 0.9947 | 0.0364 | +0.0000 | 0.0001 | +0.0000 | -0.0000 |
| ItemKNN | quantized-flat(bits=4) | ov=8 | 0.9981 | 0.0364 | +0.0000 | 0.0001 | +0.0000 | -0.0000 |
| ItemKNN | quantized-flat(bits=4) | ov=16 | 0.9996 | 0.0364 | +0.0000 | 0.0000 | +0.0000 | +0.0000 |
| ALS | exact | - | 1.0000 | 0.0000 | +0.0000 | 0.0000 | +0.0000 | +0.0000 |
| ALS | hnsw(m=16,efc=200) | ef=32 | 0.9413 | 0.0000 | -0.0000 | 0.0930 | +0.0091 | -0.0005 |
| ALS | hnsw(m=16,efc=200) | ef=64 | 0.9413 | 0.0000 | -0.0000 | 0.0930 | +0.0091 | -0.0005 |
| ALS | hnsw(m=16,efc=200) | ef=128 | 0.9430 | 0.0000 | -0.0000 | 0.0833 | +0.0075 | -0.0005 |
| ALS | quantized-flat(bits=8) | ov=1 | 0.9515 | 0.0000 | +0.0000 | 0.0000 | -0.0000 | +0.0000 |
| ALS | quantized-flat(bits=8) | ov=2 | 1.0000 | 0.0000 | +0.0000 | 0.0000 | -0.0000 | +0.0000 |
| ALS | quantized-flat(bits=8) | ov=4 | 1.0000 | 0.0000 | +0.0000 | 0.0000 | -0.0000 | +0.0000 |
| ALS | quantized-flat(bits=8) | ov=8 | 1.0000 | 0.0000 | +0.0000 | 0.0000 | -0.0000 | +0.0000 |
| ALS | quantized-flat(bits=8) | ov=16 | 1.0000 | 0.0000 | +0.0000 | 0.0000 | -0.0000 | +0.0000 |
| ALS | quantized-flat(bits=4) | ov=1 | 0.5453 | 0.0000 | +0.0000 | 0.0001 | +0.0000 | +0.0001 |
| ALS | quantized-flat(bits=4) | ov=2 | 0.5761 | 0.0000 | +0.0000 | 0.0000 | -0.0000 | +0.0001 |
| ALS | quantized-flat(bits=4) | ov=4 | 0.6246 | 0.0000 | +0.0000 | 0.0000 | -0.0000 | +0.0000 |
| ALS | quantized-flat(bits=4) | ov=8 | 0.7130 | 0.0000 | +0.0000 | 0.0000 | -0.0000 | +0.0000 |
| ALS | quantized-flat(bits=4) | ov=16 | 0.9994 | 0.0000 | +0.0000 | 0.0000 | -0.0000 | +0.0000 |
What it costs to build, hold and search
| Model | Index | dial | fit | build | build %fit | index MB | vectors MB | rank median | rank q95 | speedup | users/s |
|---|---|---|---|---|---|---|---|---|---|---|---|
| RP3Beta | exact | - | 1.56 s | - | - | - | - | 32 ms | 36 ms | 1.00x | 314,382 |
| RP3Beta | hnsw(m=16,efc=200) | ef=32 | 1.56 s | 868.52 s | 55618.2% | 95.7 | 579.8 | 72 ms | 79 ms | 0.44x | 139,619 |
| RP3Beta | hnsw(m=16,efc=200) | ef=64 | 1.56 s | 868.52 s | 55618.2% | 95.7 | 579.8 | 89 ms | 96 ms | 0.36x | 111,969 |
| RP3Beta | hnsw(m=16,efc=200) | ef=128 | 1.56 s | 868.52 s | 55618.2% | 95.7 | 579.8 | 125 ms | 133 ms | 0.25x | 79,989 |
| RP3Beta | quantized-flat(bits=8) | ov=1 | 1.56 s | 50 ms | 3.2% | 35.2 | 569.2 | 251.84 s | 252.32 s | 0.00x | 40 |
| RP3Beta | quantized-flat(bits=8) | ov=2 | 1.56 s | 50 ms | 3.2% | 35.2 | 569.2 | 251.85 s | 252.02 s | 0.00x | 40 |
| RP3Beta | quantized-flat(bits=8) | ov=4 | 1.56 s | 50 ms | 3.2% | 35.2 | 569.2 | 251.94 s | 252.55 s | 0.00x | 40 |
| RP3Beta | quantized-flat(bits=8) | ov=8 | 1.56 s | 50 ms | 3.2% | 35.2 | 569.2 | 252.07 s | 252.46 s | 0.00x | 40 |
| RP3Beta | quantized-flat(bits=8) | ov=16 | 1.56 s | 50 ms | 3.2% | 35.2 | 569.2 | 252.95 s | 253.09 s | 0.00x | 40 |
| RP3Beta | quantized-flat(bits=4) | ov=1 | 1.56 s | 153 ms | 9.8% | 17.6 | 569.2 | 249.63 s | 249.83 s | 0.00x | 40 |
| RP3Beta | quantized-flat(bits=4) | ov=2 | 1.56 s | 153 ms | 9.8% | 17.6 | 569.2 | 249.76 s | 249.90 s | 0.00x | 40 |
| RP3Beta | quantized-flat(bits=4) | ov=4 | 1.56 s | 153 ms | 9.8% | 17.6 | 569.2 | 248.86 s | 248.92 s | 0.00x | 40 |
| RP3Beta | quantized-flat(bits=4) | ov=8 | 1.56 s | 153 ms | 9.8% | 17.6 | 569.2 | 249.07 s | 249.08 s | 0.00x | 40 |
| RP3Beta | quantized-flat(bits=4) | ov=16 | 1.56 s | 153 ms | 9.8% | 17.6 | 569.2 | 249.78 s | 249.78 s | 0.00x | 40 |
| BPR | exact | - | 313.27 s | - | - | - | - | 3.90 s | 3.96 s | 1.00x | 2,565 |
| BPR | hnsw(m=16,efc=200) | ef=32 | 313.27 s | 233.34 s | 74.5% | 95.7 | 349.0 | 42 ms | 43 ms | 93.55x | 239,919 |
| BPR | hnsw(m=16,efc=200) | ef=64 | 313.27 s | 233.34 s | 74.5% | 95.7 | 349.0 | 59 ms | 62 ms | 65.58x | 168,199 |
| BPR | hnsw(m=16,efc=200) | ef=128 | 313.27 s | 233.34 s | 74.5% | 95.7 | 349.0 | 96 ms | 99 ms | 40.78x | 104,580 |
| BPR | quantized-flat(bits=8) | ov=1 | 313.27 s | 125 ms | 0.0% | 43.0 | 343.7 | 41.11 s | 41.15 s | 0.09x | 243 |
| BPR | quantized-flat(bits=8) | ov=2 | 313.27 s | 125 ms | 0.0% | 43.0 | 343.7 | 41.16 s | 41.19 s | 0.09x | 243 |
| BPR | quantized-flat(bits=8) | ov=4 | 313.27 s | 125 ms | 0.0% | 43.0 | 343.7 | 41.25 s | 41.26 s | 0.09x | 242 |
| BPR | quantized-flat(bits=8) | ov=8 | 313.27 s | 125 ms | 0.0% | 43.0 | 343.7 | 41.55 s | 41.55 s | 0.09x | 241 |
| BPR | quantized-flat(bits=8) | ov=16 | 313.27 s | 125 ms | 0.0% | 43.0 | 343.7 | 41.86 s | 41.89 s | 0.09x | 239 |
| BPR | quantized-flat(bits=4) | ov=1 | 313.27 s | 254 ms | 0.1% | 21.8 | 343.7 | 42.47 s | 42.49 s | 0.09x | 235 |
| BPR | quantized-flat(bits=4) | ov=2 | 313.27 s | 254 ms | 0.1% | 21.8 | 343.7 | 42.56 s | 42.58 s | 0.09x | 235 |
| BPR | quantized-flat(bits=4) | ov=4 | 313.27 s | 254 ms | 0.1% | 21.8 | 343.7 | 42.63 s | 42.64 s | 0.09x | 235 |
| BPR | quantized-flat(bits=4) | ov=8 | 313.27 s | 254 ms | 0.1% | 21.8 | 343.7 | 42.82 s | 42.82 s | 0.09x | 234 |
| BPR | quantized-flat(bits=4) | ov=16 | 313.27 s | 254 ms | 0.1% | 21.8 | 343.7 | 43.25 s | 43.26 s | 0.09x | 231 |
| BM25 | exact | - | 592 ms | - | - | - | - | 21 ms | 25 ms | 1.00x | 474,921 |
| BM25 | hnsw(m=16,efc=200) | ef=32 | 592 ms | 14.09 s | 2381.3% | 95.7 | 209.5 | 52 ms | 62 ms | 0.40x | 191,964 |
| BM25 | hnsw(m=16,efc=200) | ef=64 | 592 ms | 14.09 s | 2381.3% | 95.7 | 209.5 | 56 ms | 61 ms | 0.37x | 177,997 |
| BM25 | hnsw(m=16,efc=200) | ef=128 | 592 ms | 14.09 s | 2381.3% | 95.7 | 209.5 | 60 ms | 69 ms | 0.35x | 165,759 |
| BM25 | quantized-flat(bits=8) | ov=1 | 592 ms | 17 ms | 2.9% | 12.1 | 198.9 | 106.23 s | 106.26 s | 0.00x | 94 |
| BM25 | quantized-flat(bits=8) | ov=2 | 592 ms | 17 ms | 2.9% | 12.1 | 198.9 | 106.27 s | 106.32 s | 0.00x | 94 |
| BM25 | quantized-flat(bits=8) | ov=4 | 592 ms | 17 ms | 2.9% | 12.1 | 198.9 | 109.09 s | 111.06 s | 0.00x | 92 |
| BM25 | quantized-flat(bits=8) | ov=8 | 592 ms | 17 ms | 2.9% | 12.1 | 198.9 | 107.16 s | 107.37 s | 0.00x | 93 |
| BM25 | quantized-flat(bits=8) | ov=16 | 592 ms | 17 ms | 2.9% | 12.1 | 198.9 | 108.08 s | 108.34 s | 0.00x | 93 |
| BM25 | quantized-flat(bits=4) | ov=1 | 592 ms | 53 ms | 8.9% | 6.1 | 198.9 | 108.87 s | 108.93 s | 0.00x | 92 |
| BM25 | quantized-flat(bits=4) | ov=2 | 592 ms | 53 ms | 8.9% | 6.1 | 198.9 | 109.15 s | 109.29 s | 0.00x | 92 |
| BM25 | quantized-flat(bits=4) | ov=4 | 592 ms | 53 ms | 8.9% | 6.1 | 198.9 | 109.52 s | 109.67 s | 0.00x | 91 |
| BM25 | quantized-flat(bits=4) | ov=8 | 592 ms | 53 ms | 8.9% | 6.1 | 198.9 | 110.14 s | 110.21 s | 0.00x | 91 |
| BM25 | quantized-flat(bits=4) | ov=16 | 592 ms | 53 ms | 8.9% | 6.1 | 198.9 | 110.64 s | 110.84 s | 0.00x | 90 |
| ItemKNN | exact | - | 579 ms | - | - | - | - | 45 ms | 49 ms | 1.00x | 223,659 |
| ItemKNN | hnsw(m=16,efc=200) | ef=32 | 579 ms | 685.97 s | 118430.4% | 95.7 | 434.4 | 72 ms | 80 ms | 0.62x | 139,287 |
| ItemKNN | hnsw(m=16,efc=200) | ef=64 | 579 ms | 685.97 s | 118430.4% | 95.7 | 434.4 | 84 ms | 90 ms | 0.53x | 118,671 |
| ItemKNN | hnsw(m=16,efc=200) | ef=128 | 579 ms | 685.97 s | 118430.4% | 95.7 | 434.4 | 114 ms | 119 ms | 0.39x | 88,030 |
| ItemKNN | quantized-flat(bits=8) | ov=1 | 579 ms | 37 ms | 6.4% | 26.2 | 423.8 | 188.67 s | 188.88 s | 0.00x | 53 |
| ItemKNN | quantized-flat(bits=8) | ov=2 | 579 ms | 37 ms | 6.4% | 26.2 | 423.8 | 188.75 s | 188.81 s | 0.00x | 53 |
| ItemKNN | quantized-flat(bits=8) | ov=4 | 579 ms | 37 ms | 6.4% | 26.2 | 423.8 | 188.97 s | 189.08 s | 0.00x | 53 |
| ItemKNN | quantized-flat(bits=8) | ov=8 | 579 ms | 37 ms | 6.4% | 26.2 | 423.8 | 189.09 s | 189.17 s | 0.00x | 53 |
| ItemKNN | quantized-flat(bits=8) | ov=16 | 579 ms | 37 ms | 6.4% | 26.2 | 423.8 | 189.98 s | 189.99 s | 0.00x | 53 |
| ItemKNN | quantized-flat(bits=4) | ov=1 | 579 ms | 114 ms | 19.7% | 13.1 | 423.8 | 185.23 s | 185.26 s | 0.00x | 54 |
| ItemKNN | quantized-flat(bits=4) | ov=2 | 579 ms | 114 ms | 19.7% | 13.1 | 423.8 | 185.31 s | 185.43 s | 0.00x | 54 |
| ItemKNN | quantized-flat(bits=4) | ov=4 | 579 ms | 114 ms | 19.7% | 13.1 | 423.8 | 185.42 s | 185.56 s | 0.00x | 54 |
| ItemKNN | quantized-flat(bits=4) | ov=8 | 579 ms | 114 ms | 19.7% | 13.1 | 423.8 | 185.67 s | 185.67 s | 0.00x | 54 |
| ItemKNN | quantized-flat(bits=4) | ov=16 | 579 ms | 114 ms | 19.7% | 13.1 | 423.8 | 186.39 s | 186.42 s | 0.00x | 54 |
| ALS | exact | - | 65.30 s | - | - | - | - | 994 ms | 1.01 s | 1.00x | 10,061 |
| ALS | hnsw(m=16,efc=200) | ef=32 | 65.30 s | 85.16 s | 130.4% | 95.7 | 52.9 | 18 ms | 20 ms | 55.84x | 561,819 |
| ALS | hnsw(m=16,efc=200) | ef=64 | 65.30 s | 85.16 s | 130.4% | 95.7 | 52.9 | 20 ms | 22 ms | 50.47x | 507,820 |
| ALS | hnsw(m=16,efc=200) | ef=128 | 65.30 s | 85.16 s | 130.4% | 95.7 | 52.9 | 25 ms | 27 ms | 39.07x | 393,142 |
| ALS | quantized-flat(bits=8) | ov=1 | 65.30 s | 30 ms | 0.0% | 5.9 | 47.6 | 13.64 s | 13.65 s | 0.07x | 733 |
| ALS | quantized-flat(bits=8) | ov=2 | 65.30 s | 30 ms | 0.0% | 5.9 | 47.6 | 13.72 s | 13.73 s | 0.07x | 729 |
| ALS | quantized-flat(bits=8) | ov=4 | 65.30 s | 30 ms | 0.0% | 5.9 | 47.6 | 13.71 s | 13.72 s | 0.07x | 729 |
| ALS | quantized-flat(bits=8) | ov=8 | 65.30 s | 30 ms | 0.0% | 5.9 | 47.6 | 13.84 s | 13.85 s | 0.07x | 722 |
| ALS | quantized-flat(bits=8) | ov=16 | 65.30 s | 30 ms | 0.0% | 5.9 | 47.6 | 14.25 s | 14.26 s | 0.07x | 702 |
| ALS | quantized-flat(bits=4) | ov=1 | 65.30 s | 50 ms | 0.1% | 3.3 | 47.6 | 13.92 s | 13.94 s | 0.07x | 719 |
| ALS | quantized-flat(bits=4) | ov=2 | 65.30 s | 50 ms | 0.1% | 3.3 | 47.6 | 14.00 s | 14.01 s | 0.07x | 714 |
| ALS | quantized-flat(bits=4) | ov=4 | 65.30 s | 50 ms | 0.1% | 3.3 | 47.6 | 14.03 s | 14.03 s | 0.07x | 713 |
| ALS | quantized-flat(bits=4) | ov=8 | 65.30 s | 50 ms | 0.1% | 3.3 | 47.6 | 14.19 s | 14.20 s | 0.07x | 705 |
| ALS | quantized-flat(bits=4) | ov=16 | 65.30 s | 50 ms | 0.1% | 3.3 | 47.6 | 14.56 s | 14.57 s | 0.07x | 687 |
What one request costs
| Model | users/request | exact | exact per user | hnsw (ef=64) | hnsw speedup | q8 (ov=4) | q8 speedup | q4 (ov=4) | q4 speedup |
|---|---|---|---|---|---|---|---|---|---|
| RP3Beta | 1 | 114 us | 114 us | 20.72 ms | 0.01x | 38.75 ms | 0.00x | 38.46 ms | 0.00x |
| RP3Beta | 10 | 474 us | 47 us | 22.07 ms | 0.02x | 263.93 ms | 0.00x | 262.16 ms | 0.00x |
| RP3Beta | 100 | 2.35 ms | 23 us | 24.97 ms | 0.09x | 2.53 s | 0.00x | 2.51 s | 0.00x |
| RP3Beta | 1,000 | 11.57 ms | 12 us | 40.58 ms | 0.29x | 25.03 s | 0.00x | 24.88 s | 0.00x |
| BPR | 1 | 1.89 ms | 1.89 ms | 9.51 ms | 0.20x | 55.13 ms | 0.03x | 57.14 ms | 0.03x |
| BPR | 10 | 21.15 ms | 2.11 ms | 9.50 ms | 2.23x | 62.08 ms | 0.34x | 64.48 ms | 0.33x |
| BPR | 100 | 42.98 ms | 430 us | 10.99 ms | 3.91x | 453.58 ms | 0.09x | 469.58 ms | 0.09x |
| BPR | 1,000 | 327.12 ms | 327 us | 19.15 ms | 17.09x | 4.16 s | 0.08x | 4.30 s | 0.08x |
| BM25 | 1 | 98 us | 98 us | 13.78 ms | 0.01x | 17.51 ms | 0.01x | 17.63 ms | 0.01x |
| BM25 | 10 | 409 us | 41 us | 15.42 ms | 0.03x | 113.62 ms | 0.00x | 117.12 ms | 0.00x |
| BM25 | 100 | 3.29 ms | 33 us | 19.54 ms | 0.17x | 1.07 s | 0.00x | 1.10 s | 0.00x |
| BM25 | 1,000 | 12.96 ms | 13 us | 32.98 ms | 0.39x | 10.70 s | 0.00x | 10.92 s | 0.00x |
| ItemKNN | 1 | 116 us | 116 us | 18.50 ms | 0.01x | 30.26 ms | 0.00x | 29.90 ms | 0.00x |
| ItemKNN | 10 | 573 us | 57 us | 21.88 ms | 0.03x | 199.60 ms | 0.00x | 196.50 ms | 0.00x |
| ItemKNN | 100 | 3.19 ms | 32 us | 23.81 ms | 0.13x | 1.90 s | 0.00x | 1.87 s | 0.00x |
| ItemKNN | 1,000 | 13.82 ms | 14 us | 44.06 ms | 0.31x | 18.86 s | 0.00x | 18.55 s | 0.00x |
| ALS | 1 | 525 us | 525 us | 9.13 ms | 0.06x | 10.94 ms | 0.05x | 11.18 ms | 0.05x |
| ALS | 10 | 6.04 ms | 604 us | 9.63 ms | 0.63x | 18.66 ms | 0.32x | 18.79 ms | 0.32x |
| ALS | 100 | 12.16 ms | 122 us | 10.60 ms | 1.15x | 145.67 ms | 0.08x | 149.10 ms | 0.08x |
| ALS | 1,000 | 84.61 ms | 85 us | 16.25 ms | 5.21x | 1.38 s | 0.06x | 1.41 s | 0.06x |
Measured on Apple M4 Pro (12 usable cores), macOS-26.7-arm64-arm-64bit-Mach-O, CPython 3.14.3, numpy 2.5.3, skrecsys 0.4.0, _core built in release mode. The quality table above is deterministic and portable; the timings below are not comparable across machines or builds.
The exact row of each model is the baseline every delta in its block is measured against. recall@10 is agreement with that exact model, not with the truth, which is why dNDCG@10 is beside it: recall counts items the index missed, and dNDCG@10 says whether missing them cost anything. The dial column names the query-time width each row was measured at: ef is HNSW's candidate list and ov is the quantized scan's shortlist, in multiples of k. Those two are not comparable to each other -- they are each index's own recall-for-latency knob, and only the columns either side of them are. The quantized rows' scores are always exact, because the shortlist is rescored with the original vectors before anything is returned: their score gap@1 moves only when the shortlist missed the true best item, never because a number was dequantized. A speedup below 1.00x means the index costs more than it saves. This is expected for neighbourhood models, whose exact path is an inverted-index scan that only touches items a user's history reaches.
What those two tables say about explicit index choices.
A latent-factor model is where an index pays. It compresses a catalog into a few
dozen dimensions, which is the geometry a graph is good at. On Amazon Books, BPR ranks
10,000 users 66x faster at 0.96 recall (ef=64) and ALS 56x faster at 0.94
(ef=32), for a build that costs about as much as the fit. Those multiples are over an
exact path that scores all 660,940 items for every user, which no kernel can make
sublinear. On a 1,680-item catalog the same models keep essentially perfect recall and
lose on time, because there is nothing there to save.
A neighbourhood model is where it does not. recommend::top_k_from_similarity is
already an inverted-index scan whose cost follows the user's history and the width of a
similarity row rather than the catalog, so a graph has little to win — and at six figures
it does not merely lose on time, it stops working. BM25 retrieves 0.0002 of the exact
answer on Amazon Books, ItemKNN 0.016. That is noise, not approximation, and the
reason is structural: an item keeps 50 neighbours out of 660,940 columns, so two item
vectors share a stored value with probability about 0.004. Nearly every pair scores
zero while the graph is being built, the graph comes out of ties, and the walk returns
whatever it reaches.
EASE is the row that justifies printing both tables. On MovieLens the graph
retrieves 0.27 of the right answer at ef=16 and gives up 0.10 of NDCG, and it is
no longer even fast: the exact path adds one catalog-wide row per item a user has seen
and ranks all 943 users in 3 ms, so the graph is 1.04x at best and 0.62x at ef=64.
The quantized scan is the same row read the other way:
on that model it keeps 1.0000 recall at bits=8, ov=2, and runs at 0.04x of the
exact path's speed.
What the quantized scan costs¶
On both datasets QuantizedFlatIndex is slower than the exact path for every model, at
every setting measured. On MovieLens the speedups run from 0.12x (ALS, bits=8, ov=1)
down to 0.01x (RP3Beta, bits=4); on Amazon Books the best is 0.09x (BPR,
bits=8) and the neighbourhood models round to 0.00x. The scan visits every
candidate on one thread, while the exact neighbourhood path only touches the items a user's
history reaches, and the exact factor path is a tiled, multi-threaded kernel.
What it does deliver is the other three columns:
- Accuracy, essentially free. At
bits=8, ov=2it reproduces the exact answer —recall@10of1.0000— onALS,BPR,ItemKNN,BM25,RP3Beta,SLIM,EASEandXSimGCLon MovieLens, and on Amazon Books it holds1.0000forALSandBPRand at least0.999for the neighbourhood models, where the graph retrieves under0.03. Compare the graph onEASE, which needsef=256to reach the same place. - Exact scores, and a
score gap@1that says something different from the graph's. 81 of the 90 MovieLens quantized rows show+0.0000, and the nine that do not are allbits=4, all under+0.0006; on Amazon Books the thirteen that do not arebits=4too, all under+0.02. Those rows are not dequantized numbers leaking into the output — they cannot be, because the shortlist is rescored with the original vectors before anything is returned. They are rows where the shortlist missed the true best item, so first place went to a genuinely lower-scoring one, and the gap is the real difference between two real scores. The graph's gaps reach+0.16on MovieLens and+0.99on Amazon Books for the same reason. - A build that rounds to nothing.
0to17 mson MovieLens, against88 msfor the graph onALSand402 msonEASE;17to254 mson Amazon Books, against graph builds of 14 s (BM25) to 14 minutes (RP3Beta).
SimpleX is the model that does not fit the summary, and is worth reading before
choosing bits. At bits=8 it needs ov=8 to pass 0.99; at bits=4 it reaches only
0.84 even at ov=16. Its factors evidently carry more in their fine structure than the
other models' do, and four bits throws that away. The graph has no such trouble with it
(0.992 at ef=16). Narrow codes are a per-model decision, not a global one.
Integer arithmetic in the quantized scan¶
QuantizedFlatIndex scans with narrow item codes and a floating-point query. A natural
extension is to quantize the query as well and run the inner loop as
int8×int8→int32, which is where real quantized indexes get their speed: one ARM udot
does sixteen multiply-accumulates against NEON's two f64 ones. It was built and
measured, and not adopted. This note records why.
The arithmetic works out cleanly — an integer accumulator can only hold Σ qc·vc, so
every scale has to come out of the sum:
<q,v> ~= sq·sv·Σ(qc·vc) + sq·ov·Σqc + oq·sv·Σvc + oq·ov·dim
with Σvc precomputed per item. Three findings decided against it:
- It was worth 1.26x, not 7x. Measured on the 60,000-item catalog:
5.44→4.33ns per candidate at eight bits,5.66→3.97at four. Still4.8xslower than the exact path, so nothing about the decision changes. udotnever appeared. LLVM emitsu32multiplies from safe Rust, not the sixteen-lane widening dot product the estimate assumed. Reaching it needsstd::arch::aarch64intrinsics, which areunsafe, and this crate does not useunsafeanywhere. That is a deliberate property worth more than 1.3x.- It set a trap for
EASE. An integer accumulator forces one global scale in place of the per-dimension ones.EASE's space is dense, so it qualified at fit time — but it always scores through sparse queries, so it fell back to the float kernel and paid the accuracy cost for none of the speed. Recall went to0.059at four bits and0.000at two, against0.998with per-dimension scales. Silent, catastrophic, and exactly the "fast and wrong" outcome the index benchmarks in the README exist to catch.
Tiling survived because it is free: it changes the loop order, not the numbers. Integer arithmetic is not free — it trades away per-dimension resolution — and at 1.26x the trade does not pay.
The Amazon Books block is a long run to reproduce: the quantized rows alone take hours
over a 660,940-item catalog, because the scan is sequential by design, and the ItemKNN
and RP3Beta graph builds are 686 s and 869 s.
python benchmarks/run.py run indexes --dataset amazon-books re-measures only the rows
whose config has changed.
Use index=None for the exact path, which is the default. The tables show where an explicit approximate index helps and where it costs time or quality; measure the model and catalog before choosing one.
MostPopularRecommender has no index parameter and appears in neither table: its score
does not depend on the query, so there is nothing for a neighbour index to narrow.