Better Binary Quantization (BBQ) in Lucene and Elasticsearch

How Better Binary Quantization (BBQ) works in Lucene and Elasticsearch.

Want to get Elastic certified? Find out when the next Elasticsearch Engineer training is running! You can start a free cloud trial or try Elastic on your local machine now.

Embedding models output float32 vectors, often too large for efficient processing and practical apps. Elasticsearch supports int8 scalar quantization to reduce vector size while preserving performance. Other methods reduce retrieval quality and are impractical for real world use. In Elasticsearch 8.16 and Lucene, we introduced Better Binary Quantization (BBQ), a new approach developed from insights drawn from a recent technique - dubbed “RaBitQ” - proposed by researchers from Nanyang Technological University, Singapore.

BBQ is a leap forward in quantization for Lucene and Elasticsearch, reducing float32 dimensions to bits, delivering ~95% memory reduction while maintaining high ranking quality. BBQ outperforms traditional approaches like Product Quantization (PQ) in indexing speed (20-30x less quantization time), query speed (2-5x faster queries), with no additional loss in accuracy.

In this blog, we will explore BBQ in Lucene and Elasticsearch, focusing on recall, efficient bitwise operations, and optimized storage for fast, accurate vector search.

Note, there are differences in this implementation than the one proposed by the original RaBitQ authors. Mainly:

  • Only a single centroid is used for simple integration with HNSW and faster indexing
  • Because we don't randomly rotate the codebook we do not have the property that the estimator is unbiased over multiple invocations of the algorithm
  • Rescoring is not dependent on the estimated quantization error
  • Rescoring is not completed during graph index search and is instead reserved only after initial estimated vectors are calculated
  • Dot product is fully implemented and supported. The original authors focused on Euclidean distance only. While support for dot product was hinted at, it was not fully considered, implemented, nor measured. Additionally, we support max-inner product, where the vector magnitude is important, so simple normalization just won't suffice.

What does the "better" in Better Binary Quantization mean?

In Elasticsearch 8.16 and in Lucene, we have introduced what we call "Better Binary Quantization". Naive binary quantization is exceptionally lossy and achieving adequate recall requires gathering 10x or 100x additional neighbors to rerank. This just doesn't cut it.

In comes Better Binary Quantization! Here are some of the significant differences between Better Binary Quantization and naive binary quantization:

  • All vectors are normalized around a centroid. This unlocks some nice properties in quantization.
  • Multiple error correction values are stored. Some of these corrections are for the centroid normalization, some are for the quantization.
  • Asymmetric quantization. Here, while the vectors themselves are stored as single bit values, queries are only quantized down to int4. This significantly increases search quality at no additional cost in storage.
  • Bit-wise operations for fast search. The query vectors are quantized and transformed in such a way that allows for efficient bit-wise operations.

Indexing with Better Binary Quantization (BBQ)

Indexing is simple. Remember, Lucene builds individual read only segments. As vectors come in for a new segment the centroid is incrementally calculated. Then once the segment is flushed, each vector is normalized around the centroid and quantized.

Here is a small example:

v1=[0.56,0.85,0.53,0.25,0.46,0.01,0.63,0.73]c=[0.65,0.65,0.52,0.35,0.69,0.30,0.60,0.76]vc1=v1c=[0.09,0.19,0.01,0.10,0.23,0.38,0.05,0.03]bin(vc1)={{1x>00otherwise:xvc1}bin(vc1)=[0,1,1,0,0,0,0,0]0b00000110=6v_{1} = [0.56, 0.85, 0.53, 0.25, 0.46, 0.01 , 0.63, 0.73] c = [0.65, 0.65, 0.52, 0.35, 0.69, 0.30, 0.60, 0.76] v_{c1}' = v_{1} - c = [-0.09, 0.19, 0.01, -0.10, -0.23, -0.38, -0.05, -0.03] bin(v_{c1}') = \left\{ \begin{cases} 1 & x\gt 0 0 & otherwise \end{cases} : x \in v_{c1}'\right\} bin(v_{c1}') = [0, 1, 1, 0, 0, 0, 0, 0] 0b00000110 = 6