Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

VSAG 博客

欢迎访问 VSAG 博客。这里会汇总项目相关的技术文章与更新。

RaBitQ 简介(2025-07-16)

极致的二值量化方法— 深度解读 RaBitQ及其在 VSAG 上的工程实践

本文作者:@ShawnShawnYou @mingyu-hkustgz

1、前言

(1)背景

向量检索是现代人工智能应用中的核心技术,广泛应用于 RAG(检索增强生成) 、搜索引擎 、推荐系统 等场景。随着深度学习的发展,高维向量变得越来越重要,因为它们能够有效捕捉数据的语义信息,为精准匹配和理解提供支持。在OpenAI最新的嵌入模型中,将文本段嵌入成 1536 甚至更高到 3072 维的向量。这对于向量检索过程引入了巨大的挑战。

挑战一:距离计算开销。在高维向量检索过程中,距离计算占比超过 90% 。这部分的计算复杂度随着维度的提升而提升。要提高检索应用的响应速度和吞吐,首要的就是降低距离计算开销。

挑战二:内存成本。如图1,对于包含 100W 条向量的 OpenAI-1536 数据集来说,每个向量由 1536 维的 32 位浮点数构成。光是存储向量数据的成本已经需要 9.55GB 。也就是说,即使是没有任何索引,也要求应用至少有 9.55GB 的内存占用,这是极大的挑战。

图 1 :OpenAI-1536 数据集展示(来源 Huggingface )

(2)什么是量化

量化(Quantization) 是一种将高精度向量映射为低精度向量的技术。有多个好处:

  1. 计算速度快
  • 通过将高维浮点向量压缩为低精度表示(如从 32 位浮点数压缩到 4 位或 8 位整数),可以显著减少距离计算的开销。
  • 使用量化可以将距离计算速度提升 2 倍以上,极大加速检索过程。
  1. 内存占用少
  • 压缩后的低精度向量占用更少的存储空间,从而大幅降低内存需求。

  • 例如,使用 4-bit 标量量化时,内存占用仅为原始浮点向量的 1/8 ,这对大规模数据集尤为重要。

(3)极致的二值量化方法—RaBitQ

RaBitQ 是南洋理工大学 Cheng LONG 老师及 Jianyang Gao 博士在数据库顶级会议 SIGMOD 2024 上发表的量化技术。其是一种新型的 二值(Binary) 量化方法,能将向量的每一维表示从 float-32 压缩为 1-bit ,极限压缩 32 倍内存成本。相比于传统的 PQ 和 SQ 量化,有以下几个好处:

  1. 极致压缩比 :RaBitQ 将每一维用 0 或者 1 来表示,引入了高达 32 倍的极致量化压缩比。
  2. 高精度 :通过组合随机旋转、残差量化、无偏估计器等策略,RaBitQ 在精度上媲美低压缩比的量化方法。
  3. 与基于图的方法兼容 :相比于 PQ 对 FastScan 强依赖,RaBitQ 更容易适配于基于图的索引方法,并提供比 SQ 更高的量化精度和压缩比率。
  4. 与基于倒排的方法兼容 :RaBitQ 也可以结合 FastScan ,从而在基于倒排的方法上也能获得高精度和高性能。

接下来,我们走入论文,细致解析为什么 RaBitQ 能在 极高压缩比 上达到 高精度 和 高性能 的效果。

2、RaBitQ 的核心竞争力

同样是二值量化,RaBitQ 的应用场景比朴素的二值量化有更长远的应用和研究价值。我们首先讨论二值量化这一思路的可行性,以及为什么朴素的二值量化不可行。

(1)朴素的二值量化:可行性和局限性

可行性: 我们以欧几里得距离( L2 )距离为例,给定维度 D 的两个向量 $x$ 和向量 $y$ ,其欧式距离如下:

$$ dis = |x-y|^2 = \sum_{d =1}^D(x[d]-y[d])^2 $$

例 1 如图 2 ,假设 $x=[1,1,1,…,1,1,1],y=[0,0,0…,0,0,0]$ ,容易计算出 $|x-y|^2 = 1000$ 。考虑第一维 $(x[1]-y[1] )^2= 1$ ,该维的信息对向量距离的贡献只有千分之一。而如果向量的维度只有 3 ,那么该维对向量距离的贡献是三分之一。

图 2 :不同维度下维度对距离贡献示例

因此,针对低维向量,量化导致的每一维的信息损失对向量距离的更大。这限制了我们必须使用高精度的量化策略才能维持检索的准确度。而针对高维向量,则可以使用更极端的量化策略。因为即使是是造成了高量化损失,对最终的距离影响也不明显。这驱动我们可以在高维向量上使用二值量化。具体而言,二值量化直接使用向量每一维的符号作为该维的bit位表示,从而将 D 维向量 压缩成 D 位 bit 串 。大于零的部分采用 1 表示,反之采用 0 。例如对于 8 维向量 $x = [1, 1, -1,-1,1,1,-1,-1]$ ,则可以使用一个字节大小(8 bits)来表示: $\bar{x}=00110011_b$ (注:bit串最右是最低位)。

局限性: 二值量化仍然引入了不小的量化损失。例如考虑另一个 8 维向量 。其虽然量化表示和 完全一致。但是这两个向量的每一维的值域完全不同(相差 100 倍)。而没有额外信息能指导如何从 解码还原。也就是说,二值量化仍然引入了较大的量化误差。RaBitQ引入了三项核心技术来解决这一误差问题。

(2)RaBitQ 的三驾马车

在 RaBitQ 中,主要有三个核心思路来提高二值量化的可用性,分别是预处理,无偏估计和 bitwise 距离计算。

(a)预处理

由于二值量化本身信息熵极小,向量维度之间的原始信息无法记录,精度损失较大。在几种情况下会有比较大影响:维度间的方差,向量间的方差。

为了解决这两个问题,RaBitQ 采用了一系列对向量的预处理:

