Lucene 和 Elasticsearch 中的改进二值量化 (BBQ)

改进二值量化在 Lucene 和 Elasticsearch 中的工作原理。

嵌入模型输出 float32 向量,通常对于高效处理和实际应用来说太大。Elasticsearch 支持 int8 标量量化以减小向量大小,同时保持性能。其他方法会降低检索质量,并且在实际应用中不切实际。在 Elasticsearch 8.16 和 Lucene 中,我们引入了改进二值量化 (BBQ),这是一种基于最近技术(由新加坡南洋理工大学的研究人员提出,称为“RaBitQ”)的见解而开发的新方法。

BBQ 是 Lucene 和 Elasticsearch 量化方面的一项重大进步,它将 float32 维度减少到比特,实现了约 95% 的内存减少,同时保持了较高的排名质量。BBQ 在索引速度方面(量化时间减少 20-30 倍)、查询速度方面(查询速度提高 2-5 倍)优于传统的 Product Quantization (PQ) 等方法,并且不会额外损失准确性。

在本博文中,我们将探讨 Lucene 和 Elasticsearch 中的 BBQ,重点关注召回率、高效的按位运算以及针对快速、准确的向量搜索而优化的存储。

“改进”二值量化中的“改进”是什么意思?

在 Elasticsearch 8.16 和 Lucene 中,我们引入了我们所说的“改进二值量化”。朴素的二值量化存在极大的信息损失,要获得足够的召回率,需要收集 10 倍或 100 倍的额外邻居进行重新排序。这显然是不够的。

于是,改进二值量化应运而生!以下是改进二值量化与朴素二值量化之间的一些重要区别

  • 所有向量都围绕一个质心进行归一化。这在量化中解锁了一些不错的特性。
  • 存储多个错误校正值。其中一些校正值用于质心归一化,一些用于量化。
  • 非对称量化。在这里,虽然向量本身存储为单个比特值,但查询仅量化到 int4。这在不增加存储成本的情况下显着提高了搜索质量。
  • 用于快速搜索的按位运算。查询向量被量化并以一种允许高效按位运算的方式进行转换。

使用改进二值量化进行索引

索引很简单。请记住,Lucene 构建单独的只读段。当新的段出现向量时,会增量计算质心。然后,一旦段被刷新,每个向量都会围绕质心进行归一化并进行量化。

这是一个简单的示例

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] \newline c = [0.65, 0.65, 0.52, 0.35, 0.69, 0.30, 0.60, 0.76] \newline v_{c1}' = v_{1} - c = [-0.09, 0.19, 0.01, -0.10, -0.23, -0.38, -0.05, -0.03] \newline bin(v_{c1}') = \left\{ \begin{cases} 1 & x\gt 0 \\ 0 & otherwise \end{cases} : x \in v_{c1}'\right\} \newline bin(v_{c1}') = [0, 1, 1, 0, 0, 0, 0, 0] \newline 0b00000110 = 6

当量化到比特级时,8个浮点数被转换为单个8位字节。

然后,每个比特都被打包到一个字节中,并与向量相似度所需的任何纠错值一起存储在段中。

对于每个向量,存储的字节数为dims/8个字节,然后是任何纠错值;欧几里得距离使用2个浮点数,点积使用3个浮点数。

快速讨论一下我们如何处理合并。

当段合并时,我们可以利用之前计算的质心。只需对质心进行加权平均,然后围绕新的质心重新量化向量。

棘手的问题是如何确保HNSW图的质量,并允许使用量化向量构建图。如果仍然需要所有内存来构建索引,那么量化的意义何在?!

除了将向量追加到现有的最大HNSW图之外,我们还需要确保向量评分可以利用非对称量化。HNSW有多个评分步骤:一个用于初始邻居的收集,另一个用于确保仅连接不同的邻居。为了有效地使用非对称量化,我们创建了一个所有向量量化为4位查询向量临时文件。

因此,当向量添加到图中时,我们首先

  • 获取存储在临时文件中的已量化查询向量。
  • 使用现有的位向量按常规搜索图。
  • 一旦我们有了邻居,多样性和反向链接评分就可以使用之前int4量化的值来完成。

