Skip to main content

PyPI Docs CI

ann-search

Python bindings for ann-search-rs: approximate nearest-neighbour search built for single-cell and computational biology workloads. The Rust crate does the work. This is a thin scikit-learn shaped layer over it.

Documentation: https://gregorlueg.github.io/ann-search-rs/

This package versions separately from the Rust crate. Changes here are in python/CHANGELOG.md; the crate's are in CHANGELOG.md.

Install

uv pip install ann-search           # numpy only
uv pip install "ann-search[sparse]" # adds scipy, for kneighbors_graph

Use

Every index is a scikit-learn style estimator. Parameters go in the constructor, data goes into fit, results come out of kneighbors.

import numpy as np
import ann_search as ann

X = np.random.default_rng(0).standard_normal((50_000, 50)).astype(np.float32)

index = ann.HnswIndex(n_neighbors=15, metric="cosine").fit(X)

distances, indices = index.kneighbors()  # self-kNN graph, fast path
distances, indices = index.kneighbors(X[:1000])  # cross-set query
graph = index.kneighbors_graph()  # scipy CSR

kneighbors returns distances first, matching scikit-learn and FAISS.

The estimators implement get_params, set_params, fit and transform, so they drop into scikit-learn pipelines and anywhere a KNeighborsTransformer is expected, scanpy included. scikit-learn isn't an install requirement for any of that.

Indices

Class Notes
ExhaustiveIndex Exact. Blocked GEMM on large batches, not a naive scan. Ground truth.
KmknnIndex Exact, k-means pruned. No Manhattan.
AnnoyIndex Random projection forest. No Manhattan.
KdTreeIndex Randomised kd spill-tree forest. Axis-aligned splits.
BallTreeIndex Metric tree of nested hyperspheres. No Manhattan.
HnswIndex Hierarchical small-world graph. The usual first choice.
IvfIndex Inverted file over k-means cells. Smallest of the approximate indices. No Manhattan.
SoarIndex IVF with spilling. Better recall per nprobe, twice the posting-list entries. No Manhattan.
LshIndex Multi-probe LSH. Cheapest build, weakest recall. No Manhattan.
NNDescentIndex Hands back the graph it built, without a search pass.
RnnDescentIndex Builds and prunes in one pass, no intermediate kNN graph.
VamanaIndex DiskANN-style flat graph.
NsgIndex Navigating spreading-out graph.

BallTreeIndex defaults its search_budget to 5% of the indexed points, which is the crate's own heuristic. It reaches 0.9865 to 0.9996 recall at 32 dimensions and drops to 0.9135 at 128, where 10% is the better starting point and takes it back to 0.9924. Past 10% recall plateaus and query time doesn't fall, so there's no reason to go higher.

Quantised

Thirteen more estimators over compressed vectors, for when memory is the binding constraint. Distances from these are the codec's estimate rather than the distance (bar QgIndex), and none of them support Manhattan.

Measure recall against ExhaustiveIndex before committing to one. The codec cost on the benchmark runs is larger than the memory saving makes it look, and it is worst on cosine. See Quantised.

Class Notes
ExhaustiveBf16Index Brute force at bf16. Half the memory, but the query runs 2 to 4x slower and recall drops to ~0.98 Euclidean, ~0.89 cosine.
IvfBf16Index IvfIndex with bf16 posting lists.
ExhaustiveSq8Index Brute force on 8-bit codes. 3.5x smaller at 32D, 3.9x at 128D. Slower than the float scan at 32D.
IvfSq8Index IvfIndex on 8-bit codes.
HnswSq8uIndex HNSW built and searched on 8-bit codes. The usual starting point. Faster to build and query; costs 0.07 recall on Euclidean, 0.26+ on cosine.
ExhaustivePqIndex Product quantisation, m bytes per vector.
IvfPqIndex IVF plus PQ, codes learned on the cell residual.
ExhaustiveOpqIndex PQ with a learned rotation in front.
IvfOpqIndex IVF-PQ with the rotation.
SoarPqIndex IVF-PQ with SOAR spilling.
SoarOpqIndex Spilling plus the rotation, so it pays for both. The largest of the PQ family, not the smallest.
HnswRaBitQIndex HNSW linked on exact distances, then the vectors are dropped. ex_bits trades size for recall; 11 to 28 MB against 49 MB of raw data at 50k x 256.
QgIndex Vamana graph with each vertex's neighbours stored as one-bit RaBitQ codes. Exact distances, but 2.1x the raw data or more: a speed structure, not a small one.

