Pyramid
Pyramid 是 VSAG 的 层级路径分区 图索引。每条向量都附带一个路径字符串(例如
"a/d/f"),Pyramid 会按路径树为每个节点构建一个子图;查询时提供一个路径前缀,
检索即被限定在相应的子树内。
这种设计非常适合多租户部署、标签分区的物料库,或者任何“一个逻辑索引服务多个群体、 群体之间不允许结果交叉”的场景。
- 源码:
src/algorithm/pyramid.{h,cpp}、src/algorithm/pyramid_zparameters.{h,cpp} - 示例(单层级):
examples/cpp/107_index_pyramid.cpp - 示例(多层级):
examples/cpp/112_index_pyramid_multi_hierarchy.cpp
工作原理
- 路径树。 每条向量在 ID 之外还携带一个
path,分隔符为/(例如"tenant_a/lang_en/topic_news")。Pyramid 会为构建期间出现过的每个路径前缀 维护一个子索引。 - 按层构建子图。 默认情况下每一层都会独立构建一张近邻图。可以用
no_build_levels跳过那些太小或太粗、不适合构图的层级——这些层级仍作为透传容器存在,但检索会退化为 线性扫描。 - 图的构建。 每个子图与 HGraph 采用同一套机制:
nsw插入或odescent,并通过graph_iter_turn、neighbor_sample_rate、alpha控制构图剪枝。底层向量按base_quantization_type存储;启用精排时另外保留一份高精度副本。 - 检索。 查询向量同样要附带路径。搜索会顺路径树向下走到最具体匹配查询路径的子图,
然后在该子图内执行图检索(
ef_search;中间层由subindex_ef_search控制)。
快速开始
#include <vsag/vsag.h>
std::string params = R"({
"dtype": "float32",
"metric_type": "l2",
"dim": 128,
"index_param": {
"base_quantization_type": "sq8",
"max_degree": 32,
"alpha": 1.2,
"graph_type": "odescent",
"graph_iter_turn": 15,
"neighbor_sample_rate": 0.2,
"no_build_levels": [0, 1],
"use_reorder": true,
"build_thread_count": 16
}
})";
auto index = vsag::Factory::CreateIndex("pyramid", params).value();
// 构建时为每条向量提供路径。
auto base = vsag::Dataset::Make();
base->NumElements(n)
->Dim(128)
->Ids(ids)
->Paths(paths) // std::string* 长度为 n,例如 "a/d/f"
->Float32Vectors(data)
->Owner(false);
index->Build(base);
// 按路径前缀执行检索。
std::string query_path = "a/d";
auto query = vsag::Dataset::Make();
query->NumElements(1)
->Dim(128)
->Float32Vectors(q)
->Paths(&query_path)
->Owner(false);
auto result = index->KnnSearch(
query, /*topk=*/10,
R"({"pyramid": {"ef_search": 100}})").value();
支持的输入数据类型
当前公开的 Build、Add 和检索路径接收通过 Dataset::Float32Vectors 提供的 FP32 向量,dtype 应设为 "float32"。base_quantization_type 选择的是内部编码和存储,本身不会使 API 接受 FP16、BF16 或 INT8 输入。
构建参数
构建参数放在 index_param 下。
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
base_quantization_type | string | — | 底层量化类型(fp32、fp16、bf16、sq8、sq4、sq8_uniform、sq4_uniform、pq、pqfs、rabitq、tq)。各量化器细节见量化章节。 |
tq_chain | string | — | base_quantization_type 为 tq 时使用的变换链,例如 "mrle, rabitq"。 |
mrle_dim | int | 0 | MRLE 保留的前缀维度;0 表示保持输入维度。 |
max_degree | int | 64 | 子图内节点的最大出度 |
graph_type | string | "nsw" | nsw 或 odescent |
graph_storage_type | string | "flat" | multi_layer 根节点的底图存储:flat 偏向构建和检索速度,compressed 减少图内存。压缩存储要求 max_degree <= 255。单层根图、路由图和子图仍使用 Sparse。 |
ef_construction | int | 400 | nsw 构图时的候选集大小 |
alpha | float | 1.2 | 构图剪枝系数 |
graph_iter_turn | int | — | ODescent 迭代轮数(graph_type: "odescent" 时生效) |
neighbor_sample_rate | float | — | ODescent 的邻居采样比率 |
no_build_levels | int[] | [] | 跳过构图的层级(从根节点开始的 0-based 下标) |
use_reorder | bool | false | 是否保留高精度副本用于精排 |
precise_quantization_type | string | "fp32" | 精排使用的量化类型。与 rabitq_bits_per_dim_precise 配合设为 "rabitq" 时,可启用从 base storage 重排的 RaBitQ x+y split。 |
rabitq_bits_per_dim_base | int | 1 | RaBitQ 底库存储码的每维位数。在 x+y split 模式下表示 x,即图遍历使用的 filter bits;范围为 [1, 8]。 |
rabitq_bits_per_dim_precise | int | 未设置 | RaBitQ split 的 y bits。和 base_quantization_type: "rabitq"、precise_quantization_type: "rabitq" 一起设置时,Pyramid 使用 split storage;rabitq_bits_per_dim_base 仍表示 x,且 x + y <= 8。 |
fast_encode_rabitq | bool | true | 对 RaBitQ 底层或精排存储使用多 bit 快速编码器;设为 false 使用精确编码器 |
fast_encode_rabitq_rounds | int | 6 | RaBitQ 快速编码的微调轮数,范围 [1, 32] |
base_io_type / precise_io_type | string | "block_memory_io" | 底层与精排存储后端;以 liburing 构建时可用 uring_io |
base_file_path / precise_file_path | string | — | buffer_io、async_io、uring_io、mmap_io 等磁盘存储必须设置 |
store_raw_vector | bool | false | 保留 FP32 原始向量,用于 GetRawVectorByIds 和精确的按 ID 距离计算 |
store_paths | bool | false | 顶层开关;保留传给 Build 和 Add 的原始路径,使 GetDataByIdsWithFlag 在选择 DATA_FLAG_PATH 时可以返回它们。该开关对所有已配置的 hierarchy 生效,不支持按 hierarchy 覆盖 |
index_min_size | int | 0 | 子索引的最小规模;小于该值的分区会退化为线性扫描 |
root_graph_type | string | "single_layer" | 根图结构:single_layer 保留原有稀疏底图;multi_layer 使用预分配的 Flat 或 Compressed 底图、类似 HGraph 的稀疏路由层以及联合构图流程。multi_layer 要求 graph_type: "nsw"。no_build_levels 禁用第 0 层时不要显式指定此选项。 |
support_duplicate | bool | false | 是否允许重复 ID |
build_thread_count | int | 1 | 构建阶段并发线程数 |
hierarchies | array | [] | 命名层级定义。每个元素可以是字符串(继承全部顶层参数)或对象(含 name 及可选覆盖参数:max_degree、ef_construction、alpha、no_build_levels、index_min_size、root_graph_type)。设置后激活多层级模式,每个层级维护独立的路径树。 |
RaBitQ split 配置
需要同时设置以下五个参数,才能启用 RaBitQ x+y split 存储和精排:
{
"use_reorder": true,
"base_quantization_type": "rabitq",
"precise_quantization_type": "rabitq",
"rabitq_bits_per_dim_base": 3,
"rabitq_bits_per_dim_precise": 5
}
Pyramid 使用 split code 的 code-code 距离完成增量 FLAT→GRAPH 晋升,因此默认不保留
内部 FP32 副本。需要访问原始向量或完整 Analyzer 指标时,可在构建时将
store_raw_vector 设置为 true。
MRLE 与 split RaBitQ
对于使用 Matryoshka Representation Learning 训练的向量,Pyramid 可以先截断维度,再编码 为 split RaBitQ:
{
"base_quantization_type": "tq",
"tq_chain": "mrle, rabitq",
"mrle_dim": 1280,
"precise_quantization_type": "rabitq",
"rabitq_bits_per_dim_base": 3,
"rabitq_bits_per_dim_precise": 5,
"use_reorder": true
}
该配置使用 3-bit filter planes 构图和搜索、5-bit supplement planes 精排;两部分编码的是
同一个截断后向量。Pyramid 使用 split code-code 距离完成构图提升,不保留原始 FP32
向量;除非构建时启用 store_raw_vector,否则依赖可解码向量的 Analyzer 指标会被标记为
不可用。如果 embedding 模型没有针对前缀维度训练,截断可能显著降低召回率。
构建缓存
ExportCache 会保存每个层级、每个节点的 NSW 图种子,ImportCache 可在后续 Build 中复用。缓存数据使用索引缓存 payload 格式,而非 streaming 索引序列化格式。需要复用缓存的索引通过 footer 序列化前应设置 persist_source_id: true,并且两次构建中的每个向量都必须提供唯一的 Dataset::SourceID。缓存预热仅适用于 graph_type: "nsw";ODescent、重复 ID 模式、缺少 source ID 或 source ID 重复时会自动回退到普通冷构建。ef_construction 不作为缓存路径的准入条件。完整恢复的缓存图行会被保留,缓存未命中的节点则使用当前向量构建。
检索参数
检索参数放在 pyramid 子对象下:
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
ef_search | int | 100 | 叶子层子图检索的候选集大小 |
factor | float | 未设置 | KNN 重排候选倍率。值 <= 1 时不增加限制;值大于 1 时,Pyramid 保持各子图原有搜索行为,先合并子图结果,再将至多 min(max(ef_search, topk), floor(topk * factor)) 个主图候选送入重排。与 HGraph 一致,RaBitQ lower-bound 安全候选可在该限制后额外并入,reorder_candidate_count 记录实际合并数量。参数必须为有限正数;范围检索或关闭重排时不生效。 |
hops_limit | int | 不限 | 根节点底图及每个非根 GRAPH 的逐图 KNN 跳数上限;不大于 ef_search 时忽略。根节点的稀疏路由层不受限制,FLAT 扫描与范围检索不受影响。 |
subindex_ef_search | int | 50 | 沿路径向下遍历中间子图时的候选集大小 |
hierarchies | string[] | [] | 指定检索哪个层级。空数组表示使用默认(匿名)层级。 |
hierarchy_op | string | "single" | 多层级结果合并方式:single(检索单个层级)、union、intersection。注意: union 和 intersection 尚未实现——设置后 KnnSearch/RangeSearch 会返回错误。 |
rabitq_error_rate | float | 1.9 | 本次搜索使用的正数 lower-bound 误差倍率。默认值 1.9 较大;值越大,精度越高,但搜索速度越慢。 |
auto result = index->KnnSearch(
query, topk,
R"({"pyramid": {"ef_search": 200, "subindex_ef_search": 80}})").value();
多层级支持 (Multi-Hierarchy)
一个 Pyramid 索引可以同时维护多棵独立的路径树,每棵树由名称标识(如
"site"、"category")。所有层级共享向量 ID 和数据——只有路径不同。每个层级可以
选择性地覆盖图构建参数。
当同一组向量需要沿多个维度同时分区时,这个特性非常有用。例如,一个电商平台可能
需要按站点(site-a/lang-en)和按品类(electronics/phones)同时分区,
检索时可以独立选择任一层级。
构建配置
在 index_param 中添加 hierarchies 数组。每个元素可以是:
- 字符串(继承全部顶层参数):
"site" - 对象(含
name和可选的参数覆盖):{"name": "category", "max_degree": 64, "no_build_levels": [0]}
可按层级覆盖的参数:max_degree、ef_construction、alpha、no_build_levels、
index_min_size、root_graph_type。
root_graph_type: "multi_layer" 只改变所选层级的根节点。底图使用顶层
graph_storage_type:默认使用 Flat,也可使用 Compressed,以构建和检索速度换取更低的图
内存;路由图和子图仍使用 Sparse。稀疏路由图先选择更好的入口点,再进入底图检索。批量
Build 与增量 Add 都使用类似 HGraph 的 route 与 bottom 联合插入流程。该结构要求
graph_type: "nsw";参数校验会拒绝 multi_layer 与 odescent 的组合。存在独立 precise
storage 时,底图和路由图的边统一使用 precise codes 构建,否则使用 base codes;查询遍历
继续使用 base codes,最终精排使用配置的 reorder source。
{
"dtype": "float32",
"metric_type": "l2",
"dim": 128,
"index_param": {
"base_quantization_type": "sq8",
"max_degree": 32,
"alpha": 1.2,
"graph_type": "odescent",
"graph_iter_turn": 15,
"neighbor_sample_rate": 0.2,
"no_build_levels": [0, 1],
"use_reorder": true,
"build_thread_count": 16,
"hierarchies": [
"site",
{"name": "category", "max_degree": 64, "no_build_levels": [0]}
]
}
}
命名层级的 Dataset API
使用重载方法 Paths(hierarchy_name, paths) 为每个层级设置路径。所有层级共享同一份
Ids() 和 Float32Vectors():
auto base = vsag::Dataset::Make();
base->NumElements(n)
->Dim(128)
->Ids(ids)
->Float32Vectors(data)
->Paths("site", site_paths) // std::string* 长度为 n
->Paths("category", category_paths) // 第二个层级的独立路径
->Owner(false);
index->Build(base);
按 ID 取回路径
将顶层构建参数 store_paths 设为 true 后,Pyramid 会保留原始路径,供按 ID
取回。在 GetDataByIdsWithFlag 中选择 DATA_FLAG_PATH 后,返回路径的顺序与请求 ID
的顺序一致。默认匿名 hierarchy 使用 GetPaths(),命名 hierarchy 使用
GetPaths(hierarchy_name):
int64_t requested_ids[] = {product_id_b, product_id_a};
auto data = index->GetDataByIdsWithFlag(
requested_ids, 2, DATA_FLAG_ID | DATA_FLAG_PATH).value();
const std::string* site_paths = data->GetPaths("site");
const std::string* category_paths = data->GetPaths("category");
GetDataByIds 以及未选择 DATA_FLAG_PATH 的 GetDataByIdsWithFlag 都不会附带路径
数组。store_paths 为 false 时选择 DATA_FLAG_PATH 会返回参数错误。开启路径存储
后,只有当所有请求 ID 在某个 hierarchy 中都有已记录的路径,结果才会包含该
hierarchy。只要其中一个 ID 在构建或追加时没有提供该 hierarchy 的路径,对应 getter
就返回 nullptr;其他路径完整的 hierarchy 仍会正常返回。单 hierarchy 模式下,
GetPaths() 遵循相同的完整性规则。
检索指定层级
通过检索参数中的 "hierarchies" 指定要检索的层级。查询 Dataset 也需要在对应的
层级名称上设置路径:
auto query = vsag::Dataset::Make();
query->NumElements(1)
->Dim(128)
->Float32Vectors(q)
->Paths("site", &query_path) // 指向 "site" 层级
->Owner(false);
auto result = index->KnnSearch(
query, /*topk=*/10,
R"({"pyramid": {"ef_search": 100, "hierarchies": ["site"]}})").value();
增量插入 (Add)
Add() 的用法与 Build() 一致——提供命名路径,索引会自动插入到所有匹配的层级:
auto new_data = vsag::Dataset::Make();
new_data->NumElements(count)
->Dim(128)
->Ids(new_ids)
->Float32Vectors(new_vectors)
->Paths("site", new_site_paths)
->Paths("category", new_cat_paths);
index->Add(new_data);
范围检索 (RangeSearch)
RangeSearch 同样支持通过检索参数选择层级:
auto result = index->RangeSearch(
query, /*radius=*/20.0f,
R"({"pyramid": {"ef_search": 100, "hierarchies": ["category"]}})").value();
序列化与反序列化
多 hierarchy 索引的序列化和反序列化完全透明。序列化格式包含所有 hierarchy
名称及其图结构。当 store_paths: true 时,常规序列化和 streaming 序列化还会持久化
已保留的原始路径,因此反序列化后仍可通过 GetDataByIdsWithFlag 获取。使用默认值
false 时,图 hierarchy 会被持久化,但不会保留按 ID 索引的原始路径:
// 序列化
auto binary_set = index->Serialize().value();
// 反序列化到新索引(必须使用相同的构建参数)
auto new_index = vsag::Factory::CreateIndex("pyramid", build_params).value();
new_index->Deserialize(binary_set);
何时选择 Pyramid
- 多租户服务:每个租户只能看到自己分区的结果,且希望避免为每个租户单独维护一份索引。
- 带有层级标签的物料库(语言 / 地域 / 品类),查询永远限定在某个已知的前缀下。
- 小分区非常多的负载:可以用
no_build_levels与index_min_size跳过那些小到不值得 构图的分区。
如果不需要按路径限定查询范围,HGraph 更简洁,性能通常也更高。
可以通过索引分析检查 Pyramid 的树结构、子索引质量、
GetStats() 输出的 base 采样召回率和重复比例。每个 hierarchy 的 root_graphs 会报告
root_graph_type、bottom_graph_storage_type、bottom_graph_node_count、
bottom_graph_size、route_graph_count、route_node_counts 和 route_graph_size。
AnalyzeIndexBySearch 还会输出按路径限定的
query 召回率、距离、耗时,以及开启 reorder 时的量化指标。query 数据集必须包含与
KnnSearch 相同的默认或命名 hierarchy 路径;批量数据集在需要或提供路径时,应为每条 query
提供一条路径。analyze_index 工具当前无法从 dense query 文件加载 hierarchy 路径,因此
按路径执行动态分析时请使用 C++ 接口。
标记删除
Pyramid 支持 RemoveMode::MARK_REMOVE。调用 Remove(ids)(默认模式)会为给定的 id
打上删除标记:它们会从后续检索结果中排除,GetNumElements() 相应减少,
GetNumberRemoved() 返回累计删除数量。删除不存在或已删除的 id 不会有任何效果。
RemoveMode::FORCE_REMOVE 不支持,调用会返回错误。
被标记删除的向量在索引重建前仍占用内存,空间不会被物理回收。