diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..b739931 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,56 @@ +name: Documentation + +on: + push: + branches: ["main"] + paths: + - "docs/**" + - ".github/workflows/docs.yml" + pull_request: + paths: + - "docs/**" + - ".github/workflows/docs.yml" + workflow_dispatch: + +concurrency: + group: documentation-${{ github.ref }} + cancel-in-progress: true + +jobs: + build: + name: Build documentation + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.11" + cache: pip + cache-dependency-path: docs/requirements.txt + - name: Install documentation dependencies + run: python -m pip install -r docs/requirements.txt + - name: Build documentation + run: mkdocs build --strict --config-file docs/mkdocs.yml + + deploy: + name: Deploy documentation + if: github.event_name == 'push' + needs: build + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + contents: write + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: actions/setup-python@v5 + with: + python-version: "3.11" + cache: pip + cache-dependency-path: docs/requirements.txt + - name: Install documentation dependencies + run: python -m pip install -r docs/requirements.txt + - name: Deploy to GitHub Pages + run: mkdocs gh-deploy --force --config-file docs/mkdocs.yml diff --git a/README.md b/README.md index 4a6a32c..74bfb39 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,8 @@ vector quantization and approximate nearest-neighbor search. It provides: [multi-bit](https://arxiv.org/abs/2409.09913) RaBitQ quantizers; - IVF, HNSW, and [SymphonyQG](https://dl.acm.org/doi/abs/10.1145/3709730) indexes powered by RaBitQ; -- Euclidean distance, inner product, and cosine similarity estimation; and +- Euclidean distance and inner-product search (cosine search is + available by normalizing vectors before using inner product); and - optimized AVX2 and AVX-512 kernels with runtime CPU dispatch. RaBitQ is developed by the @@ -29,11 +30,14 @@ University, Singapore. A GPU implementation is also available in - a C++17 compiler - CMake 3.15 or newer - OpenMP -- an x86-64 CPU with AVX2 and FMA support +- an x86-64 CPU supported by the selected kernels: most paths accept either + AVX2 with FMA or AVX-512F/BW/DQ with FMA -AVX2 and FMA are the baseline SIMD requirements. On CPUs that also support -AVX-512F, AVX-512BW, and AVX-512DQ, RaBitQ selects its AVX-512 kernels at -runtime. AVX-512 VPOPCNTDQ provides additional acceleration when available. +Most SIMD entry points select AVX-512 kernels when AVX-512F, AVX-512BW, and +AVX-512DQ are detected; otherwise they use AVX2 when AVX2 and FMA are +available. AVX-512 VPOPCNTDQ enables additional popcount kernels. The HNSW +AVX-512 core path also checks for AVX2 and FMA, and otherwise uses its AVX2 +path when available. AVX-512 translation units are compiled with FMA enabled. On Ubuntu or Debian, install the system build tools and then install RaBitQ from the repository: @@ -88,11 +92,14 @@ persistence. - CMake 3.10 or newer - a C++17 compiler with OpenMP support -- an x86-64 CPU with AVX2 and FMA support +- an x86-64 CPU supported by the selected kernels: most paths accept either + AVX2 with FMA or AVX-512F/BW/DQ with FMA -AVX2 and FMA are the baseline SIMD requirements. On CPUs that also support -AVX-512F, AVX-512BW, and AVX-512DQ, RaBitQ selects its AVX-512 kernels at -runtime. AVX-512 VPOPCNTDQ provides additional acceleration when available. +Most SIMD entry points select AVX-512 kernels when AVX-512F, AVX-512BW, and +AVX-512DQ are detected; otherwise they use AVX2 when AVX2 and FMA are +available. AVX-512 VPOPCNTDQ enables additional popcount kernels. The HNSW +AVX-512 core path also checks for AVX2 and FMA, and otherwise uses its AVX2 +path when available. AVX-512 translation units are compiled with FMA enabled. Clone and build the library and example programs: @@ -134,15 +141,18 @@ the build, formatting, pre-commit, and static-analysis workflows. ## Why RaBitQ? -- **High accuracy with tiny codes.** RaBitQ provides state-of-the-art - similarity estimation across different bit widths and remains effective at - one bit per dimension. -- **Fast distance estimation.** Its kernels use bitwise operations and - [FastScan](https://arxiv.org/abs/1704.07355) for efficient search. +- **High accuracy with tiny codes.** RaBitQ provides state-of-the-art similarity + estimation across different bit widths and remains effective with a + one-bit code per padded dimension plus per-vector factors. +- **Fast distance estimation.** IVF and SymphonyQG use + [FastScan](https://arxiv.org/abs/1704.07355) for batched estimates, while + HNSW uses single-code AVX2 or AVX-512 kernels. - **Theoretical error bounds.** RaBitQ provides an asymptotically optimal error bound that can support reliable ordering and reranking. -- **Multiple index trade-offs.** IVF and HNSW prioritize memory efficiency, - while SymphonyQG uses additional memory to optimize query performance. +- **Multiple index trade-offs.** IVF stores quantized codes without the raw + dataset. HNSW adds graph links but also searches from quantized codes. + SymphonyQG retains raw vectors and stores per-neighborhood quantization data + to improve its access pattern. In typical workloads, 4-bit, 5-bit, and 7-bit quantization can achieve roughly 90%, 95%, and 99% recall, respectively, without reranking. Actual results diff --git a/docs/docs/compact_code.md b/docs/docs/compact_code.md index 7c00d7a..dc54bfd 100644 --- a/docs/docs/compact_code.md +++ b/docs/docs/compact_code.md @@ -39,7 +39,7 @@ The code sequentially stores the binary value for each of the 64 dimensions. ![2-bit Storage](assets/img/compact/2bit.png){ width=300px align=center } -The code is stored in a byte array of length 16. Each row in the figure represents a byte. The 0-th byte stores the 2-bit codes of the 0-th, 16-th, 32-th and 48-th dimensions. The 1-th byte stores the 2-bit codes of the 1-th, 17-th, 33-th and 49-th dimensions, so on and so forth. This storage allows efficient unpacking with SIMD, i.e., shifting and masking with `SSE`. +The code is stored in a byte array of length 16. Each row in the figure represents a byte. The 0-th byte stores the 2-bit codes of the 0-th, 16-th, 32-th and 48-th dimensions. The 1-th byte stores the 2-bit codes of the 1-th, 17-th, 33-th and 49-th dimensions, and so on. This layout supports efficient shifting and masking in the library's AVX2 and AVX-512 unpacking kernels. ## 3-bit = 2-bit + 1-bit @@ -48,7 +48,7 @@ The code is stored in a byte array of length 16. Each row in the figure represen ![4-bit Storage](assets/img/compact/4bit.png){ width=600px align=center } -The code is stored in a byte array of length 32. The 0-th byte stores the 4-bit codes of the 0-th and 16-th dimensions. The 1-th byte stores the 4-bit codes of the 1-th and 17-th dimensions, so on and so forth. This storage allows efficient unpacking with SIMD, i.e., shifting and masking with `SSE`. +The code is stored in a byte array of length 32. The 0-th byte stores the 4-bit codes of the 0-th and 16-th dimensions. The 1-th byte stores the 4-bit codes of the 1-th and 17-th dimensions, and so on. This layout supports efficient shifting and masking in the library's AVX2 and AVX-512 unpacking kernels. ## 5-bit = 4-bit + 1-bit @@ -62,7 +62,8 @@ The code is stored in a byte array of length 48. - The second 16 bytes store the 6-bit codes of the 16-th to the 31-th dimensions and the upper 2-bit codes of the 48-th to 63-th dimensions. - The third 16 bytes store the lower 4-bit codes of the 32-th to 47-th dimensions and the lower 4-bit codes of the 48-th to 63-th dimensions. -This storage allows efficient unpacking with SIMD, i.e., shifting and masking with `SSE`. +This layout supports efficient shifting and masking in the library's AVX2 and +AVX-512 unpacking kernels. ## 7-bit = 6-bit + 1-bit @@ -74,4 +75,3 @@ The code of 8-bit is aligned with byte arrays and needs no specialized design. - diff --git a/docs/docs/index.md b/docs/docs/index.md index a41ef52..ff8b4ab 100644 --- a/docs/docs/index.md +++ b/docs/docs/index.md @@ -1,54 +1,130 @@ -# Welcome to The RaBitQ Library - -The RaBitQ Library provides efficient and lightweight implementations of the RaBitQ quantization algorithm ([1-bit version](https://arxiv.org/abs/2405.12497) and [multi-bit version](https://arxiv.org/abs/2409.09913)) and its applications in high-dimensional vector search. The core algorithm RaBitQ is based on the research from [VectorDB group](https://vectordb-ntu.github.io/) at Nanyang Technological University, Singapore. - -The library provides the following key features: - -* **RaBitQ** - a vector quantization algorithm as a drop-in replacement of binary and scalar quantization, offering an optimal theoretical error bound -* **RaBitQ for Vector Search** - a reference implementation of RaBitQ's combination with popular vector search indexes - -The RaBitQ Library supports estimating similarity metrics including Euclidean distance, inner product and cosine similarity. - -## RaBitQ - -RaBitQ is a vector quantization algorithm as a drop-in replacement of binary and scalar quantization. The key advantages of RaBitQ include - -- **High Accuracy with Tiny Space** - RaBitQ achieves the state-of-the-art accuracy under diverse bit-width for the estimation of similarity metrics. It produces promising accuracy with even **1-bit per dimension**. -- **Fast Distance Estimation** - RaBitQ supports to estimate the similarity metrics with high efficiency based on bitwise operations or [FastScan](https://arxiv.org/abs/1704.07355). -- **Theoretical Error Bound** - RaBitQ provides an asymptotically optimal error bound for the estimation of distances and inner product. The error bound can be used for reliable ordering and reranking. - -In this library, we provide simple interfaces to support advanced features of RaBitQ. The details are presented in [RaBitQ](rabitq/rabitq.md). - -## RaBitQ for Vector Search - In the library, RaBitQ is combined with [IVF](index/ivf.md), [HNSW](index/hnsw.md) and [QG](index/qg.md) to deliever different trade-offs among time, space and accuracy. - -Using RaBitQ with IVF and HNSW targets a balance between memory consumption and query performance. Only the quantization codes produced by RaBitQ are stored and the raw data vectors are not accessed during querying. Thus, these methods consume less memory than the raw dataset. -Using **4-bit, 5-bit and 7-bit** quantization usually suffices to produce **90%, 95% and 99% recall** respectively without reranking. - -Using RaBitQ with QG targets the best query performance by using more memory. It creates multiple quantization codes for every vector to optimize the data access pattern. Thus, QG usually consumes 2x memory of the raw dataset. - - -## RaBitQ in Industry - -The RaBitQ algorithm has been implemented in many real-world systems in industry including - -- [Milvus](https://github.com/milvus-io/milvus) - IVF + RaBitQ (C++) -- [Faiss](https://github.com/facebookresearch/faiss) - IVF + RaBitQ (C++) -- [VSAG](https://github.com/antgroup/vsag) - HGraph + RaBitQ (C++) -- [VectorChord](https://github.com/tensorchord/VectorChord) - IVF + RaBitQ (Rust) -- [Volcengine OpenSearch](https://www.volcengine.com/docs/6465/1553583) - DiskANN + RaBitQ -- [CockroachDB](https://github.com/cockroachdb/cockroach) - CSPANN + RaBitQ (Golang) -- [ElasticSearch](https://github.com/elastic/elasticsearch) - HNSW + RaBitQ (Java - the algorithm is adopted with some minor modifications and renamed as "BBQ") -- [Lucene](https://github.com/apache/lucene) - HNSW + RaBitQ (Java - the algorithm is adopted with some minor modifications and renamed as "BBQ") - -## Acknowledgement - -We acknowledge Alexandr Guzhva, Li Liu, Chao Gao, Silu Huang, Jiabao Jin, Xiaoyao Zhong and Jinjing Zhou for valuable feedbacks. - - -## Reference -Please provide a reference of our paper if it helps in your systems or research projects. - -
-Jianyang Gao, Yutong Gou, Yuexuan Xu, Yongyi Yang, Cheng Long, Raymond Chi-Wing Wong, "Practical and Asymptotically Optimal Quantization of High-Dimensional Vectors in Euclidean Space for Approximate Nearest Neighbor Search", SIGMOD 2025, available at https://arxiv.org/abs/2409.09913
-
+# Compact vector search with RaBitQ + +
+ +**RaBitQ Library** is a C++17 library with Python bindings for compact, +accurate vector quantization and approximate nearest-neighbor search. + +Build with the low-level quantizer or use complete IVF, HNSW, and SymphonyQG +indexes backed by optimized AVX2 and AVX-512 kernels. + +[Get started](quick_start.md){ .md-button .md-button--primary } +[View on GitHub](https://github.com/VectorDB-NTU/RaBitQ-Library){ .md-button } +[Install from PyPI](https://pypi.org/project/rabitqlib/){ .md-button } + +
+ +
+
+

Compact by design

+

Use RaBitQ as an alternative to binary or scalar quantization, with + useful estimates from a one-bit code per padded dimension plus a small + set of per-vector factors.

+
+
+

Fast on modern CPUs

+

Runtime dispatch selects optimized AVX2 or AVX-512 kernels. IVF and + SymphonyQG use FastScan for batched distance estimation.

+
+
+

Ready for vector search

+

Choose IVF, HNSW, or SymphonyQG to balance memory, indexing cost, + latency, and recall for your workload.

+
+
+ +## Start with Python + +Install the latest release from PyPI: + +```bash +python -m pip install rabitqlib +``` + +Build an IVF index and search a batch of queries: + +```python +import numpy as np +from rabitqlib import IvfIndex + +rng = np.random.default_rng(42) +data = rng.standard_normal((500, 64)).astype(np.float32) +queries = rng.standard_normal((5, 64)).astype(np.float32) + +cluster_ids = (np.arange(len(data)) % 5).astype(np.uint32) +centroids = np.stack( + [data[cluster_ids == cluster].mean(axis=0) for cluster in range(5)] +).astype(np.float32) + +index = IvfIndex( + dim=64, + max_elements=len(data), + num_clusters=5, + nbits=4, + metric="l2", +) +index.build(data, centroids, cluster_ids) + +ids, distances = index.search(queries, k=10, nprobe=5) +print(ids.shape, distances.shape) # (5, 10) (5, 10) +``` + +[Continue to the complete quick start](quick_start.md){ .md-button .md-button--primary } + +## Choose an index + +| Index | Best fit | Typical relative memory | Main search control | +| --- | --- | --- | --- | +| [IVF + RaBitQ](index/ivf.md) | Large datasets and predictable memory use | Lowest | Number of probed clusters | +| [HNSW + RaBitQ](index/hnsw.md) | General-purpose graph search | Moderate | Search candidate list size | +| [SymphonyQG](index/qg.md) | Latency-focused graph search | Highest | Search window size | + +IVF and HNSW store quantized vectors instead of accessing raw vectors during +search. SymphonyQG uses additional memory and multiple codes per vector to +optimize its access pattern. + +These are typical relative profiles, not fixed guarantees. Actual memory, +latency, and recall depend on vector dimension, quantization width, graph +degree, and search parameters. + +## Why RaBitQ? + +- **High accuracy with tiny codes.** RaBitQ provides strong similarity + estimates across different bit widths and remains effective with a one-bit + code per padded dimension plus per-vector factors. +- **Fast distance estimation.** IVF and SymphonyQG use + [FastScan](https://arxiv.org/abs/1704.07355) for batched estimates; HNSW + uses single-code AVX kernels. +- **Theoretical error bounds.** An asymptotically optimal error bound supports + reliable ordering and reranking. +- **Multiple integration points.** Use the quantizer directly or select a + complete vector-search index. + +The library supports Euclidean distance and inner product. Cosine similarity +can be implemented by normalizing vectors and using inner product. +It implements the [1-bit RaBitQ](https://arxiv.org/abs/2405.12497) and +[multi-bit RaBitQ](https://arxiv.org/abs/2409.09913) research from the +[VectorDB Group](https://vectordb-ntu.github.io/) at Nanyang Technological +University. + +## Used across the vector-search ecosystem + +RaBitQ has been adopted by projects including +[Milvus](https://github.com/milvus-io/milvus), +[Faiss](https://github.com/facebookresearch/faiss), +[VSAG](https://github.com/antgroup/vsag), +[VectorChord](https://github.com/tensorchord/VectorChord), +[CockroachDB](https://github.com/cockroachdb/cockroach), +[Elasticsearch](https://github.com/elastic/elasticsearch), +[Lucene](https://github.com/apache/lucene), +[turbopuffer](https://turbopuffer.com/blog/ann-v3), and +[Zvec](https://github.com/alibaba/zvec). + +## Citation + +If RaBitQ helps your research or system, please cite: + +> Jianyang Gao, Yutong Gou, Yuexuan Xu, Yongyi Yang, Cheng Long, and Raymond +> Chi-Wing Wong. “Practical and Asymptotically Optimal Quantization of +> High-Dimensional Vectors in Euclidean Space for Approximate Nearest Neighbor +> Search.” SIGMOD 2025. [arXiv:2409.09913](https://arxiv.org/abs/2409.09913). diff --git a/docs/docs/index/hnsw.md b/docs/docs/index/hnsw.md index 86601ac..1ecb6af 100644 --- a/docs/docs/index/hnsw.md +++ b/docs/docs/index/hnsw.md @@ -1,10 +1,18 @@ # HNSW + RaBitQ -[HNSW](https://arxiv.org/abs/1603.09320) is a popular graph-based index. HNSW + RaBitQ consumes the more memory than IVF + RaBitQ because it needs to store the edges of every vertex in a graph (e.g., 32 edges = 1,024 bits). In terms of the time-accuracy trade-off, HNSW + RaBitQ and IVF + RaBitQ perform differently across datasets—sometimes the former works better, and sometimes the latter does. +[HNSW](https://arxiv.org/abs/1603.09320) is a popular graph-based index. Under +comparable quantization settings, HNSW + RaBitQ typically uses more memory +than IVF + RaBitQ because it stores graph structures in addition to quantized +vectors. The actual comparison depends on vector dimension, bit width, graph +degree, and index parameters. This document describes how the library integrates HNSW with RaBitQ to support efficient vector search. ## Index Construction -We build the HNSW graph by incrementally inserting new elements following the standard HNSW routine. Currently, we support building the index using raw data vectors and storing the corresponding quantization codes. +The graph is built by incrementally inserting raw data vectors. Raw vectors +are used to construct and prune graph links, but the completed index stores +the links, centroids, cluster IDs, labels, RaBitQ codes, and factors rather +than a copy of the raw dataset. Total bit widths from 1 through 9 are +supported. Users can invoke: @@ -21,9 +29,9 @@ HierarchicalNSW::construct(size_t cluster_num, - **data**: Pointer to the raw data vectors. - **data_num**: The number of data vectors. - **centroids**: Centroids computed by K-means clustering on the raw data vectors (we recommend `cluster_num = 16`). -- **cluster_ids**: Array of length `data_num` where each entry indicates the centroid ID (0–15) for the corresponding data vector. +- **cluster_ids**: Array of length `data_num`; every entry must be in the range `[0, cluster_num)`. - **num_threads**: Number of threads to use (default: 0, which auto-selects). -- **faster**: If `true`, enales fast quantizer. +- **faster**: If `true`, enables the faster quantizer. During construction, we first rotate the centroids and then insert each element one by one. For each element: @@ -90,4 +98,3 @@ Repeat until `candidate_set` is empty: - Insert the neighbor into `candidate_set` with its (possibly refined) estimated distance. The search terminates when `candidate_set` is empty. - diff --git a/docs/docs/index/ivf.md b/docs/docs/index/ivf.md index d5dbd94..d993d14 100644 --- a/docs/docs/index/ivf.md +++ b/docs/docs/index/ivf.md @@ -1,6 +1,11 @@ # IVF + RaBitQ -[IVF](https://dl.acm.org/doi/10.1109/TPAMI.2010.57) is a classical clustering-based method for ANN. It has tiny space consumption. When it is combined with RaBitQ and [FastScan](https://dl.acm.org/doi/abs/10.1145/3078971.3078992), it produces promising time-accuracy trade-off for vector search. This part describes how the library combines IVF with RaBitQ. +[IVF](https://dl.acm.org/doi/10.1109/TPAMI.2010.57) is a classical +clustering-based ANN method. This implementation stores RaBitQ codes and +vector IDs rather than retaining the raw dataset after construction, and uses +[FastScan](https://dl.acm.org/doi/abs/10.1145/3078971.3078992) to estimate a +batch of distances. Its actual memory, latency, and recall depend on the bit +width, number of clusters, and `nprobe`. The algorithm includes two phases: indexing and querying. @@ -39,8 +44,9 @@ rabitqlib::load_vecs(centroids_file, centroids); rabitqlib::load_vecs(cids_file, cids); ``` -Then, you need to initialize an IVF object using (1) the number of data points, (2) the dimension of each vector, (3) the number of clusters -, and (4) the total bits used to quantize each vector. For example: +Then initialize an IVF object with the number of data points, vector +dimension, number of clusters, and total bits per dimension. The C++ index +accepts total bit widths from 1 through 9. ```c++ using index_type = rabitqlib::ivf::IVF; @@ -58,14 +64,16 @@ void IVF::construct( const float* data, const float* centroids, const PID* cluster_ids, - bool faster = false + bool faster = false, + size_t num_threads = std::numeric_limits::max() ); ``` - **data**: Pointer to the raw data vectors. - **centroids**: Centroids computed by K-means clustering on the raw data vectors (we recommend to tune cluster_num around 4 * the square root of the dataset following Faiss). -- **cluster_ids**: Array of length data_num where each entry indicates the centroid ID (0–15) for the corresponding data vector. +- **cluster_ids**: Array of length `data_num`; every entry must be in the range `[0, cluster_num)`. - **faster**: If true, enable fast implementations for RaBitQ (By default, it is set as `false` to pursue better accuracy.). +- **num_threads**: Maximum number of OpenMP threads used to quantize clusters. For example: ```c++ @@ -75,6 +83,8 @@ ivf.construct(data.data(), centroids.data(), cids.data(), true); During the construction phase, we quantize each cluster in parallel. For each cluster, we first rotate the centroid and vectors in this cluster using a random matrix, then compute the 1-bit codes and (total_bits - 1)-bit ex codes along with corresponding factors. +The raw `data` input is used during construction but is not stored in the +finished IVF index. After construction, you can directly save the index file to disk: ```c++ diff --git a/docs/docs/index/qg.md b/docs/docs/index/qg.md index fac4772..f215cce 100644 --- a/docs/docs/index/qg.md +++ b/docs/docs/index/qg.md @@ -1,9 +1,17 @@ # QG + RaBitQ (SymphonyQG) -[QG](https://medium.com/@masajiro.iwasaki/fusion-of-graph-based-indexing-and-product-quantization-for-ann-search-7d1f0336d0d0) is a graph-based index originated from the [NGT library](https://github.com/yahoojapan/NGT). Different from HNSW, it creates multiple quantization codes for every vector and carefully re-organizes their layout to minimize random memory accesses in querying. RaBitQ + QG in developped from our research project [SymphonyQG](https://dl.acm.org/doi/10.1145/3709730). Unlike IVF + RaBitQ and HNSW + RaBitQ, which consumes less memory than the raw datasets, RaBitQ + QG consumes more memory to pursue the best time-accuracy trade-off. -Here, we offer a toy example for the indexing and querying of QG. -To test QG on real-world datasets, please refer to `sample/symqg_indexing.cpp` and `sample/symqg_querying.cpp` for -detailed information +[QG](https://medium.com/@masajiro.iwasaki/fusion-of-graph-based-indexing-and-product-quantization-for-ann-search-7d1f0336d0d0) +is a graph-based index originating from the +[NGT library](https://github.com/yahoojapan/NGT). This implementation comes +from the [SymphonyQG](https://dl.acm.org/doi/abs/10.1145/3709730) project. For +each vertex it stores the raw vector, a fixed-size neighbor list, and batched +one-bit RaBitQ data for those neighbors. This layout uses more memory than the +raw vectors alone, but lets graph traversal estimate a group of neighbor +distances with FastScan while computing exact distances for visited vertices. + +Memory and performance depend on the dimension, degree, build window, and +search window. See `sample/cpp/symqg_indexing.cpp` and +`sample/cpp/symqg_querying.cpp` for complete programs. ## Index Construction @@ -16,7 +24,8 @@ QuantizedGraph::QuantizedGraph( size_t num, size_t dim, size_t max_deg, - RotatorType type = RotatorType::FhtKacRotator + MetricType metric_type = METRIC_L2, + RotatorType rotator_type = RotatorType::FhtKacRotator ); QGBuilder::QGBuilder( @@ -37,20 +46,20 @@ QGBuilder::QGBuilder( size_t rows = 1000000; size_t cols = 128; size_t degree = 32; -size_t ef = 200 +size_t ef = 200; -float* data = new float[rows * cols]; // only for illustration +std::vector data(rows * cols); // populate with the dataset -QuantizedGraph qg(rows, cols, degree); // init qg +QuantizedGraph qg(rows, cols, degree); -QGBuilder builder(qg, ef, data.data()); // init builder +QGBuilder builder(qg, ef, data.data()); ``` Then, we can use the builder to construct the index. Then we can save the index. ```cpp builder.build(); // build index interatively -const char* index_file = "./qg_example.index" +const char* index_file = "./qg_example.index"; qg.save(index_file); // save index ``` @@ -63,6 +72,10 @@ Each indexed element is stored in the following layout. [Edges] ``` +`Batch data for QG` contains one-bit codes and estimator factors for the +element's neighbors, organized in FastScan batches of 32. Consequently, +`max_deg` must be a multiple of 32. + ## Querying For querying, code is pretty simple. @@ -84,8 +97,8 @@ qg.load("./qg_example.index"); // load pre-constructed index size_t ef = 100; size_t topk = 10; std::vector results(topk); // result buffer -float* query = new float[cols]; // query vector (only for illustration) +std::vector query(cols); // populate with a query vector qg.set_ef(ef); // set search window size -qg.search(query, topk, results.data()); // search knn, result will be stored in results -``` \ No newline at end of file +qg.search(query.data(), topk, results.data()); +``` diff --git a/docs/docs/kernel_ip.md b/docs/docs/kernel_ip.md index a192590..7d17be6 100644 --- a/docs/docs/kernel_ip.md +++ b/docs/docs/kernel_ip.md @@ -1,7 +1,8 @@ # Kernel - Inner Product - -> The implementation of computing inner product between binary codes and query vectors heavily affects the efficiency. The best implementation may vary largely across platforms and the dimensionality of datasets. For now, we only include the implementation with `__builtin_popcountll` and expect that compilers will automatically vectorize it. +> Inner-product kernels are selected at runtime. The library provides AVX2/FMA +> and AVX-512 implementations; AVX-512 VPOPCNTDQ is used by popcount-specific +> paths when available. This part introduces how to compute the inner product between quantization codes and rotated query vectors i.e., $\left< \mathbf{x}_0,\mathbf{q}_r'\right>$ and $\left< \mathbf{x}_u,\mathbf{q}_r'\right>$. The implementation includes two types: @@ -48,4 +49,7 @@ $$ ## The Kernel for Multi-bit Codes -For the multi-bit codes, we convert the unsigned integer codes to floating point numbers with native instructions of AVX512. \ No newline at end of file +For multi-bit codes, the packed unsigned values are unpacked and accumulated +by the selected AVX2/FMA or AVX-512 kernel. The dispatch table supports zero +through eight extended bits; full RaBitQ codes therefore contain one through +nine bits per dimension. diff --git a/docs/docs/quick_start.md b/docs/docs/quick_start.md index e49069a..dd16098 100644 --- a/docs/docs/quick_start.md +++ b/docs/docs/quick_start.md @@ -1,638 +1,120 @@ # Quick Start -## RaBitQ Quantizer +RaBitQ Library provides Python bindings for complete vector-search indexes and +a C++17 API for both indexes and low-level quantization. -The RaBitQ Library provides simple interfaces, making it a drop-in replacement for scalar and binary quantization. -The interface offers two underlying RaBitQ implementations: one delivers optimal accuracy with longer quantization time, and another delivers near-optimal accuracy with significantly faster quantization. +## Requirements -The library provides advanced data formats to support efficient distance estimation. The details can be found in [Quantizer](rabitq/quantizer.md). +- An x86-64 CPU supported by the selected kernels: most paths accept either + AVX2 with FMA or AVX-512F/BW/DQ with FMA +- Python 3.9 or newer for the Python package +- A C++17 compiler with OpenMP support +- CMake 3.15 or newer for Python builds, or CMake 3.10 or newer for C++ builds -### Example Code in C++ -```cpp -#include -#include +Most SIMD entry points select AVX-512 kernels when AVX-512F, AVX-512BW, and +AVX-512DQ are detected; otherwise they use AVX2 when AVX2 and FMA are +available. AVX-512 VPOPCNTDQ enables additional popcount kernels. The HNSW +AVX-512 core path also checks for AVX2 and FMA, and otherwise uses its AVX2 +path when available. AVX-512 translation units are compiled with FMA enabled. -#include "quantization/rabitq.hpp" +## Python -int main() { - size_t dim = 128; +### Install - std::vector vector(dim); - static std::random_device rd; - static std::mt19937 gen(rd()); - std::normal_distribution dist(0, 1); +The PyPI package currently builds the native extension during installation. +On Ubuntu or Debian, install the build tools first: - // generate a random vector - for (size_t i = 0; i < dim; ++i) { - vector[i] = dist(gen); - } - - size_t bits = 8; // num of bits for total code - std::vector code(dim); // code - float delta; // delta for scalar quantization - float vl; // lower value for scalar quantization - - // scalar quantization - rabitqlib::quant::quantize_scalar(vector.data(), dim, bits, code.data(), delta, vl); - - // faster version, must init a config struct first - rabitqlib::quant::RabitqConfig config = rabitqlib::quant::faster_config(dim, bits); - rabitqlib::quant::quantize_scalar( - vector.data(), dim, bits, code.data(), delta, vl, config - ); - - // reconstruct - size_t padded_dim = dim; - std::vector reconstructed_data(padded_dim); - rabitqlib::quant::reconstruct_vec(code.data(), delta, vl, padded_dim, reconstructed_data.data()); - - return 0; -} -``` - - -## RaBitQ + IVF -[IVF](https://dl.acm.org/doi/10.1109/TPAMI.2010.57) is a classical clustering-based ANN index. IVF + RaBitQ offers the lowest memory usage compared to IVF, HNSW and QG. Powered by [FastScan](https://arxiv.org/abs/1704.07355), it also provides a promising time-accuracy trade-off. To use RaBitQ + IVF, first cluster the raw vectors (e.g., via KMeans), then quantize each cluster and build the IVF index. Below is an example of using RaBitQ + IVF for ANN search on the Deep1M dataset. - -### Dataset downloading and clustering -Use the following shell commands to download the Deep1M dataset, generate clustering information, and save it to disk. -```shell -wget http://www.cse.cuhk.edu.hk/systems/hash/gqr/dataset/deep1M.tar.gz -tar -zxvf deep1M.tar.gz -python python/ivf.py deep1M/deep1M_base.fvecs 4096 deep1M/deep1M_centroids_4096.fvecs deep1M/deep1M_clusterids_4096.ivecs -``` - -### Example Code in C++ for index construction -The following code demonstrates how to load Deep1M's vector data, centroids information, and cluster IDs from disk, build an IVF + RaBitQ index, and save the index back to disk. -```cpp -#include -#include - -#include "defines.hpp" -#include "index/ivf/ivf.hpp" -#include "utils/io.hpp" -#include "utils/stopw.hpp" - -using PID = rabitqlib::PID; -using index_type = rabitqlib::ivf::IVF; -using data_type = rabitqlib::RowMajorArray; -using gt_type = rabitqlib::RowMajorArray; - -int main(int argc, char** argv) { - if (argc < 6) { - std::cerr << "Usage: " << argv[0] << " \n" - << "arg1: path for data file, format .fvecs\n" - << "arg2: path for centroids file generated by ivf.py\n" - << "arg3: path for cluster ids file generated by ivf.py\n" - << "arg4: total number of bits for quantization\n" - << "arg5: path for saving index\n" - << "arg6: if use faster quantization (\"true\" or \"false\"), false by " - "default\n"; - exit(1); - } - - bool faster_quant = false; - if (argc > 6) { - std::string faster_str(argv[6]); - if (faster_str == "true") { - faster_quant = true; - std::cout << "Using faster quantize for indexing...\n"; - } - } - - char* data_file = argv[1]; - char* centroids_file = argv[2]; - char* cids_file = argv[3]; - size_t total_bits = atoi(argv[4]); - char* index_file = argv[5]; - - data_type data; - data_type centroids; - gt_type cids; - - rabitqlib::load_vecs(data_file, data); - rabitqlib::load_vecs(centroids_file, centroids); - rabitqlib::load_vecs(cids_file, cids); - - size_t num_points = data.rows(); - size_t dim = data.cols(); - size_t k = centroids.rows(); - - std::cout << "data loaded\n"; - std::cout << "\tN: " << num_points << '\n'; - std::cout << "\tDIM: " << dim << '\n'; - - rabitqlib::StopW stopw; - index_type ivf(num_points, dim, k, total_bits); - ivf.construct(data.data(), centroids.data(), cids.data(), faster_quant); - float miniutes = stopw.get_elapsed_mili() / 1000 / 60; - std::cout << "ivf constructed \n"; - ivf.save(index_file); - - std::cout << "Indexing time " << miniutes << '\n'; - - return 0; -} +```bash +sudo apt-get update +sudo apt-get install -y build-essential cmake libomp-dev +python -m pip install --upgrade pip +python -m pip install rabitqlib ``` -After compilation (suppose it is compiled to an executable named `ivf_build`), run the following command to build the IVF index: -```shell -./ivf_build deep1M/deep1M_base.fvecs deep1M/deep1M_centroids_4096.fvecs deep1M/deep1M_clusterids_4096.ivecs 4 deep1M/deep1M_rabitqlib_ivf_4.index true -``` -This builds an IVF index for the Deep1M dataset using RaBitQ with 4 (1+3) bits to quantize each vector. - -### Example Code in C++ for querying -After building the index, you can execute queries on it. The following code shows how to load the IVF index and queries from disk, execute the queries, and compare results against the ground truth. - -```c++ -#include -#include - -#include "defines.hpp" -#include "index/ivf/ivf.hpp" -#include "utils/io.hpp" -#include "utils/stopw.hpp" -#include "utils/tools.hpp" - -using PID = rabitqlib::PID; -using index_type = rabitqlib::ivf::IVF; -using data_type = rabitqlib::RowMajorArray; -using gt_type = rabitqlib::RowMajorArray; -static std::vector get_nprobes( - const index_type& ivf, - const std::vector& all_nprobes, - data_type& query, - gt_type& gt -); +To install the current development version instead: -static size_t topk = 10; -static size_t test_round = 5; - -int main(int argc, char** argv) { - if (argc < 4) { - std::cerr << "Usage: " << argv[0] << " \n" - << "arg1: path for index \n" - << "arg2: path for query file, format .fvecs\n" - << "arg3: path for groundtruth file format .ivecs\n" - << "arg4: whether use high accuracy fastscan, (\"true\" or \"false\"), " - "true by default\n\n"; - exit(1); - } - - char* index_file = argv[1]; - char* query_file = argv[2]; - char* gt_file = argv[3]; - bool use_hacc = true; - - if (argc > 4) { - std::string hacc_str(argv[4]); - if (hacc_str == "false") { - use_hacc = false; - std::cout << "Do not use Hacc FastScan\n"; - } - } - - data_type query; - gt_type gt; - rabitqlib::load_vecs(query_file, query); - rabitqlib::load_vecs(gt_file, gt); - size_t nq = query.rows(); - size_t total_count = nq * topk; - - index_type ivf; - ivf.load(index_file); - - std::vector all_nprobes; - all_nprobes.push_back(5); - for (size_t i = 10; i < 200; i += 10) { - all_nprobes.push_back(i); - } - for (size_t i = 200; i < 400; i += 40) { - all_nprobes.push_back(i); - } - for (size_t i = 400; i <= 1500; i += 100) { - all_nprobes.push_back(i); - } - for (size_t i = 2000; i <= 4000; i += 500) { - all_nprobes.push_back(i); - } - - all_nprobes.push_back(6000); - all_nprobes.push_back(10000); - all_nprobes.push_back(15000); - - rabitqlib::StopW stopw; - - auto nprobes = get_nprobes(ivf, all_nprobes, query, gt); - size_t length = nprobes.size(); - - std::vector> all_qps(test_round, std::vector(length)); - std::vector> all_recall(test_round, std::vector(length)); - - for (size_t r = 0; r < test_round; r++) { - for (size_t l = 0; l < length; ++l) { - size_t nprobe = nprobes[l]; - size_t total_correct = 0; - float total_time = 0; - std::vector results(topk); - for (size_t i = 0; i < nq; i++) { - stopw.reset(); - ivf.search(&query(i, 0), topk, nprobe, results.data(), use_hacc); - total_time += stopw.get_elapsed_micro(); - for (size_t j = 0; j < topk; j++) { - for (size_t k = 0; k < topk; k++) { - if (gt(i, k) == results[j]) { - total_correct++; - break; - } - } - } - } - float qps = static_cast(nq) / (total_time / 1e6F); - float recall = - static_cast(total_correct) / static_cast(total_count); - - all_qps[r][l] = qps; - all_recall[r][l] = recall; - } - } - - auto avg_qps = rabitqlib::horizontal_avg(all_qps); - auto avg_recall = rabitqlib::horizontal_avg(all_recall); - - std::cout << "nprobe\tQPS\trecall" << '\n'; - - for (size_t i = 0; i < length; ++i) { - size_t nprobe = nprobes[i]; - float qps = avg_qps[i]; - float recall = avg_recall[i]; - - std::cout << nprobe << '\t' << qps << '\t' << recall << '\n'; - } - - return 0; -} - -static std::vector get_nprobes( - const index_type& ivf, - const std::vector& all_nprobes, - data_type& query, - gt_type& gt -) { - size_t nq = query.rows(); - size_t total_count = topk * nq; - float old_recall = 0; - std::vector nprobes; - - for (auto nprobe : all_nprobes) { - nprobes.push_back(nprobe); - - size_t total_correct = 0; - std::vector results(topk); - for (size_t i = 0; i < nq; i++) { - ivf.search(&query(i, 0), topk, nprobe, results.data()); - for (size_t j = 0; j < topk; j++) { - for (size_t k = 0; k < topk; k++) { - if (gt(i, k) == results[j]) { - total_correct++; - break; - } - } - } - } - float recall = static_cast(total_correct) / static_cast(total_count); - if (recall > 0.997 || recall - old_recall < 1e-5) { - break; - } - old_recall = recall; - } - - return nprobes; -} +```bash +git clone https://github.com/VectorDB-NTU/RaBitQ-Library.git +cd RaBitQ-Library +python -m pip install . ``` -To execute queries on the Deep1M dataset, run the following command for the compiled codes (suppose that it is named `ivf_query`): -```shell -./ivf_query deep1M/deep1M_rabitqlib_ivf_4.index deep1M/deep1M_query.fvecs deep1M/deep1M_groundtruth.ivecs -``` - -## RaBitQ + HNSW -[HNSW](https://arxiv.org/abs/1603.09320) is a popular graph-based index. Compared to IVF + RaBitQ, HNSW + RaBitQ consumes more memory due to the need to store edges of every vertex in a graph (e.g., 32 edges = 1,024 bits). In terms of time-accuracy trade-off, HNSW + RaBitQ and IVF + RaBitQ perform differently across datasets—each may outperform the other depending on the scenario. -RaBitQ + HNSW takes raw data vectors as input. It begins with KMeans clustering (via a Python script), and the resulting centroids are used to normalize the data vectors for improved accuracy. - -#### Perform Clustering using Faiss -First, run [Kmeans clustering](https://github.com/VectorDB-NTU/RaBitQ-Library/blob/main/python/ivf.py) on raw data vectors to get centroid vectors. We recommend using 16 centroids (clusters). This will generate two output files: a centroids file and a cluster IDs file. -Use the following command to perform KMeans clustering on the Deep1M dataset. -```shell -python python/ivf.py deep1M/deep1M_base.fvecs 16 deep1M/deep1M_centroids_16.fvecs deep1M/deep1M_clusterids_16.ivecs l2 +### Build and search an IVF index + +The following complete example uses deterministic synthetic data and does not +require a dataset download: + +```python +import numpy as np +from rabitqlib import IvfIndex + +rng = np.random.default_rng(42) +data = rng.standard_normal((500, 64)).astype(np.float32) +queries = rng.standard_normal((5, 64)).astype(np.float32) + +# Assign vectors to five clusters and calculate their centroids. +cluster_ids = (np.arange(len(data)) % 5).astype(np.uint32) +centroids = np.stack( + [data[cluster_ids == cluster].mean(axis=0) for cluster in range(5)] +).astype(np.float32) + +index = IvfIndex( + dim=64, + max_elements=len(data), + num_clusters=5, + nbits=4, + metric="l2", +) +index.build(data, centroids, cluster_ids) + +ids, distances = index.search(queries, k=10, nprobe=5) +print(ids.shape, distances.shape) # (5, 10) (5, 10) +print(ids[0]) ``` -#### Example Code in C++ for index construction -Second, load raw data, centroids, and cluster IDs files to build the index. Index file is then saved. - -```cpp -#include -#include - -#include "index/hnsw/hnsw.hpp" -#include "utils/io.hpp" -#include "utils/stopw.hpp" - -using PID = rabitqlib::PID; -using index_type = rabitqlib::hnsw::HierarchicalNSW; -using data_type = rabitqlib::RowMajorArray; -using gt_type = rabitqlib::RowMajorArray; - -int main(int argc, char* argv[]) { - if (argc < 8) { - std::cerr << "Usage: " << argv[0] - << " \n" - << "arg1: path for data file, format .fvecs\n" - << "arg2: path for centroids file, format .fvecs\n" - << "arg3: path for cluster ids file, format .ivecs\n" - << "arg4: m (degree bound) for hnsw\n" - << "arg5: ef for indexing \n" - << "arg6: total number of bits for quantization\n" - << "arg7: path for saving index\n" - << "arg8: metric type (\"l2\" or \"ip\")\n" - << "arg9: if use faster quantization (\"true\" or \"false\"), false by " - "default\n"; - exit(1); - } +The `metric` argument accepts `"l2"` and `"ip"` (also spelled +`"innerproduct"`). To search by cosine similarity, normalize database and +query vectors first and use `metric="ip"`. - char* data_file = argv[1]; - char* centroid_file = argv[2]; - char* cid_file = argv[3]; - size_t m = atoi(argv[4]); - size_t ef = atoi(argv[5]); - size_t total_bits = atoi(argv[6]); - char* index_file = argv[7]; +Python bindings are also available for `HnswIndex` and `SymqgIndex`. The +[Python examples](https://github.com/VectorDB-NTU/RaBitQ-Library/tree/main/sample/python) +cover construction, querying, and index persistence. - rabitqlib::MetricType metric_type = rabitqlib::METRIC_L2; - if (argc > 8) { - std::string metric_str(argv[8]); - if (metric_str == "ip" || metric_str == "IP") { - metric_type = rabitqlib::METRIC_IP; - } - } - if (metric_type == rabitqlib::METRIC_IP) { - std::cout << "Metric Type: IP\n"; - } else if (metric_type == rabitqlib::METRIC_L2) { - std::cout << "Metric Type: L2\n"; - } +## C++ - bool faster_quant = false; - if (argc > 9) { - std::string faster_str(argv[9]); - if (faster_str == "true") { - faster_quant = true; - std::cout << "Using faster quantize for indexing...\n"; - } - } +Clone the repository and build the library and examples: - data_type data; - data_type centroids; - gt_type cluster_id; - - rabitqlib::load_vecs(data_file, data); - rabitqlib::load_vecs(centroid_file, centroids); - rabitqlib::load_vecs(cid_file, cluster_id); - - size_t num_points = data.rows(); - size_t dim = data.cols(); - - size_t random_seed = 100; // by default 100 - auto* hnsw = new rabitqlib::hnsw::HierarchicalNSW( - num_points, dim, total_bits, m, ef, random_seed, metric_type - ); - - rabitqlib::StopW stopw; - stopw.reset(); - - hnsw->construct( - centroids.rows(), - centroids.data(), - num_points, - data.data(), - cluster_id.data(), - 0, - faster_quant - ); - - float total_time = stopw.get_elapsed_micro(); - total_time /= 1e6; - - std::cout << "indexing time = " << total_time << "s" << '\n'; - hnsw->save(index_file); - - std::cout << "index saved..." << '\n'; - - return 0; -} +```bash +git clone https://github.com/VectorDB-NTU/RaBitQ-Library.git +cd RaBitQ-Library +cmake -S . -B build -DCMAKE_BUILD_TYPE=Release +cmake --build build --parallel ``` -After compilation (resulting in an executable named `hnsw_build`), run the following command to build the HNSW index. -```shell -./hnsw_build deep1M/deep1M_base.fvecs deep1M/deep1M_centroids_16.fvecs deep1M/deep1M_clusterids_16.ivecs 16 100 5 deep1M/deep1M_c16_b5.index l2 true -``` -This will build an HNSW index that uses 5 (1+4) bits to quantize each vector. - -#### Example Code in C++ for querying -Third, load the index, queries and ground truth files to evaluate ANN search performance. - -```cpp -#include -#include - -#include "index/hnsw/hnsw.hpp" -#include "utils/io.hpp" -#include "utils/stopw.hpp" - -std::vector efs = {10, 20, 40, 50, 60, 80, 100, 150, 170, 190, 200, - 250, 300, 400, 500, 600, 700, 800, 1000, 1500, 2000}; - -size_t test_round = 3; -size_t topk = 10; - -using PID = rabitqlib::PID; -using index_type = rabitqlib::hnsw::HierarchicalNSW; -using data_type = rabitqlib::RowMajorArray; -using gt_type = rabitqlib::RowMajorArray; - -int main(int argc, char* argv[]) { - if (argc < 4) { - std::cerr << "Usage: " << argv[0] << " \n" - << "arg1: path for index \n" - << "arg2: path for query file, format .fvecs\n" - << "arg3: path for groundtruth file format .ivecs\n" - << "arg4: metric type (\"l2\" or \"ip\")\n"; - exit(1); - } - - char* index_file = argv[1]; - char* query_file = argv[2]; - char* gt_file = argv[3]; - - data_type query; - gt_type gt; - rabitqlib::load_vecs(query_file, query); - rabitqlib::load_vecs(gt_file, gt); - size_t nq = query.rows(); - size_t total_count = nq * topk; - - index_type hnsw; - rabitqlib::MetricType metric_type = rabitqlib::METRIC_L2; - if (argc > 4) { - std::string metric_str(argv[4]); - if (metric_str == "ip" || metric_str == "IP") { - metric_type = rabitqlib::METRIC_IP; - } - } - if (metric_type == rabitqlib::METRIC_IP) { - std::cout << "Metric Type: IP\n"; - } else if (metric_type == rabitqlib::METRIC_L2) { - std::cout << "Metric Type: L2\n"; - } - - hnsw.load(index_file, metric_type); - - rabitqlib::StopW stopw; - - auto nefs = efs; - - size_t length = nefs.size(); - - std::vector> all_qps(test_round, std::vector(length)); - std::vector> all_recall(test_round, std::vector(length)); - - std::cout << "search start >.....\n"; - - for (size_t i_probe = 0; i_probe < length; ++i_probe) { - for (size_t r = 0; r < test_round; r++) { - size_t ef = nefs[i_probe]; - size_t total_correct = 0; - float total_time = 0; - - auto start = std::chrono::high_resolution_clock::now(); - std::vector>> res = - hnsw.search(query.data(), nq, topk, ef, 1); +Example executables are written to `bin/`. Their source demonstrates complete +indexing and querying workflows: - auto end = std::chrono::high_resolution_clock::now(); +- [IVF + RaBitQ](https://github.com/VectorDB-NTU/RaBitQ-Library/blob/main/sample/cpp/ivf_rabitq_indexing.cpp) +- [HNSW + RaBitQ](https://github.com/VectorDB-NTU/RaBitQ-Library/blob/main/sample/cpp/hnsw_rabitq_indexing.cpp) +- [SymphonyQG](https://github.com/VectorDB-NTU/RaBitQ-Library/blob/main/sample/cpp/symqg_indexing.cpp) +- [Low-level quantization](https://github.com/VectorDB-NTU/RaBitQ-Library/blob/main/sample/cpp/quantizer.cpp) - float elapsed_us = - std::chrono::duration(end - start).count(); +### Run the C++ tests - total_time += elapsed_us; - - for (size_t i = 0; i < nq; i++) { - for (size_t j = 0; j < topk; j++) { - for (size_t k = 0; k < topk; k++) { - if (gt(i, k) == res[i][j].second) { - total_correct++; - break; - } - } - } - } - - float qps = static_cast(nq) / ((total_time) / 1e6F); - - float recall = - static_cast(total_correct) / static_cast(total_count); - - all_qps[r][i_probe] = qps; - all_recall[r][i_probe] = recall; - } - } - - auto avg_qps = rabitqlib::horizontal_avg(all_qps); - auto avg_recall = rabitqlib::horizontal_avg(all_recall); - - std::cout << "EF\tQPS\tRecall\t" - - "\n"; - for (size_t i = 0; i < avg_qps.size(); ++i) { - std::cout << efs[i] << '\t' << avg_qps[i] << '\t' << avg_recall[i] << '\t' << '\n'; - } -} +```bash +cmake -S . -B build -DRABITQ_BUILD_TESTS=ON -DCMAKE_BUILD_TYPE=Release +cmake --build build --parallel +ctest --test-dir build --output-on-failure ``` -To execute queries on the Deep1M dataset, run the following command for the executable (named `hnsw_query`) after compilation. -```shell -./hnsw_query deep1M/deep1M_c16_b5.index deep1M/deep1M_query.fvecs deep1M/deep1M_groundtruth.ivecs l2 -``` - -## RaBitQ + QG ([SymphonyQG](https://dl.acm.org/doi/10.1145/3709730)) -[QG](https://medium.com/@masajiro.iwasaki/fusion-of-graph-based-indexing-and-product-quantization-for-ann-search-7d1f0336d0d0) is a graph-based index originating from the [NGT library](https://github.com/yahoojapan/NGT). Unlike HNSW, it generates multiple quantization codes per vector and carefully re-organizes their layout to minimize random memory accesses during querying. RaBitQ + QG is developed from our research project [SymphonyQG](https://dl.acm.org/doi/10.1145/3709730). In contrast to IVF + RaBitQ and HNSW + RaBitQ, which consumes less memory than the raw datasets, RaBitQ + QG consumes more memory to achieve the best time-accuracy trade-off. - - -#### Example Code in C++ -Combine graph-based index with RaBitQ and FastScan. -```cpp -#include -#include - -#include "index/symqg/qg.hpp" -#include "index/symqg/qg_builder.hpp" -#include "utils/stopw.hpp" - -using PID = rabitqlib::PID; - -int main() { - size_t dim = 128; - size_t N = 16000; - std::vector data(dim * N); +GoogleTest is downloaded during test configuration. - static std::random_device rd; - static std::mt19937 gen(rd()); - std::normal_distribution dist(0, 1); +## Next steps - // generate N random data vectors - for (size_t i = 0; i < dim * N; i++) { - data[i] = dist(gen); - } - - size_t degree = 32; // degree bound for graph - size_t ef = 200; // size of search window for indexing - - rabitqlib::symqg::QuantizedGraph qg(N, dim, degree); // init index - - rabitqlib::symqg::QGBuilder builder(qg, ef, data.data()); // builder of index - - rabitqlib::StopW stopw; - stopw.reset(); - - builder.build(); // construct index - - float total_time = stopw.get_elapsed_micro(); - total_time /= 1e6; - - std::cout << "indexing time = " << total_time << "s" << '\n'; - - size_t nq = 10; - std::vector query(nq * dim); - - // take first nq data vectors as query vectors - for (size_t i = 0; i < nq * dim; i++) { - query[i] = data[i]; - } - - size_t topk = 10; - size_t ef_search = 100; - - qg.set_ef(ef_search); - std::vector results(topk); - - for (size_t qid = 0; qid < nq; qid++) { - qg.search(query.data() + (dim * qid), topk, results.data()); - std::cout << "query " << qid << "'s " << topk << "NNs:" << '\n'; - for (size_t i = 0; i < topk; i++) { - std::cout << "{ID: " << results[i] << "} "; - } - std::cout << '\n'; - } - - return 0; -} -``` +- Learn how the [RaBitQ quantizer](rabitq/rabitq.md) works. +- Select an index: [IVF](index/ivf.md), [HNSW](index/hnsw.md), or + [SymphonyQG](index/qg.md). +- Review the + [contribution workflow](https://github.com/VectorDB-NTU/RaBitQ-Library/blob/main/CONTRIBUTING.md) + before opening a pull request. diff --git a/docs/docs/rabitq/estimator.md b/docs/docs/rabitq/estimator.md index 80cfd34..1c0d660 100644 --- a/docs/docs/rabitq/estimator.md +++ b/docs/docs/rabitq/estimator.md @@ -30,7 +30,12 @@ $$ where $c_B=- \frac{2^B-1}{2}$ and $B$ is the number of bits used for the quantization. $\mathbf{1}_D$ is the all-one vector of dimension $D$. $P$ is a sample of random rotation matrices. ## Estimator -The following derivation covers the estimators for Euclidean distances, inner products, and cosine similarity. The cosine similarity is supported by the same estimator as the inner product. +The following derivation covers squared Euclidean distance and inner product. +The public metric choices are `METRIC_L2` and `METRIC_IP`; there is no separate +cosine metric. For cosine search, normalize both data and query vectors, then +use inner product. The implementation represents inner-product distance as +$1-\langle\mathbf{o},\mathbf{q}\rangle$, which preserves the same ranking while +keeping the nearest-neighbor convention that smaller values are better. ### Estimator of Euclidean Distance $$ @@ -72,15 +77,18 @@ We store the following variables such that the estimator can be computed easily. ### Estimator of Inner Product -When inner product is used as the metric of vector search, it targets the data vector which has the *maximum* inner product with the query vector. To unify the question with nearest neighbor search, we follows Faiss and hnswlib to compute the negative inner product. +When inner product is used as the metric of vector search, it targets the data +vector with the *maximum* inner product with the query. The implementation +uses $1-\langle\mathbf{o},\mathbf{q}\rangle$ so that smaller distances rank +higher-inner-product vectors first. The constant 1 does not affect ordering. $$ \begin{align} -&-\left< \mathbf{o}_r,\mathbf{q}_r\right> -\\=& -\left< \mathbf{o}_r-\mathbf{c} + \mathbf{c},\mathbf{q}_r-\mathbf{c} + \mathbf{c}\right> -\\=& -\left< \mathbf{q}_r,\mathbf{c}\right> -\left< \mathbf{o}_r-\mathbf{c},\mathbf{c}\right> - \left< \mathbf{o}_r-\mathbf{c},\mathbf{q}_r-\mathbf{c} \right> -\\ \approx &-\left< \mathbf{q}_r,\mathbf{c}\right> -\left< \mathbf{o}_r-\mathbf{c},\mathbf{c}\right> +\| \mathbf{o}_r-\mathbf{c}\| \frac{\left< \mathbf{\bar o}, \mathbf{c}\right>}{\left<\mathbf{\bar o},\mathbf{o} \right>} - \frac{\Delta_x}{\left<\mathbf{\bar o},\mathbf{o} \right>}\cdot \left[ \left< \mathbf{x}_u, \mathbf{q}_r' \right>+c_B S_q\right] -\\ -\left< \mathbf{o}_r,\mathbf{q}_r\right>\approx &-\left< \mathbf{q}_r,\mathbf{c}\right> -\left< \mathbf{o}_r-\mathbf{c},\mathbf{c}\right> +\| \mathbf{o}_r-\mathbf{c}\| \frac{\left< \mathbf{\bar o}, \mathbf{c}\right>}{\left<\mathbf{\bar o},\mathbf{o} \right>} - \frac{\Delta_x}{\left<\mathbf{\bar o},\mathbf{o} \right>}\cdot \left[ \left< \mathbf{x}_u, \mathbf{q}_r' \right>+c_B S_q\right] +&1-\left< \mathbf{o}_r,\mathbf{q}_r\right> +\\=&1 -\left< \mathbf{o}_r-\mathbf{c} + \mathbf{c},\mathbf{q}_r-\mathbf{c} + \mathbf{c}\right> +\\=&1 -\left< \mathbf{q}_r,\mathbf{c}\right> -\left< \mathbf{o}_r-\mathbf{c},\mathbf{c}\right> - \left< \mathbf{o}_r-\mathbf{c},\mathbf{q}_r-\mathbf{c} \right> +\\ \approx &1 -\left< \mathbf{q}_r,\mathbf{c}\right> -\left< \mathbf{o}_r-\mathbf{c},\mathbf{c}\right> +\| \mathbf{o}_r-\mathbf{c}\| \frac{\left< \mathbf{\bar o}, \mathbf{c}\right>}{\left<\mathbf{\bar o},\mathbf{o} \right>} - \frac{\Delta_x}{\left<\mathbf{\bar o},\mathbf{o} \right>}\cdot \left[ \left< \mathbf{x}_u, \mathbf{q}_r' \right>+c_B S_q\right] +\\ 1-\left< \mathbf{o}_r,\mathbf{q}_r\right>\approx &1 -\left< \mathbf{q}_r,\mathbf{c}\right> -\left< \mathbf{o}_r-\mathbf{c},\mathbf{c}\right> +\| \mathbf{o}_r-\mathbf{c}\| \frac{\left< \mathbf{\bar o}, \mathbf{c}\right>}{\left<\mathbf{\bar o},\mathbf{o} \right>} - \frac{\Delta_x}{\left<\mathbf{\bar o},\mathbf{o} \right>}\cdot \left[ \left< \mathbf{x}_u, \mathbf{q}_r' \right>+c_B S_q\right] \end{align} $$ @@ -101,7 +109,7 @@ We store the following variables such that the estimator can be computed easily. | Name (Type) of Variable | Description | | ----------------- | ----------- | -| `F_add (float)`| $-\left< \mathbf{o}_r-\mathbf{c},\mathbf{c}\right>+\| \mathbf{o}_r-\mathbf{c}\| \frac{\left< \mathbf{\bar o}, \mathbf{c}\right>}{\left<\mathbf{\bar o},\mathbf{o} \right>}$ | +| `F_add (float)`| $1-\left< \mathbf{o}_r-\mathbf{c},\mathbf{c}\right>+\| \mathbf{o}_r-\mathbf{c}\| \frac{\left< \mathbf{\bar o}, \mathbf{c}\right>}{\left<\mathbf{\bar o},\mathbf{o} \right>}$ | | `F_rescale (float)`| $-\frac{\Delta_x}{\left<\mathbf{\bar o},\mathbf{o} \right>}$| | `F_error (float)`| $\|\mathbf{o}_r-\mathbf{c}\| \cdot \sqrt{\frac{1 - \left< \mathbf{\bar o},\mathbf{o}\right>^2}{\left< \mathbf{\bar o},\mathbf{o}\right>^2}} \frac{\epsilon_{0}}{\sqrt{D-1}}$| | `G_add (float)`| $-\left< \mathbf{q}_r,\mathbf{c}\right>$ | @@ -134,21 +142,28 @@ float ub_dist = est_dist + error_bound RaBitQ supports incremental distance estimation. We split the code into two parts: the binary code (the most significant bits) and the extended code (the remaining $B-1$ bits). Incremental distance estimation supports to first estimate a coarse distance based on the binary code. If the accuracy is insufficient, we then access the extended code to boost the accuracy. -For this, we need to prepare factors for both the binary code and the extended code. The factors for the binary code are stored in `F_add`, `F_rescale` and `F_error`. The factors for the extended code are stored in `F_add_ex`, `F_rescale_ex` and `F_error_ex`. +For this, we prepare factors for both parts. The binary layout stores `F_add`, +`F_rescale`, and `F_error`; the extended layout stores `F_add_ex` and +`F_rescale_ex`. The implementation obtains the refined error bound by scaling +the binary error factor by $2^{B-1}$ rather than storing a separate +`F_error_ex`. The factors for the query includes `G_add`, `G_error`, `G_kBxSumq` and `G_k1xSumq`. Let `ip_bin` be the inner product between the binary code and the randomly rotated query vector and `ip_ex` be the inner product between the ex-code and the randomly rotated query vector. ```cpp // 1-bit dist -float est_dist = F_add + G_add + F_rescale * (ip_bin + G_k1xSumq) -float bound = F_error * G_error -float ub_dist = est_dist + bound -float lb_dist = est_dist - bound +float est_dist = F_add + G_add + F_rescale * (ip_bin + G_k1xSumq); +float bound = F_error * G_error; +float ub_dist = est_dist + bound; +float lb_dist = est_dist - bound; // boost to full-bit dist -float ex_est_dist = F_add_ex + G_add + F_rescale_ex * (ip_bin << (bits - 1) + ip_ex + G_kBxSumq) -float ex_bound = F_error_ex * G_error +float ex_scale = static_cast(1 << (bits - 1)); +float ex_est_dist = + F_add_ex + G_add + + F_rescale_ex * (ex_scale * ip_bin + ip_ex + G_kBxSumq); +float ex_bound = F_error * G_error / ex_scale; float ex_ub_dist = ex_est_dist + ex_bound; float ex_lb_dist = ex_est_dist - ex_bound; -``` \ No newline at end of file +``` diff --git a/docs/docs/rabitq/quantizer.md b/docs/docs/rabitq/quantizer.md index 3d65927..0efc8a6 100644 --- a/docs/docs/rabitq/quantizer.md +++ b/docs/docs/rabitq/quantizer.md @@ -13,7 +13,9 @@ Various advanced data formats are provided to support the following needs. 4. Incremental distance estimation for splitted single vectors. 5. Incremental distance estimation for splitted batched vectors. -Note that these data formats only map raw floating-point vectors into codes of `uint8`/`uint32` arrays. To compactly store the code vector, please further refer to `rabitqlib/quantization/pack_ex_code.hpp`. +These data formats map raw floating-point vectors into `uint8` or `uint32` +codes. For the packed extended-code layouts, see +`rabitqlib/quantization/pack_excode.hpp`. RaBitQ quantizer is included in `rabitq_impl.hpp` and `rabitq.hpp`. ```css @@ -53,7 +55,7 @@ This format allows RaBitQ to be used as a direct replacement for uniform scalar #include #include -#include "quantization/rabitq.hpp" +#include "rabitqlib/quantization/rabitq.hpp" int main() { size_t dim = 768; @@ -103,7 +105,7 @@ This format is designed for computing distance metrics between data vectors and #include #include -#include "quantization/rabitq.hpp" +#include "rabitqlib/quantization/rabitq.hpp" int main() { size_t dim = 768; @@ -209,7 +211,7 @@ In practical implementation of our SymphonyQG index, the data is compactly store #include #include -#include "quantization/rabitq_impl.hpp" +#include "rabitqlib/quantization/rabitq_impl.hpp" int main() { size_t dim = 768; @@ -258,15 +260,17 @@ Here, we assume the data are compactly stored in the layout of `QGBatchDataMap` ```cpp size_t dim = 768; // the dimensionality - std::vector rotated_query(dim); + std::vector query(dim); + std::vector centroid(dim); std::vector batch_data(rabitqlib::QGBatchDataMap::data_bytes(dim)); - rabitqlib::BatchQuery processed_query(rotated_query.data(), dim); + rabitqlib::BatchQuery processed_query(query.data(), dim); - // The factors should be set according to the centroid vector. - // For ANN, this is preprocessed for every center vector when a query comes. + // G_add is the squared L2 distance between the query and the centroid. + // This standalone example omits rotation, so both operands are raw vectors. + // In an index that applies rotation, rotate both with the same transformation. processed_query.set_g_add( - std::sqrt(rabitqlib::euclidean_sqr(rotated_query.data(), centroid.data(), dim)) + rabitqlib::euclidean_sqr(query.data(), centroid.data(), dim) ); size_t batch_size = 32; @@ -296,7 +300,7 @@ This format supports computing distances incrementally when vectors are split ac #include #include -#include "quantization/rabitq.hpp" +#include "rabitqlib/quantization/rabitq.hpp" int main() { size_t dim = 768; @@ -360,28 +364,34 @@ int main() { ... size_t dim = 768; // the dimensionality size_t bits = 5; // the bit-width of DATA vectors - std::vector rotated_query(dim); + std::vector query(dim); // the config of fast quantizer is necessary for preprocessing queries rabitqlib::quant::RabitqConfig config = rabitqlib::quant::faster_config(dim, bits); rabitqlib::SplitSingleQuery processed_query( - rotated_query.data(), dim, bits - 1, config, rabitqlib::METRIC_L2 + query.data(), dim, bits - 1, config, rabitqlib::METRIC_L2 ); - // set factors for distance estimation. - // In ANN the factors are precomputed when a query comes. - float norm = - rabitqlib::euclidean_sqr(rotated_query.data(), centroid.data(), dim); - float error = rabitqlib::dot_product(rotated_query.data(), centroid.data(), dim); + // This standalone example omits rotation, so query and centroid are both raw. + // If rotation is used, apply the same transformation to both operands. + float distance_sqr = rabitqlib::euclidean_sqr(query.data(), centroid.data(), dim); + float distance_norm = std::sqrt(distance_sqr); // Compute estimated distances based on binary codes float ip_x0_qr; float est_dist; float low_dist; - split_single_estdist( - bin_data.data(), processed_query, dim, ip_x0_qr, est_dist, low_dist, -norm, error + rabitqlib::split_single_estdist( + bin_data.data(), + processed_query, + dim, + ip_x0_qr, + est_dist, + low_dist, + distance_sqr, + distance_norm ); // the kernel of computing inner product between compact codes and query vectors @@ -392,7 +402,7 @@ int main() { float low_dist_ex; float ip_x0_qr_ex; - split_single_fulldist( + rabitqlib::split_single_fulldist( bin_data.data(), ex_data.data(), ip_func, @@ -402,8 +412,8 @@ int main() { est_dist_ex, low_dist_ex, ip_x0_qr_ex, - -norm, - error + distance_sqr, + distance_norm ); ``` @@ -417,7 +427,7 @@ This format combines the benefits of batched processing with incremental computa #include #include -#include "quantization/rabitq.hpp" +#include "rabitqlib/quantization/rabitq.hpp" int main() { size_t dim = 768; @@ -483,7 +493,7 @@ int main() { ```cpp size_t dim = 768; // the dimensionality size_t bits = 5; // the bit-width of DATA vectors - std::vector rotated_query(dim); + std::vector query(dim); // The flag use_hacc controls the precision of FastScan. // `use_hacc = false` - each number in LUTs is quantized into 8 bits. @@ -493,15 +503,14 @@ int main() { // When the bit-width of data <= 2, `use_hacc = false` does not harm accuracy. rabitqlib::SplitBatchQuery processed_query( - rotated_query.data(), dim, bits - 1, rabitqlib::METRIC_L2, true + query.data(), dim, bits - 1, rabitqlib::METRIC_L2, true ); - // The factors should be set according to the centroid vector. - // For ANN, this is preprocessed for every center vector when a query comes. - processed_query.set_g_add( - std::sqrt(rabitqlib::euclidean_sqr(rotated_query.data(), centroid.data(), dim)), - rabitqlib::dot_product(rotated_query.data(), centroid.data(), dim) - ); + // SplitBatchQuery expects the L2 norm and squares it internally for G_add. + // This example omits rotation, so query and centroid are both raw vectors. + float distance_norm = + std::sqrt(rabitqlib::euclidean_sqr(query.data(), centroid.data(), dim)); + processed_query.set_g_add(distance_norm); size_t batch_size = 32; @@ -525,10 +534,10 @@ int main() { ); // the kernel of computing inner product between compact codes and query vectors - auto ip_func = select_excode_ipfunc(bits - 1); + auto ip_func = rabitqlib::select_excode_ipfunc(bits - 1); size_t i = 15; - split_distance_boosting( + rabitqlib::split_distance_boosting( ex_data.data() + (i * rabitqlib::ExDataMap::data_bytes(dim, bits - 1)), ip_func, processed_query, @@ -538,4 +547,3 @@ int main() { ); ``` - diff --git a/docs/docs/rabitq/rabitq.md b/docs/docs/rabitq/rabitq.md index 6fd5781..589941d 100644 --- a/docs/docs/rabitq/rabitq.md +++ b/docs/docs/rabitq/rabitq.md @@ -8,8 +8,8 @@ The RaBitQ algorithm is a drop-in replacement of binary quantization and (unifor The key advantages of RaBitQ include -- **High Accuracy with Tiny Space** - RaBitQ achieves the state-of-the-art accuracy under diverse space budgets for the estimation of similarity metrics. It produces promising accuracy with even **1-bit per dimension**. -- **Fast Distance Estimation** - RaBitQ supports to estimate the similarity metrics with high efficiency based on bitwise operations or [FastScan](https://arxiv.org/abs/1704.07355). +- **High Accuracy with Tiny Space** - RaBitQ provides the state-of-the-art estimates under diverse space budgets. Its smallest representation uses a one-bit code per padded dimension plus per-vector factors. +- **Fast Distance Estimation** - The implementation uses bitwise operations and, for batched scans in IVF and SymphonyQG, [FastScan](https://arxiv.org/abs/1704.07355). - **Theoretical Error Bound** - RaBitQ provides an asymptotically optimal error bound for the estimation of distances and inner product. The error bound can be used for reliable ordering and [reranking](reranking.md). @@ -22,5 +22,7 @@ The RaBitQ algorithm includes two steps: 2. **Quantization** - After the random rotation, the quantization algorithm quantizes a vector of floating-point numbers into a vector of low-bit unsigned integers. See [Quantizer](quantizer.md) for more details. -After the quantization, we can estimate the similarity metrics including Euclidean distance, inner product and cosine similarity based on the code vector $\mathbf{x}_u$ and the rescaling factor $\Delta_x$. See [Estimator](estimator.md) for more details. - +After quantization, the library supports Euclidean distance and inner product +using the code vector $\mathbf{x}_u$ and its factors. Cosine similarity is +obtained by normalizing data and query vectors before using inner-product +search. See [Estimator](estimator.md) for details. diff --git a/docs/docs/stylesheets/extra.css b/docs/docs/stylesheets/extra.css new file mode 100644 index 0000000..f4a384c --- /dev/null +++ b/docs/docs/stylesheets/extra.css @@ -0,0 +1,74 @@ +:root { + --rabitq-gradient-start: #4051b5; + --rabitq-gradient-end: #7e57c2; +} + +.hero { + margin: 0 0 2rem; + padding: 2.5rem; + color: #fff; + background: linear-gradient( + 135deg, + var(--rabitq-gradient-start), + var(--rabitq-gradient-end) + ); + border-radius: 0.8rem; + box-shadow: 0 0.5rem 1.5rem rgb(32 45 94 / 20%); +} + +.hero > :first-child { + margin-top: 0; +} + +.hero .md-button { + margin-top: 0.5rem; + margin-right: 0.35rem; + color: #fff; + border-color: rgb(255 255 255 / 70%); +} + +.hero .md-button:hover, +.hero .md-button:focus { + color: var(--md-primary-fg-color); + background-color: #fff; + border-color: #fff; +} + +.hero .md-button--primary { + color: var(--md-primary-fg-color); + background-color: #fff; + border-color: #fff; +} + +.feature-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 1rem; + margin: 1.5rem 0 2.5rem; +} + +.feature-card { + padding: 1.2rem; + border: 0.05rem solid var(--md-default-fg-color--lightest); + border-radius: 0.6rem; +} + +.feature-card h2 { + margin-top: 0; + font-size: 1rem; +} + +.feature-card p { + margin-bottom: 0; + color: var(--md-default-fg-color--light); +} + +@media screen and (max-width: 44.9844em) { + .hero { + padding: 1.5rem; + } + + .feature-grid { + grid-template-columns: 1fr; + } +} diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index ad1a134..afa5111 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -1,33 +1,85 @@ site_name: RaBitQ Library +site_description: Compact, accurate vector quantization and approximate nearest-neighbor search +site_url: https://vectordb-ntu.github.io/RaBitQ-Library/ +repo_url: https://github.com/VectorDB-NTU/RaBitQ-Library +repo_name: VectorDB-NTU/RaBitQ-Library +edit_uri: edit/main/docs/docs/ +copyright: Copyright © VectorDB Group, Nanyang Technological University nav: - - Introduction: index.md + - Home: index.md - Quick Start: quick_start.md - RaBitQ: - - Introduction: rabitq/rabitq.md - - Rotator: rabitq/rotator.md - - Quantizer: rabitq/quantizer.md - - Estimator: rabitq/estimator.md - - Reranking: rabitq/reranking.md - - Kernel: - - Compact Code Storage: compact_code.md - - Inner Product: kernel_ip.md - - RaBitQ for Vector Search: - - IVF + RaBitQ: index/ivf.md - - HNSW + RaBitQ: index/hnsw.md - - QG + RaBitQ (SymphonyQG): index/qg.md + - Overview: rabitq/rabitq.md + - Rotation: rabitq/rotator.md + - Quantization: rabitq/quantizer.md + - Estimation: rabitq/estimator.md + - Reranking: rabitq/reranking.md + - Kernels: + - Compact Code Storage: compact_code.md + - Inner Product: kernel_ip.md + - Vector Search: + - IVF + RaBitQ: index/ivf.md + - HNSW + RaBitQ: index/hnsw.md + - SymphonyQG: index/qg.md +theme: + name: material + language: en + icon: + logo: material/vector-polygon + repo: fontawesome/brands/github + palette: + - media: "(prefers-color-scheme: light)" + scheme: default + primary: indigo + accent: deep purple + toggle: + icon: material/brightness-7 + name: Switch to dark mode + - media: "(prefers-color-scheme: dark)" + scheme: slate + primary: indigo + accent: deep purple + toggle: + icon: material/brightness-4 + name: Switch to light mode + features: + - content.action.edit + - content.code.copy + - navigation.footer + - navigation.indexes + - navigation.sections + - navigation.top + - search.highlight + - search.suggest markdown_extensions: + - admonition + - attr_list + - footnotes + - md_in_html - pymdownx.arithmatex - pymdownx.details + - pymdownx.highlight: + anchor_linenums: true - pymdownx.superfences - - pymdownx.highlight - - attr_list - - footnotes + - pymdownx.tabbed: + alternate_style: true + - toc: + permalink: true + +extra_css: + - stylesheets/extra.css extra_javascript: - https://cdn.jsdelivr.net/npm/mathjax@2/MathJax.js?config=TeX-AMS-MML_HTMLorMML -theme: - name: material +extra: + social: + - icon: fontawesome/brands/github + link: https://github.com/VectorDB-NTU/RaBitQ-Library + name: RaBitQ Library on GitHub + - icon: fontawesome/brands/python + link: https://pypi.org/project/rabitqlib/ + name: RaBitQ Library on PyPI diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 0000000..83a4198 --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1 @@ +mkdocs-material==9.7.6 diff --git a/tests/README.md b/tests/README.md index bf33e3e..5a17249 100644 --- a/tests/README.md +++ b/tests/README.md @@ -1,25 +1,24 @@ -# RaBitQ Testing Framework +# RaBitQ Tests -This directory contains the comprehensive testing framework for the RaBitQ library +This directory contains the C++ unit and integration tests and the Python +binding tests for RaBitQ Library. ## Prerequisites -- CMake 3.10 or higher -- C++17 compatible compiler (GCC, Clang, or MSVC) -- Google Test (automatically downloaded via CMake FetchContent) +- CMake 3.10 or newer +- A GCC- or Clang-compatible C++17 compiler with OpenMP support +- An x86-64 CPU supported by RaBitQ's AVX2 or AVX-512 runtime dispatch +- Network access during the first configuration so CMake can download + GoogleTest 1.14.0 -### Installing CMake +The current CMake configuration uses GCC/Clang command-line options and does +not provide a supported MSVC build path. -For macos -```bash -brew install cmake -``` - -for (Ubuntu/Debian) +On Ubuntu or Debian, install the required build tools with: ```bash sudo apt-get update -sudo apt-get install cmake +sudo apt-get install -y build-essential cmake libomp-dev ``` ## Building and Running Tests @@ -29,29 +28,20 @@ sudo apt-get install cmake From the project root directory: ```bash -# Create build directory -mkdir build bin -cd build - -# Configure with tests enabled (tests are OFF by default) -cmake .. -DRABITQ_BUILD_TESTS=ON - -# Build the tests -make -j$(nproc) - -# Run all tests -./tests/rabitq_tests - -# Or use CTest for detailed output -ctest --output-on-failure +cmake -S . -B build -DRABITQ_BUILD_TESTS=ON -DCMAKE_BUILD_TYPE=Release +cmake --build build --parallel +ctest --test-dir build --output-on-failure ``` +The combined test executable is also available as `build/tests/rabitq_tests`. + ### Building without Tests By default, tests are **not built**. If you want to build only the library: ```bash -cmake .. +cmake -S . -B build -DCMAKE_BUILD_TYPE=Release +cmake --build build --parallel ``` @@ -59,16 +49,27 @@ cmake .. ``` tests/ -├── CMakeLists.txt # Automatic test discovery & suite configuration -├── main.cpp # Test runner entry point -├── common/ # Test utilities and helpers -│ ├── test_data.hpp # Test data generation utilities +├── .gitignore +├── CMakeLists.txt # C++ targets and CTest discovery +├── README.md +├── common/ # Shared C++ test utilities +│ ├── test_data.hpp │ ├── test_data.cpp -│ └── test_helpers.hpp # Custom assertions and helpers -├── unit/ # Unit tests (auto-discovered) -├── integration/ # Integration tests (auto-discovered) -└── benchmark/ # Performance benchmarks (to be added) +│ └── test_helpers.hpp +├── integration/ +│ └── bit_pack_unpack_test.cpp +├── python/ +│ ├── conftest.py +│ ├── test_hnsw.py +│ ├── test_import.py +│ ├── test_ivf.py +│ └── test_symqg.py +└── unit/rabitqlib/utils/ + ├── cpu_features_test.cpp + ├── rotator_test.cpp + ├── space_test.cpp + └── visited_set_test.cpp ``` -## Test Coverage - +CMake discovers C++ files matching `*_test.cpp` under `unit/` and +`integration/`. The Python tests are run separately with `python -m pytest`.