Repository navigation
Add RaBitQ-Library client with HNSW-RaBitQ index - #900
Open
TamimEhsan wants to merge 5 commits into
Open
TamimEhsan wants to merge 5 commits into
TamimEhsan wants to merge 5 commits into
Conversation
|
[APPROVALNOTIFIER] This PR is NOT APPROVED This pull-request has been approved by: TamimEhsan The full list of commands accepted by this bot can be found here. DetailsNeeds approval from an approver in each of these files:Approvers can indicate their approval by writing |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This PR adds an embedded client for RaBitQ-Library (
rabitqlib) and its first index type, HNSW-RaBitQ. HNSW-RaBitQ searches an HNSW graph over RaBitQ-quantized codes. The library runs in-process, like the existingzvecandqdrantlocalclients.How it fits the benchmark's process model
VectorDBBench loads, optimizes and searches in separate processes. An in-process library therefore has to keep all its state in files under
--path:Load:
insert_embeddingsappends each batch to a raw buffer (buffer.vectors.f32andbuffer.ids.i64). The two files stay row-aligned because appends hold a lock, which matters because up to 4 insert threads share one client.Optimize: the whole buffer goes into one
HnswIndex.add. The library trains its centroids inside that call, over the full dataset. The client then saves three things:The manifest is written last, so its presence marks a complete build. All three files are named after the index type (
rabitqlib_hnsw.*), so later index types can share a path.Search: each search process loads the saved index when it unpickles the client (
__setstate__). This happens before the concurrent runner's start barrier. Loading ininit()would happen after the barrier and count the load time as search time. On GIST1M at concurrency 32, that made reported QPS 23K when the real figure was 55K.Behaviour and limits
supported_filter_types = [NonFilter]and filtered cases are rejected up front. Vectors inserted after the build return aNonRetryableInsertErrorrather than being silently lost.need_normalize_cosinereturns True).--skip-drop-old --skip-load: the client compares the manifest against the requested parameters. Only--ef-searchmay differ from the build. If any build parameter differs, the client refuses the index instead of reporting results for a different index.drop_olddeletes only the buffer and this index type's files, never the rest of the directory.num_threads=1), to match the other clients' per-process concurrency.Options
--path--nbits--num-clusters--m--ef-construction--ef-searchkwhen smaller--seed--build-threadsOMP_NUM_THREADSChanges
vectordb_bench/backend/clients/rabitqlib/: new client, configs and CLI command.backend/clients/__init__.py,api.py: registerDB.RaBitQLibandIndexType.RABITQLIB_HNSW.cli/vectordbbench.py: register therabitqlibhnswcommand.pyproject.toml: newrabitqliboptional dependency.tests/test_rabitqlib.py: 20 tests. They cover:init;drop_oldleaving unrelated files alone.README.md: install row and a usage section.Benchmark: GIST1M (960-d, L2, k=100)
The baseline is Zvec fp32 HNSW, built with the same graph parameters as the RaBitQ index (M=32, ef_construction=200). Both are embedded clients, so they ran under the same process model on the same machine. Machine: 2× Intel Xeon Gold 6418H (96 threads), 1 TB RAM.
RaBitQ: nbits=9, num_clusters=16. RaBitQ built in 106 s; Zvec took about 2 min.
At equal
ef_searchthe two reach the same recall, as expected for the same graph parameters. At equal recall, RaBitQ gives 3.6–7.4× the QPS with about a third of the memory.To check the client itself, I queried the saved index directly through
rabitqliband scored it against the GIST ground truth. Recall matched VectorDBBench's to four decimals (0.8266 / 0.9124 / 0.9582).To reproduce, convert GIST1M to a custom parquet dataset and run:
Test plan
pytest tests/test_rabitqlib.py: 20 passedruff check/ruff format --checkon the changed filesPerformance1536D50K): recall 0.9538; QPS 3,385 at c=1 and 11,010 at c=4