合并完成后,临时文件将被删除,只留下位量化向量。

临时文件将每个查询向量存储为一个int4字节数组,占用dims/2个字节,一些浮点纠错值(欧几里得距离为3个,点积为4个),以及一个表示向量维度总和的短整型值。

非对称量化,有趣的部分

我提到了非对称量化以及我们如何为图构建设置查询。但是,向量是如何实际转换的?它是如何工作的?

“非对称”部分很简单。我们将查询向量量化为更高的保真度。因此,文档值是位量化的,查询向量是int4量化的。更有趣的是这些量化向量如何被转换以进行快速查询。

以我们上面提到的示例向量为例,我们可以将其量化为以质心为中心的int4。

vc1=v1c=[0.09,0.19,0.01,0.10,0.23,0.38,0.05,0.03]maxvc1=max(vc1)=0.19,minvc1=min(vc1)=0.38Q(xs)={(xminvc1)×15maxvc1minvc1:xxs}Q(vc1)={(x(0.38))×150.19(0.38):xvc1}={(x+0.38)×26.32:xvc1}=[8,15,10,7,4,0,9,9]v_{c1}' = v_{1} - c = [-0.09, 0.19, 0.01, -0.10, -0.23, -0.38, -0.05, -0.03] \newline max_{v_{c1}'}=max(v_{c1}')=0.19,min_{v_{c1}'}=min(v_{c1}')=-0.38 \newline Q(x_{s}) = \{(x-min_{v_{c1}'}) \times \frac{15}{max_{v_{c1}'} - min_{v_{c1}'}} : x \in x_{s} \} \newline Q(v_{c1}') = \{(x-(-0.38)) \times \frac{15}{0.19 -(-0.38)} : x \in v_{c1}' \} \newline = \{(x + 0.38) \times 26.32 : x \in v_{c1}' \} \newline = [8, 15, 10, 7, 4, 0, 9, 9]

有了量化向量,乐趣就开始了。因此,我们可以将向量比较转换为按位点积,位被移位。

也许最好直接可视化正在发生的事情

这里,每个int4量化值将其相对位置位移位到单个字节中。注意所有第一位是如何首先打包在一起的,然后是第二位,依此类推。

但这如何真正转化为点积?请记住,点积是分量积的总和。对于上面的示例,让我们完整地写出来

bin(vc1)Q(vc1)=[0,1,1,0,0,0,0,0][8,15,10,7,4,0,9,9]=[0×8+1×15+1×10+0×7+0×4+0×0+0×9+0×9]=15+10=25bin(v_{c1}') \cdot Q(v_{c1}') = [0, 1, 1, 0, 0, 0, 0, 0] \cdot [8, 15, 10, 7, 4, 0, 9, 9] \newline = [0 \times 8 + 1 \times 15 + 1 \times 10 + 0 \times 7 + 0 \times 4 + 0 \times 0 + 0 \times 9 + 0 \times 9] \newline = 15 + 10 = 25

我们可以看到,它只是存储向量位为1的查询分量的总和。并且由于所有数字都只是位,当使用二进制展开表示时,我们可以稍微移动一些东西来利用按位运算。

&\&之后将被翻转的位将是构成点积的数字的各个位。在本例中为15和10。

Remember our originally stored vectorstoredVecBits=bin(vc1)=[0,1,1,0,0,0,0,0]We rotate the combine the bits resulting instoredVectBits=0b11000000The query vector, int4 quantizedQ(vc1)=[8,15,10,7,4,0,9,9]The binary values of each dimensionbits(Q(vc1))=[0b1000,0b1111,0b1010,0b0111,0b0100,0b0000,0b1001,0b1001]We shift the bits and align as shown in above visualizationqVecBits=align(bits(Q(vc1)))=[0b11001010,0b00001110,0b00011010,0b11000111]qVecBits&storedVectBits={qVecBits&bits:bitsstoredVectBits}=[0b00000010,0b00000110,0000000010,0b0000110]\text{Remember our originally stored vector} storedVecBits = bin(v_{c1}') = [0, 1, 1, 0, 0, 0, 0, 0] \newline \text{We rotate the combine the bits resulting in} \newline storedVectBits = 0b11000000 \newline \text{The query vector, int4 quantized} \newline Q(v_{c1}') = [8, 15, 10, 7, 4, 0, 9, 9] \newline \text{The binary values of each dimension} \newline bits(Q(v_{c1}')) = [0b\textcolor{lime}{1}\textcolor{red}{0}\textcolor{cyan}{0}\textcolor{orange}{0}, 0b\textcolor{lime}{1}\textcolor{red}{1}\textcolor{cyan}{1}\textcolor{orange}{1}, 0b\textcolor{lime}{1}\textcolor{red}{0}\textcolor{cyan}{1}\textcolor{orange}{0}, 0b\textcolor{lime}{0}\textcolor{red}{1}\textcolor{cyan}{1}\textcolor{orange}{1}, 0b\textcolor{lime}{0}\textcolor{red}{1}\textcolor{cyan}{0}\textcolor{orange}{0}, 0b\textcolor{lime}{0}\textcolor{red}{0}\textcolor{cyan}{0}\textcolor{orange}{0}, 0b\textcolor{lime}{1}\textcolor{red}{0}\textcolor{cyan}{0}\textcolor{orange}{1}, 0b\textcolor{lime}{1}\textcolor{red}{0}\textcolor{cyan}{0}\textcolor{orange}{1}] \newline \text{We shift the bits and align as shown in above visualization} \newline qVecBits = align(bits(Q(v_{c1}'))) = [0b\textcolor{orange}{11001010}, 0b\textcolor{cyan}{00001110}, 0b\textcolor{red}{00011010}, 0b\textcolor{lime}{11000111}] \newline qVecBits \, \& \, storedVectBits = \{qVecBits \, \& \, bits : bits \in storedVectBits\} \newline = [0b00000010, 0b00000110, 0000000010, 0b0000110]

