Quantised indices¶
The eleven estimators over compressed vectors. See Quantised for what each codec does and when to reach for it.
Two things differ from the uncompressed estimators: the distances are the codec's estimate rather than the distance, and none of them support Manhattan.
quantised
¶
Quantised estimators: the same indices, over compressed vectors.
These trade recall for memory. Storage drops from dim floats per vector to
somewhere between two bytes per dimension (BF16) and a handful of bytes per
vector (PQ), and in several cases the query gets faster too, because integer
kernels beat float ones.
Two things differ from the uncompressed estimators, both consequences of the codec rather than choices:
- Distances are estimates. A quantised index reports the codec's estimate
of a distance, not the distance. It is close enough to rank on, which is what
an index is for, but don't feed it anywhere the absolute value matters
without checking it against
ExhaustiveIndexfirst. - Manhattan is unavailable on all eleven. Every codec here is built on inner products, which is what makes the integer arithmetic work.
Everything else is identical: same four-method surface, same padding, same persistence, float32 and float64 both supported.
Which one to reach for, roughly. HnswSq8uIndex if you want the usual first
choice at a quarter of the memory. IvfSq8Index if you were already on
IvfIndex. The PQ family when a quarter is not enough of a saving, which
generally means a high-dimensional embedding space rather than a 30-dimensional
PCA. See the benchmark tables for numbers.
ExhaustiveBf16Index
¶
Bases: _BaseQuantisedIndex
Brute force over bf16 storage.
bf16 keeps float32's exponent range and throws away mantissa bits from
roughly the third significant digit on, so it halves the memory without the
overflow traps of float16. The scan is still exhaustive, so the only thing
between this and ExhaustiveIndex is the codec's rounding. The cheapest
quantisation to reason about, and the one with the least to go wrong.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
n_neighbors
|
int
|
Neighbours per query, and the default |
15
|
metric
|
str
|
|
'euclidean'
|
verbose
|
bool
|
Progress to the process stdout, not |
False
|
IvfBf16Index
¶
IvfBf16Index(
n_neighbors: int = 15,
metric: str = "euclidean",
nlist: int | None = None,
nprobe: int | None = None,
kmeans_iters: int | None = None,
kmeans_balanced: bool = False,
seed: int = 42,
verbose: bool = False,
)
Bases: _BaseQuantisedIndex
IvfIndex with the posting lists held at bf16.
Half the vector memory for a codec error that lands well inside IVF's own
approximation, so at a fixed nprobe the recall barely moves. If you are
already using IvfIndex and memory is the binding constraint, this is the
swap to make first.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
n_neighbors
|
int
|
Neighbours per query, and the default |
15
|
metric
|
str
|
|
'euclidean'
|
nlist
|
int | None
|
Voronoi cells to cut the space into. |
None
|
nprobe
|
int | None
|
Cells visited per query, the recall knob. |
None
|
kmeans_iters
|
int | None
|
Lloyd iterations when training the cells. |
None
|
kmeans_balanced
|
bool
|
Reseed starved centroids each iteration, evening out the posting lists. |
False
|
seed
|
int
|
Fixes k-means initialisation. |
42
|
verbose
|
bool
|
Progress to the process stdout, not |
False
|
ExhaustiveSq8Index
¶
ExhaustiveSq8Index(
n_neighbors: int = 15,
metric: str = "euclidean",
quant_drop_ratio: float | None = None,
quant_sample_rows: int | None = None,
seed: int = 42,
verbose: bool = False,
)
Bases: _BaseQuantisedIndex
Brute force over 8-bit codes.
One byte per dimension, with per-dimension offsets and a single scale shared
across all of them. The shared scale is the point: it makes the integer code
distance preserve the ordering of the float one, so the whole scan runs on
u8 kernels and usually comes out faster than the float version as well
as four times smaller.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
n_neighbors
|
int
|
Neighbours per query, and the default |
15
|
metric
|
str
|
|
'euclidean'
|
quant_drop_ratio
|
float | None
|
Fraction trimmed from each tail of every dimension
before the range is fixed; values outside it clamp to the end codes.
|
None
|
quant_sample_rows
|
int | None
|
Rows sampled to calibrate the range. |
None
|
seed
|
int
|
Fixes the calibration row sample. |
42
|
verbose
|
bool
|
Progress to the process stdout, not |
False
|
IvfSq8Index
¶
IvfSq8Index(
n_neighbors: int = 15,
metric: str = "euclidean",
nlist: int | None = None,
nprobe: int | None = None,
kmeans_iters: int | None = None,
kmeans_balanced: bool = False,
quant_drop_ratio: float | None = None,
quant_sample_rows: int | None = None,
seed: int = 42,
verbose: bool = False,
)
Bases: _BaseQuantisedIndex
IvfIndex with the posting lists held as 8-bit codes.
A quarter of IVF's vector memory, and the integer kernels usually make the
cell scan faster rather than slower. The natural default if you were already
on IvfIndex and want the saving without thinking about subspaces.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
n_neighbors
|
int
|
Neighbours per query, and the default |
15
|
metric
|
str
|
|
'euclidean'
|
nlist
|
int | None
|
Voronoi cells to cut the space into. |
None
|
nprobe
|
int | None
|
Cells visited per query, the recall knob. |
None
|
kmeans_iters
|
int | None
|
Lloyd iterations when training the cells. |
None
|
kmeans_balanced
|
bool
|
Reseed starved centroids each iteration. |
False
|
quant_drop_ratio
|
float | None
|
Fraction trimmed from each tail of every dimension
before the range is fixed. |
None
|
quant_sample_rows
|
int | None
|
Rows sampled to calibrate the range. |
None
|
seed
|
int
|
Fixes k-means initialisation and the calibration sample. |
42
|
verbose
|
bool
|
Progress to the process stdout, not |
False
|
HnswSq8uIndex
¶
HnswSq8uIndex(
n_neighbors: int = 15,
metric: str = "euclidean",
m: int = 16,
ef_construction: int = 200,
ef_search: int = 100,
quant_drop_ratio: float | None = None,
quant_sample_rows: int | None = None,
seed: int = 42,
verbose: bool = False,
)
Bases: _BaseQuantisedIndex
HnswIndex built and searched entirely on 8-bit codes.
Inspired by pyglass. The graph is constructed in the space it is searched in, so there is no float copy hanging around for re-ranking and no mismatch between the edges and the distances that traverse them. Roughly a quarter of HNSW's vector memory, and the same recall knob.
If you want one quantised index and no further reading, this is it.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
n_neighbors
|
int
|
Neighbours per query, and the default |
15
|
metric
|
str
|
|
'euclidean'
|
m
|
int
|
Edges per node on the upper layers; the base layer gets |
16
|
ef_construction
|
int
|
Candidate list size during insertion. Larger means a better graph and a slower build, and it costs nothing at query time. |
200
|
ef_search
|
int
|
Beam width at query time, the recall knob. Raised to |
100
|
quant_drop_ratio
|
float | None
|
Fraction trimmed from each tail of every dimension
before the range is fixed. |
None
|
quant_sample_rows
|
int | None
|
Rows sampled to calibrate the range. |
None
|
seed
|
int
|
Fixes the level assignment and the calibration sample. |
42
|
verbose
|
bool
|
Progress to the process stdout, not |
False
|
ExhaustivePqIndex
¶
ExhaustivePqIndex(
n_neighbors: int = 15,
m: int = 8,
metric: str = "euclidean",
max_iters: int | None = None,
n_pq_centroids: int | None = None,
seed: int = 42,
verbose: bool = False,
)
Bases: _BaseQuantisedIndex
Brute force over product-quantised codes.
Each vector is cut into m subvectors and each subvector is replaced by the
id of its nearest sub-codebook centroid, so a vector costs m bytes rather
than dim floats. A query builds one lookup table per subspace and then
scores each point by summing m table reads.
The compression is the whole point and it is aggressive: at dim=512 and
m=64 that is 64 bytes against 2 KB. Expect the recall to reflect that.
This is a method for high-dimensional embedding spaces, not for a
30-dimensional PCA where ExhaustiveSq8Index will do better on both counts.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
n_neighbors
|
int
|
Neighbours per query, and the default |
15
|
m
|
int
|
Subspaces. |
8
|
metric
|
str
|
|
'euclidean'
|
max_iters
|
int | None
|
Lloyd iterations when training the sub-codebooks. |
None
|
n_pq_centroids
|
int | None
|
Centroids per subspace. |
None
|
seed
|
int
|
Fixes the codebook initialisation. |
42
|
verbose
|
bool
|
Progress to the process stdout, not |
False
|
ExhaustiveOpqIndex
¶
ExhaustiveOpqIndex(
n_neighbors: int = 15,
m: int = 8,
metric: str = "euclidean",
max_iters: int | None = None,
n_pq_centroids: int | None = None,
seed: int = 42,
verbose: bool = False,
)
Bases: _BaseQuantisedIndex
ExhaustivePqIndex with a learned rotation in front of it.
Plain PQ splits on the original axis order, so a space where the variance is concentrated in a few coordinates gets subspaces of wildly unequal difficulty. OPQ learns an orthogonal rotation that spreads it before splitting. It costs more at build time and nothing at query time, since the rotation folds into the query once.
Worth it on a raw embedding space, rarely worth it after a PCA has already done the rotating.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
n_neighbors
|
int
|
Neighbours per query, and the default |
15
|
m
|
int
|
Subspaces. |
8
|
metric
|
str
|
|
'euclidean'
|
max_iters
|
int | None
|
Alternating rotation/codebook iterations. |
None
|
n_pq_centroids
|
int | None
|
Centroids per subspace. |
None
|
seed
|
int
|
Fixes the codebook initialisation. |
42
|
verbose
|
bool
|
Progress to the process stdout, not |
False
|
IvfPqIndex
¶
IvfPqIndex(
n_neighbors: int = 15,
m: int = 8,
metric: str = "euclidean",
nlist: int | None = None,
nprobe: int | None = None,
kmeans_iters: int | None = None,
kmeans_balanced: bool = False,
n_pq_centroids: int | None = None,
seed: int = 42,
verbose: bool = False,
)
Bases: _BaseQuantisedIndex
Inverted file plus product quantisation.
The two compress different things and stack cleanly: the inverted file cuts
how many vectors get scored, PQ cuts what each one costs. Codes are learned
on the residual from the cell centroid rather than the vector itself, so the
sub-codebooks only have to cover within-cell spread, which is why this
reaches better recall than ExhaustivePqIndex at the same m.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
n_neighbors
|
int
|
Neighbours per query, and the default |
15
|
m
|
int
|
Subspaces. |
8
|
metric
|
str
|
|
'euclidean'
|
nlist
|
int | None
|
Voronoi cells to cut the space into. |
None
|
nprobe
|
int | None
|
Cells visited per query, the recall knob. |
None
|
kmeans_iters
|
int | None
|
Lloyd iterations when training the cells. |
None
|
kmeans_balanced
|
bool
|
Reseed starved centroids each iteration. |
False
|
n_pq_centroids
|
int | None
|
Centroids per subspace. |
None
|
seed
|
int
|
Fixes both the cell and the codebook initialisation. |
42
|
verbose
|
bool
|
Progress to the process stdout, not |
False
|
IvfOpqIndex
¶
IvfOpqIndex(
n_neighbors: int = 15,
m: int = 8,
metric: str = "euclidean",
nlist: int | None = None,
nprobe: int | None = None,
kmeans_iters: int | None = None,
kmeans_balanced: bool = False,
n_pq_centroids: int | None = None,
opq_iters: int | None = None,
seed: int = 42,
verbose: bool = False,
)
Bases: _BaseQuantisedIndex
IvfPqIndex with the learned rotation in front of the sub-codebooks.
Same trade as ExhaustiveOpqIndex against ExhaustivePqIndex: better codes
for a slower build, free at query time.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
n_neighbors
|
int
|
Neighbours per query, and the default |
15
|
m
|
int
|
Subspaces. |
8
|
metric
|
str
|
|
'euclidean'
|
nlist
|
int | None
|
Voronoi cells to cut the space into. |
None
|
nprobe
|
int | None
|
Cells visited per query, the recall knob. |
None
|
kmeans_iters
|
int | None
|
Lloyd iterations when training the cells. |
None
|
kmeans_balanced
|
bool
|
Reseed starved centroids each iteration. |
False
|
n_pq_centroids
|
int | None
|
Centroids per subspace. |
None
|
opq_iters
|
int | None
|
Alternating rotation/codebook iterations. |
None
|
seed
|
int
|
Fixes both the cell and the codebook initialisation. |
42
|
verbose
|
bool
|
Progress to the process stdout, not |
False
|
SoarPqIndex
¶
SoarPqIndex(
n_neighbors: int = 15,
m: int = 8,
metric: str = "euclidean",
nlist: int | None = None,
nprobe: int | None = None,
rule: str | None = None,
rule_param: float | None = None,
kmeans_iters: int | None = None,
kmeans_balanced: bool = False,
n_pq_centroids: int | None = None,
seed: int = 42,
verbose: bool = False,
)
Bases: _BaseQuantisedIndex
IvfPqIndex with SOAR spilling.
Every vector also lands in a second cell, chosen so its residual points somewhere the first one doesn't. That fixes IVF's main failure, a true neighbour sitting just over a cell boundary, at the cost of roughly twice the posting-list size, which is exactly what quantisation makes affordable.
Read the trade against query time, not against nprobe: at a fixed
nprobe a spilled index scans about twice the candidates, so comparing at
equal nprobe flatters it and answers nothing.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
n_neighbors
|
int
|
Neighbours per query, and the default |
15
|
m
|
int
|
Subspaces. |
8
|
metric
|
str
|
|
'euclidean'
|
nlist
|
int | None
|
Voronoi cells to cut the space into. |
None
|
nprobe
|
int | None
|
Cells visited per query, the recall knob. |
None
|
rule
|
str | None
|
Spilling rule: |
None
|
rule_param
|
float | None
|
|
None
|
kmeans_iters
|
int | None
|
Lloyd iterations when training the cells. |
None
|
kmeans_balanced
|
bool
|
Reseed starved centroids each iteration. |
False
|
n_pq_centroids
|
int | None
|
Centroids per subspace. |
None
|
seed
|
int
|
Fixes both the cell and the codebook initialisation. |
42
|
verbose
|
bool
|
Progress to the process stdout, not |
False
|
SoarOpqIndex
¶
SoarOpqIndex(
n_neighbors: int = 15,
m: int = 8,
metric: str = "euclidean",
nlist: int | None = None,
nprobe: int | None = None,
rule: str | None = None,
rule_param: float | None = None,
kmeans_iters: int | None = None,
kmeans_balanced: bool = False,
n_pq_centroids: int | None = None,
opq_iters: int | None = None,
seed: int = 42,
verbose: bool = False,
)
Bases: _BaseQuantisedIndex
SoarPqIndex with the learned rotation in front of the sub-codebooks.
The most compressed index in the package, and the slowest to build. Reach for it when memory is the hard constraint and the build is a one-off.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
n_neighbors
|
int
|
Neighbours per query, and the default |
15
|
m
|
int
|
Subspaces. |
8
|
metric
|
str
|
|
'euclidean'
|
nlist
|
int | None
|
Voronoi cells to cut the space into. |
None
|
nprobe
|
int | None
|
Cells visited per query, the recall knob. |
None
|
rule
|
str | None
|
Spilling rule: |
None
|
rule_param
|
float | None
|
|
None
|
kmeans_iters
|
int | None
|
Lloyd iterations when training the cells. |
None
|
kmeans_balanced
|
bool
|
Reseed starved centroids each iteration. |
False
|
n_pq_centroids
|
int | None
|
Centroids per subspace. |
None
|
opq_iters
|
int | None
|
Alternating rotation/codebook iterations. |
None
|
seed
|
int
|
Fixes both the cell and the codebook initialisation. |
42
|
verbose
|
bool
|
Progress to the process stdout, not |
False
|