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

Pyramid

Pyramid 是 VSAG 的 层级路径分区 图索引。每条向量都附带一个路径字符串(例如 "a/d/f"),Pyramid 会按路径树为每个节点构建一个子图;查询时提供一个路径前缀, 检索即被限定在相应的子树内。

这种设计非常适合多租户部署、标签分区的物料库,或者任何“一个逻辑索引服务多个群体、 群体之间不允许结果交叉”的场景。

工作原理

  1. 路径树。 每条向量在 ID 之外还携带一个 path,分隔符为 / (例如 "tenant_a/lang_en/topic_news")。Pyramid 会为构建期间出现过的每个路径前缀 维护一个子索引。
  2. 按层构建子图。 默认情况下每一层都会独立构建一张近邻图。可以用 no_build_levels 跳过那些太小或太粗、不适合构图的层级——这些层级仍作为透传容器存在,但检索会退化为 线性扫描。
  3. 图的构建。 每个子图与 HGraph 采用同一套机制:nsw 插入或 odescent,并通过 graph_iter_turnneighbor_sample_ratealpha 控制构图剪枝。底层向量按 base_quantization_type 存储;启用精排时另外保留一份高精度副本。
  4. 检索。 查询向量同样要附带路径。搜索会顺路径树向下走到最具体匹配查询路径的子图, 然后在该子图内执行图检索(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();

支持的输入数据类型

当前公开的 BuildAdd 和检索路径接收通过 Dataset::Float32Vectors 提供的 FP32 向量,dtype 应设为 "float32"base_quantization_type 选择的是内部编码和存储,本身不会使 API 接受 FP16、BF16 或 INT8 输入。

构建参数

构建参数放在 index_param 下。

参数类型默认值说明
base_quantization_typestring底层量化类型(fp32fp16bf16sq8sq4sq8_uniformsq4_uniformpqpqfsrabitqtq)。各量化器细节见量化章节
tq_chainstringbase_quantization_typetq 时使用的变换链,例如 "mrle, rabitq"
mrle_dimint0MRLE 保留的前缀维度;0 表示保持输入维度。
max_degreeint64子图内节点的最大出度
graph_typestring"nsw"nswodescent
graph_storage_typestring"flat"multi_layer 根节点的底图存储:flat 偏向构建和检索速度,compressed 减少图内存。压缩存储要求 max_degree <= 255。单层根图、路由图和子图仍使用 Sparse。
ef_constructionint400nsw 构图时的候选集大小
alphafloat1.2构图剪枝系数
graph_iter_turnintODescent 迭代轮数(graph_type: "odescent" 时生效)
neighbor_sample_ratefloatODescent 的邻居采样比率
no_build_levelsint[][]跳过构图的层级(从根节点开始的 0-based 下标)
use_reorderboolfalse是否保留高精度副本用于精排
precise_quantization_typestring"fp32"精排使用的量化类型。与 rabitq_bits_per_dim_precise 配合设为 "rabitq" 时,可启用从 base storage 重排的 RaBitQ x+y split。
rabitq_bits_per_dim_baseint1RaBitQ 底库存储码的每维位数。在 x+y split 模式下表示 x,即图遍历使用的 filter bits;范围为 [1, 8]
rabitq_bits_per_dim_preciseint未设置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_rabitqbooltrue对 RaBitQ 底层或精排存储使用多 bit 快速编码器;设为 false 使用精确编码器
fast_encode_rabitq_roundsint6RaBitQ 快速编码的微调轮数,范围 [1, 32]
base_io_type / precise_io_typestring"block_memory_io"底层与精排存储后端;以 liburing 构建时可用 uring_io
base_file_path / precise_file_pathstringbuffer_ioasync_iouring_iommap_io 等磁盘存储必须设置
store_raw_vectorboolfalse保留 FP32 原始向量,用于 GetRawVectorByIds 和精确的按 ID 距离计算
store_pathsboolfalse顶层开关;保留传给 BuildAdd 的原始路径,使 GetDataByIdsWithFlag 在选择 DATA_FLAG_PATH 时可以返回它们。该开关对所有已配置的 hierarchy 生效,不支持按 hierarchy 覆盖
index_min_sizeint0子索引的最小规模;小于该值的分区会退化为线性扫描
root_graph_typestring"single_layer"根图结构:single_layer 保留原有稀疏底图;multi_layer 使用预分配的 Flat 或 Compressed 底图、类似 HGraph 的稀疏路由层以及联合构图流程。multi_layer 要求 graph_type: "nsw"no_build_levels 禁用第 0 层时不要显式指定此选项。
support_duplicateboolfalse是否允许重复 ID
build_thread_countint1构建阶段并发线程数
hierarchiesarray[]命名层级定义。每个元素可以是字符串(继承全部顶层参数)或对象(含 name 及可选覆盖参数:max_degreeef_constructionalphano_build_levelsindex_min_sizeroot_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_searchint100叶子层子图检索的候选集大小
factorfloat未设置KNN 重排候选倍率。值 <= 1 时不增加限制;值大于 1 时,Pyramid 保持各子图原有搜索行为,先合并子图结果,再将至多 min(max(ef_search, topk), floor(topk * factor)) 个主图候选送入重排。与 HGraph 一致,RaBitQ lower-bound 安全候选可在该限制后额外并入,reorder_candidate_count 记录实际合并数量。参数必须为有限正数;范围检索或关闭重排时不生效。
hops_limitint不限根节点底图及每个非根 GRAPH 的逐图 KNN 跳数上限;不大于 ef_search 时忽略。根节点的稀疏路由层不受限制,FLAT 扫描与范围检索不受影响。
subindex_ef_searchint50沿路径向下遍历中间子图时的候选集大小
hierarchiesstring[][]指定检索哪个层级。空数组表示使用默认(匿名)层级。
hierarchy_opstring"single"多层级结果合并方式:single(检索单个层级)、unionintersection注意: unionintersection 尚未实现——设置后 KnnSearch/RangeSearch 会返回错误。
rabitq_error_ratefloat1.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_degreeef_constructionalphano_build_levelsindex_min_sizeroot_graph_type

root_graph_type: "multi_layer" 只改变所选层级的根节点。底图使用顶层 graph_storage_type:默认使用 Flat,也可使用 Compressed,以构建和检索速度换取更低的图 内存;路由图和子图仍使用 Sparse。稀疏路由图先选择更好的入口点,再进入底图检索。批量 Build 与增量 Add 都使用类似 HGraph 的 route 与 bottom 联合插入流程。该结构要求 graph_type: "nsw";参数校验会拒绝 multi_layerodescent 的组合。存在独立 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_PATHGetDataByIdsWithFlag 都不会附带路径 数组。store_pathsfalse 时选择 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_levelsindex_min_size 跳过那些小到不值得 构图的分区。

如果不需要按路径限定查询范围,HGraph 更简洁,性能通常也更高。

可以通过索引分析检查 Pyramid 的树结构、子索引质量、 GetStats() 输出的 base 采样召回率和重复比例。每个 hierarchy 的 root_graphs 会报告 root_graph_typebottom_graph_storage_typebottom_graph_node_countbottom_graph_sizeroute_graph_countroute_node_countsroute_graph_sizeAnalyzeIndexBySearch 还会输出按路径限定的 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 不支持,调用会返回错误。

被标记删除的向量在索引重建前仍占用内存,空间不会被物理回收。

相关文档