现在我们可以计算位数,移位并将它们加总在一起。我们可以看到,所有剩下的位都是15和10的位置位。

=(bitCount(0b00000010)<<0)+(bitCount(0b00000110)<<1)+(bitCount(0b00000010)<<2)+(bitCount(0b0000110)<<3)=(1<<0)+(2<<1)+(1<<2)+(2<<3)=25 = (bitCount(0b00000010) << 0) + (bitCount(0b00000110) << 1) + (bitCount(0b00000010) << 2) + (bitCount(0b0000110) << 3) \newline = (1 << 0) + (2 << 1) + (1 << 2) + (2 << 3) \newline = 25

与直接对维度求和的结果相同。

这是一个简化的 Java 代码示例。

byte[] bits = new byte[]{6};
byte[] queryBits = new byte[]{202, 14, 26, 199};
for (int i = 0; i < 4; i++) {
  sum += Integer.bitCount(bits[0] & queryBits[i] & 0xFF) << i;
}

好的,请展示数字。

我们已经对 Lucene 和 Elasticsearch 中的 BBQ 进行了广泛的测试。以下是一些结果。

Lucene 基准测试

此处的基准测试基于三个数据集:E5-smallCohereV3CohereV2。其中,每个元素表示在 [1, 1.5, 2, 3, 4, 5] 过采样下的 recall@100。

E5-small

这是从 Quora 数据集构建的 500k 个 E5-small 向量。

量化索引时间强制合并时间内存需求
bbq161.8442.3757.6MB
4 位215.1659.98123.2MB
7 位267.1389.99219.6MB
原始249.2677.81793.5MB

令人震惊的是,我们仅用一位精度就获得了 74% 的召回率。由于维度较少,BBQ 距离计算速度并没有比我们优化的 int4 快多少。

CohereV3

这是使用 CohereV3 模型的 100 万个 1024 维向量。

量化索引时间强制合并时间内存需求
bbq338.97342.61208MB
4 位398.71480.78578MB
7 位437.63744.121094MB
原始408.75798.114162MB

在这里,1 位量化和 HNSW 在仅 3 倍过采样下即可获得超过 90% 的召回率。

CohereV2

这是使用 CohereV2 模型和最大内积相似度的 100 万个 768 维向量。

