From 024e210114af55163eeff57e8ae8fcec447a72ae Mon Sep 17 00:00:00 2001
From: gouyt13clear
Date: Sat, 31 Oct 2026 23:57:55 +0800
Subject: [PATCH 1/3] ci: deploy docs on every main push and fix stale script
paths
A newer push could cancel a docs-changing run on main, and its own diff
would then skip the deploy, leaving the site behind main. Main pushes now
always build and deploy the documentation.
Also drop the deleted benchmarks/plot_homepage.py from CI routing, extract
the GIST archive in sample/run_cpp.sh into ./data like run_python.sh (the
archive has a top-level gist/ directory), and record the OpenAI dataset
source the downloader actually uses.
---
.github/scripts/ci_changes.py | 1 -
.github/scripts/test_ci_changes.py | 5 ++---
.github/scripts/test_ci_workflow_contracts.py | 1 +
.github/workflows/docs.yml | 4 +++-
benchmarks/DBPEDIA_MANIFEST.json | 2 +-
sample/run_cpp.sh | 2 +-
6 files changed, 8 insertions(+), 7 deletions(-)
diff --git a/.github/scripts/ci_changes.py b/.github/scripts/ci_changes.py
index a3a3d9d6..7a52564e 100644
--- a/.github/scripts/ci_changes.py
+++ b/.github/scripts/ci_changes.py
@@ -55,7 +55,6 @@ def selects_core(path):
"build_indexes",
"download_datasets",
"install_baselines",
- "plot_homepage",
"plot_results",
"query_index",
"reproduce",
diff --git a/.github/scripts/test_ci_changes.py b/.github/scripts/test_ci_changes.py
index be376042..cc1ee247 100644
--- a/.github/scripts/test_ci_changes.py
+++ b/.github/scripts/test_ci_changes.py
@@ -67,7 +67,7 @@ def test_known_support_paths_do_not_select_unrelated_builds(self):
".github/ISSUE_TEMPLATE/bug_report.yml": set(),
".github/CODEOWNERS": set(),
".github/release.yml": {"python_quality"},
- "benchmarks/plot_homepage.py": {"python_quality"},
+ "benchmarks/plot_results.py": {"python_quality"},
"sample/run_cpp.sh": {"shell"},
"sample/run_python.sh": {"shell"},
"example.sh": {"shell"}, # Deleted paths still occur in a push diff.
@@ -87,7 +87,7 @@ def test_known_support_paths_do_not_select_unrelated_builds(self):
self.assertEqual(classify([path]), expected)
def test_support_paths_do_not_mask_source_or_unknown_changes(self):
- support = [".github/ISSUE_TEMPLATE/config.yml", "benchmarks/plot_homepage.py"]
+ support = [".github/ISSUE_TEMPLATE/config.yml", "benchmarks/plot_results.py"]
self.assertEqual(
classify(support + ["src/core/api.cpp"]),
{
@@ -364,7 +364,6 @@ def test_tagged_main_routing_changes_preserve_lightweight_companion_checks(self)
for companions, expected in (
(["scripts/check-python.sh"], {"python_quality", "shell"}),
(["sample/run_cpp.sh"], {"python_quality", "shell"}),
- (["benchmarks/plot_homepage.py"], {"python_quality"}),
(["benchmarks/build_indexes.py"], {"python_quality"}),
([".github/ISSUE_TEMPLATE/config.yml"], {"python_quality"}),
(["src/core/api.cpp"], set(GROUPS)),
diff --git a/.github/scripts/test_ci_workflow_contracts.py b/.github/scripts/test_ci_workflow_contracts.py
index b4b9d2c3..8b56366d 100644
--- a/.github/scripts/test_ci_workflow_contracts.py
+++ b/.github/scripts/test_ci_workflow_contracts.py
@@ -869,6 +869,7 @@ def test_documentation_summary_is_available_for_non_doc_prs(self):
self.assertEqual(needs(build), ["changes"])
self.assertIn("needs.changes.outputs.docs == 'true'", build["if"])
self.assertIn("needs.changes.result != 'success'", build["if"])
+ self.assertIn("github.event_name == 'push'", build["if"])
self.assertEqual(classify(["docs/docs/quick_start.md"]), {"docs"})
diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
index b9a6fa00..bcb41ed4 100644
--- a/.github/workflows/docs.yml
+++ b/.github/workflows/docs.yml
@@ -19,7 +19,9 @@ jobs:
build:
needs: changes
- if: ${{ !cancelled() && (needs.changes.result != 'success' || needs.changes.outputs.docs == 'true') }}
+ # Main pushes always deploy: a newer push can cancel a docs-changing run, and
+ # its own diff would otherwise leave the site at the older commit.
+ if: ${{ !cancelled() && (github.event_name == 'push' || needs.changes.result != 'success' || needs.changes.outputs.docs == 'true') }}
name: Build documentation
runs-on: ubuntu-latest
timeout-minutes: 10
diff --git a/benchmarks/DBPEDIA_MANIFEST.json b/benchmarks/DBPEDIA_MANIFEST.json
index d596abcf..bef4920a 100644
--- a/benchmarks/DBPEDIA_MANIFEST.json
+++ b/benchmarks/DBPEDIA_MANIFEST.json
@@ -1,6 +1,6 @@
{
"dataset": "openai-1536-angular",
- "source": "SuperKMeans benchmark data_openai.bin and data_openai_test.bin",
+ "source": "openai-1536-angular.hdf5 train/test splits (Google Drive file 1cwIY76n_HEbbZAANJVWiVkjBsIy3b9jB), converted to fvecs",
"metric": "inner_product",
"normalization": "unit-normalized source values preserved exactly",
"dimension": 1536,
diff --git a/sample/run_cpp.sh b/sample/run_cpp.sh
index c1619b80..a6125a01 100755
--- a/sample/run_cpp.sh
+++ b/sample/run_cpp.sh
@@ -13,7 +13,7 @@ mkdir -p ./data/gist
if [ ! -f ./data/gist/gist.tar.gz ]; then
wget -P ./data/gist ftp://ftp.irisa.fr/local/texmex/corpus/gist.tar.gz
fi
-tar -xzvf ./data/gist/gist.tar.gz -C ./data/gist
+tar -xzvf ./data/gist/gist.tar.gz -C ./data
# indexing and querying for symqg
./bin/symqg_indexing ./data/gist/gist_base.fvecs 32 400 ./data/gist/symqg_32.index
From df4d1a6d39a19d78496ac5fbe8169a2ac43860a2 Mon Sep 17 00:00:00 2001
From: gouyt13clear
Date: Sat, 31 Oct 2026 23:57:55 +0800
Subject: [PATCH 2/3] docs: align documentation with the current code
Correct statements that no longer match the code: advanced-encoder
preconditions, HNSW ef/pool clamping, add capacity, M validation and
cluster_id_of, the IVF k limit, wheel OpenMP bundling, contributor
commands (ctest --no-tests=error, clang-tidy flags, Ruff coverage of
.github/scripts), CI routing and caching notes, and the test layout.
Add the missing 0.7.1 release notes, point README links at absolute URLs
so they also work on PyPI, and remove an unused image.
---
.github/CONTRIBUTING.md | 4 +-
AGENTS.md | 2 +-
README.md | 56 ++++++++++++++--------------
benchmarks/README.md | 4 +-
benchmarks/core/README.md | 8 ++--
benchmarks/hotpaths/README.md | 9 ++++-
docs/development.md | 32 +++++++++++-----
docs/docs/assets/img/SingleData.png | Bin 13356 -> 0 bytes
docs/docs/cpp_integration.md | 4 +-
docs/docs/index/hnsw.md | 29 +++++++++-----
docs/docs/index/ivf.md | 4 +-
docs/docs/installation.md | 5 ++-
docs/docs/rabitq/core_api.md | 2 +-
docs/docs/rabitq/quantizer.md | 14 ++++---
docs/docs/releases/0.7.1.md | 55 +++++++++++++++++++++++++++
docs/maintaining.md | 32 ++++++++++------
docs/mkdocs.yml | 1 +
tests/README.md | 46 ++++++++++++++---------
18 files changed, 209 insertions(+), 98 deletions(-)
delete mode 100644 docs/docs/assets/img/SingleData.png
create mode 100644 docs/docs/releases/0.7.1.md
diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md
index 29c0f9f7..e16b1592 100644
--- a/.github/CONTRIBUTING.md
+++ b/.github/CONTRIBUTING.md
@@ -104,10 +104,10 @@ cmake -S . -B build -DRABITQ_BUILD_TESTS=ON \
-DRABITQ_BUILD_SAMPLES=OFF -DCMAKE_BUILD_TYPE=Release \
-DRABITQ_ENABLE_NATIVE_OPTIMIZATION=OFF
cmake --build build --parallel 2
-ctest --test-dir build --output-on-failure
+ctest --no-tests=error --test-dir build --output-on-failure
```
-The first configuration needs Git and network access to download GoogleTest.
+The first configuration needs network access to download GoogleTest.
Increase `--parallel 2` if your machine has enough memory for more compiler jobs.
Native CPU tuning is disabled here to keep generic code portable. On Windows
or Apple Silicon, use the matching commands in [the test guide](../tests/README.md#quick-start).
diff --git a/AGENTS.md b/AGENTS.md
index b8767b24..e5b94d10 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -63,7 +63,7 @@ Use `ctest --no-tests=error`, including filtered and installed-consumer runs.
C++17 quantization/ANN library with Python bindings (`rabitqlib`). Components depend in one
direction: `index` → `fastscan` → `core`; core and FastScan support OpenMP OFF, ANN/Python require ON.
-Stable float32 v1 API: `include/rabitqlib/core/api.hpp`. Implementations: `src/{core,simd,fastscan,index}/`.
+Stable float32 v1 API: `include/rabitqlib/core/api.hpp`. Implementations: `src/{core,simd,utils,fastscan,index}/`.
Bindings: `python_bindings/`; tests: `tests/{unit,python}/`; site sources: `docs/docs/`.
See the [full code map](docs/development.md#project-and-code-map) when locating unfamiliar code.
diff --git a/README.md b/README.md
index 80697cfb..70bea351 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
-
+
@@ -12,17 +12,17 @@
RaBitQ-Library is a research-backed library for vector quantization and
-approximate nearest-neighbor search. See the [research papers](docs/docs/papers.md).
+approximate nearest-neighbor search. See the [research papers](https://vectordb-ntu.github.io/RaBitQ-Library/papers/).
## News
- **v0.7.2:** Python type hints, expanded API documentation, path-like inputs,
clearer input errors, and concise index representations. Internal quantization
refactoring and streamlined regression checks preserve existing APIs and index formats.
- See the [release notes](docs/docs/releases/0.7.2.md).
+ See the [release notes](https://vectordb-ntu.github.io/RaBitQ-Library/releases/0.7.2/).
- **v0.7.0:** Modular C++ components, stable core API v1, optional
OpenMP for numerical components, and unified index counts. See the
- [upgrade notes](docs/docs/releases/0.7.0.md).
+ [upgrade notes](https://vectordb-ntu.github.io/RaBitQ-Library/releases/0.7.0/).
## Vector indexes
@@ -33,14 +33,14 @@ QPS–recall tradeoffs.
| Index | Search approach | Storage and updates |
| --- | --- | --- |
-| [IVF-RaBitQ](docs/docs/index/ivf.md) | Partitioned search with batch scoring. | 1–9-bit codes or raw-vector reranking; add/remove. |
-| [HNSW-RaBitQ](docs/docs/index/hnsw.md) | Graph traversal over quantized vectors. | 1–9-bit codes plus graph edges; add/remove and resize. |
-| [SymphonyQG](docs/docs/index/qg.md) | Graph search with batched distance estimation. | Raw or 4/8-bit storage; updates require rebuilding. |
+| [IVF-RaBitQ](https://vectordb-ntu.github.io/RaBitQ-Library/index/ivf/) | Partitioned search with batch scoring. | 1–9-bit codes or raw-vector reranking; add/remove. |
+| [HNSW-RaBitQ](https://vectordb-ntu.github.io/RaBitQ-Library/index/hnsw/) | Graph traversal over quantized vectors. | 1–9-bit codes plus graph edges; add/remove and resize. |
+| [SymphonyQG](https://vectordb-ntu.github.io/RaBitQ-Library/index/qg/) | Graph search with batched distance estimation. | Raw or 4/8-bit storage; updates require rebuilding. |
-[Native clustering methods](docs/docs/clustering.md) prepare centroids and
+[Native clustering methods](https://vectordb-ntu.github.io/RaBitQ-Library/clustering/) prepare centroids and
assignments for IVF-RaBitQ and HNSW-RaBitQ.
-See the [benchmarks](docs/docs/benchmarks/bench.md) for measured results,
+See the [benchmarks](https://vectordb-ntu.github.io/RaBitQ-Library/benchmarks/bench/) for measured results,
settings, and limitations.
## Quick start
@@ -53,7 +53,7 @@ python -m pip install --upgrade "rabitqlib>=0.7.2"
Wheels support CPython 3.11–3.14 on Linux x86-64/ARM64, Windows x86-64,
and macOS 14+ ARM64. x86-64 requires AVX2/FMA and optionally uses
-AVX-512; ARM64 uses NEON. See [platform requirements and source installation](docs/docs/installation.md#requirements).
+AVX-512; ARM64 uses NEON. See [platform requirements and source installation](https://vectordb-ntu.github.io/RaBitQ-Library/installation/#requirements).
Build and search a small IVF-RaBitQ index with synthetic data; no dataset download is
required. This example demonstrates the API, not performance.
@@ -78,17 +78,17 @@ print(ids.shape, distances.shape) # (5, 10) (5, 10)
IDs identify input rows; distances here estimate squared L2 distance, with smaller
values first. Quantization remains approximate even when all clusters are probed.
-The [complete quick start](docs/docs/quick_start.md) explains result handling,
+The [complete quick start](https://vectordb-ntu.github.io/RaBitQ-Library/quick_start/) explains result handling,
measures recall while varying `nprobe`, and shows save/load.
-For other indexes, see the [HNSW-RaBitQ](docs/docs/index/hnsw.md#minimal-workflow) and
-[SymphonyQG](docs/docs/index/qg.md#minimal-workflow) examples.
+For other indexes, see the [HNSW-RaBitQ](https://vectordb-ntu.github.io/RaBitQ-Library/index/hnsw/#minimal-workflow) and
+[SymphonyQG](https://vectordb-ntu.github.io/RaBitQ-Library/index/qg/#minimal-workflow) examples.
### C++
-Use the [C++ integration guide](docs/docs/cpp_integration.md) for source builds,
+Use the [C++ integration guide](https://vectordb-ntu.github.io/RaBitQ-Library/cpp_integration/) for source builds,
component linking, and installation. Start with the
-[C++ index examples](docs/docs/cpp_integration.md#c-examples), or use the
+[C++ index examples](https://vectordb-ntu.github.io/RaBitQ-Library/cpp_integration/#c-examples), or use the
quantization components below in your own engine.
## Quantization components
@@ -98,12 +98,12 @@ engine, independently of the included indexes.
| Component | What it provides | Start here |
| --- | --- | --- |
-| `rabitqlib::core` | Rotation, per-vector encoding, query preparation, and distance estimation. | [Core example](sample/cpp/core/) · [Stable v1 API](docs/docs/rabitq/core_api.md) |
-| `rabitqlib::fastscan` | Batch encoding and lookup-table scoring over 32-vector layouts, built on core. | [FastScan example](sample/cpp/fastscan/) |
+| `rabitqlib::core` | Rotation, per-vector encoding, query preparation, and distance estimation. | [Core example](https://github.com/VectorDB-NTU/RaBitQ-Library/tree/main/sample/cpp/core) · [Stable v1 API](https://vectordb-ntu.github.io/RaBitQ-Library/rabitq/core_api/) |
+| `rabitqlib::fastscan` | Batch encoding and lookup-table scoring over 32-vector layouts, built on core. | [FastScan example](https://github.com/VectorDB-NTU/RaBitQ-Library/tree/main/sample/cpp/fastscan) |
-See [component integration](docs/docs/cpp_integration.md#core-only-c-integration)
-for build options and compatibility, the [quantizer guide](docs/docs/rabitq/quantizer.md)
-for encoding choices, and the [core benchmark](benchmarks/core/README.md) for
+See [component integration](https://vectordb-ntu.github.io/RaBitQ-Library/cpp_integration/#core-only-c-integration)
+for build options and compatibility, the [quantizer guide](https://vectordb-ntu.github.io/RaBitQ-Library/rabitq/quantizer/)
+for encoding choices, and the [core benchmark](https://github.com/VectorDB-NTU/RaBitQ-Library/blob/main/benchmarks/core/README.md) for
component throughput and estimation error.
## Adoption and resources
@@ -114,8 +114,8 @@ Two examples of direct library integration:
| Project | Integration |
| --- | --- |
-| [txtai](docs/docs/integrations/txtai.md) | Uses the Python IVF-RaBitQ and HNSW-RaBitQ indexes as an embedding-search backend. |
-| [zvec](docs/docs/integrations/zvec.md) | Uses core quantization in its own IVF-RaBitQ and HNSW-RaBitQ indexes, with FastScan batch scoring in IVF-RaBitQ. |
+| [txtai](https://vectordb-ntu.github.io/RaBitQ-Library/integrations/txtai/) | Uses the Python IVF-RaBitQ and HNSW-RaBitQ indexes as an embedding-search backend. |
+| [zvec](https://vectordb-ntu.github.io/RaBitQ-Library/integrations/zvec/) | Uses core quantization in its own IVF-RaBitQ and HNSW-RaBitQ indexes, with FastScan batch scoring in IVF-RaBitQ. |
The linked walkthroughs describe integration boundaries and provide versioned
source references.
@@ -191,15 +191,15 @@ listed above.
### Further reading
-- [Documentation](https://vectordb-ntu.github.io/RaBitQ-Library/) and [algorithm overview](docs/docs/rabitq/rabitq.md).
-- [Papers and citations](docs/docs/papers.md) for the research behind the library.
-- [Release notes](https://github.com/VectorDB-NTU/RaBitQ-Library/releases) and [0.7 migration guide](docs/docs/modular_migration.md).
+- [Documentation](https://vectordb-ntu.github.io/RaBitQ-Library/) and [algorithm overview](https://vectordb-ntu.github.io/RaBitQ-Library/rabitq/rabitq/).
+- [Papers and citations](https://vectordb-ntu.github.io/RaBitQ-Library/papers/) for the research behind the library.
+- [Release notes](https://github.com/VectorDB-NTU/RaBitQ-Library/releases) and [0.7 migration guide](https://vectordb-ntu.github.io/RaBitQ-Library/modular_migration/).
- [GPU implementation](https://github.com/Stardust-SJF/cuvs_rabitq/tree/cuvs_ivf_rabitq) in cuVS.
## Contributing
-Start with the [contribution guide](.github/CONTRIBUTING.md#your-first-contribution)
-or [starter tasks](.github/CONTRIBUTING.md#starter-tasks). Report bugs and request
+Start with the [contribution guide](https://github.com/VectorDB-NTU/RaBitQ-Library/blob/main/.github/CONTRIBUTING.md#your-first-contribution)
+or [starter tasks](https://github.com/VectorDB-NTU/RaBitQ-Library/blob/main/.github/CONTRIBUTING.md#starter-tasks). Report bugs and request
features through [GitHub Issues](https://github.com/VectorDB-NTU/RaBitQ-Library/issues/new/choose).
## Authors and acknowledgements
@@ -213,4 +213,4 @@ Jiabao Jin, Xiaoyao Zhong, and Jinjing Zhou for their valuable feedback.
## License
-RaBitQ Library is available under the [Apache License 2.0](LICENSE).
+RaBitQ Library is available under the [Apache License 2.0](https://github.com/VectorDB-NTU/RaBitQ-Library/blob/main/LICENSE).
diff --git a/benchmarks/README.md b/benchmarks/README.md
index c4ec5a71..ff751b00 100644
--- a/benchmarks/README.md
+++ b/benchmarks/README.md
@@ -3,8 +3,8 @@
For embedding the quantizer in another system, use the independent
[core C++ benchmark](core/README.md). It needs only core and reports per-repeat timings,
distance-estimate error and quantized full-scan recall. The workflows below benchmark
-complete ANN/clustering systems. For dispatch, encoding, FastScan, sidecar loading and
-Python ID-conversion regression measurements, use the
+complete ANN/clustering systems. For dispatch, encoding, FastScan, sidecar loading, IVF add
+and Python ID-conversion regression measurements, use the
[component hot-path benchmarks](hotpaths/README.md).
See the [benchmark overview](../docs/docs/benchmarks/bench.md) for datasets,
diff --git a/benchmarks/core/README.md b/benchmarks/core/README.md
index 9f460873..1a3de18e 100644
--- a/benchmarks/core/README.md
+++ b/benchmarks/core/README.md
@@ -12,7 +12,7 @@ cmake -S . -B build-core-bench -DCMAKE_BUILD_TYPE=Release \
-DRABITQ_BUILD_SAMPLES=OFF -DRABITQ_USE_OPENMP=OFF \
-DRABITQ_BUILD_BENCHMARKS=ON -DRABITQ_ENABLE_NATIVE_OPTIMIZATION=OFF
cmake --build build-core-bench --parallel
-ctest --test-dir build-core-bench --output-on-failure
+ctest --no-tests=error --test-dir build-core-bench --output-on-failure
./build-core-bench/benchmarks/core/rabitq_core_benchmark > core-results.csv
```
@@ -22,7 +22,7 @@ For an installed package, configure this directory directly:
cmake -S benchmarks/core -B build-core-bench-installed \
-DCMAKE_BUILD_TYPE=Release -DCMAKE_PREFIX_PATH=""
cmake --build build-core-bench-installed --parallel
-ctest --test-dir build-core-bench-installed --output-on-failure
+ctest --no-tests=error --test-dir build-core-bench-installed --output-on-failure
./build-core-bench-installed/rabitq_core_benchmark 128 4 4096 64 7 > core-results.csv
```
@@ -107,8 +107,8 @@ valid code/factor prototypes are encoded, then replicated at distinct memory add
This models access and call costs, not dataset-dependent ANN quality. Inputs are already
in the numerical working domain; no rotation or top-k retrieval is timed or evaluated.
The metric is L2 with a zero centroid. Prototype scores must match all three references
-exactly before timing. Raw timings and double-precision checksums are printed for every
-path and repetition; path order rotates and each timed pass has its own warmup.
+within a computed float32 rounding bound before timing. Raw timings and
+double-precision checksums are printed for every path and repetition; path order rotates and each timed pass has its own warmup.
Compare baseline/candidate binaries with identical harness source, compiler flags and
parameters. Alternate process order, retain all samples, and verify checksums alongside
diff --git a/benchmarks/hotpaths/README.md b/benchmarks/hotpaths/README.md
index 41d5c304..7b7a4bd4 100644
--- a/benchmarks/hotpaths/README.md
+++ b/benchmarks/hotpaths/README.md
@@ -11,6 +11,7 @@ Enable `RABITQ_BUILD_BENCHMARKS=ON` in a Release build. Targets follow enabled c
| `rabitq_core_hotpaths` | core only | One-bit compact encoding or raw L2 calls |
| `rabitq_fastscan_hotpaths` | FastScan | Cached standard/HACC batch scoring |
| `rabitq_load_hotpaths` | index | HNSW centroid-sidecar load and validation |
+| `rabitq_ivf_add_hotpaths` | index | One IVF `add` call on a freshly loaded index |
| `rabitq_ids_benchmark` module + `ids.py` | Python bindings | Production cluster-ID conversion helper |
No additional production dependencies are introduced. Core/FastScan benchmarks support
@@ -27,7 +28,7 @@ cmake -S . -B build-hotpaths -DCMAKE_BUILD_TYPE=Release \
-DRABITQ_BUILD_BENCHMARKS=ON -DRABITQ_BUILD_SAMPLES=OFF \
-DRABITQ_ENABLE_NATIVE_OPTIMIZATION=OFF
cmake --build build-hotpaths --parallel
-ctest --test-dir build-hotpaths -R 'Hotpath' --output-on-failure
+ctest --no-tests=error --test-dir build-hotpaths -R 'Hotpath' --output-on-failure
```
Set both `RABITQ_BUILD_INDEX=OFF` and `RABITQ_BUILD_FASTSCAN=OFF` for pure core.
@@ -50,6 +51,9 @@ Examples (from the repository root):
./build-hotpaths/benchmarks/hotpaths/rabitq_load_hotpaths prepare "$TMPDIR/centroids" 128 16384 1
./build-hotpaths/benchmarks/hotpaths/rabitq_load_hotpaths load "$TMPDIR/centroids" 128 16384 7
+# path dim clusters base_n add_n repetitions single|routed; path receives the base index
+./build-hotpaths/benchmarks/hotpaths/rabitq_ivf_add_hotpaths "$TMPDIR/ivf-add" 64 8 256 16 3 routed
+
python benchmarks/hotpaths/ids.py --module-dir build-hotpaths/python_bindings
```
@@ -61,7 +65,8 @@ the deterministic input. Original production indexes are not benchmark fixtures.
- IVF add verifies the expected point counts and samples new points' self-neighbors
outside the timed call, in addition to comparing output hashes across repetitions.
-- Inputs use an LCG with seed 42 (query/centroid seed 91 where applicable), metric L2.
+- Inputs use an LCG with seed 42 (query/centroid seed 91 where applicable; IVF add uses
+ seed 91 for added points and seed 7 for centroids), metric L2.
Dimensions for encoding/scoring are multiples of 32. Use 64, 128 and 960 dimensions;
encoding rows 32/4096 and threads 1/4 retain the cases from the original investigation.
- Core encoding times the existing serial template at one thread and OpenMP template
diff --git a/docs/development.md b/docs/development.md
index 8cc3b04b..5e049d76 100644
--- a/docs/development.md
+++ b/docs/development.md
@@ -140,6 +140,10 @@ ruff check --fix python_bindings sample/python tests/python tests/test_mypy.py t
ruff format python_bindings sample/python tests/python tests/test_mypy.py tests/test_pyright.py benchmarks
```
+`check-python.sh` does not cover the CI automation in `.github/scripts`; CI also
+runs `ruff format --check .github/scripts` and `ruff check .github/scripts`, so
+run both after changing those scripts.
+
### Shell scripts
Run ShellCheck after changing a contributor or automation script:
@@ -167,8 +171,10 @@ It recompiles the library sources from `compile_commands.json` with that build's
(extra arguments such as `-O1` are appended) and fails when copies of a weak function
differ between ISA flag sets. CI checks the Release (`-O3`) configuration; with GCC 14
the check also passes at `-O1` and `-O2 -g`, which is what the sanitizer and
-RelWithDebInfo configurations compile. Debug (`-O0`) still emits standard-library
-helpers (`std::min`, `std::round`, `std::fill`) per source and is not clean. The FastScan
+RelWithDebInfo configurations compile. Debug (`-O0`) is not clean: it emits
+standard-library helpers (`std::min`, `std::round`, `std::fill`) and small
+first-party inline functions (for example `SearchBuffer` and query accessors) per
+source. The FastScan
estimator correction is written as plain loops rather than Eigen expressions so that no
ISA source instantiates a shared Eigen helper; keep it that way, since a static library
only hides such sharing while its generic objects precede the ISA objects. The script
@@ -197,6 +203,7 @@ cmake -S . -B build-tidy -G Ninja \
-DRABITQ_BUILD_SAMPLES=OFF \
-DRABITQ_BUILD_TESTS=OFF \
-DRABITQ_BUILD_PYTHON_BINDINGS=ON \
+ -DRABITQ_ENABLE_NATIVE_OPTIMIZATION=OFF \
-DCMAKE_BUILD_TYPE=Release \
-Dpybind11_DIR="$pybind11_DIR"
CXX=clang++-23 ./scripts/check-tidy.sh build-tidy
@@ -538,7 +545,9 @@ that corrupt or mismatched files are reported before allocation, without treatin
crafted file as a supported input.
Do not silently reinterpret an old file. Add a magic/version discriminator, use fixed-width
-serialized fields for new formats, validate sizes before allocation, check every read, and retain a
+serialized fields for new formats (the current IVF, HNSW and QG bodies still store several
+`size_t` fields; change those only with a format version bump), validate sizes before
+allocation, check every read, and retain a
compatibility test fixture or an explicit rejection path. Review each affected format;
changes to shared serialization helpers require auditing every format using them.
IVF-RaBitQ, HNSW-RaBitQ, and SymphonyQG have separate formats. SymphonyQG includes a versioned quantized format plus a legacy
@@ -703,7 +712,7 @@ public and protected names, Python keywords, field order and numerical operation
role is clear.
- Clustering's private state uses `dim_`, `num_points_`, `num_clusters_`,
`num_workers_` and `data_`. Keep public `d`, `k`, `num_threads` and graph parameter
- names compatible. Retain the existing private `niter_` graph iteration name.
+ names compatible. The private graph iteration count is `graph_build_iterations_`.
Per-point centroid IDs are `assignments` (arrays) and `cluster_id` (scalars);
`label` stays reserved for HNSW's caller-visible labels.
- FHT backend arguments use `rotated_data`, `padded_dim`, `trunc_dim` and
@@ -878,8 +887,10 @@ nothing in an old file is reinterpreted. A file that has removals is rejected by
through 0.5.1, whose `load` range-checks every cluster ID; that rejection is the compatibility path.
This mark is unrelated to the `SearchBuffer` ID marker. Keep these rules when touching HNSW-RaBitQ code:
-- Read cluster IDs only through `cluster_id_of`, which masks the bit. Estimators,
- routing, and the `add` reconstruction index centroids with it.
+- Read cluster IDs only through the masking accessors. Internal code (estimators,
+ routing, and the `add` reconstruction) uses the unchecked private `stored_cluster_id`;
+ the public `cluster_id_of` adds storage and bounds checks and must stay out of the
+ per-candidate search kernels.
- A removed point stays in the graph: base-layer search still inserts it into `search_pool`, and
`add` may link to it. Only the result buffer skips it, including when it is the entry point.
- `load` masks the bit before range-checking the cluster ID.
@@ -971,8 +982,10 @@ the compiler inlines them. `scripts/check-isa-symbols.sh` checks Release builds;
[ISA symbol check](#isa-symbol-check).
The AVX FHT intrinsics in `src/simd/fht_kernels.hpp` use the private kernel namespace
for the same reason. Rotation uses this implementation on GCC, Clang, and MSVC;
-no assembler is required. Keep the FFHT attribution and MIT license in the
-intrinsics header intact.
+no assembler is required. Keep the FFHT attribution comment in the intrinsics
+header intact. The intrinsics reimplement the transform following FFHT's stage
+schedule without copying its code, so the header does not carry FFHT's MIT notice;
+code copied from FFHT must bring that notice with it.
#### Eigen isolation
@@ -1073,7 +1086,8 @@ source groups/flags with all three dispatchers and `src/utils/cpu_features.cpp`.
### Performance and compatibility
-- Use fixed-width integer types for serialized values and persisted index data.
+- Use fixed-width integer types for new serialized values and persisted index data;
+ existing `size_t` fields change only with a format version bump.
- Preserve existing public headers, aliases, and index formats unless a change
is explicitly documented as breaking.
- Add backend-independent tests when introducing or changing SIMD kernels.
diff --git a/docs/docs/assets/img/SingleData.png b/docs/docs/assets/img/SingleData.png
deleted file mode 100644
index b5bf95654c2284ce5133d6157491384ed16d561a..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001
literal 13356
zcmd73bx>VhvnPxkJi#qE1eXB8-Q8UdP6+Pq?i$?PEx3D-Ai;vW1b27%cAoc}syp{S
zQ}gdo1qb#LR(G%6`s;8-d5QOkc!&@X5bvcVMU^2SKS|*P+?@e~-U?d_
z7z*o8qnOM~Cw4?IIr+nzuW5bCujrN43_4HS@s5cn))gKeo~NwER+etAy(k4QFE3sx
zbSPm#cz8%s5>SvID(Zjv0~v~+jC?
@@ -48,6 +50,8 @@ of clusters probed to balance storage, recall, and query time.
**Updates:** append and remove vectors.
+**GIST1M, 5-bit:** 1,038 QPS at 92.8% Recall@10; 0.60 GiB index.
+
@@ -59,6 +63,8 @@ search window and graph degree alongside the code precision.
**Updates:** append and remove vectors.
+**GIST1M, 5-bit:** 1,866 QPS at 92.7% Recall@10; 0.72 GiB index.
+
@@ -70,6 +76,8 @@ Combine batched neighbor scoring with raw or 4/8-bit vector storage.
**Updates:** rebuild to change the dataset.
+**GIST1M, 4-bit:** 3,086 QPS at 90.4% Recall@10; 4.39 GiB index.
+
@@ -87,9 +95,11 @@ For input shapes, distance conventions, memory estimates, and threading, see
## Compare measured performance
-See the [benchmarks](benchmarks/bench.md) for datasets, settings, measured
-results, and limitations. Evaluate recall and query time on your own dataset
-when choosing an index.
+The GIST1M numbers above are the fastest measured setting with Recall@10 ≥ 90%,
+using one search thread and one query per call with RaBitQ-Library 0.7.1; index
+size is serialized storage. See the [benchmark results](benchmarks/bench.md#key-results)
+for datasets, baselines, settings, and limitations. Evaluate recall and query time
+on your own dataset when choosing an index.
## Built with RaBitQ-Library
diff --git a/docs/docs/stylesheets/extra.css b/docs/docs/stylesheets/extra.css
index db9c52d6..963f64c6 100644
--- a/docs/docs/stylesheets/extra.css
+++ b/docs/docs/stylesheets/extra.css
@@ -71,12 +71,15 @@ body { -webkit-font-smoothing: antialiased; }
.md-header__button.md-logo { color: var(--md-typeset-a-color); }
.md-grid { max-width: 68rem; }
.md-main__inner { margin-top: 1.6rem; }
+/* Tables, code and figures use the full column; prose keeps a readable measure. */
.md-content__inner {
box-sizing: border-box;
- max-width: 34rem;
+ max-width: 44rem;
margin-inline: auto;
padding-inline: 1rem;
}
+.md-typeset > :is(p, ul, ol, dl, blockquote, h2, h3, h4) { max-width: 34rem; }
+.md-typeset > p:has(> img.chart) { max-width: none; }
.md-content__inner:has(.hero) { max-width: none; }
.hero .hero-logo {
display: block;
@@ -150,6 +153,12 @@ body { -webkit-font-smoothing: antialiased; }
border-left: 2px solid var(--rq-line);
}
.md-typeset :is(.admonition, details) { border-radius: .4rem; box-shadow: none; }
+.md-typeset img.chart {
+ display: block;
+ width: 100%;
+ border: 1px solid var(--rq-line);
+ border-radius: .4rem;
+}
.md-typeset .md-button {
padding: .55em 1.3em;
@@ -208,6 +217,7 @@ body { -webkit-font-smoothing: antialiased; }
font-size: .9rem;
line-height: 1.65;
}
+.hero .nowrap { white-space: nowrap; }
.hero .hero-actions {
display: flex;
flex-wrap: wrap;
@@ -287,6 +297,9 @@ body { -webkit-font-smoothing: antialiased; }
.hero h1 { font-size: clamp(1.65rem, 7.5vw, 2.4rem); }
.hero .hero-description { font-size: .86rem; }
.feature-grid { grid-template-columns: 1fr; gap: 1rem; }
+ /* Keep chart text legible; the figure scrolls sideways instead of shrinking. */
+ .md-typeset > p:has(> img.chart) { overflow-x: auto; }
+ .md-typeset img.chart { width: 36rem; max-width: none; }
.md-typeset pre > code,
.md-typeset .highlight pre > code { padding: .8rem; }
}
diff --git a/tests/python/test_benchmark_protocol.py b/tests/python/test_benchmark_protocol.py
index 18792270..1a1df4df 100644
--- a/tests/python/test_benchmark_protocol.py
+++ b/tests/python/test_benchmark_protocol.py
@@ -186,3 +186,62 @@ def test_kmeans_export_rejects_wrong_metric_before_writing(tmp_path):
with pytest.raises(ValueError, match="unexpected benchmark configuration"):
publish(tmp_path / "results", docs, datasets=["gist1m"], methods=["faiss"])
assert page.read_text() == "original content"
+
+
+def _write_sweep(directory, name, cpu):
+ (directory / "gist1m").mkdir(parents=True, exist_ok=True)
+ (directory / "sweeps" / "gist1m").mkdir(parents=True, exist_ok=True)
+ record = {
+ "config": {"query_count": 10},
+ "build": {"cpu_placement": {"allowed_cpus": [0]}},
+ "hardware": {"cpu": cpu},
+ }
+ (directory / "gist1m" / f"{name}.json").write_text(json.dumps(record))
+ sweep = {
+ "status": "single_query_set",
+ "dataset": "gist1m",
+ "query_count": 10,
+ "query_points": [
+ {"setting": {"nprobe": 1}, "recall": 0.9, "one_query_service_s": 0.01}
+ ],
+ "thread_environment": dict.fromkeys(
+ ("OMP_NUM_THREADS", "MKL_NUM_THREADS", "OPENBLAS_NUM_THREADS"), "1"
+ ),
+ "cpu_affinity": [0],
+ }
+ (directory / "sweeps" / "gist1m" / f"{name}.json").write_text(json.dumps(sweep))
+
+
+@pytest.mark.parametrize("cpus", [("CPU A", "CPU A"), ("CPU A", "CPU B")])
+def test_plot_subtitle_uses_the_recorded_cpu(tmp_path, monkeypatch, cpus):
+ monkeypatch.setenv("MPLCONFIGDIR", str(tmp_path))
+ pytest.importorskip("matplotlib")
+ plot_results = importlib.import_module("benchmarks.plot_results")
+ monkeypatch.setattr(plot_results, "validate_record", lambda *_: None)
+ monkeypatch.setattr(plot_results, "settings", lambda _: [{"nprobe": 1}])
+ methods = ("rabitq-ivf-1p4", "faiss-rabitqfs-1p4")
+ for name, cpu in zip(methods, cpus):
+ _write_sweep(tmp_path / "in", name, cpu)
+ output = tmp_path / "out"
+ monkeypatch.setattr(
+ sys,
+ "argv",
+ [
+ "plot_results.py",
+ "--input-dir",
+ str(tmp_path / "in"),
+ "--output-dir",
+ str(output),
+ "--datasets",
+ "gist1m",
+ "--methods",
+ *methods,
+ ],
+ )
+ if cpus[0] != cpus[1]:
+ with pytest.raises(ValueError, match="selected records span CPUs"):
+ plot_results.main()
+ assert not (output / "ivf-gist1m-qps-recall.svg").exists()
+ return
+ plot_results.main()
+ assert "CPU: CPU A" in (output / "ivf-gist1m-qps-recall.svg").read_text()