SINDI
SINDI(Sparse INverted Dense Index)是 VSAG 面向 稀疏向量 的索引——
例如 BM25、SPLADE 以及其他学习稀疏(learned sparse)编码器产出的向量。与稠密索引
(HGraph、IVF)不同,SINDI 直接在“词项-权重”对上工作,是 VSAG 中接受
dtype: "sparse" 的索引之一。
- 源码:
src/algorithm/sindi/ - 示例:
examples/cpp/109_index_sindi.cpp
工作原理
- 基于窗口的倒排表。 文档按固定窗口大小(
window_size)分组,每个窗口独立维护一套 倒排表——即“词项 →(doc_id, value)列表”的映射。 - 可选的剪枝与量化。 构建时可通过
doc_prune_ratio按文档粒度丢弃权重最低的词项;use_quantization: true使用 SQ8,use_quantization: "fp16"使用半精度值。 - 打分。 检索时,SINDI 遍历查询向量的非零项,按窗口访问对应的倒排表,使用大小为
n_candidate的大顶堆聚合得分,最后取 top-k。启用use_reorder时,候选会在正排 存储上重打分。默认正排存储保留 fp32 值;设置rerank_type: "dmq8"时使用压缩的 DMQ 正排以降低重排内存。
返回的距离为 1 - inner_product,使结果与稠密索引一样按升序排序。
需要同时支持内存与磁盘 I/O 时,请选择 SINDI_V2。
快速开始
#include <vsag/vsag.h>
std::string params = R"({
"dtype": "sparse",
"metric_type": "ip",
"dim": 1024,
"index_param": {
"term_id_limit": 30000,
"window_size": 50000,
"doc_prune_ratio": 0.0,
"use_quantization": false,
"use_reorder": false,
"remap_term_ids": false
}
})";
auto index = vsag::Factory::CreateIndex("sindi", params).value();
// 使用 SparseVector 构建数据集。
auto base = vsag::Dataset::Make();
base->NumElements(n)
->SparseVectors(sparse_vectors) // vsag::SparseVector*
->Ids(ids)
->Owner(false);
index->Build(base);
// 执行检索。
auto query = vsag::Dataset::Make();
query->NumElements(1)->SparseVectors(&query_vec)->Owner(false);
auto result = index->KnnSearch(
query, /*topk=*/10,
R"({"sindi": {"n_candidate": 100}})").value();
构建参数
构建参数放在 index_param 下。dtype 必须 为 "sparse",metric_type 必须
为 "ip"。
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
dim | int | —(必填) | 单条稀疏向量允许的最大非零项数量,不是 词表大小 |
term_id_limit | int | 1000000 | 词项 ID 的上界(应 ≥ 最大词项 ID + 1,最高 50 000 000) |
window_size | int | 50000 | 每个窗口容纳的文档数(取值范围 10 000 – 60 000) |
doc_prune_ratio | float | 0.0 | 构建阶段按文档丢弃权重最低词项的比例,取值范围为 [0.0, 1.0) |
use_quantization | bool 或 string | false | false 存 FP32,true 存 SQ8,"fp16" 存 FP16 |
use_reorder | bool | false | 是否保留一份正排存储,在 SINDI 粗排后对候选做精排 |
rerank_type | string | "fp32" | use_reorder 开启时使用的正排存储类型。fp32 保留精确值;dmq8 使用压缩的 8-bit DMQ 编码 |
dmq_shared_codebook_threshold | int | 1024 | rerank_type: "dmq8" 时,出现次数不超过该值的 term 共用一个 codebook;更高频的 term 保持独立 codebook。设为 0 可关闭共享 |
remap_term_ids | bool | false | 是否在建索引前重映射词项 ID,适用于词项 ID 很稀疏或存在大量空洞的词表 |
avg_doc_term_length | int | 100 | 仅用于内存估算 |
immutable | bool | false | 构建或加载紧凑的只读运行态;Build() 会逐窗口压缩以降低峰值内存,增量 Add() 会被拒绝 |
dim与term_id_limit的区别。 对于稀疏向量{0:0.1, 2:0.5, 177:0.8},dim为3(三个非零项),而term_id_limit至少应为178(最大词项 ID + 1)。term_id_limit要按词表大小估计,这是使用时最常见的坑。
稀疏值格式
use_quantization 保留原有 bool 行为,同时新增一个字符串取值:
| 取值 | 存储格式 | 取舍 |
|---|---|---|
false | FP32 | 词项权重精度最高,posting payload 最大 |
true | SQ8 | 权重存储最小;首次构建时学习量化范围 |
"fp16" | FP16 | 权重字节数为 FP32 的一半,不需要 SQ8 范围校准 |
使用 false 或 true 构建的索引仍保留旧版序列化表示。旧版本 VSAG 无法解析使用新
"fp16" 格式的 SINDI 索引;部署 FP16 产物前应先升级读取端。
不可变低内存构建
完成后的 SINDI 只读时,可以设置 immutable: true:
{
"dtype": "sparse",
"metric_type": "ip",
"dim": 1024,
"index_param": {
"term_id_limit": 30000,
"window_size": 50000,
"use_quantization": "fp16",
"remap_term_ids": true,
"immutable": true
}
}
Build() 会把每个完成的可变窗口压缩成不可变 payload,并在继续构建前释放临时窗口。该功能
提交时的 Sparse4M FP16 实测中,构建峰值内存从 27.03 GB 降到 6.08 GB(降低 77.51%),
构建时间则从约 330 秒增加到 599 秒。这些数字是特定负载的实测证据,不是容量保证。
不可变运行态支持 KNN、范围搜索以及旧版 Serialize/Deserialize。它不支持增量
Add、GetSparseVectorByInnerId、CalcDistanceById 与 CalDistanceById。
mutable 和 immutable 运行态均支持 SerializeStreaming、DeserializeStreaming 与
Index::Load。
反序列化时,新建 SINDI 的 immutable 设置必须与存储格式一致。
新索引会记录有序倒排链格式版本,加载时可跳过归一化排序;缺少该标记的旧索引仍保持兼容,
并在加载时完成排序归一化。
Host 过滤
mutable 和 immutable SINDI 及 SINDI_V2 索引都可以按单值数值 host 对文档
分组,避免返回其他 host 的文档。host-aware Build() 或 mutable Add() 批次需要提供完整的
uint32_t host_id 数组;没有 host 的文档使用 0:
base->NumElements(n)
->SparseVectors(sparse_vectors)
->Ids(ids)
->UInt32Metadata("host_id", base_host_ids)
->Owner(false);
index->Build(base);
uint32_t query_host_id = 42;
query->NumElements(1)
->SparseVectors(&query_vec)
->UInt32Metadata("host_id", &query_host_id)
->Owner(false);
每个 Build() 或 mutable Add() 批次都会按 host 对 inner ID 分组,同时保持 external label
不变。host 查询统一扫描相关 posting window,并在该 host 的一个或多个 inner-ID 区间上执行
精确成员检查。多次 mutable Add() 可以为同一 host 追加互不连续的区间;删除标记和额外的
用户 Filter 会与 host 成员检查共同生效。
host ID 0 是缺失 host 分组,1 到 UINT32_MAX 表示普通 host;不同 host 的数量不能超过
成功写入索引的文档数。mutable 索引一旦包含 host metadata,后续每次 Add() 都必须提供
完整的 host_id 数组;已有 host-unaware 文档后不能再引入 host metadata。查询
host_id: 0 时只检索缺失 host 的文档,不提供 host_id 时保留全索引 KNN 行为;查询没有
已索引文档的 host 返回空结果。构建时没有 base host metadata 的索引会忽略查询 host
metadata,行为保持不变。host 过滤当前仅适用于 KNN;范围搜索仍使用原有全索引路径。
检索参数
检索参数放在 sindi 子对象下:
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
n_candidate | int | 0 | 候选堆大小。为 0 时自动取 SPARSE_AMPLIFICATION_FACTOR · topk(500 倍);若显式设置,须满足 1 ≤ n_candidate ≤ SPARSE_AMPLIFICATION_FACTOR · topk |
filter_callback_limit | uint64 | 0 | 单次带过滤检索最多调用用户 Filter::CheckValid 回调的次数;0 表示不限制。删除 ID 过滤器等内部检查不计数。达到正数上限时,在正常处理最后一次回调结果后停止候选及后续窗口扫描,并返回已经通过过滤的候选,因此结果可能不完整。该限制同时适用于 KNN、范围检索以及常规 API 和 SearchWithRequest |
query_prune_ratio | float | 0.0 | 查询时丢弃权重最低查询项的比例,取值范围为 [0.0, 1.0) |
term_prune_ratio | float | 0.0 | 每条倒排链中按 value 丢弃低权 posting 的比例,取值范围为 [0.0, 1.0) |
term_retain_threshold | uint64 | 0 | 单个 term 在所有 window 中最多扫描的 posting 总数;0 表示关闭此限制,正数使每个 window 的非空 posting list 最多扫描 max(1, floor(threshold / window_count)) 个 |
合并 ratio 与 threshold 限制后,每条非空倒排链至少扫描一个 posting。
同一 window 内的单 term posting 先按实际存储值降序排列,值相同时按内部 doc id
升序排列。ratio 和 threshold 同时生效时,实际扫描
floor(list_size · (1 - ratio)) 与 floor(threshold / window_count) 中的较小值。
SINDI 会根据构建阶段的 doc_prune_ratio 与检索阶段的 query_prune_ratio
自动选择堆插入策略。按当前 0.1 阈值,当两个比例都 <= 0.1 时,SINDI 使用
基于距离数组的入堆路径;只要任一比例大于 0.1,就使用基于 term-list 的入堆路径。
旧版 use_term_lists_heap_insert 检索参数会被忽略;请改用剪枝比例控制该行为。
auto result = index->KnnSearch(
query, topk,
R"({"sindi": {"n_candidate": 200, "filter_callback_limit": 10000, "query_prune_ratio": 0.1}})").value();
何时选择 SINDI
- 使用 BM25、SPLADE、uniCOIL 等学习稀疏编码器的稀疏检索场景。
- 稠密 + 稀疏的混合检索管线:SINDI 负责稀疏一路,HGraph / IVF 负责稠密 embedding。
- 稀疏语料的内存受限部署:
use_quantization: true选择 SQ8,"fp16"把 FP32 权重字节数减半;use_reorder: true以正排内存换召回,rerank_type: "dmq8"可降低这部分正排开销。 - 需要降低构建峰值内存的只读快照:使用
immutable: true,接受更慢的构建和不能增量写入。
SINDI 不支持 稠密向量,只支持内积相似度。范围检索与基于 ID 的过滤均已支持,
具体用法参见示例代码。
当 rerank_type 为 dmq8 时,码本由首次构建固定,因此模型建立后的增量 Add
和 UpdateVector 不受支持。
实践建议
- 中文数据集的稀疏向量,推荐使用 BGE-M3 编码;英文数据集更常见的默认选择是 SPLADE。
- BGE-M3 同时支持 sparse 和 dense 输出。当前 SINDI 负责稀疏一路,VSAG 未来计划 支持稀疏与稠密融合打分检索。
- 稀疏向量不能完全替代 BM25 全文检索。实践中,BM25 + 稀疏向量 + 稠密向量的三路 召回通常优于任意两路组合。
- 在索引层面,SINDI 也可以承载 BM25 风格打分:查询侧用逆文档频率作为词项权重, 文档侧用词频等特征计算出的词项权重作为向量值即可。
常用配置
- 扁平暴力搜索索引。倒排索引保留全部非零项(
doc_prune_ratio: 0.0),不保留正排索引 重排(use_reorder: false),不开启量化(use_quantization: false)。这是最直接的 高召回基线。 - 剪枝高精索引。构建时剪掉大部分低权重词项(
doc_prune_ratio: 0.4),保留正排索引 用于重排(use_reorder: true),并开启量化减少倒排索引内存 (use_quantization: true)。这是常见的精度与内存折中配置。 - 压缩正排重排索引。在上一种配置基础上,设置
rerank_type: "dmq8",与use_reorder: true一起使用,以降低正排重排内存。 - 超大稀疏词表支持。对于词项 ID 在
uint32范围内非常稀疏的场景,例如基于哈希的 分词器、外部词表 ID,或存在大量空白区间的词表,建议设置remap_term_ids: true。 这样可以避免管理大量空倒排列表带来的内存浪费,也能降低触达term_id_limit上限的风险。 - 只读低内存构建。当构建峰值内存比构建速度更重要,且完成后不再增量写入时,增加
immutable: true,通常配合use_quantization: "fp16"与remap_term_ids: true使用。
标记删除
SINDI 支持 RemoveMode::MARK_REMOVE。调用 Remove(ids)(默认模式)会为给定的 id
打上删除标记:它们不再出现在检索结果中,GetNumElements() 相应减少,
GetNumberRemoved() 返回累计删除数量。删除不存在或已删除的 id 不会有任何效果。
RemoveMode::FORCE_REMOVE 不支持,调用会返回错误。
被标记删除的文档在索引重建前仍占用内存,空间不会被物理回收。