量化索引时间强制合并时间内存需求
bbq395.18411.67175.9MB
4 位463.43573.63439.7MB
7 位500.59820.53833.9MB
原始493.44792.043132.8MB

很有意思的是,BBQ 和 int4 在此基准测试中表现出高度的一致性。令人惊喜的是,BBQ 仅用 3 倍过采样就能在内积相似度下获得如此高的召回率。

更大规模的 Elasticsearch 基准测试

我们更大规模向量搜索博客中所述,我们有一个用于更大规模向量搜索基准测试的Rally 轨道

此数据集包含 1.38 亿个 1024 维浮点向量。如果不进行任何量化,使用 HNSW 将需要大约 535 GB 的内存。使用更好的二进制量化,估计值将降至约 19 GB。

对于此测试,我们在 Elastic Cloud 中使用了一个 64GB 的节点,并使用以下轨道参数。

{
        "mapping_type": "vectors-only",
        "vector_index_type": "bbq_hnsw",
        "number_of_shards": 2,
        "initial_indexing_bulk_indexing_clients": 12,
        "standalone_search_clients": 8,
        "aggressive_merge_policy": true,
        "search_ops": [[10, 20, 0], [10, 20, 20], [10, 50, 0], [10, 50, 20], [10, 50, 50], [10, 100, 0], [10, 100, 20], [10, 100, 50], [10, 100, 100], [10, 200, 0], [10, 200, 20], [10, 200, 50], [10, 200, 100], [10, 200, 200], [10, 500, 0], [10, 500, 20], [10, 500, 50],[10, 500, 100],[10, 500, 200],[10, 500, 500],[10, 1000, 0], [10, 1000, 20], [10, 1000, 50], [10, 1000, 100], [10, 1000, 200], [10, 1000, 500], [10, 1000, 1000]]
}

重要提示:如果您想复制此测试,下载所有数据需要大量时间,并且需要超过 4TB 的磁盘空间。需要这么多磁盘空间的原因是,此数据集还包含文本字段,您需要磁盘空间来存储压缩文件及其解压后的数据。

参数如下:

  • k 是要搜索的邻居数量。
  • num_candidates 是每个分片中用于探索的候选者数量。
  • rerank 是要重新排序的候选者数量,因此我们将收集每个分片上的这么多值,收集总的 rerank 大小,然后使用原始 float32 向量对前 k 个值进行重新评分。

索引时间大约需要 12 个小时。这里不展示所有结果,只列出三个比较有趣的结果。

k-num_candidates-rerank平均访问节点数最佳 NDGC 的百分比召回率单查询延迟多客户端 QPS
knn-recall-10-100-5036,079.80190% 70% 18毫秒451.596
knn-recall-10-2015,915.211 78% 45% 9毫秒1,134.649
knn-recall-10-1000-200115,598.11797% 90% 42.534毫秒167.806

这表明平衡召回率、过采样、重新排序和延迟的重要性。显然,每个参数都需要根据您的具体用例进行调整,但考虑到这在以前是不可能的,现在我们可以在单个节点上拥有 1.38 亿个向量,这真是太棒了。

结论

感谢您抽出时间阅读关于更好的二进制量化 (Better Binary Quantization) 的内容。我来自阿拉巴马州,现在住在南卡罗来纳州,BBQ 一直在我心中占据着特殊的位置。现在,我更有理由热爱 BBQ 了!

我们将在 8.16 版本中以技术预览版发布此功能,或者您现在也可以在无服务器环境中使用。要使用此功能,只需将您的 dense_vector.index_type 设置为 bbq_hnsw  bbq_flat 在 Elasticsearch 中。

Elasticsearch 充满了新功能,可以帮助您为您的用例构建最佳搜索解决方案。深入了解我们的 示例笔记本 以了解更多信息,开始 免费云试用,或立即在您的 本地机器 上试用 Elastic。

准备好构建最先进的搜索体验了吗?

足够先进的搜索并非一蹴而就。Elasticsearch 由数据科学家、机器学习运维工程师、软件工程师以及许多其他对搜索充满热情的人共同打造,他们与您一样热爱搜索。让我们携手合作,构建出能够满足您需求的梦幻搜索体验。

亲自试用