Skip to main content

scann-core

ScaNN's nearest-neighbour search core as a standalone CMake project with C++, Python and Rust APIs. It doesn't need TensorFlow to build or run; using it from TensorFlow or PyTorch is optional (see docs/tensorflow.md and docs/integrations.md).

  • C++: libscann_core (static and/or shared), with ScaNN's pybind-free facade research_scann::ScannInterface and a C++ port of Python's ScannBuilder (scann_core::ConfigBuilder).
  • Python: the same scann_pybind module and scann.scann_ops_pybind API as the upstream wheel, from upstream's Python sources (with one bug fix, see NOTICE). import scann doesn't import TensorFlow; scann.tf and scann.torch are optional wrappers for TensorFlow and PyTorch code.
  • Rust: the scann-core crate, a safe API over the C++ library via cxx.

It runs on x86-64 and aarch64 Linux. On x86-64 it's about 50% faster than the upstream wheel at the same recall: the wheel never detects the CPU, so its AVX2/AVX-512 kernels never run. On aarch64 it includes Arm's Neon/SVE kernels. See docs/benchmarks.md.

Extracted from google-research commit 758b894e (scann/ subdirectory); see NOTICE for provenance and the list of upstream files that were changed.

scann-core is a derived work of ScaNN. It is not an official Google product and is not affiliated with or endorsed by Google.

Contents

Install

Python pip install scann-core, or pip install . from a checkout
Rust cargo add scann-core, or a git/path dependency on this repository
C++ CMake FetchContent/add_subdirectory, linking scann::core; see examples/fetchcontent

All three build the C++ library from source, which needs:

  • Linux on x86-64 or aarch64.
    • Both are built and tested in CI (GitHub Actions, Ubuntu 26.04), aarch64 on native Arm runners. aarch64 was also tested on AWS Graviton4 (Neoverse V2).
    • The C++ tests also run under QEMU on six emulated Arm CPUs, from Neon-only Cortex-A57 to SVE2 (see Cross-compiling for aarch64).
    • The SIMD kernels (AVX2/AVX-512 on x86-64, Neon/SVE on aarch64) are chosen at run time from the CPU's features.
    • The macOS code paths exist, inherited from upstream, but are untested.
  • clang ≥ 19 or GCC ≥ 13. Tested with clang 19–24 and GCC 13–16. CI runs the oldest and newest that Ubuntu 26.04 packages: clang 19 and 22, and GCC 13 and 15. clang is upstream's compiler, and the one the equivalence checks use. GCC builds give the same recall (checked on GloVe-100), with last-bit differences in distances. They are slower: the partitioned pipeline by about 5%, and batched brute-force search at about half of clang's throughput. Use clang for speed. When no compiler is chosen, clang is picked if it's on PATH. The AMX kernels (Sapphire Rapids and later) need clang ≥ 20.
  • CMake ≥ 3.27, and network access to download the C++ dependencies (or local copies; see Dependencies).
  • For Python: Python ≥ 3.10 with numpy and protobuf ≥ 7.36.2 (pip installs them). Free-threaded Python (3.14t, 3.15t) is supported: the module runs without the GIL (see Threads). For Rust: Rust ≥ 1.88.

The version is in VERSION; see CHANGELOG.md.

Layout

scann-core/
├── CMakeLists.txt        options and library targets
├── cmake/                Flags, Dependencies (pinned FetchContent), Proto,
│                         SourceFlags (per-file copts), BundleStatic
├── src/scann/            upstream C++ sources (see NOTICE for the fixes)
├── core/scann_core/      scann-core additions: ConfigBuilder
├── python/               pybind11 module + upstream Python package
├── tf_op/                optional TensorFlow op + scann_tf_ops package (source-only)
├── rust/                 the Rust crate (cxx bridge, safe API, tests)
├── examples/             Python, C++ and Rust examples, FetchContent consumer template
├── third_party/          vendored: cnpy, googletest's gtest_prod.h
├── tests/                C++/Python tests, upstream-equivalence harness
├── benchmarks/           ann-benchmarks runner, GloVe by default (docs/benchmarks.md)
├── scripts/ci.sh         what CI runs (also runnable locally)
├── scripts/python-versions.sh  the Python tests on CPython 3.10-3.15t
├── scripts/cross-aarch64.sh  aarch64 cross-build + tests under QEMU
├── Cargo.toml            the Rust crate (sources in rust/)
├── pyproject.toml        the Python package (scikit-build-core)
└── docs/                 tutorial, benchmarks, API reference, algorithms, AVQ explainer,
                          TensorFlow

Building

Requirements as under Install.

cmake -S . -B build -G Ninja
cmake --build build
Option Default Effect
SCANN_BUILD_STATIC ON libscann_core.a, CMake target scann::core_static (linked whole-archive for you)
SCANN_BUILD_SHARED ON* libscann_core.so, target scann::core_shared
SCANN_BUILD_PYTHON ON* the Python package in build/python/ (for python3 on PATH, or -DPython_EXECUTABLE=)
SCANN_BUILD_RUST_BINDINGS ON* the Rust crate, via cargo (needs SCANN_BUILD_STATIC)
SCANN_BUILD_TESTS ON* tests, run with ctest
SCANN_BUILD_EXAMPLES ON* C++ examples
SCANN_BUILD_TF_OP OFF the TensorFlow op and its scann_tf_ops package in build/python/, against the TensorFlow in Python_EXECUTABLE (Linux; source-only, see docs/tensorflow.md)
SCANN_ARCH_FLAGS -mavx;-mfma (x86-64), -march=armv8-a+simd (arm64) ISA flags for scann-core and all dependencies
SCANN_SANITIZE empty e.g. address,undefined or thread; instruments dependencies too
SCANN_USE_SYSTEM_DEPS OFF try find_package first (versions must match the pins exactly)
SCANN_ENABLE_LTO OFF IPO for scann-core's own objects
SCANN_HWY_DISABLED_TARGETS empty HWY_DISABLED_TARGETS, applied globally
SCANN_ALLOW_UNSUPPORTED_COMPILER OFF configure with a compiler other than clang or GCC anyway (expect errors)

* ON when scann-core is the top-level project, OFF when it's pulled into another one with FetchContent or add_subdirectory.

The static and shared libraries are linked from the same object files, so building both costs no extra compilation. scann::core is the static library if it's built, otherwise the shared one.

Static linking needs whole-archive. Distance measures register themselves from static initializers (Bazel's alwayslink), so without it a linker drops them and configs fail at runtime with an unknown distance measure. scann::core_static does this for CMake consumers. Outside CMake, link libscann_core.a whole-archive plus libscann_core_deps.a (every transitive static dependency merged into one archive):

c++ app.o -Wl,--whole-archive build/libscann_core.a -Wl,--no-whole-archive \
    build/libscann_core_deps.a -lpthread -lrt -lm

Dependencies

All fetched with FetchContent, pinned by URL and SHA-256 in cmake/Dependencies.cmake (latest releases as of 2026-09-22):

version
abseil-cpp 20260817.0
protobuf 36.2 (Python runtime ≥ 7.36.2 for the generated _pb2 modules)
highway 1.4.0
Eigen 5.0.1
zlib 1.3.2 (static, only for cnpy)
cnpy commit 57184ee0, vendored in third_party/cnpy (not downloaded)
googletest gtest_prod.h only, v1.18.0, vendored in third_party/googletest
pybind11 3.1.0 (Python only)
cxx 1.x (Rust only, from crates.io)

Offline / reproducible builds. Point FetchContent at local sources and forbid network access:

cmake -S . -B build -DFETCHCONTENT_FULLY_DISCONNECTED=ON \
  -DFETCHCONTENT_SOURCE_DIR_ABSL=/src/abseil-cpp-20260817.0 \
  -DFETCHCONTENT_SOURCE_DIR_PROTOBUF=/src/protobuf-36.2 \
  -DFETCHCONTENT_SOURCE_DIR_HIGHWAY=/src/highway-1.4.0 \
  -DFETCHCONTENT_SOURCE_DIR_EIGEN=/src/eigen-5.0.1 \
  -DFETCHCONTENT_SOURCE_DIR_ZLIB=/src/zlib-1.3.2 \
  -DFETCHCONTENT_SOURCE_DIR_PYBIND11=/src/pybind11-3.1.0

(an existing build's _deps/*-src directories work), or use -DSCANN_USE_SYSTEM_DEPS=ON to take installed packages when their versions match.

Compile flags

Carried over from the Bazel build:

  • Global (reach every dependency too, like --copt): the ISA flags (SCANN_ARCH_FLAGS), -fsized-deallocation, -w, -std=c++17 (not gnu++17, as in the Bazel build), and -O2 as the release baseline (not CMake's -O3).
  • Per file (cmake/SourceFlags.cmake), as in the Bazel copts: -O3 on the LUT16 kernels and on many-to-many distances; -mtune=generic on the many-to-many fixed8/sfp8/orthogonality files (upstream's workaround for an AMX codegen problem); -fno-tree-vectorize on limited_inner_product; -fomit-frame-pointer on asymmetric_hashing_impl_omit_frame_pointer.
  • The LUT16 template sharding ({BATCH_SIZE} = 1..9) of Bazel's batch_size_sharder.

On aarch64 the default -march=armv8-a+simd covers every Armv8 CPU. The SVE kernels are compiled with a target("+sve") attribute and used only when the CPU has SVE, so -march=native isn't needed for them.

Not carried over: HWY_DISABLED_TARGETS=(HWY_AVX3_SPR|HWY_AVX10_2). It worked around highway 1.3.0 failing to compile vqsort with clang 23, and isn't needed with highway 1.4.0. Thin LTO isn't on by default (SCANN_ENABLE_LTO).

Cross-compiling for aarch64

cmake/toolchains/aarch64-linux-gnu.cmake cross-compiles with clang and lld, against the Debian/Ubuntu aarch64-linux-gnu sysroot. It uses qemu-aarch64 to run the tools the build runs (protoc) and the tests:

cmake -S . -B build-aarch64 -G Ninja \
  -DCMAKE_TOOLCHAIN_FILE=cmake/toolchains/aarch64-linux-gnu.cmake \
  -DSCANN_BUILD_PYTHON=OFF -DSCANN_BUILD_RUST_BINDINGS=OFF
cmake --build build-aarch64
QEMU_CPU=neoverse-n2 ctest --test-dir build-aarch64

SCANN_CLANG_SUFFIX=-19 picks clang-19. The build needs clang, lld, g++-aarch64-linux-gnu and qemu-user. scripts/cross-aarch64.sh does all of it in a throwaway container. It runs the tests on six emulated CPUs with different feature sets, covering both the Neon and the SVE kernels:

docker run --rm --platform linux/amd64 -v $PWD:/src -w /src ubuntu:26.04 scripts/cross-aarch64.sh

Emulated timings mean nothing. For speed, see the Graviton4 numbers in docs/benchmarks.md. Python and Rust build natively on aarch64 the same way as on x86-64.

Using it

C++

#include "scann/scann_ops/cc/scann.h"
#include "scann_core/config_builder.h"

scann_core::TreeOptions tree;
tree.num_leaves = 200;
tree.num_leaves_to_search = 20;
scann_core::AhOptions ah;
ah.anisotropic_quantization_threshold = 0.2;
auto config = scann_core::ConfigBuilder(10, scann_core::DistanceMeasure::kDotProduct, dim)
                  .Tree(tree).ScoreAh(ah).Reorder({100})
                  .BuildText(n);

research_scann::ScannInterface index;
absl::Status s = index.Initialize(dataset /* n*dim floats */, n, *config, 0);
research_scann::NNResultsVector res;
s = index.Search(query_ptr, &res, /*final_nn=*/-1, /*pre_reorder_nn=*/-1, /*leaves=*/-1);

Full programs: examples/cpp/quickstart.cc (cmake --build build --target scann_core_example_quickstart) and examples/cpp/updating.cc (adding, updating and removing points). ConfigBuilder produces the same configs as the Python builder, except that it returns an error where Python silently drops or ignores an option. See the header for the full list.

Python

pip install . builds and installs the scann package (same import name and API as upstream's wheel, so don't install both in one environment). In a CMake build, build/python/ is the same package, importable directly:

PYTHONPATH=build/python python -c "import scann; print(scann.__version__)"

Full programs: examples/python/.

scann.scann_ops (the TensorFlow op) is not included. scann.tf wraps the searcher for TensorFlow code instead (eager mode, tf.function, tf.data; not SavedModels): pip install 'scann-core[tf]', and see docs/tensorflow.md, which also covers serving alongside a TensorFlow model. An op that can be saved in SavedModels, scann_tf_ops, can be built from source (-DSCANN_BUILD_TF_OP=ON; unsupported, see the same page).

scann.torch makes the searcher a torch.nn.Module whose searches take and return tensors (CPU or GPU) and compile with torch.compile(fullgraph=True), for models that search: pip install 'scann-core[torch]', and see docs/integrations.md. Not yet exportable with torch.export.

Threads

A searcher can be shared between Python threads:

  • Searches run in parallel with each other.
  • upsert, delete, rebalance, reserve, set_num_threads and serialize each run on their own. A search sees the index before or after an update, never during one.

With the GIL, searches release it while they run, but the Python-side work around each call is serialized. On a 64-thread machine, concurrent search() calls plateau around 100k QPS.

On free-threaded Python (3.14t and later), the module declares that it doesn't need the GIL, so importing it doesn't turn the GIL back on. The same threads then reach about 330k QPS, level with batched search. See tutorial part 5.

Upstream relied on the GIL for thread safety, but its searches release it. A search could therefore run during an upsert or delete, and map its results to the wrong docids. scann-core adds a reader/writer lock in C++ and one around the docid bookkeeping. Plain searches cost the same; a search on a searcher with docids costs about 1 µs more.

Rust

use scann_core::{AhOptions, ConfigBuilder, DistanceMeasure, ReorderOptions,
                 SearchOptions, TreeOptions};

let index = ConfigBuilder::new(10, DistanceMeasure::DotProduct, dim)
    .tree(TreeOptions::new(200, 20))
    .score_ah(AhOptions::new(2).anisotropic_quantization_threshold(0.2))
    .reorder(ReorderOptions::new(100))
    .build_index(&dataset)?;
let nn = index.search(query, SearchOptions::default())?;   // nn.indices, nn.distances

ScannIndex covers single, batched and parallel batched search, add/upsert/delete/reserve/rebalance, serialize/load (the on-disk format is shared with Python), set_num_threads, config and health stats. It is Send + Sync: search takes &self and can run from many threads, and mutation takes &mut self. Shapes are validated before anything reaches C++, and every C++ error becomes a ScannError.

Full programs: examples/rust/quickstart.rs and examples/rust/updating.rs (upsert, delete, rebalance, re-saving).

The crate's manifest is the repository root's Cargo.toml. Built on its own (cargo build, or as a dependency), its build script builds the C++ library with CMake; SCANN_CORE_CMAKE_ARGS passes extra -D options, such as local dependency sources for offline builds. Inside a CMake build, the scann_core_rust target builds the crate against the CMake-built libraries instead; for plain cargo or rust-analyzer to do the same, export SCANN_CORE_BUILD_ENV=<build>/rust/scann_core_rust_build.env.

Examples

Short programs on synthetic data, meant to be copied from. Each runs in about a second and checks its own results, so they also run as tests (example_* below).

python/quickstart.py build tree + AH + reorder, search one query and batches, recall against exact search, save with docids and load from a moved directory
python/updating.py upsert new and existing docids, delete, health stats, rebalance(), saving over an existing index
python/serving_threads.py search_batched_parallel vs. Python threads calling search(), with or without the GIL
python/tensorflow_wrapper.py scann.tf eagerly and in tf.function, docids with tf.gather (needs TensorFlow)
python/tensorflow_serving.py a Keras query tower exported as a SavedModel, the index saved beside it, and a service that loads both (needs TensorFlow)
python/torch_retrieval.py scann.torch: index a toy encoder's embeddings, then a model that encodes and searches, compiled with torch.compile, on a GPU if present (needs PyTorch)
cpp/quickstart.cc, rust/quickstart.rs build with the config builder, search, add a point, save and reload
cpp/updating.cc, rust/updating.rs add, update and delete points by index, retrain, save over an existing index and reload
fetchcontent/ a CMake project that pulls in scann-core with FetchContent (built by scripts/ci.sh, not a ctest)

Run a Python one with PYTHONPATH=build/python python examples/python/quickstart.py, the C++ ones from build/examples/, the Rust ones with cargo run --release --example updating (see Rust).

Testing

ctest --test-dir build --output-on-failure

runs everything that needs nothing beyond the build:

test what
api_exercise, api_exercise_threaded the C++ API end to end on synthetic data, for 12 configs (brute force, AH, autopilot, tree + AH + reorder for both distances, SOAR with bfloat16 reordering): search modes agree, serialize/reload, mutation, retraining, bad input (including NaN/infinity)
api_exercise_avx2 the same, with the AVX2 kernels forced on an AVX-512 machine (SCANN_TEST_FORCE_AVX2=1), so both kernel sets get tested (and sanitized)
mutation_regressions a failed rebalance() leaves a working index; tree + bfloat16 add/update/delete; a failed update in a SOAR tree (injected leaf failure) changes nothing; every stored vector keeps finding itself
artifact_loading about 60 damaged or mixed index directories, generated at run time (bad .npy headers, dtypes and shapes, out-of-range tokens, files from another index, SOAR mismatches, manifest errors) fail to load with an error; all-deleted and bfloat16-leaf trees round-trip; SerializeToDirectory replaces a previous index, and one that fails midway leaves a directory that fails to load. Every directory also loads from memory (LoadArtifactsFromMemory) with the same outcome
artifact_loading_avx2 the same, with the AVX2 kernels forced (SCANN_TEST_FORCE_AVX2=1); includes searching an index whose leaves are all empty
config_regressions raw configs that crashed upstream (zero block sizes, LUT16 with other than 16 clusters, binary or unsupported distances, bad quantiles) are errors; tree + PCA/TRUNCATE + AH without residuals builds, searches well and reloads
config_builder ConfigBuilder against the Python builder's output for 75 option sets (the expected configs are generated from this build's Python package first)
python_docid_bookkeeping a failed upsert/delete leaves docids in sync with the index
python_input_validation NaN/infinity in builds, upserts and batched queries, and leaves_to_search on indexes without a tree, are clean errors (not crashes); a failed batch upsert changes nothing; padded results map to None
python_config_validation the same raw configs through create_searcher; the builder's pca()/truncate() with a squared-L2 tree builds; incremental_threshold with pca(), truncate() or upper_tree() raises ValueError
python_wrapper_edge_cases upsert with a repeated docid is rejected before anything changes; zero-query batches return empty (0, k) results; more than 2^32 - 1 rows is a clear error; spherical trees store unit vectors at build, upsert and rebalance(); rebalance() without float data fails cleanly
python_projection_mutation trees with PCA/TRUNCATE projections through inserts, updates, deletes and rebalance(): points stay findable, health stats stay consistent
python_rebalance_flow an index grown from empty with batched upserts, then retrained with rebalance(config) into a SOAR tree (the big-ann-benchmarks flow); the builder's SOAR options; a clear error for more leaves than points
python_serialization serialize()/load_searcher() round trips for 10 configs, also with every point deleted; re-serializing over another index leaves no stale files or docids; a re-serialize that fails or is killed (SIGKILL) midway leaves the old index, the new one, or a directory that fails to load, never a mix
python_concurrency 3 s of concurrent searches, upserts, deletes and rebalances from Python threads; every point keeps finding itself by docid. On free-threaded Python, also checks that importing scann keeps the GIL disabled
python_tf scann.tf returns exactly the pybind searcher's results as int32/float32 tensors, eagerly and in tf.function (unknown batch size, static shapes), from tf.data maps and concurrent threads; docids, padding, serialize_to_module() raising; import scann doesn't import TensorFlow. Skipped without TensorFlow
python_langchain LangChain's ScaNN vector store on scann-core: results equal an exact search for both distance strategies, normalize_L2 and a tree + AH config; filters; save/load and re-saving into the same folder. Skipped without langchain-community
python_torch_input PyTorch tensors at every entry point give the same results as numpy arrays: float32/64/16 and bfloat16, non-contiguous views, tensors that require grad, zero rows, CUDA tensors when a GPU is present; no copy for float32 CPU tensors. Skipped without PyTorch
python_torch scann.torch returns exactly the pybind searcher's results as int64/float32 tensors on the queries' device (CPU, and CUDA when present) for brute force, AH and a tree, padding with -1/NaN; torch.compile(fullgraph=True, dynamic=True) compiles once for many batch sizes, also a whole model that encodes and searches; concurrent threads; torch.export raises; deleting the module frees the searcher; import scann doesn't import PyTorch. Skipped without PyTorch
rust cargo test: exactness against naive search, mode agreement, round trip, mutation, concurrency, errors
example_py_quickstart, example_py_updating, example_py_serving_threads the Python examples: recall above 0.9, identical results after reloading, every inserted or updated point found under its docid, a repeated upsert docid rejected, concurrent search() calls agreeing with a batched search
example_py_tensorflow_wrapper, example_py_tensorflow_serving the TensorFlow examples: scann.tf results equal the pybind searcher's; the SavedModel + index service returns docids with recall above 0.9. Skipped without TensorFlow
example_py_torch_retrieval the PyTorch example: a compiled encode-and-search model with recall above 0.9 against exact search, agreeing with the eager model. Skipped without PyTorch
example_cpp_quickstart, example_cpp_updating the C++ examples (built with SCANN_BUILD_EXAMPLES)
example_rust_quickstart, example_rust_updating the Rust examples, with cargo run --example
tf_op_symbols (with -DSCANN_BUILD_TF_OP=ON) the op library exports no symbols, imports only TensorFlow's TF_* C functions and the C/C++ runtime (nothing of TensorFlow's C++ API, abseil or protobuf), needs libtensorflow_framework.so.2, has no rpath
python_tf_ops (with -DSCANN_BUILD_TF_OP=ON) scann_tf_ops against the pybind searcher for 10 configs (bit for bit; SOAR to the last bit), eagerly and in tf.function; one build per index; stale variables and restored checkpoints rebuild; SavedModel reloaded in a fresh process (functions, serving signature); round trips; errors; concurrent calls. Skipped without TensorFlow
example_py_tensorflow_op (with -DSCANN_BUILD_TF_OP=ON) examples/python/tensorflow_op.py: a model with the index in one SavedModel, reloaded in a fresh process. Skipped without TensorFlow

The Python tests need numpy and protobuf ≥ 7.36.2 in the interpreter the module is built for; CMake says so at configure time if they're missing. python_tf and the two TensorFlow examples also need TensorFlow, and python_langchain needs langchain-community, and python_torch_input, python_torch and example_py_torch_retrieval PyTorch; ctest reports them as skipped without those. scripts/python-versions.sh runs them (and the Python examples) on every supported CPython, 3.10 to 3.15 and free-threaded 3.14t and 3.15t, with interpreters from uv, and installs tensorflow-cpu, langchain-community and PyTorch (CPU) for 3.12 so that those tests run there. It compiles the C++ library once and rebuilds only the Python module for each version.

The comparison against the upstream wheel is separate, since it needs that wheel installed: tests/equivalence/run.py --build-dir build --python <wheel venv python> --core-python <python with numpy/protobuf> writes fixtures, and configuring with -DSCANN_TEST_FIXTURES=build/equivalence/fixtures adds them to ctest. The Rust test picks them up automatically.

Equivalence with upstream

The harness uses a fixed seed and two datasets (5000×128 and 4000×768). The deterministic configs are brute force, AH + int8 reorder, autopilot, and tree + AH + reorder with k-means++ initialization, for dot product and squared L2. Indexes serialized by either build load in the other.

  • aarch64: every deterministic config gives bit-identical neighbour lists and distances to the upstream wheel, for single and batched search. Checked on Graviton4.
  • x86-64: bit-identical as well in scann-core 0.1.0. Since 0.2.0, the CPU-detection fix makes scann-core run the AVX2/AVX-512 kernels that the wheel never does. Distances now differ in the last bits (≤ 2×10⁻⁷).
    • Neighbour lists are identical in 13 of the 14 config/search-mode pairs.
    • The exception is k-means++ training on the 768-dimensional data. It trains a slightly different partitioner: 183 of 200 queries give identical neighbours, and recall is 0.962 against 0.982.
    • The wheel gives the same 0.962 on aarch64, so this is ordinary training variation.
    • The harness, which demands exact equality, therefore reports a mismatch for that config on x86-64. On GloVe the recall matches to the fourth decimal place.
    • Details in docs/benchmarks.md.

Upstream's default tree(random_init=True) is not reproducible even against itself. The initial centers go into an absl::flat_hash_set, whose iteration order is randomized per process. For those configs the harness compares recall distributions over 8 trainings per build, and checks that the means agree within 3 standard errors.

On one config (tree + AH + reorder, dot product, 5000×128), scann-core's mean recall has come out 0.2–0.9 points lower than the wheel's in every run so far. On x86-64 that stays within the bound. On aarch64 it doesn't: the wheel's random init there is nearly deterministic (1–2 distinct indexes in 8), which narrows the bound. That gives 0.991 ± 0.006 against 0.998 ± 0.002. It happens with and without Arm's kernels, and the k-means++ configs aren't affected. Why is still open.

Sanitizers and static analysis

The C++ API test (every fixture config, all search modes, serialization, mutation, retraining, bad input) runs clean under ASan + UBSan and under TSan (threaded training and parallel search). Valgrind memcheck on the portable (-mavx -mfma) build reports no leaks and no errors in ScaNN code. Its only reports are uninitialised-value warnings inside protobuf's descriptor/reflection code; these look like the known false positive with clang's combined bitfield loads, but that hasn't been confirmed. clang-tidy (bugprone-*, clang-analyzer-* and a few others) was run over all sources and its findings triaged.

Bugs found and fixed this way are listed in NOTICE. Among them: configs with parse errors were silently accepted; n_points == 0 divided by zero; RetrainAndReindex destroyed a locked mutex; parallel batched search crashed with batch_size = 0 (SIGFPE) or without a thread pool (null dereference; the default pool is GetNumCPUs() - 1 threads, so this hit every single-CPU machine); a failed Python upsert/delete left docids pointing at the wrong vectors.

Intentional differences from upstream

  • TensorFlow is optional: the wheel has no TensorFlow op (upstream's scann.scann_ops), and import scann doesn't import TensorFlow. scann.tf offers the op's Python API without the op, and can't be saved in a SavedModel. An op built on TensorFlow's C API, with upstream's scann_ops API as the scann_tf_ops package, can be built from source against one TensorFlow (-DSCANN_BUILD_TF_OP=ON; unsupported, not loadable by TensorFlow Serving). See docs/tensorflow.md for both and for serving next to a TensorFlow model.
  • CMake instead of Bazel; dependencies are upgraded to current releases.
  • The bug fixes above: some inputs upstream accepted (bad configs, inconsistent shapes, batch_size = 0) are now errors.
  • The Python module is safe to share between threads, and runs without the GIL on free-threaded Python (see Threads).
  • Rust batched search returns exactly the neighbours found per query. The Python API pads short rows with index 0 and NaN distance (docid None when the searcher has docids; upstream returned docids[0]).
  • ConfigBuilder (C++/Rust) returns errors where Python's builder silently ignores options, and keeps upper_tree(soar_lambda=0) (Python turns it into 1.5).

Documentation

  • docs/tutorial/: a seven-part, hands-on tutorial on a real million-vector dataset. It covers measuring recall and speed, the partition/score/reorder pipeline, tuning, serving, updating, and C++ and Rust. Every number in it comes from running the scripts included with it.
  • docs/benchmarks.md: speed and recall against the upstream wheel on x86-64 and aarch64 (Graviton4), and how to reproduce them with benchmarks/ann_benchmarks.py (GloVe by default, or any ann-benchmarks dataset).
  • docs/api_reference.md: the config options and search parameters, and what they mean.
  • docs/tensorflow.md: using scann-core from TensorFlow code (scann.tf), serving retrieval next to a TensorFlow model, the optional source-built TensorFlow op (scann_tf_ops, for SavedModels) and its limits, and why the wheel has no op.
  • docs/integrations.md: installing scann-core in place of the scann wheel, PyTorch tensors as inputs, scann.torch for searching from PyTorch models (and torch.compile), and libraries that use it (LangChain).
  • docs/algorithms.md: partitioning, asymmetric hashing, anisotropic quantization, reordering.
  • docs/anisotropic_quantization_explained.md: a plain-language walkthrough of the paper.

License

Apache 2.0 (see LICENSE).

  • ScaNN: Copyright The Google Research Authors.
  • scann-core's additions and modifications: Copyright 2026 Elias Benali (@ebenali) and TheCleaners.

Dependencies carry their own licenses (see NOTICE). scann-core is not an official Google product and is not affiliated with or endorsed by Google.

Metadata

Release files for scann-core 0.2.0

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

Source distribution (sdist)

Source distribution for scann-core 0.2.0
File Size Uploaded
scann_core-0.2.0.tar.gz 742.7 kB Details

Built distributions (wheels)

Table of built distributions (wheels) for scann-core 0.2.0
File
scann_core-0.2.0-cp315-cp315t-manylinux_2_34_x86_64.whl CPython 3.15 CPython 3.15 free-threading Linux glibc 2.34+ x86-64 Details
scann_core-0.2.0-cp315-cp315t-manylinux_2_34_aarch64.whl CPython 3.15 CPython 3.15 free-threading Linux glibc 2.34+ ARM64 Details
scann_core-0.2.0-cp315-cp315-manylinux_2_34_x86_64.whl CPython 3.15 CPython 3.15 Linux glibc 2.34+ x86-64 Details
scann_core-0.2.0-cp315-cp315-manylinux_2_34_aarch64.whl CPython 3.15 CPython 3.15 Linux glibc 2.34+ ARM64 Details
scann_core-0.2.0-cp314-cp314t-manylinux_2_34_x86_64.whl CPython 3.14 CPython 3.14 free-threading Linux glibc 2.34+ x86-64 Details
scann_core-0.2.0-cp314-cp314t-manylinux_2_34_aarch64.whl CPython 3.14 CPython 3.14 free-threading Linux glibc 2.34+ ARM64 Details
scann_core-0.2.0-cp314-cp314-manylinux_2_34_x86_64.whl CPython 3.14 CPython 3.14 Linux glibc 2.34+ x86-64 Details
scann_core-0.2.0-cp314-cp314-manylinux_2_34_aarch64.whl CPython 3.14 CPython 3.14 Linux glibc 2.34+ ARM64 Details
scann_core-0.2.0-cp313-cp313-manylinux_2_34_x86_64.whl CPython 3.13 CPython 3.13 Linux glibc 2.34+ x86-64 Details
scann_core-0.2.0-cp313-cp313-manylinux_2_34_aarch64.whl CPython 3.13 CPython 3.13 Linux glibc 2.34+ ARM64 Details
scann_core-0.2.0-cp312-cp312-manylinux_2_34_x86_64.whl CPython 3.12 CPython 3.12 Linux glibc 2.34+ x86-64 Details
scann_core-0.2.0-cp312-cp312-manylinux_2_34_aarch64.whl CPython 3.12 CPython 3.12 Linux glibc 2.34+ ARM64 Details
scann_core-0.2.0-cp311-cp311-manylinux_2_34_x86_64.whl CPython 3.11 CPython 3.11 Linux glibc 2.34+ x86-64 Details
scann_core-0.2.0-cp311-cp311-manylinux_2_34_aarch64.whl CPython 3.11 CPython 3.11 Linux glibc 2.34+ ARM64 Details
scann_core-0.2.0-cp310-cp310-manylinux_2_34_x86_64.whl CPython 3.10 CPython 3.10 Linux glibc 2.34+ x86-64 Details
scann_core-0.2.0-cp310-cp310-manylinux_2_34_aarch64.whl CPython 3.10 CPython 3.10 Linux glibc 2.34+ ARM64 Details

Total release size: 128.6 MB

Release files / scann_core-0.2.0.tar.gz

Download URL scann_core-0.2.0.tar.gz
Size 742.7 kB
Tags Source
SHA-256 checksum
How to use checksums
d6d94dcb67b32213c1d25d6894bdda98af2c8fc5cdb0de6c941e84f0777cedc2
BLAKE2b-256 checksum
How to use checksums
86f3603b043475124ceb89ce608df851ef04776351db999d9adc6d600d910dfe
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Release files / scann_core-0.2.0-cp315-cp315t-manylinux_2_34_x86_64.whl

Download URL scann_core-0.2.0-cp315-cp315t-manylinux_2_34_x86_64.whl
Size 8.9 MB
Tags CPython 3.15 CPython 3.15 free-threading Linux glibc 2.34+ x86-64
SHA-256 checksum
How to use checksums
83e121815d1e402830c6319bc155ea1e61b940df1a12d229e38d92ce6731953b
BLAKE2b-256 checksum
How to use checksums
32dc538d4245f323030cafe36d2566f67b036391dd58c779612fee46609e3923
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Release files / scann_core-0.2.0-cp315-cp315t-manylinux_2_34_aarch64.whl

Download URL scann_core-0.2.0-cp315-cp315t-manylinux_2_34_aarch64.whl
Size 7.1 MB
Tags CPython 3.15 CPython 3.15 free-threading Linux glibc 2.34+ ARM64
SHA-256 checksum
How to use checksums
ba5c7ad8fa0067c159da3262e8eba02ead0e9a4d8c6f5639a3a1731732344659
BLAKE2b-256 checksum
How to use checksums
efb6b117fd8eb771cf9313eb6ab866a9dc3495fbe6179422bfa6193cf8e6a291
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Release files / scann_core-0.2.0-cp315-cp315-manylinux_2_34_x86_64.whl

Download URL scann_core-0.2.0-cp315-cp315-manylinux_2_34_x86_64.whl
Size 8.9 MB
Tags CPython 3.15 Linux glibc 2.34+ x86-64
SHA-256 checksum
How to use checksums
c5748ba66cf24b0d595a7a60cc6170a488c5296501bb210cc71a094618acb04a
BLAKE2b-256 checksum
How to use checksums
e039406c0cadd1da0c748ca159bb29dce676c550bf32917c05a0853fc903cdb1
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Release files / scann_core-0.2.0-cp315-cp315-manylinux_2_34_aarch64.whl

Download URL scann_core-0.2.0-cp315-cp315-manylinux_2_34_aarch64.whl
Size 7.1 MB
Tags CPython 3.15 Linux glibc 2.34+ ARM64
SHA-256 checksum
How to use checksums
d2267851439cd5c971a166942432af7117e2f7759a4a75d6d647c835bf14264e
BLAKE2b-256 checksum
How to use checksums
560417f0decc3b315b52f4efdf1a2ea9cabbb2b1e9d8dc111ae833d28446afb2
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Release files / scann_core-0.2.0-cp314-cp314t-manylinux_2_34_x86_64.whl

Download URL scann_core-0.2.0-cp314-cp314t-manylinux_2_34_x86_64.whl
Size 8.9 MB
Tags CPython 3.14 CPython 3.14 free-threading Linux glibc 2.34+ x86-64
SHA-256 checksum
How to use checksums
e475285c602b7aac93c7e0f242c0192237f0de35276ba73efe9dd1223b268af0
BLAKE2b-256 checksum
How to use checksums
487ca35a7bf7e174eb5e785d9afcd8b40e556a39354664df9e67e7ec5e1a3d13
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Release files / scann_core-0.2.0-cp314-cp314t-manylinux_2_34_aarch64.whl

Download URL scann_core-0.2.0-cp314-cp314t-manylinux_2_34_aarch64.whl
Size 7.1 MB
Tags CPython 3.14 CPython 3.14 free-threading Linux glibc 2.34+ ARM64
SHA-256 checksum
How to use checksums
8fbf5171d01e86c0f0a7a8f058ab2961707b680da3a468fe6ce386bee3aff200
BLAKE2b-256 checksum
How to use checksums
56c0d069d8018a766fd9d4983579953bc792ce93072607b563e9277bb2464131
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Release files / scann_core-0.2.0-cp314-cp314-manylinux_2_34_x86_64.whl

Download URL scann_core-0.2.0-cp314-cp314-manylinux_2_34_x86_64.whl
Size 8.9 MB
Tags CPython 3.14 Linux glibc 2.34+ x86-64
SHA-256 checksum
How to use checksums
4b3cb7a7ee8683ec103bbbc142fcbe718f2352786e186fe81a7918ba89717f64
BLAKE2b-256 checksum
How to use checksums
c83ccc67783d887e20e433a6f6f480ac40b5df4762132f44c2ea42ba9b963082
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Release files / scann_core-0.2.0-cp314-cp314-manylinux_2_34_aarch64.whl

Download URL scann_core-0.2.0-cp314-cp314-manylinux_2_34_aarch64.whl
Size 7.1 MB
Tags CPython 3.14 Linux glibc 2.34+ ARM64
SHA-256 checksum
How to use checksums
f35dcf9ce4783f97db9f269014e84bbf32bbc5a615c5cf6851b1ff22ee0f2f67
BLAKE2b-256 checksum
How to use checksums
a038556c7946f72770a48b7888ed557872d4f046034ca59a8649070770b54a68
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Release files / scann_core-0.2.0-cp313-cp313-manylinux_2_34_x86_64.whl

Download URL scann_core-0.2.0-cp313-cp313-manylinux_2_34_x86_64.whl
Size 8.9 MB
Tags CPython 3.13 Linux glibc 2.34+ x86-64
SHA-256 checksum
How to use checksums
1de26e398660facfeeb8bb8b7c2009bab94f5460665417e5773b808067853e56
BLAKE2b-256 checksum
How to use checksums
fa5ca41923f360a5df9ad758d1c6610613caa18585ffdd6eaa714975eb2fac04
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Release files / scann_core-0.2.0-cp313-cp313-manylinux_2_34_aarch64.whl

Download URL scann_core-0.2.0-cp313-cp313-manylinux_2_34_aarch64.whl
Size 7.1 MB
Tags CPython 3.13 Linux glibc 2.34+ ARM64
SHA-256 checksum
How to use checksums
efd308a8ba8c2d40aa86561a65d94e725812461d9c01ea90bc4c1d70e0a49f23
BLAKE2b-256 checksum
How to use checksums
440212539179dfec91f18643b19aef12f6f59f9abfae0fd316aea546e053a2d4
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Release files / scann_core-0.2.0-cp312-cp312-manylinux_2_34_x86_64.whl

Download URL scann_core-0.2.0-cp312-cp312-manylinux_2_34_x86_64.whl
Size 8.9 MB
Tags CPython 3.12 Linux glibc 2.34+ x86-64
SHA-256 checksum
How to use checksums
0661238f58730bc6eb8abe91572b97356ef88a5cd1d6a95058b56d41361367dd
BLAKE2b-256 checksum
How to use checksums
58e92bdd901c7cc298f31c547372602479077ace8614e4b0fb18bcff895e421f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Release files / scann_core-0.2.0-cp312-cp312-manylinux_2_34_aarch64.whl

Download URL scann_core-0.2.0-cp312-cp312-manylinux_2_34_aarch64.whl
Size 7.1 MB
Tags CPython 3.12 Linux glibc 2.34+ ARM64
SHA-256 checksum
How to use checksums
6bb3960a3ea76eadcc8cd93cf9f03aa843142f99bd407ebf0ebbc6cbb50c3f1a
BLAKE2b-256 checksum
How to use checksums
a85cbda2a6683a9c18bdac7ac5036d5acdb9464eddb3a2e10740c4a3d91c1bfc
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Release files / scann_core-0.2.0-cp311-cp311-manylinux_2_34_x86_64.whl

Download URL scann_core-0.2.0-cp311-cp311-manylinux_2_34_x86_64.whl
Size 8.9 MB
Tags CPython 3.11 Linux glibc 2.34+ x86-64
SHA-256 checksum
How to use checksums
b9279138474510b2029265ee3317ed18b8b8223dc4f113326e2d852d7b021eec
BLAKE2b-256 checksum
How to use checksums
12e18c55db4be7797927dba9551366cee572369df54e51c54ad3ab08a79b52b5
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Release files / scann_core-0.2.0-cp311-cp311-manylinux_2_34_aarch64.whl

Download URL scann_core-0.2.0-cp311-cp311-manylinux_2_34_aarch64.whl
Size 7.1 MB
Tags CPython 3.11 Linux glibc 2.34+ ARM64
SHA-256 checksum
How to use checksums
2c009ca6fa2df9409e1610063adaa930323b0be08265f1fb3ce31f65ed0caa4c
BLAKE2b-256 checksum
How to use checksums
c489e2c9275e7ae32cf8c3d10cb82d4f34b0b1b0f56d1357ee873f53399d977e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Release files / scann_core-0.2.0-cp310-cp310-manylinux_2_34_x86_64.whl

Download URL scann_core-0.2.0-cp310-cp310-manylinux_2_34_x86_64.whl
Size 8.9 MB
Tags CPython 3.10 Linux glibc 2.34+ x86-64
SHA-256 checksum
How to use checksums
88a959345953266b8f9c935e88294e49de2a33c47c8cdf93199fcc8a1ab13343
BLAKE2b-256 checksum
How to use checksums
e502416aed986466df44988742c858e8304d60bad3eee52c45b8d3b743af999a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Release files / scann_core-0.2.0-cp310-cp310-manylinux_2_34_aarch64.whl

Download URL scann_core-0.2.0-cp310-cp310-manylinux_2_34_aarch64.whl
Size 7.0 MB
Tags CPython 3.10 Linux glibc 2.34+ ARM64
SHA-256 checksum
How to use checksums
8b5cb599738796977c1ca9d064770c20de0b3bd218b367b0953da956be20e88f
BLAKE2b-256 checksum
How to use checksums
6c40cb09dfda08826462a98da3272a88e2da0b121348188b6786f2c152e4b5f2
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Release history Release notifications | RSS feed

0.2.1

17 release files

This release

0.2.0 This release

17 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