The remaining binary indices in the Rust crate (binary, RaBitQ and TurboQuant, exhaustive and IVF) aren't bound yet. They follow the same pattern when they land.

GPU

Three more estimators. They ship in the ordinary wheel: there's no separate package and no extra to ask for.

Class Notes
ExhaustiveGpuIndex Brute force on the device. Exact, and cheap ground truth.
IvfGpuIndex k-means and vectors both resident. Bounded by device memory.
CagraGpuIndex NN-Descent on device, pruned to a CAGRA graph, beam-searched.
import ann_search as ann

if ann.gpu_available():
    index = ann.CagraGpuIndex(n_neighbors=15).fit(X)

gpu_available() answers the only question worth asking: is there an adapter on this machine. The backend is wgpu, so that means Metal on macOS and Vulkan or DX12 elsewhere. There's no CUDA runtime to install and nothing extra to pip install. On a box with no GPU it returns False and the CPU estimators are unaffected.

On the M1 Max the benchmarks ran on, the GPU exhaustive path comes out around 1.7x faster than the CPU one, and CAGRA's build cost means it only wins overall past a few hundred thousand points. See GPU.

Any n_features works. The kernels want rows padded to a multiple of four, and the index does that for you on fit and on kneighbors.

Three differences from the CPU estimators, all forced by the backend:

  • float32 only. WGSL has no float64, so fit narrows rather than failing inside a kernel. Not the only narrowing in the package: a float64 query against a float32 index is narrowed too, on every index.
  • No persistence. These hold device buffers and sit outside the crate's serialise feature, so save, load and pickle raise NotImplementedError. Rebuild instead.
  • No Manhattan, on any of the three.

A fitted CagraGpuIndex is also the one index here that is not safe to query from two threads at once: the beam search memoises its graph upload behind a mutable borrow, and the GIL is released while the kernel runs, so concurrent calls raise rather than queueing. Give each thread its own handle.

A CPU-only build

GPU support is compiled in by default, which costs roughly 3 MB of wheel. That seemed the better trade than a second distribution, since wgpu has no driver runtime to ship and the alternative is a version pin between two packages that can drift.

If the megabytes matter, build without it:

maturin develop --release --no-default-features

gpu_available() then returns False on any machine, and import ann_search.gpu raises with an explanation. Everything on the CPU side is untouched.

Synthetic data

Uniform Gaussian noise is a bad ANN benchmark. Past a few dozen dimensions every point sits at roughly the same distance from every other, so recall stops telling you anything. Four generators with structure real single-cell data has:

from ann_search import datasets

X, labels = datasets.make_clustered(50_000, dim=32, n_clusters=25, seed=42)
Q = datasets.subsample_queries(X, 5_000, seed=42)
Generator What it stresses
make_clustered Separated blobs with inter-cluster bridges. The baseline.
make_correlated Local anisotropy plus a shared off-axis subspace. Where OPQ and PQ pull apart.
make_low_rank A low-dimensional manifold in a high-dimensional space, with trajectories.
make_cell_embeddings Geneformer/scGPT flavoured: heavy tails, rogue dimensions, anisotropy cone. Gets painful for quantised indices.

Each returns (X, labels), so ground-truth cluster labels come free. Output is float32.

These are the same generators, same seeds, behind the benchmark tables in the Rust crate's docs/. A Python benchmark and a cargo run --example gridsearch_hnsw run see identical points, and the Python test suite pins checksums taken from the Rust side to keep it that way.

subsample_queries matters more than it looks. Querying an index with rows it was built from flatters it: every query has an exact hit at distance zero.

Metrics

"euclidean" / "l2", "sqeuclidean", "cosine", "manhattan" / "l1".

The Rust core computes squared Euclidean distances. "euclidean" and "l2" take the square root on the way out so the numbers match scikit-learn and scipy; "sqeuclidean" hands back the raw squared values and skips that.

An unknown metric raises ValueError. The Rust core would quietly fall back to squared Euclidean and warn to a stdout you can't see, which is a much worse failure across FFI than a loud one.

Padding

Approximate indices can return fewer than k neighbours for a query. Those slots come back as index -1 and distance inf. Mask on indices >= 0 before you slice with them. kneighbors_graph already drops them.

Threads

ann.set_num_threads(8)  # 0 restores the default pool
ann.num_threads()

The default pool honours RAYON_NUM_THREADS. Rayon worker threads don't survive fork, so use the spawn start method for multiprocessing.

Persistence

index.save("my_index")  # a directory, not a file
index = ann.HnswIndex.load("my_index")

import pickle

blob = pickle.dumps(index)  # works with joblib and multiprocessing

Caveats

  • verbose=True writes to the process stdout, not sys.stdout. In Jupyter that lands in the terminal running the kernel, not in the cell.
  • Ctrl-C can't interrupt an index build. Python signal handlers only run while the GIL is held, and the build releases it.
  • return_distance=False saves the copy into numpy but not the distance computation, which happens either way.
  • Indices are immutable. There's no incremental add, so rebuild instead.

Development

uv venv
uv pip install "maturin>=1.15,<2" numpy scipy pytest scikit-learn beartype
maturin develop --release   # --release matters, the tests build real indices
pytest tests -q

Metadata

Release files for ann-search 0.4.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Built distributions (wheels)

Table of built distributions (wheels) for ann-search 0.4.0
File Interpreter ABI Platform
ann_search-0.4.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl CPython 3.10 abi3 Linux glibc 2.17+ x86-64 Details
ann_search-0.4.0-cp310-abi3-macosx_11_0_arm64.whl CPython 3.10 abi3 macOS 11.0+ ARM64 Details
ann_search-0.4.0-cp310-abi3-macosx_10_12_x86_64.whl CPython 3.10 abi3 macOS 10.12+ x86-64 Details

Total release size: 19.3 MB

Release files / ann_search-0.4.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl

Download URL ann_search-0.4.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Size 6.9 MB
Tags CPython 3.10 Linux glibc 2.17+ x86-64 abi3
SHA-256 checksum
How to use checksums
fa495a4da0830ef9aa7920ad0dfdfd2a0231f4664110e3d17df8a043a7d40eb4
BLAKE2b-256 checksum
How to use checksums
118272e4d6987fde16f1b5af05baa148722c4fb9bc3c337f0f0587c51dba5b1c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 5, 2026.

Transparency log

Release files / ann_search-0.4.0-cp310-abi3-macosx_11_0_arm64.whl

Download URL ann_search-0.4.0-cp310-abi3-macosx_11_0_arm64.whl
Size 5.9 MB
Tags CPython 3.10 abi3 macOS 11.0+ ARM64
SHA-256 checksum
How to use checksums
fd226ab245fdd744f862c60c8e6c0cbe175110b58bd3003589581914c618733e
BLAKE2b-256 checksum
How to use checksums
65e9b77c0f610525d9a51cb29c4b36389b6d81427e65d4d3f7ca1d47cb6e1949
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 5, 2026.

Transparency log

Release files / ann_search-0.4.0-cp310-abi3-macosx_10_12_x86_64.whl

Download URL ann_search-0.4.0-cp310-abi3-macosx_10_12_x86_64.whl
Size 6.6 MB
Tags CPython 3.10 abi3 macOS 10.12+ x86-64
SHA-256 checksum
How to use checksums
d4df8b09771a4c56dc36ae0aa733c33ff0f2bbc03ce1471692c4b16c6d4bf219
BLAKE2b-256 checksum
How to use checksums
e4c28e2a0e7a26f5b0088ce1fdf28495d0467851eb20030ee6ecccd9a3d9e8c0
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 5, 2026.

Transparency log

Release history Release notifications | RSS feed

0.5.1

3 release files

0.5.0

3 release files

0.4.1

3 release files

This release

0.4.0 This release

3 release files

0.3.2

3 release files

0.3.1

3 release files

0.3.0

3 release files

0.2.4

3 release files

0.2.3

3 release files

0.2.2

3 release files

0.2.1

3 release files

0.2.0

3 release files

0.1.0

3 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page