Skip to content

feat: three-line index workflow, faster IVF add, and v0.8.2 - #209

Merged
gouyt13 merged 7 commits into
mainfrom
release/v0.8.2
Nov 2, 2026
Merged

gouyt13 merged 7 commits into
mainfrom
release/v0.8.2

Conversation

@gouyt13

@gouyt13 gouyt13 commented Nov 2, 2026

Copy link
Copy Markdown
Member

Summary

Python users now get an index running in three lines, with the same shape for every index:

index = IvfIndex(dim, num_clusters, metric="ip")  # or HnswIndex(dim) / SymqgIndex(dim)
index.add(vectors)  # the first add trains the index
ids, distances = index.search(queries, k=10)

This PR also makes IVF-RaBitQ add much faster when it assigns clusters itself, refreshes the benchmark results, and prepares the v0.8.2 release. Release notes: docs/docs/releases/0.8.2.md.

Changes

IVF add routing (fix)

  • With the Flat initializer (auto below 5,000 clusters), add without cluster_ids scanned every centroid separately for each vector.
  • It now rotates 128 MiB row blocks and uses k-means' blocked exact_assign, with centroid norms cached per centroid set.
  • GIST1M, 4,096 clusters, 48 threads: add drops from 24–26 s to 6.4–6.7 s, with byte-identical index files.
  • Single-row adds stay close to 0.8.1: 0.47 ms vs 0.45 ms at 960 dimensions and 4,096 clusters, single thread.
  • Assignments can differ only at float-rounding ties; in those cases the new result matches a float64 reference.

Python API (additive)

  • The first add on an untrained IvfIndex or HnswIndex trains it. The training runs on a fresh object that is swapped in only on success, so a failed call leaves the index untrained and empty.
  • nbits defaults to 7.
  • nprobe is optional; it defaults to num_clusters / 16.
  • k may exceed the point count; missing slots are padded instead of raising, in both C++ and Python.
  • num_threads=None is accepted by train, add and search.
  • SymqgIndex.add returns IDs.
  • New HnswIndex.M and HnswIndex.ef_construction properties, and rabitqlib.__version__.

Changes to results (in the release notes)

  • Graph search ef now defaults to max(k, 64) instead of max(k, 10).
  • Index training (cp, and C++ default_training_parameters) runs 10 iterations instead of 25, for both IVF and HNSW.

Review fixes
An independent code review and docs review of the first two commits found the following; all are fixed with regression tests:

  • QuantizedGraph::search_with_scratch overflowed memory when given a result pool larger than k. It now checks that the pool capacity equals k.
  • Internal search pools are sized by the point count instead of by k.
  • Single-row IVF add had regressed 4–7×; fixed by the cached centroid norms.
  • An empty first IVF add raised the wrong error type.
  • __version__ failed without package metadata; it is now compiled into the extension from the CMake version.
  • Several stale passages in the docs and migration guide.

Benchmarks
All library methods were rerun on GIST1M and DBpedia with a wheel built from 1d49ecd (GCC 14.3.0, Release, native optimization OFF). IVF-RaBitQ now uses the library's default train + add path.

Total IVF build time 0.8.0 0.8.2
GIST1M, 1+4 17.2 s 11.0 s
DBpedia, 1+4 28.0 s 17.6 s
  • Query QPS medians are 0.95–1.01× of 0.8.0; recall changes by ±0.003, except GIST IVF, which gains 0.006–0.008.
  • k-means results are bit-identical to 0.8.0.
  • Faiss and SuperKMeans results are retained, not rerun.

Docs and release

  • README "Why RaBitQ-Library" section and 0.8.2 news entry.
  • Quick start, index pages, usage page, migration guide, docstrings and type stubs updated.
  • Version bumped to 0.8.2 in pyproject.toml and CMakeLists.txt.

Checks

Run on Linux x86-64, Xeon Gold 6418H, GCC 14.3.0 / clang 23.

Check Result
C++ ctest (native optimization OFF) 490/490 passed
Python pytest tests/python All passed; skips are environment-only (C++ examples not built, macOS/ARM64 wheel tests)
mypy and Pyright editor tests 4/4 passed
check-format.sh, check-python.sh Clean
check-tidy.sh (full) Passed
check-isa-symbols.sh Passed
mkdocs build --strict Passed
.github/scripts unit tests Passed

Doc examples run: README, quick start, and the HNSW and SymphonyQG minimal workflows.

Downstream compatibility (before merging the version bump)

  • zvec: this PR changes no core or FastScan file, and zvec uses only those components. The zvec build check has not been run.
  • txtai: tests/python/test_txtai_compat.py, which mirrors txtai's adapter call for call, passes. The real txtai adapter has not been run; txtai is not installed in the test environment. txtai's HNSW searches omit ef, so they pick up the new ef default, which changes their results and latency.

add without cluster_ids routed each vector with the flat initializer's
single-query centroid scan, so every vector streamed the whole centroid
table from memory. With train() leaving assignment to add, this dominated
the default build path (GIST1M, 4096 clusters, 48 threads: add 24-26 s
versus 2.5 s with explicit cluster ids).