a. 随机旋转:在二值量化前,先随机旋转原始向量。让不同维度的数据更加“均匀”,减少维度间的方差。 b. 残差量化:类似 IVFPQ 的残差量化,只处理原始向量和质心之间的残差向量。 c. 归一化:将残差向量归一化,使得其分布在单位球面上。为后续的处理提供强约束。 具体来说,假设原始入库向量为 $o_r$ ,查询向量为 $q_r$ ,质心为 $c$ 。计算两个向量的 L2 距离可以转化为:

$$ |o_r-q_r|^2 = |(o_r-c)-(q_r-c)|^2 \ =|o_r-c|^2+|q_r-c|^2 - 2 \cdot |o_r-c|\cdot |q_r-c|\cdot < o,q > $$

其中 $o=\frac{(o_r-c)}{|(o_r-c)|},q= \frac{(q_r-c)}{|(q_r-c)|}$ ,是归一化向量。 $|o_r-c|$ 和 $|q_r-c|$ 都可以预计算出,而不需要实时计算。从而距离计算问题被归约到$< o,q >$的内积计算问题。内积计算即可通过二值量化后的向量来进行快速模拟。

例 2 假设 $\bar{o}=00110011_b,\bar{q} = 00110011_b$ 。那么内积即可通过两个比特串的与操作,并统计bit位为1的个数来得到距离。现代CPU通常提供了高效的popcnt指令来支持这一功能。

然而,使用比特与计算出的内积和原始向量内积不在一个空间。计算出的距离只是一个“标识”,能区分大小但是无法还原出完整的距离。

(b)RaBitQ的精度核心:无偏估计

在RaBitQ中,用于将二值量化距离还原为真实距离的核心组件是无偏估计器。其保证了RaBitQ即使是在高压缩比下也有极佳的量化精度。

  1. 二值码本 :对于二值向量,其中所有的1比特项被还原为 $+\frac{1}{\sqrt{D}}$ ,0比特项被还原为 $-\frac{1}{\sqrt{D}}$ 。这样还原的好处在于,还原后的向量模长为1。
  2. 无偏估计器 :通过原文里一系列理论证明,可以通过 $\frac{< \bar{o}, q >}{< \bar{o}, o >}$ 来估计 $< o, q >$ 。其中, $\bar{o}$ 是一个底库向量的二值量化向量。同时, $< \bar{o}, o >$ 可以被预计算并存储。在计算时,只需要计算一个二值量化的向量和FP32表示的预处理后查询向量的距离即可。

在向量索引的内存表示上,可以只存储比特编码的底库向量。在查询时,只需要使用无偏估计器来对比特编码的向量和预处理后的查询向量距离。 从而能让 RaBitQ 既保持了极限的量化压缩比(从 FP32 压缩为 1 bit ),也保持了较高的量化精度。

例 3 假设有底库向量 $o_r = [1, 1, -1,-1,1,1,-1,-1]$ ,质心 $c = [0, 0, 0,0,0,0,0,0]$ 。则通过归一化和残差预处理后有 $o = [0.353, 0.353, -0.353,-0.353,0.353,0.353,-0.353,-0.353]$ 。其使用二值量化后的结果为 $\bar{o}=00110011_b$ (注:bit串最右是最低位),还原成FP32表示后的向量为 $\bar{o} = [\frac{1}{\sqrt{8}}, \frac{1}{\sqrt{8}}, -\frac{1}{\sqrt{8}},-\frac{1}{\sqrt{8}},\frac{1}{\sqrt{8}}, \frac{1}{\sqrt{8}},-\frac{1}{\sqrt{8}},-\frac{1}{\sqrt{8}}]$ ,也就是 $\bar{o} = [0.353, 0.353, -0.353,-0.353,0.353,0.353,-0.353,-0.353]$ 。在建立索引阶段即可预计算出 $< \bar{o}, o > = 1.0$ 。 给定另一个查询向量 $q_r = [1, 1, -1,-1,1,1,-1,-1]$ 。则通过归一化和残差预处理后有 $q = [0.353, 0.353, -0.353,-0.353,0.353,0.353,-0.353,-0.353]$ 。在实时查询阶段可以计算出 $< \bar{o}, q> = 1.0$ 。从而,根据无偏估计器可以计算 $< o, q > = \frac{< \bar{o}, q >}{< \bar{o}, o >} = \frac{1.0}{1.0}=1.0$ 。 $|o_r-q_r|^2 = |(o_r-c)-(q_r-c)|^2 \ =|o_r-c|^2+|q_r-c|^2 -2\cdot |o_r-c|\cdot |q_r-c|\cdot < o,q > \ =8 + 8 -2 * \sqrt{8}*\sqrt{8}*1.0=0$。 在这个情况下,直接使用原始的FP32表示的向量计算两个的L2距离是 $|o_r-q_r|^2 =0$ 。可以发现,使用RaBitQ后在这个例子中的量化误差为0。

然而,在计算时,仍然需要将向量还原为 FP32 的表示。这使得计算复杂度仍然和两个 FP32 的向量做计算没有差别。 RaBitQ 通过 bitwise 距离计算来对计算过程进行加速。

(c)RaBitQ 的性能核心:bitwise 距离计算

在 RaBitQ 的二值量化处理后,底库向量已经都被量化为了比特串。所有的计算复杂度瓶颈都在查询向量上,因为其是 FP32 表示的。这一点和木桶效应相似,计算的瓶颈在 FP32 这一“长板”上。在结合 SIMD 中的 AVX512 指令集优化时,单条指令最多处理 512 位的数据。如果能降低向量每一维度的表示精度,从 32 位降低为 8 位或者更低。能在 AVX512 指令中一次处理更多的维度,从而显著加速计算。因此,可以引入标量量化来将查询向量量化为 INT4,INT8 等类型,降低计算复杂度。

以 4-bits 标量量化为例,当查询向量被量化到了每一维使用 4 位表示时。一条 AVX512 加载指令能比使用 32 位表示多加载 8 倍更多的维度! 这表示计算吞吐的理论上限最高为 8 倍! 在此基础上, RaBitQ 创新性地提出了按比特纵向分解的 bitwise 距离计算算法 。能高效计算一个二值量化的向量和标量量化向量的距离。

图 3 :bitwise 计算示例

