Skip to content

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 ExhaustiveIndex first.
  • 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

ExhaustiveBf16Index(
    n_neighbors: int = 15,
    metric: str = "euclidean",
    verbose: bool = False,
)

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 k for kneighbors.

15
metric str

"euclidean"/"l2", "sqeuclidean" or "cosine".

'euclidean'
verbose bool

Progress to the process stdout, not sys.stdout. In Jupyter that lands in the terminal running the kernel.

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 k for kneighbors.

15
metric str

"euclidean"/"l2", "sqeuclidean" or "cosine".

'euclidean'
nlist int | None

Voronoi cells to cut the space into. None defaults to sqrt(n).

None
nprobe int | None

Cells visited per query, the recall knob. None defaults to sqrt(nlist). Search-time: override it per call as index.kneighbors(nprobe=32).

None
kmeans_iters int | None

Lloyd iterations when training the cells. None defaults to 30.

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 sys.stdout.

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 k for kneighbors.

15
metric str

"euclidean"/"l2", "sqeuclidean" or "cosine".

'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 uses the crate default. Raise it when a few outliers are stretching the range and wasting code levels.

None
quant_sample_rows int | None

Rows sampled to calibrate the range. None auto-picks, capped at the dataset size.

None
seed int

Fixes the calibration row sample.

42
verbose bool

Progress to the process stdout, not sys.stdout.

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 k for kneighbors.

15
metric str

"euclidean"/"l2", "sqeuclidean" or "cosine".

'euclidean'
nlist int | None

Voronoi cells to cut the space into. None defaults to sqrt(n).

None
nprobe int | None

Cells visited per query, the recall knob. None defaults to sqrt(nlist). Search-time.

None
kmeans_iters int | None

Lloyd iterations when training the cells. None defaults to 30.

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 uses the crate default.

None
quant_sample_rows int | None

Rows sampled to calibrate the range. None auto-picks.

None
seed int

Fixes k-means initialisation and the calibration sample.

42
verbose bool

Progress to the process stdout, not sys.stdout.

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 k for kneighbors.

15
metric str

"euclidean"/"l2", "sqeuclidean" or "cosine".

'euclidean'
m int

Edges per node on the upper layers; the base layer gets 2 * m. Raising it gives a better graph, a larger index and a slower build.

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 k internally when it is smaller. Search-time: override it per call as index.kneighbors(ef_search=200).

100
quant_drop_ratio float | None

Fraction trimmed from each tail of every dimension before the range is fixed. None uses the crate default.

None
quant_sample_rows int | None

Rows sampled to calibrate the range. None auto-picks.

None
seed int

Fixes the level assignment and the calibration sample.

42
verbose bool

Progress to the process stdout, not sys.stdout.

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 k for kneighbors.

15
m int

Subspaces. dim must divide by it, and it sets the code length in bytes.

8
metric str

"euclidean"/"l2", "sqeuclidean" or "cosine".

'euclidean'
max_iters int | None

Lloyd iterations when training the sub-codebooks. None uses the crate default.

None
n_pq_centroids int | None

Centroids per subspace. None uses 256, which is what makes a code fit in a byte. Cannot exceed the sample count, so a small dataset needs this lowered.

None
seed int

Fixes the codebook initialisation.

42
verbose bool

Progress to the process stdout, not sys.stdout.

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 k for kneighbors.

15
m int

Subspaces. dim must divide by it.

8
metric str

"euclidean"/"l2", "sqeuclidean" or "cosine".

'euclidean'
max_iters int | None

Alternating rotation/codebook iterations. None uses the crate default. This is the build-cost knob.

None
n_pq_centroids int | None

Centroids per subspace. None uses 256.

None
seed int

Fixes the codebook initialisation.

42
verbose bool

Progress to the process stdout, not sys.stdout.

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 k for kneighbors.

15
m int

Subspaces. dim must divide by it.

8
metric str

"euclidean"/"l2", "sqeuclidean" or "cosine".

'euclidean'
nlist int | None

Voronoi cells to cut the space into. None defaults to sqrt(n).

None
nprobe int | None

Cells visited per query, the recall knob. None defaults to sqrt(nlist). Search-time.

None
kmeans_iters int | None

Lloyd iterations when training the cells. None defaults to 30.

None
kmeans_balanced bool

Reseed starved centroids each iteration.

False
n_pq_centroids int | None

Centroids per subspace. None uses 256.

None
seed int

Fixes both the cell and the codebook initialisation.

42
verbose bool

Progress to the process stdout, not sys.stdout.

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 k for kneighbors.

15
m int

Subspaces. dim must divide by it.

8
metric str

"euclidean"/"l2", "sqeuclidean" or "cosine".

'euclidean'
nlist int | None

Voronoi cells to cut the space into. None defaults to sqrt(n).

None
nprobe int | None

Cells visited per query, the recall knob. None defaults to sqrt(nlist). Search-time.

None
kmeans_iters int | None

Lloyd iterations when training the cells. None defaults to 30.

None
kmeans_balanced bool

Reseed starved centroids each iteration.

False
n_pq_centroids int | None

Centroids per subspace. None uses 256.

None
opq_iters int | None

Alternating rotation/codebook iterations. None uses the crate default. This is the build-cost knob.

None
seed int

Fixes both the cell and the codebook initialisation.

42
verbose bool

Progress to the process stdout, not sys.stdout.

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 k for kneighbors.

15
m int

Subspaces. dim must divide by it.

8
metric str

"euclidean"/"l2", "sqeuclidean" or "cosine".

'euclidean'
nlist int | None

Voronoi cells to cut the space into. None defaults to sqrt(n).

None
nprobe int | None

Cells visited per query, the recall knob. None defaults to sqrt(nlist). Search-time.

None
rule str | None

Spilling rule: "nearest", "shifted" or "orthogonal". None lets the crate pick per metric, orthogonal for cosine and shifted otherwise.

None
rule_param float | None

mu for "shifted", lambda for "orthogonal". None uses the crate's value. Ignored by "nearest".

None
kmeans_iters int | None

Lloyd iterations when training the cells. None defaults to 30.

None
kmeans_balanced bool

Reseed starved centroids each iteration.

False
n_pq_centroids int | None

Centroids per subspace. None uses 256.

None
seed int

Fixes both the cell and the codebook initialisation.

42
verbose bool

Progress to the process stdout, not sys.stdout.

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 k for kneighbors.

15
m int

Subspaces. dim must divide by it.

8
metric str

"euclidean"/"l2", "sqeuclidean" or "cosine".

'euclidean'
nlist int | None

Voronoi cells to cut the space into. None defaults to sqrt(n).

None
nprobe int | None

Cells visited per query, the recall knob. None defaults to sqrt(nlist). Search-time.

None
rule str | None

Spilling rule: "nearest", "shifted" or "orthogonal". None lets the crate pick per metric.

None
rule_param float | None

mu for "shifted", lambda for "orthogonal". None uses the crate's value.

None
kmeans_iters int | None

Lloyd iterations when training the cells. None defaults to 30.

None
kmeans_balanced bool

Reseed starved centroids each iteration.

False
n_pq_centroids int | None

Centroids per subspace. None uses 256.

None
opq_iters int | None

Alternating rotation/codebook iterations. None uses the crate default. This is the build-cost knob.

None
seed int

Fixes both the cell and the codebook initialisation.

42
verbose bool

Progress to the process stdout, not sys.stdout.

False