RaBitQ (Randomized Binary Quantization) is a 1-bit vector quantization scheme that compresses high-dimensional floating-point vectors into compact binary codes while preserving meaningful distance estimation. Unlike heuristic quantization methods, RaBitQ comes with provable theoretical error bounds on its distance estimates, making it one of the few quantization techniques where you can reason formally about the quality of approximate nearest neighbor (ANN) search results.
The core insight is elegant: by applying a random orthogonal transformation before binarization, RaBitQ ensures that the quantization error distributes uniformly across dimensions. This randomization, combined with carefully derived correction factors for norm and centroid information, allows the binary representation to produce unbiased distance estimates whose variance decreases as dimensionality grows. In practice, this means that for the high-dimensional vectors typical of embedding models (128-1536 dimensions), RaBitQ achieves remarkably accurate distance approximations using just one bit per dimension.
What makes RaBitQ especially compelling for production systems is its compatibility with SIMD hardware. The distance computation between a query vector and a quantized database vector reduces to a weighted popcount operation on binary words, which modern CPUs can execute in a handful of clock cycles using instructions like POPCNT, VPSHUFB, or ARM NEON equivalents. This means a system can scan millions of quantized vectors per second on commodity hardware, making RaBitQ an ideal building block for the lower layers of hierarchical ANN indices such as HNSW.
The foundation of RaBitQ is a shared random orthogonal matrix R drawn uniformly from the orthogonal group O(d). Every vector in the dataset is multiplied by this matrix before binarization. Because orthogonal transformations preserve inner products and norms, no geometric information is lost in the projection step itself. The randomness ensures that no adversarial data distribution can cause systematically bad quantization: the error is spread evenly across all dimensions with high probability.
After projection, each vector is centered (by subtracting the dataset centroid) and normalized. The binarization step simply takes the sign of each component: positive values become 1, negative values become 0. This produces a d-bit binary code per vector. Crucially, the original norm and the dot product with the centroid are stored as scalar side information (typically 4-8 bytes each), enabling the distance estimator to reconstruct an unbiased approximation of the true distance.
RaBitQ’s distance estimator is unbiased, meaning its expected value equals the true squared distance. The estimation formula combines the binary code (via popcount), the stored norm, and correction factors derived from the random projection. The variance of the estimate is bounded by O(1/d), so accuracy improves naturally as dimensionality increases — precisely the regime where ANN search is most needed.
The Hamming distance between two binary codes counts the number of differing bits, which is computed by XOR followed by popcount. In RaBitQ, the raw popcount is not used directly; instead, it is weighted by norm and projection factors to produce the final distance estimate. This “weighted popcount” formulation is what enables the theoretical guarantees while keeping computation cheap.
A 128-dimensional float32 vector occupies 512 bytes. Its RaBitQ code requires 128 bits (16 bytes) plus roughly 8 bytes of side information (norm, centroid dot product), totaling 24 bytes — a 21x compression. For 768-dimensional vectors common in transformer embeddings, compression exceeds 30x. This extreme compression is what allows entire vector databases to fit in CPU cache hierarchies, dramatically reducing memory bandwidth bottlenecks.
Given a dataset of n vectors in R^d, compute the centroid c = (1/n) * sum(x_i). Generate a random orthogonal matrix R (e.g., via QR decomposition of a Gaussian random matrix). Both c and R are stored and shared across all queries.
For each data vector x:
x' = x - c||x'||x'' = x' / ||x'||y = R * x''b_i = 1 if y_i >= 0, else 0Store the binary code b (d bits), the norm ||x'|| (float32), and optionally a precomputed factor sum(y_i) for faster query-time estimation.
Given a query q, compute q' = R * (q - c) in full precision. For each database vector with binary code b and stored norm r, the estimated squared distance is:
dist_est = ||q'||^2 + r^2 - 2 * r * (2/d) * (||q'||_1) * (d/2 - popcount(b XOR sign(q'))) * correction
Here, sign(q') is the binarized query, popcount(b XOR sign(q')) counts the differing bits, and the correction factor accounts for the gap between the L1 norm of the projected query and its ideal behavior under the random rotation.
The theoretical guarantee comes from concentration inequalities on the random orthogonal projection. Specifically, for a random orthogonal matrix R and unit vector u, the quantity (2/sqrt(d)) * sum(|R*u|_i) concentrates around 1 with deviation O(1/sqrt(d)). This means the multiplicative error in the distance estimate is bounded by 1 +/- O(1/sqrt(d)) with high probability, giving sub-linear error scaling that improves with dimensionality.
RaBitQ is typically used as a first-pass filter. The top-k candidates from the quantized distance scan are then re-ranked using exact (or higher-precision) distance computations on the original vectors. Because the quantized scan is 20-50x faster than a full-precision scan, even a generous candidate set (e.g., 10x the final k) yields large end-to-end speedups.
In a typical high-performance vector database implementation, RaBitQ serves as the quantization layer beneath the HNSW graph index. When a search traverses the HNSW graph, distance computations at the lower layers use RaBitQ codes rather than full-precision vectors. This allows the graph to maintain large fan-out (high ef_construction) without proportional memory growth, because the quantized vectors are compact enough to keep the entire working set in L2/L3 cache.
A distributed query path, for instance, broadcasts RaBitQ codes to worker nodes for parallel scanning. Each shard holds a contiguous block of binary codes in a memory-mapped array, and the SIMD popcount kernel processes 512-bit chunks, evaluating 512 dimensions (or 4 vectors of 128 dimensions) per instruction cycle.
Product Quantization divides vectors into subspaces and quantizes each subspace independently using k-means codebooks. PQ achieves high compression but requires codebook lookups (table-based ADC), making it less amenable to brute SIMD throughput. RaBitQ trades slightly lower per-vector accuracy for dramatically higher scan throughput because popcount is a single instruction while PQ requires multiple table lookups per subspace.
Scalar Quantization (SQ8/SQ4) quantizes each dimension independently to 8 or 4 bits. SQ preserves more information per dimension than RaBitQ’s 1 bit, but at 4-8x the memory cost. In practice, RaBitQ with re-ranking often matches SQ8 recall at a fraction of the memory, especially in high dimensions (d > 256) where the theoretical error bounds become tight.
The inner loop of RaBitQ distance computation is an XOR followed by popcount across d/8 bytes. On x86-64, this maps to vpxor + vpopcntb (AVX-512 VPOPCNTDQ) or the classic vpshufb-based popcount for AVX2. On ARM, veor + vcnt provides the equivalent. The key implementation pattern is to process binary codes in register-width chunks (32 or 64 bytes) and accumulate the popcount in a wide register before a final horizontal sum.
Binary codes should be stored contiguously in a flat array, with side information (norms, correction factors) in a parallel array. This separation ensures that the hot inner loop — which only touches binary codes — achieves maximum cache line utilization. A 128-dimensional code is 16 bytes, so 4 codes fit in a single 64-byte cache line. For 768 dimensions, each code is 96 bytes (1.5 cache lines), so alignment to cache line boundaries avoids straddling penalties.
When multiple queries arrive concurrently, batch them to amortize the cost of loading binary codes from memory. Process all queries against the same block of codes before moving to the next block. This “block-query” pattern keeps codes in cache while iterating over queries, reducing memory bandwidth by a factor proportional to the batch size (up to the cache capacity limit).
In a hybrid HNSW + RaBitQ index, the upper HNSW layers use full-precision vectors (since they contain few nodes and are traversed infrequently), while the bottom layer and leaf-level re-scoring use RaBitQ codes. The quantized codes are stored inline in the HNSW node structure at the bottom layer, eliminating a pointer chase during graph traversal. This hybrid approach achieves sub-millisecond latency on million-scale datasets with recall above 0.95.
const std = @import("std");
const simd = std.simd;
const RabitqCode = struct {
bits: []align(64) u8, // binary code, d/8 bytes, cache-aligned
norm: f32, // ||x - centroid||
sum_abs: f32, // sum of |projected_components|
};
/// Quantize a centered, projected vector into a 1-bit code.
pub fn quantize(projected: []const f32, allocator: std.mem.Allocator) !RabitqCode {
const d = projected.len;
const num_bytes = (d + 7) / 8;
const bits = try allocator.alignedAlloc(u8, 64, num_bytes);
@memset(bits, 0);
var norm_sq: f32 = 0.0;
var sum_abs: f32 = 0.0;
for (projected, 0..) |val, i| {
norm_sq += val * val;
sum_abs += @abs(val);
if (val >= 0.0) {
bits[i / 8] |= @as(u8, 1) << @intCast(i % 8);
}
}
return .{
.bits = bits,
.norm = @sqrt(norm_sq),
.sum_abs = sum_abs,
};
}
/// SIMD-accelerated distance estimation using weighted popcount.
pub fn estimateDistance(
query_bits: []const u8,
query_norm: f32,
query_l1: f32,
db_code: RabitqCode,
dim: u32,
) f32 {
// XOR + popcount in 32-byte (256-bit) chunks
var hamming: u32 = 0;
const chunks = db_code.bits.len / 32;
var i: usize = 0;
while (i < chunks) : (i += 1) {
const offset = i * 32;
const a: @Vector(32, u8) = db_code.bits[offset..][0..32].*;
const b: @Vector(32, u8) = query_bits[offset..][0..32].*;
const xored = a ^ b;
hamming += @popCount(xored); // SIMD popcount across lanes
}
// Handle remaining bytes
var j: usize = chunks * 32;
while (j < db_code.bits.len) : (j += 1) {
hamming += @popCount(db_code.bits[j] ^ query_bits[j]);
}
// Weighted distance estimation
const d_f: f32 = @floatFromInt(dim);
const ip_est = (2.0 / d_f) * query_l1 *
(@as(f32, @floatFromInt(dim / 2)) -
@as(f32, @floatFromInt(hamming)));
// Squared distance = ||q||^2 + ||x||^2 - 2*||x||*ip_est
return query_norm * query_norm +
db_code.norm * db_code.norm -
2.0 * db_code.norm * ip_est;
}
package rabitq
import (
"math"
"math/bits"
"unsafe"
)
// Code holds the quantized representation of a single vector.
type Code struct {
Bits []uint64 // binary code packed into 64-bit words
Norm float32 // ||x - centroid||
SumAbs float32 // sum of absolute projected components
}
// EstimateDistances computes approximate squared distances between
// a query and a batch of quantized database vectors.
// queryBits is the binarized query, queryNorm is ||q - centroid||,
// queryL1 is the L1 norm of the projected query.
func EstimateDistances(
queryBits []uint64,
queryNorm float32,
queryL1 float32,
codes []Code,
dim int,
results []float32,
) {
dimF := float32(dim)
halfDim := float32(dim / 2)
scale := 2.0 / dimF
for i, code := range codes {
// Compute Hamming distance via XOR + popcount
hamming := 0
for w := 0; w < len(queryBits); w++ {
hamming += bits.OnesCount64(queryBits[w] ^ code.Bits[w])
}
// Inner product estimate from popcount
ipEst := scale * queryL1 * (halfDim - float32(hamming))
// Squared Euclidean distance estimate
qn := queryNorm
cn := code.Norm
results[i] = qn*qn + cn*cn - 2.0*cn*ipEst
}
}
// BatchScan processes codes in cache-friendly blocks, scanning
// all queries against each block before advancing.
func BatchScan(
queries []QueryState,
codes []Code,
dim int,
blockSize int,
topK int,
) [][]Result {
allResults := make([][]Result, len(queries))
buf := make([]float32, blockSize)
for blockStart := 0; blockStart < len(codes); blockStart += blockSize {
blockEnd := blockStart + blockSize
if blockEnd > len(codes) {
blockEnd = len(codes)
}
block := codes[blockStart:blockEnd]
for qi, q := range queries {
EstimateDistances(
q.Bits, q.Norm, q.L1,
block, dim,
buf[:len(block)],
)
// Merge partial results into per-query top-k heap
for j, dist := range buf[:len(block)] {
allResults[qi] = heapInsert(
allResults[qi], topK,
Result{ID: blockStart + j, Dist: dist},
)
}
}
}
return allResults
}
RaBitQ is not just another heuristic binary quantization scheme. Its provable error bound of O(1/sqrt(d)) means you can predict and control approximation quality as a function of dimensionality. This makes it possible to size re-ranking candidate lists analytically rather than through trial-and-error tuning, and to provide formal guarantees on recall for production SLAs.
The power of RaBitQ lies in the alignment between its mathematical formulation and hardware capabilities. By reducing distance computation to XOR + popcount, it maps directly onto single-cycle SIMD instructions available on every modern CPU. This co-design principle — choosing algorithms whose bottleneck operations match hardware fast paths — is a recurring theme in high-performance systems and should guide the selection of quantization methods for any vector search deployment.