例 4 对于红色表示的二值向量和蓝色表示的 4-bits 量化向量。其中 4-bits 量化向量每一列对应了一个 4-bits 表示的维度值。一共有 D 维,所以有 D 列。固定第一列,从上到下每一行分别表示列 4 个位的值。我们以2维向量举例。例如对于使用 4-bits 标量量化后的查询向量 $\bar{q}_u=[1010_b,0101_b]=[10,5]$ ,对于二值量化后的底库向量 $\bar{x}_b=01_b$ 。可以将 $\bar{q}_u$ 在存储上按照每一维的低位到高位分别存储。例如对于两维的第 0 位分别是 0 和 1 ,第 1 位是 1 和 0 等等,转换存储后的表示为 $\bar{q}_u=[\bar{q}^{(0)}_u,\bar{q}^{(1)}_u,\bar{q}^{(2)}_u,\bar{q}^{(3)}_u]=[10_b,01_b,10_b,01_b]$ 。在计算距离时,可以使用 4 次 bitwise 与操作分别计算,并求和。 计算方式为:

$$ \lt \bar{x}_b,\bar{q}_u \gt = $$

$$ \sum_{j=0}^{3}2^j\cdot < \bar{x}_b,\bar{q}_u^{(j)} > = $$

$$ 2^0 * 0 + 2^1 * 1+2^2 * 0+2^3 * 1 = 10 $$

这样, RaBitQ 将一次向量计算分解为 4 次 D-bits 的按位与操作。再结合 AVX512 指令集加速能极大程度加快整个距离计算速度。然而,如果只是单纯使用 AVX512 + popcnt 并不能完美发挥这一 bitwise 计算的性能优势。在原论文和仓库中,原作者还实现将这一操作与 FastScan 结合起来。FastScan 通过查表操作能进一步极限提高 RaBitQ 的性能。由于篇幅原因,笔者在此不对 FastScan 展开介绍。

3、RaBitQ 进阶优化 — MRQ

RaBitQ 要求量化比特位等于向量维数,导致压缩比固定,限制了其效率和灵活性。我们团队创新性提出 MRQ 来解决这一缺陷。MRQ 利用数据分布来实现更好的距离校正和更高的向量压缩比。实验结果表明,MRQ-IVF 的性能显著优于基于向量量化的最先进的 AKNN 搜索方法,在相同的搜索精度下,仅使用 1/3 的量化位,即可实现高达 3 倍的效率提升。

(1)主要发现

我们从向量数据每一维的方差开始分析。现有的向量数据通常由使用神经网络获取的音频、图像和文本的高维嵌入组成,通常超过 1000 维。我们发现,应用 PCA 后,向量数据的方差遵循长尾分布。

如图 4 所示,使用 OpenAI-1536 文本嵌入模型生成的数据,PCA 后的前 512 个维度捕获了近 90% 的方差,而近 1000 个残差维度仅捕获了约 10% 的方差。其他类型的嵌入也表现出同样的现象,例如来自 Deep 数据集的图像嵌入,经过 PCA 旋转后,只需要 128 个维度就能保留与原始数据 90% 的方差。

图 4 :DEEP 和 OpenAI-1536 两个数据集通过 PCA 降维后的方差

(2)MRQ(Minimized Residual Quantization)

基于上述观察,我们考虑一种更灵活的基于几何关系的量化方法,使其不受向量维数的限制。同时,该算法仍然需要适应误差驱动的距离校正;因此,我们进行了进一步的误差分析,以确保其能够利用误差界限进行距离校正。通过观察PCA投影后数据维度的方差分布,一个直观的想法是将数据投影到低维空间,然后对投影后的向量进行量化。这样,向量量化的压缩率可以通过投影的维数来控制。

具体来说,考虑一个向量数据集,其中包含位于 $D$ 维欧氏空间 $\mathbb{R}^{D}$ 中的点。向量$x \in \R^D$可以使用正交矩阵 $\mathbf{R}$投影到$d$维空间$d<D$ ,即 $x_d = \mathbf{R}x$ 。然后,我们可以将RabitQ量化方法应用于 $x_d$ 。那么,数据库向量 $x$ 和查询向量 $q$ 之间的精确平方欧氏距离 $dis$ 可以表示为:

$$ dis = ||x -q|| = ||x_d -q_d|| + ||x_r - q_r|| $$

其中 $x_r$ 是向量残差维数,其中,$r+d=D$。RabitQ 要求对向量进行归一化和中心化。投影距离 $||x_d-q_d||^2$ 可以进一步表示为,设 $c$ 为投影数据 $x_d$ 的质心,归一化后,投影距离 $||x_d - q_d||^2$ 可以转换为内积和向量范数的组合。投影内积部分可以通过 RaBitQ 直接量化,使用 $d$ 位二进制向量,只留下残差部分 $|x_r-q_r|^2$ 。然后,我们还对残差维度的范数进行了拆分,将欧氏距离转化为投影和残差部分。得到以下公式:

$$ ||x-q||^2 = ||x_d -q_d||^2 + ||x_r||^2 + ||q_r||^2 -2 \cdot < x_r,q_r> $$

其中,基向量的残差部分和查询向量的残差部分的范数项 $||x_r||^2 + ||q_r||^2$ 可以预先计算并存储,单次搜索只需计算一次。残差部分的内积项 $-2 \cdot < x_r,q_r>$ 则是误差项。 $||x_d -q_d||^2$ 即是使用 RaBitQ 计算两个 PCA 降维后的向量的 L2 距离。分解后的示例如图 5 :

图 5 :使用 MRQ 对 PCA 降维后的向量做分解

(3)实验结果

在具体的实现中,我们还进一步优化了缓存命中率(方法 MRQ+ )。与 RaBitQ 相比,MRQ 由于使用了更少的量化位数,可以实现了更高的内存 AKNN 搜索效率。MRQ 仅使用了 RaBitQ 量化位数的 1/7 ,在 GIST 数据集上实现了 2 倍的搜索延迟降低,以及相对 HNSW 的 4x 性能提升。如图 6 所示:

图 6:MRQ + IVF 在 GIST 和 DEEP 数据集的性能表现

值得注意的是,MRQ 不能提供更准的距离估计,相反使用降维方法会对量化精度有一定影响。性能提升的核心在于更少的量化位所对应的代价更低的距离计算。残差维度对整体量化误差影响受数据分布影响较大,我们在 MRQ 的论文中详细讨论了这一点。

4、VSAG 中使用 RaBitQ

(1)启用参数示例

如下代码是在 VSAG 中启用 RaBitQ,重排阶段使用磁盘中的 FP32 向量的参数示例:

std::string hgraph_build_parameters = R"(
    {
        "dtype": "float32",
        "metric_type": "l2",
        "dim": 960,
        "index_param": {
            "base_quantization_type":"rabitq",
            "rabitq_use_fht":true,
            "rabitq_pca_dim":512,
            "rabitq_query_dims":4,
            "use_reorder":true,
            "precise_quantization_type":"fp32",
            "precise_file_path":"./rabitq_fht_500_36_reorder",
            "precise_quantization_type":"fp32",
            "precise_io_type":"buffer_io"
            "max_degree": 72,
            "ef_construction": 500
        }
    }
    )";

所有参数如表1 :

参数名范围含义推荐用法
必填base_quantization_type“rabitq”启用 RaBitQ“rabitq”
use_reordertrue使用重排和高精度向量true
precise_quantization_type“fp32”, “sq8” 等高精度向量的量化方法精度关键场景使用 “fp32” 内存关键场景使用 “sq8”
precise_io_type“buffer_io” , “async_io” 等使用磁盘中的高精度向量做重排“buffer_io”
precise_file_path文件路径磁盘中的高精度向量的路径用户确定
可选rabitq_use_fhttrue 或 false是否使用 FHT 加速搜索过程,会有一定的精度损失,但在大部分场景下有显著 QPS 提升。true
rabitq_pca_dim小于数据的维度,例如数据为 1024 维,可以设置该值为 512使用 PCA 对原始向量降维,进一步提高 RaBitQ 的内存成本节省。数据维度的 1 / 2 到 3 / 4 。最好是 256 的整数倍,以获取高效的SIMD 性能。
rabitq_bits_per_dim_query4 或 32搜索时 query 使用标量量化的比特位。4 bit 量化是兼顾工程优化,精度,QPS 的较优值。在 Hgraph 当前版本中推荐不设置该值(默认为 32 )。因为其性能收益并不明显,依赖后续对 FastScan 的适配。

表 1 :VSAG 上使用 RaBitQ 的参数列表

(2)性能参考

我们在 GIST1M 数据集上测试了在 VSAG(版本 commit id:dec00f)上 RaBitQ 算法。注意在当前版本中,我们仅实现了 MRQ 的 PCA ,没有实现 FastScan 和缓存优化。如表 2 所示,为召回率大于 90% 的性能表现和内存成本,仅供参考:

量化方法QPS纯向量内存总内存
fp325283.57 GB (1)3.91 GB (1)
sq85580.89 GB (1 / 8)1.19 GB (~ 1 / 3)
rabitq779 (~ 1.5x)0.11 GB (1 / 32)0.55 GB (~ 1 / 8)
rabitq + pca 4805120.05 GB (1 / 64)0.37 GB (~ 1 / 10)

表 2 :VSAG 上 Hgraph 结合各个量化方法的性能对比

可以观察到,通过极致压缩向量在索引中的内存占比(压缩率 32 倍到 64 倍),能极大程度减少总的内存开销(压缩率 8 倍到 10 倍)。相比于使用8bit标量量化,有明显的内存收益。同时,相较于 fp32 和 sq8,也有显著的 QPS 提升(从 528 到 779 ,提升大约 1.5 倍)。

5、总结

RaBitQ 是最近学术界和业界最热门的量化技术,能以极高的性价比来压缩原始向量。其提供了 32 倍的向量压缩比的同时,仍然保持了较高的召回率和更快的计算速度。在结合 PCA、MRQ 等技术后,VSAG 可以进一步提高 RaBitQ 的压缩率,到 64 倍以上。在内存攸关的场景下,可以将原始向量放到磁盘中,或者只存储在外部的数据库引擎中用于重排。对于向量检索过程,只使用 RaBitQ 量化后的向量,能极大程度减少内存成本(整体 10 倍的内存减少),并带来明显的 QPS 提升。

6、参考

(1)数据集地址:

[1] OpenAI-1536数据集:https://huggingface.co/datasets/Qdrant/dbpedia-entities-openai3-text-embedding-3-large-1536-1M

[2] OpenAI-3072数据集:https://huggingface.co/datasets/Qdrant/dbpedia-entities-openai3-text-embedding-3-large-3072-1M

[3] GIST-1M数据集:http://corpus-texmex.irisa.fr/

(2)参考文献:

[1] RaBitQ 论文:https://doi.org/10.1145/3654970

[2] RaBitQ 博客:https://dev.to/gaoj0017/quantization-in-the-counterintuitive-high-dimensional-space-4feg

[3] MRQ 论文:https://arxiv.org/abs/2411.06158

(3)参考代码:

[1] VSAG:https://github.com/antgroup/vsag

[2] RaBitQ:https://github.com/VectorDB-NTU/RaBitQ-Library

[3] MRQ:https://github.com/mingyu-hkustgz/RESQ

SINDI 简介(2026-07-13)

稀疏向量检索方案:SINDI 在 VSAG 上的设计与实践(ICDE 2026)

本文作者:蚂蚁集团 VSAG 团队 李若瑄

1、为什么需要 SINDI

(1)从用户问题说起

在阅读本文之前,读者应该对向量检索、倒排索引等基础概念有一定了解。读完本文后,你将了解:1)稀疏向量检索的核心挑战;2)SINDI 的窗口化倒排、多层剪枝、量化与重排等关键设计;3)如何在 VSAG 中配置和调优 SINDI 索引。

在 RAG(Retrieval-Augmented Generation,检索增强生成) 、智能搜索和推荐系统里,用户的提问越来越“混合”。有人会直接搜产品型号、错误码或专有名词,有人会描述一个模糊需求,也有人会用同义词、简称、口语表达来提问。在实际检索系统中,召回链路既要听懂意思,又不能放过关键字。只靠一条召回链路,很难把这些情况都覆盖住。

稠密向量召回(Dense Retrieval) 擅长语义泛化:用户没有使用原文关键词时,模型仍然可以通过语义相似性找回相关内容。但它也有明显短板,例如专有名词、产品型号、错误码、长尾实体、代码符号等细粒度词项,往往需要非常精确的匹配信号。

传统词项召回(BM25 / TF-IDF) 擅长关键词匹配:BM25(Best Match 25,一种基于概率模型的词项权重算法)和 TF-IDF 会根据词项匹配关系给出稳定、可解释的分数。但它们缺少深层语义理解,对同义改写、上下文语义和隐式意图不够敏感。

所以在工业检索里,更常见的做法是把 BM25 + 稀疏向量 + 稠密向量 放在一起用。BM25 负责字面匹配;稀疏向量负责可解释的语义化词项匹配;稠密向量负责整体语义泛化。三路信号互补后,召回效果通常会高于任意两路组合。

SINDI 要解决的,就是实现更高性能、更低成本的 稀疏向量检索(Sparse Vector Search) 。

(2)什么是稀疏向量

稀疏向量(Sparse Vector) 是一种只记录高维词表或特征空间中少量非零 词项(Term) 及其权重的向量表示。

稀疏向量的关键特征可以被概括成以下三点:

  1. 高维且极度稀疏:维度可以达到几万甚至几十万,但每条文档或查询只包含少量非零项。
  2. 语义化的词项扩展:学习稀疏模型(Learned Sparse Model) 不只统计原文词频,还可以通过模型激活与上下文相关的词项和同义词,让稀疏向量具备一定语义扩展能力。
  3. 可解释性强:每个非零维度都对应明确的词项或特征,权重也可以直接参与排查。线上出现 bad case 时,研发人员能够看到是哪些 term 拉高或拉低了分数。

图 1:稀疏向量和稠密向量的表示差异

例 1 如图 1,同一句 “I love black cat” 可以被表示成两类完全不同的向量。稠密向量(Dense Vector) 会把整句话压成一段固定长度的浮点数组,例如 [0.18, -0.42, 0.07, ...]。这些维度共同表达整体语义,但单独看某一维,通常无法解释它对应哪个词或特征。

稀疏向量(Sparse Vector) 则建立在一个很大的词表空间上。图 1 中,词表空间可以达到 30,000 维,但真正有权重的位置只有少数几个。原文中的 love、black、cat 会被记录下来,学习稀疏模型还可能进一步激活 feline、kitty 这类和 cat 语义相关的词项。

存储时,稀疏向量通常不会保存完整的 30,000 维数组,而是只保存非零项及其权重,压缩成 {love: 0.71, black: 0.82, cat: 0.93, feline: 0.51, kitty: 0.28}。在工程实现中即保存为一组 (term_id, weight) 对。这样既保留了可解释的词项信号,也避免了为大量 0 值浪费存储空间。

因此,二者的核心区别在于:稠密向量每一维通常不可直接解释,但整体语义表达能力强;稀疏向量每一维都对应实际词项,通过激活相关词支持一定的语义能力。

所以,稀疏向量不是“只会做关键词匹配”。它也有一定语义能力,只是表达方式仍然保留在词表空间里。

稀疏向量检索的核心是 最大内积搜索(Maximum Inner Product Search,MIPS) 。给定查询稀疏向量 $ q $ 和文档稀疏向量 $ d $,只需要计算两边重合词项上的 权重乘积和 :

$$ score(q,d)=\sum_{t \in q \cap d}q_t \cdot d_t $$

VSAG 的 SINDI 正是围绕这一计算模式设计的稀疏向量索引。

(3)SINDI 带来的核心价值

SINDI 由 VSAG 团队与华东师范大学合作研发,发表于 ICDE(IEEE International Conference on Data Engineering,数据库领域 CCF-A 类顶级会议) 。它适合 BM25 、SPLADE(一种基于 Transformer 的学习稀疏模型) 、BGE-M3 sparse(BAAI 开源多语言嵌入模型中的稀疏向量版本) 等召回链路中的稀疏表示。

和传统倒排索引、图索引相比,SINDI 的第一点优势是 低成本索引构建。这里的低成本,主要体现在两件事上:构建速度快,索引规模小。在 1000 万规模的中文数据集上,SINDI 的构建时间只有 33.0 秒,索引规模为 4.99 GB。作为对比,传统倒排方案需要 551.4 秒,索引规模达到 19.18 GB;开源图索引方案的索引规模接近,但构建时间达到 1915.77 秒。

图 2:索引规模和构建时间对比

第二点优势是 效率和精度的平衡 。在高召回目标下,SINDI 仍然能保持很高吞吐。下表里,Zilliz 是 BigANN Benchmark sparse track 的 SOTA(State of the Art,当前最优) 索引,PyANNS 是开源 SOTA 索引。可以看到,当召回要求从 90% 提升到 99% 时,SINDI 的 QPS(Queries Per Second,每秒查询数) 均为 最优,且下降更为平缓。

方案召回 90% QPS召回 95% QPS召回 99% QPS
SINDI14236118393920
Zilliz138158046<2000
PyANNS124276025<2000

第三点优势是 参数更容易分析 。SINDI 提供了静态和动态两类索引分析能力,可以指导开发者分析索引内存利用率、词表和数据预处理是否匹配、倒排链是否存在明显长尾,以及 剪枝(Pruning) 、量化(Quantization) 、候选池(Candidate Pool) 和 重排(Reranking) 分别对召回与耗时产生了多大影响。这样在调整候选数量、查询剪枝、词项剪枝、文档剪枝、量化和重排参数时,索引更易调优,可以在使用成本和效果之间取得更好的平衡。

在设计 SINDI 时,团队也对比过纯图索引方案和纯倒排方案。图索引在极高召回场景下有较强表达能力,但构建成本和参数复杂度较高;纯倒排方案构建快、结构简单,但查询吞吐容易受长倒排链和随机访问影响。SINDI 最终选择窗口化带权倒排结构,是在构建成本、查询性能、内存占用和召回率之间的综合权衡。

下面继续看 SINDI 是怎么做到这些效果的。

2、SINDI 的关键设计

(1)Window-based 带权倒排结构

图 3:SINDI 的窗口化倒排结构

稀疏向量检索的瓶颈除了乘法和加法计算本身,还有内存访问。

传统倒排表和图索引如果只保存 doc id(文档编号) ,计算分数时还需要回到原始向量区域查找 term 权重 。这个过程会带来大量 随机访问(Random Access) ,CPU Cache 命中率 就会下降。CPU Cache 是处理器高速缓存,命中率越高说明数据读取越高效;一旦频繁缓存未命中,吞吐就很容易被内存延迟限制住。

SINDI 采用的是 带权倒排列表(Value-based Inverted List) :每个 term list(词项倒排链) 不只保存文档编号,同时保留该 term 在文档里的权重。查询访问某个 term 时,可以沿着一段连续数组顺序读出 局部文档编号 + 文档侧权重 ,直接计算 查询侧权重 × 文档侧权重 ,再累加到距离数组里。这样查询时按数组顺序扫描,减少回查原始向量带来的随机跳转。

SINDI 还引入了 Window-based(窗口化) 组织方式。这里的窗口可以理解为一批连续编号的文档,每个窗口大小相同。构建时,全量文档会被切成多个窗口,每个窗口维护一组独立的 term list。窗口分片限制了单次处理的文档范围,距离数组只需要覆盖当前窗口。相比维护一张面向全量文档的巨大分数表,窗口内的距离数组更容易留在缓存里,更新得分时也能减少随机访问带来的 缓存未命中(Cache Miss) 。同时,窗口内 doc id 以 uint16_t(无符号 16 位整数类型,可表示 0 到 65535 的范围)保存,因此 window_size 被限制在 10,000 到 60,000 之间,还可以降低 doc id 的存储占用。

简单说,SINDI 希望把查询尽量变成“顺序读、就地算、少跳转”。这也是它在大规模稀疏向量检索中保持高吞吐的关键。

(2)Term-based 高效得分机制

图 4:SINDI 的按词项得分计算流程

SINDI 查询阶段采用 Term-based(按词项驱动) 的得分方式。给定一个查询,系统会先拿到查询里有权重的词项。然后在每个窗口里,逐个访问这些词项对应的倒排列表。

每次访问倒排列表时,SINDI 会把 查询侧权重 × 文档侧权重 的结果累加到当前 window 的 距离表(Distance Table) 中。等当前窗口里所有查询词项都扫完后,距离表里的分数就是每个候选文档和查询的完整内积分数。接下来,SINDI 再把分数较高的文档更新到 top-k 候选堆 里。

这个做法的好处是很明显的。它不是把每篇文档都拿出来和查询完整比较,而是让查询词项主动去倒排列表里找可能相关的文档。对于不相关的词项和文档,SINDI 直接跳过。这极大地减少了查找共同词项的开销,因为大部分文档和查询只会在少数词项上有交集。

配合前面的 window-based 布局,SINDI 每次只维护当前窗口内的一张临时距离表。扫描倒排列表、累加分数、更新候选,都尽量在小范围内完成。这样内存访问更连续,距离表也更容易被缓存住。简单说,就是少看无关文档,少做随机访问,把计算集中在真正可能命中的文档上。

例 2 如图 4,SINDI 的搜索过程可以理解成嵌套循环:外层遍历所有 window,内层遍历查询里的 term。系统先进入 Window i,为这个窗口准备一张只覆盖窗口内文档的距离表;然后逐个处理查询词项。当前窗口处理完成后,SINDI 会用该窗口里的高分文档更新全局 top-k 候选堆,再进入下一个 window,重复同样的过程。

以 Window i 为例,假设当前查询包含词项 t2,且查询侧权重为 0.5。SINDI 会在当前 window 中找到 t2 对应的 term list。这个列表里包含若干个 posting ,例如 (D3, 0.8)、(D8, 0.6)、(D1, 0.3)、 (D6, 0.5)。这里每个 posting 都表示“某篇文档包含 t2,且文档侧权重是多少”。

计算时,SINDI 不会把文档完整向量取出来比较,而是直接用查询侧权重乘以 posting 中的文档侧权重:

$$D3: 0.5 \times 0.8 = 0.40$$

$$D8: 0.5 \times 0.6 = 0.30$$

$$D1: 0.5 \times 0.3 = 0.15$$

$$D6: 0.5 \times 0.5 = 0.25$$

这些结果会被累加到 Window i 的距离表里。比如 D3 在 t2 上先得到 0.40,如果后续又命中 t7 或 t9,新的乘积会继续加到 D3 的同一个距离表位置上。等当前窗口里的所有查询词项都处理完,距离表中的值就是该窗口内每篇候选文档的近似内积分数。最后,SINDI 用这个窗口里的高分文档更新全局 top-k 候选堆,并清空或复用距离表,继续处理下一个窗口。

在 VSAG 开源库的接口设计中,最终返回的是距离值。为了和 距离越小越相似 的统一语义对齐,SINDI 会把内积分数转换成距离:

$ distance = 1 - inner_product $

内积越大,距离越小,排序方向就和其他索引保持一致。

(3)多层剪枝策略

图 5:SINDI 的三类剪枝策略

稀疏向量检索的计算量,主要看两个因素:查询里有多少词项,以及这些词项对应的倒排列表有多长。SINDI 围绕这两个因素做了三层剪枝:构建期的文档侧剪枝、搜索期的查询侧剪枝,以及倒排列表侧剪枝。

第一层是 文档侧剪枝(Document Pruning) ,它发生在构建索引时。SINDI 会先看每篇文档自己的稀疏向量,按权重累计的质量比例做剪枝,优先保留权重更高的词项,丢掉一部分贡献较小的词项。被剪掉的词项不会写入倒排索引,因此可以直接缩短倒排列表并降低索引规模。

这种剪枝更适合权重有长尾分布的稀疏向量。也就是少数高权重词项贡献了主要内积分数,大量低权重尾端词项贡献很小。剪掉这些尾端词项,可以明显缩短倒排列表,降低计算量和索引规模,同时对内积精度影响相对有限。

第二层是 查询侧剪枝(Query Pruning) 。它发生在搜索时。一次查询里也会有很多词项,但不是每个词项都同样重要。SINDI 可以只保留查询里权重更高的词项。被剪掉的查询词项,本次搜索就不再访问它的倒排列表。这样访问的列表更少,延迟也会下降。对延迟敏感的在线服务,这一层很实用。

第三层是 倒排列表侧剪枝(Term-list Pruning) 。即使某个查询词项被保留下来,它对应的倒排列表也可能很长。比如一个高频词项,可能命中大量文档。如果每次都扫完整列表,成本还是会很高。SINDI 可以只扫描列表前面更有价值的一部分,跳过后面一部分 posting(倒排项) 。这样可以继续压低单个词项带来的计算量。

例 3 如图 5,上半部分展示的是构建期的 文档侧剪枝。文档 D3 原本有 5 个词项:t7:0.42、t2:0.28、t9:0.18、t5:0.08、t1:0.04,总质量为 1.00。当 doc_prune_ratio = 0.2 时,可以理解为保留约 80% 的主要质量。SINDI 按权重从高到低累计:

$$t7: 0.42$$

$$t7 + t2: 0.42 + 0.28 = 0.70$$

$$t7 + t2 + t9: 0.42 + 0.28 + 0.18 = 0.88$$

累计到 0.88 后已经覆盖主要质量,因此 t7、t2、t9 会被写入倒排索引,t5、t1 会被丢弃。右侧倒排索引里能看到 D3 出现在 t7、t2 等保留词项列表中,而不会出现在 t5、t1 这些低贡献词项里。

下半部分展示的是搜索期的两类剪枝。首先是 查询侧剪枝:查询向量里有 t2:0.50、t7:0.31、t4:0.12、t9:0.07。当 query_prune_ratio = 25% 时,4 个查询词项中保留权重最高的 3 个,所以 t2、t7、t4 被保留,低权重的 t9 本次搜索不再访问。

然后是 倒排列表侧剪枝:对于仍然被访问的 t2 和 t7,如果 term_prune_ratio = 30%,则每条倒排列表只扫描前 70% posting。图中 t2 列表会扫描 D3、D8、D1,跳过后面的 D6;t7 列表会扫描 D2、D3、D9,跳过后面的 D5。三层剪枝叠加后,SINDI 同时减少了写入索引的词项、查询访问的列表数量和单条列表的扫描长度。

这三层剪枝可以组合使用。实际调参时,一般不会一上来就剪很多。更常见的做法是先轻剪,通过 analyze(索引分析) 功能输出观察 recall(召回率) 、QPS 和索引规模的变化,然后逐步加大剪枝强度。

(4)量化与重排:精度与内存的平衡

剪枝可以提升效率,也能降低索引规模。但它毕竟丢掉了一部分信息,所以会带来一定精度损失。为了把精度拉回来,SINDI 支持 重排(Reranking) 。它会先用倒排索引快速召回一批候选,再用原始稀疏向量重新计算分数,让最终 top-k 更接近精确结果。

图 6:SINDI 的倒排索引和正排数据组织

也就是说,SINDI 的索引包含两部分: 倒排索引 和精排用的 正排数据(Forward Data) 。图 6 展示了 SINDI 在 VSAG 中的索引结构和布局:左侧是 倒排索引 window_term_list_ 。它先按 window 切分,每个 window 对应一个 SparseTermDatacell 。在一个 datacell 里,每个 term 都有两条列表– doc_id[] 和 value[]。这里的 doc_id 是窗口内编号,不是全局文档编号,所以可以用更小的数据类型保存;value 是该 term 在对应文档里的权重。

右侧是重排用的 正排数据 rerank sparse vectors 。它按 doc 组织,每篇文档对应一个 sparse vector block,block 里保存这篇文档的 len、term_id[] 和 value[]。上面的 offset table 记录的是 doc id 到向量 block 位置的映射。这样粗排拿到候选 doc 后,就能通过 offset 直接找到这篇文档的完整稀疏向量,再重新计算内积分数。

这里会引出一个新的问题:重排需要额外保存一份原始数据。当数据规模变大时,这两部分都需要量化压缩。

第一部分是 倒排索引压缩 。前面提到过,SINDI 通过 window 组织倒排列表,可以把局部 doc id 从 uint32 压到 uint16。除此之外,SINDI 还可以用 SQ8(Scalar Quantization 8-bit,8 位标量量化) 把倒排索引里的 value 压缩到 8-bit。查询粗排时直接用量化后的 value 参与打分,不需要先解码成 float;并且,倒排量化结合重排后对最终精度基本无损。

从实验数据看,倒排量化让倒排索引内存占用减半。在英文 880 万规模数据集上,优化前内存为 9.26 GB,量化后为 4.25 GB,降低 54.10%。在中文 1000 万规模数据集上,优化前内存为 6.82 GB,量化后为 3.19 GB,降低 53.23%。

数据集数据集规模优化前内存量化后内存降低比例
英文数据集8.42 GB9.26 GB4.25 GB54.10%
中文数据集3.06 GB6.82 GB3.19 GB53.23%

第二部分是 正排数据压缩 。SINDI 可以用 DMQ(Distribution Maintenance Quantization,分布维持量化) 压缩精排用的原始稀疏向量副本:term id 按实际词表范围做 bit packing(位打包) ,value 用 8-bit DMQ code 保存。解码时再结合每个 term 的 码本(Codebook) ,以及每篇文档自己的均值和缩放因子,还原出近似 value 参与精排。

在 1000 万规模数据集上,DMQ 可以把精排原始数据压缩约 50%,召回只下降约 1%。同时,QPS(Queries Per Second,用于评估查询吞吐性能)下降不到 10%。也就是说,它用较小的召回和吞吐损失,换来了明显的内存下降。

3、VSAG 中使用 SINDI

(1)启用参数示例

下面是一个 SINDI 构建参数示例:

std::string sindi_build_parameters = R"({
    "dtype": "sparse",
    "metric_type": "ip",
    "dim": 128,
    "index_param": {
        "term_id_limit": 1000000,
        "window_size": 60000,
        "doc_prune_ratio": 0.0,
        "use_quantization": false,
        "use_reorder": true,
        "remap_term_ids": false
    }
})";

auto index = vsag::Factory::CreateIndex("sindi", sindi_build_parameters).value();

上述代码构建后,预期会返回一个支持最大内积搜索的稀疏向量索引对象。

搜索时,参数放在 sindi 子对象下:

std::string sindi_search_parameters = R"({
    "sindi": {
        "n_candidate": 200,
        "query_prune_ratio": 0.0,
        "term_prune_ratio": 0.0,
        "use_term_lists_heap_insert": true
    }
})";

auto result = index->KnnSearch(query, 10, sindi_search_parameters).value();

上述代码会返回当前查询的 top-k 近邻结果。

(2)核心参数说明

参数名位置范围/默认值含义推荐用法
必填dtype顶层"sparse"声明输入数据为稀疏向量。使用 SINDI 时固定为 "sparse"。
metric_type顶层"ip"使用最大内积搜索;ip 表示 Inner Product,SINDI 会返回 1 - inner_product 作为距离。使用 SINDI 时固定为 "ip"。
dim顶层正整数单条稀疏向量允许的最大非零项数量,不是词表大小。按模型输出的最大非零项数设置;例如每条向量最多保留 128 个词项,则设为 128。
可选(构建)term_id_limitindex_param(0, 50000000],默认 1000000词项 ID 上界,应不小于最大 term id + 1;未开启 term id 重映射 时会影响倒排结构的可寻址范围。按词表大小或最大 term id + 1 设置;term id 很稀疏时优先考虑开启 remap_term_ids。
window_sizeindex_param[10000, 60000],默认 50000每个窗口容纳的文档数;窗口内 doc id 使用 uint16_t 保存,并影响距离表的缓存友好性。通常使用默认值;更关注吞吐时可在 50000 到 60000 间尝试。
doc_prune_ratioindex_param[0.0, 0.9],默认 0.0构建期文档侧剪枝比例,按权重丢弃每篇文档中贡献较小的词项。高召回基线设为 0.0;需要压缩索引或提速时从 0.1、0.2 逐步调高,并配合 analyze 观察召回损失。
use_quantizationindex_paramtrue 或 false,默认 false是否对倒排索引里的 value 开启 8-bit 标量量化(SQ8) ,用于降低倒排索引内存。内存敏感场景建议开启;极致精度基线可先关闭。
use_reorderindex_paramtrue 或 false,默认 false是否额外保留高精度原始稀疏向量,用于对粗筛候选做 精排 。追求召回时建议开启;内存极敏感且可接受召回损失时关闭。
remap_term_idsindex_paramtrue 或 false,默认 false是否在建索引前重映射 term id,减少稀疏或有大量空洞词表带来的内存浪费。外部词表 ID、哈希 ID 或 term id 分布很稀疏时开启;连续紧凑词表可关闭。
avg_doc_term_lengthindex_param正整数,默认 100每篇文档平均非零词项数,仅用于内存估算,不改变索引构建和检索结果。做 EstimateMemory 时按数据集统计值填写;普通构建可以不设置。
可选(搜索)n_candidatesindi[0, 500 * topk],默认 0粗筛候选堆 大小;为 0 时实际候选规模至少为 topk,显式设置时不能超过 500 * topk。开启重排时通常设为 10 * topk 到 50 * topk 起步;召回不足时调大,延迟过高时调小。
query_prune_ratiosindi[0.0, 0.9],默认 0.0查询侧剪枝比例,搜索时丢弃查询向量中低权重的词项。延迟敏感场景从 0.1 或 0.2 试起;高召回场景保持 0.0。
term_prune_ratiosindi[0.0, 0.9],默认 0.0倒排列表侧剪枝比例,搜索时跳过列表中低权重的 posting。存在超长倒排列表或高频 term 时尝试调高;如果召回下降明显,应降低该值或增大 n_candidate。
use_term_lists_heap_insertsinditrue 或 false,默认 true是否按访问过的 term list 顺序插入候选堆,通常能减少无效堆操作。一般保持默认 true;只有在做性能对比或排查时再切换验证。

4、总结

SINDI 是 VSAG 为稀疏向量检索设计的 高性能倒排索引 。它利用稀疏向量 只在少量词项上非零 的特点,把最大内积搜索转换成对相关倒排列表的顺序扫描和得分叠加。再通过 窗口化数据布局 、带权倒排列表 、多层剪枝 、8-bit 量化 和 高精度重排 ,形成一套兼顾成本、性能和召回的工程方案。

在 RAG 和搜索系统中,SINDI 可以很好地承担 稀疏向量召回 这一层。它比 纯词法检索(Lexical Retrieval) 更有语义扩展能力,又比稠密向量更容易解释和排查。对于已使用 VSAG 稠密索引的开发者,SINDI 也是一个自然的补充方向,可以让 混合召回链路 更加完整。

SINDI 更适合以下场景:1)需要语义扩展能力的稀疏向量召回;2)内存、构建成本和查询吞吐都比较敏感的大规模检索;3)需要通过 term 匹配排查 bad case 的调试场景。它不适合替代纯稠密向量检索;如果任务只需要稠密语义召回,可以优先使用 VSAG 的 HGraph 等稠密索引。对于超小规模数据,倒排索引的结构优势也不一定明显。

需要注意的是,SINDI 目前不支持硬删除,对于过滤场景的支持也比较有限。此外,窗口大小、候选数量、剪枝比例和量化配置都和数据分布相关,需要通过 analyze 输出和召回评测逐步调参。

5、参考

(1)数据集地址:

[1] sparse-1M数据集:https://storage.googleapis.com/ann-challenge-sparse-vectors/csr/base_1M.csr.gz

[2] sparse-full数据集:https://storage.googleapis.com/ann-challenge-sparse-vectors/csr/base_full.csr.gz

(2)参考文献:

[1] SINDI 论文:https://arxiv.org/abs/2509.08395

[2] SINDI 博客:https://mp.weixin.qq.com/s/A5pu7YBtApECJz4uAWMhBQ

(3)参考代码:

[1] VSAG:https://github.com/antgroup/vsag