Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
cmake_minimum_required(VERSION 3.20)

project(RaBitQLib VERSION 0.8.1 LANGUAGES CXX)
project(RaBitQLib VERSION 0.8.2 LANGUAGES CXX)

include(CMakePackageConfigHelpers)
include(GNUInstallDirs)
Expand Down
33 changes: 23 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,26 @@
RaBitQ-Library is a research-backed library for vector quantization and
approximate nearest-neighbor search. See the [research papers](https://vectordb-ntu.github.io/RaBitQ-Library/papers/).

## Why RaBitQ-Library

- **Reference implementation.** Developed with the RaBitQ authors: 1–9-bit codes
with a theoretical error bound, plus indexes and clustering from the same research.
- **Fast to build and search.** Optimized SIMD kernels and batch scoring keep both
indexing and queries fast; see the
[benchmarks](https://vectordb-ntu.github.io/RaBitQ-Library/benchmarks/bench/).
- **Use an index or the quantizer.** Build ready-made indexes in Python or C++,
or link only the core and FastScan components into your own engine, as
[zvec](https://vectordb-ntu.github.io/RaBitQ-Library/integrations/zvec/) does.

## News

- **v0.8.2:** three lines to an index: the first `add` trains, `nbits` defaults to 7
and search settings are optional; faster IVF-RaBitQ `add`
([notes](https://vectordb-ntu.github.io/RaBitQ-Library/releases/0.8.2/)).
- **v0.8.1:** the default `num_threads=0` respects `OMP_NUM_THREADS`, fixing a macOS
crash alongside Faiss and PyTorch ([notes](https://vectordb-ntu.github.io/RaBitQ-Library/releases/0.8.1/)).
- **v0.8.0:** Faiss-style `train`, `add` and `search` for all Python indexes; 0.7 calls
stay as deprecated forms until 0.9 ([notes](https://vectordb-ntu.github.io/RaBitQ-Library/releases/0.8.0/)).
- **v0.7.2:** Python type hints, path-like inputs and clearer input errors
([notes](https://vectordb-ntu.github.io/RaBitQ-Library/releases/0.7.2/)).

Earlier versions: [all releases](https://github.com/VectorDB-NTU/RaBitQ-Library/releases).

Expand Down Expand Up @@ -49,7 +61,7 @@ settings, and limitations.
### Python

```bash
python -m pip install --upgrade "rabitqlib>=0.8.1"
python -m pip install --upgrade "rabitqlib>=0.8.2"
```

Wheels support CPython 3.11–3.14 on Linux x86-64/ARM64, Windows x86-64,
Expand All @@ -67,16 +79,17 @@ rng = np.random.default_rng(42)
data = rng.standard_normal((500, 64)).astype(np.float32)
queries = rng.standard_normal((5, 64)).astype(np.float32)

index = IvfIndex(64, 5, nbits=4, metric="l2")
index.train(data)
index.add(data)
ids, distances = index.search(queries, k=10, nprobe=5)
index = IvfIndex(64, 5, metric="l2")
index.add(data) # the first add learns 5 centroids from data
ids, distances = index.search(queries, k=10, nprobe=5) # probe all 5 clusters
print(ids.shape, distances.shape) # (5, 10) (5, 10)
```

`train` learns the centroids, and `add` quantizes the vectors into their
clusters. IDs identify added rows; distances here estimate squared L2 distance,
with smaller values first. Quantization remains approximate even when all clusters are probed.
The first `add` learns the centroids from the data, then quantizes the vectors
into their clusters. IDs identify added rows; distances here estimate squared L2
distance, with smaller values first. Without `nprobe`, search probes
`num_clusters / 16` clusters (at least one); probe more for higher recall.
Quantization remains approximate even when all clusters are probed.
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. Code written for
0.7 still runs in 0.8 with deprecation warnings; see the
Expand Down
2 changes: 1 addition & 1 deletion benchmarks/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ Results: [IVF](../docs/docs/benchmarks/ivf.md),

From the repository root, after installing the documented system prerequisites
and activating a Python 3.13 environment, these commands measure the active checkout.
Published library results use 0.8.0; each baseline retains its latest measurement.
Published library results use 0.8.2; each baseline retains its latest measurement.
See the [recorded revisions and environments](../docs/docs/benchmarks/bench.md#package-installation).
Keep measurements of different revisions in separate output directories.

Expand Down
61 changes: 38 additions & 23 deletions benchmarks/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,10 +116,34 @@ def build_index(cfg, base, path):
if hasattr(index, "k_factor"):
index.k_factor = cfg["k_factor"]
details = {"faiss_factory": spec}
elif method in ("rabitq-ivf", "rabitq-hnsw"):
from rabitqlib import FinalAssignmentMode, IvfIndex, RaBitQKMeans
elif method == "rabitq-ivf":
from rabitqlib import IvfIndex

# The library's default training: QGKMeans above 2,000 clusters, then add
# routes each vector with the index's centroid initializer.
index = IvfIndex(
dim, cfg["nlist"], nbits=cfg["bits"], metric=cfg["metric"], seed=42
)
cp = index.cp
cp.niter = 10
index.cp = cp
stage_start = time.perf_counter()
index.train(base, num_threads=cfg["threads"])
training_s = time.perf_counter() - stage_start
stage_start = time.perf_counter()
index.add(base, num_threads=cfg["threads"])
indexing_s = time.perf_counter() - stage_start
details = {
"clustering": "IvfIndex.train",
"clustering_seed": 42,
"clustering_niter": 10,
"cluster_count": cfg["nlist"],
"assignment": "add routing",
}
elif method == "rabitq-hnsw":
from rabitqlib import FinalAssignmentMode, HnswIndex, RaBitQKMeans

cluster_count = cfg["nlist"] if method == "rabitq-ivf" else cfg["hnsw_clusters"]
cluster_count = cfg["hnsw_clusters"]
kmeans = RaBitQKMeans(
dim,
cluster_count,
Expand All @@ -134,23 +158,16 @@ def build_index(cfg, base, path):
kmeans.train(base)
training_s = time.perf_counter() - stage_start
stage_start = time.perf_counter()
if method == "rabitq-ivf":
index = IvfIndex(
dim, cluster_count, nbits=cfg["bits"], metric=cfg["metric"]
)
else:
from rabitqlib import HnswIndex

index = HnswIndex(
dim,
nbits=cfg["bits"],
num_clusters=cluster_count,
M=cfg["m"],
ef_construction=cfg["ef_construction"],
metric=cfg["metric"],
seed=42,
)
index.resize(n)
index = HnswIndex(
dim,
nbits=cfg["bits"],
num_clusters=cluster_count,
M=cfg["m"],
ef_construction=cfg["ef_construction"],
metric=cfg["metric"],
seed=42,
)
index.resize(n)
index.set_centroids(kmeans.centroids)
index.add(base, cluster_ids=kmeans.assignments, num_threads=cfg["threads"])
indexing_s = time.perf_counter() - stage_start
Expand Down Expand Up @@ -430,9 +447,7 @@ def main():
if args.method == "rabitq-ivf" and args.metric == "ip":
norms = np.linalg.norm(base, axis=1)
if not np.allclose(norms, 1, atol=1e-4):
parser.error(
"RaBitQKMeans spherical training requires normalized base vectors"
)
parser.error("spherical IVF training requires normalized base vectors")
if args.groundtruth:
if len(base) != total_base:
parser.error("supplied ground truth is invalid for a truncated base")
Expand Down
39 changes: 22 additions & 17 deletions docs/docs/benchmarks/bench.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@ differ; this is neither an exact-recall nor an equal-memory comparison.

| Configuration | Measured Recall@10 | Queries / second | Index GiB | Selected search setting |
| --- | ---: | ---: | ---: | --- |
| IVF-RaBitQ · 5-bit | 92.82% | 1,025 | 0.60 | `nprobe=128` |
| HNSW-RaBitQ · 5-bit | 92.80% | 2,042 | 0.72 | `ef=512` |
| SymphonyQG · 4-bit | 90.71% | 3,062 | 4.39 | `ef=512` |
| IVF-RaBitQ · 5-bit | 93.28% | 976 | 0.60 | `nprobe=128` |
| HNSW-RaBitQ · 5-bit | 92.67% | 2,027 | 0.72 | `ef=512` |
| SymphonyQG · 4-bit | 90.45% | 3,070 | 4.39 | `ef=512` |

These use the 0.8.0 measurements with one search thread and one query per call.
These use the 0.8.2 measurements with one search thread and one query per call.
Each setting has one timing pass. QPS includes Python call overhead after warm-up
and excludes loading. Index size is serialized storage including sidecars, not
resident or peak RAM. The figure and this table are a static export:
Expand Down Expand Up @@ -72,10 +72,10 @@ L2 ground truth for OpenAI.
| CPU | One Intel Xeon Gold 6418H, 24 cores, 48 hardware threads |
| Construction / clustering | 48 threads |
| Query throughput | 1 process, 1 search thread, 1 query per call |
| Platform | Ubuntu 22.04, x86-64; RaBitQ 0.8.0: Linux 6.8.0-138-generic; retained baselines: Linux 6.8.0-84-generic |
| Platform | Ubuntu 22.04, x86-64; RaBitQ 0.8.2: Linux 6.8.0-138-generic; retained baselines: Linux 6.8.0-84-generic |
| Python / NumPy | 3.13.15 / 2.5.2 |
| Compiler / CMake | RaBitQ 0.8.0: GCC 14.3.0 / 3.28.3, Release, native optimization OFF; retained baselines: GCC 13.1.0 / 3.28.3, Release |
| RaBitQ-Library | 0.8.0, measured 2026-11-01 (Asia/Singapore) |
| Compiler / CMake | RaBitQ 0.8.2: GCC 14.3.0 / 3.28.3, Release, native optimization OFF; retained baselines: GCC 13.1.0 / 3.28.3, Release |
| RaBitQ-Library | 0.8.2, measured 2026-11-02 (Asia/Singapore) |
| Baselines | Faiss 1.15.0, SuperKMeans 0.2.0; Intel MKL 2020.4, GNU OpenMP |

## Construction and search settings
Expand All @@ -98,20 +98,24 @@ index loading. Each setting has one timing pass.
Each method has one build or fit per dataset; results do not measure run-to-run
variation.
The committed CSVs support checking the displayed tables and plotted points,
but do not include raw run logs or measurement binaries. The 0.8.0 raw records,
but do not include raw run logs or measurement binaries. The 0.8.2 raw records,
logs, indexes, source/build manifest and validation logs are not published.
Historical baseline raw logs and
binaries are not included. No run-to-run or shared-host noise correction is
applied. The experiments in the cited papers are separate measurements.

## Package installation

The RaBitQ-Library rows and curves were refreshed on 2026-11-01 (Asia/Singapore)
using a wheel built locally from the `v0.8.0` tag
(`4768b74fe9595f87c8298287b034cee5cecd645a`) without local changes. The wheel
SHA-256 was `cb498a7c7aa3a4905dac204e0628daf59c50c6ebddaadf7b54565b87c11b2a03`,
The RaBitQ-Library rows and curves were refreshed on 2026-11-02 (Asia/Singapore)
using a wheel built locally, without local changes, from the 0.8.2 release
candidate `1d49ecd186955cf7ba454fac43bbe11c18a2e25f`, before the version
number was raised, so the wheel and the records report 0.8.1. The wheel
SHA-256 was `4555497f3b6170b295e0f3a7dcde766437eb95216f5f268e9311258824b42792`,
and the measured extension SHA-256 was
`a375e636bb0bf89d07039085bfab866fdab9c790dca5189388792d7dd5427b40`.
`12ddcc2b03525728474d261e74df147d372d0d122ff7dd2784d3e6544fd2ceb1`.
IVF-RaBitQ results now follow the library's default construction
(`IvfIndex.train` with 10 iterations, then `add`); 0.8.0 results ran
RaBitQKMeans with exact final assignment and passed the cluster IDs to `add`.

The latest available Faiss and SuperKMeans measurements are retained unchanged from
snapshot `a332c1ce97588aaed4f6a8a5476f84a81f947313`; they were not rerun.
Expand All @@ -124,7 +128,7 @@ To use the measured library source, start in a separate checkout and
activate your benchmark Python environment:

```bash
git clone --branch v0.8.0 --depth 1 https://github.com/VectorDB-NTU/RaBitQ-Library.git RaBitQ-benchmark-snapshot
git clone --branch v0.8.2 --depth 1 https://github.com/VectorDB-NTU/RaBitQ-Library.git RaBitQ-benchmark-snapshot
cd RaBitQ-benchmark-snapshot
```

Expand All @@ -151,14 +155,15 @@ and verification records are saved in `build/benchmark-baselines/`.
Use `--dry-run` to check prerequisites and print the installation commands
without running them. The installer defaults to the historical GCC 13 toolchain
and builds RaBitQ with `RABITQ_ENABLE_NATIVE_OPTIMIZATION=ON`; it does not
recreate the GCC 14.3.0, native-optimization-OFF wheel used for the 0.8.0
measurements. That wheel was built and installed in the same environment with:
recreate the GCC 14.3.0, native-optimization-OFF wheel used for the 0.8.2
measurements. From a v0.8.2 checkout, the equivalent build, installed in the same
environment, is:

```bash
python -m pip wheel . --no-build-isolation --no-deps -w build/benchmark-wheel \
-Ccmake.define.CMAKE_CXX_COMPILER=g++-14 -Ccmake.define.CMAKE_BUILD_TYPE=Release \
-Ccmake.define.RABITQ_ENABLE_NATIVE_OPTIMIZATION=OFF
python -m pip install --no-deps --force-reinstall build/benchmark-wheel/rabitqlib-0.8.0-*.whl
python -m pip install --no-deps --force-reinstall build/benchmark-wheel/rabitqlib-0.8.2-*.whl
```

Match the recorded compiler and native-optimization setting when comparing new
Expand Down
Loading
Loading