Flat routing now rotates rows in 128 MiB blocks and assigns them with
k-means' blocked exact assignment, which reuses each centroid tile across
many rows. On the same workload add takes 6.4 s with byte-identical index
files; on other data, assignments differ only at float-rounding ties,
where the new choice matches a float64 reference. Flat RaBitQ and HNSW
routing are unchanged.
Make the three indexes start with the same three lines: construct, add,
search.

- IvfIndex and HnswIndex train on the first add when untrained, as
  train(data) does, staging the work on a fresh index so a failed call
  leaves the index untrained and empty. HNSW trains one centroid per row
  when given fewer rows than num_clusters. add with cluster_ids still
  requires training.
- nbits defaults to 7 for IvfIndex and HnswIndex.
- IVF nprobe may be omitted: num_clusters / 16, rounded, at least 1.
- Graph search ef defaults to max(k, 64) instead of max(k, 10).
- Index training defaults to 10 k-means iterations instead of 25, for
  IvfIndex and HnswIndex (C++ default_training_parameters).
- k may exceed the number of points: IVF, HNSW and SymphonyQG pad missing
  results with ID 2**32 - 1 and distance inf instead of raising, in C++
  and Python. A trained index without points returns only padding.
- num_threads accepts None everywhere (add: all CPUs, search: 1 thread).
- SymqgIndex.add returns the point IDs; HnswIndex exposes M and
  ef_construction; rabitqlib.__version__ reports the package version.

The ef and niter defaults change results of code that relies on them.
Docs describe the new workflow and recommend np.isfinite(distances) to
find missing results.
- QuantizedGraph::search_with_scratch rejects a result pool whose capacity
  is not k. Padding had removed the check that previously caught it, so a
  larger pool overflowed the result buffers.
- Flat routing in IVF add caches the centroid norms on first use, cleared
  whenever the initializer changes, and passes them to exact_assign. Single
  -row adds no longer recompute every norm (960 dims, 4096 clusters, one
  thread: 3.31 ms -> 0.47 ms per add; 0.45 ms before blocked routing).
- IVF and SymphonyQG size their internal result and search pools by the
  point count, not by k, so a large k only costs the output arrays.
- An empty first add on an untrained IvfIndex raises RuntimeError, as for
  HnswIndex, instead of a row-count ValueError.
- __version__ comes from the CMake project version compiled into the
  extension, so installs without package metadata still import.
- Docs: fix the migration guide (k padding, ef changing results, state
  errors, untrained 0.7 HNSW files), restore nprobe in the README example,
  correct thread-default and is_trained wording, document the 0.7-form IVF
  exception, and note that flat add routing and query routing may resolve
  near-ties differently.
- Tests cover the scratch pool check, batching and reload independence of
  flat routing, first-add settings (raw storage, initializers, cp, drawn
  seed, preallocated HNSW capacity) and the empty first add.
Rerun every RaBitQ-Library method on GIST1M and DBpedia with a wheel built
from the 0.8.2 release candidate (1d49ecd; GCC 14.3.0, Release, native
optimization OFF). Faiss and SuperKMeans results are retained.

IVF-RaBitQ now builds the way the library does by default: IvfIndex.train
(10 iterations, QGKMeans above 2,000 clusters) followed by add, which
assigns the vectors, instead of RaBitQKMeans with exact final assignment
and explicit cluster IDs. The benchmark script, protocol text, provenance,
key numbers and homepage figure are updated. HNSW-RaBitQ and SymphonyQG
protocols are unchanged; k-means results are bit-identical to 0.8.0.
Summarize what the library offers before the news: the reference implementation from the RaBitQ authors, fast indexing and search with a link to the benchmarks, and the choice between ready-made indexes and the core components.
Bump the version to 0.8.2, update install pins and version text, and add the
0.8.2 release notes with their navigation entry and a README news entry. The
notes call out the two default changes that alter results: graph search ef
and index training iterations.
@github-actions github-actions Bot added the release:added Release category managed from the PR title label Nov 2, 2026
- Homepage benchmark figure: label the results RaBitQ-Library 0.8.2.
- 0.8.2 notes: a 0.7-form IvfIndex is filled only by build, while a 0.7-form
  HnswIndex trains on its first add; an empty first add raises RuntimeError;
  state that the 24-26 s -> 6.4-6.7 s add timing is a separate A/B run with
  nbits=5, not part of the benchmark tables.
- HNSW removal example uses the IDs returned by add, so the page runs in order.
- Migration guide lists the 0.8.2 C++ behavior changes.
- Benchmark pages: SymphonyQG uses nbits, and the wheel command is described
  as the equivalent v0.8.2 build.
- usage.md: deprecated search_batch accepts None; only build needs an integer.
- num_clusters docs and docstrings mention the first add and HNSW's row cap;
  the 0.7 IvfIndex deprecation message says the first add trains.
@gouyt13
gouyt13 merged commit df9d34b into main Nov 2, 2026
48 checks passed
@gouyt13
gouyt13 deleted the release/v0.8.2 branch November 2, 2026 12:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

release:added Release category managed from the PR title

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant