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 全称 Vector Search Algorithm Group,是一个用于相似性检索的向量索引库。VSAG 允许用户在各种规模的向量集合中进行高效搜索,包括无法完全放入内存的集合,同时提供基于向量维度和数据规模自动生成参数的能力,使开发者无需深入了解底层算法原理即可快速上手。

VSAG 使用 C++ 编写,并提供:

  • Python 包装 pyvsag
  • Node.js / TypeScript 绑定 vsag(由 napi-rs 生成)

该项目由蚂蚁集团发起并主导开发,目前以开源社区的方式维护。

主要特性

  • 低内存占用:通过量化(RaBitQ、PQ、SQ4/SQ8)降低使用成本;
  • 高性能检索:针对 x86_64(SSE/AVX/AVX2/AVX512/AMX)和 ARM(Neon/SVE)做了指令集适配;
  • 丰富的索引类型:HGraph、IVF、Pyramid、BruteForce、SINDI / SINDI_V2(稀疏)等;
  • 灵活的过滤与混合搜索:支持 bitmap 与 callback 两种过滤方式,以及混合 (data vector, attribute) 查询;
  • 易于集成:提供基于 CMake 的集成方式,详见 README

如何阅读本文档

  • 用户指南:如果你是新用户,请从安装、创建索引和搜索开始。
  • 索引:比较不同索引类型,并查询索引参数。
  • 高级功能:深入了解搜索、序列化、内存管理和混合索引能力。
  • API 参考include/vsag/ 下公有头文件的 C++ 类、方法与类型参考手册。
  • 性能与调优:查看最佳实践、Tune、性能参考和评估工具。
  • 开发者指南:了解源码构建、测试和贡献流程。
  • 资源:查看版本日志、路线图、社区、关联项目、论文和贡献者信息。

Contributing

VSAG 是免费和开源的。你可以在 GitHub 上获取到源代码,以及提交错误报告和功能请求到 GitHub问题跟踪器 上。VSAG 依靠社区来修复错误和增加功能:如果你想做出贡献,请阅读 贡献指南 并考虑 创建合并请求

License

VSAG 源代码和文档在 Apache-2.0 许可证下发布。

安装

VSAG 是一个向量检索库,支持在 C++、Python 和 Node.js / TypeScript 程序中使用。核心 C++ 库支持 Linux,也支持在 Apple Silicon 上使用 macOS 14 或更高版本从源码构建。完整 CI 仍以 Linux 为主;预编译 C++ 包和 pyvsag wheel 当前也主要面向 Linux。

如果使用的是 Python,可以从官方第三方包仓库 PyPI 下载,包名为 pyvsagpyvsag 的版本与源代码版本一一对应,版本功能可以直接参考 GitHub 发布日志。Python 包使用 manylinux2014 构建,可以在绝大部分 Linux 环境中运行。通过如下命令获得最新版本:

pip install pyvsag

如果使用的是 Node.js,可以从 npm 直接安装 vsag 包:

npm install vsag

下载预编译二进制包

我们为 C++ 用户提供预编译的二进制包,可以在 GitHub Releases 中找到。

预编译二进制分为两个版本:

  • 旧的 pre-C++11 ABI:文件名为 vsag-vX.Y.Z-pre-cxx11-abi.tar.gz,使用 -D_GLIBCXX_USE_CXX11_ABI=0 编译;
  • C++11 ABI:文件名为 vsag-vX.Y.Z-cxx11-abi.tar.gz,使用 -D_GLIBCXX_USE_CXX11_ABI=1 编译。

其中 X.Y.Z 是版本号。两个版本分别满足不同应用对 ABI 的需求。

使用 Docker 镜像

我们也提供了包含完整开发工具链的 Docker 镜像,推荐用于开发和 CI:

docker pull vsaglib/vsag:ubuntu

镜像内的工具版本(clang-format / clang-tidy 等)与项目要求保持一致。

从源代码构建

VSAG 可以使用 CMake 从源代码构建,支持 Linux x86_64 / aarch64,以及 macOS arm64。macOS 当前验证范围是核心 C++ 库的 make debugmake releasemake test;Python wheel 打包仍以 Linux 为主。

构建依赖:

  • 操作系统:
    • Ubuntu 20.04 或更高版本
    • CentOS 7 或更高版本
    • macOS 14 或更高版本(Apple Silicon)
  • 编译器:
    • GCC 9.4.0 或更高版本
    • Clang 13.0.0 或更高版本
    • macOS 使用 Xcode Command Line Tools 提供的 Apple Clang
  • 构建工具:
    • CMake 3.18.0 或更高版本
    • clang-format 15(精确版本,用于代码格式化)
    • clang-tidy 15(精确版本,用于静态检查)
  • 其他依赖项:
    • Linux:gfortran、OpenMP、libaio、Python 3.6+、curl
    • macOS:Xcode Command Line Tools、Homebrew

依赖项可以通过以下脚本安装:

# 自动识别当前平台
./scripts/deps/install_deps.sh

# 也可以直接调用平台脚本
./scripts/deps/install_deps_ubuntu.sh  # Debian / Ubuntu
./scripts/deps/install_deps_centos.sh  # CentOS / AliOS
./scripts/deps/install_deps_macos.sh   # macOS

如需 Linux io_uring 存储后端,请安装 liburing,并在直接配置 CMake 时添加 -DENABLE_LIBURING=ON。未启用时,uring_io 配置会回退到 buffer_io。用法与 限制见磁盘索引

VSAG 使用 CMake 管理工程,常用构建目标封装在项目根目录的 Makefile 中。运行以下命令可以在发布模式下编译并安装:

make release && make install

更多构建选项请参考 编译构建

创建索引

VSAG 中所有检索能力都围绕 Index 接口展开。要使用某种索引,首先需要通过工厂方法 vsag::Factory::CreateIndex(name, parameters) 创建实例,其中:

  • name 是索引类型名称,对应 include/vsag/constants.h 中定义的常量;
  • parameters 是一段 JSON 字符串,声明数据类型、距离度量、维度等构建参数。

当前支持的索引类型

名称name 字符串文档适用场景
HGraphhgraphHGraphVSAG 自研图索引,支持多级量化和调优(详见 examples/cpp/103_index_hgraph.cpp
IVFivfIVF倒排索引,适合大 k 和批量查询
SINDIsindiSINDI稀疏向量上的倒排索引
SINDI_V2sindi_v2SINDI_V2支持内存与磁盘 I/O 的稀疏向量索引
PyramidpyramidPyramid多层级 / 按路径分区的索引结构
BruteForcebrute_force暴力搜索,用作基准或小数据集
GNO-IMIgno_imi基于 GNO-IMI 的倒排索引变体(作为 ivfpartition_strategy_type

完整示例可在 examples/cpp/ 目录中按照前缀编号依次查看(101_ ~ 109_ 为索引类型,2xx_ 为自定义资源,3xx_ 为功能特性)。

使用磁盘 I/O 的 SINDI_V2

std::string params = R"(
{
    "dim": 1024,
    "dtype": "sparse",
    "metric_type": "ip",
    "index_param": {
        "term_id_limit": 30000,
        "use_reorder": true,
        "term_io": {
            "type": "async_io",
            "file_path": "/path/to/sindi_v2.terms"
        },
        "rerank_io": {
            "type": "async_io",
            "file_path": "/path/to/sindi_v2.rerank"
        }
    }
}
)";
auto index = vsag::Factory::CreateIndex("sindi_v2", params).value();

通用的构建参数

所有索引在创建时都需要声明以下字段:

  • dtype:向量数据类型,当前常用为 "float32";部分索引也支持 "fp16""bf16""int8"
  • metric_type:距离度量方式,支持 "l2""ip""cosine"
  • dim:向量维度,必须与后续写入的数据一致。

索引特有参数以嵌套对象形式提供,例如 HGraph 在构建期使用 index_param 子对象(hgraph 保留给 ef_search 等查询期参数)。

示例:创建 HGraph 索引

#include <vsag/vsag.h>

auto hgraph_build_parameters = R"(
{
    "dtype": "float32",
    "metric_type": "l2",
    "dim": 128,
    "index_param": {
        "base_quantization_type": "fp32",
        "max_degree": 16,
        "ef_construction": 100
    }
}
)";
auto index = vsag::Factory::CreateIndex("hgraph", hgraph_build_parameters).value();

示例:创建 HGraph 索引(SQ8 量化)

auto hgraph_build_parameters = R"(
{
    "dtype": "float32",
    "metric_type": "l2",
    "dim": 128,
    "index_param": {
        "max_degree": 32,
        "ef_construction": 100,
        "base_quantization_type": "sq8"
    }
}
)";
auto index = vsag::Factory::CreateIndex("hgraph", hgraph_build_parameters).value();

不同索引支持的参数及其语义,请参考 索引参数

k-近邻搜索

以下内容假设你已经安装 VSAG。我们提供了 C++、Python、TypeScript 三种语言的代码示例,可以在 examples/ 目录找到。本页以 C++ BruteForce 索引为例,完整示例见 examples/cpp/105_index_brute_force.cpp

在多数情况下,程序入口需要调用一次 vsag::init() 来完成一次性的初始化(如全局日志、分配器等)。本页示例中省略了其他辅助代码,以突出关键步骤。

获取一些向量

VSAG 主要用于处理固定维度 d 的向量集合,通常维度为几百到几千维。这些向量需要按照按行的方式组织才能写入 VSAG,类似于 vector[num_vectors][dim] 这样的 C++ 数组。从接口上说,VSAG 只依赖传入的向量集合首地址指针 (const float* 类型),所以应用可以自由选择使用 C++ 数组、std::vector 或者手动分配的内存来存储原始向量。

当前 VSAG 只支持 32-bit 的浮点数向量。

一次 k-近邻搜索需要两个向量集合。

  • base 集合代表数据库中的所有向量,我们将会在其中进行搜索,它的大小是 向量数 * 向量维度
  • query 集合代表查询向量,我们要为其查找最近的邻居,它的大小是 向量数 * 向量维度。当前,VSAG 只支持 向量数 = 1的查询,即不支持批量查询;

现在,我们生成一些 d=128 维的随机向量,以及它们对应的 ID(搜索结果会以 ID 形式返回)。

    int64_t num_vectors = 10000;
    int64_t dim = 128;
    int64_t *ids = new int64_t[num_vectors];
    float *datas = new float[num_vectors * dim];
    std::mt19937 rng(47);
    std::uniform_real_distribution<float> distrib_real;
    for (int64_t i = 0; i < num_vectors; ++i) {
        ids[i] = i;
    }
    for (int64_t i = 0; i < dim * num_vectors; ++i) {
        datas[i] = distrib_real(rng);
    }

    float* query_vector = new float[dim];
    for (int64_t i = 0; i < dim; ++i) {
        query_vector[i] = distrib_real(rng);
    }

这里使用的是 C++ 原生数组。当然,你也可以使用 std::vector<float> 来实现,并且通过 data() 方法得到数组首地址。

构建索引并写入向量

VSAG 库的使用主要围绕着 Index 接口,它封装了向量集合,并且提供了一系列能力。在 VSAG 中,Index 有多种实现,每种实现具备的能力和适用的场景不尽相同。在这个示例中,我们将使用最简单的版本,基于暴力搜索的索引 BruteForce

所有索引都需要显式地创建,从而声明向量的维度和相似度计算方法。在这个示例中,向量的维度 dim=128,相似度使用欧几里得距离(L2)计算。

    std::string brute_force_build_parameters = R"(
    {
        "dtype": "float32",
        "metric_type": "l2",
        "dim": 128
    }
    )";
    auto index = vsag::Factory::CreateIndex("brute_force", brute_force_build_parameters).value();

向量索引的数据写入涉及到两个方法:BuildAdd。Build 方法带初始化性质,一些依赖训练过程来分析数据分布的索引,需要通过调用 Build 方法来启用。Add 是一般性的向量数据写入方法,大部分的索引都实现了这个方法,除了部分完全静态的索引类型。

BruteForce 索引支持用 Build 和 Add 方法写入数据,这里我们用 Add 方法来演示。

    auto base = vsag::Dataset::Make();
    base->NumElements(num_vectors)
        ->Dim(dim)
        ->Ids(ids)
        ->Float32Vectors(datas)
        ->Owner(false);
    index->Add(base);

搜索

向量索引的一个核心作用是 k-近邻 搜索,即对于每个查询向量,在数据库中查找 k 个最相近的邻居。

搜索方法需要传入查询向量、k 值以及搜索参数。BruteForce 索引没有搜索参数,所以这里传入一个空的 Json 字符串。返回的结果中包含两个信息:最相近邻居的 ID ,最相近邻居与查询向量的距离。这两个信息可以分别通过结果集的 GetIds() 和 GetDistances() 方法获得。

    auto query = vsag::Dataset::Make();
    query->NumElements(1)->Dim(dim)->Float32Vectors(query_vector)->Owner(false);

    auto brute_force_search_parameters = R"({})";
    int64_t topk = 10;
    auto result = index->KnnSearch(query, topk, brute_force_search_parameters).value();

    std::cout << "results: " << std::endl;
    for (int64_t i = 0; i < result->GetDim(); ++i) {
        std::cout << result->GetIds()[i] << ": " << result->GetDistances()[i] << std::endl;
    }

搜索请求至多返回 k 个结果,这些结果按照最近邻和查询向量的距离升序排序。输出的结果类似于:

按阈值过滤

阈值过滤支持仍在维护的索引:BruteForce、HGraph、IVF、Pyramid、SINDI 和 SIMQ。 HNSW 和 DiskANN 已移除且不受支持;本选项不会为这两个旧索引新增或修改行为。

KNN 搜索参数支持在 JSON 顶层设置可选的 threshold

{
  "threshold": 4.0,
  "hgraph": { "ef_search": 64 }
}

设置后,结果至多返回 k 个邻居,并且每个返回距离都小于或等于 threshold。 边界值会被包含,结果仍按距离升序排列,因此可能返回少于 k 个结果,甚至没有结果。 不设置该字段时,KNN 行为保持不变。阈值使用索引返回的距离语义:l2 为 L2 平方距离, ip1 - inner_productcosine1 - cosine_similarity。因此 ip 可以使用负阈值; 阈值必须是有限的 JSON 数字。

通过统一请求接口时,BruteForce、HGraph 和 IVF 支持使用 SearchRequest::threshold_; Pyramid、SINDI 和 SIMQ 通过 KNN JSON 参数支持该选项。这六种索引是本功能支持的仍在维护的索引。 该选项只限制 KNN 结果;范围搜索请使用 RangeSearch 及其 radius 参数。threshold 可以和现有索引专用搜索参数同时传入。

results:
6519: 13.855
2332: 15.2735
2126: 15.5844
7388: 15.6583
795: 15.5958
3979: 15.815
4756: 15.9983
510: 16.1128
8703: 16.1161
5583: 16.1256

C API

VSAG 在 include/vsag/vsag_c_api.h 中导出 C ABI。它封装了与 C++ API 相同的索引实现, 适合需要不透明句柄和 C 风格函数的应用及语言绑定。

当前安装的头文件不能直接被 C 编译单元包含:其中使用了未加条件保护的 extern "C" 代码块和 C++ bool。请将包含该头文件的编译单元按 C++ 编译(例如使用 .cpp 文件)。 如果应用的其余部分使用 C,请在这个 C++ 编译单元中封装所需操作,再导出应用自己的 C 接口。

生命周期

在 C++ 编译单元中包含安装后的头文件,并链接普通 VSAG 库:

#include <vsag/vsag_c_api.h>

基本生命周期如下:

  1. 使用 vsag_index_factory 创建不透明的 vsag_index_t
  2. 构建索引,或先训练再增量添加向量。
  3. 执行搜索、更新、序列化或读取操作。
  4. 使用 vsag_index_destroy 释放句柄。

创建失败时,vsag_index_factory 返回 nullptr。其他函数返回 Error_tcode == VSAG_SUCCESS 表示成功,否则可从 message 读取错误详情。

const char* build_params =
    "{\"dtype\":\"float32\",\"metric_type\":\"l2\",\"dim\":128,"
    "\"index_param\":{\"base_quantization_type\":\"fp32\","
    "\"max_degree\":32,\"ef_construction\":200}}";

vsag_index_t index = vsag_index_factory("hgraph", build_params);
if (index == nullptr) {
    /* 处理创建失败 */
}

Error_t error = vsag_index_build(index, base, ids, 128, count);
if (error.code != VSAG_SUCCESS) {
    /* 检查 error.message */
}

vsag_index_destroy(index);

搜索结果缓冲区

SearchResult_t 中的 idsdists 缓冲区由调用方持有。执行 KNN 或范围 搜索前,两个缓冲区都至少分配 k 个元素。VSAG 最多写入 k 条结果,并把实际 数量写入 count

int64_t result_ids[10];
float result_dists[10];
SearchResult_t result{};
result.dists = result_dists;
result.ids = result_ids;

Error_t error =
    vsag_index_knn_search(index, query, 128, 10, "{\"hgraph\":{\"ef_search\":100}}", &result);

带过滤器的重载接收 FilterFunc_t。回调返回 true 表示该 ID 有效,可以出现 在结果中。

范围搜索结果上限

两个 C 范围搜索接口都要求显式传入正数结果上限:

Error_t
vsag_index_range_search(vsag_index_t index,
                        const float* query,
                        uint64_t dim,
                        float radius,
                        int64_t k,
                        const char* parameters,
                        SearchResult_t* result);

k 必须至少为 1。它会传递给 C++ 范围搜索的 limited_size 参数,同时保护 调用方缓冲区:result.count 不会超过 k。从旧签名升级时,需要为 vsag_index_range_searchvsag_index_range_search_with_filter 同时补上该参数。

接口分组

  • 创建与释放: vsag_index_factoryvsag_index_destroy
  • 写入数据: vsag_index_buildvsag_index_trainvsag_index_add
  • 搜索: vsag_index_knn_searchvsag_index_range_search 及其过滤器重载。
  • 读取与更新: vsag_index_calculate_distance_by_idsvsag_index_get_vector_by_idsvsag_index_update_idsvsag_index_update_vectorvsag_index_update_vector_force
  • 复制与复用: vsag_index_clonevsag_index_export_model
  • 持久化: vsag_serialize_filevsag_deserialize_filevsag_serialize_write_funcvsag_deserialize_read_func

具体接口是否可用仍取决于索引类型。不支持的操作返回 VSAG_UNSUPPORTED_INDEX_OPERATION

持久化错误

vsag_serialize_filevsag_deserialize_file 会校验目标文件是否成功打开。 不要假定文件操作已经完成;应检查返回的 Error_t 是否为 VSAG_READ_ERRORVSAG_MISSING_FILE 或其他非成功状态。

应用自行管理存储时,可以使用基于回调的持久化接口。回调中的 offset 和 size 使用头文件里声明的 OffsetTypeSizeType

参考

include/vsag/vsag_c_api.h 是函数签名、错误码、回调和结构体的完整源码级 参考。搜索参数 JSON 与对应索引页面中的 C++ API 相同。

pyvsag

pyvsag 是 VSAG 的 Python 绑定包,接口封装基于 pybind11 实现,源代码位于仓库 python_bindings/ 目录,打包脚本位于 python/

安装

从 PyPI 安装最新发布版本:

pip install pyvsag

需要在 Linux 环境下使用(manylinux2014 wheel)。如果希望构建本地 wheel,可以运行:

# 构建特定 Python 版本的 wheel
make pyvsag PY_VERSION=3.11

# 或一次构建所有受支持版本
make pyvsag-all

快速开始

pyvsag 暴露一个与 C++ Index 对象对应的 Index 类,构建与搜索参数使用 JSON 字符串传递:

import json
import numpy as np
import pyvsag

dim = 128
num_elements = 1000

ids = np.arange(num_elements, dtype=np.int64)
data = np.float32(np.random.random((num_elements, dim)))

index_params = json.dumps({
    "dtype": "float32",
    "metric_type": "l2",
    "dim": dim,
    "index_param": {
        "base_quantization_type": "fp32",
        "max_degree": 16,
        "ef_construction": 100,
    },
})

index = pyvsag.Index("hgraph", index_params)
index.build(vectors=data, ids=ids, num_elements=num_elements, dim=dim)

query = np.float32(np.random.random(dim))
search_params = json.dumps({"hgraph": {"ef_search": 100}})
result_ids, result_dists = index.knn_search(
    vector=query, k=10, parameters=search_params,
)
for rid, rdist in zip(result_ids, result_dists):
    print(f"{rid}: {rdist}")

完整示例请查阅仓库中的 examples/python/ 目录,建议从 103_index_hgraph.py 开始。

保存与加载

index.save("index.bin")

new_index = pyvsag.Index("hgraph", index_params)
new_index.load("index.bin")

当前 saveload 包装不会传播底层序列化和反序列化操作返回的错误。请确认输出文件 已经生成,并对重要索引执行保存后重新加载的 round-trip 校验,再依赖持久化结果。

索引操作

除构建、搜索、保存与加载外,Python 绑定还提供以下索引管理方法:

方法返回值说明
get_num_elements()int索引当前元素数量
get_memory_usage()int索引占用的内存字节数
check_id_exist(id)bool指定 ID 当前是否存在
get_min_max_id()(min_id, max_id)操作失败时返回 (-1, -1)
add(vectors, ids, num_elements, dim)None添加连续的稠密向量矩阵;dtype 必须与索引一致
remove(ids)int删除一维 int64 ID 数组;没有 ID 被删除和底层操作失败时都会返回 0
cal_distance_by_id(query, ids)numpy.ndarray每个 ID 返回一个 float32 距离;无效 ID 为 -1,底层操作失败时整个结果保持为 -1
# 添加两条稠密向量
new_vectors = np.random.random((2, dim)).astype(np.float32)
new_ids = np.array([10_001, 10_002], dtype=np.int64)
index.add(new_vectors, new_ids, len(new_ids), dim)

assert index.check_id_exist(10_001)
print(index.get_num_elements(), index.get_memory_usage())
print(index.get_min_max_id())

# 计算一个 query 到候选 ID 列表的距离
candidate_ids = np.array([10_001, 10_002], dtype=np.int64)
distances = index.cal_distance_by_id(query, candidate_ids)

# 删除 ID;返回值为实际删除数量
removed = index.remove(candidate_ids)

add 接受展开的连续数组或 row-major 二维矩阵。dtype: "float16" 时传入 numpy.float16dtype: "bfloat16" 时传入包含 BF16 位模式的 numpy.uint16 数组。 具体操作是否受支持仍取决于索引类型。输入校验和 add 等操作会抛出 Python 异常; removecal_distance_by_idsaveload 则使用上述 sentinel 或未检查行为, 不会统一传播底层操作错误。

与 C++ 库的关系

pyvsag 绑定的是同一份核心 C++ 实现,行为和性能特征与 C++ 版本保持一致。因此:

  • 大多数 C++ 参数在 Python 中以相同的 JSON 字段传递;
  • C++ 版本新增的索引类型、量化方式、距离度量会随 pyvsag 的下一个 release 一同发布;
  • 构建 wheel 时所使用的依赖项与发布版 C++ 库相同(OpenBLAS、libaio 等)。

关于可用参数和索引类型,请参考 创建索引索引参数

索引

VSAG 提供了一系列索引实现,它们共享同一套构建式 API、同一种序列化格式、同一组操作 (BuildAddKnnSearchRangeSearchRemoveSerialize / Deserialize 等), 差异在于底层使用的数据结构与折中取舍。

本节覆盖当前活跃维护的索引:

索引文档适用场景
hgraphHGraph通用高召回图索引,量化选项丰富
lazy_hgraphLazyHGraph从小规模精确检索开始、后续自动转换为 HGraph 的 FP32 集合
ivfIVF基于分桶的检索,适合高吞吐批查询与超大规模语料
sindiSINDI稀疏向量(BM25 / 学习稀疏)上的内积检索
simqSIMQ多向量检索(ColBERT / late-interaction),基于 MaxSim 打分
sindi_v2SINDI_V2支持内存与磁盘 I/O 的稀疏向量(BM25 / 学习稀疏)检索
pyramidPyramid多租户 / 标签分区的层级索引

brute_force 作为精确检索基线也可使用(见 创建索引examples/cpp/105_index_brute_force.cpp)。

参数约定

所有索引共享以下顶层构建字段:

字段可选值说明
dim正整数向量维度;构建后不可变
dtypefloat32 / float16 / bfloat16 / int8 / sparsesparse 仅 SINDI / SINDI_V2 使用
metric_typel2 / ip / cosine查询时必须保持一致(SINDI / SINDI_V2 仅支持 ip

索引特有的构建参数放在 index_param 子对象中;查询参数放在以索引名命名的子对象中 (例如 hgraphivfsindisindi_v2pyramid)。LazyHGraph 转换到 graph 阶段后也使用 hgraph 搜索参数。具体参数定义在各索引页面内给出,也可查阅 索引参数 进行总览。

索引参数

本页汇总 VSAG 各索引类型的常用参数。完整枚举请参考源码:

  • 构建参数键:src/constants.cpp
  • 公开常量:include/vsag/constants.h
  • 每个索引的示例:examples/cpp/*_index_*.cpp(例如 103_index_hgraph.cpp

通用参数

所有索引在构建时都接受以下顶层字段。其中 dtypemetric_type 必填,repr 可选。 非稀疏数据必须提供 dimdtype: "sparse" 省略 dim 时默认使用 4096

字段取值说明
dim正整数向量维度,构建后不可更改
dtypefloat32 / fp16 / bf16 / int8 / sparse标量值类型;sparse 为稀疏索引兼容取值
reprdense / sparse / multi_vector可选的数据布局;省略时,dtype: "sparse" 推断为 sparse,其他类型推断为 dense
metric_typel2 / ip / cosine距离度量

dtyperepr 描述不同维度:前者是标量编码,后者是记录布局。显式设置 repr 时, dtype: "sparse" 要求 repr: "sparse"。受支持的多向量索引使用 repr: "multi_vector",并配合 float32 等标量 dtype

HGraph

HGraph 的构建参数使用通用的 index_param 键(参见 examples/cpp/103_index_hgraph.cpp); hgraph 键则保留给搜索期参数。

{
    "dim": 128,
    "dtype": "float32",
    "metric_type": "l2",
    "index_param": {
        "base_quantization_type": "fp32",
        "max_degree": 32,
        "ef_construction": 400
    }
}
字段典型值说明
max_degree16~48每节点最大出边数
ef_construction200~500构建阶段候选集大小,越大召回越高、构建越慢
base_quantization_typefp32 / fp16 / bf16 / sq8 / sq4 / pq主存储的量化策略 —— 支持的全部取值见量化章节
use_reverse_edgesfalse跟踪入边,实现 O(1) 反向邻居查找;边存储约翻倍,且压缩图存储不支持
label_remap_typepglabel map 实现:默认 pg,或 robin
reorder_sourcepreciseprecise 存储或直接从 base 重排;RaBitQ x+y split(包括 tq_chain="mrle, rabitq")会自动选择 base
persist_source_idfalse序列化 HGraph 时保留 Source ID 元数据;适用于恢复索引后继续导出构建缓存
use_conjugate_graphfalse启用 HGraph 反馈/预训练并持久化辅助共轭图
mrle_dim0MRLE 输出维度,范围 [0, dim]0 表示输入维度
fast_encode_rabitqtrue使用多 bit RaBitQ 快速编码;设为 false 恢复精确编码器
fast_encode_rabitq_rounds6快速编码器微调轮数,范围 [1, 32]

搜索时:

{"hgraph": {"ef_search": 100}}

ef_search 接受任意正的有符号 64 位整数,不再存在与 topk 相关的上限。非常大的取值会 明显增加延迟和搜索前沿占用的内存。 use_conjugate_graph_search 为布尔值(默认 true);当构建时设置 use_conjugate_graph: true 后,它控制是否使用已学习的共轭边。

hgraph 搜索参数还接受 brute_force_threshold[0.0, 1.0] 区间的 float, 默认 0.0)。当取值 > 0 且当前请求的 filter 的 ValidRatio() 不超过该 阈值时,HGraph 会跳过图遍历,直接在通过过滤的 id 上做精确暴扫。详见 HGraph 索引文档

LazyHGraph

LazyHGraph 的构建参数可以放在顶层 lazy_hgraph 对象中(推荐,语义更清晰),也可以放在 通用的 index_param 对象中。hgraph 子对象会转交给转换后的内部 HGraph。

{
    "dim": 128,
    "dtype": "float32",
    "metric_type": "l2",
    "lazy_hgraph": {
        "transition_threshold": 1000,
        "hgraph": {
            "base_quantization_type": "sq8",
            "max_degree": 26,
            "ef_construction": 100
        }
    }
}
字段典型值说明
transition_threshold1000 或按业务规模设置从精确 flat 搜索转换到 HGraph 的正整数向量数量阈值
hgraphHGraph 构建对象graph 阶段的参数;见 HGraph

LazyHGraph 只支持 dtype: "float32"。搜索参数使用 hgraph 对象,例如 {"hgraph": {"ef_search": 100}}。详见 LazyHGraph 索引文档

hgraph 搜索参数还接受以下 filter 相关参数:

参数类型默认值说明
skip_ratiofloat0.2控制带 filter 搜索时跳过候选检查的比例,取值范围为 [0.0, 1.0]。值越大,跳过越激进,搜索越快但可能影响召回。
skip_strategystring"deterministic_accumulative"跳过策略。支持 "random""deterministic_accumulative"

IVF

{
    "ivf": {
        "nlist": 4096,
        "base_quantization_type": "sq8",
        "nprobe": 32
    }
}

Brute Force

{"brute_force": {}}

无需额外参数。

Pyramid

Pyramid 构建参数同样放在 index_param 下:

{
    "dtype": "float32",
    "metric_type": "l2",
    "dim": 128,
    "index_param": {
        "base_quantization_type": "sq8",
        "max_degree": 24,
        "ef_construction": 300,
        "store_paths": true
    }
}

store_paths 是 Pyramid 顶层构建参数,默认值为 false。需要通过 GetDataByIdsWithFlagDATA_FLAG_PATH 返回默认或命名 hierarchy 的原始路径时应启用它;路径完整性与持久化语义见 Pyramid 参数表

MRLE 与 split RaBitQ 组合使用 base_quantization_type: "tq"tq_chain: "mrle, rabitq"mrle_dim,以及 rabitq_bits_per_dim_base/rabitq_bits_per_dim_precise。该配置会自动从 split base codes 精排,并保留原始 FP32 向量供仅解码路径使用。完整配置及存储/召回权衡见 Pyramid 页面

SINDI(稀疏向量)

{
    "dtype": "sparse",
    "metric_type": "ip",
    "dim": 1024,
    "index_param": {
        "term_id_limit": 30000,
        "doc_prune_ratio": 0.1
    }
}

use_quantization、不可变构建与 n_candidate 等搜索参数见 SINDI 页面

SINDI_V2(稀疏向量)

SINDI_V2 兼容 SINDI 的全部功能,并支持内存与磁盘 I/O。

{
    "dtype": "sparse",
    "metric_type": "ip",
    "dim": 1024,
    "index_param": {
        "term_id_limit": 30000,
        "use_reorder": true,
        "term_io": {
            "type": "async_io",
            "file_path": "/path/to/sindi_v2.terms"
        },
        "rerank_io": {
            "type": "async_io",
            "file_path": "/path/to/sindi_v2.rerank"
        }
    }
}

详情见 SINDI_V2 页面

运行期参数

除构建参数外,Index::TuneSearchParam 可在运行时调整 ef_searchnprobe 等参数。参考 优化器 与各 examples/cpp/3xx_feature_*.cpp 示例。

HGraph

HGraph 是 VSAG 的旗舰 图索引。它构建的是多层近邻图, 提供了丰富的量化方案、统一的构建参数 schema(index_param),并原生支持精排(reorder)、 增量更新、删除、以及基于 ELP 的运行时自动调优。

对于大多数稠密向量场景(文本 / 图像 / 多模态 embedding,维度 64–4096,规模从数千到数亿), HGraph 都是推荐的默认索引。

工作原理

  1. 构图。 向量被组织成层级近邻图:上层作为导航入口,底层连接每个数据点到在 max_degree 预算内的最近邻。构图算法可以是 NSW 风格插入(graph_type: "nsw",默认) 或 ODescent(graph_type: "odescent")。
  2. 量化。 底层存储使用可配置的量化器进行压缩(base_quantization_typefp32fp16bf16sq8sq4sq8_uniformsq4_uniformpqpqfsrabitqtq)。 可选地再保留一份高精度副本(use_reorder: true 搭配 precise_quantization_type), 用于对粗排结果进行重打分。
  3. 搜索。 自顶向下在图上做贪心 beam search,扩展候选集到 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,
        "ef_construction": 400
    }
})";
auto index = vsag::Factory::CreateIndex("hgraph", params).value();

// 构建索引。
auto base = vsag::Dataset::Make();
base->NumElements(n)->Dim(128)->Ids(ids)->Float32Vectors(data)->Owner(false);
index->Build(base);

// 执行检索。
auto query = vsag::Dataset::Make();
query->NumElements(1)->Dim(128)->Float32Vectors(q)->Owner(false);
auto result = index->KnnSearch(
    query, /*topk=*/10, R"({"hgraph": {"ef_search": 100}})").value();

构建参数

构建参数放在 index_param 下。下表列出最常用的配置项;完整列表请见 索引参数

参数类型默认值说明
base_quantization_typestring—(必填)fp32fp16bf16sq8sq4sq8_uniformsq4_uniformpqpqfsrabitqtq —— 各量化器细节见量化章节
max_degreeint64图节点最大出度
ef_constructionint400构建阶段的候选集大小(越大召回越高,构建越慢)
graph_typestring"nsw"构图算法:nswodescent
use_reverse_edgesboolfalse跟踪入边,实现 O(1) 反向邻居查找;边存储约翻倍,且 graph_storage_type: "compressed" 不支持
label_remap_typestring"pg"label 到内部 ID 的 map 实现:"pg""robin";恢复或组合兼容索引时应保持一致
use_reorderboolfalse是否额外保留一份高精度副本用于精排
reorder_sourcestring"precise""precise" 编码或直接从 "base" 编码重排;RaBitQ x+y split(包括 tq_chain: "mrle, rabitq")会自动设置为 "base"
precise_quantization_typestring"fp32"精排使用的量化类型(仅在 use_reorder: true 时生效)
base_pq_dimint1PQ 子空间数(pq / pqfs 时必填)
mrle_dimint0tq_chain 中 MRLE 的输出维度,范围 [0, dim]0 表示输入维度
fast_encode_rabitqbooltrue使用多 bit RaBitQ 快速编码;设为 false 使用原有精确编码器
fast_encode_rabitq_roundsint6RaBitQ 快速编码的坐标微调轮数,范围 [1, 32]
build_thread_countint100构建阶段并发线程数
support_duplicateboolfalse是否在插入时做重复 ID 检测
deduplicate_storageboolfalse让重复向量共享存储;需同时设置 support_duplicate: true
duplicate_distance_thresholdfloat0.0重复判定距离阈值。大于 0 时按最近候选的距离判重;等于 0 时退化为当前编码 memcmp 判重
support_removeboolfalse是否启用 mark-remove 恢复路径所需的图删除追踪元数据
support_force_removeboolfalse是否启用 RemoveMode::FORCE_REMOVE 及其额外同步
store_raw_vectorboolfalse除量化副本外再保留原始向量(cosine 场景有用)
use_elp_optimizerboolfalse构建完成后自动调优检索参数
base_io_type / precise_io_typestring"block_memory_io"存储后端(memory_ioblock_memory_iobuffer_ioasync_iouring_iommap_io
base_file_path / precise_file_pathstring磁盘后端时的文件路径(使用 mmap_io / async_io / uring_io / buffer_io 时必填)
base_direct_read / precise_direct_readboolfalse使用 uring_io 时,以 direct IO 打开对应文件而非经过页缓存
hgraph_init_capacityint100初始容量提示(不会限制最终规模)
persist_source_idboolfalse序列化时保留 Source ID 元数据,使恢复后的索引仍可导出可复用的构建缓存
use_conjugate_graphboolfalse启用 Feedback/Pretrain 图增强;详见图索引增强
resize_increase_count_bitint10扩容批次 slot 数的 log2,取值范围为 1311 表示每次按 2 个 slot 对齐,10 表示按 1024 个 slot 对齐。较小取值减少预分配,但可能增加重分配次数。

use_reverse_edges 面向需要快速检查入邻居、图分析或图维护算法的负载。维护反向邻接表会让边 存储约翻倍,因此默认关闭。

label_remap_type 只改变内部 label map,不改变用户 ID。默认值为 "pg""robin" 选择另一种 robin-map 实现,建议针对实际 ID 分布实测后再调整。

向量存储去重

同时设置 support_duplicate: truededuplicate_storage: true 后,重复向量会共享 同一个物理编码槽位,但仍保留各自的标签。该选项目前仅支持使用 graph_type: "nsw" 的 稠密向量 HGraph 索引;graph_type: "odescent" 不支持。

启用存储去重后,暂不支持以下操作和配置:

  • 强制删除(support_force_remove: true);
  • 调用 ImportCache() 后基于缓存加速构建;
  • Merge
  • v0.14 旧版序列化格式。

UpdateVector 仅支持尚未与其他重复组成员共享向量存储的 ID。

当前序列化格式和 streaming serialization 均受支持。

支持的输入数据类型

顶层构建配置中的 dtype 字段决定 Dataset 如何解释原始向量字节。HGraph 支持四种输入类型, dtype 值、对应的 Dataset setter 与演示示例见下表。

dtype元素类型Dataset setter示例
float32floatFloat32Vectors103_index_hgraph.cpp
int8int8_tInt8Vectors316_index_int8_hgraph.cpp
float16uint16_t(按 IEEE 754 binary16 位模式打包)Float16Vectors321_index_fp16_hgraph.cpp
bfloat16uint16_t(按 Brain Float 位模式打包)Float16Vectors(与 FP16 共用)321_index_fp16_hgraph.cpp 基础上按下文调整

dim 始终表示逻辑维度(元素数量),与字节长度无关,因此四种数据类型下 dim 取值相同。

int8 输入

量化好的 int8 向量直接通过 Int8Vectors 传入:

std::vector<int8_t> data(num_vectors * dim);  // 填入 int8 元素
auto base = vsag::Dataset::Make();
base->NumElements(num_vectors)->Dim(dim)->Ids(ids)
    ->Int8Vectors(data.data())->Owner(false);

对应构建配置(注意 dtype: "int8"):

{
    "dtype": "int8",
    "metric_type": "l2",
    "dim": 128,
    "index_param": {
        "base_quantization_type": "pq",
        "max_degree": 26,
        "ef_construction": 100,
        "alpha": 1.2
    }
}

查询时同样使用 Int8Vectors 和相同的 dtype。可运行示例: 316_index_int8_hgraph.cpp

float16 / bfloat16 输入

FP16 与 BF16 都通过 Float16Vectors 传入,参数类型为 const uint16_t*,指向各元素的 16 位 存储。从 float 到 16 位格式的转换由调用方负责。VSAG 源码树内提供了便捷辅助函数 (vsag::generic::FloatToFP16 位于 src/simd/fp16_simd.hvsag::generic::FloatToBF16 位于 src/simd/bf16_simd.h), 但它们是内部头文件,并未通过 include/vsag/ 对外安装。链接已安装版 VSAG 库的应用需要自行 完成转换(例如复制这段小工具函数、使用 _cvtss_sh / F16C 内置指令,或调用任意 FP16 库)。下面 的示例代码为了简洁直接使用了源码树内的辅助函数:

// 下面的 fp16/bf16 辅助函数位于 src/simd/,并未随 VSAG 一并安装。
// 链接已安装版 VSAG 时,请替换为自行实现的 float -> uint16_t 转换。
#include "simd/fp16_simd.h"  // FloatToFP16(BF16 场景改为 simd/bf16_simd.h / FloatToBF16)

std::vector<uint16_t> data(num_vectors * dim);
for (size_t i = 0; i < data.size(); ++i) {
    data[i] = vsag::generic::FloatToFP16(some_float_source());
}
auto base = vsag::Dataset::Make();
base->NumElements(num_vectors)->Dim(dim)->Ids(ids)
    ->Float16Vectors(data.data())->Owner(false);

构建配置:

{
    "dtype": "float16",
    "metric_type": "l2",
    "dim": 128,
    "index_param": {
        "base_quantization_type": "pq",
        "max_degree": 26,
        "ef_construction": 100,
        "alpha": 1.2
    }
}

切换到 BF16 时,将 dtype 改为 "bfloat16"、把 FloatToFP16 替换为 FloatToBF16 即可; Float16Vectors setter 与构建/检索流程不变。可运行 FP16 示例: 321_index_fp16_hgraph.cpp

注意。 321_index_fp16_hgraph.cpp 文件头注释提到 BFloat16Vectors(),但该 setter 并不 存在 —— FP16 与 BF16 都通过 Float16Vectors 传入。无论 dtype"float16" 还是 "bfloat16",都使用同一个 setter。

输入类型选择建议

  • 对精度要求最高、且内存预算充裕时,选 float32(默认)。
  • 想把输入存储减半,选 float16 / bfloat16。FP16 指数范围更小,BF16 尾数位更少但指数范围 与 FP32 一致,对 embedding 类向量通常更友好。
  • 数据本身已是整数量化结果(来自上游量化器或 int8 输出的模型)时,选 int8。此时通常仍配合 pq / sq8 之类的索引内量化器使用。

dtype 仅约束输入表示;索引内的实际存储仍由 base_quantization_type(以及 use_reorder: true 下的 precise_quantization_type)决定,因此 dtype: "float16" + base_quantization_type: "sq8" 这样的组合是允许的。

检索参数

检索参数放在 hgraph 子对象下:

参数类型默认值说明
ef_searchint64—(必填)正数搜索前沿大小;接受到 INT64_MAX,不存在与 topk 相关的上限。值越大,召回、延迟和前沿内存通常都越高。
hops_limitint不限beam search 在返回当前前沿前允许的最大跳数。
skip_ratiofloat0.2过滤场景下的性能调优参数。控制跳过无效点的比例,取值范围 [0.0, 1.0]skip_ratio=0.2 表示跳过 20% 的无效点,只检查 80% 的无效点。值越大性能越好但召回率可能越低。仅在带 filter 的搜索中生效。详见下文过滤跳过策略
skip_strategystring"deterministic_accumulative"过滤跳过的策略。可选值:"random"(随机跳过)或 "deterministic_accumulative"(确定性累积跳过)。详见下文过滤跳过策略
brute_force_thresholdfloat0.0选择率感知的暴搜回退开关。当取值 > 0 且当前 filter 的 ValidRatio() 小于等于 brute_force_threshold 时,搜索会完全跳过图遍历,直接在通过过滤的 id 上用最佳精度的 flatten 编码做一次暴力扫描(细节见下一节)。取值范围 [0.0, 1.0];默认 0.0 表示关闭,保持原有行为。
rabitq_one_bit_searchboolfalse启用 RaBitQ filter/lower-bound 路径;对 x+y split 索引会使用全部 x 个 filter bits,详见 RaBitQ x+y Split
rabitq_error_ratefloat索引默认值本次搜索使用的正数 lower-bound 误差倍率;调整它不需要重建 split 索引。
auto result = index->KnnSearch(
    query, topk, R"({"hgraph": {"ef_search": 200}})").value();

高选择性过滤下的暴搜回退(brute_force_threshold

图搜索在大多数候选都能通过过滤时是最优策略——图遍历能很快进入查询邻域。但是当 过滤越来越严格(只有极少数向量能通过)时,beam 需要扩展非常多的节点才能凑够 ef_search通过过滤的候选,此时召回率会下降,延迟反而上升。在某个临界点, 对通过过滤的 id 做一次完整暴扫既更快又精确。

brute_force_threshold 允许 HGraph 在每次查询时自动按 filter 选择率做这个切换:

// 当 filter 仅保留 ≤ 1% 的 id 时,自动改走暴力扫描。
auto params = R"({"hgraph": {"ef_search": 200, "brute_force_threshold": 0.01}})";
auto result = index->KnnSearch(query, topk, params, my_filter).value();

工作原理(实现位于 src/algorithm/hgraph/hgraph_search.cpp):

  • 暴搜回退仅在同时满足以下条件时触发:
    • brute_force_threshold > 0.0并且
    • 提供了 filter,并且
    • filter->ValidRatio() <= brute_force_threshold
  • Filter::ValidRatio() 的准确性会直接影响是否切换 —— 这是用户提供的提示值。 详见 带过滤的搜索 中关于该方法的约定。
  • 暴搜会遍历所有通过过滤的内部 id,并按 64 一批用当前最精确的 flatten 存储 计算距离(顺序:若启用了 store_raw_vector 则用原始向量;否则若 use_reorder=true 则用精排副本;否则用基础量化编码)。
  • 由于暴搜在有精排副本时本身就用了精确编码,走暴搜分支的查询不会再做精排
  • 该机制对 KnnSearch(非迭代器重载,也即 SearchWithRequest 与标准的 KnnSearch(query, k, params, filter) 走的入口)和 RangeSearch 生效;对 迭代器风格的 KnnSearch(..., IteratorContext*&, ...) 不生效,因为一次 扫描无法分页跨越多次迭代调用。

取值建议:

  • 不带过滤或过滤通过率较高的场景,保持默认 0.0
  • 高选择性过滤(如 ValidRatio ≤ 0.05)下,0.01–0.05 是合理起点。再往上调 实际上等于「只要带 filter 就走暴搜」。
  • 暴搜的代价大致是 O(N × dim)N 是索引内总向量数(与选择率无关,因为 每个 id 都要走一次 CheckValid)。当原本需要把 ef_search 调到很大才能 维持召回时,暴搜带来的收益最明显。

可运行示例: 322_feature_hgraph_brute_force_threshold.cpp

过滤跳过策略(skip_ratio 与 skip_strategy)

当搜索带有 filter 时,HGraph 在图遍历过程中需要频繁调用 Filter::CheckValid() 来验证每个候选点是否有效。这个检查可能很耗时(特别是复杂过滤逻辑)。skip_ratio 和 skip_strategy 提供了一种概率性优化:通过跳过部分 filter 检查来加速搜索,但可能降低召回率。

工作原理

这是一个概率性优化策略:我们事先不知道哪些点是有效的,因此按概率决定是否访问每个点。

  • skip_ratio(默认 0.2):控制跳过 filter 检查的激进程度。skip_ratio=0.2 表示跳过 20% 的无效点,只检查 80% 的无效点。值越大,跳过的越多,速度越快,但召回率可能越低。
  • skip_strategy(默认 “deterministic_accumulative”):决定如何分配跳过:
    • “random”:随机跳过。每个点被访问的概率为 visit_ratio = valid_ratio + (1 - valid_ratio) * (1 - skip_ratio),大约跳过 skip_ratio 比例的无效点。
    • “deterministic_accumulative”:确定性累积跳过。按固定间隔做出访问决策,使长期访问比例趋近于目标 visit_ratio,相比 random 策略方差更小。

具体公式:

  • 设 valid_ratio 为 filter 的全局有效率(来自 Filter::ValidRatio())
  • 每个点被访问的概率 = valid_ratio + (1 - valid_ratio) * (1 - skip_ratio)
  • 如果 Filter::ValidRatio() 估计准确,期望跳过约 skip_ratio 比例的无效候选检查

使用示例

// 保守设置:跳过 10% 的无效候选检查,适合召回率要求高、延迟不那么关键的场景
auto params = R"({"hgraph": {"ef_search": 200, "skip_ratio": 0.1}})";
auto result = index->KnnSearch(query, topk, params, my_filter).value();

// 使用随机策略
auto params = R"({"hgraph": {"ef_search": 200, "skip_ratio": 0.2, "skip_strategy": "random"}})";
auto result = index->KnnSearch(query, topk, params, my_filter).value();

// 激进跳过:跳过 50% 的无效候选检查,以更低延迟为目标
auto params = R"({"hgraph": {"ef_search": 200, "skip_ratio": 0.5}})";
auto result = index->KnnSearch(query, topk, params, my_filter).value();

取值建议

  • 默认 0.2:适合大多数场景,在性能和召回率之间取得平衡。
  • 0.1 或更低:保守设置,适合对召回率要求高、延迟不那么关键、可接受召回率下降的场景(如实时推荐系统)。
  • 0.5 或更高:激进跳过,适合对延迟敏感、可接受召回率下降的场景。
  • 0.0:不跳过任何点,等同于关闭此优化(所有点都会被检查)。

注意事项:

  • 仅在带 filter 的搜索中生效。无 filter 时这些参数会被忽略。
  • 如果 Filter::ValidRatio() 估计准确,性能优化效果更好。
  • 与 brute_force_threshold 可同时使用:当 filter 非常严格(ValidRatio 很小)时,brute_force_threshold 会触发暴搜回退;否则使用图遍历 + skip 优化。

何时选择 HGraph

  • 维度大约在 64–4096 的稠密 float 向量。
  • 对延迟敏感且要求高召回的场景。
  • 需要增量插入(可选通过 support_force_remove 打开物理删除)的混合负载。
  • 内存受限环境,可用 sq8 / sq4_uniform / pq 压缩,再配合 use_reorder 弥补召回。

对于 Source ID 稳定的每日构建或快照构建,参见 HGraph 构建缓存。其中说明了 Dataset::SourceIDExportCache/ImportCachepersist_source_id 与缓存命中率诊断。

如果你的业务偏向粗粒度分桶(每次查询只扫部分桶)或严重受 SSD I/O 制约,建议先对比 IVF 再决定是否选择 HGraph。

相关文档

LazyHGraph

LazyHGraph 是一个自适应的稠密向量索引:数据量较小时先使用精确的 BruteForce 索引,达到可配置的 transition_threshold 后自动转换为 HGraph。它适合“初始规模较小、 后续持续增长”的集合:早期查询保持精确并避免构图开销,规模变大后获得 HGraph 的近似检索 延迟与量化能力。

工作方式

  1. Flat 阶段。 达到阈值前,数据存放在内部 BruteForce 索引中,使用 FP32 向量, 搜索结果是精确的。
  2. 转换。Build 收到不少于 transition_threshold 条向量,或 Add 让 flat 阶段增长到该规模时,LazyHGraph 会用 flat 数据构建内部 HGraph。
  3. Graph 阶段。 转换完成后,新增数据与查询都交给内部 HGraph 处理。搜索参数仍使用 hgraph 子对象。

快速开始

#include <vsag/vsag.h>

std::string params = R"({
    "dtype": "float32",
    "metric_type": "l2",
    "dim": 128,
    "lazy_hgraph": {
        "transition_threshold": 1000,
        "hgraph": {
            "base_quantization_type": "sq8",
            "max_degree": 26,
            "ef_construction": 100,
            "build_thread_count": 4
        }
    }
})";
auto index = vsag::Factory::CreateIndex("lazy_hgraph", params).value();

auto base = vsag::Dataset::Make();
base->NumElements(n)->Dim(128)->Ids(ids)->Float32Vectors(data)->Owner(false);
index->Add(base);

auto query = vsag::Dataset::Make();
query->NumElements(1)->Dim(128)->Float32Vectors(q)->Owner(false);
auto result = index->KnnSearch(
    query, /*topk=*/10, R"({"hgraph": {"ef_search": 100}})").value();

构建参数

LazyHGraph 的构建参数放在顶层 lazy_hgraph 对象中。为了兼容通用工厂参数形态, 同一个对象也可以放在 index_param 中。

参数类型默认值说明
transition_thresholduint641000从 flat 阶段转换到 HGraph 的向量数量阈值,必须为正数。
hgraphobject{}转换后内部 HGraph 使用的构建参数。见 HGraph

LazyHGraph 只支持顶层 dtype: "float32"。flat 阶段固定使用 FP32 BruteForce 存储,不接受单独的 flat 量化参数。

搜索参数

搜索参数与 HGraph 一样,使用 hgraph 子对象:

{"hgraph": {"ef_search": 100}}

在 flat 阶段,搜索是精确的;进入 graph 阶段后,内部 HGraph 会使用传入的 HGraph 搜索参数,例如 ef_search

生命周期说明

  • Build 会根据输入规模选择初始阶段:小于 transition_threshold 保持 flat; 大于等于阈值则直接构建 HGraph。
  • Add 可能触发从 flat 到 graph 的单向转换。
  • flat 阶段的 Remove 始终执行物理删除,即使调用方传入 RemoveMode::MARK_REMOVE, 这样后续图转换不会携带 tombstone。
  • GetExtraInfoByIdsUpdateExtraInfo 与基于 extra_info 的过滤在两个阶段都支持。 见 Extra Info

何时使用 LazyHGraph

  • 稠密 FP32 集合初始较小,并会持续增长。
  • 集合较小时希望获得精确结果。
  • 希望同一个索引在规模变大后自动切换到 HGraph。

如果数据在构建时已经很大、需要非 FP32 输入类型,或希望从第一条插入开始就使用图索引行为, 请直接使用 HGraph

相关文档

IVF

IVF(Inverted File,倒排索引)是 VSAG 的 分桶式 索引。它在构建时将语料聚类成若干桶, 查询时只扫描与查询距离最近的若干个桶的中心对应的倒排列表,把 O(N) 的线性扫描降为 O(N · scan_buckets_count / buckets_count),并通过这两个参数在召回与延迟之间进行权衡。

与图索引相比,IVF 在召回上略有损失,但换来了更低的内存开销、更高的批量吞吐以及更简单的 切片方式——因此在语料非常大(数亿及以上)、内存紧张、或查询可天然并行化的场景中, IVF 通常是一个更合适的默认选择。

工作原理

  1. 聚类。 在数据集的采样上运行 k-means(或随机采样,ivf_train_type: "random") 得到 buckets_count 个中心(centroid)。
  2. 分配。 每条向量被写入距离最近的中心对应的倒排列表,以配置的粗量化 (base_quantization_type)存储;可选地再保留一份高精度副本(use_reorder: true) 用于精排。
  3. 检索。 查询时先计算查询向量与所有中心的距离,选出最近的 scan_buckets_count 个桶;然后只在这些桶内对向量打分。启用精排时,factor 控制从粗排阶段多取多少候选 再送入精排器重打分。

此外还有一种 GNO-IMI 策略(partition_strategy_type: "gno_imi"),它把空间按两组 正交中心划分(first_order_buckets_count × second_order_buckets_count),在超大规模 语料上能得到更精细的分区。

快速开始

#include <vsag/vsag.h>

std::string params = R"({
    "dtype": "float32",
    "metric_type": "l2",
    "dim": 128,
    "index_param": {
        "buckets_count": 256,
        "base_quantization_type": "sq8",
        "partition_strategy_type": "ivf",
        "ivf_train_type": "kmeans"
    }
})";
auto index = vsag::Factory::CreateIndex("ivf", params).value();

// 构建索引。
auto base = vsag::Dataset::Make();
base->NumElements(n)->Dim(128)->Ids(ids)->Float32Vectors(data)->Owner(false);
index->Build(base);

// 执行检索。
auto query = vsag::Dataset::Make();
query->NumElements(1)->Dim(128)->Float32Vectors(q)->Owner(false);
auto result = index->KnnSearch(
    query, /*topk=*/10,
    R"({"ivf": {"scan_buckets_count": 16}})").value();

支持的输入数据类型

当前公开的 BuildAdd 和检索路径接收通过 Dataset::Float32Vectors 提供的 FP32 向量,dtype 应设为 "float32"base_quantization_type 选择的是内部编码和存储,不会使 API 接受 FP16/BF16 输入。创建 IVF 索引时不支持 dtype: "int8"

构建参数

构建参数放在 index_param 下。完整列表请见 索引参数

参数类型默认值说明
partition_strategy_typestring"ivf"分桶策略:ivf(单层)或 gno_imi(双层正交)
buckets_countint10倒排列表数量(ivf 策略下生效)
first_order_buckets_countint10第一级桶数(gno_imi 策略下生效)
second_order_buckets_countint10第二级桶数(gno_imi 策略下生效)
ivf_train_typestring"kmeans"中心训练方式:kmeansrandom
route_max_degreeint64路由 HGraph 的最大度数(ivf 策略下生效)
route_ef_constructionint300路由 HGraph 的构建搜索宽度(ivf 策略下生效)
base_quantization_typestring"fp32"fp32fp16bf16sq8sq4sq8_uniformsq4_uniformpqpqfsrabitq —— 各量化器细节见量化章节
base_pq_dimint1PQ 子空间数(pq / pqfs 时必填)
rabitq_pca_dimint0base_quantization_type: "rabitq" 时可选的 PCA 预处理维度
rabitq_bits_per_dim_queryint32rabitq 查询每维位数;允许值为 432
rabitq_bits_per_dim_baseint1rabitq 底库存储码每维位数;允许范围为 [1, 8]
rabitq_versionstring"standard"rabitq 布局:"standard""split_1bit_7bit"
rabitq_error_ratefloat1.9rabitq 编码的正数误差预算参数
rabitq_use_fhtboolfalserabitq 二值化前是否启用 FHT 旋转
fast_encode_rabitqbooltrue多 bit rabitq 是否使用 CAQ 快速构建;设为 false 时使用精确编码
fast_encode_rabitq_roundsint6CAQ 微调轮数,允许范围 [1, 32]
use_reorderboolfalse是否保留高精度副本用于精排
precise_quantization_typestring"fp32"精排量化类型(use_reorder: true 时使用)
precise_codes_layoutstring"flat"精排 codes 的存储布局:"flat" 保持旧的一向量一码布局;"bucket" 在 basic posting 的相同 bucket 和 offset 保存高精度 code
base_io_typestring"memory_io"粗排向量的存储后端;以 liburing 构建时支持 uring_io
precise_io_typestring"block_memory_io"精排向量的存储后端(memory_ioblock_memory_iommap_iobuffer_ioasync_iouring_ioreader_io
precise_file_pathstring""当精排 IO 为磁盘后端时的文件路径

precise_codes_layout: "bucket" 要求 use_reorder: true,支持 memory_ioblock_memory_iobuffer_ioasync_iouring_io (需要构建环境支持 io_uring)。不支持 mmap_iopqfs 精排量化。bucket 布局当前 要求 buckets_per_data: 1;一个向量分配到多个 bucket 的配置会被拒绝。

对于 flatbucket 两种布局,序列化后的精排 codes 都可以通过外部 Reader 只读加载。向 Index::Load 传入 precise_io_type: "reader_io"precise_readerflat 布局的 reader 必须恰好对应 high_precision_codes block payload,bucket 布局 则必须对应 ivf_precise_bucket block payload。启用 precise_enable_read_cache 后, reader_io 会使用通用读缓存。

vsag::LoadParameters load_parameters;
load_parameters.Set("precise_io_type", "reader_io")
    .Set("precise_enable_read_cache", true)
    .Set("precise_cache_total_size", 256ULL * 1024 * 1024)
    .SetReader("precise_reader", precise_codes_reader);
auto loaded = vsag::Index::Load(stream, load_parameters).value();

reader_io 是加载与查询阶段的只读放置策略。构建时应使用可写的 precise IO,序列化后 再在查询服务加载索引时绑定外部 reader。

buckets_count 的经验值一般为 sqrt(N) ~ 4 * sqrt(N),其中 N 是语料规模。

检索参数

检索参数放在 ivf 子对象下:

参数类型默认值说明
scan_buckets_countint—(必填)每次查询扫描的桶数,须 ≤ buckets_countdisable_bucket_scan 为 true 时可更大,空槽位补 -1
disable_bucket_scanboolfalse返回桶 ID 及到桶中心距离,不扫描桶内向量。支持批量查询。
factorfloat2.0启用精排时,粗排阶段会预取 factor * topk 个候选再重打分
enable_reorderbooltrue即使索引构建时启用了 reorder,也可以在单次请求里设为 false 跳过最终精排
parallelismint1单次查询内扫描桶时使用的线程数
timeout_msdouble+∞单次查询最长耗时(毫秒),超时会返回当前的部分结果
auto result = index->KnnSearch(
    query, topk,
    R"({"ivf": {"scan_buckets_count": 32, "factor": 2.0, "parallelism": 4}})").value();
auto fast_result = index->KnnSearch(
    query, topk,
    R"({"ivf": {"scan_buckets_count": 32, "factor": 2.0, "enable_reorder": false}})").value();

何时选择 IVF

  • 超大规模语料(数亿及以上),工作集无法完全放入内存。
  • 对每秒查询数(QPS)敏感、对单次延迟相对宽松的批量或高吞吐场景。
  • 内存紧张的部署,可使用激进的量化方案(sq8sq4_uniformpqpqfs)配合 use_reorder 恢复召回。
  • 对切片友好的部署:桶天然映射到分片或磁盘块。

对于延迟敏感、要求高召回的稠密 embedding 场景,请优先比较 HGraph

相关文档

SINDI

SINDI(Sparse INverted Dense Index)是 VSAG 面向 稀疏向量 的索引—— 例如 BM25、SPLADE 以及其他学习稀疏(learned sparse)编码器产出的向量。与稠密索引 (HGraph、IVF)不同,SINDI 直接在“词项-权重”对上工作,是 VSAG 中接受 dtype: "sparse" 的索引之一。

工作原理

  1. 基于窗口的倒排表。 文档按固定窗口大小(window_size)分组,每个窗口独立维护一套 倒排表——即“词项 → (doc_id, value) 列表”的映射。
  2. 可选的剪枝与量化。 构建时可通过 doc_prune_ratio 按文档粒度丢弃权重最低的词项; use_quantization: true 使用 SQ8,use_quantization: "fp16" 使用半精度值。
  3. 打分。 检索时,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"

参数类型默认值说明
dimint—(必填)单条稀疏向量允许的最大非零项数量,不是 词表大小
term_id_limitint1000000词项 ID 的上界(应 ≥ 最大词项 ID + 1,最高 50 000 000)
window_sizeint50000每个窗口容纳的文档数(取值范围 10 000 – 60 000)
doc_prune_ratiofloat0.0构建阶段按文档丢弃权重最低词项的比例,取值范围为 [0.0, 1.0)
use_quantizationbool 或 stringfalsefalse 存 FP32,true 存 SQ8,"fp16" 存 FP16
use_reorderboolfalse是否保留一份正排存储,在 SINDI 粗排后对候选做精排
rerank_typestring"fp32"use_reorder 开启时使用的正排存储类型。fp32 保留精确值;dmq8 使用压缩的 8-bit DMQ 编码
dmq_shared_codebook_thresholdint1024rerank_type: "dmq8" 时,出现次数不超过该值的 term 共用一个 codebook;更高频的 term 保持独立 codebook。设为 0 可关闭共享
remap_term_idsboolfalse是否在建索引前重映射词项 ID,适用于词项 ID 很稀疏或存在大量空洞的词表
avg_doc_term_lengthint100仅用于内存估算
immutableboolfalse构建或加载紧凑的只读运行态;Build() 会逐窗口压缩以降低峰值内存,增量 Add() 会被拒绝

dimterm_id_limit 的区别。 对于稀疏向量 {0:0.1, 2:0.5, 177:0.8}dim3(三个非零项),而 term_id_limit 至少应为 178(最大词项 ID + 1)。 term_id_limit 要按词表大小估计,这是使用时最常见的坑。

稀疏值格式

use_quantization 保留原有 bool 行为,同时新增一个字符串取值:

取值存储格式取舍
falseFP32词项权重精度最高,posting payload 最大
trueSQ8权重存储最小;首次构建时学习量化范围
"fp16"FP16权重字节数为 FP32 的一半,不需要 SQ8 范围校准

使用 falsetrue 构建的索引仍保留旧版序列化表示。旧版本 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。它不支持增量 AddGetSparseVectorByInnerIdCalcDistanceByIdCalDistanceById。 mutable 和 immutable 运行态均支持 SerializeStreamingDeserializeStreamingIndex::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 分组,1UINT32_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_candidateint0候选堆大小。为 0 时自动取 SPARSE_AMPLIFICATION_FACTOR · topk(500 倍);若显式设置,须满足 1 ≤ n_candidate ≤ SPARSE_AMPLIFICATION_FACTOR · topk
filter_callback_limituint640单次带过滤检索最多调用用户 Filter::CheckValid 回调的次数;0 表示不限制。删除 ID 过滤器等内部检查不计数。达到正数上限时,在正常处理最后一次回调结果后停止候选及后续窗口扫描,并返回已经通过过滤的候选,因此结果可能不完整。该限制同时适用于 KNN、范围检索以及常规 API 和 SearchWithRequest
query_prune_ratiofloat0.0查询时丢弃权重最低查询项的比例,取值范围为 [0.0, 1.0)
term_prune_ratiofloat0.0每条倒排链中按 value 丢弃低权 posting 的比例,取值范围为 [0.0, 1.0)
term_retain_thresholduint640单个 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_typedmq8 时,码本由首次构建固定,因此模型建立后的增量 AddUpdateVector 不受支持。

实践建议

  • 中文数据集的稀疏向量,推荐使用 BGE-M3 编码;英文数据集更常见的默认选择是 SPLADE。
  • BGE-M3 同时支持 sparse 和 dense 输出。当前 SINDI 负责稀疏一路,VSAG 未来计划 支持稀疏与稠密融合打分检索。
  • 稀疏向量不能完全替代 BM25 全文检索。实践中,BM25 + 稀疏向量 + 稠密向量的三路 召回通常优于任意两路组合。
  • 在索引层面,SINDI 也可以承载 BM25 风格打分:查询侧用逆文档频率作为词项权重, 文档侧用词频等特征计算出的词项权重作为向量值即可。

常用配置

  1. 扁平暴力搜索索引。倒排索引保留全部非零项(doc_prune_ratio: 0.0),不保留正排索引 重排(use_reorder: false),不开启量化(use_quantization: false)。这是最直接的 高召回基线。
  2. 剪枝高精索引。构建时剪掉大部分低权重词项(doc_prune_ratio: 0.4),保留正排索引 用于重排(use_reorder: true),并开启量化减少倒排索引内存 (use_quantization: true)。这是常见的精度与内存折中配置。
  3. 压缩正排重排索引。在上一种配置基础上,设置 rerank_type: "dmq8",与 use_reorder: true 一起使用,以降低正排重排内存。
  4. 超大稀疏词表支持。对于词项 ID 在 uint32 范围内非常稀疏的场景,例如基于哈希的 分词器、外部词表 ID,或存在大量空白区间的词表,建议设置 remap_term_ids: true。 这样可以避免管理大量空倒排列表带来的内存浪费,也能降低触达 term_id_limit 上限的风险。
  5. 只读低内存构建。当构建峰值内存比构建速度更重要,且完成后不再增量写入时,增加 immutable: true,通常配合 use_quantization: "fp16"remap_term_ids: true 使用。

标记删除

SINDI 支持 RemoveMode::MARK_REMOVE。调用 Remove(ids)(默认模式)会为给定的 id 打上删除标记:它们不再出现在检索结果中,GetNumElements() 相应减少, GetNumberRemoved() 返回累计删除数量。删除不存在或已删除的 id 不会有任何效果。 RemoveMode::FORCE_REMOVE 不支持,调用会返回错误。

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

相关文档

SINDI_V2

SINDI_V2 是 VSAG 的 term-first 稀疏向量索引。它与 SINDI 使用相同的窗口化倒排检索模型和 1 - inner_product 距离定义,但按 term 保存 posting,因此搜索时只需读取查询实际使用的倒排链。

  • 工厂入口:sindi_v2
  • 源码:src/algorithm/sindi_v2/
  • dtype 必须为:sparse
  • metric_type 必须为:ip

存储布局

每个 term payload 只保存非空 window:

non_empty_window_count
ordered {window_id, posting_count}[]
local_doc_ids[]
alignment padding
encoded_values[]

查询时,SINDI_V2 将稀疏 metadata 展开为 window_count + 1 个 window offset, 并通过 reader_iommap_iobuffer_ioasync_io 或内存后端,只加载 查询保留 term 对应的 posting。

这与 SINDI 的 window-first 序列化不同。两个工厂入口使用不同的持久化格式, 加载索引时不能互换。

快速开始

#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.1,
        "use_quantization": true,
        "use_reorder": true,
        "remap_term_ids": true,
        "term_io": {
            "type": "buffer_io",
            "file_path": "/tmp/sindi_v2.terms"
        },
        "rerank_io": {
            "type": "block_memory_io"
        }
    }
})";
auto index = vsag::Factory::CreateIndex("sindi_v2", params).value();
index->Build(base);

auto result = index->KnnSearch(
    query, 10,
    R"({"sindi_v2": {
        "n_candidate": 100,
        "query_prune_ratio": 0.1,
        "term_prune_ratio": 0.2,
        "term_retain_threshold": 10000
    }})").value();

构建参数

构建参数放在 index_param 下。

参数类型默认值说明
dimint必填单条稀疏向量允许的最大非零项数量,不是词表大小。
term_id_limitint1000000term ID 上界,最高为 50 000 000。
window_sizeint50000每个 window 的文档数,范围为 10 000 到 60 000。
doc_prune_ratiofloat0.0构建时丢弃最低权重文档 term 的比例,范围为 [0.0, 1.0)
use_quantizationbool 或 stringfalsefalse 存 FP32,true 存 SQ8,"fp16" 存 FP16。
use_reorderboolfalse保存高精度向量并对粗排候选重排。
rerank_typestring"fp32"重排存储类型:fp32dmq8
dmq_shared_codebook_thresholdint1024低频 term 使用共享 DMQ codebook 的阈值。
remap_term_idsboolfalse压缩稀疏或间隔很大的外部 term ID。
avg_doc_term_lengthint100仅用于内存估算。
immutableboolfalse选择紧凑的只读内存 term DataCell。
term_ioobject{"type": "reader_io"}加载后保存 term-first posting 的后端。
rerank_ioobject{"type": "block_memory_io"}保存重排向量的后端。
rerank_layoutuint320top-terms-signature 布局使用的前导 term 数量;0 表示关闭。

文件型 term_io 支持 mmap_iobuffer_ioasync_io,并要求设置 file_path。文件型 rerank_io 未设置 file_path 时,VSAG 使用 <term_io.file_path>.rerank

rerank_layout > 0 要求 use_reorder: truererank_type: "dmq8" 要求 rerank_layout: 0,并使用默认的 block_memory_io 重排后端。

Host 过滤

SINDI_V2 支持与 SINDI 相同的 uint32_t host_id 构建数据和 KNN 查询 metadata 约定。mutable 与 immutable 索引均支持该功能。两者共用 host 分组与路由组件,同时 SINDI_V2 保持自己的 term-first posting 存储。host 成员检查统一在 posting-window 检索中执行, 并支持 mutable Add() 产生的多个区间。缺失 host ID 0、mutable Add() metadata、streaming 序列化及仅支持 KNN 的规则均与 SINDI 相同。

检索参数

检索参数放在 {"sindi_v2": {...}} 下。

参数类型默认值说明
n_candidateint0粗排候选数量;0 使用根据 topk 推导的索引默认值。
query_prune_ratiofloat0.0跳过最低权重查询 term 的比例,范围为 [0.0, 1.0)
term_prune_ratiofloat0.0每条 term list 中跳过最低存储权重的比例,范围为 [0.0, 1.0)
term_retain_thresholduint640单个 term 在所有 window 中最多扫描的 posting 总数;0 表示关闭,正数使每个非空 window posting list 最多扫描 max(1, floor(threshold / window_count)) 条。

合并 ratio 与 threshold 限制后,每条非空 term list 至少扫描一个 posting。

SINDI V2 会根据构建阶段的 doc_prune_ratio 与检索阶段的 query_prune_ratio 自动选择堆插入策略。按当前 0.1 阈值,当两个比例都 <= 0.1 时,使用基于距离数组的 入堆路径;只要任一比例大于 0.1,就使用基于 term-list 的入堆路径。

如何选择 SINDI 与 SINDI_V2

完整稀疏索引常驻内存、且 window-first 序列化合适时选择 SINDI。需要按 term 组织持久化 posting,或希望搜索时只从外部存储读取查询 term list 时选择 SINDI_V2。

SIMQ

SIMQ 是 VSAG 面向 多向量(multi-vector) 检索的索引——适用于每篇文档 由一组 token 级向量(而非单个 embedding)表示的数据场景。这种模式常见于 ColBERT 等 late-interaction 模型,其中文档由每个 token 对应一个向量表示, 相关性通过 MaxSim(各查询 token 的最大相似度之和)计算。

  • 源码:src/algorithm/simq/

工作原理

  1. Token 向量动态聚类。 构建阶段,所有文档的全部 token 向量被抽取到一个 扁平池中,使用基于 HGraph 的动态聚类算法进行聚类。初始聚类中心按 init_cluster_ratio 控制的比例采样;超过 max_cluster_size 的簇会被增量切分。
  2. 代表性图用于粗排。 在簇中心上构建一个代表性 HGraph。检索时,每个查询 token 在该图上搜索最近的若干个簇(由 coarse_k 控制),跨查询 token 累加簇得分,得到候选集。
  3. 精确 MaxSim 精排。 对得分最高的 rerank_k 个候选文档,从磁盘(或内存) 读回原始 token 向量,计算查询 token 与文档 token 之间的精确 MaxSim 相似度。

聚类粗排与精确精排的两阶段结合, 为多向量检索提供了可调的召回率/延迟权衡。

快速开始

#include <vsag/vsag.h>

std::string build_params = R"({
    "dtype": "float32",
    "metric_type": "ip",
    "dim": 256,
    "index_param": {
        "base_io_type": "async_io",
        "base_file_path": "/path/to/simq_base_codes.bin",
        "init_cluster_ratio": 0.1,
        "max_cluster_size": 160,
        "split_start_idx": 80,
        "random_seed": 42,
        "coarse_k": 50,
        "rerank_k": 1000
    }
})";
auto index = vsag::Factory::CreateIndex("simq", build_params).value();

// 使用 MultiVector 构建数据集。
// 每篇文档包含可变数量的 token 向量,每个向量维度为 dim。
std::vector<vsag::MultiVector> base_mvs(num_docs);
std::vector<int64_t> ids(num_docs);
for (int64_t i = 0; i < num_docs; ++i) {
    base_mvs[i].len_ = doc_token_counts[i];             // 文档 i 的 token 数量
    base_mvs[i].vectors_ = doc_token_vectors[i];        // 扁平数组:len_ * dim 个 float
    ids[i] = i;
}
auto base = vsag::Dataset::Make();
base->NumElements(num_docs)
    ->Dim(dim)
    ->Ids(ids.data())
    ->MultiVectors(base_mvs.data())
    ->MultiVectorDim(dim)
    ->Owner(false);
index->Build(base);

// 使用多向量查询进行检索。
vsag::MultiVector query_mv;
query_mv.len_ = query_token_count;
query_mv.vectors_ = query_token_vectors;
auto query = vsag::Dataset::Make();
query->NumElements(1)
    ->Dim(dim)
    ->MultiVectors(&query_mv)
    ->MultiVectorDim(dim)
    ->Owner(false);

std::string search_params = R"({
    "simq": {
        "coarse_k": 600,
        "rerank_k": 5000
    }
})";
auto result = index->KnnSearch(query, /*topk=*/100, search_params).value();

// 读取结果。
const int64_t* result_ids = result->GetIds();
const float* result_dists = result->GetDistances();
int64_t result_count = result->GetDim();
for (int64_t i = 0; i < result_count; ++i) {
    int64_t id = result_ids[i];
    float dist = result_dists[i];
}

构建参数

SIMQ 专属构建参数放在 index_param 下。dimdtypemetric_type 是顶层字段。dtype 必须"float32"metric_type 必须"ip"

参数类型默认值说明
dimint—(必填)每个 token 向量的维度
base_io_typestring"async_io"精排阶段使用的原始多向量数据存储后端
base_file_pathstring"./default_file_path"磁盘 IO 类型使用的文件路径
init_cluster_ratiofloat0.2初始聚类中心的 token 向量采样比例
max_cluster_sizeint64单个簇允许的最大 token 向量数
split_start_idxint32簇切分时新簇的起始位置
random_seedint42聚类打乱的随机种子
coarse_kint8构建时每个查询 token 搜索的最近簇数量
rerank_kint100构建时进入精排的候选文档数量上限
  • dim — 所有文档和查询中的所有 token 共享同一维度
  • base_io_type — 可选值:async_iouring_iomemory_ioblock_memory_iobuffer_iommap_ioreader_io。原生 uring_io 需要以 liburing 构建,否则回退到 buffer_io
  • base_file_path — 默认值为占位符,使用磁盘类型(async_iouring_iobuffer_iommap_io)时需提供真实路径
  • init_cluster_ratio — 取值范围 (0, 1]。值越小簇越少越大, 值越大簇越多越细
  • max_cluster_size — 必须 > 1
  • split_start_idx — 通常设为 max_cluster_size 的一半, 取值范围 (1, max_cluster_size)
  • coarse_krerank_k — 必须 > 0

聚类参数的选择。 init_cluster_ratiomax_cluster_size 共同控制 簇的数量与大小。较小的 init_cluster_ratio 搭配较大的 max_cluster_size 会产生更少的簇,粗排更快但召回降低。建议以 init_cluster_ratio = 0.10.2max_cluster_size = 2 × split_start_idx 为起点,再通过检索参数调优。

检索参数

检索参数放在 simq 子对象下:

参数类型默认值说明
coarse_kint(构建时默认值)每个查询 token 搜索的最近簇数量
rerank_kint(构建时默认值)进入精排的候选文档数量上限
  • coarse_k — 覆盖构建时的值。值越大候选范围越广, 召回越高但延迟也越大
  • rerank_k — 覆盖构建时的值。值越大召回越高, 但磁盘读取和计算开销也越大
  • 不设置时使用构建时的默认值。显式设置时两个值都必须 > 0
auto result = index->KnnSearch(
    query, topk,
    R"({"simq": {"coarse_k": 600, "rerank_k": 5000}})").value();

搜索统计

KnnSearchRangeSearch 会在返回的 dataset 上附带 JSON 统计对象。可以读取完整对象, 也可以按 key 获取单项值:

std::cout << result->GetStatistics() << '\n';
auto values = result->GetStatistics(
    {"simq_coarse_candidate_count", "simq_rerank_candidate_count"});
Key含义
dist_cmp精排阶段执行的精确 MaxSim 距离计算次数
simq_coarse_dist_cmp代表图搜索执行的距离计算次数
simq_coarse_probe_count所有 query token 探测的代表簇数量
simq_coarse_candidate_count粗排产生且尚未应用 rerank_k 的唯一文档候选数
simq_rerank_candidate_count应用 rerank_k 后保留的候选数
simq_filtered_candidate_count被调用方 filter 排除的精排候选数
simq_result_count精排并应用 range/top-k 限制后最终返回的结果数
simq_limited_size_appliedRangeSearch 是否因 limited_size 截断结果

这些计数器可以在调节召回和延迟时,区分候选池过窄、过滤、精排和输出上限各自的影响。

何时选择 SIMQ

  • Late-interaction 检索:使用 ColBERT 等模型,每篇文档是一组 token 向量, 相关性通过 MaxSim 计算。
  • 多向量粒度匹配:单 embedding 丢失过多信息,需要 token 级细粒度匹配。
  • 大规模多向量语料:暴力 MaxSim 检索过慢,需要粗排 + 精排的两阶段管线 来平衡召回与延迟。

SIMQ 仅接受 float32 多向量数据,仅支持内积相似度。 不支持 单稠密向量或稀疏向量(请使用 HGraph 或 SINDI)。

实践建议

  • 调整 coarse_krerank_k 增大 coarse_k 扩大簇级候选范围; 增大 rerank_k 让更多文档进入精确打分。实践中 rerank_k 对召回的影响 更大,但每个额外候选都需要一次磁盘读取和完整的 MaxSim 计算,延迟也会增加。
  • IO 类型选择。 语料规模超出内存时使用 async_io;多向量数据可以放入 内存时,使用 memory_ioblock_memory_io 获得最低的精排延迟。
  • 簇大小配置。max_cluster_size 设为 split_start_idx 的约两倍。切分位置 决定了簇溢出时 token 向量的分配方式,居中设置可使两半保持平衡。

MultiVector 字段说明

字段类型说明
len_uint32_t当前文档或查询包含的 token 向量数量
vectors_float*len_ * dim 个 float 的连续数组

相关文档

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 不支持,调用会返回错误。

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

相关文档

BruteForce

BruteForce 是 VSAG 提供的精确扁平索引。查询时直接对语料中的每条向量计算距离并返回真实的 top-k —— 没有图遍历、没有倒排表、不做近似。它的主要用途是为 HGraph、IVF 等近似索引提供 ground truth 基准,也适合用于小规模语料或对召回率有严格要求的生产场景。

工作原理

  1. Build。 向量按照 base_quantization_type(默认 fp32,即原始精度)编码后保存到一个 扁平数据单元中。对于不压缩的量化器,不需要训练;当使用 PQ/SQ_uniform 等需要训练的量化器 时,Build 会先跑一遍训练。
  2. Add。 新向量直接追加到扁平存储中,没有再平衡或重建成本。
  3. Search。 针对每条查询,按照配置的 metric_typel2ipcosine)逐条计算 距离,再用 top-k 小顶堆得到最近邻 id。距离计算使用 SIMD 内核,并支持单查询内并行: 通过 parallelism 搜索参数可以把同一条查询的扫描拆分到多个线程上(实现见 BruteForce::SearchWithRequestsrc/algorithm/brute_force.cpp)。

由于索引保留了每一条向量(除非选择了有损量化器),当 base_quantization_type = fp32 时 结果是完全精确的,因此 eval_performance 工具默认用 BruteForce 作为生成 ground truth 的 参考索引。

快速开始

#include <vsag/vsag.h>

std::string params = R"({
    "dtype": "float32",
    "metric_type": "l2",
    "dim": 128
})";
auto index = vsag::Factory::CreateIndex("brute_force", params).value();

// 构建。
auto base = vsag::Dataset::Make();
base->NumElements(n)->Dim(128)->Ids(ids)->Float32Vectors(data)->Owner(false);
index->Build(base);

// 搜索 —— 没有索引特有的旋钮,传空 JSON 即可(也可以设置 `parallelism`)。
auto query = vsag::Dataset::Make();
query->NumElements(1)->Dim(128)->Float32Vectors(q)->Owner(false);
auto result = index->KnnSearch(query, /*topk=*/10, "{}").value();

完整可运行示例见 examples/cpp/105_index_brute_force.cpp

支持的输入数据类型

当前公开的 BuildAddKnnSearchRangeSearchUpdateVector 路径仅接受 FP32 向量。运行时通过 Dataset::Float32Vectors 传入向量;创建索引时 dtype 应设为 "float32",不支持 dtype: "int8"

base_quantization_type 描述的是索引内部编码和存储,而非输入类型。选择内部 fp16bf16 等量化器不会使 API 接受 FP16/BF16 输入。

构建参数

最简配置只需要三个顶层字段(dtypemetric_typedim)。大多数场景下不需要 index_param,这也是 示例 105 所采用的形式。进阶用法可通过 index_param 启用量化或存储相关的开关:

参数类型默认值说明
base_quantization_typestring"fp32"fp32fp16bf16sq8sq4sq8_uniformsq4_uniformpqpqfsrabitq —— 各量化器细节见量化章节
use_attribute_filterboolfalse启用属性过滤(参见 属性过滤
resize_increase_count_bitint10扩容批次 slot 数的 log2,取值范围为 1311 表示每次按 2 个 slot 对齐,10 表示按 1024 个 slot 对齐。较小取值减少预分配,但可能增加重分配次数。

关于 store_raw_vector 的说明。 store_raw_vector 字段会被共用的 InnerIndexParameter 解析,但 BruteForce 不会根据它决定是否启用 GetRawVectorByIds。在 BruteForce 上,原始向量读取能力仅在 base_quantization_type = fp32、并且度量不是 cosine 或量化器配置了 持有向量范数(hold_molds)时开启。在 BruteForce 上设置 store_raw_vector: true 目前不会改变任何能力标志 —— 如果需要量化索引同时 支持 GetRawVectorByIds,请使用 HGraph 或 IVF。

下面是一个使用 sq8 量化以节省内存、同时保持线性扫描语义的示例:

{
    "dtype": "float32",
    "metric_type": "ip",
    "dim": 128,
    "index_param": {
        "base_quantization_type": "sq8"
    }
}

base_quantization_type 选择了需要训练的量化器(sq8sq4sq8_uniformsq4_uniformpqpqfsrabitq)时,Build 会先用传入的数据集训练量化器,此时召回率不再保证 100%。只有 fp32fp16bf16 不需要训练,能保持精确距离(仅受浮点数值精度影响)。

搜索参数

BruteForce 没有任何索引特有的搜索旋钮(不存在 efnprobe 之类的参数),但通用的 IndexSearchParameter 字段都生效:

参数类型默认值说明
parallelismint1把单条查询的线性扫描拆分到索引内部线程池中的若干线程上。该参数同时作用于 KnnSearchRangeSearch。该值越大,大语料下的单查询延迟越低,代价是占用更多 CPU 核。<= 0 的取值会被钳制到 1
// 默认单线程扫描。
auto r1 = index->KnnSearch(query, topk, "{}").value();

// 用 8 个线程并行扫描同一条查询。
auto r2 = index->KnnSearch(query, topk, R"({"parallelism": 8})").value();

// RangeSearch 使用同一个 parallelism 参数。
auto r3 = index->RangeSearch(query, radius, R"({"parallelism": 8})").value();

范围查询语义参见 范围搜索

删除向量

BruteForce 同时支持 RemoveMode::MARK_REMOVERemoveMode::FORCE_REMOVE;两种模式都不需要 配置 HGraph 专用的 support_force_remove

启用 use_attribute_filter: true 时,BruteForce 不支持任一种删除模式;如需删除属性过滤数据,请重建索引。

  • MARK_REMOVE 是默认模式。它只写入墓碑标记,后续搜索会过滤该 id,但向量存储仍保持已分配状态。 GetNumElements() 不计已标记的 id,GetNumberRemoved() 返回其数量。
  • FORCE_REMOVE 会物理删除请求的每条向量。必要时它用最后一个内部槽位覆盖被删除槽位,并保留被移动 向量的外部 id。成功的物理删除不会新增墓碑;之前已标记删除的数量仍由 GetNumberRemoved() 反映。
index->Remove(std::vector<int64_t>{id1, id2}, vsag::RemoveMode::MARK_REMOVE);
index->Remove(std::vector<int64_t>{id3, id4}, vsag::RemoveMode::FORCE_REMOVE);

物理删除是同步操作,开销高于标记删除。它会获取索引的全局独占锁,因此会等待进行中的搜索,并在 槽位交换与删除完成前阻止新的搜索。应尽可能批量传入 id,避免在延迟敏感路径上执行物理删除。

BruteForce 默认使用 block_memory_io 存储向量。每个成功的物理删除批次之后,逻辑大小会收缩到 存活条目数,并释放末尾完整的内存块;最后一个未填满的块会保留。可选额外信息存储也遵循相同的块粒度 回收行为。为避免反复复制整张标签表,标签表会在存活条目数不超过当前容量一半时回收其底层分配。 之后 GetMemoryUsage() 会反映紧凑后的索引。它衡量的是索引分配量,不保证进程分配器会立即把内存归还 操作系统,也不保证 RSS 立即下降。

索引能力

BruteForce 声明的能力标志如下(参见 BruteForce::InitFeaturessrc/algorithm/brute_force.cpp):

能力说明
SUPPORT_BUILD / SUPPORT_ADD_AFTER_BUILD / SUPPORT_ADD_CONCURRENT支持一次构建、增量追加,以及并发插入。
SUPPORT_ADD_FROM_EMPTY仅在非训练型量化器(fp32fp16bf16)下可用。
SUPPORT_KNN_SEARCH / SUPPORT_KNN_SEARCH_WITH_ID_FILTER / SUPPORT_SEARCH_CONCURRENT标准 top-k 查询、id 列表过滤,以及并发搜索。
SUPPORT_RANGE_SEARCH / SUPPORT_RANGE_SEARCH_WITH_ID_FILTER仅在非训练型量化器(fp32fp16bf16)下可用。
SUPPORT_DELETE_BY_ID / SUPPORT_DELETE_CONCURRENT支持按 id 删除。查询和删除操作会同步;FORCE_REMOVE 会获取独占锁。
SUPPORT_CAL_DISTANCE_BY_ID与已存储向量计算距离(仅非训练型量化器)。
SUPPORT_UPDATE_VECTOR_CONCURRENT支持 UpdateVector:以维度相同的 FP32 向量替换已有向量。BruteForce 没有图连通性检查,因此 force_update 不改变更新行为。
SUPPORT_GET_RAW_VECTOR_BY_IDS仅当 base_quantization_type = fp32,且度量不是 cosine 或底层量化器持有向量范数(hold_molds)时才声明。量化的 BruteForce 索引不会声明该能力。
SUPPORT_CHECK_ID_EXIST / SUPPORT_CLONE / SUPPORT_ESTIMATE_MEMORY / SUPPORT_GET_MEMORY_USAGE标准的内省与生命周期接口。
SUPPORT_SERIALIZE_BINARY_SET / SUPPORT_SERIALIZE_FILE / SUPPORT_SERIALIZE_WRITE_FUNC完整的保存能力。
SUPPORT_DESERIALIZE_BINARY_SET / SUPPORT_DESERIALIZE_FILE / SUPPORT_DESERIALIZE_READER_SET完整的加载能力。(没有对应的 DESERIALIZE_WRITE_FUNC,读路径使用 READER_SET 形式。)
NEED_TRAINbase_quantization_typesq8sq4sq8_uniformsq4_uniformpqpqfsrabitq 之一时声明。

BruteForce 不支持 的能力包括:SUPPORT_UPDATE_ID_CONCURRENTSUPPORT_EXPORT_MODEL

适用场景

  • 召回基准。 为近似索引计算 ground truth(eval_performance 工具就是这么做的)。
  • 小规模语料。 几百到几十万条向量,全量扫描成本可接受,且无需做参数调优。
  • 强召回需求。 完全不能容忍近似误差的业务。
  • 小规模量化实验。 在同一条线性扫描流水线上对比不同 base_quantization_type 的效果, 排除图结构 / 倒排表带来的干扰。

如果数据规模更大,请优先选择 HGraph(延迟敏感、高召回)或 IVF(吞吐量优先、内存友好)。

参考

量化

向量量化是 VSAG 中权衡内存与召回的核心手段。每种索引类型都通过一个 基础量化器(由 base_quantization_type 配置)存储向量,并可以额外保留 一个精确量化器用于重排(precise_quantization_type + use_reorder: true)。本章介绍每一种受支持的量化器:它做什么、接受哪些 JSON 参数、是否 需要训练、支持哪些度量、以及何时选用它。

存储与搜索流水线

                  +---------------------+
   原始向量 ----->|  可选变换           |   (TQ 链:pca / rom / fht / mrle)
                  +----------+----------+
                             |
                             v
                  +---------------------+
                  |   基础量化器        |   fp32 / fp16 / bf16 /
                  |                     |   sq8 / sq4 / sq8_uniform /
                  |                     |   sq4_uniform / pq / pqfs /
                  |                     |   rabitq
                  +----------+----------+
                             |
                             v
                   +-------------------+
                   |   索引存储        |   (HGraph / IVF / Pyramid /
                   |                   |    BruteForce / SINDI)
                   +---------+---------+
                             |
                             v
                    图 / 倒排链路游走
                             |
             +---------------+-----------------+
             |                                 |
    use_reorder: false                use_reorder: true
             |                                 |
             v                                 v
        top-K 结果                  +---------------------+
                                    | 精确量化器          |  重排
                                    | (默认 fp32;        |
                                    |  fp16/bf16/sq8 可)  |
                                    +----------+----------+
                                               |
                                               v
                                          top-K 结果

use_reorderprecise_quantization_type 并非某一具体量化器专属——只要 索引支持重排,它们就生效(见 HGraphIVFPyramid)。

支持的量化器一览

src/datacell/flatten_interface.cpp 的工厂会根据 JSON 中的 type 字段分派到具体量化器。

base_quantization_type每维位数(约)需要训练是否无损典型场景
fp3232参考基线 / 精确重排存储
fp1616近似无损半精度存储;高维 float 向量的良好默认
bf1616近似无损fp16 同样大小,动态范围更宽
sq88通用的省内存基线
sq44激进压缩,不重排时召回会下降
sq8_uniform8全局 min/max,SIMD 友好的 SQ8
sq4_uniform4SIMD 友好的 SQ4;支持 sq4_uniform_trunc_rate
pq~pq_bits × pq_dim / dim基于码本,非常紧凑
pqfs4 × pq_dim / dimPQ FastScan——SIMD 加速版 PQ
rabitq1 或 HGraph x+y1 比特 / 低比特 split 二值量化,最强压缩
tq取决于链路取决于末端量化器量化变换:在另一个量化器之前串接旋转 / PCA

int8sparse 不作为通用的 base_quantization_type 暴露:

  • int8 在使用 dtype: "int8" 时被自动选用,并非一种压缩模式。
  • sparseSINDI 的倒排链表服务,密集索引不可 直接选择。

训练需求

上表中标记为的量化器实现了 NEED_TRAIN 标志,必须先调用 Build (在输入向量上内部完成训练)或显式 Train 之后再 Add。完整生命周期见 索引构建与训练

对 HGraph 而言,训练数据就是传给 Build 的基础向量;对 IVF 而言,先训练 聚类中心,再把残差喂给所配置的基础量化器。

度量兼容性

本章所列量化器全部支持三种稠密度量(l2 / ip / cosine)。对 cosine, 索引会在量化前对向量做归一化,因此底层量化器看不到原始模长。一些实践 要点:

  • pq / pqfs 在每个子空间上做距离查表;当 pq_dim 非常小(≤ 4), 在 ip / cosine 上比 l2 更容易受各向异性影响。
  • rabitq 在输入向量去相关的情况下效果最好——要么开启 rabitq_use_fht / rabitq_pca_dim,要么用 tq 链路如 "pca, rom, rabitq" 包一层。

量化器选择

一份实用的决策树:

  1. 需要精确距离或精确重排存储?fp32
  2. 只想内存减半且召回基本无损?fp16(若数据动态范围大,例如未 归一化的嵌入,则用 bf16)。
  3. 想要约 4× 的内存节省并愿意启用重排?sq8(在 l2 / ip 上 想要更高 SIMD 吞吐可用 sq8_uniform)。
  4. 内存紧张、可在重排前承受更大召回损失?sq4_uniform
  5. 高维向量,希望基于码本做强压缩?pq,平台支持 SIMD 路径时用 pqfs
  6. 追求最强压缩(1 比特)并能承担重排代价?rabitq,最好搭配 rabitq_use_fht: truetq 链路。

对上述任何一种有损量化器,将 use_reorder: true 配合 precise_quantization_type: "fp32" 是恢复召回的标准做法,代价是额外内存; 具体行为参考 HGraph 参数表

量化在何处暴露

并非每种索引都将所有参数都暴露为外部 key。当前情况:

  • HGraph 暴露最完整的集合:base_quantization_typeprecise_quantization_typeuse_reorderbase_pq_dimrabitq_pca_dimrabitq_bits_per_dim_queryrabitq_bits_per_dim_baserabitq_bits_per_dim_preciserabitq_error_raterabitq_use_fhtsq4_uniform_trunc_ratetq_chain (见 src/algorithm/hgraph.cpp)。
  • IVF 暴露 base_quantization_typebase_pq_dim、通用重排相关 key, 以及 rabitq_pca_dimrabitq_bits_per_dim_queryrabitq_bits_per_dim_baserabitq_versionrabitq_error_raterabitq_use_fht 这些 RabitQ 调参 key。
  • Pyramid 暴露 base_quantization_typebase_pq_dim、通用重排相关 key, 以及 RabitQ 的 PCA、底库/查询位数和 FHT 相关 key。
  • BruteForce 暴露 base_quantization_type 与通用重排相关 key;部分可调项 (如 tq_chain)目前在内部接好但未作为外部 key 暴露。

每种索引的完整参数列表见对应索引页。

本章内容

FP32(基线)

fp32 把每个坐标按 32 位 IEEE-754 浮点存储——与输入向量布局一致。它是 VSAG 中唯一完全无损的选项,作为所有其他量化器对比的参考基线。

实现:src/quantization/fp32_quantizer.cpp,参数文件 fp32_quantizer_parameter.cpp

何时使用

  • 重排 / 精确存储。use_reorder: true 时, precise_quantization_type: "fp32" 是默认的精确存储;图上游走使用便宜 的基础量化器,对 top-K 候选再用 fp32 精确重打分。
  • 参考 / 基准真值。base_quantization_type: "fp32" 构建索引能拿 到该索引类型可达的最高召回,是其他量化器对比的标准基线 (docs/docs/en/src/resources/eval.md)。
  • 小规模数据集,内存不是瓶颈时。
  • BruteForce 原始向量取回。 仅当 base_quantization_typefp32 且度量允许时,SUPPORT_GET_RAW_VECTOR_BY_IDS 才会被广播 (src/index/brute_force.cpp)。

内存代价

仅码本身的开销为每向量 4 × dim 字节。当 fp32 作为某个基础量化器之上的 精确存储时,每向量代价为 base 码 + 4 × dim

参数

fp32 没有量化器专属的 JSON 参数。

{
    "dtype": "float32",
    "metric_type": "l2",
    "dim": 128,
    "index_param": {
        "base_quantization_type": "fp32",
        "max_degree": 32,
        "ef_construction": 300
    }
}

训练

不需要。fp32 设置 NEED_TRAIN

度量兼容性

l2ipcosine——全部支持,无特殊处理。

相关页面

半精度浮点(FP16 / BF16)

fp16bf16 把每个坐标用 16 位而非 32 位存储,把码内存减半且近似 无损。它们没有量化器专属的 JSON 参数;二者的唯一差异是浮点格式自身的 位布局。

实现:src/quantization/scalar_quantization/half_precision_quantizer.cpp, 类型特征在 half_precision_traits.h。 可运行示例:examples/cpp/321_index_fp16_hgraph.cpp

FP16 与 BF16 一览

格式符号位指数位尾数位有效范围精度
fp161510~±6.55e4约 3 位十进制
bf16187fp32 相同(~±3.4e38)约 2 位十进制

实践含义:

  • fp16 保留更多尾数位——对取值大致在 [-1, 1] 的归一化嵌入精度更 好。是 cosine 归一化向量的标准选择。
  • bf16 保留与 fp32 一致的指数范围——对原始、未归一化的特征(如 加权和、累加器式嵌入)更安全。相对 fp16,在接近零的取值上损失一些 精度。

不确定时:归一化嵌入选 fp16,未归一化或范围较宽的数据选 bf16

何时使用

  • fp32 基线之上作为“即插即用“的内存优化。在标准基准(SIFT、GIST、 Glove、句向量)上召回损失通常低于 1%。
  • 作为精确重排存储,体积仅为 fp32 的一半: precise_quantization_type: "fp16""bf16" 配合 use_reorder: true
  • 高维浮点向量,32 位存储成为瓶颈时。

内存代价

仅码本身每向量 2 × dim 字节。

参数

fp16bf16 均没有量化器专属 JSON 参数。

{
    "dtype": "float32",
    "metric_type": "l2",
    "dim": 768,
    "index_param": {
        "base_quantization_type": "fp16",
        "max_degree": 32,
        "ef_construction": 300
    }
}

"fp16" 替换为 "bf16" 即可切换格式。输入 dtype 仍是 "float32": 量化器会在运行时转换。

训练

不需要。fp16bf16 均不设置 NEED_TRAIN

度量兼容性

l2ipcosine——全部支持。cosine 通过先归一化输入再以 16 位精度 存储实现。

何时不要使用

  • 当你还需要一个内存更激进的基础量化器(如 sq8pq)——它们已经 把存储压到远低于 2 字节/维。
  • 当你需要精确距离(用 fp32)。

相关页面

标量量化(SQ4 / SQ8)

sq8sq4逐维标量量化器:每个坐标按训练得到的逐维 [min, max] 范围,从 float32 映射到 8 位(sq8)或 4 位(sq4)整数。它们共享同 一份实现,仅以位宽参数化,位于 src/quantization/scalar_quantization/scalar_quantizer.cppscalar_quantizer_parameter.h

如果想要 SIMD 更友好、使用全局 [min, max] 的变种,见 Uniform 标量量化

SQ4 与 SQ8 一览

类型每维位数相对 fp32 内存典型精度备注
sq88~1/4轻微召回下降通用省内存基线
sq44~1/8不重排时下降明显激进压缩;配合 use_reorder: true

训练得到的是逐维 min/max,重尾分布的坐标可能浪费码位。如果数据各 向异性强,可考虑改用 Uniform 标量量化 或先旋转的 量化变换 链路,例如 "rom, sq8_uniform"

内存代价(仅码)

  • sq8:每向量 dim 字节。
  • sq4:每向量 ceil(dim / 2) 字节。

此外还有一份小型逐维范围表(8 × dim 字节,所有向量摊销)。

参数

目前 sq8sq4 均无量化器专属 JSON 参数 (scalar_quantizer_parameter.h:36-58)。位宽仅由 type 字符串决定。

{
    "dtype": "float32",
    "metric_type": "l2",
    "dim": 128,
    "index_param": {
        "base_quantization_type": "sq8",
        "max_degree": 32,
        "ef_construction": 300,
        "use_reorder": true,
        "precise_quantization_type": "fp32"
    }
}

"sq8" 替换为 "sq4" 即得到 4 位码。

训练

设置了 NEED_TRAIN。训练从输入向量样本中收集逐维 min / maxBuild(base) 会内部完成训练;对需要显式 Train 的索引(如部分 IVF 流程), 请在 Add 之前调用。详见 索引构建与训练

度量兼容性

l2ipcosine——全部支持。距离通过把整数码解码回逐维缩放浮点后 计算。

sq8sq4 如何选

  • sq8:图索引(HGraph、Pyramid)追求约 4× 内存压缩时的默认选择。 召回损失通常很小,use_reorder 经常可选,但搭配 precise_quantization_type: "fp32" 启用重排是最稳妥的配置。
  • sq4:内存紧张且能承担精确重排存储时选用。几乎总是要配合 use_reorder: true
  • 如果数据大致维度同质,改选 sq*_uniform;uniform 变种具有更高的 SIMD 吞吐。
  • 对重尾 / 各向异性数据,更推荐前置旋转的 量化变换 链路。

相关页面

标量量化 Uniform(Scalar Quantization Uniform:SQ4 / SQ8 Uniform)

sq8_uniformsq4_uniformsq8 / sq4 类似,是标量量化 器,但它们学习的是全局唯一[min, max] 范围,对所有维度都使用同一 份缩放参数。这一权衡——逐维自适应能力略弱,但解码路径更简单——换来了 显著更快的 SIMD 距离计算(l2ip),并保持更紧凑的码布局。

实现:src/quantization/scalar_quantization/sq8_uniform_quantizer.cppsrc/quantization/scalar_quantization/sq4_uniform_quantizer.cpp

为什么快:距离计算停留在整数域

这是在条件允许时优先选用 sq*_uniform 而非 sq* 的核心原因。由于每个 维度共享同一对 (min, max),仿射解码 x = min + code · (max - min) / (2^b - 1)所有坐标都使用相同的 scale 与 offset。这在热路径上带来三点收益:

  • query 用同一份全局 (min, max) 只编码一次,存入 uint8(或打包的 半字节)缓冲,见 ProcessQueryImplsrc/quantization/scalar_quantization/sq8_uniform_quantizer.cpp:179)。
  • base 向量的 code 从不解码回 fp32。kernel SQ8UniformComputeCodesIP(uint8_t* q, uint8_t* x, dim) / SQ4UniformComputeCodesIP(...) 把两个操作数都按原始整数 code 读入, 在 uint8 / 打包半字节通道上直接用 AVX-512 / AMX(ARM 上为 NEON)做 点积,一次处理一个 cache line。内层循环里没有任何逐元素的 fp 反量化。
  • 共享的 scale 与 offset 在整数累加完成之后对每对向量只补偿一次, 即可还原 fp 距离。某些度量需要的额外项(每向量的 norm 或 sum)也在 循环外加上,参见 sq8_uniform_quantizer.cpp:200 的 trailing metadata 说明以及 SQ8UniformComputeCodesIPBatch 批量 kernel。

而在逐维的 sq* 量化器里,每个坐标都有自己的 (min_i, max_i),kernel 要么在循环内乘以逐维 scale 表,要么先把至少一边的操作数解码回 fp。 省掉这一步,就是 uniform 变种在同等召回下显著更快的根本原因。

何时使用

  • HGraph / IVF / Pyramid 的热路径。 当瓶颈在基础量化器距离计算时, 在相近召回下,sq8_uniform / sq4_uniform 几乎总是比对应的非 uniform 变种更快。
  • 维度间取值范围相近的数据。 归一化嵌入(cosine),或已通过 量化变换 链路(如 "rom, sq8_uniform""fht, sq8_uniform")旋转过的向量,都是理想 输入。
  • 作为 tq 链路的末端量化器。 最常见的链路是 "pca, rom, sq8_uniform",参考示例 501。

SQ4 uniform 与 SQ8 uniform 对比

类型每维位数相对 fp32 内存典型精度
sq8_uniform8~1/4轻微召回下降
sq4_uniform4~1/8需重排以保持高召回

参数

Key类型默认适用含义
sq4_uniform_trunc_ratefloat0.05sq4_uniform对离群值的有限对称截断比例,取值范围为闭区间 [0.0, 0.5]。值越大,越多极端坐标被截断,从而减少主体数据的范围浪费,代价是尾部被裁掉。

sq8_uniform 没有量化器专属的 JSON 参数。

在 HGraph 上,sq4_uniform_trunc_rate 作为顶层 key 暴露,并被映射到 嵌套的量化参数中(src/algorithm/hgraph.cpp:409-416)。

{
    "dtype": "float32",
    "metric_type": "l2",
    "dim": 128,
    "index_param": {
        "base_quantization_type": "sq4_uniform",
        "sq4_uniform_trunc_rate": 0.05,
        "max_degree": 32,
        "ef_construction": 300,
        "use_reorder": true,
        "precise_quantization_type": "fp32"
    }
}

若需 8 位变种,把 "base_quantization_type" 设为 "sq8_uniform" 并去掉 trunc_rate key 即可。

训练

设置了 NEED_TRAIN。训练在所有维度上估计单一的 [min, max]sq4_uniform 时可附加截断)。Build 会内部完成训练。

度量兼容性

l2ipcosine——全部支持。cosine 会先归一化再量化,这也使得 uniform 缩放在该度量下接近最优。

uniform 与非 uniform 之间如何选

  • 数据已归一化(cosine 或预归一化 l2)→ 选 uniform
  • 数据各维度取值范围差异极大(如混合特征块)→ 先尝试非 uniform 的 sq*,或在旋转变换后再用 uniform("rom, sq*_uniform")。
  • 吞吐比最后一点点召回更重要 → uniform

相关页面

乘积量化(PQ)

乘积量化把一个向量切成 pq_dim 个等长的子向量,每个子向量独立地按 含 2^pq_bits 个中心的小型码本进行量化。最终存储的码为每向量 pq_dim × pq_bits 比特——比 fp32 小数量级。查询时的距离计算通过每查询 预先计算的查找表(LUT)完成。

实现:src/quantization/product_quantization/product_quantizer.cpp, 参数文件 product_quantizer_parameter.cpp

何时使用

  • 高维浮点向量(≥ 256 维),且 sq8 仍嫌过大。
  • 内存紧张、精度可接受的工作负载,需要相对 fp32 约 16× 压缩。
  • 配合 use_reorder: true 与一个小型 fp16/fp32 精确存储,PQ 是大规模 场景下“压缩图索引“的标准配方。

如需在 pq_bits = 4 时获得更高的 SIMD 吞吐,见 PQ FastScan

内存代价(仅码)

每向量 ceil(pq_dim × pq_bits / 8) 字节,外加一份只存一次的小型码本 (pq_dim × 2^pq_bits × subspace_dim × 4 字节)。以典型配置 (pq_dim = 32pq_bits = 8dim = 128)为例:

  • 码大小 = 32 × 8 / 8 = 32 字节/向量(对比 fp32 的 128 × 4 = 512 → 小 16×)。

参数

Key类型默认含义
pq_dimint1子向量数量。必须整除 dim。取值越大,量化越细,但码本数量与码大小也会变大(product_quantizer_parameter.h:38)。
pq_bitsint8每个子向量的位数(1–8)。取 8 时每个子向量一字节。8 最稳;4 位 SIMD 变种见 PQ FastScan

在 HGraph 上,这些以顶层 key base_pq_dimpq_bits 暴露 (src/algorithm/hgraph.cpp:465-472)。

{
    "dtype": "float32",
    "metric_type": "l2",
    "dim": 128,
    "index_param": {
        "base_quantization_type": "pq",
        "base_pq_dim": 32,
        "max_degree": 32,
        "ef_construction": 300,
        "use_reorder": true,
        "precise_quantization_type": "fp16"
    }
}

训练

设置了 NEED_TRAIN。训练在每个子空间上跑 k-means 来学习 2^pq_bits 个 中心;这通常是所有内建量化器中最贵的训练步骤。每个子空间使用至少 256 × 2^pq_bits 个训练样本,码本会更稳定;Build(base) 会自动从输入 采样。

度量兼容性

l2ipcosine——全部支持。查询时距离通过每子空间的 LUT 计算: l2 使用查询子向量与每个中心的 L2 平方;ip 使用点积。cosine 在预 归一化向量上等价于 ip

实践要点

  • pq_dim 应整除 dim。常用比例是 dim/4dim/8
  • 极小的 pq_dim(如 dim/16)能得到非常紧凑的码,但召回会迅速下降; 务必配合重排。
  • 对各向异性数据,在 PQ 前接一层旋转变换能显著提升召回:用 量化变换 链路如 "rom, pq"

相关页面

PQ FastScan

pqfs乘积量化 的一个 SIMD 加速变种:将 pq_bits 固定为 4, 并采用专为 AVX-2 / AVX-512 “FastScan” 查表内核设计的内存布局。代价是仅 支持 4 位,但能带来显著更高的距离计算吞吐。

实现:src/quantization/product_quantization/pq_fastscan_quantizer.cpp, 参数文件 pq_fastscan_quantizer_parameter.cpp

何时使用

  • 平台有 AVX-2(最好还有 AVX-512);FastScan 内核正是选用 pqfs 而非 pq 的主要理由。
  • 关注的不只是内存,还有搜索吞吐。
  • 4 位子空间码本(每子向量 16 个中心)能满足召回目标——通常配合重排即 可。

如果平台不具备所需的 SIMD 宽度,请回退到普通 pq

内存代价(仅码)

每向量 ceil(pq_dim / 2) = (pq_dim + 1) / 2 字节——奇数和偶数 pq_dim 都支持(src/quantization/product_quantization/pq_fastscan_quantizer.cpp:41)。 码本:pq_dim × 16 × subspace_dim × 4 字节——因为每子空间只有 16 个中心, 比 8 位 pq 的码本小很多。

参数

Key类型默认含义
pq_dimint1子向量数量。必须整除 dimpq_bits 在内部固定为 4 且不可配(pq_fastscan_quantizer_parameter.cpp:28-33)。

在 HGraph 上以 base_pq_dim 暴露(src/algorithm/hgraph.cpp:465-472)。

{
    "dtype": "float32",
    "metric_type": "l2",
    "dim": 128,
    "index_param": {
        "base_quantization_type": "pqfs",
        "base_pq_dim": 32,
        "max_degree": 32,
        "ef_construction": 300,
        "use_reorder": true,
        "precise_quantization_type": "fp16"
    }
}

训练

设置了 NEED_TRAIN。在每子空间训练 16 中心码本;比 pq 的 256 中心训练 更便宜。

度量兼容性

l2ipcosine——覆盖与 pq 一致。LUT 布局因度量而异,但由量化器 透明处理。

实践要点

  • pq_dim 最好是内核预期 SIMD 批宽的倍数(实现在 AVX-512 上内部使用 32)。拿不准时,选 pq_dim ∈ {32, 64, 96, 128}
  • 相对 pq 的优势是相同召回下的吞吐,而非内存(4 位码本身就小,但 pqpq_bits = 4 同样能匹配大小)。
  • 想最大化召回恢复,配合 use_reorder: truefp16fp32 精确 存储。

相关页面

RaBitQ

rabitq 是 VSAG 的二值 / 低比特量化器。默认模式下每个坐标用 1 比特 编码,给出所有内建量化器中最高的压缩率。在 HGraph 和 Pyramid 上,x+y split 模式把 底库码拆成 x 个过滤 bit 和 y 个 supplement bit:图遍历只使用 filter code, 重排 / full-distance 阶段只额外读取 supplement bits。

实现:src/quantization/rabitq_quantization/rabitq_quantizer.cpp, 参数文件 rabitq_quantizer_parameter.cpp。 split 的完整存储布局、lower bound 公式和 IO 模式见 RaBitQ x+y Split

何时使用

  • 最强压缩。 1 比特码是稠密向量可能的最小存储。
  • 高维嵌入——旋转 + 二值化后仍能保留足够近邻搜索所需的几何信息。
  • 配合精确重排存储(fp16 / fp32)——标准做法就是 “RaBitQ + 重排”, 因为 1 比特距离本身噪声较大。

为获得最佳精度,请同时启用 rabitq_use_fht: true,或者用 量化变换 链路如 "pca, rom, rabitq" 包一层。

内存代价(仅码)

  • rabitq_bits_per_dim_base = 1:每向量 ceil(dim / 8) 字节。dim = 768 时为 96 字节(对比 fp32 的 3072 → 小 32×)。
  • HGraph 或 Pyramid 上 rabitq_bits_per_dim_base = xrabitq_bits_per_dim_precise = y:split 模式约存储 (x + y) * dim / 8 字节的 RaBitQ code。例如 3+5 约为每向量 dim 字节。

参数

Key类型默认含义
pca_dimint0(= 输入维度)RaBitQ 内部可选的 PCA 预处理维度。0 表示不做 PCA 降维(rabitq_quantizer_parameter.cpp:30-32)。
rabitq_bits_per_dim_queryint32搜索时查询的每维位数。允许值:432rabitq_quantizer_parameter.cpp:38-43)。
rabitq_bits_per_dim_baseint1standard RaBitQ 下表示底库码每维位数;HGraph/Pyramid x+y split 下,这个外部 key 表示 x,即图遍历过滤阶段使用的 filter bits。范围 [1, 8]
rabitq_bits_per_dim_preciseint未设置HGraph/Pyramid split 模式 key。和 base_quantization_type: "rabitq"precise_quantization_type: "rabitq" 一起出现时表示 y,即重排 / full-distance 阶段读取的 supplement bits。要求 x + y <= 8
rabitq_error_ratefloat1.9HGraph/Pyramid split 搜索的默认 lower-bound 误差倍率;必须为有限正数,也可以在 hgraphpyramid 搜索参数中按次覆盖。
use_fhtboolfalsetrue 时在二值化前应用快速 Hadamard 变换旋转。以 O(dim log dim) 的廉价代价提升各向异性数据上的精度(rabitq_quantizer_parameter.cpp:76-78)。
fast_encode_rabitqbooltrue对大于 1 bit 的底库码启用基于 CAQ 的快速编码;设为 false 时使用原有精确编码。1 bit 编码会忽略此参数。
fast_encode_rabitq_roundsint6CAQ 坐标微调轮数,范围 [1, 32];每个坐标在每轮最多移动一级。

启用 fast_encode_rabitq 后,多 bit RaBitQ 先进行 LVQ 初始化,再执行固定轮数的 坐标微调,把码字选择复杂度从约 O(2^B * dim * log(dim)) 降到 O(rounds * dim),且不改变编码布局和查询估计器。实现参考 SAQ 中的 CAQ;在新数据集上评估质量与速度 权衡时可关闭该参数使用精确编码。两个参数仅影响构建,不影响已保存索引的加载 兼容性。VSAG 采用 clean-room 实现,不依赖采用 Apache-2.0 许可证的 SAQ 参考仓库

各索引页会把 RaBitQ 设置暴露为 index_param 顶层 key:HGraph 暴露 rabitq_pca_dimrabitq_bits_per_dim_queryrabitq_bits_per_dim_baserabitq_bits_per_dim_preciserabitq_error_raterabitq_use_fht;IVF 暴露 rabitq_pca_dimrabitq_bits_per_dim_queryrabitq_bits_per_dim_baserabitq_versionrabitq_error_raterabitq_use_fht;Pyramid 为底层量化器暴露 PCA、底库/查询位数、split precise 位数、误差倍率和 FHT 相关 key。其中 rabitq_use_fht 是索引层对量化器内部 use_fht key 的别名,会由索引层重写。

fast_encode_rabitqfast_encode_rabitq_rounds 同时适用于 HGraph、IVF 和 Pyramid,并会传播给 base 与 precise RaBitQ 量化器。

对于启用 split RaBitQ 和 fast_encode_rabitq=true 的普通首次 HGraph 构建时,HGraph 会先并行把所有向量编码为逐维一个无符号字节的 scalar code,同时保存标准 RaBitQ metadata,并在独立数组中为每个 向量保存 8 字节 code sum。编码完成后才启动构图任务,使用 scalar SIMD 计算 code-code 距离。raw inner-product 内核不感知配置的 x+y 总位数, 量化器仍会应用对应的量化范围和中心值。构图完成后,scalar code 只执行 一次 bit-plane pack,并写入最终 filter 与 supplement 存储,不会重新执行 PCA、ROM/FHT 或 RaBitQ 量化。scalar record 与 code-sum 数组都会在 Build 返回前释放。总位数为 8 时,scalar 与 packed payload 大小相同; 总位数更低时会以额外构建内存换取更快的构图距离计算。

{
    "dtype": "float32",
    "metric_type": "l2",
    "dim": 768,
    "index_param": {
        "base_quantization_type": "rabitq",
        "rabitq_use_fht": true,
        "rabitq_pca_dim": 0,
        "rabitq_bits_per_dim_base": 1,
        "rabitq_bits_per_dim_query": 32,
        "max_degree": 32,
        "ef_construction": 300,
        "use_reorder": true,
        "precise_quantization_type": "fp32"
    }
}

切换到高精度的 x+y split 模式:把 base 和 precise 量化都设置为 RaBitQ, 并提供 rabitq_bits_per_dim_precise。HGraph 和 Pyramid 会自动选择 split datacell。 下面例子中,图遍历使用 x = 3 个 filter bits,重排只读取 y = 5 个 supplement bits:

{
    "base_quantization_type": "rabitq",
    "precise_quantization_type": "rabitq",
    "rabitq_bits_per_dim_base": 3,
    "rabitq_bits_per_dim_precise": 5,
    "rabitq_use_fht": true
}

训练

设置了 NEED_TRAIN。训练学习让 1 比特编码均衡的旋转与逐维统计。可选的 FHT 旋转是固定的(无需学习),因此不增加训练代价;PCA 预处理(`pca_dim

0`)会训练一个投影矩阵。

度量兼容性

l2ipcosine——全部支持。二值距离内核是对 XOR 后的码字做 popcount; 对 ip / cosine,实现还会追踪一份残差范数,使内积估计无偏。

实践要点

  • 始终启用重排,除非你已经验证 1 比特召回在你的数据上可接受。 use_reorder: true + precise_quantization_type: "fp32" 是稳妥默认。
  • 先旋转。 对未归一化数据,设 rabitq_use_fht: true,或在 tq 链路 中包含 rom / fht
  • 精度优先时用 split 模式。 HGraph/Pyramid x+y split 保留 x bit 快速 过滤路径,再添加 y 个 supplement bits 用于重排;相对纯 1 比特,使用 更多总 bit 时召回明显更高。

相关页面

RaBitQ x+y Split

RaBitQ x+y split 是 HGraph 和 Pyramid 面向低比特底库码的存储与搜索模式。每条向量拆成 两条记录:

  • 图遍历和 lower-bound 过滤只读取 x 个 filter bits。
  • 只有进入重排的候选才读取 y 个 supplement bits。
  • 最终重排距离使用完整的 x+y bits。

这种布局缩小了图遍历的热数据,同时保留更高精度的 RaBitQ 距离用于最终排序。 它也支持把 filter record 留在内存中,把访问频率更低的 supplement record 放到磁盘。

启用 split 模式

当 base 和 precise 的量化类型都为 rabitq,并且配置了 rabitq_bits_per_dim_precise 时,HGraph 和 Pyramid 自动选择 split 模式:

{
    "dtype": "float32",
    "metric_type": "l2",
    "dim": 960,
    "index_param": {
        "base_quantization_type": "rabitq",
        "precise_quantization_type": "rabitq",
        "use_reorder": true,
        "rabitq_bits_per_dim_query": 32,
        "rabitq_bits_per_dim_base": 3,
        "rabitq_bits_per_dim_precise": 5,
        "rabitq_error_rate": 1.9,
        "max_degree": 64,
        "ef_construction": 400
    }
}

相关参数如下:

参数含义
base_quantization_type必须为 "rabitq"
precise_quantization_typesplit 模式下同样必须为 "rabitq"
rabitq_bits_per_dim_basex,图遍历时读取的 filter bit 数。
rabitq_bits_per_dim_precisey,重排时额外读取的 supplement bit 数。
rabitq_bits_per_dim_querysplit storage 必须使用 32
rabitq_error_ratelower-bound 误差项的默认正数倍率。
use_reorder建议设为 true,使用 x+y 距离排序候选。

参数约束为:

1 <= x <= 8
1 <= y <= 8
x + y <= 8

如果不配置 rabitq_bits_per_dim_precise,HGraph 和 Pyramid 使用 standard RaBitQ 路径, 不会创建 split storage。

使用以下搜索参数启用 filter/lower-bound 搜索路径:

{
    "hgraph": {
        "ef_search": 200,
        "parallelism": 4,
        "rabitq_one_bit_search": true,
        "rabitq_error_rate": 1.9
    }
}

外部搜索参数仍命名为 rabitq_one_bit_search,但对 split 索引,它会使用 rabitq_bits_per_dim_base 配置的全部 x 个 filter bits。 hgraph.rabitq_error_rate 可以为单次搜索覆盖索引默认值。record 中保存的是乘倍率前的 几何误差尺度,因此 sweep 这个搜索参数不需要重建索引。

搜索流程

split 搜索分为四个阶段:

  1. query 只做一次变换和归一化;对支持的 filter bit 数,还会为每个 query 构建一次 byte lookup table。
  2. 图遍历只读取 filter record,为每个访问到的向量计算 x-bit 距离估计和 保守的 lower bound。
  3. 重排先丢弃 lower bound 不可能进入结果集的候选,只为剩余候选读取 y-bit supplement record。
  4. 最终距离把 filter contribution 与 supplement contribution 合成为 x+y-bit RaBitQ 估计。

因此,图搜索不会为每个访问到的向量都计算 x+y 距离并放入搜索堆。图遍历由 低成本的 x-bit 距离驱动,更精确的距离只在候选重排阶段计算。

编码和 bit-plane

定义:

d       = 变换后的维度
x       = 每维 filter bit 数
y       = 每维 supplement bit 数
B       = x + y
P       = ceil(d / 8),一个 bit-plane 的字节数
q_i     = 变换并归一化后的 query 坐标
u_i     = 无符号 B-bit 底库码,0 <= u_i < 2^B

完整 code 的中心化表示为:

c_B = (2^B - 1) / 2
z_i = u_i - c_B
N_B = sqrt(sum_i z_i^2)

PackIntoPlanesu_i 的每一个逻辑 bit 存成独立 bit-plane。filter 和 supplement 的划分为:

f_i = floor(u_i / 2^y)    # 高 x bits
s_i = u_i mod 2^y         # 低 y bits
u_i = 2^y * f_i + s_i

物理布局让高位 filter planes 连续存储:

filter record:     logical B-1, B-2, ..., B-x
supplement record: logical 0, 1, ..., y-1

因此图遍历只需扫描 x * P 字节的 plane payload;重排只额外读取 y * P 字节的 plane payload,不计元数据和对齐。

Datacell 布局

RaBitQSplitDataCell 内部维护两个 RaBitQSplitCodeStorage

Filter record

x_bit_cell_ 中的 filter record 包含:

x 个高位 bit-plane
base norm
x > 1 时的 filter-code norm
可选 MRQ residual norm
IP/cosine 使用的可选 raw norm
lower-bound error
filter approximation error

每条向量的 filter plane payload 为:

FilterPlanesSize = x * ceil(d / 8)

filter record 是图遍历的热数据。只要 x-bit 估计有效,图搜索和预取都不需要 访问 supplement record。

Supplement record

supplement_cell_ 中的 supplement record 包含:

y 个低位 bit-plane
full-code norm
full-code approximation error
当前 metric 和 transform 所需的其他元数据

每条向量的 supplement plane payload 为:

SupplementPlanesSize = y * ceil(d / 8)

完整 code 的 payload 约为每条向量 (x+y) * d / 8 字节,此外还有对齐后的 norm、error 和可选 transform 元数据。

X-bit filter 距离和 lower bound

i 维 filter code 为 f_i,取值范围 [0, 2^x - 1]。定义:

c_x   = (2^x - 1) / 2
N_x   = sqrt(sum_i (f_i - c_x)^2)
S_x   = sum_i q_i * f_i
Q_sum = sum_i q_i
rho_x = (S_x - c_x * Q_sum) / N_x

构建索引时,RaBitQ 保存 filter approximation error 的绝对值 E_x,并计算 几何误差尺度:

E_safe    = clamp(abs(E_x), 1e-5, 1)
epsilon_x = sqrt(max(0, 1 - E_safe^2) / max(1, d - 1))

修正后的 filter 内积估计为:

rho_hat_x = rho_x / abs(E_x)

对 L2,设 base norm 为 N_o、query norm 为 N_q,x-bit 距离和 lower bound 为:

D_x = N_o^2 + N_q^2 - 2 * N_o * N_q * rho_hat_x

LB = D_x
     - 2 * N_o * N_q * rabitq_error_rate * epsilon_x / abs(E_x)

实现还会从 LB 中减去一个很小的浮点保护量。IP 和 cosine 会按各自的 metric 换算误差项。

lower bound 只用于安全地排除候选。D_x 是图遍历距离,最终排序使用完整的 x+y 距离。

Query lookup table 和 SIMD

x = 2x = 3 时,query computer 会构建 FastScan 风格的 byte lookup table。每一行对应八个 query 坐标,并包含 256 个表项:

LUT[block][byte_value]
    = byte_value 在该 8-D block 中置位位置对应的 q_i 之和

随后每个 filter plane 的每个字节只需要查表一次,不必逐坐标解码八次。不同 filter plane 再按二进制权重合成为 S_x

AVX2 和 AVX512 kernel 会同时 gather 多个 LUT 表项,并提供 batch-of-four 路径; scalar 实现作为可移植 fallback。关键入口为:

  • RaBitQFloatMultiBitIPByLookup
  • RaBitQFloatMultiBitIPBatch4ByLookup
  • RaBitQFloatBuildByteIPLookupTable

不在专用范围内的 x-bit 宽度仍由通用 bit-plane 计算路径支持。

Reorder 只扫描 y 个 supplement bits

完整无符号 code 满足:

sum_i q_i * u_i
    = 2^y * sum_i q_i * f_i
      + sum_i q_i * s_i

对使用 x-bit lookup filter 的 L2 搜索,HGraph 和 Pyramid 会把之前计算的 filter distance 作为 hint 传给 reorder。ComputeDistWithSplitCodeAndFilterDist 从 hint 恢复第一项, 只从 y 个 supplement planes 计算第二项:

full contribution = shifted filter contribution + supplement contribution

因此 3+5 索引会复用 3-bit filter 结果,每个重排候选只扫描 5 个新的 bit-plane。 如果 hint 不存在或不能使用,代码会回退到 ComputeDistWithSplitCode,直接从两个 split records 计算相同的最终距离。

内存、磁盘和混合 IO

如果没有单独配置 supplement IO,两个 record 使用相同的 base IO 类型。

两个 record 都在内存

{
    "base_io_type": "block_memory_io"
}

两个 record 都在磁盘

{
    "base_io_type": "async_io",
    "base_file_path": "/data/hgraph_rabitq_split"
}

VSAG 会为 filter 和 supplement record 创建不同的 backing path。

Filter 在内存,supplement 在磁盘

{
    "base_io_type": "block_memory_io",
    "base_supplement_io_type": "async_io",
    "base_file_path": "/data/hgraph_rabitq_split"
}

当前支持的 mixed-IO 组合把 x_bit_cell_ 保存在 block memory,把 supplement_cell_ 放在 async IO。批量重排时,filter record 通过直接指针读取, MultiRead 只拉取 supplement records。可以显式设置 base_supplement_file_path;否则 VSAG 根据 base_file_path 生成 supplement path。

序列化和加载

使用标准的索引级序列化接口即可,业务侧不需要分别持久化两个 record。

std::ofstream out("/path/to/index.bin", std::ios::binary);
auto serialized = index->Serialize(out);

auto loaded = vsag::Factory::CreateIndex("hgraph", index_params).value();
std::ifstream in("/path/to/index.bin", std::ios::binary);
auto deserialized = loaded->Deserialize(in);

split datacell 按以下顺序序列化:

  1. datacell 基础状态和 supplement IO type。
  2. filter storage。
  3. supplement storage。
  4. RaBitQ quantizer 状态。

创建目标索引时必须使用与序列化索引兼容的参数,尤其是 dimmetric_type、 x/y bit 数和 query bits。修改编码参数需要重建索引;只调整搜索参数 hgraph.rabitq_error_rate 不需要。

实现位置

模块文件 / 入口
外部 x/y 参数映射src/algorithm/hgraph/hgraph_param_mapping.cpp
split record 和 IOsrc/datacell/rabitq_split_datacell.h
plane 布局和 code 拆分RaBitQuantizer::StoredPlaneIndexSplitCode
filter 距离和 lower boundComputeDistWithOneBitLowerBound
直接计算 split distanceComputeDistWithSplitCode
使用 filter hint 的 reorderComputeDistWithSplitCodeAndFilterDist
SIMD dispatchsrc/simd/rabitq_simd.cpp
AVX2 / AVX512 lookup kernelsrc/simd/avx2.cppsrc/simd/avx512.cpp
内存/磁盘/混合 IO 示例examples/cpp/323_index_hgraph_rabitq_split.cpp

使用注意

  • split storage 当前可用于 HGraph 和 Pyramid,并且要求 fp32 query code。Pyramid 的 split 索引默认启用 one-bit split 搜索路径;如需强制使用普通搜索路径,可以在 pyramid 搜索参数下传 rabitq_one_bit_search: false
  • 支持 l2ipcosine;利用 filter hint 的 reorder 快速路径当前针对 L2。
  • 除非已经验证仅靠 x-bit 遍历距离能满足召回要求,否则应保持 use_reorder: true
  • 修改 x、y、metric 或 transform 参数后必须重建索引;在搜索参数中覆盖 hgraph.rabitq_error_rate 不需要重建。
  • RaBitQ 通用说明见 RaBitQ,完整 HGraph 参数见 HGraph 索引Pyramid 索引

量化变换(Quantization Transform)

变换量化器base_quantization_type: "tq")在最终量化器之前串联一个或多个向量变换。 变换会重塑向量分布,让后续量化器能更准确、更紧凑地编码 —— 例如把向量旋转一下,让能量分散 到各个维度(RaBitQ / SQ 受益最大),或者先用 PCA 降维再存储。

可运行示例:examples/cpp/501_quantization_transform.cpp

为什么需要变换层

纯量化器直接压缩向量。对低比特量化器(如 sq4sq*_uniformrabitq),编码精度严重 依赖向量坐标的分布:长尾或各向异性的维度会浪费 code bit。变换层可以缓解这个问题:

  • 随机旋转romfht)让坐标去相关,均匀/标量量化器在每个轴上工作得更好。
  • PCApca)在保留主要方差的同时降低维度 —— code 大小按比例缩小。
  • MRLEmrle)是为 L2/IP 搜索设计的距离可恢复低秩编码。

变换后的输出再喂给一个标准量化器(fp32sq8sq8_uniformrabitq ……),由后者 真正存储 code。整条链被称为 tq(Transform Quantizer)

快速上手

tq 已作为对外可配置的量化类型暴露给 HGraphPyramid。两者都会把 tq_chain 与降维参数映射到 base_codes.quantization_params。MRLE + RaBitQ x+y split 组合在两个索引中均可用,并自动从 base split datacell 重排。IVF、BruteForce 与 WARP 目前仍未通过外部参数映射暴露 tq_chain

std::string params = R"({
    "dtype": "float32",
    "metric_type": "l2",
    "dim": 128,
    "index_param": {
        "base_quantization_type": "tq",
        "tq_chain": "pca, rom, sq8_uniform",
        "rabitq_pca_dim": 64,
        "max_degree": 32,
        "ef_construction": 300,
        "use_reorder": true,
        "precise_quantization_type": "fp32"
    }
})";

vsag::Resource resource(vsag::Engine::CreateDefaultAllocator(), nullptr);
vsag::Engine engine(&resource);
auto index = engine.CreateIndex("hgraph", params).value();
index->Build(base);
auto result = index->KnnSearch(query, topk, search_params).value();

上面的例子里,base 向量先从 128 维降到 64 维(pca),随后做随机旋转(rom),最后用 sq8_uniform 量化。开启了 reorder,HGraph 同时保留一份 fp32 精确副本,对图搜索返回的 top 候选做精排(include/vsag/index.h;存储影响见 内存管理)。

tq_chain 语法

tq_chain 是一个以逗号分隔的字符串:一个或多个变换名,最后跟一个唯一的量化器名。 token 两侧的空白会被自动 trim (src/quantization/transform_quantization/transform_quantizer_parameter.cpp:53-74)。

"<变换1>, <变换2>, ..., <量化器>"

示例:

作用
"rom, fp32"随机旋转后以 fp32 存储(多用于基线/sanity)。
"fht, sq8_uniform"快速 Hadamard 旋转 + 8 位均匀标量量化。
"pca, rom, sq8_uniform"先 PCA 降维,再随机旋转,再 8 位均匀量化 —— 即示例 501。
"pca, rom, rabitq"PCA + 旋转后喂给 RaBitQ 二值量化器。
"mrle, fp32"MRLE 投影再以 fp32 存储(MRLE 必须放在最前)。
"mrle, rabitq"先做 MRLE 降维,再由 RaBitQ 编码;使用 x+y split 存储时,filter 与 supplement 编码仍由末端 RaBitQ 生成。

约束(transform_quantizer_parameter.cpp:33-45):

  • 链至少包含 1 个变换 + 1 个量化器(长度 ≥ 2)。空串或单 token 会抛 INVALID_ARGUMENT
  • 最后一个 token 必须是 TQ flatten 路径能够 dispatch 的量化器 —— fp32sq8sq8_uniformsq4sq4_uniformbf16fp16pqpqfsrabitq 之一 (src/datacell/flatten_interface.cpp:126-164)。TransformQuantizerParameter 解析层会 额外接受 sparseint8tq,但 flatten 工厂没有针对 int8/tq 的分发分支,并且当 is_transform_quantizer=true 时显式拒绝 sparsesrc/datacell/flatten_interface.cpp:166),因此这三个不能用作 TQ 末端,否则会在构建索引时 以 “unsupported quantization type” 失败。
  • RaBitQ x+y split 存储仅支持精确链 "mrle, rabitq"。split datacell 复用末端 RaBitQ 编码器,并保留 RaBitQ 内部的 FHT/ROM 随机旋转;其他带变换的 split 链会被拒绝。
  • 未识别的变换名会抛 INVALID_ARGUMENT: invalid transformer nametransform_quantizer.h:225-227)。

支持的变换

src/quantization/transform_quantization/transform_quantizer.h:192-227 的工厂当前识别 4 个变换名:

名称输出维度描述实现
pca设置了 pca_dim 则取该值,否则同输入主成分分析投影;在保留方差的前提下降维。src/impl/transform/pca_transformer.h
rom同输入随机正交矩阵;旋转向量以让各维去相关。src/impl/transform/random_orthogonal_transformer.h
fht同输入快速 Hadamard / KAC 随机旋转;rom 的低开销变体。src/impl/transform/fht_kac_rotate_transformer.h
mrlemrle_dim(≤ 输入维)距离可恢复低秩编码;必须是链中第一个变换src/impl/transform/mrle_transformer.h

说明:

  • mrle 必须位于首位由 transform_quantizer.h:155-159 强制;mrle_dim ≤ input_dimtransform_quantizer.h:217-220 强制。
  • header 中声明的其它字符串(residualnormalize接入工厂,会被拒绝。

变换参数

变换 JSON 由 VectorTransformerParameter::FromJson 解析 (src/impl/transform/vector_transformer_parameter.cpp:22-35):

类型默认含义
pca_dimint0(= 输入维)pca 变换的输出维。
mrle_dimint0(= 输入维)mrle 变换的输出维。
input_dimint自动由链自动填充 —— 不要手动设置。

HGraph 顶层映射

使用 HGraph 时,顶层快捷键会被映射到嵌套的量化器参数中:

  • tq_chainbase_codes.quantization_params.tq_chain
  • rabitq_pca_dimbase_codes.quantization_params.pca_dim
  • mrle_dimbase_codes.quantization_params.mrle_dim

rabitq_pca_dim 这个名字早于 Transform Quantizer 引入;当链中包含 pca 时,它实际 驱动的是 pca 变换的输出维(与 RaBitQ 无关)。如果链以 rabitq 结尾且未使用 pca,则同一个键会配置 RaBitQ 自身的 PCA 预处理 (src/quantization/rabitq_quantization/rabitq_quantizer_parameter.cpp:30)。

mrle_dim 接受 [0, dim] 内的整数;0 表示输入维度。它在 tq_chain 包含 mrle 时生效,且 mrle 必须保持为链中的第一个变换。

Reorder 与精确码存储

变换链在设计上一定有信息损失(旋转无损,但 pca / sq*_uniform / rabitq 有损)。 把 tqreorder 组合使用 —— 即额外保留一份精确(通常是 fp32)副本,对 top 候选 做精排 —— 可以以较小的内存成本恢复精度:

  • use_reorder: true 会让 HGraph 额外维护一份 flatten 存储,称为精确码存储src/algorithm/hgraph.cpp:76-79)。
  • precise_quantization_type 决定精确码使用的量化器(默认 fp32;若想用内存换精度, 也可以设为 fp16 / bf16 / sq8)。
  • 搜索时先用低成本的 tq base codes 走图,得到的 top-K 候选再用精确码重新打分 (hgraph.cpp:978-981 及附近调用)。

use_reorderprecise_quantization_type 并非 tq 专属 —— 当 base_quantization_typesq8pqrabitq 等时同样适用。完整的逐索引参数表见 HGraph 索引

链该怎么选

经验法则:

目标建议链备注
激进压缩 + 精度恢复"pca, rom, sq8_uniform" + use_reorder: trueprecise_quantization_type: "fp32"示例 501 的基线。
最大压缩"pca, rom, rabitq" + reorder1 bit 量化 + 旋转校正;不开 reorder 精度损失明显。
各向异性数据、不降维"rom, sq8_uniform""fht, sq8_uniform"高维下用 fht 构建成本更低。
距离保持的低秩"mrle, fp32"度量感知降维,不再量化。

请在自有数据上 benchmark —— tq 的激进程度与 use_reorder 的取舍最终取决于数据分布、 目标召回率以及内存预算。

兼容性与合并

两个 tq 配置只有在链长度、每一步变换名、最终量化器都完全一致时才被视为兼容 (src/quantization/transform_quantization/transform_quantizer_parameter.cpp:99-117)。 这一点对序列化往返以及未来的合并/克隆操作至关重要 —— 准备合在一起的索引,应保持 chain 字符串稳定。

chain 字符串一致只是必要条件,并不充分。 tq_chain token 列表并不编码变换器参数 (例如 pca_dim / mrle_dim,它们作为兄弟 JSON 键单独读取,见 src/quantization/transform_quantization/transform_quantizer.h:200-216),也不编码末端量化器 的内部参数(例如 pq 子空间数、rabitq 旋转种子等)。这些参数会改变实际 code 的维度与 布局,因此两个构建要真正可合并/可克隆,必须保持整套 transform + quantizer 参数一致, 不能只对齐 chain 字符串。

相关页面

  • HGraph 索引 —— base_quantization_typeuse_reorderprecise_quantization_type 等参数说明。
  • 内存管理 —— base + precise 存储的内存开销。

代码目录结构

VSAG 项目代码处于快速迭代中,目录组织并不完美,这里仅对当前目录的功能划分做简要介绍。

项目结构

  • .circleci/:CircleCI 配置文件;
  • .github/:GitHub 配置文件,包括 CI、Issue 模版、代码 Owner 等;
  • cmake/:CMake 工具函数,例如检测编译平台的指令集支持;
  • docker/:构建 CI 的 Dockerfile 以及用于二进制分发的 Dockerfile;
  • docs/:设计文档、用户文档(含本站点源)和博客文章;
  • examples/:C++、Python、TypeScript 的示例代码;
  • extern/:第三方库,以 CMake 的方式从 GitHub 下载和集成;
  • include/:公开头文件,对外稳定 API 都位于此目录;
  • python/:pyvsag 打包和安装工具;
  • python_bindings/:基于 pybind11 的 Python 绑定实现;
  • typescript/:Node.js / TypeScript 绑定及对应 npm 包源代码;
  • scripts/:一些有用的工具脚本,例如安装依赖、计算代码覆盖率等;
  • src/:核心源代码和单元测试(*_test.cpp);
  • tests/:功能测试用例;
  • tools/:相关工具,包括索引性能测试和兼容性检查工具。

核心源代码

  • src/*.cpp:各种公共功能代码实现,包括内存分配器、线程池等;
  • src/algorithm/:索引算法目录;
  • src/data_cell/:data cell 是数据的逻辑单元,索引算法依赖于 data cell;
  • src/impl/:一些功能和算子的实现,例如图结构增强、k-means 聚类等;
  • src/index/:索引层实现,和 algorithm 目录相互配合;
  • src/io/:数据 IO 实现,包括基于内存访问数据和基于磁盘访问数据的方法;
  • src/quantization/:量化方法,当前支持 SQ4/SQ8、PQ、RaBitQ 等量化方式;
  • src/simd/:指令集加速模块,根据运行平台自动选择最快的距离计算方法;
  • src/utils/:工具函数目录。

C++ 编码指南

VSAG 以 Google C++ 风格指南 为基础,并针对 代码格式、命名和静态检查制定了项目规则。仓库根目录下的配置文件是这些规则的准确定义:

VSAG 严格要求使用 clang-format 15clang-tidy 15。其他版本可能产生不同的格式化 结果或诊断信息,无法通过 CI。

代码组织

  • 公开 API 放在 include/vsag/,实现代码放在 src/
  • C++ 源文件使用 .cpp 后缀,不使用 .cc
  • 除非文件有明确的特殊要求,代码应放在 vsag 命名空间中;
  • 除非明确计划进行破坏性变更,否则应保持 API 兼容;
  • 修改公开 API 时,应添加或更新 Doxygen 注释;
  • 新代码优先使用 uint64_t 而不是 size_t,避免 macOS 兼容性问题;
  • 除非修改目标明确涉及第三方依赖,否则不要修改 extern/
  • 所有提交的文本文件都应以换行符结尾。

格式化规则

格式化配置以 Google 风格为基础,并应用以下 VSAG 规则:

规则VSAG 风格
缩进4 个空格
C++ 行宽最多 100 列
访问控制符相对成员代码减少一级缩进
返回类型与函数名分行书写
简短代码块、函数和 if 语句不压缩到单行
指针和引用*& 与类型结合
头文件引用自动排序
换行后的参数和实参每行一个,不进行 bin-pack
行尾注释连续成组时自动对齐
已有注释文本不自动重新换行

例如,clang-format 15 会将类声明格式化为:

class Example {
public:
    Result<DatasetPtr>
    Build(const DatasetPtr& base, const std::string& parameters);

private:
    void
    reset_state();

    DatasetPtr dataset_;
};

100 列限制只适用于 C++ 源文件和头文件,不要求 Markdown 或其他非 C++ 文本遵循这一行宽。

命名规则

.clang-tidy 中的 readability-identifier-naming 会检查以下命名:

对象风格示例
命名空间lower_casevsag::storage
CamelCaseHGraphIndex
结构体CamelCaseSearchResult
类型模板参数CamelCaseDataType
值模板参数lower_casemax_degree
自由函数或 C 风格函数lower_casenormalize_vector
公有方法CamelCaseBuild
私有方法lower_casereset_state
变量lower_casesearch_result
私有或受保护的数据成员lower_case_search_result_
宏定义UPPER_CASECHECK_ARGUMENT
枚举值UPPER_CASEROUTING
全局常量UPPER_CASEDEFAULT_RESIZE_INCREASE_COUNT_BIT

make lint 还会运行 scripts/linters/check-struct-names.py,检查所有已被 Git 跟踪的项目 C/C++ 文件中的具名结构体,包括 clang-tidy 常规检查范围之外的文件。因此,新建的具名结构体都必须使用 CamelCase

对于没有项目专用规则的对象,请遵循 Google C++ 风格以及所在模块的现有约定。

选择 class 还是 struct

当类型是被动、值语义的数据载体时,使用 struct

  • 主要用途是将一组相关数据组织在一起;
  • 数据成员有意保持公开,调用方可以彼此独立地修改它们;
  • 不需要保护对象不变量,也不需要隐藏内部表示;
  • 不负责需要受控访问或生命周期管理的资源。

例如 BinarySparseVectorMultiVectorstruct 仍然可以包含成员默认值、构造函数或简单的 辅助函数。是否存在成员函数并不是选择 class 的决定因素,关键在于该类型是否仍是透明的数据值。

当类型表示行为或必须控制自身状态时,使用 class

  • 通过公开方法维护对象不变量;
  • 使用私有或受保护成员隐藏实现细节;
  • 持有或管理需要控制生命周期的资源;
  • 提供行为接口或多态接口,尤其是包含虚函数的接口。

例如 BinarySetDatasetFilterIndex。新建接口和多态基类时,优先使用 class

无法确定时,可以判断调用方是否应当以任意顺序直接修改每一个数据成员。如果允许,使用 struct;如果 类型必须校验、协调或限制这些修改,使用 class。不要仅仅为了省略 public: 而使用 struct,也不要 为了支持聚合初始化而暴露可变的实现细节。

类和结构体都使用 CamelCase。除非有意进行 API 变更,否则应保留公开类型当前使用的形式,不要把 classstruct 之间的转换作为纯风格清理。

静态检查

所有已启用的 clang-tidy 诊断都按错误处理。当前配置启用了以下检查组:

  • bugprone-*:发现可能的逻辑缺陷和可疑写法;
  • clang-analyzer-*:进行路径敏感的正确性和生命周期分析;
  • clang-diagnostic-*:通过 clang-tidy 报告编译器诊断;
  • modernize-*:检查项目支持的现代 C++ 写法;
  • openmp-*:检查 OpenMP 使用的正确性;
  • performance-*:发现不必要的复制和低效操作;
  • readability-*:检查可维护性和命名一致性。

部分检查会与 VSAG 的底层、高性能代码冲突,或者在当前代码库中产生过多噪声,因此被明确禁用:

禁用的检查原因
clang-analyzer-deadcode.DeadStores“写入但未读取”的诊断噪声过多
clang-diagnostic-deprecated-builtins工具链或平台兼容仍需要部分已弃用的 builtin
bugprone-easily-swappable-parameters误报过多
modernize-use-trailing-return-type与项目的返回类型风格冲突
modernize-avoid-c-arraysSIMD 和其他底层代码会有意使用 C 数组
readability-identifier-length上下文清晰时允许使用短名称
readability-magic-numbers数值密集的向量索引代码会产生过多噪声
readability-function-cognitive-complexity在解决现有问题前暂不启用
readability-redundant-access-specifiers允许重复访问控制符来划分方法组

禁用检查并不意味着可以忽略它所关注的问题。在能够提升可读性的情况下,仍应使用含义明确的参数名和 具名常量。

抑制诊断

应优先修复诊断指出的问题。如果诊断属于误报,或者相关写法是兼容性或性能所必需的,应使用范围最小的 抑制,并说明这样做安全的原因:

// NOLINTNEXTLINE(performance-no-int-to-ptr): platform API 将该句柄存储为整数。
auto* handle = reinterpret_cast<Handle*>(raw_handle);

NOLINTNOLINTNEXTLINE 或范围严格受控的 NOLINTBEGIN/NOLINTEND 中写出具体检查名。 除非无法采用更小的范围,否则不要使用不带检查名的 NOLINT 或对整个文件禁用检查。

运行检查

首先安装指定版本的工具:

# Ubuntu 或 Debian
sudo apt-get install clang-format-15 clang-tidy-15 clang-tools-15

# macOS
brew install llvm@15

格式化所有项目 C++ 文件并检查修改:

make fmt
git diff --check

clang-tidy 需要 build-release/ 中的编译数据库,因此应先完成发布模式构建:

make release
make lint

默认 lint 目标以 src/ 下的生产环境 .cpp 文件作为 clang-tidy 入口,并排除 *_test.cpp;同时还会 运行覆盖整个仓库的结构体命名检查。测试和其他目录不会在该命令中作为独立的 clang-tidy 入口。

make fix-lint 会直接应用 clang-tidy 建议的修改,可能产生较大范围的变更。请在干净或已安全备份的 工作区中使用,检查完整 diff,然后重新运行格式化、静态检查和相关测试:

make fix-lint
make fmt
make lint
make test

提交修改前

  • 使用 clang-format 15 格式化所有修改过的 C++ 文件;
  • 使用 clang-tidy 15 检查修改过的生产环境源码;
  • 确认命名符合上述规则,尤其是结构体使用 CamelCase
  • 确保每个 NOLINT 都指定具体检查,并说明原因;
  • 新功能包含测试,bug 修复包含回归测试;
  • 保持 C++ 库的单元测试覆盖率不低于 90%;
  • 运行 git diff --check 并检查完整 diff。

新索引接入检查清单

新增 VSAG 索引实现时使用这份检查清单。第一版应保持范围可控:先让索引能通过公开 factory 创建,支持它声明的生命周期方法;只有在行为已经实现并经过测试后,再开启对应的 feature flag。

必做项

  • 选择公开索引名称和类型。

    • 如果新索引需要公开名称常量,请在 include/vsag/constants.hsrc/inner_string_params.h 中添加。
    • 当调用方需要通过 Index::GetIndexType() 区分该索引时,在 include/vsag/index.h 中添加 IndexType 枚举值。
    • 保持公开名称稳定。src/factory/index_registry.cpp 会在查找前把 factory 名称 归一化为小写。
  • 在公开 Index API 背后实现索引。

    • 对新的内存索引,优先沿用 src/index/index_impl.h 中现有的 IndexImpl<T> 模式: 在 src/algorithm/<name>/ 下实现一个 InnerIndexInterface 子类 T
    • 实现 static CheckAndMappingExternalParam(const JsonType&, const IndexCommonParam&), 让 IndexImpl<T> 能校验外部 JSON 并构造内部参数对象。
    • 按内部索引契约实现 GetName()GetIndexType()GetNumElements()Add()KnnSearch()Serialize(StreamWriter&)Deserialize(StreamReader&);索引支持 Build() 时再实现它。InnerIndexInterface::Add() 是纯虚函数,因此每个子类都必须重写, 即使索引只支持 Build();不支持时应抛出 UNSUPPORTED_INDEX_OPERATION,且不要开启对应 的 feature flag。
    • 对不支持的操作保留基类默认行为,不要把未实现能力声明为已支持。
  • 接入 factory 和 engine 创建路径。

    • src/factory/index_creators.cpp 中添加 creator。
    • register_all_index_creators() 中注册。
    • 对共享字段使用 src/index_common_param.cpp 中的 IndexCommonParam::CheckAndCreate()dtypemetric_typedim、可选 repr、 可选 extra_info_size、allocator、thread pool,以及旧序列化格式兼容信息。
    • src/factory/factory_test.cpp 或实现附近的专项测试中,覆盖可接受名称、非法参数 和不支持的参数形态。
  • 添加构建系统接入。

    • 添加 src/algorithm/<name>/CMakeLists.txt,并从 src/algorithm/CMakeLists.txt 引入。
    • 将新源码加入最近的现有 target,不要创建平行构建路径。
    • 文件后缀保持 .cpp;除非变更本身涉及第三方依赖,否则不要修改 extern/
  • 定义并校验索引参数。

    • 当索引有自己的 schema 时,把实现参数放在 <name>_parameter.{h,cpp} 中。
    • 为序列化/重建参数校验实现 JSON 解析、ToJson()CheckCompatibility()
    • 对非法维度、metric/data type 组合、缺失的必需配置块和未知模式,通过已有的 CHECK_ARGUMENT / VsagException 流程返回 ErrorType::INVALID_ARGUMENT
    • 如果参数对用户可见,更新 docs/docs/{en,zh}/src/resources/index_parameters.md 和对应 索引文档。
  • 明确实现生命周期行为。

    • 决定索引是否支持 Train()Build()ContinueBuild()、build 后 Add()、空索引 Add()
    • 决定是否支持 Remove()UpdateId()UpdateVector()UpdateAttribute()UpdateExtraInfo()
    • 对每一种支持的 mutation,测试空数据集、重复 ID(如适用)、缺失 ID、不可变索引行为以及 mutation 后的搜索正确性。
    • 保持 InitFeatures() 与已经实现的操作一致。
  • 实现搜索行为和结果打包。

    • 支持该索引要求的公开 KnnSearch() 重载,包括声明支持时的 BitsetPtrstd::function<bool(int64_t)>FilterPtr 过滤路径。
    • 如果索引支持新的 request 路径,实现 SearchWithRequest()
    • 一致地返回 Dataset 字段:ID、距离、num_elements、结果维度和可选结果统计信息。
    • 按 HGraph 等现有索引使用的嵌套索引名约定解析搜索参数。
  • 保持序列化兼容。

    • 同时实现 Serialize(StreamWriter&)Deserialize(StreamReader&);基类 InnerIndexInterface 会把它们适配到 BinarySetReaderSet 和 stream。
    • 保存足够的元数据以拒绝不兼容二进制,包括参数兼容性,以及存在 extra info 时的 extra_info_size
    • 当索引同时支持 BinarySetReaderSet 时,添加两条路径的 round-trip 测试。
    • 如果既有索引的二进制格式发生变化,更新兼容性测试并记录迁移路径。
  • 在声明 feature 前补齐测试。

    • 单元测试应覆盖参数解析、build/add/search、序列化、feature flag、已实现的内存估算以及 错误路径。
    • tests/ 下的功能测试应覆盖用户能通过 Factory::CreateIndex() 触达的公开 API 行为。
    • 保持 src/include/ 的 C++ 单元测试覆盖率不低于项目阈值。

可选适配点

仅当新索引确实实现对应行为时才添加这些能力。实现后,在 InitFeatures() 中开启匹配的 IndexFeature,并添加专项测试。

  • Extra info(extra_info / extrainfo)。

    • 通过 IndexCommonParam 解析 extra_info_size
    • 保存来自 Dataset::GetExtraInfos() 的定长逐向量 payload,并在 Build()Add()UpdateExtraInfo() 中校验 Dataset::GetExtraInfoSize()
    • 实现 GetExtraInfoByIds(),并在支持该 feature 时填充搜索结果中的 extra info。
    • 如果索引支持 extra-info 过滤,记录并测试会切换到 Filter::CheckValid(const char*) 的 搜索参数。
    • 参考 docs/docs/zh/src/advanced/extra_info.mdexamples/cpp/320_feature_extra_info.cpp
  • 统计与分析。

    • 为能帮助运维理解索引的静态结构数据实现 GetStats()
    • 只有在需要基于 query 分析时,才实现 AnalyzeIndexBySearch(const SearchRequest&)
    • 当 search-time 指标有价值时,通过 Dataset::Statistics() 附带结果统计。
    • 保持工具输出兼容 tools/analyze_indexdocs/docs/zh/src/resources/analyze_index.md
  • 范围搜索。

    • 必须重写 InnerIndexInterface 要求的纯虚主重载 RangeSearch(..., const FilterPtr&, ...),即使算法不支持范围搜索;不支持时应抛出 UNSUPPORTED_INDEX_OPERATION,且不要开启对应的 feature flag。
    • 只有当算法能遵守 radius 语义和 limited_size 时,才实现其他 RangeSearch() 重载。
    • 测试不限量、限量、带过滤和空结果场景。
    • 参考 docs/docs/zh/src/advanced/range_search.md
  • 过滤器与属性。

    • 只有当各路径已接入搜索时,才支持 BitsetPtrstd::function<bool(int64_t)>FilterPtr
    • 如果支持属性过滤,实现属性存储/更新路径,并记录可接受的属性 schema。
    • 测试 bitset invalidation 语义和 Filter::CheckValid() keep 语义之间的差异。
  • Allocator、resource 和线程集成。

    • 使用 IndexCommonParam::allocator_ 或派生的 allocator-aware 组件分配长期结构。
    • 当 build/search 工作并行化时,使用 Resource thread pool。
    • 确认自定义 allocator 和自定义 thread-pool 示例仍准确描述行为。
    • 只有在 add/search/delete/update 交互已测试后,才标记并发相关 feature。
  • 内存和自省 API。

    • 当索引能报告有意义数字时,实现 EstimateMemory()EstimateBuildMemory()GetMemoryUsage()GetMemoryUsageDetail()
    • 只有在底层存储支持时,才实现 GetMinAndMaxId()CheckIdExist()ExportIDs()GetVectorByIds()GetDataByIds()GetIndexDetailInfos()GetDetailDataByName()
  • 模型导出、clone、merge、tune 和 cache 导入/导出。

    • 当索引可以在不错误共享可变存储的情况下复制时,实现 Clone()ExportModel()
    • 只有在参数兼容性、ID 重映射和删除语义清晰时,才实现 Merge()
    • 只有带有明确参数解析和测试时,才实现 Tune()ExportCache()ImportCache()
  • 绑定、示例、benchmark 和文档。

    • Python 绑定通常只在公开 API surface 变化时需要更新;当前 pyvsag 用户通过名称和 JSON 参数创建索引。
    • 当索引引入新的用户工作流时,在 examples/cpp/ 下添加 C++ 示例。
    • 如果行为可从 pyvsag 触达,在 tests/python/ 下添加 Python 示例/测试。
    • 当 reviewer 需要可重复性能数据时,在 benchs/ 下添加 benchmark YAML。
    • 对用户可见的索引或参数,在 docs/docs/{en,zh}/src/ 下添加英文和中文网站文档。

Review Checklist

  • Factory::CreateIndex()Engine::CreateIndex() 能按文档名称创建索引。
  • CheckFeature() 只对已实现且已测试的行为返回 true。
  • 不支持的操作通过现有 wrapper 返回 UNSUPPORTED_INDEX_OPERATION
  • 序列化 round trip 保留索引声明支持的 ID、向量或压缩码、参数、删除状态、属性和 extra info。
  • 每个支持的生命周期转换后,搜索结果仍然有效。
  • 文档列出用户可见参数、支持的 metric/data type 和不支持的操作。
  • 已运行实际验证:变更代码对应的单元/功能测试,以及文档-only 变更需要的格式检查或 git diff --check

编译构建

VSAG 是一个 C++ 项目,使用 CMake 构建。项目源码使用 C++17 标准编写,请确保你使用的编译器支持 C++17 的语法。我们建议你使用 GCC 9.4.0 或者 Clang 13.0.0 以后的版本,因为这些版本在我们的开发中工作良好。

支持的平台包括 Ubuntu 20.04+、CentOS 7+,以及 Apple Silicon 上的 macOS 14+。 macOS 使用 Xcode Command Line Tools 提供的 Apple Clang 和 Homebrew 依赖;当前支持范围是 arm64 核心 C++ 构建,预编译 C++ 包与 Python wheel 仍以 Linux 为主。首次构建前运行:

./scripts/deps/install_deps.sh

脚本会自动选择 Linux 发行版或 macOS 对应的依赖安装流程。

如果需要启用 Linux io_uring 后端,请安装 liburing,并在直接配置 CMake 时添加 -DENABLE_LIBURING=ON。默认值为 OFF;在非 Linux 平台或未找到 liburing 时, 请求 uring_io 的配置会打印一次性告警并回退到 buffer_io

在 CMake 配置中,有许多参数和编译目标。为了方便使用,我们将常用的编译目标(或命令)写到了 Makefile 中,使用 Unix Makefiles 进行管理,已避免记忆各种配置或者从命令行输入大段参数。这些编译目标(或命令)可以通过在项目根目录运行 make help 查看:

Usage: make <target>

Targets:
help:                    ## Show the help.
##
## ================ development ================
debug:                   ## Build vsag with debug options.
dev:                     ## Build full developer configuration.
test:                    ## Build and run unit tests.
asan:                    ## Build with AddressSanitizer option.
test_asan: asan          ## Run unit tests with AddressSanitizer option.
tsan:                    ## Build with ThreadSanitizer option.
test_tsan: tsan          ## Run unit tests with ThreadSanitizer option.
clean:                   ## Clear build/ directory.
##
## ================ integration ================
fmt:                     ## Format codes.
cov:                     ## Build unit tests with code coverage enabled.
lint:                    ## Check coding styles defined in `.clang-tidy`.
fix-lint:                ## Fix coding style issues in-place via clang-apply-replacements, use it be careful!!!
test_parallel:           ## Run all tests parallel (used in CI).
test_asan_parallel: asan ## Run unit tests parallel with AddressSanitizer option.
test_tsan_parallel: tsan ## Run unit tests parallel with ThreadSanitizer option.
##
## ================ distribution ================
release:                 ## Build vsag with release options.
dist-pre-cxx11-abi:      ## Build vsag with distribution options.
dist-cxx11-abi:          ## Build vsag with distribution options.
dist-libcxx:             ## Build vsag using libc++.
pyvsag:                  ## Build a specific Python version wheel. Usage: make pyvsag PY_VERSION=3.10
pyvsag-all:              ## Build wheels for all supported versions. Usage: make pyvsag-all
clean-release:           ## Clear build-release/ directory.
install:                 ## Build and install the release version of vsag.

编译 VSAG 库

make debug 是我们开发中最常用的命令,它会以开发模式编译整个项目,禁用大多数优化(-O0)并生成调试信息(-g)。该目标默认关闭测试、示例、工具和 Python 绑定;如需同时启用它们,可使用 make dev

在默认设置下,开发模式的编译产物会生成在 ./build/ 目录中。可以通过如下命令运行单元测试:

./build/tests/unittests

以及通过如下命令运行功能测试:

./build/tests/functests

运行测试用例

除了上面提到的方法——编译后手动运行测试用例,VSAG 还支持用一条命令完成编译和运行所有测试:

make test

在我们的开发工作流中,代码修改完成后需要使用上述命令通过所有测试后,才会提交到 GitHub 仓库中。

内存和多线程测试

VSAG 是一个索引库,有大量的内存分配和并行计算的代码。我们依赖 AddressSanitizer 和 ThreadSanitizer 来检查发现内存和多线程的问题。当你在开发过程中遇到可疑的内存问题或者多线程问题,可以使用 make test_asan 或者 make test_tsan 来帮助问题发现。

清除编译工作区

当你在调试第三方库引入,或者 CMake options 时,可能会遇到明明修改了 cmake 文件却没有变化的问题,不妨试试 make clean 指令。它会清除掉 build/ 目录的所有内容,然后你就可以像刚下载的新项目一样从头编译了。

格式化代码

我们使用 clang-format 工具来保持代码风格的统一,对应的配置文件路径是 vsag/.clang-format

make fmt 命令会自动将 VSAG 的源代码格式化。这个命令需要你的环境中安装有 clang-format。GitHub CI 会在每一个 Pull Request 中运行代码风格检查,以保证合并进主分支的代码风格一致。

代码覆盖率统计

make cov 会使用 coverage 参数来编译 VSAG 项目,使得测试用例运行后能够得到代码覆盖率统计文件。

静态代码分析

VSAG 使用 clang-tidy 工具来实现静态代码分析,旨在提前暴露一些编程规范上的问题,对应的配置文件路径是 vsag/.clang-tidy

使用 make lint 可以在本地执行静态代码分析任务。同样地,可以使用 make fix-lint 来自动完成代码修复。

需要注意的是,fix-lint 命令会在源文件上直接修改,请确定你希望这样做!

编译发布模式

在生产环境中,我们需要使用发布模式的 VSAG 库。在此模式下,编译器会尽可能优化代码生成,以实现更好的运行性能。使用以下命令生成发布模式的 VSAG 库:

make release

为了和开发模式的产物区分开,发布模式的产物默认生成在 ./build-release/ 目录中。

如果你需要分发预编译产物,可使用以下目标以控制 ABI:

  • make dist-pre-cxx11-abi:使用 -D_GLIBCXX_USE_CXX11_ABI=0 构建(pre-C++11 ABI);
  • make dist-cxx11-abi:使用 -D_GLIBCXX_USE_CXX11_ABI=1 构建(C++11 ABI);
  • make dist-libcxx:使用 libc++ 代替 libstdc++ 构建。

编译 pyvsag 包

pyvsag 是 VSAG 的 Python 版本。通过 pip install pyvsag 下载安装的 wheel 包就是通过 make pyvsag 命令构建出来的。

默认会使用 PY_VERSION=3.10,你可以显式指定目标 Python 版本:

make pyvsag PY_VERSION=3.11

或者一次构建所有受支持版本的 wheel:

make pyvsag-all

环境变量

在 Makefile 文件的开始可以看到一些 VSAG 编译系统定义的环境变量。这些变量可以通过命令行运行 export 命令,或者在 .bashrc / .zshrc 等 shell 配置文件中设置来修改。

环境变量说明如下:

  • CMAKE_GENERATOR:CMake 内部使用什么来编译项目,默认是 "Unix Makefiles",其他可选值请参考 CMake Generators
  • CMAKE_INSTALL_PREFIX:安装路径,即运行 make install 后头文件和库文件会被安装到哪里,一般不需要修改;
  • COMPILE_JOBS:编译并行度,默认是 6 并行编译,建议设置成你的 CPU 核数以提高编译速度;
  • DEBUG_BUILD_DIR:开发模式产物目录,非必要不修改;
  • RELEASE_BUILD_DIR:发布模式产物目录,非必要不修改;
  • VSAG_ENABLE_INTEL_MKL:是否启用 Intel MKL 作为 BLAS 后端,默认 OFF;关闭时使用 OpenBLAS;
  • VSAG_ENABLE_LIBAIO:是否启用 libaio,默认 ON

离线 / 内网环境构建

VSAG 会在配置 / 构建阶段下载第三方库。在离线或网络受限的环境中,可以设置按依赖的 VSAG_THIRDPARTY_* 环境变量,从本地路径或内网镜像(内网 HTTP 服务、OSS 存储桶等)获取每个 压缩包。完整的变量列表与示例见离线 / 内网环境构建

发布流程

如果要在 GitHub 上手动发布 Release,请到 GitHub Actions 页面运行 Build and Publish Release 工作流,并填写以下参数:

  • branch:要发布的分支、tag 或 commit SHA
  • tag_name:新的发布标签,例如 v1.0.0
  • prerelease:是否标记为预发布版本

如果你想在本地手动执行同样的打包流程,可以运行:

COMPILE_JOBS=6 bash ./scripts/release/dist.sh

如果机器内存足够,可以适当调大 COMPILE_JOBS;默认值会比较保守,以避免 CI 里再次触发 内存不足。

离线 / 内网环境构建

VSAG 在 CMake 配置 / 构建阶段会下载一批第三方库(通过 ExternalProject_AddFetchContent)。在没有外网访问、或网络较慢 / 受限的机器上,这些下载可能失败或 超时。本文介绍如何把每个依赖指向本地路径内网镜像(内网 HTTP 服务、对象 存储、Artifactory 等),从而在完全离线的环境中完成编译。

第三方下载的解析顺序

对每一个需要下载的依赖,VSAG 会构造一个候选 URL 列表,由 CMake 按顺序依次尝试, 命中第一个成功的即停止。以 antlr4 为代表 (extern/antlr4/antlr4.cmake):

set (antlr4_urls
    https://github.com/antlr/antlr4/archive/refs/tags/4.13.2.tar.gz
)
vsag_resolve_thirdparty_override (ANTLR4 4.13.2 antlr4_urls)

ExternalProject_Add (antlr4
    URL ${antlr4_urls}
    URL_HASH MD5=3b75610fc8a827119258cba09a068be5
    ...)

因此解析顺序为:

  1. 当前分支确切依赖标识符对应的固定版本变量(pinned variable) VSAG_THIRDPARTY_<LIB>_<PIN_SUFFIX>
  2. 未带版本的 VSAG_THIRDPARTY_<LIB>,作为已弃用的兼容兜底。如果两者都设置,固定版本变量优先。
  3. 权威的上游 URL(一个或多个,来自 GitHub / 项目发布页)。

VSAG 不提供项目控制的压缩包镜像。如需使用本地或内网镜像,请设置固定版本变量(推荐), 或临时使用已弃用的旧变量。

只会读取当前分支确切 pin 对应的固定版本变量。其他版本的变量可以一直保留在环境中,它们会被 忽略,因此同一个 CI 镜像或 shell 配置可以安全地支持多个 VSAG 分支。

固定版本变量命名

后缀由确切的固定标识符生成:

  • 数字版本会去掉一个开头的 v/V,以及紧挨数字之前的依赖名装饰;ASCII 字母转为大写, 其他字符的连续段替换为一个下划线。因此 10.2.1v10.2.1 得到 10_2_1hdf5_1.14.4 得到 1_14_4yaml-cpp-0.9.0 得到 0_9_0
  • 完整提交哈希得到 COMMIT_<12_HEX>,例如 VSAG_THIRDPARTY_CPUINFO_COMMIT_CA678952A9A8
  • 其他标签或 ref 得到 TAG_<NORMALIZED>_H<12_HEX>。十六进制部分是对大小写敏感的确切 标识符计算 SHA-256 后的大写前缀,所以规范化后相同的不同拼写仍可区分。
  • 同一依赖的两个 pin 若在 12 个十六进制字符处冲突,两边会确定性地逐字符扩展。如果完整哈希 或摘要仍无法区分,配置会失败,而不会接受有歧义的名称。

开始前的关键事项

  • 取值可以是本地路径,也可以是 URL。 支持绝对文件路径 (/data/deps/fmt-10.2.1.tar.gz)、file:// URL,或任意 http(s):// URL—— 包括内网 HTTP 服务或对象存储。
  • 依然会校验压缩包哈希。 每个依赖都声明了 URL_HASH(MD5 或 SHA256)。你镜像 / 本地的压缩包必须与上游压缩包逐字节一致,否则 CMake 会因哈希不匹配而中止。最稳妥的 做法是把上游原始文件下载一次,原封不动地重新托管。
  • 覆盖项在配置阶段读取。 如果你在上一次配置之后修改了变量,请重新执行 CMake 配置或 先运行 make clean,新值才会生效。
  • 请使用非空值,否则就不要设置。 CMake 把“已 export 但为空”的变量视为已定义,因此 export VSAG_THIRDPARTY_FMT_10_2_1= 会把一个空项 prepend 到 URL 列表里,导致下载失败。若要停用 某个覆盖项,请 unset 它,而不要把它设为空字符串。
  • 每个依赖相互独立。 没有单一的全局镜像变量;每个需要的依赖各自设置一个固定版本变量。 你只需为本次构建实际拉取的依赖设置变量(见 我需要哪些依赖?)。
  • 日志中的确认信息。 CMake 会报告依赖、pin、选中的来源类别与变量,以及兜底 / 弃用行为。 由于 URL 可能包含凭据,日志不会打印变量值。

环境变量

main 上的固定版本变量需镜像的上游压缩包何时被拉取
VSAG_THIRDPARTY_JSON_3_11_3nlohmann/json 3.11.3github.com/nlohmann/json/.../v3.11.3.tar.gz始终
VSAG_THIRDPARTY_ANTLR4_4_13_2ANTLR4 runtime 4.13.2github.com/antlr/antlr4/.../4.13.2.tar.gz始终
VSAG_THIRDPARTY_OPENBLAS_0_3_24OpenBLAS 0.3.24github.com/OpenMathLib/OpenBLAS/.../OpenBLAS-0.3.24.tar.gz默认 BLAS 后端(未使用系统库 / MKL 时)
VSAG_THIRDPARTY_CPUINFO_COMMIT_CA678952A9A8pytorch/cpuinfogithub.com/pytorch/cpuinfo/archive/ca678952...tar.gz始终
VSAG_THIRDPARTY_FMT_10_2_1fmt 10.2.1github.com/fmtlib/fmt/.../10.2.1.tar.gz始终(除非使用系统 fmt)
VSAG_THIRDPARTY_THREAD_POOL_COMMIT_3507796E172Dlog4cplus/ThreadPoolgithub.com/log4cplus/ThreadPool/archive/3507796e...tar.gz始终
VSAG_THIRDPARTY_TSL_1_4_0Tessil/robin-map 1.4.0github.com/Tessil/robin-map/.../v1.4.0.tar.gz始终
VSAG_THIRDPARTY_ROARINGBITMAP_3_0_1CRoaring 3.0.1github.com/RoaringBitmap/CRoaring/.../v3.0.1.tar.gz始终
VSAG_THIRDPARTY_CATCH2_3_7_1Catch2 3.7.1github.com/catchorg/Catch2/.../v3.7.1.tar.gzENABLE_TESTS=ON
VSAG_THIRDPARTY_HDF5_1_14_4HDF5 1.14.4github.com/HDFGroup/hdf5/.../hdf5_1.14.4.tar.gzENABLE_TOOLS=ON(且 C++11 ABI)
VSAG_THIRDPARTY_ARGPARSE_3_1p-ranav/argparse 3.1github.com/p-ranav/argparse/.../v3.1.tar.gzENABLE_TOOLS=ON(且 C++11 ABI)
VSAG_THIRDPARTY_YAML_CPP_0_9_0yaml-cpp 0.9.0github.com/jbeder/yaml-cpp/.../yaml-cpp-0.9.0.tar.gzENABLE_TOOLS=ON(且 C++11 ABI)
VSAG_THIRDPARTY_TABULATE_COMMIT_3A58301067BBp-ranav/tabulategithub.com/p-ranav/tabulate/archive/3a583010...tar.gzENABLE_TOOLS=ON(且 C++11 ABI)
VSAG_THIRDPARTY_HTTPLIB_0_35_0cpp-httplib 0.35.0github.com/yhirose/cpp-httplib/.../v0.35.0.tar.gzENABLE_TOOLS=ON(且 C++11 ABI)
VSAG_THIRDPARTY_PYBIND11_2_11_1pybind11 2.11.1github.com/pybind/pybind11/.../v2.11.1.tar.gzPython 绑定(pyvsag / ENABLE_PYBINDS=ON

每个依赖确切的上游 URL 以及期望的 URL_HASH,其唯一权威来源是对应的 extern/<lib>/<lib>.cmake 文件。镜像时(尤其是版本升级后)请以该文件为准。

此处未列出的(不下载,因此无需覆盖):Intel MKL(通过 find_path 在主机上查找)。

我需要哪些依赖?

你只需镜像本次构建实际会下载的依赖:

  • 核心库make debug / make release):JSONANTLR4OPENBLASCPUINFOFMTTHREAD_POOLTSLROARINGBITMAP。 其中两个是条件依赖:当 BLAS 由 Intel MKL(x86_64 且 ENABLE_INTEL_MKL=ON)或系统 OpenBLAS 提供时,OPENBLAS 不会下载;当找到系统 fmt 时,FMT 会被跳过。
  • + 测试make testENABLE_TESTS=ON):另加 CATCH2
  • + 工具ENABLE_TOOLS=ON ENABLE_CXX11_ABI=ON):另加 HDF5ARGPARSEYAML_CPPTABULATEHTTPLIB——仅当两个选项同时开启时才会下载 (见 cmake/VSAGThirdParty.cmake)。
  • + Python wheelmake pyvsag):另加 PYBIND11

示例

A. 内网 HTTP 服务或对象存储(推荐)

将上游压缩包原封不动地重新托管到内网地址,再让每个变量指向它。用一个基础 URL 的 shell 变量可以让配置更简洁:

# 内网镜像,逐字节提供上游压缩包
export VSAG_MIRROR=https://mirror.corp.example.com/vsag-thirdparty

export VSAG_THIRDPARTY_JSON_3_11_3=$VSAG_MIRROR/v3.11.3.tar.gz
export VSAG_THIRDPARTY_ANTLR4_4_13_2=$VSAG_MIRROR/antlr4-4.13.2.tar.gz
export VSAG_THIRDPARTY_OPENBLAS_0_3_24=$VSAG_MIRROR/OpenBLAS-0.3.24.tar.gz
export VSAG_THIRDPARTY_CPUINFO_COMMIT_CA678952A9A8=$VSAG_MIRROR/cpuinfo-ca678952.tar.gz
export VSAG_THIRDPARTY_FMT_10_2_1=$VSAG_MIRROR/fmt-10.2.1.tar.gz
export VSAG_THIRDPARTY_THREAD_POOL_COMMIT_3507796E172D=$VSAG_MIRROR/thread_pool-3507796e.tar.gz
export VSAG_THIRDPARTY_TSL_1_4_0=$VSAG_MIRROR/robin-map-1.4.0.tar.gz
export VSAG_THIRDPARTY_ROARINGBITMAP_3_0_1=$VSAG_MIRROR/CRoaring-3.0.1.tar.gz

make release

对象存储端点的用法完全相同:使用其网络可达的对象 URL 即可。

B. 预先下载的本地文件(完全离线)

在完全没有网络的机器上,先把压缩包拷贝到本机(例如 /data/vsag-deps),再让变量指向本地 文件:

export VSAG_THIRDPARTY_JSON_3_11_3=/data/vsag-deps/v3.11.3.tar.gz
export VSAG_THIRDPARTY_ANTLR4_4_13_2=/data/vsag-deps/antlr4-4.13.2.tar.gz
export VSAG_THIRDPARTY_OPENBLAS_0_3_24=/data/vsag-deps/OpenBLAS-0.3.24.tar.gz
export VSAG_THIRDPARTY_CPUINFO_COMMIT_CA678952A9A8=/data/vsag-deps/cpuinfo-ca678952.tar.gz
export VSAG_THIRDPARTY_FMT_10_2_1=/data/vsag-deps/fmt-10.2.1.tar.gz
export VSAG_THIRDPARTY_THREAD_POOL_COMMIT_3507796E172D=/data/vsag-deps/thread_pool-3507796e.tar.gz
export VSAG_THIRDPARTY_TSL_1_4_0=/data/vsag-deps/robin-map-1.4.0.tar.gz
export VSAG_THIRDPARTY_ROARINGBITMAP_3_0_1=/data/vsag-deps/CRoaring-3.0.1.tar.gz

make release

使用 file:// URL(export VSAG_THIRDPARTY_FMT_10_2_1=file:///data/vsag-deps/fmt-10.2.1.tar.gz) 同样有效。

C. 只覆盖单个依赖

如果只有某一个下载不稳定,只覆盖它即可,其余继续使用默认地址:

export VSAG_THIRDPARTY_OPENBLAS_0_3_24=https://mirror.example.com/OpenBLAS-0.3.24.tar.gz
make release

D. 同时保留多个分支的 pin

这些变量可以共存。后续各发布分支独立回移后,切换分支时会自动选择该分支自己的 pin, 无需修改环境:

export VSAG_THIRDPARTY_OPENBLAS_0_3_23=/data/vsag-deps/OpenBLAS-0.3.23.tar.gz
export VSAG_THIRDPARTY_OPENBLAS_0_3_24=/data/vsag-deps/OpenBLAS-0.3.24.tar.gz
export VSAG_THIRDPARTY_YAML_CPP_0_8_0=/data/vsag-deps/yaml-cpp-0.8.0.tar.gz
export VSAG_THIRDPARTY_YAML_CPP_0_9_0=/data/vsag-deps/yaml-cpp-0.9.0.tar.gz

git switch main   # 选择 OpenBLAS 0.3.24 与 yaml-cpp 0.9.0
make release

备选方案:复用系统库

对于主机上已安装的依赖,你可以直接跳过下载,而不必镜像。设置 VSAG_USE_SYSTEM_DEPS=ON(或按依赖设置 VSAG_USE_SYSTEM_<DEP>=ON)。当前支持系统复用的 依赖列表见 DEVELOPMENT.md

常见问题

  • 哈希不匹配 / 出现 “HASH mismatch” 错误 —— 你镜像或本地的压缩包与上游文件不是逐字节 一致。请重新下载确切的上游压缩包并原样托管,或在 extern/<lib>/<lib>.cmake 中核对期望的 URL_HASH
  • 覆盖项似乎未生效 —— 确认变量是在运行 make / cmake 的同一个 shell 中 export 的, 然后重新执行配置(或 make clean),因为取值是在 CMake 配置阶段读取的。确认配置输出中的 Third-party override 诊断包含预期的 pin 与变量。
  • 仍然在访问网络 —— 多半是漏掉了本次构建会拉取的某个依赖。请对照 我需要哪些依赖? 与你启用的选项(ENABLE_TESTSENABLE_TOOLS、 Python 绑定)逐项核对。

版本可用性

固定版本变量目前在 main 上实现;1.00.180.170.16 的回移需要独立完成, 因为每个分支拥有自己的 pin。已弃用的未带版本覆盖仍保持兼容。它最初由 #1606main 引入,并通过 #2308 回移到发布线。内置 URL 兜底与 复用系统库的行为不变。

运行测试

VSAG 采用 Catch2 作为测试框架,测试分为两类:

  • 单元测试:与源码同目录,位于 src/ 下,聚焦单个类/函数的行为。
  • 功能测试:位于 tests/ 目录,覆盖跨模块、端到端的索引行为。典型用例包括 test_hgraph.cpptest_ivf.cpptest_pyramid.cpptest_sindi.cpptest_brute_force.cpptest_memleak.cpp 等。

构建并运行全部测试

make test 会以 Debug 配置重新编译(启用 ENABLE_TESTS=ON)并运行单元与功能测试:

make test

说明:

  1. 运行 src/ 下的单元测试;
  2. 运行 tests/ 下的功能测试;
  3. make test 并未开启覆盖率(ENABLE_COVERAGE=ON)。需要覆盖率报告时请使用 make cov:该目标仅完成带覆盖率插桩的编译,随后需要手动运行测试二进制以生成报告。

仅运行单个测试二进制

构建完成后,可直接运行单个测试:

./build-debug/tests/functional_tests "[hgraph]"
./build-debug/tests/functional_tests "[hgraph][concurrent]"

Catch2 支持按名字、tag、通配符等方式筛选用例,详见 --help

覆盖率

贡献时应保持 src/include/ 下代码的行覆盖率不低于 90%。在本地执行:

make cov
# 然后运行测试二进制以采集覆盖率,例如:
./build-debug/tests/functional_tests

报告会输出到 build-debug/coverage/ 下,可用浏览器打开 index.html 查看未覆盖的分支。

内存泄漏与多线程

  • test_memleak.cpp:基于 AddressSanitizer / LeakSanitizer,对索引的构造/销毁路径进行验证。
  • test_index/test_index_concurrent.cpp:对声明了相应 feature flag 的在维护索引, 验证共享的并发添加与搜索行为。

Python 测试

tests/python/ 包含 pyvsag 的 pytest 用例。构建好 pyvsag 后:

make pyvsag PY_VERSION=3.10
cd tests/python && pytest -q

参考

  • 功能测试源代码目录:tests/
  • 脚本入口:Makefile 中的 testcovasan 目标

贡献到 VSAG

首先,感谢你愿意花时间为 VSAG 做贡献!正是像你一样的贡献者帮助 VSAG 项目变得更好。🎉

如果你是第一次参与开源项目,我们非常推荐你跟着 这个项目 了解开源贡献的基本流程。

以下是为 VSAG 做贡献你可能需要知道的,了解这些有助于你更加轻松地为此项目做出贡献。

我可以做哪些贡献

  1. 【报告错误】要报告 bug 或者文档问题,请创建 bug issue 并提供问题的详细信息。如果你认为该问题需要被优先关注,请在问题评论中 @ VSAG开发组。

  2. 【提议新功能】要提议新功能,请创建 feature request issue。描述预期的功能,并与 VSAG 开发组和社区讨论设计和实现。一旦 VSAG 开发组同意该计划,就可以按照 贡献流程 来实施它。

  3. 【开发功能或者修复错误】要开发未实现的功能或者修复错误,请遵循 贡献流程 。如果你需要关于这个问题的更多背景信息,可以在该问题上发表评论并 @VSAG开发组。

我该如何贡献

贡献代码

如果你有任何改进 VSAG 项目的地方,请创建你的 pull request!记得在你的 pull request 中引用相关 issue,如果有的话。

贡献流程

我们使用 GitHub Flow 来协作开发 VSAG 项目。了解 GitHub Flow 可以帮助你更快地参与到 VSAG 的社区开发中。

  1. 在 GitHub 上 fork 一个 VSAG 仓库。
  2. 使用 git clone git@github.com:<yourname>/vsag.git 命令将你的 fork 仓库下载到本地计算机。
  3. 使用 git checkout -b my-topic-branch 创建分支。
  4. 在本地进行修改,通过本地检查,创建提交并使用 git push --set-upstream origin my-topic-branch 推送到 GitHub。
  5. 访问 GitHub 网站并创建 pull request。

如果你已有本地仓库,请在开始之前对其进行更新,以最大程度减少产生合并冲突的可能性。

git remote add upstream git@github.com:antgroup/vsag.git
git checkout main
git pull upstream main
git checkout -b my-topic-branch

一些准则

在创建 pull request 前,请确保你的修改通过了本地测试,并且符合 VSAG 编码风格。

  • 在提交新功能时,pull request 需要包含功能测试,以证明你的代码是正常工作的,还可以避免未来的修改意外地破坏了这个功能。
  • 在修复 bug 时,需要添加触发 bug 的测试用例,因为 bug 的存在通常表明测试覆盖不足。
  • 在 VSAG 中修改代码时,要保持 API 的兼容性。
  • 不要在 VSAG 的公开头文件(include/ 目录)中引用内部头文件(src/ 目录)。
  • 当你向 VSAG 项目贡献新功能时,维护成本(默认情况下)会转移给 VSAG 开发组。这意味着我们要考虑贡献的好处和维护的成本。

签署 DCO(Developer Certificate of Origin)

对于本项目的所有贡献必须同意并附带 Developer Certificate of Origin(后简称 DCO) 的确认。对 DCO 的确认和同意必须包含在每一个 Commit Message 中,并采用 Signed-off-by: {{Full Name}} <{{email address}}>(不带 {})的形式。如果贡献者不能或不愿意同意 DCO,其贡献将不会被接收。

贡献者可以通过在 Commit Message 中添加如下 Signed-off-by 行来签署 DCO:

This is my commit message

Signed-off-by: Random J Developer <random@developer.example.org>

Git 还有一个 -s 命令行选项,可以在提交时自动附加 Signed-off-by 行:

git commit -s -m "This is my commit message"

对于借助 AI Coding Agent(如 OpenCode、Claude Code、Codex 等)完成的贡献,仅由人类贡献者 签署 DCO;AI Agent 不得添加自己的 Signed-off-by trailer,因为只有人类才能合法地证明 DCO。每一位人类贡献者仍按常规各自添加自己的 Signed-off-by: trailer。除签名外,请按 Linux 内核 AI Coding Assistants 规范 使用 Assisted-by: trailer 标注 AI 协助,格式为 Assisted-by: AgentName:ModelVersion。 在 trailer 顺序上,请将人类的 Signed-off-by: 放在前面,Assisted-by: 放在其后,例如:

Signed-off-by: Random J Developer <random@developer.example.org>
Assisted-by: OpenCode:claude-opus-4.7

人类提交者需对 AI 生成的修改进行审阅、确保许可证合规,并对该贡献承担全部责任。

Commit 信息与 PR 标签

  • Commit 信息请遵循 Conventional Commits,常用前缀包括 feat:fix:docs:chore:refactor:test:ci: 等;
  • 如果该 commit 无需触发 CI,请将 [skip ci] 放在 commit subject 的开头,例如 [skip ci] docs: fix typo in README
  • 每一个 PR 都必须至少包含以下两类 label(由 Mergify 强制校验,否则无法合并):
    • kind/*:变更类型,可选值为 kind/bugkind/featurekind/improvementkind/documentation
    • version/*:目标版本,例如 version/1.0version/0.18

编码风格

修改 C++ 代码前,请阅读 C++ 编码指南。其中说明了 VSAG 要求的 clang-format 15 和 clang-tidy 15 规则、命名约定、诊断抑制方式以及本地检查命令。

本地测试

VSAG 项目使用 Makefile 提供了方便运行所有测试的命令,请执行并确认所有测试通过:

make test

索引构建与训练

VSAG 把索引构建拆成三个阶段:

  1. Train —— 在样本数据上拟合内部量化器 / 分区器。
  2. Add —— 用训练好的编码器把向量插入索引。
  3. Build —— 一站式包装:在同一份数据上先 TrainAdd

绝大多数用户只需要调用 Build。下面两种情况值得单独说明:

  • Train + 增量 Add 当语料规模大或者数据是分批到达时,可以先用代表性样本训练,再通过 Add 流式追加(无需重建索引)。参考 examples/cpp/311_feature_train.cpp
  • ODescent。 HGraph / Pyramid 的另一种构图算法,采用批量迭代精修而非逐条插入。参考 examples/cpp/312_feature_odescent.cpp

Train API

tl::expected<void, Error> Index::Train(const DatasetPtr& data);

声明位置 include/vsag/index.h。在(通常是抽样的)数据集上训练索引,但写入这些 向量。返回 tl::expected<void, Error>,使用 .has_value() 判断成功与否。

具备实质训练逻辑的索引:HGraphIVFBruteForceWARPPyramid。对它们 来说,Build(data) 会先训练再写入向量 —— 默认 NSW 构图模式下相当于 Train(data) 之后再 Add(data),而当 HGraph / Pyramid 配置 graph_type: "odescent" 时,写入阶段会走 ODescent 的批量构图路径,而不是逐条 Add(见 src/algorithm/ 下的 HGraph::build_by_odescent / Pyramid::Build)。

何时需要单独调用 Train

  • 基础量化器需要训练。能力标志 IndexFeature::NEED_TRAIN 在 HGraph 与 IVF 中可靠反映这一点:HGraph 当 base_quantization_type 不是 fp32 / fp16 / bf16 时设置(src/algorithm/hgraph.cpp:1803);IVF 始终设置 (src/algorithm/ivf.cpp:316),因为其聚类中心必须训练。Pyramid 目前在 InitFeatures()不会设置 NEED_TRAIN,即使其内部 HGraph 量化器需要训练,因此请勿依赖 HasFeature(NEED_TRAIN) 来判断 Pyramid —— 当你选用需要训练的 base_quantization_type 时请显式调用 Train。fp32 / fp16 / bf16 不需要训练(即使调用了 Train 也是无副作用的 空操作)。
  • 希望分多批次写入向量,而不是一次性通过 Build 写完。
  • 希望导出已训练的模型供其他索引实例复用(通过 ExportModel)。

用法:训练一次,流式追加

auto params = R"({
    "dtype": "float32",
    "metric_type": "l2",
    "dim": 128,
    "index_param": {
        "max_degree": 32,
        "ef_construction": 100,
        "base_quantization_type": "sq8"
    }
})";
auto index_result = vsag::Factory::CreateIndex("hgraph", params);
if (!index_result.has_value()) {
    std::cerr << "Create index failed: " << index_result.error().message << std::endl;
    return -1;
}
auto index = index_result.value();

// 第 1 步 —— 在全量(或代表性样本)上训练。
auto train_result = index->Train(base);
if (!train_result.has_value()) {
    std::cerr << "Train failed: " << train_result.error().message << std::endl;
    return -1;
}

// 第 2 步 —— 逐条或小批量追加向量。
for (int64_t i = 0; i < num_vectors; ++i) {
    auto one = vsag::Dataset::Make();
    one->NumElements(1)
       ->Dim(dim)
       ->Ids(ids + i)
       ->Float32Vectors(vectors + i * dim)
       ->Owner(false);
    auto add_result = index->Add(one);
    if (!add_result.has_value()) { /* handle */ }
}

完整示例见 examples/cpp/311_feature_train.cpp

Train / Build / Add 三者对比

调用是否训练量化器?是否写入向量?适用场景
Build(data)是(写入全部 data一次性批量加载:手头已经有完整数据集。
Train(data)之后需要分批写入向量。
Add(data)否(需先 TrainBuild索引已训练后的增量写入。

ODescent:另一种构图算法

HGraph 与 Pyramid 默认使用 NSW 风格 构图 —— 每条向量逐条插入,在插入时通过搜索找到邻居 并建边(graph_type: "nsw")。ODescent(“Optimized NN-Descent”)是另一种实现:先在 完整数据集上初始化一张随机 k-NN 图,然后通过若干轮采样候选交换迭代精修边。

在大批量构建场景下,ODescent 通常能在召回率相当的情况下显著降低构图开销,因为精修循环可以 在数据维度上整齐并行,避免了逐条插入时的单点搜索。

ODescent 的实现位于 src/impl/odescent/odescent_graph_builder.{h,cpp},目前被 HGraphPyramid(构图路径)使用。

在 HGraph 中启用 ODescent

在 HGraph 的 index_param 中加入 graph_type: "odescent"

{
    "dtype": "float32",
    "metric_type": "l2",
    "dim": 128,
    "index_param": {
        "base_quantization_type": "sq8",
        "max_degree": 26,
        "ef_construction": 100,
        "graph_type": "odescent",
        "graph_iter_turn": 10,
        "neighbor_sample_rate": 0.3,
        "alpha": 1.2
    }
}

然后正常调用 Build(data) 即可,无需其他 API 调整。完整示例见 examples/cpp/312_feature_odescent.cpp

ODescent 构图参数

下列键放在 index_param 中,与常规 HGraph 参数并列:

参数默认值(HGraph 模板)说明
graph_type"nsw"设为 "odescent" 启用该构图算法。
graph_iter_turn30精修迭代轮数。值越大图质量越高,但构图越慢。
neighbor_sample_rate0.2每轮迭代中从每个节点邻居采样的比例(用于候选交换)。
alpha1.2多样性剪枝阶段的 α 因子。值越大边越稀疏、多样性越强。
min_in_degree1剪枝后修复阶段所保证的最小入度。
build_block_size10000并行粒度(每个 worker 处理的向量数)。

max_degree 沿用 HGraph 顶层配置,无需在 ODescent 这里重复指定;图的上层会自动使用 max_degree / 2

ODescent vs NSW 如何选择

  • 选 ODescent:已经有完整数据集,并希望充分利用多核机器加速构图。批量精修比逐条插入的 并行度更高。
  • 选 NSW(默认):需要增量构建索引,或希望构图阶段内存占用尽量小,又或者尚未观察到构图 耗时的瓶颈。

两种算法构出的图在查询期完全等价,所有搜索参数(ef_searchpq_rerank 等)保持不变。

参考

HGraph 构建缓存

HGraph 可以从一次构建中导出图邻居信息,并在下一次 Build() 前导入。向量通过稳定的 字符串 Source ID 匹配,因此未变化的数据可以从旧图 warm-start,新数据或已变化数据 仍走正常的 refine 流程。

该流程适用于周期性快照构建,例如每天从大部分重叠的语料重建索引。它是构建加速器, 不是索引序列化格式;可搜索索引仍应使用普通序列化接口持久化。

使用条件

  • 索引类型必须是 HGraph。
  • 流程中的每个 base Dataset 都必须设置 Dataset::SourceID
  • 希望跨构建匹配的逻辑记录必须使用稳定且唯一的 Source ID。
  • 在调用 Build() 前,把缓存导入全新、为空且兼容的 HGraph。
  • 两次构建的维度、度量和存储/量化参数应保持兼容。

不同快照中的 Dataset::Ids 数字 label 可以变化;缓存使用对应的 SourceID 字符串匹配。

首次构建并导出缓存

构建源索引时,为每个向量提供一个 Source ID:

std::vector<std::string> source_ids = load_stable_source_ids();

auto base = vsag::Dataset::Make();
base->NumElements(count)
    ->Dim(dim)
    ->Ids(ids.data())
    ->Float32Vectors(vectors.data())
    ->SourceID(source_ids.data())
    ->Owner(false);

auto index = vsag::Factory::CreateIndex("hgraph", build_params).value();
index->Build(base).value();

std::ofstream cache_out("hgraph.cache", std::ios::binary);
index->ExportCache(cache_out).value();

Build() 执行期间要保持 std::string 数组有效。缓存包含后续构建使用的 Source ID 到邻居信息映射,本身不可直接用于搜索。

Warm-start 下一次构建

创建空的兼容 HGraph,导入旧缓存,再构建新快照:

auto next_index = vsag::Factory::CreateIndex("hgraph", build_params).value();

std::ifstream cache_in("hgraph.cache", std::ios::binary);
next_index->ImportCache(cache_in).value();

auto next_base = vsag::Dataset::Make();
next_base->NumElements(next_count)
    ->Dim(dim)
    ->Ids(next_ids.data())
    ->Float32Vectors(next_vectors.data())
    ->SourceID(next_source_ids.data())
    ->Owner(false);

next_index->Build(next_base).value();

调用 ImportCache() 后,Build() 会自动进入缓存辅助路径。两个快照中都存在的 Source ID 使用缓存邻居 warm-start;未匹配记录作为 cache miss 走正常构建 refine。 缓存辅助 Build() 未设置 Dataset::SourceID 时会返回参数错误。

随索引持久化 Source ID

HGraph 默认不会在序列化中写入 Source ID 元数据。如果反序列化后的索引还需要 调用 ExportCache(),请在 index_param 中设置 persist_source_id: true

{
    "dtype": "float32",
    "metric_type": "l2",
    "dim": 128,
    "index_param": {
        "base_quantization_type": "sq8",
        "max_degree": 32,
        "ef_construction": 400,
        "persist_source_id": true
    }
}

该选项会给序列化索引增加 Source ID 元数据。如果缓存始终从首次内存构建中导出, 恢复后的索引不需要 Source ID 映射,则无需开启。

度量缓存复用

warm-start 构建完成后调用 GetStats(),检查:

字段含义
build_cache_hit_rate构建节点中成功匹配并从导入缓存 warm-start 的比例
build_cache_hit_nodes成功匹配的节点数量
build_cache_missed_nodes没有匹配缓存条目、按正常路径构建的节点数量

如果上一次构建没有使用导入缓存,统计会输出 skipped_reason。其他 HGraph 指标见索引分析

限制与运维建议

  • 必须先 Import 再 Build();缓存辅助构建要求索引为空。
  • 构建缓存依赖兼容的 HGraph 配置;参数发生不兼容变化后应重新生成。
  • deduplicate_storage: true 不能与缓存辅助构建组合。
  • 生产使用前,应和完整重建对比召回率与构建时间。
  • 检查文件流是否成功打开,并处理 API 返回值。缓存文件与可搜索的序列化索引相互独立, 应和生成它的快照一起版本化,或通过原子方式替换。

API 签名见 Index 缓存接口,Source ID 字段见 Dataset

范围搜索

除了 k-近邻搜索(KnnSearch),VSAG 还支持范围搜索RangeSearch):返回所有与查询向量距离 小于或等于指定半径的结果。该接口适用于阈值过滤、去重、近似召回等场景。

基本用法

#include <vsag/vsag.h>

// 1. 构造索引(以 HGraph 为例)
auto index = vsag::Factory::CreateIndex("hgraph", hgraph_build_params).value();
index->Build(dataset);

// 2. 准备查询
auto query = vsag::Dataset::Make();
query->NumElements(1)->Dim(dim)->Float32Vectors(query_vec)->Owner(false);

// 3. 范围搜索
float radius = 0.5f;
auto result = index->RangeSearch(query, radius, search_params);
if (result.has_value()) {
    auto ids = result.value()->GetIds();
    auto dists = result.value()->GetDistances();
    int64_t n = result.value()->GetDim();
    // ...
}

完整示例参见 examples/cpp/302_feature_range_search.cpp

limited_size 参数

RangeSearch 支持通过 limited_size 限制返回结果的最大数量:

// 返回最多 100 条满足半径条件的结果
auto result = index->RangeSearch(query, radius, search_params, /*limited_size=*/100);
  • limited_size = -1(默认):返回所有满足条件的结果(不限)。
  • limited_size > 0:在满足半径条件的候选中返回最多这么多条。
  • limited_size = 0:非法取值,实现中会显式拒绝 (CHECK_ARGUMENT(limited_size != 0, ...))。

与 Filter 组合

RangeSearch 的签名与 KnnSearch 一致,同样支持传入过滤器(见 examples/cpp/301_feature_filter.cpp)。 过滤器在搜索过程中即时生效,而不是事后过滤,效率更高。

支持情况

索引类型支持 RangeSearch
hgraph
ivf
brute_force
sindi稀疏向量场景支持

注意事项

  • 距离度量(内积 / L2 / 余弦)会影响 radius 的语义。请与索引创建时的 metric_type 保持一致。
  • radius 过大时结果集可能巨大,建议配合 limited_size 使用。
  • 对于图类索引(HGraph),RangeSearchef 等运行期参数与 KnnSearch 共享含义。

按 ID 计算距离

除了 KnnSearchRangeSearch,VSAG 还提供了在已建好索引的向量上按 ID 计算距离的 接口,可用于对外部候选集进行重排、召回核验,或在 VSAG 之上构建自定义检索流水线。

接口分为两种形式:

  • CalcDistanceById — 单个 ID,返回单个距离值。
  • CalcDistancesById — 一批 ID,返回一个包含距离数组的 DatasetPtr

每种形式都有两个重载:一个接收 const float*(稠密向量),另一个接收 DatasetPtr (稠密或稀疏均可)。

迁移说明。 CalDistanceById 是批量接口的历史拼写。为兼容已有代码,1.1 迁移窗口内 仍保留该弃用别名;新代码应使用 CalcDistancesById。两者语义完全相同。详情参见 issue #2068

接口概览

// 单个 ID,稠密浮点指针
tl::expected<float, Error>
CalcDistanceById(const float* vector,
                 int64_t id,
                 bool calculate_precise_distance = true) const;

// 单个 ID,DatasetPtr(稠密或稀疏)
tl::expected<float, Error>
CalcDistanceById(const DatasetPtr& vector,
                 int64_t id,
                 bool calculate_precise_distance = true) const;

// 批量 ID,稠密浮点指针
tl::expected<DatasetPtr, Error>
CalcDistancesById(const float* query,
                const int64_t* ids,
                int64_t count,
                bool calculate_precise_distance = true,
                int64_t topk = -1) const;

// 批量 ID,DatasetPtr(稠密或稀疏)
tl::expected<DatasetPtr, Error>
CalcDistancesById(const DatasetPtr& query,
                const int64_t* ids,
                int64_t count,
                bool calculate_precise_distance = true,
                int64_t topk = -1) const;

对于 NumElements() > 1DatasetPtr 查询,请先检查 SUPPORT_BATCH_CALC_DISTANCE_BY_IDcount 表示每个 query 的 ID 数,ids 需要包含 NumElements() * count 个 row-major ID,返回距离使用相同布局。当 topk > 0 时, 每个 query 返回按距离升序排列的 min(topk, count) 个结果,并同时返回对应 ID。

声明位于 include/vsag/index.h

calculate_precise_distance

  • true(默认):尽量使用高精度向量表示(如完整 float32)来计算距离。当索引仅保留 量化编码时,获取精确值可能开销更大。
  • false:可以使用索引内存中已有的量化 / 近似表示,速度更快但距离是近似值。

返回值含义

  • 单 ID 重载返回 float 距离值。
  • topk == -1 时,裸指针批量重载返回一行 count 个距离;DatasetPtr 重载返回 NumElements() 行、每行 count 个距离。两者都保持输入顺序,且不返回 ID。
  • topk > 0 时,批量重载每个 query 返回按距离升序排列的最小 min(topk, count) 个 距离,GetIds() 包含对应 ID。无效 ID(距离为 -1)排在有效距离之后,仅在有效 ID 不足时才出现在结果中。
  • 距离的语义由建索引时设置的 metric_type(IP / L2 / cosine)决定,参见 度量语义

基本用法

#include <vsag/vsag.h>

// 1. 构建 HGraph 索引
auto index = engine.CreateIndex("hgraph", hgraph_build_parameters).value();
index->Build(base);

// 2. 单 ID 距离
auto d = index->CalcDistanceById(query_vector.data(), /*id=*/42);
if (d.has_value()) {
    std::cout << "distance to id 42 = " << d.value() << std::endl;
}

// 3. 批量 ID 距离
std::vector<int64_t> ids = { 1, 2, 3, 4, 5 };
auto result = index->CalcDistancesById(query_vector.data(), ids.data(), ids.size());
if (result.has_value()) {
    const float* dists = result.value()->GetDistances();
    for (size_t i = 0; i < ids.size(); ++i) {
        if (dists[i] == -1.0f) {
            std::cout << ids[i] << " -> 无效 ID" << std::endl;
        } else {
            std::cout << ids[i] << " -> " << dists[i] << std::endl;
        }
    }
}

多查询

当索引公布 SUPPORT_BATCH_CALC_DISTANCE_BY_ID 能力时,DatasetPtr 批量重载可以一次接收 多个 query。候选 ID 和输出都采用 row-major 布局。对于 LazyHGraph,调用会委托给当前生效的 内部索引;即使 wrapper 公布了该能力,实际支持也可能在 phase 切换后改变,因此仍需处理返回错误:

// 两个稠密 query,每个 query 三个候选 ID。
auto queries = vsag::Dataset::Make()
                   ->NumElements(2)
                   ->Dim(dim)
                   ->Float32Vectors(query_vectors.data())
                   ->Owner(false);
std::vector<int64_t> candidate_ids = {
    10, 11, 12,  // query 0 的候选
    20, 21, 22   // query 1 的候选
};

if (index->CheckFeature(vsag::SUPPORT_BATCH_CALC_DISTANCE_BY_ID)) {
    auto result = index->CalDistanceById(queries, candidate_ids.data(), 3, true, /*topk=*/2);
    if (result.has_value()) {
        auto batch = result.value();
        // batch 的 NumElements() == 2,Dim() == 2。
        for (int64_t q = 0; q < batch->GetNumElements(); ++q) {
            for (int64_t j = 0; j < batch->GetDim(); ++j) {
                const int64_t offset = q * batch->GetDim() + j;
                std::cout << batch->GetIds()[offset] << ": "
                          << batch->GetDistances()[offset] << '\n';
            }
        }
    }
}

topk == -1 时,GetDim() 等于 count,位置 q * count + j 对应输入 ID ids[q * count + j]topk 为正数时,行跨度为 GetDim();每行先放最近的有效候选, 只有有效 ID 少于 topk 时才会在行尾保留无效 ID。

可运行的完整示例见 examples/cpp/306_feature_calculate_distance_by_id.cpp

稀疏向量

对于 SINDI 等稀疏向量索引,const float* 重载不适用。需要通过 SparseVectors(...) 把查询 封装为 DatasetPtr,并调用 DatasetPtr 重载:

auto query = vsag::Dataset::Make();
query->NumElements(1)->SparseVectors(&sparse_query)->Owner(false);

auto d = index->CalcDistanceById(query, /*id=*/42);

支持矩阵

索引类型单 ID 稠密重载(const float*单 ID DatasetPtr多查询 DatasetPtr 批量重载说明
hgraph支持支持公布能力时支持遵循 calculate_precise_distance
ivf支持支持公布能力时支持可用性取决于是否保留精确存储。
brute_force支持支持公布能力时支持仅稠密单向量索引。
pyramid支持支持支持
lazy_hgraph支持不支持取决于当前生效的内部索引DatasetPtr 批量调用会委托给当前 BruteForce/HGraph;可用性可能在 phase 切换后改变。
sindi不支持支持支持仅稀疏向量。

对于未实现某重载的索引,调用会返回 UNSUPPORTED_INDEX_OPERATION 错误。

注意事项

  • 稠密重载中,查询向量的维度必须与索引维度一致。
  • 批量重载存在默认实现:循环调用单 ID 接口;部分索引会重写以做批量优化。
  • 与 VSAG 其他只读接口一样,这些方法可以与 KnnSearch 等只读操作并发调用。

带过滤的搜索

带过滤的搜索(Filtered Search)允许在 KnnSearchRangeSearch 中只保留满足应用自定义条件 的向量。当底层索引算法支持时,VSAG 会在图遍历过程中应用该谓词,从而避免“先取 top-k 再丢弃” 所带来的召回率损失与额外延迟。

本文介绍三种基于 id 的过滤 API:

  • 位图过滤(Bitset filter):以向量 id 作为下标的紧凑位数组。
  • 函数回调过滤(Function callback)std::function<bool(int64_t)>
  • Filter 对象:继承自 vsag::Filter 的子类,除了判定逻辑之外还可以向算法暴露 有效占比、分布等提示信息。

如果谓词是结构化字段上的 SQL 风格表达式,请阅读 属性过滤(混合搜索);如果是基于每条向量的不透明字节负载在图内过滤, 请阅读 Extra Info(附加信息)

真值约定

三种 API 关于「这个 id 是否被排除」的语义并不一致,混用前请仔细对照下表。

API方法返回 true 表示…
BitsetTest(id)该 id 被过滤掉
std::functionf(id)该 id 被过滤掉
Filter::CheckValidCheckValid(id)保留该 id

位图与 std::function 两种重载在内部都会被包装为 BlackListFilter (见 src/impl/filter/black_list_filter.cpp):位被置上、或回调返回 true,都表示该 id 被排除。Filter::CheckValid 则相反——返回 true 表示保留。如果你已经维护了一份 「删除 id 位图」,最自然的方式是位图过滤;如果是任意谓词逻辑、并且能提供有效占比等提示, Filter 对象会更合适。

位图过滤

vsag::Bitsetinclude/vsag/bitset.h)是按序号下标的可增长位数组。

auto invalid = vsag::Bitset::Make();
for (int64_t i = 0; i < num_vectors; ++i) {
    if (ids[i] % 2 == 0) {
        invalid->Set(ids[i]);    // 偶数 id 被排除
    }
}

auto search_params = R"({ "hgraph": { "ef_search": 100 } })";
auto result = index->KnnSearch(query, /*topk=*/10, search_params, invalid).value();

位图按向量 id 索引,但查询时 id 会被掩码到低 32 位 (bit_index = id & ROW_ID_MASKROW_ID_MASK = 0xFFFFFFFFLL,见 src/impl/filter/black_list_filter.cpp)。低 32 位相同的两个 id 会在位图中冲突,因此使用 位图过滤时请把 id 控制在 [0, 2^32),否则改用 Filter 对象。位图按 id 索引而非按插入 顺序;如果应用层会复用 id,请自行处理一致性。

函数回调过滤

直接使用 lambda 或 std::function<bool(int64_t)> 即可。回调返回 true 表示该 id 被 排除(内部会被包装成 BlackListFilter):

// 排除偶数 id:返回 true 即被过滤掉。
std::function<bool(int64_t)> drop_even = [](int64_t id) { return id % 2 == 0; };
auto result = index->KnnSearch(query, 10, search_params, drop_even).value();

适合写少量自定义逻辑而不需要继承类的场景。如果你更习惯「返回 true 表示保留」的写法, 请改用 Filter 对象。

Filter 对象

最完整的 API 是 vsag::Filterinclude/vsag/filter.h)。当算法可以利用谓词的额外提示 (如有效占比)时,建议继承它:

class MyFilter : public vsag::Filter {
public:
    bool CheckValid(int64_t id) const override {
        return id % 2 == 1;
    }

    // 谓词通过率的近似估计;搜索算法据此调整候选缓冲区大小,
    // 估计准确可同时改善延迟与召回率。
    float ValidRatio() const override { return 0.5F; }

    // 通过的 id 是否在向量空间中聚集。
    // NONE 表示「无关」;如果谓词与向量位置相关(例如地理标签),用 RELATED_TO_VECTOR。
    Distribution FilterDistribution() const override { return Distribution::NONE; }
};

auto filter = std::make_shared<MyFilter>();
auto result = index->KnnSearch(query, 10, search_params, filter).value();

主要方法:

方法默认实现用途
CheckValid(int64_t id)纯虚必填。返回 true 表示保留该 id。
CheckValid(const char* data)返回 true用于在图内基于 extra_info 字节负载过滤,参见 Extra Info
ValidRatio()1.0F[0, 1] 区间内的有效占比提示。
FilterDistribution()NONENONERELATED_TO_VECTOR
GetValidIds(...)空实现极端选择性谓词下的可选白名单接口。

ValidRatio 估计错误不会导致结果错误,但偏大会增大延迟、偏小会拉低召回率。

重载列表

KnnSearchRangeSearch 都提供四种过滤形态(include/vsag/index.h):

// KnnSearch
index->KnnSearch(query, topk, params);                                    // 不过滤
index->KnnSearch(query, topk, params, BitsetPtr invalid);
index->KnnSearch(query, topk, params, std::function<bool(int64_t)> f);
index->KnnSearch(query, topk, params, FilterPtr filter);

// RangeSearch
index->RangeSearch(query, radius, params, limited_size);                  // 不过滤
index->RangeSearch(query, radius, params, BitsetPtr invalid, limited_size);
index->RangeSearch(query, radius, params, std::function<bool(int64_t)> f, limited_size);
index->RangeSearch(query, radius, params, FilterPtr filter, limited_size);

limited_sizeRangeSearch 返回结果的最大数量:

  • limited_size < 0:不限制(默认 -1)。
  • limited_size == 0:API 会显式拒绝(CHECK_ARGUMENT(limited_size != 0, ...)), 「不限制」请传 -1
  • limited_size > 0:限定结果列表最多这么多条。

也支持迭代式过滤搜索:

vsag::IteratorContext* ctx = nullptr;
index->KnnSearch(query, topk, params, filter, ctx, /*is_last_search=*/false);
// 用同一个 ctx 反复调用;最后一次调用时把 is_last_search 置为 true 以释放上下文。

索引支持矩阵

所有索引类型都接受位图、函数与 FilterPtr 三种形式——内部会把位图与 lambda 自动包装成 FilterPtr。下表中的列对应每个索引登记的能力标志(见 include/vsag/index_features.h),运行时 CheckFeature 返回的也是这些。

索引_KNN_SEARCH_WITH_ID_FILTER_RANGE_SEARCH_WITH_ID_FILTER_KNN_ITERATOR_FILTER_SEARCH
HGraph支持支持支持
IVF支持支持
BruteForce支持支持
Pyramid支持支持
SINDI / WARP支持支持

基于 id 的过滤可在运行时通过 index->CheckFeature(vsag::SUPPORT_KNN_SEARCH_WITH_ID_FILTER)SUPPORT_RANGE_SEARCH_WITH_ID_FILTERSUPPORT_KNN_ITERATOR_FILTER_SEARCH 查询。 SUPPORT_KNN_SEARCH_WITH_EX_FILTER 与本文无关,它对应的是基于 extra_info 字节负载的 过滤,详见 Extra Info

性能要点

  • 谓词越严格(ValidRatio 越小),搜索需要扩展的候选越多。对图索引而言,谓词非常严格时 应同步增大 ef_search,否则当通过率低于约 1% 时召回率会显著下降。
  • HGraph 还提供选择率感知的暴搜回退:在搜索参数里设置 brute_force_threshold(例如 0.01–0.05),当 Filter::ValidRatio() 足够 小时,HGraph 会自动跳过图遍历,对通过过滤的 id 做一次精确暴扫。当谓词非常 严格时,这通常比一味把 ef_search 调到很大更划算。详见 HGraph 索引文档 以及示例 322_feature_hgraph_brute_force_threshold.cpp
  • 位图过滤最快,因为 Test() 只是一次位查询。Filter 对象内若有重逻辑,需注意它会被 调用很多次。
  • RangeSearch 在过滤通过率较高、范围较宽时建议设定一个合理的 limited_size,避免结果 集无界增长。
  • 属性过滤 组合时,使用 SearchRequest 即可,所有启用的过滤项 会按逻辑 AND 连接。

通过 SearchRequest 组合过滤

SearchRequestinclude/vsag/search_request.h)是 SearchWithRequest 的统一入口, 可同时携带位图、Filter 对象与属性表达式,所有启用的过滤项按 AND 连接:

vsag::SearchRequest req;
req.query_                = query;
req.mode_                 = vsag::SearchMode::KNN_SEARCH;
req.topk_                 = 10;
req.params_str_           = R"({ "hgraph": { "ef_search": 200 } })";
req.enable_filter_        = true;
req.filter_               = std::make_shared<MyFilter>();
req.enable_bitset_filter_ = true;
req.bitset_filter_        = invalid;
auto result = index->SearchWithRequest(req).value();

attribute_filter_str_ 字段的语法见 属性过滤

示例

Python 状态

过滤 API 暂未暴露到 Python;examples/python/todo_examples/301_feature_filter.py 是一个空占位文件。当前请使用 C++ API 进行带过滤的搜索。

迭代式搜索

VSAG 支持迭代式搜索(Iterator Search):调用方无需一次性请求 top-k,而是可以分多次、增量地 拉取结果,VSAG 在调用之间保留内部搜索状态。后续调用会从上一次结束的位置继续,返回不重叠的 新结果。

适用场景:

  • 上层应用有外部 rerank 或后过滤逻辑,需要边拉取边判断,直到攒够通过条件的结果。
  • 结果消费是惰性 / 流式的(如分页 UI、服务器端游标)。
  • 最终需要的 k 不确定,需按需扩展。

工作原理

迭代式搜索依赖一个生命周期较长的 IteratorContext 对象,其中保存:

  • 当前的候选堆与已访问位图;
  • 在底层图 / 倒排链上的游标。

首次调用时,如果传入的指针为 nullptr,索引会在内部创建一个 IteratorContext;后续调用复用它, 搜索因此可以“继续“而不是“重新开始“。调用方完成后需要自行 delete 这个 IteratorContext—— 迭代器持有的内部状态由 delete 释放。

is_last_search 标记是可选的:当置为 true 时,索引会把上下文里仍缓存的候选(“discard heap” 中尚未对外返回的部分)作为该次调用的结果一次性输出。如果你需要这部分尾部候选,就发起一次 is_last_search=true 的调用;如果不需要,直接 delete 上下文即可,无需“收尾调用“。注意返回结果 仍会被 k 截断,想拿到全部尾部候选时需要把 k 设得足够大。

基本用法(SearchParam API)

#include <vsag/vsag.h>

// 1. 构造索引(以 HGraph 为例)
auto index = vsag::Factory::CreateIndex("hgraph", hgraph_build_params).value();
index->Build(dataset);

// 2. 准备查询
auto query = vsag::Dataset::Make();
query->NumElements(1)->Dim(dim)->Float32Vectors(query_vec)->Owner(false);

// 3. 以迭代模式配置 SearchParam
nlohmann::json search_parameters = {
    {"hgraph", {{"ef_search", 100}}},
};
std::string param_str = search_parameters.dump();

vsag::SearchParam search_param(
    /*iter_filter_flag=*/true,   // 开启迭代模式
    param_str,
    /*filter=*/nullptr,
    /*allocator=*/&allocator,
    /*iter_ctx=*/nullptr,        // 首次调用:内部自动创建上下文
    /*last_search_flag=*/false);

// 4. 第一页
auto page1 = index->KnnSearch(query, /*k=*/10, search_param).value();

// 5. 后续页:上下文延续,结果与 page1 不重叠
auto page2 = index->KnnSearch(query, /*k=*/10, search_param).value();

// 6. (可选)取出上下文中仍缓存的候选;如果不需要,可跳过本步,
//    清理只依赖第 7 步的 delete。
search_param.is_last_search = true;
auto page3 = index->KnnSearch(query, /*k=*/10, search_param).value();

// 7. 由调用方销毁上下文——这才是真正释放资源的地方。
delete search_param.iter_ctx;

参考示例:examples/cpp/313_feature_search_allocator.cppexamples/cpp/314_feature_hgraph_search_allocator.cpp

另一种写法:显式传入 IteratorContext

更底层的 KnnSearch 重载允许直接传入 IteratorContext*&,VSAG 自身的测试用例 tests/test_index/test_index_search.cpp 即采用这种形式连续调用:

vsag::IteratorContext* iter_ctx = nullptr;

auto r1 = index->KnnSearch(query, k1, param_str, filter, iter_ctx, /*is_last_search=*/false);
auto r2 = index->KnnSearch(query, k2, param_str, filter, iter_ctx, /*is_last_search=*/false);
auto r3 = index->KnnSearch(query, k3, param_str, filter, iter_ctx, /*is_last_search=*/false);

delete iter_ctx;

每次调用都会推进 iter_ctx;多次结果的并集就是按距离顺序、不重叠的延续序列。如果还想取出 上下文中仍缓存的尾部候选,可以在最后再加一次 is_last_search=true 的调用。

SearchRequest API。 SearchRequest 中定义了 enable_iterator_search_ / p_iter_ctx_ / is_last_search_ 三个字段,但仓库内当前的 SearchWithRequest 实现尚未 读取这些字段,无法通过 SearchWithRequest 触发迭代式搜索。在这部分接入完成之前,请使用 上面两种 KnnSearch 形式。

与过滤器组合

迭代式搜索可以与常规过滤器(label filter、attribute filter、bitset filter)组合,典型场景是 “持续迭代直到外部检查通过的结果攒够”:

size_t needed = 50;
std::vector<int64_t> kept;
vsag::IteratorContext* ctx = nullptr;

while (kept.size() < needed) {
    auto page = index->KnnSearch(query, 32, param_str, filter, ctx, /*is_last_search=*/false);
    if (!page.has_value() || page.value()->GetDim() == 0) break;

    for (int64_t i = 0; i < page.value()->GetDim(); ++i) {
        if (external_check(page.value()->GetIds()[i])) {
            kept.push_back(page.value()->GetIds()[i]);
        }
    }
}

// 释放迭代器内部状态;不需要"收尾调用",
// 仅当还想取出上下文里仍缓存的候选时,再加一次 is_last_search=true 的调用。
delete ctx;

HGraph 图索引在迭代模式下还支持一个额外的运行期参数 skip_ratio,用于控制延续搜索时跳过已探索区域 的力度,详见 examples/cpp/314_feature_hgraph_search_allocator.cpp

支持情况

通过 Index::CheckFeature 查询 SUPPORT_KNN_ITERATOR_FILTER_SEARCH 是否被支持:

索引类型是否支持迭代搜索
hgraph
ivf
brute_force
sindi

使用前请在运行时通过 index->CheckFeature(vsag::SUPPORT_KNN_ITERATOR_FILTER_SEARCH) 检查,后续版本 中支持范围可能会扩大。

注意事项

  • 所有权。 IteratorContext 由调用方持有,忘记 delete 会泄漏内部搜索状态(堆、已访问位图、 allocator 临时分配)。资源释放完全依赖 delete,与 is_last_search 无关。
  • 最后一次调用是可选的。 is_last_search = true 不是清理步骤,唯一作用是让索引把上下文里 仍缓存的候选作为该次调用的结果输出(仍受 k 截断)。仅当你需要这些尾部候选时再发起这次调用, 并把 k 设得足够大以避免截断。
  • 参数一致性。 同一个上下文复用期间,不要更换查询向量、距离度量或过滤器——只有保持逻辑上的 同一次查询,迭代结果才有意义。
  • 每次调用的 k k 只作用于单次调用;多次结果互不重叠,每次最多增加 k 条(不足则表示 索引候选已耗尽)。
  • 线程安全。 单个 IteratorContext 不能在多线程间并发使用;不同查询应各自持有独立上下文。

属性过滤(混合搜索)

属性过滤(Attribute Filter),又称混合搜索(Hybrid Search)或带结构化谓词的近邻搜索, 让 KnnSearch / RangeSearch 只返回结构化标签满足某个 SQL 风格表达式的向量。相比 带过滤的搜索 中基于 id 的过滤方式,它能直接表达类似下面的谓词:

category = "electronics" AND price <= 1000 AND multi_in(tag, "promo|new", "|")

而无需写回调代码。VSAG 在向量索引旁额外构建一份属性倒排索引;表达式只解析一次,并在图遍历 过程中完成判定,从而尽早剪除不可能满足条件的候选。

本文中的“混合搜索”指的是向量 + 结构化属性的混合检索,而非存储布局上的混合。

何时选择哪种过滤 API

需求推荐
排除一组已知 id(例如墓碑)位图 / 函数过滤
在 id 上跑用户自定义逻辑Filter 对象
在图内基于每条向量的字节负载过滤Extra Info
命名、有类型的字段上做 AND/OR/IN 判定本文

三者可以同时放进同一个 SearchRequest,按 AND 组合。

索引支持情况

索引构建时启用 use_attribute_filterSearchWithRequest + 属性表达式UpdateAttribute
HGraph支持支持支持
IVF支持支持支持
BruteForce支持支持支持
WARP(稀疏)支持支持支持
SINDI / Pyramid仅支持基于 id 的过滤,详见 带过滤的搜索

启用 use_attribute_filter 后,BruteForce 暂不支持 Remove(如需删除请重建索引)。

属性数据模型

属性按向量定义,组织成 AttributeSetinclude/vsag/attribute.h)。每个属性包含:

  • 名称(字符串);
  • 值类型AttrValueType 枚举);
  • 值列表——所有字段都是多值字段,因此 IN 风格的成员判定能自然适用于标签类字段。

支持的值类型:

enum AttrValueType {
    INT8 = 5,  INT16 = 7,  INT32 = 1,  INT64  = 3,
    UINT8 = 6, UINT16 = 8, UINT32 = 2, UINT64 = 4,
    STRING = 9,
};

字段的 (名称, 类型) 在首次构建/插入时被锁定;后续插入必须保持一致。

构造一个 AttributeSet

auto* category = new vsag::AttributeValue<std::string>();
category->name_ = "category";
category->GetValue() = { "electronics" };

auto* tags = new vsag::AttributeValue<std::string>();
tags->name_ = "tag";
tags->GetValue() = { "promo", "new" };       // 多值字段

auto* price = new vsag::AttributeValue<int32_t>();
price->name_ = "price";
price->GetValue() = { 899 };

vsag::AttributeSet set;
set.attrs_ = { category, tags, price };

Attribute* 的生命周期取决于承载该 AttributeSetDatasetOwner(...) 标志:

  • Owner(true)(默认):DatasetImpl 析构时会 delete 每个 Attribute*delete[] AttributeSet 数组,调用方不要再自行释放。
  • Owner(false)(下文示例所用):调用方保留所有权,需在 Build / Add 返回后自行释放 Attribute*(以及若为堆分配的 AttributeSet 数组)。

同一个 dataset 请只选一种策略,避免双重释放或泄漏。

构建支持属性过滤的索引

index_param.use_attribute_filter 设为 true,可选地在 attr_params 下调整属性 倒排索引参数。

std::string build_params = R"(
{
    "dtype": "float32",
    "metric_type": "l2",
    "dim": 128,
    "index_param": {
        "use_attribute_filter": true,
        "attr_params": {
            "has_buckets": false
        }
    }
}
)";
auto index = vsag::Factory::CreateIndex("hgraph", build_params).value();

has_buckets 控制倒排索引中倒排链的存储布局,不同索引的默认值不同:

索引has_buckets 默认
HGraphfalse
IVFtrue
BruteForcetrue

如果没有性能数据明确指向需要修改,建议保留默认值。

在 Build / Add 时附加属性

Dataset::AttributeSets 接收一个长度等于向量数的 AttributeSet 数组 (include/vsag/dataset.h):

std::vector<vsag::AttributeSet> sets(num_vectors);
for (int64_t i = 0; i < num_vectors; ++i) {
    sets[i] = build_attrs_for_row(i);
}

auto base = vsag::Dataset::Make();
base->NumElements(num_vectors)
    ->Dim(dim)
    ->Ids(ids)
    ->Float32Vectors(vectors)
    ->AttributeSets(sets.data())
    ->Owner(false);

index->Build(base);     // 或 index->Add(base)

通过 SearchRequest 查询

属性过滤目前仅通过 SearchWithRequest 暴露 (include/vsag/search_request.h):

vsag::SearchRequest req;
req.query_                    = query;
req.mode_                     = vsag::SearchMode::KNN_SEARCH;
req.topk_                     = 10;
req.params_str_               = R"({ "hgraph": { "ef_search": 200 } })";
req.enable_attribute_filter_  = true;
req.attribute_filter_str_     =
    "category = \"electronics\" AND price <= 1000 "
    "AND multi_in(tag, \"promo|new\", \"|\")";

auto result = index->SearchWithRequest(req).value();
for (int64_t i = 0; i < result->GetDim(); ++i) {
    std::cout << result->GetIds()[i] << " " << result->GetDistances()[i] << "\n";
}

可同时启用 enable_filter_(提供 FilterPtr)和 enable_bitset_filter_(提供 BitsetPtr), 所有启用的过滤项按逻辑 AND组合。

过滤表达式语法

文法定义见 src/attr/grammar/FC.g4。语法虽然紧凑,但已经能覆盖结构化过滤的常见需求。

逻辑运算符

形式别名
ANDANDand&&
ORORor||
NOT!(expr)
分组(...)

NOT 仅支持前缀写法 !(...)

比较运算符

数值字段:=!=><>=<=。 字符串字段:仅 =!=

数值比较的左侧可以包含算术运算(+ - * /):

(price - discount) <= 100

列表成员判定

提供两种写法。它们使用同一组关键字INNOT_IN,含下方别名),但参数形态不同

方括号中缀形式——配合字面量列表使用:

id IN [1, 2, 3, 4]
category NOT_IN ["electronics", "clothing"]

列表元素必须是 INTEGER 字面量或双引号字符串;文法不接受单引号。

函数式竖线形式——上游已经把候选值拼接成字符串时使用。第二个参数必须是单个用 | 分隔的字符串字面量,第三个(可选)参数是分隔符,必须为 "|"

multi_in(category, "electronics|clothing", "|")
multi_notin(uid, "1961|8669|9090", "|")

函数形式不接受方括号列表(multi_in(field, [...]) 是文法错误);中缀形式也不接受 竖线分隔字符串。

两种形式的别名:IN / in / MULTI_IN / multi_inNOT_IN / not_in / NOTIN / notin / MULTI_NOTIN / multi_notin

对多值字段而言,只要其中任一个值出现在列表中,成员谓词即为真。

字面量

类型示例
整数42-7
浮点3.141.5e-3
字符串"electronics""new"(始终双引号)
引号包裹的整型字符串"123"(在 multi_in 中按字符串处理)

标识符匹配 [a-zA-Z_][a-zA-Z0-9_]*,可以含 .(即 namespace.field 视为同一个标识符)。

注释以 # 开头,到行尾。

示例

# 等值
category = "electronics"

# 数值范围 + 多值字段
price >= 100 AND price <= 1000 AND tag IN ["promo", "new"]

# 取反
!(status = "archived") AND multi_notin(region, "us-east|us-west", "|")

# 比较左侧的算术运算
(end_ts - start_ts) > 3600 AND charge_type = 5

更新属性

调用 index->UpdateAttribute(id, new_attrs)(或同时传入旧属性的重载,可让倒排索引更新更高效):

vsag::AttributeSet new_attrs = build_new_attrs();
auto status = index->UpdateAttribute(/*id=*/123, new_attrs);

向量本身不会改变,只更新倒排索引,后续搜索立即可见新属性值。

性能要点

  • 属性倒排索引的内存占用大致与「字段平均值数量 × 向量数」成正比;字符串字段还要额外占用 与「不同值数量」成正比的字典空间。
  • 谓词越严格,候选越早被剪除,搜索越快;不严格的谓词大致等于无过滤搜索的成本加一个常数开销。
  • 对图索引,谓词非常严格时应同步增大 ef_search,否则可能因存活候选不足而无法收敛。
  • 优先使用 multi_in / IN,避免冗长的 OR 链——倒排索引可以一次扫描完成成员判定。

测试用例参考

最完整的使用示例在测试套件中:

  • tests/test_index.cpp 中的 TestIndex::TestWithAttr:构建属性、用 SearchRequest 查询, 以及 UpdateAttribute 后再次查询。
  • tests/fixtures/data/vector_generator.cpp 中的 generate_attributes:演示如何按程序化方式 构造混合类型的 AttributeSet* 数组。
  • src/attr/expression_visitor_test.cpp:穷举式的语法用例,可作为 DSL 的参考实现。

Python 状态

属性 / 混合搜索 API 目前仅 C++ 可用,pyvsag 暂未提供绑定, examples/python/todo_examples/301_feature_filter.py 是一个空占位文件。

序列化格式

VSAG 索引可通过现有序列化接口进行序列化与反序列化,便于持久化、跨进程共享及分布式部署。

本文介绍 SerializeDeserialize 使用的现有序列化格式。后续引入的 header-first 流式格式见 新序列化格式。两种格式互不兼容。

三种接口

1. BinarySet / ReaderSet

最灵活的方式,把索引拆分为多个命名二进制段。适合用户自己管理存储介质(例如对象存储、KV、分片上传)。

// 保存
vsag::BinarySet bs = index->Serialize().value();
for (const auto& key : bs.GetKeys()) {
    auto binary = bs.Get(key);
    // 写入存储介质
}

// 加载
vsag::BinarySet bs_loaded;
// 从介质中读取每个 key 对应的 Binary 放入 bs_loaded
auto empty = vsag::Factory::CreateIndex("hgraph", build_params).value();
empty->Deserialize(bs_loaded);

ReaderSetBinarySet 类似,但通过用户自定义的 Reader 按需读取,避免一次性加载全部数据, 常用于内存受限或部分反序列化场景。

2. 文件流(std::ostream / std::istream

最简单的方式,将索引整体写入文件或内存流:

std::ofstream out("index.bin", std::ios::binary);
index->Serialize(out);

std::ifstream in("index.bin", std::ios::binary);
empty->Deserialize(in);

3. 自定义写函数(WriteFuncType

对于流式/分块写入的后端,可传入写回调:

index->Serialize([&](const void* buf, uint64_t offset, uint64_t size) {
    // 将 [buf, buf+size) 写入 offset 位置
});

注意事项

  • Deserialize 要求目标索引为索引,并且参数配置与序列化时一致(如 dimmetric_type)。
  • Serialize/Deserialize 保留现有 footer-based 格式。新的 SerializeStreaming 格式是 header-first 格式,需要用 DeserializeStreamingLoad 读取。
  • 跨大版本升级时请关注 版本日志 中的兼容性说明。
  • 示例参考:examples/cpp/318_feature_tune.cppexamples/cpp/401_persistent_kv.cppexamples/cpp/402_persistent_streaming.cpp

新序列化格式

新的序列化格式面向大索引产物和 forward-only 读取场景。它的设计目标是:

  • 让文件从头部开始就是自描述的,读取端无需先 seek 到文件尾部,就能识别 magic、版本、metadata 和 block manifest。
  • 将索引内容拆分成带类型的 TLV block,便于工具检查各部分大小,也便于未来 reader 跳过未知的 non-critical block。
  • 为完整恢复(DeserializeStreaming)和带策略加载(Load)提供统一的流式入口。
  • 提供稳定、可检查的布局,便于调试和运维工具进行可视化。

新的序列化格式与之前的 Serialize/Deserialize 格式不兼容SerializeStreaming 写出的文件 必须用 DeserializeStreamingLoad 读取;Serialize 写出的文件必须用 Deserialize 读取。

使用模型

序列化和反序列化是索引产物的存储、传输路径。SerializeStreaming 将已经构建好的索引写成自描述文件, DeserializeStreaming 在调用方已经知道要创建哪种索引对象时,恢复完整的内存索引。Index::Load 才是 提供搜索服务的加载路径:它从文件 metadata 中创建索引对象,并返回可以直接用于搜索的 IndexPtr

流式序列化

SerializeStreamingDeserializeStreamingLoad 读写 forward-only 的索引文件。该格式面向 较大的索引产物:读取端不需要先 seek 到文件尾部解析 footer,就能从文件头拿到格式版本和 block 清单。 当前 BruteForce、HGraph、IVF、SINDI 和 Pyramid 已实现该路径。

auto index = vsag::Factory::CreateIndex("hgraph", build_params).value();
index->Build(base).value();

{
    std::ofstream out("hgraph.streaming", std::ios::binary);
    index->SerializeStreaming(out).value();
}

auto restored = vsag::Factory::CreateIndex("hgraph", build_params).value();
{
    std::ifstream in("hgraph.streaming", std::ios::binary);
    restored->DeserializeStreaming(in).value();
}

vsag::IndexPtr loaded;
{
    std::ifstream in("hgraph.streaming", std::ios::binary);
    loaded = vsag::Index::Load(in, "{}").value();
}

Static Load

Index::Load 是新 streaming 格式的带策略加载入口。它和 DeserializeStreaming 的区别是:调用方不需要 先创建一个空索引对象。Load 会先读取 streaming metadata,识别序列化文件中的索引类型和 basic_info["index_param"],在内部创建匹配的索引对象,然后按照 load parameters 加载后续 TLV body blocks。

std::ifstream in("hgraph.streaming", std::ios::binary);
vsag::LoadParameters load_parameters(R"({"base_io_type":"block_memory_io"})");
auto loaded = vsag::Index::Load(in, load_parameters).value();

返回值是可直接使用的 IndexPtr,因此它是把索引加载起来并提供搜索服务时优先使用的路径。load parameters 用来控制已支持 block 的加载策略;参数对象既可以从 JSON 字符串构造,也可以通过 SetReader 携带 reader 对象。不支持的策略会返回错误,不会静默 fallback。当前该 API 支持 streaming BruteForce、HGraph、IVF、SINDI 和 Pyramid 索引。其中 BruteForce 支持有限的 block placement 策略; HGraph 支持通过 precise_readerhigh_precision_codes 绑定到外部 reader;IVF 的两种 精排 codes 布局也都支持外部 reader;SINDI 和 Pyramid 目前会把写出的 streaming blocks 加载到内存。

文件布局

流式文件由固定头部和一组 TLV block 构成:

magic("vsagstm0")
format_version
metadata_length
metadata_json
metadata_checksum
block_header + block_payload
block_header + block_payload
...
section_end

metadata JSON 中保存索引名称、基础索引信息和 block manifest;构建参数保存在 basic_info["index_param"] 中。manifest 描述预期的 block tag、block version,以及该 block 是否 critical。未知 critical block 会导致反序列化失败;未知 non-critical block 可以被兼容 reader 跳过。

TLV Block 版本兼容

format_version 描述 streaming 文件整体结构,例如固定头、metadata 布局和 TLV framing。当某个 block payload 的二进制语义发生不兼容变化时,不应直接升级整体格式,而应升级对应 TLV block 的 block_version。例如 HGraph 的 base_codes payload 如果因为 basic_flatten_codes 实现变化而无法被旧 reader 正确解析,就需要升级 base_codes block version。

每个可独立演进的 block 都需要区分两类版本信息:

  • 当前写出版本:当前代码序列化该 block 时写入的 block_version
  • 支持读取版本:当前代码能读取的该 block 版本集合或版本范围。

reader 读取 TLV header 后,先检查 tag + block_version 是否被当前代码支持:

  • 支持的版本按对应 block reader 继续解析 payload。
  • 不支持的 critical block 直接返回错误,避免旧代码误读新格式。
  • 不支持的 non-critical block 使用 value_len 跳过 payload,并继续读取后续 block。

因此,后续如果某个 block 从 v1 升级到 v2,不能只把当前写出版本改成 v2,还需要同步维护该 block 的支持读取版本。如果新代码仍保留 v1 reader,则 supported versions 应包含 v1 和 v2,这样 v2 代码仍可读取 v1 索引;如果不再支持 v1,则应显式从 supported versions 中移除,并让读取旧 critical block 失败。

metadata 中的 block manifest 用来让工具和 reader 在读取 body 前知道预期 block 版本;真正解析 body 时, TLV header 中的 block_version 仍是每个 payload 的权威版本。

BruteForce Blocks

BruteForce 按顺序写入以下 streaming blocks:

Block内容是否必需
attribute_filter开启属性过滤时写入的可选属性过滤索引条件必需
base_codes暴力搜索使用的 flatten codes
label_table外部 label 和 label remap

DeserializeStreaming 会恢复完整的内存 BruteForce 索引。Load 当前要求 base_codes 加载到内存中; 必需的 BruteForce codes 不支持 reader-based 加载。

HGraph Blocks

HGraph 按顺序写入以下 streaming blocks:

Block内容是否必需
label_table外部 label、label remap、可选 source id table
base_codes图搜索使用的 base flatten codes
bottom_graph覆盖全部向量的底层图
high_precision_codesreorder 使用独立精排 codes 时的高精度 codes条件必需
route_graphs所有上层 route graph
extra_info可选 extra info 数据条件必需
attribute_filter可选属性过滤索引条件必需
raw_vector可选原始向量存储条件必需

DeserializeStreaming 会恢复完整的内存索引。Load 默认把 HGraph blocks 加载到内存中;如果 load parameters 中设置 precise_io_type,可以覆盖 precise_codes 的 IO 类型。如果同时提供 precise_reader,并且该 reader 大小与 high_precision_codes payload 大小一致,Load 会校验该外部 reader 的 payload checksum,然后将 reorder codes 绑定到该 reader。

IVF Blocks

IVF 按顺序写入以下 streaming blocks:

Block内容是否必需
ivf_bucket倒排列表使用的 bucket datacell 数据
ivf_partition_strategypartition strategy 状态,例如已训练的中心点
label_table外部 label 和 label remap
high_precision_codesIVF reorder 使用 flat 布局时的精排 codes条件必需
ivf_precise_bucketIVF 使用 bucket 布局时与倒排桶对齐的精排 codes条件必需
attribute_filter开启属性过滤时写入的可选属性过滤索引条件必需

Load 可以把任一种精排 codes block 绑定到外部 reader。设置 precise_io_type: "reader_io" 并提供 precise_readerflat 布局的 reader 必须恰好 对应 high_precision_codes payload,bucket 布局则对应 ivf_precise_bucket payload。 Load 会先校验 reader 的大小和 checksum,再返回可查询的索引。 precise_enable_read_cacheprecise_cache_total_size 用于配置远程精排 codes 的通用读缓存。

DeserializeStreaming 会恢复完整的内存 IVF 索引。Index::Load 可以直接从 streaming metadata 创建 IVF 索引对象。默认情况下会把 IVF blocks 加载到内存;使用 reader 的精排 codes 会保留在外部。两种精排 codes block 互斥,由 precise_codes_layout 选择。

SINDI Blocks

SINDI 按顺序写入以下 streaming blocks:

Block内容是否必需
sindi_windowssparse term windows 和量化运行时状态
label_table外部 label 和 label remap
sindi_rerank_indexrerank 开启时的可选 rerank flat index条件必需
sindi_term_id_mapper可选 term-id remap 表条件必需
sindi_host_metadata可选 host ID 及每个 host 的一个或多个 inner-ID 区间条件必需

DeserializeStreaming 会恢复完整的内存 SINDI 索引。Index::Load 可以直接从 streaming metadata 创建 SINDI 索引对象,当前会把写出的 SINDI blocks 都加载到内存中。mutable 与 immutable SINDI 运行态均支持该 streaming 路径。旧版序列化格式保持不变,不保存 host metadata。

SINDI_V2 Blocks

SINDI_V2 按顺序写入以下 streaming blocks:

Block内容是否必需
sindi_v2_term_layoutterm-first posting 布局、文档数量和量化状态
label_table外部 label 和 label remap
sindi_rerank_indexrerank 开启时的可选 rerank index条件必需
extra_info可选逐文档 extra information条件必需
sindi_term_id_mapper可选 term-id remap 表条件必需
sindi_host_metadata可选 host ID 及每个 host 的一个或多个 inner-ID 区间条件必需

DeserializeStreamingIndex::Load 可以从这些 blocks 恢复 mutable 或 immutable SINDI_V2。 旧版序列化格式保持不变,不保存 host metadata。

Pyramid Blocks

Pyramid 按顺序写入以下 streaming blocks:

Block内容是否必需
label_table外部 label 和 label remap
base_codes图搜索使用的 base flatten codes
high_precision_codesreorder 开启时的精排 codes条件必需
pyramid_hierarchieshierarchy 名称和 graph roots

DeserializeStreaming 会恢复完整的内存 Pyramid 索引。Index::Load 可以直接从 streaming metadata 创建 Pyramid 索引对象,当前会把写出的 Pyramid blocks 都加载到内存中。

可视化流式索引

构建工具后,传入 streaming index 文件:

cmake --build build --target visualize_index
build/tools/visualize_index/visualize_index \
  --index_path /tmp/vsag-hgraph-streaming.index \
  --html /tmp/vsag-hgraph-streaming.html

CLI 输出包含按真实字节比例展示的 raw horizontal layout,以及高密度的 logical-block layout。HTML 输出会把相关的小 segment 聚合展示,例如 TLV header 与 payload,并在表格中保留精确 segment 明细。

streaming serialization 和 Index::Load 的可运行示例见 examples/cpp/403_persistent_streaming_load.cpp

内存管理

VSAG 在关键路径上大量使用自定义 AllocatorResource,允许用户:

  • 接入业务侧已有的内存池;
  • 对索引内存占用进行度量与上限控制;
  • 在多进程 / NUMA 环境下精细分配内存来源。

自定义 Allocator

class MyAllocator : public vsag::Allocator {
public:
    std::string Name() override { return "my_allocator"; }
    void* Allocate(size_t size) override;
    void Deallocate(void* p) override;
    void* Reallocate(void* p, size_t size) override;
    // ...
};

auto allocator = std::make_shared<MyAllocator>();
auto resource = std::make_shared<vsag::Resource>(allocator, /*thread_pool=*/nullptr);
auto engine = vsag::Engine(resource);

auto index = engine.CreateIndex("hgraph", build_params).value();

完整示例参见 examples/cpp/201_custom_allocator.cpp

搜索路径上的临时 Allocator

KnnSearch / RangeSearch 支持为单次搜索注入临时 Allocator,用于在线程局部的 arena 中分配工作区, 避免与全局堆竞争:

vsag::SearchParam search_param;
search_param.allocator = thread_local_allocator.get();
auto result = index->KnnSearch(query, k, search_param);

示例:examples/cpp/313_feature_search_allocator.cppexamples/cpp/314_feature_hgraph_search_allocator.cpp

估算与查询内存占用

EstimateMemory(data_num)

Index::EstimateMemory(data_num) 返回索引在插入 data_num 条向量后预期占用的字节数。它仅基于 构建参数(dim、量化方式、max_degree 等)推算,不会分配任何向量存储,因此可以在空索引上安全 调用,是入库前评估节点规格的推荐方式:

if (index->CheckFeature(vsag::SUPPORT_ESTIMATE_MEMORY)) {
    uint64_t estimated = index->EstimateMemory(1'000'000);  // 字节
}

完整示例:examples/cpp/308_feature_estimate_memory.cpp

EstimateBuildMemory(num_elements)

Index::EstimateBuildMemory(num_elements) 返回构建 num_elements 条向量的索引时构建过程中 所需的预估内存(字节数)。与 EstimateMemory(估算最终索引的稳态大小)不同,该接口考虑了构建 过程中仅临时存在的缓冲区与中间数据结构。构建期间的峰值内存通常高于构建完成后的内存占用:

uint64_t peak = index->EstimateBuildMemory(1000000);  // 字节

目前维护中的索引均未提供有效实现;调用方需处理默认的“不支持操作”异常。

GetMemoryUsage()

Index::GetMemoryUsage() 返回索引当前占用的字节数:

uint64_t bytes = index->GetMemoryUsage();

特性:

  • 所有索引类型均实现了该方法,但只有通过 CheckFeature 公布 vsag::SUPPORT_GET_MEMORY_USAGE 的索引才保证返回有意义的数值。HGraph、IVF、BruteForce、Pyramid、WARP 均声明了该能力 (见 src/algorithm/{hgraph,ivf,brute_force,pyramid,warp}.cpp);SINDI 出于 接口纯虚函数的要求实现了该方法,但当前未设置该 feature flag,请仅把返回值视为参考信息。
  • 线程安全;可与搜索并发轮询。
  • 延迟在微秒量级 —— 适合生产环境的实时内存监控。
  • 统计的是索引自身占用的内存(向量、图、量化器状态)。该值通常小于操作系统层面观察到的 RSS: RSS 还包含 allocator 的开销、临时 scratch buffer、以及索引外部持有的数据(例如用户自有的输入 向量缓冲)。SINDI 索引尤其建议在构建完成之后调用 GetMemoryUsage() 才能拿到具有代表性的 数值。

可运行示例:examples/cpp/319_feature_get_memory_usage.cpp,其中包含一个辅助函数将接口值与进程 驻留内存进行对照。

GetMemoryUsageDetail()

Index::GetMemoryUsageDetail() 返回索引当前内存占用按组件的细分:

std::unordered_map<std::string, uint64_t> detail = index->GetMemoryUsageDetail();
for (const auto& [component, bytes] : detail) {
    std::cout << component << ": " << bytes << " bytes\n";
}

返回的 map 的 key 为组件名,value 为对应内存字节数。该接口有助于了解索引内部的内存分布。

目前仅 HGraph 提供了有效实现,返回的组件包括 basic_flatten_codesbottom_graphroute_graphneighbors_mutexpoollabel_tablehigh_precise_codesextra_infosraw_vector。SINDI 返回空 map,其他索引类型默认抛出异常。

能力标志

标志含义
vsag::SUPPORT_ESTIMATE_MEMORY支持 EstimateMemory(data_num)
vsag::SUPPORT_GET_MEMORY_USAGE支持 GetMemoryUsage()

两个标志均可通过 index->CheckFeature(...) 查询 —— 参见 索引自省

线程池

Resource 也接受用户提供的 ThreadPool,与 Allocator 配合可完全托管并行度与资源归属。见 examples/cpp/203_custom_thread_pool.cpp

注意事项

  • 自定义 Allocator 必须是线程安全的。
  • Allocator 生命周期必须覆盖所有引用它的索引与结果对象。
  • 若未显式指定,VSAG 会创建一个默认的基于 malloc 的 allocator。

搜索路径 Allocator

VSAG 提供一个与索引自身 allocator 解耦的 per-call Allocator 注入点,适合:

  • 把单次查询的内存与索引长期持有的堆隔离开;
  • 在高并发在线场景下,每个线程绑一个 thread-local arena,彼此之间没有原子争用;
  • 独立于索引地核算或限制每次查询的内存占用。

这个 Allocator 通过两个入口暴露:SearchRequest::search_allocator_(推荐)和旧版 SearchParam::allocator但具体有多少搜索路径真正消费这个 allocator,取决于索引与入口的实现。 目前只有 HGraph::SearchWithRequestsearch_allocator_ 端到端贯通了(既用于临时缓冲,也用 于结果 Dataset);其它 SearchWithRequest 实现(IVF / BruteForce / WARP)只在部分临时 状态上使用 search_allocator_,结果 Dataset 仍由索引自身的 allocator 分配。详见下文 与索引 Allocator 的关系

适用范围。 Allocator 注入目前只通过 KnnSearchSearchParam 重载)和 SearchWithRequest 暴露。RangeSearch 没有携带 Allocator 的重载; SearchRequest::search_allocator_ 也不会被 range-search 路径读取。

推荐 API —— SearchRequest::search_allocator_

#include "vsag/search_request.h"

vsag::SearchRequest req;
req.query_ = query;
req.mode_ = vsag::SearchMode::KNN_SEARCH;
req.topk_ = 10;
req.params_str_ = R"({"hgraph":{"ef_search":100}})";
req.search_allocator_ = thread_local_allocator.get();  // 可选,可为 nullptr

auto result = index->SearchWithRequest(req).value();

SearchRequestinclude/vsag/search_request.h)是当前未废弃、推荐用来驱动单次搜索的入口。 search_allocator_ 字段是可选的,留空时索引会回退到它所属 Resource 上的 allocator。

可用性。 Index::SearchWithRequest 默认实现会返回 不支持 错误。目前只有 HGraph、 IVF、BruteForce、WARP、SINDI 和 Pyramid 实现了它。Pyramid 支持 KNN 请求搜索,并可通过 expected_labels_ 生成 reasoning 报告;携带 expected labels 的 Range 请求暂不支持。对于 尚未 override 的索引(SINDI_V2),请使用下文的旧版 SearchParam 路径。

旧版 API —— SearchParam::allocator(已弃用)

#include "vsag/search_param.h"

nlohmann::json search_params = {{"hgraph", {{"ef_search", 100}}}};
std::string param_str = search_params.dump();

vsag::SearchParam search_param(/*iter_filter=*/false,
                               param_str,
                               /*filter=*/nullptr,
                               /*allocator=*/thread_local_allocator.get());
auto result = index->KnnSearch(query, /*k=*/10, search_param).value();

SearchParaminclude/vsag/search_param.h 中以文档注释的形式标注为已弃用 (“Use SearchRequest instead”),仅为源码兼容保留。注意当前只是注释层面的弃用 —— struct 本身并没有 C++ [[deprecated]] 属性,编译器不会发出弃用告警;但新代码如果所用索引已支持 SearchRequest/SearchWithRequest,仍应优先使用该路径。 examples/cpp/314_feature_hgraph_search_allocator.cpp(HGraph)展示了旧版形式。

结果所有权

结果 Dataset 的所有权契约取决于具体实现 SearchWithRequest 的索引:

  • HGraph 是目前唯一把 request.search_allocator_ 贯通到 create_fast_dataset 的索引 (见 src/algorithm/hgraph.cppctx.alloc = request.search_allocator_)。其结果 Dataset 被标记为 Owner(true, allocator),析构时会自动用该 allocator 释放 ids / distances
  • IVF / BruteForce / WARP 当前用 create_fast_dataset(..., allocator_) 构造结果,即索引 自身的 allocator(src/algorithm/ivf/ivf.cppsrc/algorithm/bruteforce/bruteforce.cpp; WARP 使用 BruteForce 的 WARP 模式实现)。这些路径上 request.search_allocator_ 只会被部分 临时缓冲读取,结果缓冲仍由索引 allocator 持有。在这些索引上请把结果 Dataset 的生命周期 视为绑定到索引 allocator。

实际意义:

  • 不要手动 Deallocate 结果缓冲。Dataset 离开作用域即可;同时手动 Deallocate(...) 与析构器释放会触发双重释放,属于未定义行为。
  • 持有结果的那个 allocator 必须比结果 Dataset 活得更久。 HGraph 上是 per-search allocator;IVF / BruteForce / WARP 上是索引 allocator(索引活着它就活着)。
  • examples/cpp/314_feature_hgraph_search_allocator.cpp 目前显式地 Deallocate。 这是早期 API 迭代遗留的写法;针对当前 owner-tracking 行为的新代码应改为依赖 Dataset 析构器。

最简单的安全模式是「一线程一 allocator,批与批之间 reset」:

ArenaAllocator arena;       // thread-local,足以容纳一批

for (const auto& q : batch) {
    vsag::SearchRequest req;
    req.query_ = q;
    req.topk_ = topk;
    req.params_str_ = params;
    req.search_allocator_ = &arena;
    auto result = index->SearchWithRequest(req).value();
    consume(result);
    // result Dataset 在这里析构;arena 通过自己的 Deallocate 释放 ids/distances。
}
arena.reset();              // 一次性回收本批所有 per-query 缓冲

与索引 Allocator 的关系

场景使用的 allocator
索引构建、插入、持久状态Resource 的 allocator(未传入则使用默认 allocator)。
HGraph::SearchWithRequest 的临时缓冲与结果 Dataset已设置 search_allocator_ 时使用它,否则使用 Resource 的 allocator。HGraph 是目前唯一把 search_allocator_ 贯通到结果的索引。
IVF / BruteForce / WARP SearchWithRequest 的结果 Dataset始终使用索引自身的 allocator(allocator_)。目前消费 search_allocator_
IVF / BruteForce / WARP SearchWithRequest 的部分临时状态设置 search_allocator_ 时会用它分配部分临时缓冲,否则使用索引 allocator。
KnnSearch(query, k, SearchParam)(旧版)在支持 SearchParam::allocator 的索引上(如 HGraph 示例)使用该 allocator,否则使用 Resource allocator。
KnnSearch(query, k, parameters_str)无 per-search Allocator 入口,统一使用 Resource 的 allocator。
RangeSearch(...)(所有形态)使用 Resource 的 allocator;没有 per-search Allocator 入口。

设置 per-search Allocator 不会影响索引的永久数据结构。它只是收窄了某一次搜索调用所触碰内存的 生命周期 —— 且仅限于索引/入口实际消费它的那部分(详见各行说明)。

约束

  • allocator 只有在跨线程共享时才必须线程安全;thread-local arena 不需要内部同步。
  • allocator 的生命周期必须超过它产生的每一个结果 Dataset
  • Reallocate(nullptr, size) 必须等价于 Allocate(size)。VSAG 的内部容器依赖该契约。

可运行示例

  • examples/cpp/314_feature_hgraph_search_allocator.cpp —— HGraph(sq8)+ 自定义 allocator。

参见 内存管理 了解索引级 Allocator / Resource 的设置,以及 过滤搜索 了解如何在 SearchRequest 中同时使用 per-search Allocator 与 自定义过滤器。

索引自省

VSAG 提供三类自省 API,让调用方可以发现某个索引的能力、对已有向量计算距离,以及读出关于已构建 索引的结构化信息,而无需重新执行一次搜索:

  • CheckFeature(IndexFeature) —— 运行时能力探测。
  • CalcDistancesById(...) —— 计算 query 到已存入向量 id 的距离。
  • GetIndexDetailInfos() / GetDetailDataByName(...) —— 读取索引各项结构化详情数据。

这些 API 均为只读操作,可与搜索并发调用。

能力探测 —— CheckFeature

当底层索引实现公布了某项能力时,index->CheckFeature(vsag::SUPPORT_*) 返回 true。当代码路径 持有一个具体类型未知的 IndexPtr(例如用户配置注入、多态存储)时,应使用此 API:

if (index->CheckFeature(vsag::SUPPORT_ESTIMATE_MEMORY)) {
    uint64_t est = index->EstimateMemory(100'000);
}

if (not index->CheckFeature(vsag::SUPPORT_DELETE_BY_ID)) {
    // 跳过 / 通过另一个索引以 remove + re-add 方式回退。
}

能力标志几乎覆盖了库中所有可选接口:build / add / 序列化变体、各种并发组合、度量类型、属性 过滤、extra-info 过滤、CloneExportModelTune 等。完整枚举见 include/vsag/index_features.h

可运行示例:examples/cpp/307_feature_check_features.cpp

到已有 id 的距离 —— CalcDistancesById

CalcDistancesById 计算 query 与索引中已存在的一个或多个向量之间的距离,无需执行一次 搜索。它适用于 re-rank、A/B 评估、ground-truth 校验,或对已知候选集合做成对距离计算。

提供两个重载:

// 稠密向量索引(HGraph、BruteForce、IVF)
auto r = index->CalcDistancesById(query_ptr, ids, count, /*calculate_precise_distance=*/true);

// 稀疏向量索引(SINDI、SINDI_V2)—— 用 Dataset 封装查询
auto query_ds = vsag::Dataset::Make();
query_ds->NumElements(1)->SparseVectors(/* ... */);
auto r = index->CalcDistancesById(query_ds, ids, count, /*calculate_precise_distance=*/true);

裸指针重载返回一行 count 个距离。对于公布 SUPPORT_BATCH_CALC_DISTANCE_BY_ID 能力的 索引,DatasetPtr 重载可以处理多个 query,此时候选 ID 和距离是 NumElements() * count 的 row-major 矩阵。无效 ID 对应 -1.0F。传入正数 topk 时,每行会返回按距离排序的最近候选及其 ID。完整布局和支持矩阵见 按 ID 计算距离

calculate_precise_distance

末尾的 bool 参数在精度与延迟之间做取舍:

取值行为
true(默认)使用全精度向量表征。在内存-磁盘混合索引上可能引发磁盘 I/O。
false使用搜索路径缓存的量化 / 近似表征。更快、无 I/O。

可运行示例:examples/cpp/306_feature_calculate_distance_by_id.cpp

详情数据 —— GetIndexDetailInfos / GetDetailDataByName

GetIndexDetailInfos() 返回一组 IndexDetailInfo 记录,描述索引可对外暴露的每一项命名结构化 数据。每条记录包含 namedescription 和一个 type 枚举,后者用于选择 DetailData 上的 合适访问器。

是否支持取决于索引类型 —— 这两个 API 没有专门的 SUPPORT_* flag。Index 基类默认抛 std::runtime_error("Index doesn't support ...")GetIndexDetailInfosGetDetailDataByName,见 include/vsag/index.h:658,674);HGraph / IVF / BruteForce / Pyramid / SINDI / WARP 通过 InnerIndexInterface 提供了实现。调用时请始终处理 tl::expected 的 error 分支。

auto infos = index->GetIndexDetailInfos().value();
for (const auto& info : infos) {
    std::cout << info.name << " : " << info.description << '\n';
}

知道哪些项可用后,调用 GetDetailDataByName(name, info) 获取对应类型的数据:

vsag::IndexDetailInfo info;
auto detail = index->GetDetailDataByName(vsag::INDEX_DETAIL_NAME_NUM_ELEMENTS, info).value();
int64_t n = detail->GetDataScalarInt64();

detail = index->GetDetailDataByName(vsag::INDEX_DETAIL_NAME_LABEL_TABLE, info).value();
auto table = detail->GetData2DArrayInt64();   // [row][col] int64 矩阵

detail = index->GetDetailDataByName(vsag::INDEX_DETAIL_DATA_TYPE, info).value();
std::string dt = detail->GetDataScalarString();

数据类型

info.type 决定 DetailData 上哪一个访问器有效:

IndexDetailDataType访问器
TYPE_SCALAR_INT64GetDataScalarInt64()
TYPE_SCALAR_DOUBLEGetDataScalarDouble()
TYPE_SCALAR_BOOLGetDataScalarBool()
TYPE_SCALAR_STRINGGetDataScalarString()
TYPE_1DArray_INT64GetData1DArrayInt64()
TYPE_2DArray_INT64GetData2DArrayInt64()

include/vsag/index_detail_info.h 中以常量形式给出的标准详情名:

常量典型类型含义
INDEX_DETAIL_NAME_NUM_ELEMENTSTYPE_SCALAR_INT64索引当前包含的向量数。
INDEX_DETAIL_NAME_LABEL_TABLETYPE_2DArray_INT64逐向量的 label 表(如内部 id ↔ 用户 id 映射)。
INDEX_DETAIL_DATA_TYPETYPE_SCALAR_STRING底层向量数据类型(如 "float32")。

具体索引可能额外暴露其他名称;运行期通过 GetIndexDetailInfos() 遍历即可发现。可运行示例: examples/cpp/317_feature_get_detail_data.cpp

注意事项与限制

  • CheckFeature 是常数时间复杂度。相比对不支持的调用做 try / catch,应优先使用它。
  • CalcDistancesById 要求底层索引保留足够信息以重新计算距离。对于纯量化索引(不保留原始向量), 即使传入 calculate_precise_distance = true,也可能返回量化距离。
  • GetIndexDetailInfosGetDetailDataByName 是只读快照。返回的数值反映调用瞬间的索引状态, 并发修改可能使其失效。

可扩展性

VSAG 暴露了一组稳定的 C++ 扩展点,方便应用接入自有基础设施而无需 fork 库本身。 本页梳理 哪些可以扩展哪些不可以,并给出可运行示例的链接。

公开扩展点

扩展点头文件用途
vsag::Allocatorvsag/allocator.h自定义内存分配策略。
vsag::Loggervsag/logger.h把 VSAG 日志重定向到你的日志体系。
vsag::ThreadPoolvsag/thread_pool.h复用外部线程池执行 build 和 IO。
vsag::Filtervsag/filter.hKnnSearch / RangeSearch 提供自定义预过滤器。
vsag::Reader(含 ReaderSetvsag/readerset.h自定义反序列化的 IO 后端。

这五个都是抽象基类。每个至少声明一个必须实现的纯虚方法;部分还声明了带默认实现的非纯虚方法 (例如 Filter::CheckValid(const char*)Filter::ValidRatio()Filter::FilterDistribution()Filter::GetValidIds(),以及 Reader::MultiRead()),只在需 要自定义行为时才需要 override。实现必须的方法、用 std::shared_ptr 包装(或在 API 要求时 直接传裸指针),然后交给 VSAG 即可。

把扩展接入索引

主要有两条接入路径。

1. 通过 Engine 注入按索引生效的资源

vsag::Enginevsag/engine.h)是绑定自定义 AllocatorThreadPool 的 推荐方式,绑定后由它创建的每个索引都会共享这些资源:

auto allocator   = std::make_shared<MyAllocator>();
auto thread_pool = std::make_shared<MyThreadPool>();
vsag::Resource resource(allocator, thread_pool);
vsag::Engine engine(&resource);

auto index = engine.CreateIndex("hgraph", parameters).value();
// ... 使用索引 ...
engine.Shutdown();

Engine(Resource*) 接收的是 non-owning 裸指针(见 include/vsag/engine.h:38-42):调用者必须保证 Resource(连同它持有的 allocator 与 thread pool)的生命周期长于 engine 以及 engine 创建的所有索引。 Engine::Shutdown() 释放 engine 内部资源,但不会销毁外部的 ResourceResource 提供两个构造器(include/vsag/resource.h:45,59-60):既可以传裸 Allocator* / ThreadPool*(生命周期由调用者管理),也可以传 shared_ptr 重载,让 Resource 共享所有权。完整的所有权模型见 内存管理, 把 allocator 收敛到单次搜索调用的用法见 搜索路径 Allocator

如果只是想快速跑通,Engine::CreateDefaultAllocator()Engine::CreateThreadPool(num_threads) 会返回开箱即用的实现。

2. 通过 Factory::CreateIndex 传裸 allocator

vsag::Factory::CreateIndex(name, params, allocator)vsag/factory.h)接受 一个可选的 Allocator*。这条路径不接受线程池,新代码建议改用 Engine

Filter

实现 vsag::Filter,通过 SearchRequest::filter_(或已弃用的 SearchParam::filter)传入 FilterPtr 即可。使用 SearchRequest 时,必须 同时把 enable_filter_ 设为 true,filter 才会真正生效 (见 include/vsag/search_request.h:113,123)。只有 CheckValid(int64_t id) 是必须实现的,其他都是可选的优化钩子:

  • CheckValid(const char* data):基于向量 extra info 过滤。
  • ValidRatio():向规划器提示选择度。
  • FilterDistribution():返回 NONE(默认)或 RELATED_TO_VECTOR,声明有效 id 的分布是否与向量在底层存储中的位置相关 (见 include/vsag/filter.h:27-30)。
  • GetValidIds(...):对于选择度极低的过滤器,提供预先计算好的有效 id 列表。

可运行示例:examples/cpp/301_feature_filter.cpp。过滤接入的细节见 过滤搜索

Reader / ReaderSet

Index::Deserialize(const ReaderSet&)include/vsag/index.h:810)允许通过 per-stream 的 Reader 从任意存储后端(本地文件、对象存储、远程文件系统…) 反序列化索引。至少实现 ReadAsyncReadSize 三个方法;MultiRead 是 可选的,当底层支持批量 IO 时能显著提升吞吐。vsag::Factory::CreateLocalFileReader 是本地文件的参考实现。

如何构造 ReaderSet序列化类型;完整的 序列化/反序列化矩阵见 序列化

Logger

VSAG 使用全局唯一的 logger,通过 Options 单例配置:

class MyLogger : public vsag::Logger { /* 实现 Trace/Debug/Info/... */ };
static MyLogger my_logger;
vsag::Options::Instance().set_logger(&my_logger);

logger 指针的所有权 归 VSAG —— 必须在所有 VSAG 调用期间保持其存活。 传入 nullptr 则回退到内置 logger。

可运行示例:examples/cpp/202_custom_logger.cpp

通过 Options 进行全局调参

vsag::Options::Instance()vsag/options.h)是进程级单例,承载与具体索引 无关的设置:

接口默认值备注
set_num_threads_building(n)4构建磁盘索引使用的线程数。
set_block_size_limit(bytes)128 MiB单次分配 block 的最大值,必须 ≥ 256 KiB(见 src/options.cpp:53-57)。
set_direct_IO_object_align_bit(bits)9Direct-IO 对齐位数,必须 ≤ 21(见 src/options.cpp:40-46)。
set_logger(Logger*)内置见上文 Logger

这些 option 对进程内所有索引生效,建议启动时设置一次。它们 不会 覆盖 HGraph 的 build_thread_count 等具体索引参数。

哪些 提供公开扩展接口

以下能力目前 没有 稳定的公开扩展接口:

  • 量化器(Quantizer)。 具体量化类型(SQ8、PQ、RaBitQ…)通过索引参数选择, 不支持用户代码继承扩展。
  • 距离计算器 / 距离类型。 每个索引的可选 metric 固定为 l2ipcosine
  • 索引内部的 DataCell / IO / 存储后端。 这些都是实现细节。若需要自定义 IO,请在反序列化边界使用 Reader 接口。

如果你的场景需要上述任一能力,请提 issue 描述使用场景。

关于 vsag::ext

vsag/vsag_ext.h 提供了一组基于 handle 的精简 API(IndexHandlerDatasetHandlerBitsetHandler ……),用于 语言绑定 / FFI,并不是面向最终用户的扩展层。 C++ 应用应直接使用标准的 vsag::Index API。

相关示例

  • examples/cpp/201_custom_allocator.cpp
  • examples/cpp/202_custom_logger.cpp
  • examples/cpp/203_custom_thread_pool.cpp
  • examples/cpp/301_feature_filter.cpp
  • examples/cpp/401_persistent_kv.cpp

图索引增强

图类索引在“困难查询“(与真实近邻连通性较弱)下可能出现召回率下降。 VSAG 通过 Conjugate Graph(共轭图)机制对这类查询进行在线/离线修补,在几乎不增加索引体积的 情况下显著改善尾部召回。

启用共轭图

构建时开启:

{
    "index_param": {
        "base_quantization_type": "fp32",
        "max_degree": 32,
        "ef_construction": 400,
        "use_conjugate_graph": true
    }
}

搜索时通过搜索参数 JSON 中的 use_conjugate_graph_search 字段控制是否启用 (KnnSearch 并不存在额外的布尔参数重载):

std::string search_param_json = R"({
    "hgraph": {
        "ef_search": 100,
        "use_conjugate_graph_search": true
    }
})";
auto result = index->KnnSearch(query, k, search_param_json);

工作原理

已知困难查询的精确最近邻标签时,可调用 Feedback(query, k, search_parameters, global_optimum_id);省略 global_optimum_id 时, HGraph 会通过精确扫描计算。离线增强可调用 Pretrain(base_ids, k, search_parameters), 它会在选定的 float32 基础向量及其近邻之间生成查询并反馈失败路径。两个方法均返回新插入的 共轭边数量,重复反馈返回 0。

共轭图保存从局部结果标签到已知全局最优标签的映射,并在 HGraph 常规遍历后补充候选。 use_conjugate_graph 默认关闭;未开启时调用 FeedbackPretrain 会返回 UNSUPPORTED_INDEX_OPERATION。索引启用共轭图后,搜索增强默认开启,也可按次搜索关闭。

示例

examples/cpp/304_feature_enhance_graph.cpp 给出了从构建、训练到对比召回率的完整流程。

适用场景

  • 数据分布存在稀疏簇或离群点;
  • 对 P99 召回敏感的在线场景;
  • 希望在不重建索引的前提下小幅提升召回。

注意事项

  • 启用后构建时间会略有增加。
  • 共轭图数据会随索引一并序列化。
  • UpdateId 会同时更新 HGraph 标签表与共轭边。
  • Pretrain 当前支持 float32 HGraph;Feedback 支持 HGraph 的全部数据类型。
  • Tune 可以叠加使用,分别作用于路由质量与运行期参数。

Extra Info(附加信息)

extra_info 是与每条向量一同存放在索引内部的、定长的不透明字节负载。它允许把少量与向量配对的 非向量元数据(例如时间戳、类目 id、权限标签、应用自定义字段)直接保存在向量旁边,从而:

  • 通过向量 id 直接获取元数据,无需额外的 KV 存储。
  • 在不重新插入向量的前提下,原地更新某条向量对应的元数据。
  • 在搜索过程中基于元数据过滤候选,而不是事后再过滤搜索结果。

VSAG 把该负载视为原始字节流,其内存布局、序列化与解释完全由用户自行决定。

索引支持情况

各索引支持的操作如下:

  • HGraph:支持 Build/Add 时存入、GetExtraInfoByIdsUpdateExtraInfouse_extra_info_filter,以及在搜索结果中返回 extra info。
  • LazyHGraph:两个阶段都支持与 HGraph 相同的能力。flat 阶段由 BruteForce 提供能力, 转换后 graph 阶段由 HGraph 提供能力。
  • BruteForce:支持 Build/Add 时存入、GetExtraInfoByIdsUpdateExtraInfouse_extra_info_filter,以及在搜索结果中返回 extra info。
  • IVFSINDI:支持 Build/Add 时存入 extra info,但不提供获取、更新、 extra-info 过滤或在搜索结果中返回 extra info。

extra_info_size > 0 时,HGraph、LazyHGraph 和 BruteForce 会注册相关能力标志位。 运行时可通过 index->CheckFeature(...) 进行检查。

启用 Extra Info

在创建索引的参数中,添加顶层整型字段 extra_info_size,其值为每条向量预留的字节数。索引一旦 建立,该大小即被固定,并随索引一同序列化。

{
    "dtype": "float32",
    "metric_type": "l2",
    "dim": 128,
    "extra_info_size": 12,
    "index_param": {
        "base_quantization_type": "sq8",
        "max_degree": 26,
        "ef_construction": 100
    }
}

LazyHGraph 也使用顶层 extra_info_size;LazyHGraph 自身参数仍放在 lazy_hgraph 对象中:

{
    "dtype": "float32",
    "metric_type": "l2",
    "dim": 128,
    "extra_info_size": 12,
    "lazy_hgraph": {
        "transition_threshold": 1000,
        "hgraph": {
            "base_quantization_type": "sq8",
            "max_degree": 26,
            "ef_construction": 100
        }
    }
}

未设置 extra_info_size 或将其设为 0 即表示禁用该特性。

在 Build / Add 时提供 Extra Info

通过 Dataset 的链式接口绑定字节缓冲区。该缓冲区必须连续,第 i 条向量的负载位于 i * extra_info_size 字节偏移处。

auto base = vsag::Dataset::Make();
base->NumElements(num_vectors)
    ->Dim(dim)
    ->Ids(ids.data())
    ->Float32Vectors(vectors.data())
    ->ExtraInfos(extra_infos.data())   // 总长度 num_vectors * extra_info_size 字节
    ->ExtraInfoSize(extra_info_size)   // 必须与索引的 extra_info_size 完全一致
    ->Owner(false);

index->Build(base);   // 或 index->Add(base)

ExtraInfoSize 必须和索引创建时的 extra_info_size 完全相等,否则调用会被拒绝。

获取 Extra Info

在搜索结果中获取

extra_info_size > 0 时,支持该能力的索引会在结果 Dataset 中填入每个返回 id 对应的 字节负载:

auto result = index->KnnSearch(query, k, search_params).value();
const char* infos = result->GetExtraInfos();
auto info_size = result->GetExtraInfoSize();

请使用 info_size 计算返回缓冲区中的偏移。

通过 ID 批量获取(GetExtraInfoByIds

调用方需要预先分配 count * extra_info_size 字节的缓冲区:

if (index->CheckFeature(vsag::SUPPORT_GET_EXTRA_INFO_BY_ID)) {
    std::vector<char> out(count * extra_info_size);
    index->GetExtraInfoByIds(ids, count, out.data());
}

若该能力未开启,调用会返回 UNSUPPORTED_INDEX_OPERATION

原地更新 Extra Info

无需触碰向量本身,即可更新单条向量的负载:

if (index->CheckFeature(vsag::SUPPORT_UPDATE_EXTRA_INFO_CONCURRENT)) {
    auto upd = vsag::Dataset::Make();
    upd->NumElements(1)
       ->Ids(&id)
       ->ExtraInfos(buffer.data())
       ->ExtraInfoSize(extra_info_size)
       ->Owner(false);
    index->UpdateExtraInfo(upd);
}

数据集必须只包含一条记录,且大小必须匹配。

基于 Extra Info 过滤

在过滤命中率较低的场景下,事后过滤会浪费大量计算。HGraph 与 LazyHGraph 可以在图遍历过程中 对每个候选向量直接调用用户定义的过滤器并传入其 extra_info 字节,从而让被过滤掉的候选不进入 结果集。LazyHGraph 在转换前也支持同样的字节负载过滤,此时 flat 阶段会执行精确扫描。

  1. 重写 vsag::Filter 中接收字节缓冲区的版本:

    class CategoryFilter : public vsag::Filter {
    public:
        CategoryFilter(uint32_t lo, uint32_t hi) : lo_(lo), hi_(hi) {}
        bool CheckValid(int64_t /*id*/) const override { return true; }
        bool CheckValid(const char* data) const override {
            uint32_t category_id;
            std::memcpy(&category_id, data, sizeof(category_id));
            return category_id >= lo_ && category_id <= hi_;
        }
        float ValidRatio() const override { return 0.5F; }
    private:
        uint32_t lo_, hi_;
    };
    
  2. 在搜索参数中的 hgraph 块开启 use_extra_info_filter,并把过滤器传入 KnnSearch

    std::string search_params = R"({
        "hgraph": {
            "ef_search": 100,
            "use_extra_info_filter": true
        }
    })";
    auto filter = std::make_shared<CategoryFilter>(3, 7);
    auto result = index->KnnSearch(query, k, search_params, filter).value();
    

use_extra_info_filtertrue 时,搜索路径会调用 CheckValid(const char*) 而不是 CheckValid(int64_t)。可使用 index->CheckFeature(vsag::SUPPORT_KNN_SEARCH_WITH_EX_FILTER) 进行能力检查。

LazyHGraph 说明

  • 创建 LazyHGraph 索引时必须配置 extra_info_size;该字段不放在 lazy_hgraphhgraph 对象内部。
  • flat 阶段写入的 extra info 会在转换时迁移到内部 HGraph。
  • GetExtraInfoByIdsUpdateExtraInfo、搜索结果返回 extra info,以及 use_extra_info_filter 在转换前后都可用。
  • 序列化 LazyHGraph 时会保留当前阶段和已存储的 extra info。

能力标志

  • vsag::SUPPORT_GET_EXTRA_INFO_BY_ID:支持 GetExtraInfoByIds
  • vsag::SUPPORT_UPDATE_EXTRA_INFO_CONCURRENT:支持线程安全的 UpdateExtraInfo
  • vsag::SUPPORT_KNN_SEARCH_WITH_EX_FILTER:搜索时支持 use_extra_info_filter

注意事项与限制

  • 负载是不透明的字节流,序列化/反序列化由用户负责,库内部仅按偏移复制。
  • extra_info_size 在 Build 时即被固定,并写入序列化后的索引。
  • 存储开销为 extra_info_size * num_elements 字节;支持该存储统计的索引会将其计入 EstimateMemory
  • 请尽量保持负载紧凑,因为 extra-info 过滤时会读取该负载。
  • 该特性目前仅提供 C++ 接口,未提供 Python 绑定。

示例

完整的可运行示例位于 examples/cpp/320_feature_extra_info.cpp,演示了在 HGraph 上启用 extra_info、按 id 获取、extra-info 过滤搜索以及原地更新等用法。

索引生命周期管理

索引构建完成后,VSAG 提供一组用于原地修改索引或从已有索引派生新索引的操作。本页文档化完整的 生命周期接口:

  • Remove —— 按 id 删除向量。
  • UpdateVector / UpdateId —— 修改已有向量或重命名其 id。
  • Clone —— 对已有索引进行深拷贝。
  • ExportModel —— 将训练好的模型导出为空索引以复用。

每个操作均为可选项。带有专用能力标志的操作应通过 index->CheckFeature(...) 检查;Remove 没有专用标志,必须按索引类型和 RemoveMode 判断。

能力标志

操作能力标志HGraphPyramidIVFBruteForceSINDI
Remove(暂无专用标志,参见下文)RemoveMode::MARK_REMOVERemoveMode::MARK_REMOVERemoveMode::MARK_REMOVE
UpdateVectorSUPPORT_UPDATE_VECTOR_CONCURRENT
UpdateIdSUPPORT_UPDATE_ID_CONCURRENT
CloneSUPPORT_CLONE
ExportModelSUPPORT_EXPORT_MODEL

对于带能力标志的操作,请在调用前通过 index->CheckFeature(vsag::SUPPORT_*) 在运行时进行检查; 不支持的索引会返回 UNSUPPORTED_INDEX_OPERATIONRemove 目前未提供专用能力标志,是否可用 (以及支持哪种模式)参见下一节。

删除向量

Remove 按 id 删除向量。HGraph 支持两种删除模式,要求不同:

  • RemoveMode::MARK_REMOVE(默认):仅通过 label table 写入墓碑标记,不依赖 support_force_remove 即可调用。该 id 会在后续搜索中被过滤掉,但底层图节点与向量存储仍然保留。
  • RemoveMode::FORCE_REMOVE:物理重写图并回收存储槽。该模式仅在索引以 index_paramsupport_force_remove: true 构建时可用。该开关会启用 force remove 路径及其额外同步; 若索引未带 support_force_remove: true 构建,调用 FORCE_REMOVE 会失败。
{
    "dtype": "float32",
    "metric_type": "l2",
    "dim": 128,
    "index_param": {
        "base_quantization_type": "sq8",
        "max_degree": 16,
        "ef_construction": 100,
        "support_force_remove": true
    }
}

上述 JSON 仅在打算使用 FORCE_REMOVE 时是必需的。若只用 MARK_REMOVE,可以省略 support_force_remove 字段。

{
    "dtype": "float32",
    "metric_type": "l2",
    "dim": 128,
    "index_param": {
        "base_quantization_type": "sq8",
        "max_degree": 16,
        "ef_construction": 100
    }
}
// 提供单 id 与批量两种重载。
index->Remove(id);
index->Remove(std::vector<int64_t>{id1, id2, id3});

删除模式

可选的 RemoveMode 参数用于选择删除策略:

模式行为
RemoveMode::MARK_REMOVE(默认)对 id 打墓碑标记;速度快,不收缩、不修图。后续搜索会跳过该 id。不要求 support_force_remove: true
RemoveMode::FORCE_REMOVE物理删除向量并修复图结构。开销较大。要求索引以 support_force_remove: true 构建。

Remove 返回成功删除的 id 数量。原本不存在的 id 会被静默跳过,不计入返回值。

可运行示例:examples/cpp/303_feature_remove.cpp

Pyramid 与 IVF 删除

Pyramid 和 IVF 都只支持 RemoveMode::MARK_REMOVE:操作写入墓碑、让后续检索排除对应 id,但不会 物理回收向量存储。两者均不支持 FORCE_REMOVE

BruteForce 删除

BruteForce 也支持这两种模式,但它不使用 HGraph 的 support_force_remove 配置。 MARK_REMOVE 会写入墓碑并保留向量存储。FORCE_REMOVE 不需要额外配置:它会物理删除条目,必要时 移动最后一个内部槽位,同时保持外部 id 不变。

BruteForce 处于非 multi-vector 模式且启用 use_attribute_filter: true 时,不支持任一种删除模式;如需删除属性过滤数据,请重建索引。

在 multi-vector 模式下,BruteForce 仅支持 RemoveMode::MARK_REMOVE,不支持 FORCE_REMOVE;即使启用 use_attribute_filter: true 也是如此。

BruteForce 默认使用 block_memory_io。每个成功的物理删除批次都会将逻辑向量和可选额外信息存储收缩到 存活条目数,并释放末尾完整的块;最后一个未填满的块会保留。标签表会在存活条目数不超过当前容量一半时 回收底层分配,以避免反复复制整张表。GetMemoryUsage() 会反映紧凑后的索引分配量。这不保证进程分配器 会立即将内存归还给操作系统,也不保证 RSS 降低。物理删除是同步操作,会获取 BruteForce 的全局独占锁:它会等待进行中的搜索,并在执行期间阻止新搜索;因此应批量传入 id,避免在延迟敏感路径上使用。

index->Remove(std::vector<int64_t>{id1, id2}, vsag::RemoveMode::FORCE_REMOVE);

更新向量与 id

UpdateVector

UpdateVector(id, new_base, force_update = false) 在原地替换已有 id 对应的向量数据。默认的 force_update = false 模式会做连通性检查:若新向量距离原向量较远(这会破坏图质量),更新会被 拒绝,调用方应当退回到 Remove + Add 方案。

std::vector<float> new_vec(dim);  // 填入替换向量
auto upd = vsag::Dataset::Make();
upd->NumElements(1)->Dim(dim)->Ids(&id)->Float32Vectors(new_vec.data())->Owner(false);

auto status = index->UpdateVector(id, upd, /*force_update=*/false);
if (status.has_value() && *status) {
    // 已原地更新
} else if (status.has_value() && not *status) {
    // 被拒绝:新向量距离原向量太远 —— 退回到 remove + add
    index->Remove(id);
    index->Add(upd);
}

force_update 置为 true 会跳过检查并强制更新;请谨慎使用,可能损失召回率。

上述连通性检查适用于 HGraph。BruteForce 也支持使用维度相同的 FP32 向量进行 UpdateVector,但它 没有图连通性检查,因此 force_update 不会改变更新行为。Pyramid 和 IVF 不支持 UpdateVector

UpdateId

UpdateId(old_id, new_id) 重命名已有 id 而不动底层向量。成功返回 true,若 old_id 不存在 或 new_id 已被占用则返回 false

index->UpdateId(123, 456);

结合 UpdateVectorRemoveAdd 的可运行示例:examples/cpp/305_feature_update.cpp

多个索引可经共享的 label table 路径调用 UpdateId,但只有声明 SUPPORT_UPDATE_ID_CONCURRENT 的索引承诺对应的并发更新语义。

克隆索引

Clone() 对整个索引做深拷贝 —— 包括向量、图、量化器状态与元数据 —— 返回一个独立的 IndexPtr。该克隆体可独立于源索引进行搜索、修改或序列化。

auto cloned = index->Clone().value();

// 克隆完成后,两个索引返回的搜索结果完全一致。
auto r1 = index->KnnSearch(query, k, params).value();
auto r2 = cloned->KnnSearch(query, k, params).value();

Clone 还可选传入自定义 Allocator,使克隆索引使用与源不同的内存区 —— 便于把索引交给拥有 自己内存分配器的线程或组件。分配器细节参见 内存管理

可运行示例:examples/cpp/309_feature_clone.cpp

导出训练模型

ExportModel() 返回一个保留了源索引全部训练状态(量化码本、聚类中心、超参数)但不含任何 向量的空索引。这是在多个分片、进程或主机之间共享预训练模型而无需重新训练的标准做法。

auto exported = index->ExportModel();
if (not exported.has_value()) {
    // 索引不支持 ExportModel —— 处理错误
    return;
}
auto model = *exported;

// 向空模型注入一批新的(可与源不同的)向量。
for (int64_t i = 0; i < num_vectors; ++i) {
    auto one = vsag::Dataset::Make();
    one->NumElements(1)->Dim(dim)->Ids(ids + i)
       ->Float32Vectors(vectors + i * dim)->Owner(false);
    model->Add(one);
}

返回的索引行为上等同于一个通过 Factory::CreateIndex(...) 新建并在源数据上完成训练的索引 —— 仅每条向量的存储为空。该模式对训练(中心点 k-means)开销占主导的 IVF 类索引尤其有用。

可运行示例:examples/cpp/310_feature_export_model.cpp

注意事项与限制

  • 当对应的 *_CONCURRENT 能力标志被置位时,HGraph 上的 RemoveUpdateVectorUpdateId 是并发安全的。该标志组还约束与并发搜索、增加之间的安全组合(如 SUPPORT_ADD_SEARCH_DELETE_CONCURRENT)。
  • MARK_REMOVE 不会释放内存;如需回收空间请使用 FORCE_REMOVE 或定期重建索引。
  • Clone 的开销与索引规模线性相关。如果只需要磁盘快照,对大索引来说更适合采用「序列化 + 由 专用 reader 反序列化」的方案。
  • ExportModel 保留训练状态,但保留任何已插入的向量。导出的模型可以在尚未添加任何向量 之前自由序列化、分发。

HGraph MCI 挂件

HGraph 可以选择构建一个 MCI(Maximal Clique Index)挂件,用于带过滤条件的 KNN 搜索。这个挂件把团信息存放在 HGraph 索引内部,并复用 HGraph 的向量存储。它不是 独立索引类型:创建索引时仍然使用 hgraph,不要使用 mci

当主要负载是过滤搜索,并且过滤后只保留较小比例的向量时,可以启用这个功能。搜索 时,HGraph 会比较 Filter::ValidRatio() 和阈值,自动选择普通 HGraph 搜索或 MCI 挂件搜索。

构建配置

MCI 构建参数直接放在 index_param 下,不再使用嵌套对象。满足以下任一条件即 启用挂件:use_mci 为 true,或出现任意 MCI 构建参数。mci_knng_source 用于 选择构团所需 KNN 图的来源:可以从已经构建好的 HGraph bottom graph 派生,也可以 单独构建一张 ODescent 图。

{
  "dtype": "float32",
  "metric_type": "l2",
  "dim": 128,
  "index_param": {
    "base_quantization_type": "fp32",
    "max_degree": 32,
    "ef_construction": 400,
    "mci_mcs": 200,
    "mci_clique_max": 50,
    "mci_alpha": 1.2,
    "mci_knng_source": "odescent"
  }
}

mci_knng_source 默认为 hgraph,保持现有行为不变。设为 odescent 时, MCI 会直接基于已存储向量构建专用 KNN 图。若内部配置了外部 KNN 图文件路径, 外部文件的优先级高于此选项。

参数作用
use_mci设为 true 时使用默认构建参数启用 MCI。
mci_mcs构建团时使用的候选邻居数量。
mci_clique_max全量构建时的最大团大小。
mci_knng_sourceKNN 图来源:hgraph(默认)或 odescent
mci_alpha团构建扩展系数。
mci_incremental_join_ratio_thresholdAdd 时加入已有团的阈值。
mci_incremental_added_mct新节点最多加入的已有团数量。
mci_incremental_clique_max增量创建新团时的最大团大小。

搜索配置

搜索参数放在 hgraph 搜索对象下:

{
    "hgraph": {
      "ef_search": 120,
      "use_mci": true,
      "mci_seed_ratio": 0.1,
      "hgraph_valid_ratio_threshold": 0.2
    }
}

use_mci 在搜索时默认为 true,可以设为 false 来仅对本次查询关闭 MCI。 hgraph_valid_ratio_threshold 是搜索路由阈值:ValidRatio() 低于该阈值时走 MCI, 否则走 HGraph。默认值是 0.05

seed 数量按 ceil(sqrt(当前向量总数) * mci_seed_ratio) 计算,并且至少为 1。 mci_seed_ratio 默认值为 0.1,必须是有限的非负数。最终 seed 数量不会超过 满足过滤条件的点数。

MCI 挂件依赖过滤器提供合理的 ValidRatio()。bitset 和函数过滤器也可以 使用,但自定义 Filter 能给搜索规划提供更准确的选择率信息。

Add、序列化和统计

通过扁平构建参数启用 MCI 后,HGraph::Add() 会在每个新点插入 HGraph 后更新 MCI 挂件。它会先尝试加入合适的已有团;如果没有好的候选团,则为新点创建 一个小的增量团。

注意:MCI 索引不应通过在空索引上不断调用 Add() 从 0 开始构建。增量添加路径适用于 已有初始索引后,再追加少量向量的场景。

团数据会作为 HGraph 索引的一部分序列化。加载 HGraph 索引时,MCI 挂件会自动恢复。

GetStats() 会包含以下 MCI 质量字段:

  • mci_has_index
  • mci_total_nodes
  • mci_covered_nodes
  • mci_total_clique_count
  • mci_total_membership_count
  • mci_avg_membership_per_node
  • mci_avg_clique_size
  • mci_max_clique_size
  • mci_memory_usage

示例

最小构建和过滤搜索流程见 examples/cpp/324_feature_hgraph_mci_companion.cpp

API 参考

本章是 VSAG 公有 C++ API 的参考手册,即安装在 include/vsag/ 下的头文件。它按职责分组,记录应用 程序需要链接的类、结构体、枚举和自由函数。已安装的头文件始终是权威来源;本章的页面负责解释设计意图、 所有权,以及各部分之间如何协作。

想了解如何配置索引(JSON index_param / 搜索键)?相关内容请见 索引参数 与各 索引页面。本章覆盖的是 代码层面的接口(类型与方法),而不是 JSON 配置模式。

头文件与命名空间

单个总入口头文件即可引入全部公有 API,所有符号都位于 vsag 命名空间中:

#include <vsag/vsag.h>   // 引入 factory.h、index.h、dataset.h、engine.h ...

int main() {
    vsag::init();                       // 进程级一次性初始化
    std::string ver = vsag::version();  // 由 git 版本号派生的版本字符串
}
自由函数头文件说明
bool vsag::init()vsag/vsag.h初始化库。在调用其他 API 前调用一次。总是返回 true
std::string vsag::version()vsag/vsag.h返回由 git 版本号派生的构建版本。

错误处理模型

几乎所有可能失败的调用都返回 tl::expected<T, Error>(一个 std::expected 风格的类型,定义在 vsag/expected.hpp),而不是抛出异常。少数遗留的统计访问器在不支持时仍会抛出 std::runtime_error; 这些会在 Index 页面明确标注。

auto result = vsag::Factory::CreateIndex("hgraph", params);
if (not result.has_value()) {
    const vsag::Error& err = result.error();
    std::cerr << "create failed: " << static_cast<int>(err.type) << " " << err.message << "\n";
    return;
}
std::shared_ptr<vsag::Index> index = result.value();

Error 携带一个机器可读的 type 和一段人类可读的 message

struct Error {
    ErrorType type;
    std::string message;
};

ErrorType

定义在 vsag/errors.h。取值从 1 开始(0 保留)。

类别取值含义
通用UNKNOWN_ERROR未知错误。
通用INTERNAL_ERROR算法内部错误。
通用INVALID_ARGUMENT参数非法。
行为WRONG_STATUS索引处于不允许该调用的状态。
行为BUILD_TWICE索引已构建,无法再次构建。
行为INDEX_NOT_EMPTY在非空索引上执行反序列化。
行为UNSUPPORTED_INDEX请求了不存在的索引类型。
行为UNSUPPORTED_INDEX_OPERATION该索引未实现所调用的方法。
行为DIMENSION_NOT_EQUAL请求维度与索引维度不一致。
行为INDEX_EMPTY索引为空,无法搜索或序列化。
运行时NO_ENOUGH_MEMORY内存分配失败。
运行时READ_ERROR从二进制读取失败。
运行时MISSING_FILE反序列化时缺少必需的索引文件。
运行时INVALID_BINARY序列化的二进制内容非法。

由于大多数索引方法都是 virtual,其默认实现返回 UNSUPPORTED_INDEX_OPERATION,因此“不支持”是正常 且预期的结果:它表示具体索引未实现该可选能力。可用 Index::CheckFeature 提前探测支持情况。

头文件映射

头文件主要符号参考页面
factory.hengine.hvsag.hFactoryEngineinitversionFactory 与 Engine
index.hIndexIndexTypeRemoveModeMergeUnitIndex
dataset.hDatasetSparseVectorMultiVectorDataset
search_request.hfilter.hbitset.hsearch_param.hiterator_context.hSearchRequestFilterBitset搜索请求与过滤器
binaryset.hreaderset.hBinarySetBinaryReaderReaderSet序列化类型
resource.hallocator.hthread_pool.hoptions.hlogger.hResourceAllocatorThreadPoolOptionsLogger资源管理
attribute.hindex_features.hindex_detail_info.hutils.hconstants.hAttributeIndexFeatureIndexDetailInfo辅助类型

本章内容

  • Factory 与 Engine —— 创建索引和 reader;用 Engine 持有资源。
  • Index —— 核心索引接口:构建、搜索、更新、序列化、自省。
  • Dataset —— 用于承载向量、id 和元数据的 builder 模式容器。
  • 搜索请求与过滤器 —— SearchRequestFilterBitset、迭代上下文。
  • 序列化类型 —— BinarySet / BinaryReader / ReaderSet
  • 资源管理 —— allocator、线程池、engine 资源、options、logger。
  • 辅助类型 —— 属性、能力标志、索引细节信息与工具函数。

Factory 与 Engine

每个 VSAG 工作流都从获取一个 Index 开始。这里有两个入口:

  • Factory —— 创建索引或文件 reader 的最简单方式。它使用默认(或调用方提供)的 allocator,并在内部管理资源。
  • Engine —— 显式持有共享资源(allocator + 线程池)。当你希望多个索引共享同一个内存 allocator / 线程池,或需要对资源生命周期进行确定性控制时,使用它。

本页还介绍进程级的库初始化,以及用于参数生成与校验的顶层辅助函数

库初始化

#include <vsag/vsag.h>

vsag::init();                       // 在调用其他任何 API 之前调用一次
std::string ver = vsag::version();  // 例如由 git 派生的构建版本字符串
函数签名说明
vsag::initbool init()进程级一次性初始化。返回 true
vsag::versionstd::string version()由 git 版本号派生的构建版本。

Factory

声明于 vsag/factory.hFactory 是一个无状态的工具类,只有静态方法,无法被实例化。

CreateIndex

static tl::expected<std::shared_ptr<Index>, Error>
CreateIndex(const std::string& name,
            const std::string& parameters,
            Allocator* allocator = nullptr);

创建给定类型的索引。

参数说明
name索引类型名,例如 "hgraph""ivf""brute_force""sindi""pyramid"。已移除的 "hnsw""fresh_hnsw""diskann" 返回 UNSUPPORTED_INDEX
parameters描述索引配置的 JSON 字符串(dtype、dim、metric,以及各索引特有的键)。见 索引参数
allocator可选的自定义 Allocator。为 nullptr 时,VSAG 使用内置的默认 allocator。调用方必须在返回索引的整个生命周期内保持 allocator 有效。

成功时返回 std::shared_ptr<Index>,失败时返回 Errorname 未知时通常是 UNSUPPORTED_INDEXparameters 非法时是 INVALID_ARGUMENT)。

auto index = vsag::Factory::CreateIndex("hgraph", R"(
{
    "dtype": "float32",
    "metric_type": "l2",
    "dim": 128,
    "index_param": { "base_quantization_type": "sq8" }
})");
if (not index.has_value()) {
    std::cerr << index.error().message << std::endl;
    return;
}
std::shared_ptr<vsag::Index> hgraph = index.value();

CreateLocalFileReader

static std::shared_ptr<Reader>
CreateLocalFileReader(const std::string& filename, int64_t base_offset, int64_t size);

创建一个 Reader,它从本地文件的 base_offset 处开始读取 size 字节的 窗口。最常见的用法是构建 ReaderSet 以流式反序列化磁盘上的索引。与上面 的方法不同,它返回一个普通的 std::shared_ptr(没有可失败的 Error 通道)。

Engine

声明于 vsag/engine.hEngine 绑定一个 Resource(allocator + 线程池), 并让你创建共享它的索引。engine 不会接管传入的 Resource* 的所有权;其生命周期由你控制。

vsag::Resource resource(vsag::Engine::CreateDefaultAllocator().get(), nullptr);
vsag::Engine engine(&resource);

auto index = engine.CreateIndex("hgraph", params);
// ... 使用 index ...

engine.Shutdown();   // 释放 engine 持有的状态;若存在悬挂引用会发出告警

构造与生命周期

成员签名说明
构造函数explicit Engine(Resource* resource)绑定一个外部持有的 Resource。该 Resource 由 engine 管理。
Shutdownvoid Shutdown()优雅地拆除 engine 持有的状态。若仍存在对 engine 资源的外部引用,会发出告警,以防悬挂引用。

CreateIndex

[[nodiscard]] tl::expected<std::shared_ptr<Index>, Error>
CreateIndex(const std::string& name, const std::string& parameters);

语义与 Factory::CreateIndex 相同,区别在于索引是基于 engine 的共享 Resource (allocator 与线程池)创建的,而不是每次调用各自的 allocator。

静态资源辅助方法

成员签名说明
CreateDefaultAllocatorstatic std::shared_ptr<Allocator> CreateDefaultAllocator()创建 VSAG 内置的 allocator。失败时返回空指针 —— 需检查是否为 null。
CreateThreadPoolstatic tl::expected<std::shared_ptr<ThreadPool>, Error> CreateThreadPool(uint32_t num_threads)创建含 num_threads 个工作线程的线程池。数量非法时返回 Error

关于 ResourceAllocatorThreadPool 如何协作,见 资源管理;可运行示例见 examples/cpp/201_custom_allocator.cpp / 203_custom_thread_pool.cpp

参见

  • Index —— 索引创建之后你能用它做什么。
  • 资源管理 —— allocator、线程池与 Resource 细节。
  • 索引参数 —— CreateIndex 接受的 JSON 模式。

Index

vsag::Index(声明于 vsag/index.h)是本库的核心抽象。每一种具体索引 —— HGraph、IVF、 BruteForce、SINDI、Pyramid 等 —— 都实现这一接口。你从不直接实例化 Index,而是通过 Factory::CreateIndexEngine::CreateIndex 获取,并用 IndexPtrstd::shared_ptr<Index>)持有它。

using IndexPtr = std::shared_ptr<Index>;

如何阅读本参考

Index 暴露了许多可选能力。基类为几乎每个方法都提供了默认实现

  • 当具体索引未实现某方法时,大多数方法返回 tl::unexpected(Error(ErrorType::UNSUPPORTED_INDEX_OPERATION, ...))
  • 少数统计访问器则会抛出 std::runtime_error(下文会明确标注)。若在可能不支持它们的索引上调用, 请用 try/catch 包裹。

由于“不支持”是正常且预期的结果,请用 CheckFeature 提前探测能力,而不要假设某个方法一定 可用。标注为 (纯虚函数) 的方法必须由每种索引实现,调用它们总是安全的。

本页通篇用到的指针/句柄类型:DatasetPtrDataset)、FilterPtrFilter)、BitsetPtrBitset)、BinarySet / ReaderSet序列化类型)。

枚举与辅助类型

IndexType

enum class IndexType {
    HGRAPH = 2, IVF = 3, PYRAMID = 4, BRUTEFORCE = 5, SPARSE = 6, SINDI = 7,
    WARP = 8, LAZY_HGRAPH = 9, SIMQ = 10
};

GetIndexType 返回。

RemoveMode

enum class RemoveMode {
    MARK_REMOVE = 0,   // 标记删除;不收缩/不修复 —— 快
    FORCE_REMOVE = 1,  // 物理删除并修复图 —— 重
};

传入 Remove

MergeUnit 与 IdMapFunction

using IdMapFunction = std::function<std::tuple<bool, int64_t>(int64_t)>;

struct MergeUnit {
    IndexPtr index = nullptr;         // 要合并进来的源子索引
    IdMapFunction id_map_func = nullptr;  // 逐 id 的过滤 + 重映射
};

对每个源 id,id_map_func 返回 {keep, new_id}keep == true 表示将该向量以目标 id new_id 纳入。 由 Merge 使用。

Checkpoint

struct Index::Checkpoint {
    BinarySet data;       // 中间状态
    bool finish = false;  // 构建完成后为 true
};

ContinueBuild 返回,用于驱动增量构建。

数据选择标志

用于 GetDataByIdsWithFlag 的位标志,可通过按位或组合:

选取
DATA_FLAG_FLOAT32_VECTOR0x01float32 向量
DATA_FLAG_INT8_VECTOR0x02int8 向量
DATA_FLAG_SPARSE_VECTOR0x04稀疏向量
DATA_FLAG_EXTRA_INFO0x10extra info 数据块
DATA_FLAG_ATTRIBUTE0x20属性
DATA_FLAG_ID0x40id
DATA_FLAG_PATH0x80Pyramid 通过 store_paths 保留的路径

WriteFuncType

using OffsetType = uint64_t;
using SizeType = uint64_t;
using WriteFuncType = std::function<void(OffsetType, SizeType, const void*)>;

用于流式 Serialize 的落盘回调。每次调用要求你把 SizeType 字节(位于给定源指针处)持久化 到输出中逻辑偏移 OffsetType 的位置。

构建与训练

方法签名说明
Buildtl::expected<std::vector<int64_t>, Error> Build(const DatasetPtr& base)(纯虚函数) 从全部向量构建索引。返回插入失败的 id。
Traintl::expected<void, Error> Train(const DatasetPtr& data)训练索引(如 IVF 聚类中心、量化器)而不插入数据。
Tunetl::expected<bool, Error> Tune(const std::string& parameters, bool disable_future_tuning = false)应用运行期调优。见 优化器
ContinueBuildtl::expected<Checkpoint, Error> ContinueBuild(const DatasetPtr& base, const BinarySet& binary_set)为无法增量插入的索引提供动态性;用返回的 Checkpoint 驱动。
Addtl::expected<std::vector<int64_t>, Error> Add(const DatasetPtr& base)向已构建的索引插入新向量。返回插入失败的 id。

索引构建与训练examples/cpp/311_feature_train.cpp

更新与删除

方法签名说明
Removetl::expected<uint32_t, Error> Remove(const std::vector<int64_t>& ids, RemoveMode mode = RemoveMode::MARK_REMOVE)删除多个 id;返回被删除的数量。
Removetl::expected<uint32_t, Error> Remove(int64_t id, RemoveMode mode = RemoveMode::MARK_REMOVE)单 id 便捷重载。
UpdateIdtl::expected<bool, Error> UpdateId(int64_t old_id, int64_t new_id)为一个基础点重新打标签。
UpdateVectortl::expected<bool, Error> UpdateVector(int64_t id, const DatasetPtr& new_base, bool force_update = false)替换 id 对应的向量。force_update = false 会执行连通性检查。
UpdateExtraInfotl::expected<bool, Error> UpdateExtraInfo(const DatasetPtr& new_base)更新存储的 extra-info 数据块。
UpdateAttributetl::expected<void, Error> UpdateAttribute(int64_t id, const AttributeSet& new_attrs)替换 id 的属性。
UpdateAttributetl::expected<void, Error> UpdateAttribute(int64_t id, const AttributeSet& new_attrs, const AttributeSet& origin_attrs)同上,但提供旧属性以便更快地原地更新。

examples/cpp/303_feature_remove.cpp

搜索

推荐的入口是 SearchWithRequest,它接收单个 SearchRequest,其中携带查询、模式、top-k / 半径以及各类过滤器。较旧的 逐参数 KnnSearch / RangeSearch 重载为兼容性保留。

每次搜索都返回一个 DatasetPtr:对 KNN,num_elements == 1ids / distances 长度为 k;对范围 搜索,结果长度即命中数。如何读取结果见 Dataset

SearchWithRequest

[[nodiscard]] tl::expected<DatasetPtr, Error>
SearchWithRequest(const SearchRequest& request) const;

SearchRequest 驱动的统一 KNN 或范围搜索。这是新代码首选的 API;它通过 一个结构体即可支持属性过滤、回调过滤、bitset 过滤、逐次搜索 allocator 以及迭代式搜索。

KnnSearch 重载

// (1) bitset 预过滤 —— 纯虚函数
tl::expected<DatasetPtr, Error>
KnnSearch(const DatasetPtr& query, int64_t k, const std::string& parameters,
          BitsetPtr invalid = nullptr) const;

// (2) 回调预过滤 —— 纯虚函数
tl::expected<DatasetPtr, Error>
KnnSearch(const DatasetPtr& query, int64_t k, const std::string& parameters,
          const std::function<bool(int64_t)>& filter) const;

// (3) Filter 对象
tl::expected<DatasetPtr, Error>
KnnSearch(const DatasetPtr& query, int64_t k, const std::string& parameters,
          const FilterPtr& filter) const;

// (4) Filter + 迭代上下文
tl::expected<DatasetPtr, Error>
KnnSearch(const DatasetPtr& query, int64_t k, const std::string& parameters,
          const FilterPtr& filter, IteratorContext*& iter_ctx, bool is_last_search) const;

// (5) SearchParam —— [[deprecated]],请改用 SearchWithRequest
tl::expected<DatasetPtr, Error>
KnnSearch(const DatasetPtr& query, int64_t k, SearchParam& search_param) const;

关于 filter 参数的说明:

  • 在重载 (1)/(2) 中,谓词/bitset 标记的是被过滤掉的向量。对 bitsetTest(id) == true 表示该 id 被排除;对 std::function 谓词,返回 true 表示该 id 被排除。
  • 重载 (3)/(4) 接收 Filter 对象,其 CheckValid(id) 采用相反约定 (true 表示保留)。完整语义见 带过滤的搜索examples/cpp/301_feature_filter.cpp
  • 重载 (4) 支撑迭代式搜索;跨调用传入同一个 iter_ctx,并在最后一次 调用时设置 is_last_search

RangeSearch 重载

// (1) 普通 —— 纯虚函数
tl::expected<DatasetPtr, Error>
RangeSearch(const DatasetPtr& query, float radius, const std::string& parameters,
            int64_t limited_size = -1) const;

// (2) bitset 预过滤 —— 纯虚函数
tl::expected<DatasetPtr, Error>
RangeSearch(const DatasetPtr& query, float radius, const std::string& parameters,
            BitsetPtr invalid, int64_t limited_size = -1) const;

// (3) 回调预过滤 —— 纯虚函数
tl::expected<DatasetPtr, Error>
RangeSearch(const DatasetPtr& query, float radius, const std::string& parameters,
            const std::function<bool(int64_t)>& filter, int64_t limited_size = -1) const;

// (4) Filter 对象
tl::expected<DatasetPtr, Error>
RangeSearch(const DatasetPtr& query, float radius, const std::string& parameters,
            const FilterPtr& filter, int64_t limited_size = -1) const;

radius 限定距离上界;limited_size 限制结果数量(<= 0 表示不限,0 为错误)。见 范围搜索examples/cpp/302_feature_range_search.cpp

按 id 计算距离

方法签名说明
CalcDistanceByIdtl::expected<float, Error> CalcDistanceById(const float* vector, int64_t id, bool calculate_precise_distance = true) const稠密查询到已存向量 id 的距离。
CalcDistanceByIdtl::expected<float, Error> CalcDistanceById(const DatasetPtr& vector, int64_t id, bool calculate_precise_distance = true) const同上,接收 DatasetPtr(适用于 SINDI 等稀疏索引)。
CalcDistancesByIdtl::expected<DatasetPtr, Error> CalcDistancesById(const float* query, const int64_t* ids, int64_t count, bool calculate_precise_distance = true, int64_t topk = -1) const规范的批量版本;topk > 0 时返回按距离升序排列的最小距离及对应 ID,无效 -1 距离排在最后。
CalcDistancesByIdtl::expected<DatasetPtr, Error> CalcDistancesById(const DatasetPtr& query, const int64_t* ids, int64_t count, bool calculate_precise_distance = true, int64_t topk = -1) const规范的 DatasetPtr 查询批量版本。query->GetNumElements() > 1 时索引需声明 SUPPORT_BATCH_CALC_DISTANCE_BY_IDids 包含 NumElements * count 个 row-major 条目。topk > 0 时每个 query 返回 min(topk, count) 个排序距离及对应 ID。
CalDistanceByIdCalcDistancesById 相同的签名已弃用的兼容别名;新代码请使用 CalcDistancesById

calculate_precise_distance = true 时可能会加载全精度向量(可能来自磁盘)而非量化编码。见 按 ID 计算距离examples/cpp/306_feature_calculate_distance_by_id.cpp

共轭图增强

方法签名说明
Pretraintl::expected<uint32_t, Error> Pretrain(const std::vector<int64_t>& base_tag_ids, uint32_t k, const std::string& parameters)通过检索生成的查询来增强选定的基础向量。返回成功插入数。
Feedbacktl::expected<uint32_t, Error> Feedback(const DatasetPtr& query, int64_t k, const std::string& parameters, int64_t global_optimum_tag_id = INT64_MAX)把已知最优解反馈到共轭图中。

图索引增强

数据获取

方法签名说明
GetMinAndMaxIdtl::expected<std::pair<int64_t, int64_t>, Error> GetMinAndMaxId() const索引中最小与最大的 id。
GetExtraInfoByIdstl::expected<void, Error> GetExtraInfoByIds(const int64_t* ids, int64_t count, char* extra_infos) constids 的 extra-info 数据块拷贝到调用方提供的缓冲区。
GetRawVectorByIdstl::expected<DatasetPtr, Error> GetRawVectorByIds(const int64_t* ids, int64_t count, Allocator* specified_allocator = nullptr) const返回已存向量。其值接近原始值,但不保证逐位一致(量化/精度)。
GetDataByIdstl::expected<DatasetPtr, Error> GetDataByIds(const int64_t* ids, int64_t count) const返回实现默认提供的已存字段;可选字段可能需要显式选择。
GetDataByIdsWithFlagtl::expected<DatasetPtr, Error> GetDataByIdsWithFlag(const int64_t* ids, int64_t count, uint64_t selected_data_flag) const通过 DATA_FLAG_* 选择支持的字段。Pyramid 路径需要同时设置 store_paths: trueDATA_FLAG_PATH
GetIndexDetailInfostl::expected<std::vector<IndexDetailInfo>, Error> GetIndexDetailInfos() const列出可自省的细节字段。见 IndexDetailInfo
GetDetailDataByNametl::expected<DetailDataPtr, Error> GetDetailDataByName(const std::string& name, IndexDetailInfo& info) const按名称获取一份细节数据负载。

索引自省examples/cpp/317_feature_get_detail_data.cpp

能力探测、合并、克隆与导出

方法签名说明
CheckFeaturebool CheckFeature(IndexFeature feature) const探测某个可选能力是否受支持。见 IndexFeature
Mergetl::expected<void, Error> Merge(const std::vector<MergeUnit>& merge_units)合并同类型子索引并进行 id 重映射。见 MergeUnit
Clonetl::expected<IndexPtr, Error> Clone(const std::shared_ptr<Allocator>& allocator = nullptr) const深拷贝索引。
ExportModeltl::expected<IndexPtr, Error> ExportModel() const返回一个只携带已训练模型的空索引。
ExportIDstl::expected<DatasetPtr, Error> ExportIDs() const以 dataset 形式返回全部 id。
SetImmutabletl::expected<void, Error> SetImmutable()冻结索引;后续的增/删将被拒绝。

examples/cpp/309_feature_clone.cpp310_feature_export_model.cpp315_feature_hgraph_merge.cpp,以及 索引生命周期管理

序列化

方法签名说明
Serializetl::expected<BinarySet, Error> Serialize() const(纯虚函数) 序列化为内存中的 BinarySet
Serializetl::expected<void, Error> Serialize(WriteFuncType write_func) const通过 WriteFuncType 落盘回调流式输出序列化结果。
Serializetl::expected<void, Error> Serialize(std::ostream& out_stream)序列化到一个已打开的输出流。
Deserializetl::expected<void, Error> Deserialize(const BinarySet& binary_set)(纯虚函数)BinarySet 恢复。索引非空时失败。
Deserializetl::expected<void, Error> Deserialize(const ReaderSet& reader_set)(纯虚函数)ReaderSet(如磁盘 reader)恢复。
Deserializetl::expected<void, Error> Deserialize(std::istream& in_stream)从一个已打开的输入流恢复。

在非空索引上反序列化会得到 INDEX_NOT_EMPTY。见 序列化格式examples/cpp/401_persistent_kv.cpp / 402_persistent_streaming.cpp

缓存(构建加速)

方法签名说明
ExportCachetl::expected<void, Error> ExportCache(std::ostream& out_stream) const写出构建期缓存(如图邻居),可加速后续的 Build
ImportCachetl::expected<void, Error> ImportCache(std::istream& in_stream)加载之前导出的缓存;下一次 Build 会复用它。

HGraph 通过 Dataset::SourceID 匹配缓存条目。应在 Build() 前导入空的兼容索引;完整流程、 persist_source_id 与命中率诊断见 HGraph 构建缓存

统计与自省

除非另有说明,这些方法直接返回值。标注为“抛出”的方法在索引不支持时会抛出 std::runtime_error (而非 tl::expected)。

方法签名说明
GetIndexTypeIndexType GetIndexType() const不支持时抛出
GetNumElementsint64_t GetNumElements() const(纯虚函数) 存活元素数。
GetNumberRemovedint64_t GetNumberRemoved() const不支持时抛出。已删除元素数。
GetMemoryUsageint64_t GetMemoryUsage() const(纯虚函数) 索引占用的字节数。
GetMemoryUsageDetailstd::string GetMemoryUsageDetail() const不支持时抛出。各组件内存的 JSON。
EstimateMemoryuint64_t EstimateMemory(uint64_t num_elements) const不支持时抛出num_elements 的预估字节数。
GetEstimateBuildMemoryint64_t GetEstimateBuildMemory(int64_t num_elements) const不支持时抛出。预估构建峰值内存。
GetStatsstd::string GetStats() const不支持时抛出。运行期统计的 JSON。
AnalyzeIndexBySearchstd::string AnalyzeIndexBySearch(const SearchRequest& request)不支持时抛出。一次探测搜索的分析 JSON。
CheckIdExistbool CheckIdExist(int64_t id) const不支持时抛出id 是否存在。

examples/cpp/308_feature_estimate_memory.cpp319_feature_get_memory_usage.cpp,以及 索引分析工具

参见

Dataset

vsag::Dataset(声明于 vsag/dataset.h)是 VSAG 用于输入(要构建/添加的基础向量、要搜索的查询 向量)与输出(搜索结果、取回的向量)的通用容器。你始终通过 DatasetPtr 持有它:

using DatasetPtr = std::shared_ptr<Dataset>;

Builder 模式

Dataset 采用流式 builder:Make() 创建实例,每个 setter 都返回同一个 DatasetPtr,因此调用可以链式 书写。setter 只存储指针/值 —— 它们不会拷贝你的缓冲区。

auto base = vsag::Dataset::Make()
                ->Dim(128)
                ->NumElements(10000)
                ->Ids(ids)                 // const int64_t*
                ->Float32Vectors(vectors)  // const float*
                ->Owner(false);            // 由调用方保留 ids/vectors 的所有权

所有权

所有权决定由谁释放底层缓冲区:

调用含义
Owner(true)dataset 拥有其缓冲区,并在析构时释放(使用默认 allocator)。
Owner(true, allocator)dataset 拥有其缓冲区,并通过所提供的 Allocator 释放。
Owner(false)由调用方保留所有权;dataset 只借用这些指针。它们必须比 dataset 活得更久。

对于你已经持有的构建/查询输入,使用 Owner(false)。索引返回的搜索结果使用 Owner(true),因此你读取 之后可以让 DatasetPtr 释放全部内容。

DatasetPtr Make();               // 静态工厂

DatasetPtr Owner(bool is_owner, Allocator* allocator);
DatasetPtr Owner(bool is_owner);              // 使用默认 allocator
DatasetPtr Append(const DatasetPtr& other);   // 拼接另一个 dataset
DatasetPtr DeepCopy(Allocator* allocator = nullptr) const;  // 独立副本

元信息

SetterGetter类型含义
NumElements(int64_t)GetNumElements()int64_t元素(向量/行)数量。
Dim(int64_t)GetDim()int64_t稠密向量维度。
Ids(const int64_t*)GetIds()const int64_t*逐元素 id(长度为 NumElements)。
Distances(const float*)GetDistances()const float*距离(搜索输出;长度取决于 k/命中数)。

向量负载

一个 dataset 只携带一种向量表示。需要同时根据索引的标量 dtype 与记录布局 repr 选择对应的 payload setter:

SetterGetter元素类型配合使用
Float32Vectors(const float*)GetFloat32Vectors()floatdtype: float32
Float16Vectors(const uint16_t*)GetFloat16Vectors()uint16_tdtype: float16 bfloat16(原始 16 位负载)
Int8Vectors(const int8_t*)GetInt8Vectors()int8_tdtype: int8
SparseVectors(const SparseVector*)GetSparseVectors()SparseVectordtype: sparse(SINDI)

稠密向量按行主序排列:元素 i 的维度 j 位于 vectors[i * dim + j]。 多向量数据集使用 repr: multi_vector,并配合标量 dtype(通常为 float32)及下文的 多向量 payload。

多向量负载

用于每篇文档包含多个稠密子向量的场景:

SetterGetter类型含义
MultiVectors(const MultiVector*)GetMultiVectors()MultiVector每篇文档一个条目。
MultiVectorDim(int64_t)GetMultiVectorDim()int64_t每个子向量的 float 数(独立于 Dim)。
VectorCounts(const uint32_t*)GetVectorCounts()const uint32_t*每篇文档的子向量数量。

元数据负载

SetterGetter类型含义
AttributeSets(const AttributeSet*)GetAttributeSets()AttributeSet用于混合搜索的逐元素属性。
ExtraInfos(const char*)GetExtraInfos()const char*打包的 extra-info 数据块。
ExtraInfoSize(int64_t)GetExtraInfoSize()int64_t每个 extra-info 数据块的字节数。
Paths(const std::string*)GetPaths()const std::string*层级路径(Pyramid)。默认层级。
Paths(const std::string& hierarchy, const std::string*)GetPaths(const std::string& hierarchy)const std::string*命名层级的路径。
SourceID(const std::string*)GetSourceID()const std::string*每个元素可选的稳定来源标识;HGraph 用它跨快照匹配构建缓存条目。

属性过滤(混合搜索)Extra Info(附加信息)HGraph 构建缓存

诊断负载

SetterGetter类型含义
Statistics(const std::string&)GetStatistics() / GetStatistics(keys)std::string / std::vector<std::string>序列化的统计信息;带键的 getter 返回所请求键的值。
Reasoning(const std::string&)GetReasoning()std::string解释 expected_labels_ 召回情况的推理报告(JSON)。

读取搜索结果

搜索方法返回一个 DatasetPtr,你用 getter 读回:

auto result = index->KnnSearch(query, 10, search_params);
if (result.has_value()) {
    auto r = result.value();
    for (int64_t i = 0; i < r->GetDim(); ++i) {
        int64_t id = r->GetIds()[i];
        float dist = r->GetDistances()[i];
    }
}

对 KNN,GetNumElements()1,ids/distances 数组长度为 k。对范围搜索,命中数通过结果的维度报告。 见 k-近邻搜索

SparseVector

struct SparseVector {
    uint32_t len_ = 0;         // 非零项的数量
    uint32_t* ids_ = nullptr;  // term id,长度 len_(索引内部按升序排列)
    float* vals_ = nullptr;    // term 权重,长度 len_

    // 可选的原始分词(保留顺序/重复,与 ids_ 不同)
    uint32_t token_seq_len_ = 0;
    uint32_t* token_sequence_ = nullptr;
};

建议在插入前把 ids_ 按升序排序。token_sequence_ 是可选的,仅被消费原始 token 顺序的索引使用。

MultiVector

struct MultiVector {
    uint32_t len_ = 0;          // 本文档中的子向量数量
    float* vectors_ = nullptr;  // len_ * MultiVectorDim 个 float 的扁平数组
};

当设置了 Owner(true) 时,每个元素的 vectors_ 必须各自独立分配,因为析构函数会分别释放每个 vectors_

参见

单次搜索距离统计

维护中的 HGraph、BruteForce、IVF、Pyramid、SINDI 和 SIMQ 搜索结果会在 GetStatistics() 中附加 统计信息。一次逻辑 query-to-candidate 距离或边界评估计数一次;批量 N 个候选计为 N, 重复评估每次计数,而距离调用之前被拒绝的候选、过滤检查、图边、预取、堆操作和跳过的下界不计数。 下界与之后的精确重排分别计数。阶段为 routingapproximatererank,backend 是 fp32fp16bf16int8sq8sq4pqpq_fastscanrabitqbinary 和稀疏表示族,不包含 ISA 或批量变体。

distance_evaluations 等于阶段之和;已知 backend 之和等于总数。未知工作记入 unknown 并使 completefalse。数值是无符号 64 位 JSON 整数,加法饱和。旧的 dist_cmpreorder_distance_count 保持兼容且含义不变。Python 保留 (ids, distances) 解包方式;可通过 knn_search_with_statistics 显式获取统计信息:稠密重载返回一个统计 JSON 字符串,稀疏 CSR 重载为每个查询返回一个字符串。range_search_with_statistics 返回范围搜索数组及一个统计 JSON 字符串。C 保留 SearchResult_t 布局,统计信息通过新增的显式访问方式提供:C 调用 vsag_search_result_enable_statistics() 后获取统计信息,并用 vsag_search_result_destroy_statistics() 释放;未选择统计的旧调用保留 other_result 所有权, 不会产生统计分配。旧版 HNSW 和 DiskANN 不在此合约内。

搜索请求与过滤器

本页介绍描述如何搜索的类型:统一的 SearchRequest、过滤原语 FilterBitset,以及用于增量搜索的 IteratorContext。已废弃的 SearchParam 在末尾给出以便 迁移。

SearchRequest

声明于 vsag/search_request.hSearchRequest 是一个普通结构体,打包了 Index::SearchWithRequest 的每一个选项。填入你需要的字段,其余 保持默认即可。

vsag::SearchRequest request;
request.query_ = query;      // 含单个查询向量的 DatasetPtr
request.mode_ = vsag::SearchMode::KNN_SEARCH;
request.topk_ = 10;
request.params_str_ = R"({"hgraph": {"ef_search": 100}})";

auto result = index->SearchWithRequest(request);

SearchMode

enum class SearchMode {
    KNN_SEARCH = 1,    // 返回 top-k 个最近向量
    RANGE_SEARCH = 2,  // 返回 radius_ 范围内的所有向量
};

基础字段

字段类型默认值含义
query_DatasetPtrnullptr查询。IVF KNN 请求支持多个查询向量,其他请求只允许一个。
mode_SearchModeKNN_SEARCHKNN 还是范围搜索。
topk_int64_t10要返回的邻居数(KNN 模式)。必须为正。
radius_float0.5距离阈值(范围模式)。非负。
limited_size_int64_t-1范围结果的上限;-1 表示不限。
params_str_std::string""算法特有的搜索参数 JSON(如 ef_search)。

自定义查询距离回调

distance_batch_func_ 可选地让应用提供 query 到向量的分数。它接收稳定的外部向量 ID, 因此闭包可以在 VSAG 之外持有 query 和该 query 的上下文:

request.distance_batch_size_ = 32;
request.distance_batch_func_ = [query](const int64_t* ids, uint64_t count, float* scores) {
    for (uint64_t i = 0; i < count; ++i) {
        scores[i] = Score(query, ids[i]);
    }
};
字段类型默认值含义
distance_batch_func_SearchDistanceBatchFuncnullptr按输入外部 ID 的顺序填写对应分数。分数越小越好,且必须是有限值。
distance_batch_size_uint64_t1单次回调的最大 ID 数。批量推理或外部数据访问应设置为 scorer 的高效批大小;设置回调时必须为正。

回调仅属于当前请求,不参与序列化。它可以捕获 query。hgraph 的回调模式允许 query_ 为 null, 但 ivf 仍需要一个查询向量用于桶路由。回调在一次请求内必须稳定;若调用方启用并行执行则必须 线程安全;不得抛异常,且只能写入有限分数。

brute_force 用该回调执行精确 KNN 和范围搜索。hgraph 仅支持 KNN:回调驱动图遍历和最终 排序,但图仍由配置的内置 metric 构建,因此无法保证该回调分数下的 recall。HGraph 可能为保持 图连通性而计算被过滤的遍历节点,但不会返回这些节点。 回调 HGraph 不支持并行检索或 brute_force_threshold;这些配置会被拒绝,而不会静默改变检索语义。

ivf 支持带回调的 KNN,但必须提供一个非空的单查询向量。IVF 先以配置的内置 metric 选出 scan_buckets_count 个桶,再对这些桶中的候选调用回调。回调决定候选排序,但无法召回未被选中的 桶内向量;增大 scan_buckets_count 可提高 recall。回调 IVF 不支持范围搜索、disable_bucket_scan、 bucket graph 或并行搜索,这些配置会被拒绝。回调模式会自动关闭精排。其他索引不支持该回调。

IVF 桶路由

IVF 可通过 params_str_ 接收 {"ivf":{"scan_buckets_count":N,"disable_bucket_scan":true}}。该仅路由模式按查询返回 N 个 bucket ID,而非向量 label。NumElements() 为查询数,Dim()scan_buckets_countGetIds() 包含桶 ID(空槽位为 -1), GetDistances() 为到各桶中心的距离。不扫描桶内向量,因此过滤器、topk、范围限制、精排和 reasoning 选项均会被忽略。

IVF 桶 ID 旁路

bucket_ids_ 字段允许调用方提供预选的桶 ID,绕过 IVF 的内部桶路由(ClassifyDatasForSearch)。 当非空时,搜索跳过基于质心的选择,仅扫描指定的桶。

字段类型默认值含义
bucket_ids_std::vector<std::vector<int64_t>>{}用于绕过 IVF 路由的预选桶 ID。

语义:

  • (默认):使用正常的桶路由(ClassifyDatasForSearch)。
  • 非空:跳过路由,仅扫描提供的桶 ID。
  • 批量 IVF KNNbucket_ids_ 外层每项对应一个查询向量。 结果为矩形:NumElements() 是查询数,Dim()topk_。 缺失邻居用 -1 和无穷距离填充。

约束:

  • 批量 IVF 搜索仅支持 KNN;不支持自定义查询距离和 reasoning labels。
  • 非空外层向量必须为每个查询向量提供一个非空条目。
  • 每个桶 ID 必须在 [0, bucket_count) 范围内。
  • 重复的桶 ID 会被拒绝。
  • disable_bucket_scan 模式不兼容。
  • 调用方负责桶 ID 的排序(不会自动排序)。

过滤字段

有三种过滤机制可用;当启用了多于一种时,它们以逻辑**与(AND)**组合。

字段类型默认值含义
enable_attribute_filter_boolfalse启用 SQL 风格的属性过滤。
attribute_filter_str_std::string""过滤表达式(见下)。需要 enable_attribute_filter_
enable_filter_boolfalse启用自定义 Filter 回调。
filter_FilterPtrnullptrfilter 对象。需要 enable_filter_
enable_bitset_filter_boolfalse启用 Bitset 过滤。
bitset_filter_BitsetPtrnullptrbitset。Test(id) == true 表示排除该 id。需要 enable_bitset_filter_

attribute_filter_str_ 的语法类似 SQL。示例:

category = 'electronics' AND price != 1000
multi_in(category, ['electronics', 'clothing']) AND multi_notin(color, ['red', 'blue'])

属性过滤(混合搜索)带过滤的搜索

资源与迭代器字段

字段类型默认值含义
search_allocator_Allocator*nullptr逐次搜索 allocator;为 null 时回退到索引 allocator。
enable_iterator_search_boolfalse启用增量(迭代式)搜索。
p_iter_ctx_IteratorContext**nullptr迭代状态的句柄,跨调用复用。
is_last_search_boolfalse标记迭代序列的最后一次调用。
expected_labels_std::vector<int64_t>{}期望出现在结果中的 id;启用对漏召回的推理分析。

搜索路径 Allocator迭代式搜索,allocator 示例见 examples/cpp/313/314

Filter

声明于 vsag/filter.h。实现这个抽象类以表达任意的“是否保留该 id?”逻辑。通过 FilterPtrstd::shared_ptr<Filter>)持有它。

class Filter {
public:
    enum class Distribution { NONE = 0, RELATED_TO_VECTOR };

    virtual bool CheckValid(int64_t id) const = 0;          // true  => 保留该 id
    virtual bool CheckValid(const char* data) const;         // extra-info 变体(默认 true)
    virtual float ValidRatio() const;                        // 保留比例(默认 1.0)
    virtual Distribution FilterDistribution() const;         // 提示(默认 NONE)
    virtual void GetValidIds(const int64_t** valid_ids, int64_t& count) const;
};

约定: Filter::CheckValid(id) 返回 true 表示保留该向量。这与 Index 上的 bitset / std::function<bool(int64_t)> 预过滤重载 相反 —— 在那些重载里 true 表示被过滤掉。选择重载时请牢记这一区别。

成员用途
CheckValid(int64_t id)核心谓词。true 使该 id 保留在结果中。
CheckValid(const char* data)对元素 extra-info 字节的谓词。默认为 true
ValidRatio()预估通过的向量比例;让引擎选择策略。
FilterDistribution()RELATED_TO_VECTOR 提示有效性与向量位置相关。
GetValidIds(...)可选地暴露显式的有效 id 集合。

examples/cpp/301_feature_filter.cpp

Bitset

声明于 vsag/bitset.h。一个按位置索引的紧凑位标志集合,通过 BitsetPtr 持有。它既用作过滤输入,也可 作为工具(如 l2_and_filtering 的返回值)。

static BitsetPtr Random(int64_t length);  // 给定长度的随机 bitset
static BitsetPtr Make();                  // 空 bitset

void Set(int64_t pos, bool value);
void Set(int64_t pos);       // = Set(pos, true)
bool Test(int64_t pos) const;
uint64_t Count();            // 置位的数量
std::string Dump();          // 调试转储

Bitset 被用作搜索预过滤(bitset_filter_,或 KnnSearch / RangeSearchinvalid 参数) 时,Test(id) == true 表示该 id 被过滤掉

IteratorContext

声明于 vsag/iterator_context.h。一个不透明句柄,保存进行中的迭代式搜索的位置,使后续调用能从上一次 停止处继续。

class IteratorContext {
public:
    virtual ~IteratorContext() = default;
};

你无需直接构造或检查它。VSAG 在首次迭代式搜索时分配它;在之后每次调用中把同一个句柄传回 (通过 SearchRequest::p_iter_ctx_,或 KnnSearch 的迭代重载),并在最后一次调用时设置 last-search 标志,以便引擎释放它。见 迭代式搜索

SearchParam(已废弃)

声明于 vsag/search_param.hSearchParam 早于 SearchRequest,仅为已废弃的 KnnSearch(query, k, SearchParam&) 重载而保留。

struct SearchParam {  // [[deprecated]] 请改用 SearchRequest
    bool is_iter_filter{false};
    bool is_last_search{false};
    const std::string& parameters;
    FilterPtr filter{nullptr};
    Allocator* allocator{nullptr};
    IteratorContext* iter_ctx{nullptr};
};

所有新代码请优先使用 SearchRequest + SearchWithRequest SearchParam 以引用方式持有 parameters,因此被引用的字符串必须比该调用活得更久。

参见

  • Index —— 消费这些类型的搜索方法。
  • Dataset —— 构造 query_ 并读取结果。
  • 辅助类型 —— 属性过滤所用的属性值类型。

序列化类型

VSAG 可以用两种形态持久化索引:内存中的 BinarySet(一组带名字的字节块),或在磁盘 / 流式场景下由惰性 Reader 对象组成的 ReaderSet。这些类型正是传给 Index::Serialize / Index::Deserialize 的负载。

端到端流程与基于流的序列化见 序列化格式examples/cpp/401_persistent_kv.cpp / 402_persistent_streaming.cpp

Binary

声明于 vsag/binaryset.h。一块带长度的、有名字的字节缓冲区。

struct Binary {
    std::shared_ptr<int8_t[]> data;  // 字节数据
    uint64_t size;                   // 字节数
};

shared_ptr 拥有该缓冲区,因此 Binary 可以自由拷贝与存储,无需担心生命周期。

BinarySet

声明于 vsag/binaryset.h。一个以字符串为键的 Binary 块映射 —— 标准的内存序列化容器。索引会把自身 序列化为若干带名字的部分(图、向量、量化器等),全部汇集在一个 BinarySet 中。

class BinarySet {
public:
    void Set(const std::string& name, Binary binary);   // 存入一个块
    Binary Get(const std::string& name) const;          // 不存在时返回 {nullptr, 0}
    std::vector<std::string> GetKeys() const;            // 所有已存名字
    bool Contains(const std::string& key) const;
};
方法说明
Set(name, binary)name 存入 binary,覆盖任何已有条目。
Get(name)返回该块;名字不存在时返回空的 Binary{nullptr, 0}
GetKeys()返回每一个已存名字。
Contains(key)key 下是否存有块。
// 序列化为 BinarySet,然后按你喜欢的方式持久化每个部分。
auto serialized = index->Serialize();
if (serialized.has_value()) {
    vsag::BinarySet bs = serialized.value();
    for (const auto& key : bs.GetKeys()) {
        vsag::Binary part = bs.Get(key);
        // 以 `key` 为键,把 part.data[0 .. part.size) 写入你的存储
    }
}

要恢复,从你的存储重建 BinarySet,并在一个全新(空)索引上调用 Deserialize(const BinarySet&)

Reader

声明于 vsag/readerset.h。一个抽象的字节来源,索引按需从中读取 —— 这是在不把全部内容载入内存的前提下 反序列化大型磁盘常驻索引的基础。可从 Factory::CreateLocalFileReader 获取本地文件 reader,或为 自定义后端(对象存储、mmap 等)实现 Reader。通过 ReaderPtrstd::shared_ptr<Reader>)持有它。

class Reader {
public:
    virtual void Read(uint64_t offset, uint64_t len, void* dest) = 0;                  // 同步
    virtual void AsyncRead(uint64_t offset, uint64_t len, void* dest, CallBack cb) = 0; // 异步
    virtual bool MultiRead(uint8_t* dests, const uint64_t* lens,
                           const uint64_t* offsets, uint64_t count);                    // 批量
    virtual uint64_t Size() const = 0;
};
方法说明
Read(offset, len, dest)同步地从 offset 拷贝 len 字节到 dest。线程安全。
AsyncRead(offset, len, dest, callback)异步读;完成时以 IOErrorCode 和 message 调用 callback
MultiRead(dests, lens, offsets, count)在一次调用中执行 count 次同步读;任一失败返回 false
Size()底层来源的总字节数。

IOErrorCode

enum class IOErrorCode {
    IO_SUCCESS = 0,  // 操作成功
    IO_ERROR = 1,    // 一般 I/O 错误
    IO_TIMEOUT = 2,  // 操作超时
};

CallBack

using CallBack = std::function<void(IOErrorCode code, const std::string& message)>;

AsyncRead 的完成回调。

ReaderSet

声明于 vsag/readerset.h。一个以字符串为键的 Reader 对象映射 —— BinarySet 的流式对应物。序列化 索引的每个带名字的部分都映射到一个按需拉取该部分的 Reader。把一个填充完整的 ReaderSet 传给 Deserialize(const ReaderSet&)

class ReaderSet {
public:
    void Set(const std::string& name, ReaderPtr reader);
    ReaderPtr Get(const std::string& name) const;   // 不存在时为 nullptr
    std::vector<std::string> GetKeys() const;
    bool Contains(const std::string& key) const;
};

其方法语义与 BinarySet 一致,只是值为 ReaderPtr 而非 Binary

vsag::ReaderSet readers;
readers.Set("graph", vsag::Factory::CreateLocalFileReader("index.graph", 0, graph_size));
readers.Set("vectors", vsag::Factory::CreateLocalFileReader("index.vectors", 0, vec_size));

auto fresh = vsag::Factory::CreateIndex("hgraph", params).value();
fresh->Deserialize(readers);

参见

资源管理

VSAG 允许你掌控它所使用的内存与线程。本页介绍 Allocator(自定义内存管理)、 ThreadPool(自定义并发)、Resource(由 Engine 共享的两者打包)、进程级的 Options 单例,以及可插拔的 Logger

可运行示例:examples/cpp/201_custom_allocator.cpp202_custom_logger.cpp203_custom_thread_pool.cpp。另见 内存管理可扩展性

Allocator

声明于 vsag/allocator.h。用于自定义内存管理的抽象接口。实现它即可把索引的全部分配路由到你自己的池、 arena 或记账层,然后把它传给 Factory::CreateIndex 或一个 Resource

class Allocator {
public:
    virtual std::string Name() = 0;
    virtual void* Allocate(uint64_t size) = 0;
    virtual void Deallocate(void* p) = 0;
    virtual void* Reallocate(void* p, uint64_t size) = 0;

    template <typename T, typename... Args> T* New(Args&&... args);  // 分配 + 构造
    template <typename T> void Delete(T* p);                          // 析构 + 释放
};
成员说明
Name()allocator 实现的标识(用于诊断)。
Allocate(size)返回至少 size 字节的内存块。
Deallocate(p)释放先前由本 allocator 返回的内存块。
Reallocate(p, size)调整内存块大小,保留其内容。
New<T>(args...)辅助方法:分配并构造一个 T;若构造函数抛异常则释放并重新抛出。
Delete<T>(p)辅助方法:析构 *p 并释放其存储(对 null 安全)。

传给索引的 allocator 必须比该索引活得更久。VSAG 内置 allocator 可通过 Engine::CreateDefaultAllocator 获取。

ThreadPool

声明于 vsag/thread_pool.h。一个抽象的任务执行器。提供你自己的实现,即可让 VSAG 共享你应用的线程,而 不是自行创建。

class ThreadPool {
public:
    virtual void WaitUntilEmpty() = 0;
    virtual void SetQueueSizeLimit(std::uint64_t limit) = 0;
    virtual void SetPoolSize(std::uint64_t limit) = 0;
    virtual std::future<void> Enqueue(std::function<void(void)> task) = 0;
};
成员说明
WaitUntilEmpty()阻塞直到所有已入队任务完成。
SetQueueSizeLimit(limit)限制待处理任务队列;超过上限后的行为由实现定义。
SetPoolSize(limit)限制工作线程数量。
Enqueue(task)提交一个任务;返回用于其完成状态的 std::future<void>

现成的线程池可通过 Engine::CreateThreadPool 创建。

Resource

声明于 vsag/resource.hResource 把一个 Allocator 与一个 ThreadPool 打包,使一个 Engine —— 及其创建的每个索引 —— 都能共享它们。

class Resource {
public:
    explicit Resource(Allocator* allocator, ThreadPool* thread_pool);
    explicit Resource(const std::shared_ptr<Allocator>& allocator,
                      const std::shared_ptr<ThreadPool>& thread_pool);
    explicit Resource();  // 默认 allocator,无线程池

    std::shared_ptr<Allocator> GetAllocator() const;
    std::shared_ptr<ThreadPool> GetThreadPool() const;
};
构造函数 / 方法说明
Resource(Allocator*, ThreadPool*)使用你拥有的裸指针。allocator 为 null 表示“创建并拥有一个默认的”;线程池为 null 表示“无线程池”。
Resource(shared_ptr, shared_ptr)同上,采用共享所有权。
Resource()默认 allocator,无线程池。
GetAllocator()该资源的 allocator(若未提供则为默认的)。
GetThreadPool()该资源的线程池,若未提供则为 null。
auto alloc = vsag::Engine::CreateDefaultAllocator();
auto pool = vsag::Engine::CreateThreadPool(4).value();
vsag::Resource resource(alloc, pool);
vsag::Engine engine(&resource);
auto index = engine.CreateIndex("hgraph", params);

Options

声明于 vsag/options.h。用于全局配置的进程级单例,通过 Options::Instance() 访问。线程安全。OptionOptions 的类型别名。

vsag::Options::Instance().set_num_threads_building(8);
vsag::Options::Instance().set_logger(&my_logger);
配置项访问器默认值含义
构建线程num_threads_building() / set_num_threads_building(n)4构建索引的线程数。
块大小上限block_size_limit() / set_block_size_limit(bytes)128 MB每个分配块的最大字节数(必须 > 2 MB)。
Direct-IO 对齐direct_IO_object_align_bit() / set_direct_IO_object_align_bit(bits)9Direct-IO 对象对齐,以位为单位(< 21)。
Loggerlogger() / set_logger(Logger*)nullptr当前 Logger;设置成功返回 true

Logger

声明于 vsag/logger.h。一个抽象的日志汇。实现它并通过 Options::set_logger 注册,即可把 VSAG 的日志 输出路由到你应用的日志系统。

内置 logger 默认使用 info。在内置 logger 创建前设置 VSAG_LOG_LEVEL,可选择 tracedebuginfowarn/warningerrorcriticaloff。无效值会被忽略,并保留默认等级。 显式调用 SetLevel 仍会覆盖从环境变量得到的等级。

启动 init banner

VSAG 会在 vsag::init() 运行时输出启动初始化 banner。如需隐藏该 banner, 请在进程启动前设置 VSAG_SUPPRESS_INIT_BANNER。这适用于测试、CI 任务, 或需要更安静启动日志的应用。

可识别的真值为 1ontrueontrue 按 ASCII 大小写不敏感匹配, 因此 ONOnTRUE 也有效。其他值会保留 banner 输出。

请务必在进程启动前设置该变量。VSAG 也会在静态初始化阶段运行 vsag::init(),因此在进程内部稍后再设置该变量,无法隐藏首次 banner。

banner 包含类似 48C503Ginstance spec 值,将 cpuinfo 获取的核心数与物理内存总量合并展示。 内存使用 1024^3 字节进行整 GiB 向下取整;如果平台查询失败,内存部分显示为 ?G。包括 neonsve 在内的 SIMD 行,仍保持原有的 distribution/platform/using 能力语义。

VSAG_SUPPRESS_INIT_BANNER=1 ./your_vsag_app
VSAG_SUPPRESS_INIT_BANNER=true ./your_vsag_test
class Logger {
public:
    enum Level : int {
        kTRACE = 0, kDEBUG = 1, kINFO = 2, kWARN = 3, kERR = 4, kCRITICAL = 5, kOFF = 6, kN_LEVELS
    };

    virtual void SetLevel(Level log_level) = 0;
    virtual void Trace(const std::string& msg) = 0;
    virtual void Debug(const std::string& msg) = 0;
    virtual void Info(const std::string& msg) = 0;
    virtual void Warn(const std::string& msg) = 0;
    virtual void Error(const std::string& msg) = 0;
    virtual void Critical(const std::string& msg) = 0;
};
成员说明
SetLevel(level)仅发出等级不低于 level 的消息。kOFF 关闭日志。
Trace / Debug / Info / Warn / Error / Critical以相应严重级别发出一条消息。

examples/cpp/202_custom_logger.cpp

参见

辅助类型

本页汇总其余的公有类型:用于混合(属性过滤)搜索的属性系统、 IndexFeature 能力标志、索引细节信息自省类型、utils.h 中的 工具函数,以及 constants.h 中的字符串常量

Attributes

声明于 vsag/attribute.h。属性是附加到每个向量上的、带类型且具名的元数据,可在搜索时进行 SQL 风格的 过滤(见 属性过滤(混合搜索))。

AttrValueType

enum AttrValueType {
    INT32 = 1, UINT32 = 2, INT64 = 3, UINT64 = 4,
    INT8 = 5, UINT8 = 6, INT16 = 7, UINT16 = 8,
    STRING = 9,
};

属性所携带的元素类型。

Attribute

class Attribute {
public:
    std::string name_{};

    virtual AttrValueType GetValueType() const = 0;
    virtual uint64_t GetValueCount() const = 0;
    virtual Attribute* DeepCopy() const = 0;
    virtual bool Equal(const Attribute* other) const = 0;
};
using AttributePtr = std::shared_ptr<Attribute>;

一个抽象的、具名的属性。每个属性可持有多个值(GetValueCount()),因此单个字段可以表示一个多值的标签 集合。

成员说明
name_属性(字段)名。
GetValueType()所存值的 AttrValueType
GetValueCount()所持有的值的数量。
DeepCopy()分配一个独立副本。
Equal(other)与另一个属性做值相等比较。

AttributeValue<T>

template <class T>
class AttributeValue : public Attribute {
public:
    AttrValueType GetValueType() const override;
    uint64_t GetValueCount() const override;
    std::vector<T>& GetValue();
    const std::vector<T>& GetValue() const;
    Attribute* DeepCopy() const override;
    bool Equal(const Attribute* other) const override;
};

Attribute 的具体、带类型实现。用与目标 AttrValueType 匹配的 C++ 类型实例化它 (如 AttributeValue<int32_t>AttributeValue<std::string>),设置 name_,并把值 push 进 GetValue()

auto tag = std::make_shared<vsag::AttributeValue<int32_t>>();
tag->name_ = "category";
tag->GetValue().push_back(7);

AttributeSet

struct AttributeSet {
    std::vector<Attribute*> attrs_;
};

描述单个元素的一组属性。可通过 AttributeSets(...) 把逐元素的 AttributeSet 数组附加到 Dataset,或把一个传给 Index::UpdateAttribute

IndexFeature

声明于 vsag/index_features.h。一个可选能力的枚举,你可以在调用某个可选方法前用 Index::CheckFeature 探测它。

enum IndexFeature {
    NEED_TRAIN = 1,
    SUPPORT_BUILD,
    SUPPORT_ADD_AFTER_BUILD,
    SUPPORT_KNN_SEARCH,
    SUPPORT_RANGE_SEARCH,
    SUPPORT_DELETE_BY_ID,
    SUPPORT_SERIALIZE_BINARY_SET,
    SUPPORT_CAL_DISTANCE_BY_ID,
    SUPPORT_MERGE_INDEX,
    SUPPORT_CLONE,
    /* ... 还有很多 ... */
    INDEX_FEATURE_COUNT   // 哨兵;始终是最后一个值
};

该枚举把能力分为几个族:

示例
生命周期NEED_TRAINSUPPORT_BUILDSUPPORT_ADD_AFTER_BUILDSUPPORT_ADD_FROM_EMPTYSUPPORT_RESET
搜索SUPPORT_KNN_SEARCHSUPPORT_RANGE_SEARCHSUPPORT_*_WITH_ID_FILTERSUPPORT_KNN_ITERATOR_FILTER_SEARCHSUPPORT_BATCH_SEARCH
度量SUPPORT_METRIC_TYPE_L2SUPPORT_METRIC_TYPE_INNER_PRODUCTSUPPORT_METRIC_TYPE_COSINE
序列化SUPPORT_SERIALIZE_FILE / _BINARY_SET / _WRITE_FUNCSUPPORT_DESERIALIZE_FILE / _BINARY_SET / _READER_SET
并发SUPPORT_ADD_CONCURRENTSUPPORT_SEARCH_CONCURRENTSUPPORT_ADD_SEARCH_DELETE_CONCURRENT,以及 SUPPORT_*_WITH_MULTI_THREAD 的构建/训练变体
自省与运维SUPPORT_ESTIMATE_MEMORYSUPPORT_GET_MEMORY_USAGESUPPORT_CHECK_ID_EXISTSUPPORT_MERGE_INDEXSUPPORT_CLONESUPPORT_EXPORT_MODELSUPPORT_EXPORT_IDSSUPPORT_TUNESUPPORT_CAL_DISTANCE_BY_IDSUPPORT_GET_*_BY_ID(S)

INDEX_FEATURE_COUNT 标记枚举末尾,并非真正的能力。见 examples/cpp/307_feature_check_features.cpp

索引细节信息

声明于 vsag/index_detail_info.h。这些类型描述并承载 Index::GetIndexDetailInfosIndex::GetDetailDataByName 返回的结构化数据。见 索引自省examples/cpp/317_feature_get_detail_data.cpp

IndexDetailDataType

enum class IndexDetailDataType {
    TYPE_2DArray_INT64,
    TYPE_1DArray_INT64,
    TYPE_SCALAR_INT64,
    TYPE_SCALAR_DOUBLE,
    TYPE_SCALAR_STRING,
    TYPE_SCALAR_BOOL,
};

告诉你对某个字段哪个 DetailData getter 是有效的。

IndexDetailInfo

class IndexDetailInfo {
public:
    std::string name;
    std::string description;
    IndexDetailDataType type;
};

单个可自省字段的描述符:其 name、人类可读的 description,以及负载 type

DetailData

class DetailData {
public:
    virtual std::vector<int64_t> GetData1DArrayInt64();
    virtual std::vector<std::vector<int64_t>> GetData2DArrayInt64();
    virtual std::string GetDataScalarString();
    virtual bool GetDataScalarBool();
    virtual int64_t GetDataScalarInt64();
    virtual double GetDataScalarDouble();
    // ... const 重载 ...
};
using DetailDataPtr = std::shared_ptr<DetailData>;

负载本身。用与描述符 IndexDetailDataType 匹配的 getter 读取它;调用不匹配的 getter 没有意义。

工具函数

声明于 vsag/utils.h。用于聚类与召回评估的自由辅助函数。

kmeans_clustering

float kmeans_clustering(uint64_t d, uint64_t n, uint64_t k, const float* x,
                        float* centroids, const std::string& dis_type);

n 个维度为 d 的点运行 k-means,将 k 个聚类中心写入预分配的 centroids(大小 k * d)。 dis_type"l2""cosine""ip" 之一。返回最终的量化误差。

l2_and_filtering

BitsetPtr l2_and_filtering(int64_t dim, int64_t nb, const float* base,
                           const float* query, float threshold);

返回一个 Bitset:当基础向量 i 落在与 query 的 L2 距离 threshold 之内时, 将第 i 位置为 true。这是 range_search_recall 使用的 ground truth。注意其置位极性与搜索预过滤 相反:预过滤中置位表示该 id 被排除(见 Bitset);若要作为 invalid / bitset_filter_ 掩码复用,需先取反。

knn_search_recall / range_search_recall

float knn_search_recall(const float* base, const int64_t* id_map, int64_t base_num,
                        const float* query, int64_t data_dim,
                        const int64_t* result_ids, int64_t result_size);

float range_search_recall(const float* base, const int64_t* base_ids, int64_t num_base,
                          const float* query, int64_t dim,
                          const int64_t* result_ids, int64_t result_size, float threshold);

针对由基础向量导出的 ground truth,计算 KNN 或范围搜索结果的召回率。便于测试与基准评测;见 标准环境性能参考

常量

声明于 vsag/constants.h。一大批 extern const char* const 字符串常量,对应贯穿于基于 JSON 的配置中 所用的键与枚举字符串值。用这些常量而非裸字符串字面量可避免拼写错误。它们分为若干组:

示例
索引类型名INDEX_HGRAPHINDEX_IVFINDEX_BRUTE_FORCEINDEX_SINDIINDEX_PYRAMID
Dataset 字段名DIMNUM_ELEMENTSIDSDISTSFLOAT32_VECTORSSPARSE_VECTORS
度量名METRIC_L2METRIC_COSINEMETRIC_IP
数据类型名DATATYPE_FLOAT32DATATYPE_FLOAT16DATATYPE_BFLOAT16DATATYPE_INT8DATATYPE_SPARSE
顶层参数PARAMETER_DTYPEPARAMETER_DIMPARAMETER_METRIC_TYPEINDEX_PARAM
各索引参数HGRAPH_*IVF_*PYRAMID_*BRUTE_FORCE_*
统计键STATSTIC_MEMORYSTATSTIC_KNN_TIMESTATSTIC_RANGE_TIME

各参数键的含义见 索引参数 与各 索引页面

参见

  • Index —— CheckFeatureGetIndexDetailInfosUpdateAttribute
  • Dataset —— 把 AttributeSet 数据附加到元素上。
  • 搜索请求与过滤器 —— 属性过滤表达式与 Bitset

最佳实践

本页整理在生产环境使用 VSAG 的经验性建议,作为参数手册与性能调优的补充。

索引选型

场景推荐索引理由
中等规模(≤ 1000 万)纯内存、对召回/延迟要求极高hgraph统一的高质量图索引,支持多种量化与 Tune
候选召回层 / 粗排ivf训练后即可大规模并行
小规模、需要 100% 精度brute_force暴力搜索,作为召回率 baseline
多租户 / 分区数据pyramid一个索引内部多棵子图,支持按 tag 检索
稀疏向量(BM25 / SPLADE 类)sindi专为稀疏向量设计

详细参数参见 索引参数

构建阶段

  • 先确定 metricl2 / ip / cosine 不可在构建后变更。
  • ef_construction:典型 200~500。过小召回不足;过大构建显著变慢。
  • max_degree / M:典型 16~48。越大召回越高、内存也越高。
  • 量化策略:延迟敏感场景建议 sq8pq;精度敏感建议 fp32fp16
  • 并行构建:使用自定义 ThreadPool(见 examples/cpp/203_custom_thread_pool.cpp)以控制并发度。

搜索阶段

  • ef_search:典型 topk ~ topk * 10,可按 QPS / 召回率做 grid search。
  • 批量搜索:多查询合并可提升缓存命中;参考 examples/cpp/205_*(若提供)或业务侧批量化。
  • Filter:使用内置 Filterexamples/cpp/301_feature_filter.cpp),不要在结果侧二次过滤。
  • 临时 Allocator:高并发在线服务建议每线程一份 arena allocator,见 内存管理

调优

部署

  • 推荐使用官方 Docker 镜像,详见 安装
  • 生产二进制建议选择对应 ABI 的发布包:dist-pre-cxx11-abidist-cxx11-abidist-libcxx(见 编译构建)。
  • 开启 VSAG_ENABLE_INTEL_MKL=ON 可在 Intel CPU 上获得额外加速。

可观测

  • Index::GetMemoryUsage() 暴露运行时内存;
  • 搜索路径上可用自定义 Loggerexamples/cpp/202_custom_logger.cpp)接入业务日志;
  • 结合 eval_performance 将关键指标写入 InfluxDB 进行长期监控。

磁盘索引最佳实践

当数据规模增长到内存放不下全量向量时,把最冷、最大的那部分索引下沉到 SSD,是控制成本最直接的 手段。VSAG 让索引的每个部分各自选择存储后端,从而实现热数据留内存、冷数据从磁盘读取。本文介绍 HGraph + 磁盘 IO,给出可直接复制的配置、容量模型与调优 清单。

这里的“磁盘索引”指把索引数据分层存放在内存与磁盘之间,与“向量 + 标量属性”的混合检索无关; 后者请参见属性过滤(混合搜索)

什么时候该上磁盘

数据规模大并不必然要上磁盘。先用下面几个信号判断:

信号纯内存即可考虑磁盘
数据规模千万级及以内亿级 / 十亿级
全量 fp32 是否放得下内存放得下放不下
延迟预算亚毫秒、极敏感几毫秒~十几毫秒可接受
成本结构内存成本可接受希望用 SSD 替换大部分内存

一个快速的内存估算:全量 fp32 副本约占用 N × dim × 4 字节。例如 1e9 × 128 × 4 ≈ 512 GB1e8 × 768 × 4 ≈ 307 GB——这类规模在单机内存里几乎放不下,正是磁盘索引的目标场景。

磁盘路线的代价是每次查询会引入若干次随机读 I/O,因此延迟会高于纯内存索引。如果你的服务要求 亚毫秒级延迟且数据能压缩进内存,优先考虑纯内存的 HGraph + 量化。

核心思想

VSAG 的磁盘路线遵循同一原则:内存放小而近似的表示用于导航,磁盘放大而精确的表示用于 排序。 图遍历只访问内存中的近似量化码;随后仅对少量入围候选,用从磁盘读回的 精确副本做重排(reorder)。由于磁盘读取只发生在最后、且只针对少量候选,I/O 成本被限制在很小 范围,而召回则由精确重排补回。

HGraph 如何分层存储

HGraph 把一个索引拆成若干个相互独立的 cell(数据单元),每个 cell 都能单独指向自己的 IO 后端。这正是“热数据在内存、冷数据在磁盘”的实现基础:

Cell存放内容访问模式建议放置
graph邻近图的邻接表每一跳都读内存(内存紧张时可用 mmap_io
base用于遍历和剪枝的量化码每一跳都读内存
precise用于重排的高精度副本(use_reorder只为少量入围候选读取磁盘
raw_vector可选的原始向量(store_raw_vector很少访问,如 cosine / 精确计算内存或磁盘

可分配给 cell 的 IO 后端:

后端(*_io_type位置是否需要 *_file_path说明
memory_io内存(连续)基础内存存储
block_memory_io内存(分块分配)大 cell 的默认后端
buffer_io磁盘(带缓冲 pread通用磁盘读取,各平台可用
mmap_io磁盘(mmap + 页缓存)工作集能放入页缓存时接近内存速度
async_io磁盘(Linux libaio)高并发磁盘读;仅限 Linux + libaio,否则回退 buffer_io
uring_io磁盘(Linux io_uring)可选 io_uring 后端;需以 ENABLE_LIBURING=ON 构建,否则回退 buffer_io
reader_io自定义 Reader加载期通过用户 ReaderSet 读取(如远程 / 对象存储)

每个 cell 通过一组扁平参数配置:graph_io_type / graph_file_pathbase_io_type / base_file_pathprecise_io_type / precise_file_path,以及 raw_vector_io_type / raw_vector_file_path。当对应的 *_io_type 为磁盘型(buffer_iommap_ioasync_io)时, *_file_path 必填;这里的磁盘型后端包括 buffer_iommap_ioasync_iouring_io。内存型后端会忽略路径。所有 cell 默认均为 block_memory_io(全内存)。

启用 uring_io

uring_io 是可选的 Linux 后端。安装 liburing,并在配置 CMake 时开启:

cmake -S . -B build-release \
    -DCMAKE_BUILD_TYPE=Release \
    -DENABLE_LIBURING=ON \
    -DENABLE_EXAMPLES=ON
cmake --build build-release --target 325_feature_uring_io -j

它可以用于支持磁盘 IO 的 cell,例如:

{
    "index_param": {
        "base_io_type": "uring_io",
        "base_file_path": "/data/vsag/hgraph_base.data",
        "base_direct_read": true
    }
}

HGraph 的 base_direct_readprecise_direct_read 默认都是 false。将对应参数设为 true,可让 uring_io 通过 direct IO 打开文件并绕过页缓存。请确认目标文件系统支持 direct IO,并针对实际负载对比两种模式。

如果 VSAG 构建时没有找到 liburing,相同配置仍可加载,但会打印一次性告警并回退到 buffer_io。回退后功能保持可用,但不具备 io_uring 的提交/完成批处理能力。完整加载示例见 examples/cpp/325_feature_uring_io.cpp

推荐配置:base 留内存,precise 落磁盘

最常用的分层方案:内存里保留极为紧凑的 3 位 RaBitQ base 用于遍历,把精度更高的 sq8 副本下沉磁盘, 并开启 use_reorder,让入围候选用它重排:

{
    "dtype": "float32",
    "metric_type": "l2",
    "dim": 128,
    "index_param": {
        "base_quantization_type": "rabitq",
        "rabitq_bits_per_dim_base": 3,
        "max_degree": 32,
        "ef_construction": 400,
        "use_reorder": true,
        "precise_quantization_type": "sq8",
        "precise_io_type": "async_io",
        "precise_file_path": "/data/vsag/hgraph_precise.data"
    }
}

其中 rabitq_bits_per_dim_base 选择标准多 bit RaBitQ 的 base 编码位数(范围 [1, 8]);不要设置 rabitq_bits_per_dim_precise,以保持 base 为普通 RaBitQ 编码,而非切换到 x+y split 变体。

搜索方式不变——照常设置 ef_search,重排会透明地从磁盘读取精确副本:

{"hgraph": {"ef_search": 200}}

单次查询的数据流:图遍历只读内存中的 base 量化码与 graph 邻接表;磁盘 I/O 仅发生在最后, 由重排为少量候选读取精确副本,然后返回 top-k。这正是磁盘 HGraph 每次查询只增加有限次读取、 而非每跳一次读取的原因。

硬件与部署

  • 使用 NVMe SSD。 磁盘向量检索的瓶颈在随机读延迟;NVMe 比 SATA SSD 低一个数量级,对 async_io / mmap_io 是必需的。
  • async_io 需要 Linux + libaio,且默认开启。 CMake 选项 ENABLE_LIBAIO 默认为 ON, Makefile 也会自动传入 VSAG_ENABLE_LIBAIO=ON;只有当此前的构建关闭过 libaio 时,才需要用这些 开关把它重新打开。当 libaio 缺失时(包括 macOS),async_io 会打印一次性告警并回退 buffer_io,配置因此保持可移植但失去异步批量能力。生产吞吐场景请在 Linux 上编译进 libaio。
  • uring_io 需要 Linux + liburing,且默认关闭。 使用 -DENABLE_LIBURING=ON 开启;否则会回退到 buffer_io。请在目标内核、队列深度和 SSD 上实测 uring_ioasync_io,不要假定其中一个始终更快。
  • 预热页缓存。mmap_io 的 cell,加载后先做一次预热(如顺序读一遍文件、或跑一轮预热 查询),避免最初的查询承担冷未命中延迟。
  • 规划文件路径与生命周期。 磁盘型 cell 会写入你提供的 *_file_path;请放在快速、专用、 容量足够的卷上,并在重建时清理陈旧文件。索引的序列化与加载走常规 序列化格式 API,后端文件随之管理。

容量规划

主要量化器的每向量近似存储(另加少量每向量元数据,如范数与误差):

表示每向量字节数典型放置
fp32(精确)dim × 4磁盘
fp16 / bf16dim × 2内存或磁盘
sq8dim × 1内存或磁盘
sq4dim × 0.5内存
rabitq(b 位)dim × b / 8内存

N = 1e9dim = 128 为例:

  • 内存中的 3 位 rabitq base:1e9 × 128 × 3 / 8 ≈ 48 GB 内存。
  • 磁盘上的 sq8 精确副本:1e9 × 128 × 1 ≈ 128 GB SSD。
  • 若改用全精度 fp32(重排精度最高):1e9 × 128 × 4 ≈ 512 GB SSD。
  • 再加上图:邻居 id 约 N × max_degree × 4 字节(内存或 mmap_io)。

这就是一个若以纯 fp32 需要约 0.5 TB 内存的十亿级索引,如何被压缩到几十 GB 内存 + 一块 SSD。

调优与排错

现象可能原因处理
召回过低base 量化过粗,或未开启重排保持 use_reorder: true;将 precise_quantization_type 提升至 fp32;增大 ef_search
延迟过高每次查询磁盘读取过多降低 ef_search;把 graph/base 留内存;仅让 precise 落磁盘;用 NVMe + async_io
内存仍然偏高base 或 graph 过大把 base 改为 sq4 / pq / rabitq;把 graph 放到 mmap_io
async_io 表现为同步未编译 libaio在 Linux 上以 VSAG_ENABLE_LIBAIO=ON 重新编译;留意回退告警
冷启动延迟抖动(mmap)页缓存未预热加载后、对外服务前先预热文件

以上仅为起点,请用 eval_performance 在真实查询分布上验证;随后可用 优化器 自动确定搜索期参数。

参见

VSAG 中的度量语义

本页说明 VSAG 对 l2ipcosine 的实际处理方式。

警告:VSAG 的内部度量实现是为了性能和一致性做过优化的, 其行为可能与教科书上的数学定义不完全一致。做结果对比或准备 真值时,请以本页描述的语义为准。

VSAG 的搜索接口统一采用“越小越好”的距离模型。为保证性能和跨索引行为一致, 内部实现常会复用平方距离、归一化向量或模长缓存。

l2

  • 距离计算公式为 L2Sqr(L2 平方距离)。
  • 内部很多内核会直接使用 L2Sqr 来加速计算。
  • 平方形式是为了性能考虑,排序结果仍与 L2 距离一致。返回的距离值和 范围搜索阈值都是平方后的值。

ip

  • 距离计算公式为 1 - inner_product
  • 内积越大,距离越小。

cosine

  • 距离计算公式为 1 - cosine_similarity
  • 为了性能,某些实现会先归一化向量,或保存额外的模长信息, 以复用面向 IP 的计算内核。

cosine 搜索通常假定内部计算路径使用归一化向量。由于实现可能会 执行归一化或缓存模长,返回值的语义目标仍是“距离”,但浮点误差 可能使结果略微偏离理论值域。

返回值范围

  • l20+infinity
  • ip:无上界;当 inner_product > 1 时,值可能为负
  • cosine:理论上在 cosine similarity 落在 [-1, 1] 时为 02, 但浮点误差下可能略微越界

为什么要说明这一点

  • 数据集真值、查询语义和索引内部实现必须使用同一套度量约定。
  • 索引构建后,l2ipcosine 不能互相切换。
  • 跨工具对比结果时,要先确认对方使用的是“距离”还是“相似度”语义。

相关页面

优化器(Tune)

对于图类索引(HGraph),VSAG 提供 Tune 接口,根据给定查询集自动调整运行期参数以在召回率延迟之间取得更好的权衡。其底层实现即历史版本中的 ELP Optimizer。

基本用法

#include <vsag/vsag.h>

auto index = vsag::Factory::CreateIndex("hgraph", build_params).value();
index->Build(base_dataset);

std::string tune_params = R"(
{
    "queries_dataset": "path/or/inline/queries",
    "target_recall": 0.95,
    "top_k": 10
}
)";
auto ret = index->Tune(tune_params);

Tune 的第二个参数 disable_future_tuning=false 默认允许后续多次调用继续调整;设为 true 会冻结参数。

与 ELP Optimizer 的关系

历史文献(见 科研论文)中提到的 “ELP Optimizer” 对应实现键 use_elp_optimizer,现已收敛到统一的 Tune 接口背后,用户无需直接操作。

适用索引

索引类型支持 Tune
hgraph
ivf / sindi / brute_force

示例

examples/cpp/318_feature_tune.cpp 给出了端到端的调优流程:

  1. 构造索引并 Build
  2. 使用一份代表性查询集调用 Tune
  3. 序列化调优后的索引供生产环境使用。

注意事项

  • 调优依赖查询集的分布,建议使用真实业务分布下的样本。
  • 调优后的参数会随索引元信息一起 Serialize / Deserialize,部署后仍然生效。

标准环境性能参考

本页作为官方性能数据的入口与说明。具体数值建议以 GitHub 最新发布的 benchmark 结果为准, 并通过 性能评估工具 在目标环境中复测。

参考机型

官方基准测试默认在以下量级的机型上进行(具体 SKU 以 Release Notes 为准):

  • CPU:主流 x86_64 服务器 CPU(支持 AVX2 / AVX-512)
  • 内存:足够覆盖索引 + 操作系统 page cache 的 DDR4/DDR5
  • 操作系统:Ubuntu 20.04 / 22.04 或 CentOS 7 / 8
  • 编译make release,MKL 默认关闭VSAG_ENABLE_INTEL_MKL=OFF)。 如需启用请显式设置 VSAG_ENABLE_INTEL_MKL=ON make release (或直接使用 CMake 时使用 -DENABLE_INTEL_MKL=ON

参考数据集

官方对比常用以下数据集(HDF5 格式,兼容 ann-benchmarks):

数据集维度距离规模
SIFT-1M128L21,000,000
GIST-1M960L21,000,000
Deep-10M96L210,000,000
Text-to-Image-1M200IP1,000,000

关键指标

  • QPS(单线程 / 多线程)
  • 平均召回率(Recall@k)
  • P50 / P95 / P99 延迟
  • 峰值内存、索引体积
  • 构建时间

如何复现

make release
./build-release/tools/eval/eval_performance --config tools/eval/eval_template.yaml

将输出的 JSON / Markdown 结果与官方对比,可定位性能回归或量化退化。

如何贡献你的数据

欢迎通过 PR 向本页面补充“其他机型下的结果“章节;提交时请附:

  • 详细 CPU / 内存 / 磁盘信息;
  • VSAG 版本(git rev-parse HEAD);
  • eval_performance 输出(建议使用 JSON + Markdown 两种格式);
  • 构建命令与环境变量(如 VSAG_ENABLE_INTEL_MKL 等)。

性能评估工具(eval_performance)

eval_performance 是 VSAG 自带的命令行性能评估工具,位于 tools/eval/,编译后二进制路径为 build-release/tools/eval/eval_performance。它可以用于对比不同索引、不同参数组合的吞吐、延迟与召回率。

构建

tools/ 默认不会编译,需要显式开启:

# 通过项目 Makefile
VSAG_ENABLE_TOOLS=ON make release
# 或:make dev

# 也可直接通过 CMake
cmake -S . -B build-release -DCMAKE_BUILD_TYPE=Release -DENABLE_TOOLS=ON
cmake --build build-release -j
# 产物:./build-release/tools/eval/eval_performance

需要系统安装 HDF5(Ubuntu: apt install libhdf5-dev;CentOS: yum install hdf5-devel)。

两种模式

1. 命令行模式(适合单次快速测试)

./build-release/tools/eval/eval_performance \
    --datapath /tmp/sift-128-euclidean.hdf5 \
    --index_name hgraph \
    --type search \
    --create_params '{"dim":128,"dtype":"float32","metric_type":"l2","index_param":{"base_quantization_type":"fp32","max_degree":32,"ef_construction":300}}' \
    --search_params '{"hgraph":{"ef_search":60}}' \
    --topk 10

常用参数还包括 --search_modeknn / range / knn_filter / range_filter)、 --search-query-count--delete-index-after-search,以及一系列用于关闭单项指标的 --disable_* 开关。参考模板 tools/eval/eval_template.yaml 展示了完整的 YAML 结构。

2. 配置文件模式(适合批量对比)

YAML 文件作为位置参数直接传入(不需要 --config 标志):

./build-release/tools/eval/eval_performance my_eval.yaml

参考模板 tools/eval/eval_template.yaml。一份配置可以包含多个具名 case,并通过可选的 global 段配置共享参数,例如线程数、导出器以及内嵌的 HTTP 监控服务。

最小示例:

global:
  num_threads_building: 8
  num_threads_searching: 16
  exporters:
    print-directly:
      to: stdout
      format: table
    save-to-file:
      to: "file:///tmp/eval_results.json"
      format: json

eval_case1:
  datapath: /tmp/sift-128-euclidean.hdf5
  type: search
  index_name: hgraph
  create_params: '{"dim":128,"dtype":"float32","metric_type":"l2","index_param":{"base_quantization_type":"fp32","max_degree":32,"ef_construction":300}}'
  search_params: '{"hgraph":{"ef_search":60}}'
  index_path: /tmp/vsag_eval/hgraph_fp32
  topk: 10

注意:global.exporters 下每一项都是具名的导出器(即 YAML map),并不是数组。

支持的评估维度

  • 效率:QPS、TPS
  • 效果:平均召回率、分位召回率(P0/P10/P50/P90…)
  • 延迟:平均延迟、P50/P95/P99 延迟
  • 资源:峰值内存占用

搜索指标计量语义

  • 延迟是每次被测 Index::KnnSearch 调用的墙钟耗时,使用单调时钟 std::chrono::steady_clock 测量。
  • QPS 是成功查询数除以被测搜索阶段的墙钟时间(秒)。
  • 统计信息提取、召回率计算和内存采样在性能测试阶段之外执行,不计入延迟 或 QPS。
  • 所有被测查询均计入结果,包括每个工作线程的首个查询。工具不会隐式预热; 如有需要,应在被测阶段之前显式执行预热。

JSON 结果会输出 measurement_methodmeasurement_sample_countmeasurement_successful_query_countmeasurement_duration(s),用于核对测量方法 及其样本范围。

旧版本使用相邻监控回调间隔计算指标,其结果不能与采用上述语义的结果 直接比较。

搜索模式

search_mode 支持 knnrangeknn_filterrange_filter 四种。

输出格式与导出目标

每个导出器同时指定一种 format 与一个 to 目标。

  • 格式:table(或别名 text)、jsonline_protocol(用于 InfluxDB)。
  • 目标:
    • stdout — 输出到标准输出。
    • file://<path> — 写入文件(覆盖)。
    • influxdb://<host>:<port>/<path>?<query> — POST 到 InfluxDB v2 接口; 需要使用 format: line_protocol,并通过 vars.token 传入鉴权令牌 (值需包含 Token 前缀,例如 Token <your-influxdb-token>)。

如未配置任何导出器,结果默认以 table 格式打印到 stdout。

HTTP 监控(可选)

启用后,工具会在批量评估运行期间启动一个内嵌 HTTP 服务,实时暴露当前进度(当前案例、 总案例数、完成百分比)和最新指标,便于长时间任务的状态观察。

global:
  http_server:
    enabled: true
    port: 8080

数据集

可使用 ann-benchmarks 提供的 HDF5 格式数据集 (如 sift-128-euclidean.hdf5gist-960-euclidean.hdf5)。

参考

  • 源码:tools/eval/
  • 本地工具入口:tools/eval/README.md
  • 标准机型的基准结果见 标准环境性能参考

AutoTune 工具(autotune

autotune 用于自动完成 VSAG 构建参数和查询参数的枚举评测。AutoTune V1 通过共享的 eval 核心运行真实 VSAG 索引,按约束过滤 trial,并为一个 KNN workload 选择最优 可行候选。

CLI JSON 请求和共用报告结构见 AutoTune V1 CLI JSON 契约

V1 支持在 dense float32 数据上构建和调优 HGraph、IVF,也支持对已有 HGraph、IVF 或 Pyramid 索引调 search 参数。CLI 读取 HDF5;C++ API 接收内存 Dataset 和 Index。C++ API 是实验性接口,只在构建目录中提供,不会随 VSAG 库安装。

编译

cmake -S . -B build-release \
  -DCMAKE_BUILD_TYPE=Release \
  -DENABLE_TOOLS=ON
cmake --build build-release --target autotune -j

可执行文件位于 build-release/tools/autotune/autotune

仓库提供了可运行的 SIFT 请求模板。 它默认读取 /tmp/sift-128-euclidean.hdf5;数据集位于其他目录时请修改 data_path

C++ API

CLI 和 C++ 入口复用同一套候选生成、trial 规划、评测、约束过滤和结果选择逻辑,区别仅在 输入适配:

  • CLI 解析 JSON 并加载 HDF5;
  • TuneIndex 接收内存 Dataset,同时调优构建参数和查询参数;
  • TuneSearch 接收已经构建或加载的 IndexPtr,只调查询参数。

链接构建目录中的 vsag::autotune target 后,可以直接完成“调优、选择、加载”闭环:

vsag::autotune::IndexRequest request;
request.base = base;
request.metric_type = vsag::METRIC_L2;
request.workload = {queries, ground_truth, 10, 48};
request.index_spaces = {
    {"hgraph", create_candidate_space, search_candidate_space},
};
request.constraints = {
    {vsag::autotune::Metric::RECALL_AT_K, 0.95},
    {vsag::autotune::Metric::INDEX_MEMORY_MB, 8192.0},
};
request.objective = vsag::autotune::Metric::LATENCY_AVG_MS;

auto result = vsag::autotune::TuneIndex(request);
if (result.has_value() && result->status == vsag::autotune::TuneStatus::SUCCESS) {
    auto neighbors =
        result->index->KnnSearch(query, 10, result->search_parameters).value();
}

basequeries 必须是 dense float32 Dataset,base ID 必须存在且唯一。ground truth 的 NumElements() 等于 query 数量,Dim() 表示 ground-truth K,Ids() 是来自 base ID 空间的 展平数组。当 RECALL_AT_K 是约束或目标时必须提供 ground truth。Dataset 的 buffer 需保持有效, 直到这个同步调用返回。

每个索引拥有不同的原生参数模式,因此 IndexSpace::create_parameter_spacesearch_parameter_space 仍使用 JSON 字符串。index_spaces 为空时使用内置 HGraph 和 IVF 候选;显式参数中的数组和 $range 与 CLI 契约语义相同。

返回值包括已加载的 indexindex_name、完整具体 create_parameters、经过验证的 search_parameters、metrics、artifact 路径和完整报告。metricsreport 都是 JsonType 对象。typed API 在内存中返回报告,绝不会写报告文件。最终推荐 artifact 会 保留,因此可以用 index_name + create_parameters 重新创建空索引并反序列化该文件; 未选中的中间 artifact 默认删除。调用方应在不再需要时删除推荐 artifact。

如果没有候选满足全部约束,调用仍正常完成,并返回 status=TuneStatus::NO_FEASIBLE_CANDIDATE 和结构化 best_effort;此时 index 等推荐 字段无效。请求非法或执行失败时仍然返回 error。

完整示例见 examples/cpp/326_feature_create_index_with_constraints.cpp。同时启用工具和 示例即可编译:

cmake -S . -B build-release \
  -DCMAKE_BUILD_TYPE=Release \
  -DENABLE_TOOLS=ON \
  -DENABLE_EXAMPLES=ON
cmake --build build-release --target 326_feature_create_index_with_constraints -j

请求

将下面的请求保存为 request.jsondimdtypemetric_type 会从数据集读取,用户 不需要重复填写。

{
  "version": 1,
  "data_path": "/tmp/sift-128-euclidean.hdf5",
  "indexes": [
    {
      "name": "hgraph",
      "create_params": {
        "index_param": {
          "base_quantization_type": ["fp32", "sq8_uniform"],
          "max_degree": [16, 32],
          "ef_construction": 200
        }
      },
      "search_params": {
        "hgraph": {
          "ef_search": {
            "$range": {"start": 40, "stop": 1000}
          }
        }
      }
    }
  ],
  "workload": {
    "top_k": 10,
    "concurrency": 1
  },
  "constraints": {
    "recall_at_k": 0.95,
    "index_memory_mb": 8192
  },
  "objective": {
    "metric": "latency_avg_ms"
  },
  "tuning_config": {
    "workspace_path": "/tmp/vsag_autotune",
    "keep_intermediate": false,
    "max_trials": 1000
  }
}

运行方式:

./build-release/tools/autotune/autotune request.json

参数叶子可以是标量、数组,或带有 startstopstep$range。HGraph ef_search 还可以省略 step;AutoTune 会从 start 倍增,找到首个满足 recall_at_k 约束的区间,再在该区间内二分查找最小通过值。该策略假设固定其他配置时 recall 不会随 ef_search 增大而降低。每个实际评测点都使用全部 query。用户显式提供的值保持不变; 对于缺失的调优字段,V1 会补充一小组 HGraph 或 IVF 内置候选。省略 indexes 时会同时 评测这两种索引。

具体 create 参数相同的候选只构建一次;每组具体 search 参数仍然是独立 trial。自适应 ef_search 范围只记录实际评测过的具体参数点。

已有索引

设置 index_path 并且只提供一个索引规格,可以跳过构建、只调查询参数。其中 create_params 必须是序列化该索引时使用的具体参数。该模式不会展开 create_params, 因此索引原生的数组字段会保持不变。

程序调用方使用独立的 SearchRequestTuneSearch

vsag::autotune::SearchRequest request;
request.index = existing_index;
request.workload = {queries, ground_truth, 10, 48};
request.parameter_space = R"({"hgraph":{"ef_search":[40,80,120]}})";
request.constraints = {{vsag::autotune::Metric::RECALL_AT_K, 0.95}};
request.objective = vsag::autotune::Metric::LATENCY_AVG_MS;

auto result = vsag::autotune::TuneSearch(request);
if (result.has_value() && result->status == vsag::autotune::TuneStatus::SUCCESS) {
    auto neighbors = existing_index->KnnSearch(query, 10, result->parameters).value();
}

该接口避免序列化和文件 I/O,并让全部 search trial 复用调用方的索引。AutoTune 从 IndexPtr 推导索引类型和元素数量,因此 search tuning 只需要 query,不需要 base dataset 或 metric type。使用 recall 时还需要 ground truth:每条 query 的 recall_at_k 定义为 返回结果和 ground truth 前 top_k 个 ID 的交集大小除以 top_k,最终指标是所有 query 的平均值。recall 既不是约束也不是目标时,ground truth 可省略。

CLI 的 index_path 仍是离线适配:它先使用具体 create 参数创建并反序列化 Index,再进入 同一条 search-only 流程。

Pyramid 通过这个 search-only 模式接入。HDF5 CLI 适配器不提供 query path,因此只能评测 Pyramid 原生的默认/root 搜索。typed 请求直接从 workload.queries->GetPaths() 取得 path: 提供 path 时,每条 query 按自己的 path 评测;不提供时沿用 root 搜索语义。V1 只支持默认 未命名 hierarchy。若不同 path 需要不同 ef_search,应为每个代表性 path workload 分别 发起一次 typed AutoTune 请求,并提供与该 path 对应的 ground truth。V1 不把多个 path 的 推荐聚合成一条结果。

完整示例见 examples/cpp/327_feature_autotune_existing_index.cpp。 Pyramid path 调优示例见 examples/cpp/328_feature_autotune_existing_pyramid.cpp。 它对同一个 Pyramid 索引中的 512-vector 和 4096-vector 叶子子图使用相同 recall 目标分别 调优,用于展示不同 path 可能需要不同的 ef_search

指标

以下指标可以作为约束或优化目标:

指标约束比较方式
recall_at_kqps不低于请求值
latency_avg_mslatency_p99_ms不高于请求值
index_memory_mbindex_size_mb不高于请求值
build_secondssearch_secondsbuild_and_search_seconds不高于请求值

已有索引模式不提供 build_secondsindex_size_mbbuild_and_search_secondsindex_memory_mb 可以作为约束,但不能作为目标,因为同一个已有索引的所有 search 候选 内存相同,无法据此排序。目标方向由指标本身决定,因此不需要 direction 字段。

AutoTune 使用独立的 latency/QPS pass 和 recall/statistics pass,因此同一条逻辑 query 在 每个 trial 中会执行多次。search_seconds 包含两个内存 pass 和指标采集,但不包含索引 反序列化。候选按固定顺序运行,不做 warm-up 或随机排序。如果 latency 或 QPS 决定最终 推荐,应在有代表性且受控的机器上测量,并通过重复实验确认。

阅读输出

标准输出是便于阅读和复制的短 summary,字段顺序如下:

  • recommendation:最终选中的具体参数、指标和 trial 证据。build-and-search 结果还包含 create 参数和 artifact/build 证据;search-only 结果不包含这些字段。
  • best_effort:只有在没有 trial 满足全部约束时出现。
  • failure:请求失败或所有评测失败时出现。
  • report_path:完整可复现报告的位置。

完整报告还包含每个 build 和 trial、约束 violation、补齐后的请求,以及分阶段的结构化 失败。search-only 模式的 builds 为空,trial 不包含 artifact 或 build 字段。CLI 和 typed TuneIndex 设置 keep_intermediate=false 时都只保留最终推荐索引,并删除未选中的 artifact;设置为 true 时保留所有生成索引。

请求通过初始校验后,CLI 和 RunAutoTune 会持久化完整报告:显式 output.result_path 会覆盖输出路径,否则在 workspace 下生成默认路径。早期校验和请求文件错误不会写报告。 typed TuneIndexTuneSearch 绝不会写报告文件,而是通过返回值提供完整的 JsonType 报告。

V1 边界

V1 评测一个 KNN workload;除已支持的 HGraph ef_search 自适应搜索外,其他候选仍完整遍历。 它暂不支持过滤或范围查询 workload、query sampling、跨请求 build cache,以及基于模型的 候选生成。

AutoTune V1 CLI JSON 输入输出契约

本文档定义 autotune 命令行工具使用的离线 JSON/HDF5 适配契约。实验性的 C++ API 只在 构建目录中提供,接收 typed IndexRequestSearchRequest,不会随 VSAG 库安装。所有 入口完成输入转换后使用同一个规范化请求和调优引擎。AutoTune V1 支持 dense float32 数据、 HGraph/IVF 的构建和查询调优,以及一个使用全量 query 的 KNN workload。

请求、索引规格、workload、objective、tuning_configoutput 中的未知字段会被拒绝。 create_paramssearch_params 内部的原生参数会传给具体 VSAG 索引,在创建或查询时由 索引校验。

请求对象

字段类型必填含义
versioninteger必须为 1
data_pathstring可读的 HDF5 评测数据集。
index_pathstring用于 search-only 调优的已有序列化索引。
indexesarray索引规格;普通模式默认使用 HGraph 和 IVF。
workloadobject要优化的 KNN workload。
constraintsobject非空的“指标到阈值”映射。
objectiveobject对可行候选排序的目标指标。
tuning_configobjectworkspace、产物保留和 trial 上限。
outputobject报告路径和原始 eval 输出开关。

数据集规则

data_path 必须指向 VSAG eval 工具可读取的普通 HDF5 文件。V1 要求:

  • dense vector;
  • base 和 query 均为 float32;
  • 至少包含一个 base、一个 query 和一个 ground-truth neighbor;
  • 使用 recall 指标时,workload.top_k 不超过 ground truth 的宽度;
  • distance 属性能够映射为 l2ipcosine

AutoTune 从数据集读取 dimdtypemetric_type,并注入每组具体 create_params。请求可以重复提供这些字段,但值必须与数据集完全一致。

索引规格

每个 indexes[] 元素的形态如下:

{
  "name": "hgraph",
  "create_params": {
    "index_param": {
      "max_degree": [16, 32]
    }
  },
  "search_params": {
    "hgraph": {
      "ef_search": {"$range": {"start": 40, "stop": 1000}}
    }
  }
}
字段类型必填含义
namestringhgraphivf,或 search-only 的 pyramid;不区分大小写。
create_paramsobjectVSAG 创建参数或候选表达式。
search_paramsobject查询参数,key 必须是同一个索引名。

普通 build-and-search 模式省略 indexes 时,AutoTune 会同时评测 HGraph 和 IVF。设置 index_path 时,必须提供 indexes,并且只能包含一个元素。 Pyramid 只接受已有的内存索引或序列化索引。

候选表达式

create_paramssearch_params 中的每个叶子都可以写成:

  • 标量:固定该值;
  • 非空数组:提供离散候选值;
  • $range:必须且只能包含 startstop 和非零 step,范围包含端点。

示例:

{
  "fixed": 32,
  "choices": [16, 32, 48],
  "integer_range": {"$range": {"start": 40, "stop": 120, "step": 40}},
  "float_range": {"$range": {"start": 0.1, "stop": 0.3, "step": 0.1}}
}

HGraph ef_search 还支持一种特殊写法:

{"$range": {"start": 40, "stop": 1000}}

上下界必须是正整数并满足 start <= stop。这种写法要求存在 recall_at_k 约束,且目标为 latency_avg_mslatency_p99_msqpssearch_secondsbuild_and_search_seconds;不能同时设置 timeout_mshops_limit。对于其他参数的每个 固定组合,AutoTune 从 start 开始倍增 ef_search(不超过 stop),直到实测 recall 满足 约束;随后在最后一个失败/通过区间内二分,查找最小通过值。分支方向只由 recall 决定; latency 和 QPS 仍作为实测指标参与最终约束过滤和结果选择。该策略假设其他参数和 workload 固定时,recall 不会随 ef_search 增大而降低。

每个探测点都会评测完整 query workload。如果某个评测点执行失败或没有 recall,AutoTune 会停止该自适应范围,因为此时无法安全选择下一个区间。

V1 会把参数对象中的所有 JSON 数组和带 step 的范围解释成候选集合,计算完整笛卡尔积 并去除完全相同的候选。其他参数不接受省略 step 的范围。

计划的最坏评测次数超过 tuning_config.max_trials 时,请求失败。普通具体候选占一个 trial;自适应 ef_searchstart == stop 时占一个。否则,planner 会为每个可能的首次 通过探测点计算“到达该点的探测次数 + 前一区间的二分次数”,并取其中最大值。例如 40..1000 最坏预留 15 个 trial。

缺失字段的内置候选

用户显式字段始终优先。只有字段缺失时才补充以下候选:

索引缺失字段V1 候选
HGraphbase_quantization_typefp32sq8_uniform
HGraphmax_degree1632
HGraphef_construction100200
HGraphef_search根据 top_k 生成三个值,下限为 4080120
Pyramid,search-onlyef_search根据 top_k 生成三个值,下限为 4080120
IVFbase_quantization_typefp32sq8_uniform
IVFbuckets_count不超过 base 数量的 10242048
IVF,build-and-searchscan_buckets_count根据具体 buckets_count 生成至多四个值
IVF,search-onlyscan_buckets_count141664

HGraph 和 Pyramid 查询候选具体为 max(40, top_k)max(80, 2 * top_k)max(120, 4 * top_k),重复值会删除。build-and-search 模式的 IVF 查询候选不会超过具体 bucket 数量。AutoTune 无法推断已有 IVF 的 bucket 数量,因此 search-only 模式使用上面的 保守通用值;被已加载索引拒绝的值会记录为 failed trial,调用方可以显式提供 scan_buckets_count 候选以避免这些 trial。

规范化索引名和具体 create_params 相同的候选属于同一个 build group。AutoTune 对该组 只构建一次、序列化一次作为证据,再使用同一个内存索引实例执行所有关联的 search 候选。

已有索引模式

设置 index_path 后:

  • 路径必须指向可读普通文件;
  • 必须且只能提供一个索引规格;
  • 必须提供 create_params.index_param
  • create_params 整体视为一份具体的索引原生配置,不再做候选展开;
  • AutoTune 不补充 create 候选,也不会执行 build;
  • 缺失的 search 字段仍可使用内置查询候选;
  • build_secondsindex_size_mbbuild_and_search_seconds 不能作为约束或目标;
  • index_memory_mb 可以作为约束,但不能作为目标;
  • 不会删除调用方提供的索引。

V1 反序列化前需要使用具体创建参数实例化正确的 VSAG 索引,目前不会从序列化文件推导 这些参数。

对于 typed SearchRequest,索引已经完成实例化和加载。AutoTune 从 IndexPtr 推导索引 类型和元素数量;请求只提供 index、workload、search parameter_space、constraints、 objective 和 config,不需要 create 参数、base dataset 或 metric type。

HDF5 适配器不提供 Pyramid query path,因此 CLI 只能评测 Pyramid 原生的默认/root 查询。 按 path 调优 Pyramid 需要使用 typed SearchRequest,并通过 query Dataset 的 Dataset::Paths() 提供 path。V1 只转发默认的未命名 hierarchy,不支持 Pyramid named hierarchy。

Workload

{
  "workload": {
    "top_k": 10,
    "concurrency": 48
  }
}
字段类型必填默认值校验
top_kpositive integer不超过 base 数量;使用 recall 指标时不超过 ground truth 宽度;并能用原生 int 表示。
concurrencypositive integer1最大为 200

V1 始终评测 HDF5 文件中的全量 query,并且只支持 KNN。benchmark 机器、系统负载和运行 环境都属于 workload 的一部分;延迟和 QPS 不能直接移植到不同机器规格或负载条件。 对于 typed SearchRequesttop_k 会和 IndexPtr::GetNumElements() 比较,不依赖 base dataset。只有 recall 是约束或目标时才必须提供 ground truth。

约束和优化目标

constraints 直接把指标名映射到阈值;objective.metric 指定一个目标指标。比较方式和 目标方向由指标决定,用户不能覆盖。

{
  "constraints": {
    "recall_at_k": 0.95,
    "latency_p99_ms": 2.0,
    "index_memory_mb": 8192
  },
  "objective": {
    "metric": "latency_avg_ms"
  }
}
指标约束目标方向已有索引用法
recall_at_kactual ≥ threshold最大化
qpsactual ≥ threshold最大化
latency_avg_msactual ≤ threshold最小化
latency_p99_msactual ≤ threshold最小化
index_memory_mbactual ≤ threshold最小化仅约束
index_size_mbactual ≤ threshold最小化
build_secondsactual ≤ threshold最小化
search_secondsactual ≤ threshold最小化
build_and_search_secondsactual ≤ threshold最小化

阈值必须是有限非负数,recall_at_k 还必须不大于 1.0

V1 指标口径:

  • 每条 query 的 recall_at_k 是返回结果和 ground truth 前 top_k 个 ID 的交集大小除以 top_k,最终指标是所有 query 的平均值。它需要 ground truth,但不需要 base vector 或 metric type。
  • build_seconds 是 eval 工具报告的索引 Build 操作耗时。
  • 它不包含索引序列化、数据集加载和候选编排。
  • search_seconds 是完整内存 search eval trial 的墙钟时间,不包含索引反序列化,但包含 所有 search pass 和指标采集;线上查询性能目标应使用 latency 或 QPS。
  • build_and_search_seconds 是一次 build-and-search trial 中前两项之和。
  • index_memory_mb 来自已加载索引大于零的 GetMemoryUsage() 结果。索引报告零时, AutoTune 会把该指标视为不可用,而不是零内存;相关约束会记录 missing-metric violation。
  • index_size_mb 是生成的序列化索引文件大小,在 search-only 模式不可用。
  • AutoTune 使用独立的 latency/QPS pass 和 recall/statistics pass。latency 用 steady_clock 包围每次 KnnSearch;QPS 是成功查询数除以并发性能 pass 的墙钟时间。 因此,同一条逻辑 query 在每个 trial 中会执行多次。

search_seconds 仍表示评测成本,因为它包含两个 pass 和 monitor 处理;它不是一次线上 逻辑 query batch 的执行时间。候选会按固定顺序在复用的索引上运行,不做 warm-up 或随机 排序,因此 cache 状态和无关系统负载会影响性能指标。如果 latency 或 QPS 决定结果,应在 有代表性且受控的环境中运行,并通过重复实验确认。

Tuning 和输出配置

{
  "tuning_config": {
    "workspace_path": "/tmp/vsag_autotune",
    "keep_intermediate": false,
    "max_trials": 1000
  },
  "output": {
    "result_path": "/tmp/vsag_autotune/report.json",
    "include_raw_eval": false
  }
}
字段默认值含义
tuning_config.workspace_path/tmp/vsag_autotunerun 产物和 CLI 默认报告目录。
tuning_config.keep_intermediatefalse是否保留所有生成索引。
tuning_config.max_trials1000计划最坏 trial 数上限;硬上限为 100000
output.result_path<workspace>/run-<id>.jsonCLI 完整报告路径。
output.include_raw_evalfalse在 build/trial 中包含原生 eval JSON。

output.result_path 不能与 data_pathindex_path 指向同一文件。通过初始校验后, 离线 JSON/CLI 入口会写完整报告:显式 output.result_path 会覆盖输出位置,否则 AutoTune 在 workspace_path 下生成报告路径。早期校验和请求文件错误不会写报告。

typed TuneIndexTuneSearch 不接收报告路径,也绝不会写报告文件,而是通过结果对象 返回完整的 JsonType 报告。CLI 和 typed TuneIndex 设置 keep_intermediate=false 时都只保留推荐索引并删除未选中 artifact;设置为 true 时保留 所有生成索引。没有 recommendation 时,默认删除所有生成索引。TuneSearch 不生成 artifact,因此 workspace_pathkeep_intermediate 对它没有影响。

完整报告

完成评测后会返回以下顶层结构;CLI 还会把它写入磁盘:

{
  "version": 1,
  "status": "success",
  "recommendation": {},
  "best_effort": null,
  "builds": [],
  "trials": [],
  "request": {},
  "elapsed_seconds": 84.2,
  "report_path": "/tmp/vsag_autotune/run-....json"
}
字段含义
version报告契约版本,固定为 1
statussuccessno_feasible_candidatefailed
recommendation最优可行 trial;不存在时为 null
best_effort约束不可行时最接近的成功 trial,否则为 null
builds每组具体生成 build 一条记录;search-only 模式为空数组。
trials每组已执行的具体 search 候选一条记录。
request调优引擎实际使用的有效规范化请求。
elapsed_secondsAutoTune 墙钟时间包含 artifact 清理,不含最终报告写入。
report_path持久化完整报告路径;只在 CLI/JSON 适配器中存在。
failure整体执行失败时出现。

早期校验失败不包含 buildstrialsreport_path,也不会写报告。如果所有候选评测 失败,CLI 只要报告路径仍然可用,就会保留尝试过的 build/trial 记录并写报告。typed API 的报告绝不会包含 report_path

规范化 request 包含推导出的 dataset 元数据、workload 默认值、constraints、objective、 config 和 output。build-and-search 模式还包含 index_spaces;search-only 模式包含 index_nameparameter_space。CLI 报告还会在这个规范化对象顶层保留离线 data_path,存在已有索引输入时也保留 index_path

Status 语义

  • success:至少一个成功 trial 满足全部约束,设置 recommendation
  • no_feasible_candidate:存在成功 trial,但没有任何一个满足全部约束;设置 best_effort 用于解释,它不是有效推荐。typed 调用会返回带 TuneStatus::NO_FEASIBLE_CANDIDATE 的值,而不是 error。
  • failed:请求非法、执行或报告阶段失败,或者没有成功且带目标指标的 trial。

Recommendation 和 Best Effort

build-and-search 模式下,recommendation 包含:

{
  "index_name": "hgraph",
  "create_params": {},
  "search_params": {},
  "workload": {"top_k": 10, "concurrency": 1},
  "metrics": {},
  "artifacts": {},
  "evidence": {"build_id": "build-0", "trial_id": "trial-0"}
}

search-only 模式会省略 create_paramsartifactsevidence.build_id,只包含具体 查询参数、workload、metrics 和 evidence.trial_idbest_effort 使用相同的模式相关 字段,并额外包含 constraint_evaluation。它首先按违反或缺失约束数量选择,再比较归一化 violation 大小,只用于解释。

Build 记录

search-only 模式的 builds 为空。build-and-search 模式下,每个 builds[] 元素包含:

字段含义
build_id被 trial 引用的稳定 ID。
index_name具体索引类型。
create_params具体创建参数。
statussuccessfailed
metrics可用的 build 共享指标。
artifactssourceindex_pathuse_existing_indexretained
failure结构化失败或 null
elapsed_secondsbuild group 准备耗时。
raw_eval_result请求原始输出且真实 build eval 成功时出现。

Trial 记录

每个 trials[] 元素包含:

字段含义
trial_id稳定 trial ID。
build_id关联的 build group;仅 build-and-search 模式存在。
index_name具体索引类型。
create_params具体创建参数;仅 build-and-search 模式存在。
search_params具体查询参数。
statussuccessfailed
metrics用于约束评估和选择的可用指标。
constraint_evaluationsatisfiedviolations 数组。
artifacts从 build group 复制的产物证据;仅 build-and-search 模式存在。
failure结构化失败或 null
elapsed_secondssearch trial 墙钟时间。
raw_eval_result请求原始输出且原生 search eval 成功时出现。

对于自适应 HGraph ef_search 范围,报告会为每个实际评测点记录一条 trial。每条记录中的 search_params 都是具体值;$range 表达式只保留在规范化请求中。每条记录都会评测完整 query workload。

每条 constraint violation 包含 metriccomparisonexpectedactual。指标缺失或 非有限数时,actualnull

同一 build group 的 search trial 复用同一个已加载索引实例。新生成的索引只构建和序列化 一次。search-only 模式直接为所有 trial 复用调用方索引,或复用 CLI 适配器反序列化得到的 索引。

Artifact 语义

V1 只在 build-and-search 记录中提供 artifact 字段。artifacts.sourcegeneratedartifacts.index_path 用于说明被评测索引曾存放在哪里,不保证响应返回时路径仍然存在。 需要检查 artifacts.retained

  • true:这是最终推荐产物,或者请求保留所有生成索引;
  • false:AutoTune 计划删除或已经删除生成产物。

结构化失败

所有失败使用统一结构:

{
  "stage": "validation",
  "code": "invalid_request",
  "message": "request.workload.top_k is required"
}

常见 stage 包括 clivalidationcandidate_generationbuildsearchevaluationselectionreport。build/trial 失败保留在对应记录中,其他候选可以 继续执行。

StageCode含义
clirequest_file_errorCLI 无法读取或解析请求文件。
validationinvalid_request请求校验失败。
candidate_generationinvalid_request候选表达式或数量非法。
buildbuild_evaluation_failed一组 build 失败。
searchbuild_failed关联 build 失败,search 被跳过。
searchsearch_evaluation_failed一组 search trial 失败。
evaluationall_trials_failed所有候选 trial 都执行失败。
selectionobjective_metric_unavailable存在成功 trial,但都没有产生目标指标。
evaluationreportexecution_failed顶层执行或报告写入失败。

CLI Summary

CLI 按以下顺序输出完整报告的紧凑子集:

  1. recommendation(存在时);
  2. best_effort(存在时);
  3. failure(存在时);
  4. status
  5. elapsed_seconds
  6. report_path(存在时);
  7. version

标准输出会省略值为 null 的结果分支和详细 build/trial 数组。完整证据请读取 report_path

CLI 在 status=failed 或命令行/请求文件出错时返回 1successno_feasible_candidate 都返回 0;调用方必须检查 status,不能只用退出码判断是否存在 recommendation。

Build-tree C++ 入口

实验性的可选 CMake target vsag::autotune 暴露 tools/autotune/autotune.h 中的接口。 该 target 和头文件只在构建目录中提供,不会安装:

tl::expected<IndexResult, Error> TuneIndex(const IndexRequest& request);
tl::expected<SearchResult, Error> TuneSearch(const SearchRequest& request);
JsonType RunAutoTune(const JsonType& request);  // CLI adapter

JSON 入口只负责离线适配:加载 data_path;存在 index_path 时创建并反序列化索引;随后 构造与 typed 入口相同的内部请求。V1 不安装该 target 和头文件。由于底层 eval 路径会配置 进程级 OpenMP 状态,同一进程内的多次调用会串行执行。

TuneIndex 同时调优构建参数和查询参数:

IndexRequest request;
request.base = base;
request.metric_type = METRIC_L2;
request.workload = {queries, ground_truth, 10, 48};
request.index_spaces = {{"hgraph", create_candidate_space, search_candidate_space}};
request.constraints = {{Metric::RECALL_AT_K, 0.95}};
request.objective = Metric::LATENCY_AVG_MS;
auto result = TuneIndex(request);

返回值包含已加载、可查询的推荐索引,以及具体 create 和 search 参数。metrics 和完整 report 都以 JsonType 返回;typed 调用绝不会持久化报告。TuneSearch 对已经构建或 加载的索引调查询参数:

SearchRequest request;
request.index = existing_index;
request.workload = {queries, ground_truth, 10, 48};
request.parameter_space = search_candidate_space;
request.constraints = {{Metric::RECALL_AT_K, 0.95}};
request.objective = Metric::LATENCY_AVG_MS;
auto result = TuneSearch(request);

AutoTune 从 existing_index 推导类型和元素数量,因此 TuneSearch 不需要 base vector 或 metric type。只有 recall 是约束或目标时才需要 ground truth。TuneSearch 不提供构建阶段 指标和 index_size_mbindex_memory_mb 可以作为约束,但不能作为目标,因为它不会随 search 候选变化。

两个 typed 调用仅在请求非法或执行失败时返回 tl::unexpected<Error>。如果评测正常完成但 没有候选满足全部约束,则返回 status=TuneStatus::NO_FEASIBLE_CANDIDATE 和结构化 best_effort;该状态下推荐字段无效。

HDF5 数据集格式

VSAG 的评测与基准工具(尤其是 eval_performance)使用与 ann-benchmarks 一致的 HDF5 数据集格式。本页说明 VSAG 期望的具体布局,便于你准备自定义数据集或排查评测 失败的问题。

下文描述的是 dense(稠密) 布局(对应全局属性 type="dense",或省略该属性)。 对于 sparse(稀疏) 数据集(type="sparse"),/train/test 是形状为 (X,) 的扁平 INT8 字节流,由 VSAG 的稀疏向量序列化接口生成(由 tools/eval/eval_dataset.cpp 中的 parse_sparse_vectors 解析);其余数据集与 全局属性的约束仍然适用。

必选数据集

/train(底库向量)

  • 类型INT8FLOAT32
  • 形状(N, D)
    • N —— 底库向量数量(number_of_base
    • D —— 向量维度(dim
  • 说明:元素类型由 HDF5 自动推断:
    • H5T_INTEGER(1 字节)→ INT8
    • H5T_FLOAT(4 字节)→ FLOAT32

/test(查询向量)

  • 类型:必须与 /train 一致
  • 形状(Q, D)
    • Q —— 查询向量数量(number_of_query
    • D —— 必须等于 /trainD

/neighbors(真实近邻索引)

  • 类型INT64
  • 形状(Q, K)
    • K —— 每个查询的真实近邻个数
  • 内容:预先计算好的 Top-K 索引,指向 /train 中的向量。

/distances(真实近邻距离)

  • 类型FLOAT32
  • 形状(Q, K),与 /neighbors 相同
  • 要求:与 /neighbors 中对应位置的近邻一一对齐。

全局属性

type(向量类型)

  • 类型:ASCII 字符串
  • 必填:否(缺失时默认为 "dense"
  • 可选值
    • "dense" —— 稠密向量,按标准矩阵布局存放在 /train/test
    • "sparse" —— 稀疏向量,使用 VSAG 稀疏向量辅助接口的序列化格式

distance(距离度量)

评测工具会将 distance 视为距离(数值越小越好),并与 /distances 中的真值进行 对比。请按下方公式准备真值距离。

  • 类型:ASCII 字符串
  • 必填:是
  • 稠密向量可选值
    • "euclidean" —— L2 距离,以 sqrt(L2Sqr) 计算
    • "ip" —— 内积距离(1 - 内积),自动识别数据类型
    • "angular" —— 余弦距离(1 - 余弦相似度
  • 稀疏向量可选值
    • "ip" —— 稀疏内积距离(1 - 稀疏内积),稀疏向量暂不支持其他度量
  • 多向量可选值
    • 与稠密向量相同("euclidean""ip""angular");多向量使用与稠密 向量相同的逐子向量距离函数

可选数据集

/train_labels/test_labels

  • 类型INT64
  • 形状
    • /train_labels(N,)
    • /test_labels(Q,)
  • 要求:若使用标签,两个数据集必须同时存在。

/valid_ratios

  • 类型FLOAT32
  • 形状(L,)
  • 用途:保存每个类别的验证比例。评测工具会以原始 label 值作为下标 (valid_ratio_[label],见 tools/eval/eval_dataset.h:71),因此 label 必须为 非负整数,且 L 必须严格大于最大 label 值(通常为 L > max(label),下标范围 0..L-1)。数据集作者需自行保证该数组足够大,能覆盖 /train_labels/test_labels 中出现的所有 label。

多向量数据集

type="multi_vector" 时,文件采用平坦展开布局:将每个文档的子向量拼接为一个 二维矩阵,并辅以 vector_counts 数组记录每个文档包含多少子向量。

额外全局属性

属性类型必填说明
multi_vector_dimINT64子向量维度(每个子向量的 float 个数)

额外数据集

数据集形状类型说明
/train_multi_vectors(sum_counts_train, D)FLOAT32所有训练子向量,按行平坦拼接
/test_multi_vectors(sum_counts_test, D)FLOAT32所有查询子向量,按行平坦拼接
/train_vector_counts(N,)UINT32每个训练文档的子向量数
/test_vector_counts(Q,)UINT32每个查询文档的子向量数

D 等于 multi_vector_dimsum_counts_train/train_vector_counts 所有值 之和,sum_counts_test/test_vector_counts 所有值之和。

type="multi_vector" 时,标准的 /train/test 数据集不是必需的, 文档数量(NQ)分别从 /train_vector_counts/test_vector_counts 推导。 其余数据集(/neighbors/distances、可选标签)仍然是必填的。

评测工具会从平坦数组和 counts 重建每个文档的 vsag::MultiVector,然后将完整数组 传递给 vsag::Dataset::MultiVectors()VectorCounts()MultiVectorDim()

结构性要求

  1. 维度一致性

    • train_shape[1] == test_shape[1]D 相同)
    • neighbors.shape == distances.shape
  2. 类型映射

    HDF5 规格内部类型大小出现于
    H5T_INTEGER(size=1)INT81 字节/train/test
    H5T_FLOAT(size=4)FLOAT324 字节/train/test/distances/valid_ratios
    H5T_INTEGER(size=8)INT648 字节/neighbors/train_labels/test_labels
  3. 内存布局

    • 所有矩阵按行优先(row-major)存储。
    • 向量元素连续存放:
      • /train 总大小 = N × D × element_size(每元素 1 或 4 字节)。

Sparse 布局

当全局属性 type 取值为 "sparse" 时,/train/test 不再遵循 (N, D) 的 稠密矩阵布局,而是以扁平的 INT8H5T_INTEGER,size 1)数据集存储原始字节流。 通过 h5py 调用 f["/train"].shape 得到的形状是 (X,),其中 X 为字节流总长度; 此处的 int8 仅是传输形式,字节本身并不是 int8 向量元素

/train/test(稀疏字节流)

  • HDF5 类型H5T_INTEGER,size 1(INT8

  • HDF5 形状(X,),其中 X 为字节流总长度(所有向量记录大小之和)

  • 字节序:小端(little-endian)

  • 内容:按向量顺序首尾相接的记录序列,每条记录包含以下字段,紧密拼接, 无填充、无分隔符:

    字段类型大小说明
    lenuint324 字节该向量的非零项个数
    ids[len]uint32[]4 * len 字节非零项的特征下标(column ids)
    vals[len]float32[]4 * len 字节ids 对应的取值

    允许 len == 0 的记录,此时仅占 4 字节的长度字段。

  • 键的顺序:eval 工具在读取时会对每条向量按 ids 升序排序(vals 同步重排)。 写入侧可以输出无序键,但读取侧不应假设无序。

/train_offsets/test_offsets(随机访问索引,可选)

这两个数据集分别记录 /train/test 字节流中每条记录的起始字节偏移, 使得按下标取第 i 条稀疏向量可以做到 O(1),无需顺序扫描整条字节流。

  • HDF5 类型H5T_INTEGER,size 8(UINT64
  • HDF5 形状/train_offsets(N + 1,)/test_offsets(Q + 1,)
  • 内容offsets[i] 是第 i 条记录在对应字节流中的起始字节偏移, offsets[N] 是哨兵,等于字节流总长度,因此第 i 条记录的长度等于 offsets[i + 1] - offsets[i],整个数组非递减。

两个数据集都是可选的。VSAG 写入端在产出稀疏文件时默认写入它们; 但只包含 /train/test 而不带 offsets 的旧版稀疏 HDF5 文件依然可以 正常加载——读取端会顺序扫描一遍字节流并自动重建索引。文件中如果已有 on-disk offsets,会与重建结果做交叉校验,不一致时直接报错以防止误用 损坏的索引。

/train_token_sequences/test_token_sequences(可选)

这两个数据集保存生成每条稀疏向量的原始分词序列。它们是完全可选的: 不包含这两个数据集的稀疏 HDF5 文件依然可以正常加载。当存在时,它们必须 与 /train/test 一一对应:/train_token_sequences 中的第 i 条记录对应 /train 中的第 i 条稀疏向量(/test 同理)。

  • HDF5 类型H5T_INTEGER,size 1(INT8

  • HDF5 形状(X,),其中 X 为字节流总长度(所有记录大小之和)

  • 字节序:小端(little-endian)

  • 内容:按与 /train / /test 相同的向量顺序依次首尾相接的记录序列, 每条记录字段如下,紧密拼接,无填充、无分隔符:

    字段类型大小说明
    seq_lenuint324 字节原始文档的 token 个数
    term_ids[seq_len]uint32[]4 * seq_len 字节按原始分词顺序的 term id(保留重复与顺序)

    允许 seq_len == 0 的记录,此时仅占 4 字节的长度字段,读取端应当将其 视为“该向量没有原始分词信息“。

  • 记录数量:必须与对应分组(train/test)中稀疏向量的数量一致, 否则读取端会报错。

  • ids 的顺序差异term_ids 按原始分词顺序存储(可能包含重复), 这与 ids(被读取端排序去重)不同

/train_token_sequences_offsets/test_token_sequences_offsets(在分词序列存在时必填)

/train_token_sequences(或 /test_token_sequences)存在时,对应的 UINT64 偏移索引必须也存在。

  • HDF5 类型H5T_INTEGER,size 8(UINT64
  • HDF5 形状(N + 1,)(或 (Q + 1,)
  • 内容:与 /train_offsets 完全相同的契约,使得按下标访问第 i 条分词 记录可以 O(1) 完成。

契约:分词字节流与其偏移数据集必须同时出现,缺一即视为文件损坏—— 即只要 *_token_sequences*_token_sequences_offsets 中只出现了一个, 读取端就会拒绝该文件。两者同时存在时,读取端会将磁盘上的 offsets 与从 字节流重建的 offsets 做交叉校验,不一致即抛异常拒绝加载。

真值与距离

/neighbors/distances 的形状与类型规则与上文稠密布局相同。distance 属性 对稀疏向量仅支持 "ip"(稀疏内积距离,1 - 稀疏内积)。

Python 辅助函数

Python 包 pyvsagpyvsag.sparse 中提供解码工具:

from pyvsag.sparse import load_sparse_hdf5

data = load_sparse_hdf5("sparse.hdf5")
# data["type"]      -> "sparse"
# data["distance"]  -> "ip"
# data["train"]     -> list[dict[int, float]]   每条稀疏向量一个字典,键升序
# data["test"]      -> list[dict[int, float]]
# data["neighbors"] -> numpy.ndarray  shape (Q, K) int64
# data["distances"] -> numpy.ndarray  shape (Q, K) float32

如果已经拿到原始字节流,可以直接调用 pyvsag.sparse.decode_sparse_bytes(buffer)

参考实现

字节流的编码/解码逻辑位于 tools/eval/eval_dataset.cpp (参见 parse_sparse_vectorsserialize_sparse_vectors)。

参考

  • 与该格式兼容的公开基准数据集可在 ann-benchmarks 获取(如 sift-128-euclidean.hdf5gist-960-euclidean.hdf5)。
  • 关于该格式如何被消费,参见 性能评估工具

索引分析(AnalyzeIndexBySearchanalyze_index

VSAG 提供了对已构建或已加载索引进行内省诊断的能力,可以在不重建索引的情况下排查召回率回归、 量化质量、图结构健康度以及查询性能问题。该能力通过两种方式对外暴露:

  • C++ 接口 Index::AnalyzeIndexBySearch(声明在 include/vsag/index.h);
  • 命令行诊断工具 analyze_index,位于 tools/analyze_index/

AnalyzeIndexBySearch 接口

// include/vsag/index.h
virtual std::string
AnalyzeIndexBySearch(const SearchRequest& request);
  • 输入SearchRequest(查询数据集 + topk + 搜索参数 JSON)。
  • 输出:JSON 字符串,包含基于查询的动态指标。
  • 支持的索引类型:当前支持 HGraphIVFPyramidSINDI。未实现该接口的 索引在调用时会抛出异常。

该接口与 Index::GetStats() 互为补充:后者无需查询数据,只输出索引的静态结构指标。 对于基于图的索引,度分布、入口点质量、子索引召回率以及低召回热点节点等图健康度信息, 通过 GetStats() 而非 AnalyzeIndexBySearch 输出。

Pyramid 的查询分析遵循与 KnnSearch 相同的路径限定语义。对于默认 hierarchy, 使用 Dataset::Paths 为每条 query 设置路径;对于命名 hierarchy,使用 Dataset::Paths(hierarchy_name, paths) 设置路径,并在 Pyramid 搜索参数中选择该 hierarchy。当选中 hierarchy 的根节点为 NO_INDEX 时必须提供路径。批量 query 数据集在需要或提供路径时,必须为每条 query 提供对应路径。Pyramid 只在每条 query 的 hierarchy 和路径所允许的向量中计算真值集,并排除已标记删除的向量。Pyramid 查询分析 目前只支持 KNN 请求,并使用 SearchRequest 中的 query_topk_params_str_;不支持 带过滤条件或 iterator 的请求。分析期间应保持索引稳定,不要并发执行 add 或 remove。

GetStats() 输出的静态指标

HGraph 指标

指标含义
total_count索引中向量总数
deleted_count被标记为删除的向量数
connect_components邻近图中的连通分量数
maximal_component_size最大连通分量大小
in_degree_distribution图入度分布直方图
out_degree_distribution图出度分布直方图
average_degree有效节点的平均图度数
duplicate_ratio数据集中重复向量比例
avg_distance_base基础数据集采样向量的平均距离
recall_base基础数据集采样向量的自召回率
time_cost_query使用采样 base 向量作为查询时的平均耗时
proximity_recall_neighbor邻居列表相对真实最近邻的召回率
quantization_bias_ratio量化距离相对精确距离的偏差比率
quantization_inversion_count_rate量化导致的距离顺序倒置比率
build_cache_hit_rate上一次 Build() 中从导入缓存完成 warm-start 的节点占比;当索引并非由 ImportCache() 导入的缓存构建时,输出 skipped_reason
build_cache_hit_nodes / build_cache_missed_nodesbuild_cache_hit_rate 背后的命中 / 未命中节点数(仅在索引由导入缓存构建时存在)

Pyramid 指标

指标含义
total_count索引中的向量总数
index_node_structure路径树节点数、深度、逐层节点数及节点状态分布
leaf_node_size_distribution各叶节点向量数量的分布
subindex_quality各叶子 GRAPH/FLAT 子索引的状态与规模,以及图/暴搜数量和向量覆盖率
recall_base各图节点按规模加权的召回率;仅在采样和分析搜索参数可用时输出
graph_node_count / total_graph_size参与召回分析的图节点数量及总规模
skipped_node_count / low_recall_nodes未参与召回分析的节点数,以及低于召回阈值的节点详情
duplicate_ratio当前 hierarchy 内的重复向量比例
hierarchy_count / hierarchies多 hierarchy 数量及按名称组织的上述指标;单个匿名 hierarchy 直接在顶层输出

SINDI 指标

指标含义
total_count稀疏索引中的向量总数
window_countSINDI window 数量
active_term_count.mean / min / max每个 window 中非空 term 数占 term capacity 的比例统计
active_term_count.avg_count每个 window 的平均非空 term 数
posting_length_distribution.mean / max / p95 / p99非空 posting list 长度分布
posting_length_distribution.long_tail_threshold作为长尾阈值的 P99 posting list 长度
posting_length_distribution.long_tail_mean长度超过 P99 阈值的 posting list 比例
mean_doc_retained.meandoc prune 后每个文档平均保留的 term 比例
recall_base使用采样 base 向量作为 query、基于精确 sparse 真值集计算的自召回
doc_prune_recall禁用 query prune 时,doc-pruned 索引返回候选相对真值 top-k 的召回
doc_prune_bias_meandoc-pruned 距离相对原始精确 sparse 距离的平均相对偏差
doc_prune_inversion_count_ratedoc prune 在候选集合内导致的距离顺序倒置比例
quantization_range.min_val / max_val / diffSQ8 量化范围,仅在开启量化时输出
quantization_recall量化粗筛候选相对真值 top-k 的召回,仅在开启量化时输出
quantization_bias_ratio量化距离相对解码后 doc-pruned 距离的平均相对偏差
quantization_inversion_count_rate量化在候选集合内导致的距离顺序倒置比例

依赖原始 base 向量的 SINDI 指标在数据不可用时会输出 skipped_reason。当 use_reorder=true 时,索引内可读取原始向量;否则需要通过 analyze 参数或下方命令行参数 传入 SINDI base_path

AnalyzeIndexBySearch 输出的动态指标

HGraph 指标

指标含义
recall_query用户查询集相对真实最近邻的召回率
avg_distance_query查询向量与检索结果之间的平均距离
time_cost_query平均单次查询耗时,单位毫秒
quantization_bias_ratio_query查询阶段观察到的量化距离偏差
quantization_inversion_count_rate_query查询阶段量化导致的距离顺序倒置率

Pyramid 指标

指标含义
recall_query搜索结果相对每条 query 所选 hierarchy 和路径内精确真值集的召回率
avg_distance_query路径范围内精确真值邻居的平均距离
time_cost_query平均单次查询耗时,单位毫秒
quantization_error_querybase 量化引入的查询距离误差;开启 reorder 时输出
quantization_inversion_ratio_querybase 量化引入的查询顺序倒置率;开启 reorder 时输出

SINDI 指标

指标含义
recall_query搜索结果相对用户提供或自动生成 sparse 真值集的召回率
mean_latency_ms调用 KnnSearch 时测得的平均单 query 耗时
time_cost_querymean_latency_ms 的别名,用于和其他 analyzer 保持输出习惯一致
postings_scanned.query_term_count_after_prune_meanquery prune 后平均剩余 query term 数
postings_scanned.query_term_with_posting_mean剩余 query term 中平均有多少 term 命中至少一个非空 posting list
postings_scanned.posting_hit_mean剩余 query term 命中非空 posting list 的平均比例
doc_prune_recall禁用 query prune 时,doc-pruned 粗筛候选相对 sparse 真值集的召回
doc_prune_bias_mean抽样 query 上 doc-pruned 距离相对原始精确 sparse 距离的平均相对偏差
doc_prune_inversion_count_rate抽样 query 上 doc prune 在候选集合内导致的顺序倒置比例
quantization_recall量化粗筛候选召回,仅在开启量化时输出
quantization_bias_ratio量化距离相对解码后 doc-pruned 距离的平均相对偏差
quantization_inversion_count_rate量化在候选集合内导致的顺序倒置比例
reorder_recall.before_reorder_recall_k_at_k精排前粗筛 top-k 候选相对真值 top-k 的召回
reorder_recall.after_reorder_recall_k_at_k精排后最终 top-k 相对真值 top-k 的召回
last_topk_rank_in_heap.mean / p95 / p99 / max最终 top-k 结果在精排前候选堆中的最差名次分布

SINDI 动态召回和距离质量指标需要真值集。可通过 groundtruth_path 复用已有 .dev.gt, 也可通过 base_path 让 analyzer 基于原始 sparse base 生成精确真值集;save_groundtruth_path 可保存生成结果便于后续复用。没有可用真值集时,这些字段会输出 skipped_reasonpostings_scanned 只依赖 query 和索引 posting,仍可正常输出。

量化相关字段在不同索引下命名不一致:

索引字段含义
HGraphquantization_bias_ratio_query搜索阶段观察到的量化偏差
HGraphquantization_inversion_count_rate_query搜索阶段量化引起的距离顺序倒置率
IVFquantization_bias_ratio搜索阶段观察到的量化偏差(仅在 use_reorder_ 启用时输出)
IVFquantization_inversion_count_rate搜索阶段量化引起的距离顺序倒置率(仅在 use_reorder_ 启用时输出)
Pyramidquantization_error_query搜索阶段的量化距离误差(仅在开启 reorder 时输出)
Pyramidquantization_inversion_ratio_query搜索阶段量化引起的顺序倒置率(仅在开启 reorder 时输出)

静态结构和健康度信息(包括度分布、入口点分析与 Pyramid 子索引质量)请查看 GetStats() 的 JSON 输出。

analyze_index 工具

analyze_index 是上述分析接口的命令行封装。它从磁盘加载一个已序列化的 VSAG 索引, 打印元数据与 GetStats() 结果,并可选地针对查询文件运行 AnalyzeIndexBySearch

构建

tools/ 默认不会编译,需要显式开启:

# 通过项目 Makefile
VSAG_ENABLE_TOOLS=ON make release

# 也可直接通过 CMake
cmake -S . -B build-release -DCMAKE_BUILD_TYPE=Release -DENABLE_TOOLS=ON
cmake --build build-release -j
# 产物:./build-release/tools/analyze_index/analyze_index

命令行参数

参数缩写是否必需描述
--index_path-i待分析的 VSAG 索引文件路径。
--build_parameter-bp加载索引时使用的构建参数(JSON)。默认使用索引文件内嵌的原始参数。
--query_path-qp查询向量数据集的文件路径,不是 Pyramid hierarchy 路径元数据。如果未提供,则只进行静态分析。
--query_data_type查询数据类型:autodensesparseauto 会对 SINDI 使用 sparse 加载。
--base_pathSINDI 分析可选的 sparse CSR 原始 base 数据集路径。
--groundtruth_pathSINDI 可选的 .dev.gt 真值集路径;提供后直接复用。
--save_groundtruth_pathSINDI 自动生成真值集时的可选保存路径。
--search_parameter-sp动态分析时使用的搜索参数(JSON)。
--topk-k动态分析的 top-K(默认 100)。

查询文件格式为 tools/analyze_index/analyze_index.cppload_query() 所读取的简单二进制 布局:(uint32 rows, uint32 cols, float32 data...)

该 dense query 格式只包含向量。当前工具无法向加载后的 Dataset 附加默认或命名 Pyramid hierarchy 路径。因此,工具无法表达按路径限定的 Pyramid query,也无法在选中 hierarchy 的根节点为 NO_INDEX 时运行动态分析。通过 GetStats() 执行的 Pyramid 静态分析 仍可正常使用。如需按路径执行动态分析,请调用 C++ 接口并为 query 数据集设置路径。

SINDI 的 query/base 数据使用 CSR sparse 二进制布局:int64 nrow, int64 ncol, int64 nnz, 随后是 int64 indptr[nrow + 1]int32 indices[nnz]float32 data[nnz]。SINDI 真值集 使用 .dev.gt 布局:uint32 query_count, uint32 topk,随后是展开后的 int32 idsfloat32 distances。如果没有提供 --groundtruth_path 但提供了 --base_path,SINDI 分析会 基于原始 sparse base 生成真值集,并可通过 --save_groundtruth_path 保存复用。

两种分析模式

1. 仅静态分析(不提供查询文件)

./build-release/tools/analyze_index/analyze_index \
    --index_path /path/to/my_index.hgraph

输出索引名、维度、数据类型、距离度量、构建参数,以及 GetStats() 的 JSON。

2. 静态 + 动态分析

./build-release/tools/analyze_index/analyze_index \
    --index_path /path/to/my_index.ivf \
    --query_path /path/to/queries.bin \
    --search_parameter '{"ivf":{"scan_buckets_count":16}}' \
    --topk 50

除静态信息外,还会额外打印由 AnalyzeIndexBySearch 产出的 Search Analyze: { ... } JSON 块。

当序列化索引只内嵌 index_param 时,analyze_index 也可以在不提供 --build_parameter 的情况下 加载;缺失的 metadata 字段会尽可能使用 analyzer 默认值补齐。

典型使用场景

  • 召回率回归排查:根据指标定位问题来源——是量化质量(quantization_*)、图结构 (connect_componentsproximity_recall_neighbor),还是查询端参数 (对比 recall_queryrecall_base)。
  • 数据健康度体检:发现重复数据(duplicate_ratio)、断连分量或过多删除等情况。
  • 参数调优:使用不同的 search_parameter 反复运行 AnalyzeIndexBySearch, 在 recall_querytime_cost_query 之间选择合适的工作点,无需重建索引。
  • 假设性实验:通过 --build_parameter 在加载时覆盖原始构建参数,对未在文件中嵌入参数的索引 进行不同配置的评估。

参考

  • 接口:include/vsag/index.h 中的 Index::AnalyzeIndexBySearch
  • 实现:src/analyzer/{analyzer,hgraph_analyzer,pyramid_analyzer}.h
  • 工具源码:tools/analyze_index/
  • 本地工具入口:tools/analyze_index/README_zh.md

兼容性检查工具(check_compatibility

check_compatibility 用于验证当前 VSAG 构建是否能够加载并搜索旧版本 VSAG 生成的索引文件。 它主要用于 CI,帮助发现序列化格式和向后兼容性回归。

构建

使用项目 Makefile 时,通过 VSAG_ENABLE_TOOLS=ON 开启工具构建;底层对应的 CMake 选项是 ENABLE_TOOLS=ONENABLE_CXX11_ABI=ON

VSAG_ENABLE_TOOLS=ON make release
# 产物:./build-release/tools/check_compatibility/check_compatibility

如果直接调用 CMake,需要显式传入两个选项:

cmake -S . -B build-release -DCMAKE_BUILD_TYPE=Release \
  -DENABLE_TOOLS=ON -DENABLE_CXX11_ABI=ON
cmake --build build-release -j

输入

命令接收一个形如 <tag>_<algo_name> 的位置参数,例如 v1.1.0_hgraph。对于该标识,工具会在 /tmp/ 下查找以下文件:

文件用途
/tmp/<tag>_<algo_name>.index旧版本 VSAG 生成的序列化索引
/tmp/<tag>_<algo_name>_build.json构建该索引时使用的参数
/tmp/<tag>_<algo_name>_search.json搜索校验使用的参数
/tmp/random_512d_10K.bin搜索校验使用的测试向量

这些文件通常由旧版本兼容性 fixture 生成。

使用

./build-release/tools/check_compatibility/check_compatibility v1.1.0_hgraph

工具会创建当前版本的索引实例,反序列化旧索引文件,然后执行一次小规模 KNN 搜索。加载和搜索都成功 时输出 <identifier> success;否则输出 <identifier> failed 并以非零状态退出。

本地入口

tools/check_compatibility/README_zh.md 保留为浏览工具目录时的简短入口。

FAQ 常见问题

本页整理 VSAG 用户在选型、调参和接入过程中最常遇到的问题。 更多细节可继续阅读各主题页面。

我应该选择哪个索引?

VSAG 中常用索引面向不同场景。 建议按数据类型、规模、召回和延迟目标来选。

hgraph 是默认推荐的稠密向量索引。 它适合文本、图像、多模态 embedding 等在线检索场景, 通常用于高召回、低延迟查询。 它支持多种量化、增量插入、删除、重排和自动调优。

ivf 适合超大规模数据、高吞吐批量查询、内存较紧张的场景。 它通过分桶减少扫描范围,通常比图索引更省内存, 但同等召回下可能需要更多调参。

sindi 用于稀疏向量检索,例如 BM25、SPLADE、BGE-M3 sparse 输出。 它只接受 dtype: "sparse",并且当前主要使用 metric_type: "ip"

pyramid 适合多租户、分区、标签路径类场景。 它在一个索引内部组织多棵子图,便于按 tag 或路径分区检索。

brute_force 是暴力搜索,适合小数据集、功能验证、构造召回率 baseline。 它结果精确,但大规模下延迟和吞吐通常不可接受。

经验建议:

  • 不确定选什么,稠密向量先从 hgraph 开始。
  • 稀疏向量选 sindi
  • 小数据集或验证算法效果选 brute_force
  • 超大规模、吞吐优先、可接受分桶召回折中时,对比 ivf
  • 有明显分区、租户或路径结构时考虑 pyramid

相关页面:索引总览最佳实践

为什么同一套参数在不同数据集上的性能差很多?

这是向量检索里很常见的问题。 即使数据量、维度、索引参数完全相同, 不同数据集的搜索难度也可能差很多。

原因是数据分布不同:

  • 有些数据集近邻结构清晰,查询很容易沿图走到正确区域。
  • 有些数据集近邻边界模糊,需要扩展更多候选才能达到同样召回。
  • embedding 的归一化方式、聚类程度、维度分布和噪声水平, 都会影响搜索难度。

对 HGraph 这类图索引,ef_search 是影响召回和延迟的核心搜索参数。 它控制搜索时保留和扩展的候选规模:

  • ef_search 越大,召回率通常越高。
  • ef_search 越大,单次查询延迟通常也越高。
  • 在其他条件接近时,ef_search 和查询延迟通常近似线性相关。

因此,比较不同数据集性能时, 不建议只看“同一个 ef_search 下的 QPS”。 更合理的方式是:

  1. 先在每个数据集上分别调 ef_search
  2. 让它们达到相同目标召回率,例如 95% recall 或 98% recall。
  3. 再比较 P50 / P95 / P99 延迟和 QPS。

如果数据集 A 达到 95% recall 只需要 ef_search = 80, 而数据集 B 需要 ef_search = 300, 那么 B 的延迟显著高于 A 是正常现象。 这说明 B 的检索难度更高,不一定是索引退化。

建议在性能报告中同时记录:

  • 数据集名称和规模。
  • 维度。
  • 索引参数。
  • 目标 recall 和实际 recall。
  • ef_search
  • QPS。
  • P50 / P95 / P99 latency。

相关页面:HGraph性能评估工具

sq8_uniform 为什么通常比 sq8 更快?什么时候该开 use_reorder

sq8sq8_uniform 都是 8-bit 标量量化, 但它们的缩放方式不同。

sq8 是逐维量化:

  • 每个维度都有自己的 min_i / max_i / scale_i
  • 好处是每个维度都能适应自己的数值范围。
  • 坏处是距离计算时需要处理逐维 scale,热路径更复杂。

sq8_uniform 是全局 uniform 量化:

  • 所有维度共享同一套 min / max / scale
  • query 和 base code 更容易直接在整数域计算。
  • SIMD、AVX-512、AMX、NEON 等向量化路径更友好。
  • 距离计算可以减少逐元素反量化和逐维 scale 操作。

所以在数据分布适合时,sq8_uniform 往往比 sq8 更快。

适合用 sq8_uniform 的场景:

  • 向量已经归一化,尤其是 cosine 场景。
  • 各维度数值范围比较接近。
  • 查询瓶颈主要在距离计算。
  • 吞吐和延迟比极致召回更重要。
  • 可以配合 use_reorder 修正粗排误差。

不太适合的场景:

  • 不同维度数值范围差异很大。
  • 向量由多个异构特征块拼接而成。
  • 存在明显重尾维度或离群值。
  • 不打算开启 reorder,且对召回非常敏感。

use_reorder 的作用是: 先用压缩后的 base quantizer 做粗排, 再用更高精度的 precise quantizer 对候选结果重打分。

常见配置:

{
    "index_param": {
        "base_quantization_type": "sq8_uniform",
        "use_reorder": true,
        "precise_quantization_type": "fp32"
    }
}

建议开启 use_reorder 的情况:

  • 使用 sq4sq4_uniformpqpqfsrabitq 等有损程度较高的量化方式。
  • 使用 sq8sq8_uniform 后召回不够稳定。
  • topk 较小,但最终排序质量要求高。
  • 内存允许多保存一份更高精度表示。
  • 线上更关注召回稳定性,而不是极限内存压缩。

可以不开 use_reorder 的情况:

  • fp32fp16 已经满足召回。
  • sq8_uniform 不开 reorder 时召回已经达标。
  • 内存预算非常紧。
  • 延迟极敏感,不能接受重排开销。

简单建议:

  • 高吞吐优先:先试 sq8_uniform,不开 reorder 测召回。
  • 稳妥配置:sq8_uniform + use_reorder: true, 并设置 precise_quantization_type: "fp32"
  • 强压缩配置:sq4_uniform / pq / rabitq 通常建议配 reorder。

相关页面:Uniform 标量量化标量量化

l2ipcosine 的距离语义是什么?

VSAG 的搜索结果统一按“距离越小越相似”排序。 即使底层是 inner product 或 cosine similarity, 返回值也会转换成距离语义。

具体语义:

  • l2 返回 L2Sqr,也就是平方 L2 距离。
  • ip 返回 1 - inner_product
  • cosine 返回 1 - cosine_similarity

为什么 l2 返回平方距离? 平方 L2 距离和 L2 距离的排序完全一致, 省掉开方可以提升性能。 因此 VSAG 内部和返回值通常使用 L2Sqr

这会影响 RangeSearch 的 radius 设置:

  • 如果你希望 L2 距离小于 2.0,传入的 radius 应该是 4.0
  • 如果使用 ip,半径对应的是 1 - inner_product
  • 如果使用 cosine,半径对应的是 1 - cosine_similarity

例如 cosine 相似度希望大于等于 0.8

distance = 1 - cosine_similarity
radius = 1 - 0.8 = 0.2

注意:

  • 不同系统可能返回 similarity,也可能返回 distance。
  • 和其他库或 ground truth 对比时,要先确认距离语义是否一致。
  • 索引创建后,metric_type 不能在搜索时切换。

相关页面:度量语义范围搜索

base_quantization_typeprecise_quantization_type 有什么区别?应该怎么设置?

这两个参数分别控制粗排存储和重排存储。

base_quantization_type 是主存储量化方式:

  • 用于索引内主要向量存储。
  • 用于图搜索或倒排扫描阶段的粗排距离计算。
  • 直接影响内存占用、搜索速度和粗排召回。
  • 常见值包括 fp32fp16bf16sq8sq8_uniformpq 等。

precise_quantization_type 是重排用的高精度量化方式:

  • 只有在 use_reorder: true 时生效。
  • 用于对粗排候选进行二次精排。
  • 目的是修正有损量化带来的距离误差。
  • 常见值是 fp32,也可以根据内存预算选择 fp16bf16sq8 等。

可以理解为:

base_quantization_type    = 用什么格式快速找候选
precise_quantization_type = 用什么格式重新计算候选距离

高召回基线:

{
    "index_param": {
        "base_quantization_type": "fp32"
    }
}

内存和召回折中:

{
    "index_param": {
        "base_quantization_type": "sq8_uniform",
        "use_reorder": true,
        "precise_quantization_type": "fp32"
    }
}

更省内存:

{
    "index_param": {
        "base_quantization_type": "sq4_uniform",
        "use_reorder": true,
        "precise_quantization_type": "fp32"
    }
}

更激进压缩:

{
    "index_param": {
        "base_quantization_type": "pq",
        "use_reorder": true,
        "precise_quantization_type": "fp32"
    }
}

设置建议:

  • 召回优先、内存充足:base_quantization_type: "fp32"
  • 通用线上推荐:base_quantization_type: "sq8_uniform",必要时开启 reorder。
  • 数据分布不适合 uniform:尝试 sq8
  • 内存紧张:尝试 sq4_uniformpqrabitq,并开启 reorder。
  • 如果开启 use_reorderprecise_quantization_type 默认优先考虑 "fp32"

注意:dtype 是输入数据类型。 base_quantization_type 是索引内部存储和计算方式。 两者不是一回事。 例如输入可以是 dtype: "float32", 但内部用 base_quantization_type: "sq8_uniform" 存储。

相关页面:量化总览HGraphIVF

过滤搜索应该用 Bitset、lambda、Filter、属性过滤还是 extra_info

VSAG 提供多种过滤方式,适合不同使用场景。

Bitset 过滤适合已有一批需要排除的 id, 例如删除集合、黑名单集合、权限不可见集合。 Bitset::Test(id) == true 表示这个 id 被过滤掉。

lambdastd::function<bool(int64_t)> 适合简单过滤逻辑。 回调返回 true 表示该 id 被过滤掉。

Filter 对象适合更复杂的过滤逻辑, 也适合需要向搜索算法提供 ValidRatio() 等提示信息的场景。 Filter::CheckValid(id) == true 表示保留该 id

属性过滤适合结构化字段过滤, 例如 category = "book" AND price <= 100。 它通过 SearchRequest 使用,适合“向量 + 结构化条件”的混合搜索。

extra_info 过滤适合每条向量附带一段固定长度字节数据的场景。 HGraph 可以在图遍历过程中基于这段字节做过滤。 Filter::CheckValid(const char*) == true 表示保留对应向量。

如何选择:

  • 只想排除一批 id:用 Bitset
  • 过滤逻辑简单:用 lambda
  • 过滤逻辑复杂,且能估计通过率:用 Filter 对象。
  • 过滤条件是结构化字段:用属性过滤。
  • 元数据是固定长度字节,并希望和向量一起存储:用 extra_info

最容易混淆的是 true / false 语义:

  • Bitset::Test(id) 返回 true 表示过滤掉该 id
  • lambda 返回 true 表示过滤掉该 id
  • Filter::CheckValid(id) 返回 true 表示保留该 id
  • Filter::CheckValid(const char*) 返回 true 表示保留对应向量。

使用位图过滤时,id 最好控制在 [0, 2^32) 范围内, 避免低 32 位冲突。 如果过滤谓词非常严格,图搜索可能需要扩展更多候选才能凑够结果。 对 HGraph 可以考虑设置 brute_force_threshold, 让高选择性过滤自动走暴搜回退。

相关页面:带过滤的搜索属性过滤Extra Info(附加信息)

版本日志

VSAG 网站的版本日志按 MAJOR.MINOR 系列维护。 每个系列页面覆盖首个发布版本以及该系列后续的全部补丁版本。 GitHub Releases 仍是逐补丁 PR 清单、发布产物和贡献者名单的完整来源。

版本系列

  • VSAG 1.0
    • 首个版本:v1.0.0, 2026 年 7 月 12 日
    • 最新补丁版本:v1.0.0
    • 状态:稳定版本

后续版本沿用相同结构,例如 v1.1v1.2v2.0。 补丁版本直接更新所属系列页面,不再为每个补丁单独创建网站页面。

版本与日志归档方式

Release tag 使用 vMAJOR.MINOR.PATCH 形式。 网站按 MAJOR.MINOR 聚合,便于在一个页面说明整个版本系列; GitHub Releases 则记录每个 tag 的准确内容。

如何获取特定版本

C++ / 源码

git checkout vX.Y.Z
make release

Python

先在 PyPI 查看可用的绑定版本,再安装对应的精确版本:

pip install pyvsag==X.Y.Z

绑定版本不一定与每个 C++ core tag 对应。 仓库还包含 C 和 Node.js/TypeScript 绑定。 各绑定的具体支持范围与打包状态, 请以对应版本系列页面和仓库示例为准。

升级建议

  • 升级前先阅读目标版本系列的兼容性说明;
  • 序列化格式发生变化时,先在测试环境使用 兼容性检查工具验证旧索引产物;
  • 生产环境灰度升级,并使用 性能评估工具对比召回、延迟和资源消耗。

完整的逐补丁发布历史请查看 GitHub 上的全部 VSAG Releases

VSAG 1.0 版本日志

v1.0.0 于项目启动三周年当天(2026 年 7 月 12 日)发布。

版本概览

VSAG 1.0 是项目首个长期支持(LTS)大版本。截至该版本发布时, 公开的 0.x 历史包含从 v0.11v0.18 的 81 个版本标签。 v1.0.0 汇总了这一阶段的主线成果,覆盖稠密向量、稀疏向量、结构化过滤、 层级检索和多向量检索。

v1.0.0 官方 Release 收录了 v0.18.0 以来的 375 项变化: 48 项新增功能、134 项改进、105 项缺陷修复和 88 项其他变化。 v0.11.0...v1.0.0 主线对比包含 1,252 个提交。

本文仅描述 v1.0.0 标签中可用的 API 与功能。

主要能力

索引体系

下图按职责展示 VSAG 1.0 的索引体系:Pyramid 和 LazyHGraph 提供组合与自适应能力, 下层展示 BruteForce、HGraph、IVF、SINDI 和 SIMQ 五个核心索引族。

组合与自适应索引:

  • 分区索引 Pyramid:支持将同一条向量加入多个 Pyramid 路径, 并按指定路径限定检索范围,适合多租户与层级检索 (PR #2226)。
  • 自伸缩图索引 LazyHGraph:在数据量较小时使用精确 BruteForce, 达到可配置阈值后转换为 HGraph,适合持续增长但初始规模较小的集合 (PR #2151)。

核心索引族:

  • 暴力搜索索引 BruteForce:支持单向量和多向量精确检索,是精确检索基线,也是小规模集合的精确检索选项。
  • 图索引 HGraph:面向高召回、低延迟的稠密向量检索。从最初的 HGraph 实现发展至今, 已支持量化、过滤、范围与迭代检索、更新、标记/强制删除、缓存导入导出、 诊断和内存+磁盘配置。
  • 空间划分索引 IVF:面向大规模数据、批量查询和大 top-k 场景,支持量化、 重排、属性过滤、并行构建/检索和磁盘分桶存储。参见最初的 IVF PR
  • 稀疏向量索引 SINDI:面向 BM25 风格和学习型稀疏表示, 支持词项 ID 重映射、索引分析、不可变读取、FP16 稀疏值、词项列表压缩和 低内存不可变构建
  • 多向量索引 SIMQ:面向 ColBERT 等 late-interaction 多向量检索, 先在聚类级别生成候选,再用精确 MaxSim 重排,以平衡召回率和延迟 (PR #2357)。

各索引的参数与使用说明参见索引

量化、数据类型与硬件加速

VSAG 1.0 提供多种输入格式、量化方法、向量变换与硬件加速路径:

  • FP32、INT8、FP16、BF16 稠密输入,以及稀疏与多向量数据集; FP16/BF16 可直接输入(PR #1731);
  • SQ4/SQ8 及对应的均匀量化变体;
  • Product Quantization 和 PQ FastScan (PR #626PR #691);
  • RaBitQ、扩展位宽与 x+y split 的基础/重排布局、FHT/PCA 变换和专用 SIMD 内核;
  • Transform Quantizer 链与 MRL-E 降维;
  • x86_64 上的 SSE、AVX、AVX2、AVX-512 和部分 AMX 内核,以及 ARM NEON、SVE;
  • SQ8U 内积和 KMeans BF16 GEMM 的 AMX 加速 (PR #2032)。

支持组合与调参建议参见量化

检索、过滤与索引管理

  • 基础检索: KnnSearch 提供 KNN 查询,RangeSearch 提供可限制结果数量的范围查询。
  • 统一请求接口: SearchRequestIndex::SearchWithRequest 使用一个请求对象选择 KNN 或范围检索,并传递索引专用 JSON 参数、受支持的过滤条件和诊断输入。v1.0 中 HGraph、IVF 和 BruteForce 实现了该接口,可用字段因索引而异。
  • 过滤: 支持 ID 回调/FilterPtr、bitset 和 SQL 风格属性表达式。HGraph、IVF 和 BruteForce 支持基于属性倒排索引的结构化过滤;HGraph 还支持迭代过滤,并可在有效数据比例不高于 hgraph.brute_force_threshold 时切换到暴力检索。
  • 训练与模型复用: TrainCloneExportModelTune 分别用于独立训练、索引深拷贝、训练模型导出和索引调优。
  • 数据维护与读取: 支持批量删除、标记/强制删除、ID/向量/属性更新、Source ID、extra_info、索引详情读取和 CalcDistanceById
  • 漏召回诊断: SearchRequest::expected_labels_ 可在 HGraph、IVF 和 BruteForce 中分析目标向量未被召回的原因,推理报告随结果 Dataset 返回(PR #1838)。
  • 统计与容量规划: 检索、I/O、内存和索引专用统计用于观测运行状态;内存估算、索引内省和分析工具用于容量评估与问题定位。

具体支持范围因索引而异;可通过 Index::CheckFeature(IndexFeature) 查询其中已声明的能力。

序列化与兼容性

VSAG 1.0 并行维护两套序列化接口:既有 Serialize/Deserialize 接口继续维护, 用于保持现有接入的兼容性;新的流式序列化接口 采用头部先行(header-first)、仅需顺序读取的格式,并扩展以下能力:

  • SerializeStreaming 先写元数据,再写带类型的 TLV 数据块;
  • DeserializeStreaming 将数据恢复到已创建、为空且兼容的索引对象;
  • Index::Load 读取元数据、创建对应索引,并应用受支持的存储放置策略;
  • BruteForce、HGraph、IVF、可变 SINDI 和 Pyramid 已支持 v1.0 流式序列化; 不可变 SINDI 运行态在 v1.0 中暂不支持。

两套接口会并行保留,但两种格式不兼容,文件必须使用对应的接口读取。 既有用户可继续使用原接口;新接入建议优先使用流式序列化接口。 格式和数据块版本细节参见 新序列化格式

跨版本索引测试样本和兼容性检查工具 可用于重复验证旧索引的升级过程。

平台、绑定与工具

  • VSAG 核心 C++ 库支持 Linux 和 macOS,主要研发和完整验证链路以 Linux 为主。 Linux x86_64 与 AArch64 均通过 CI,macOS 当前验证 arm64 构建 (源码构建PR CI)。 预编译 C++ 发布包目前仅面向 Linux x86_64。
  • Python 绑定的包名为 pyvsag;v1.0.0 声明支持 CPython 3.6-3.14,并为这一版本范围配置了 wheel 构建。 Python 构建已迁移到原生 CMake 集成 (PR #1599); 绑定还支持更多索引操作、FP16/BF16 输入、稀疏向量和稀疏 HDF5 辅助工具。
  • VSAG 新增 C API 与 Node.js/TypeScript 绑定,并提供 快速入门示例。各语言绑定独立发布,使用时请确认对应包版本。
  • 构建支持系统级 OpenBLAS/fmt、自定义依赖下载镜像和可安装的 CMake 包配置。
  • eval_performance 支持稠密、稀疏和多向量数据集;analyze_indexcheck_compatibilityvisualize_index 和 HTTP 监控服务补齐了分析、兼容性验证、 序列化检查与监控工具。

稳定性与验证

功能与回归测试覆盖内存分配、泄漏与内存不足路径,以及多线程下的构建、写入、 检索、更新、删除和析构。CI 通过 ASan 持续检查内存安全,并通过 TSan 检查数据竞争; 兼容性测试样本用于验证历史索引的升级路径。

从 v0.18 升级的兼容性说明

VSAG 1.0 是大版本升级,包含源码级 API 变化。升级前请重点检查:

  1. Remove 返回删除数量并支持批量操作。 v0.18 的 tl::expected<bool, Error> Remove(int64_t) 调整为 tl::expected<uint32_t, Error>,并增加批量重载和显式删除模式 (PR #1551)。 v1.0 最终提供 RemoveMode::MARK_REMOVERemoveMode::FORCE_REMOVE; HGraph 强制删除由 PR #1810实现。
  2. 不支持的操作通常改为返回错误。 许多返回 tl::expected 的默认方法不再抛出 std::runtime_error,而是返回带 ErrorType::UNSUPPORTED_INDEX_OPERATIONtl::unexpectedPR #2141)。调用 .value() 前 应先检查 tl::expected 返回值。
  3. 内存统计接口签名发生变化。 GetMemoryUsage 使用 uint64_tGetMemoryUsageDetail 返回 std::unordered_map<std::string, uint64_t>GetEstimateBuildMemory 更名为 EstimateBuildMemoryPR #2388)。
  4. 检索接口可以渐进迁移。 新接入优先使用 SearchRequestSearchWithRequest,既有检索重载在 v1.0 中仍然保留。
  5. 不要混用两套序列化格式。 旧格式输出必须使用旧反序列化接口; 流式序列化输出必须使用 DeserializeStreamingIndex::Load
  6. SINDI 自动选择堆插入策略。 旧的 use_term_lists_heap_insert 检索参数 会被忽略。SINDI 根据 doc_prune_ratioquery_prune_ratio 推导策略;依赖 强制指定旧路径的配置需要调整。
  7. Intel MKL 改为显式开启。 默认值为 OFF。通过 Makefile 构建时设置 VSAG_ENABLE_INTEL_MKL=ON,直接使用 CMake 时设置 -DENABLE_INTEL_MKL=ON

对于持久化索引,建议在测试环境验证明确的源版本与目标版本组合。 序列化兼容性可能因索引类型、功能开关和格式家族而异。

从 v0.x 走向 1.0

v0.11.0 是 VSAG 开源后的首个正式发布版本。此前的版本号仅用于内部迭代,未作为 GitHub Release 对外发布,因此本节从 v0.11 开始回顾。

基础建设:v0.11-v0.14

  • v0.11,2024 年 9 月: 建立 HNSW/DiskANN、C++/Python、预过滤、余弦距离、锁和序列化的初始基线。
  • v0.12,2024 年 12 月: 引入 DataCell、I/O 与图抽象、HGraph、SQ4/SQ8/INT8、Engine/Factory 和 pyvsag 打包。
  • v0.13,2025 年 2 月: 新增 BruteForce,并扩展 Pyramid、内存估算、IndexFeature、过滤提示和 eval_performance
  • v0.14,2025 年 4 月: 引入 IVF、FP16/BF16、RaBitQ、异步/缓冲 I/O、稀疏数据、HGraph extra_info、迭代过滤与系统化兼容性检查。

生产能力扩展:v0.15-v0.18

  • v0.15,2025 年 6 月: 新增 Train/Clone/ExportModel、PQ/PQ FastScan、属性表达式、压缩图、HGraph 合并/标记删除,以及带兼容性 CI 的既有格式自描述序列化。
  • v0.16,2025 年 8 月: 新增 mmap HGraph、SINDI、并行 IVF、属性更新、原始向量读取和参数兼容性检查,并 通过长期补丁线持续解决 ABI、并发和旧索引兼容问题。
  • v0.17,2025 年 10 月: 扩展 SearchRequest 以覆盖主要检索场景,并增加检索超时、 更广泛的 extra_info、Transform Quantizer、HGraph 单查询并行、 数据导出与更完整的 SINDI 生命周期和统计能力。
  • v0.18,2026 年 1 月: 新增 C API、自动构建 Python wheel、稀疏 Python 绑定、磁盘 IVF、索引详情/检索/I/O 统计、MRL-E 与 HGraph 调优、扩展 RaBitQ,并继续完善 SINDI 和 Pyramid。

完整提交历史参见 v0.11.0…v1.0.0 对比

v1.0 补丁版本

  • v1.0.0 (2026 年 7 月 12 日):首个长期支持大版本。

后续 v1.0.x 补丁版本会追加到本节。 完整 PR 清单和各版本贡献者名单继续维护在 GitHub Releases。

致谢

VSAG 1.0 是蚂蚁集团 VSAG 团队与开源社区共同贡献的成果。 感谢所有参与算法设计、功能实现、问题反馈、代码评审、 测试改进和文档建设的贡献者。

完整名单参见贡献者页面官方 Release

2025 路线图

本页记录了通往 VSAG 1.0 的历史路线图。v1.0.0 已于 2026 年 7 月 12 日发布。 实际交付内容参见 VSAG 1.0 版本日志

当下,随着 AI 能力的持续增强和优秀开源大模型的普及, 非结构化数据检索需求激增。 向量算法作为非结构化数据检索技术的关键,其重要性不言而喻。 VSAG 社区将会持续投入算法研发,帮助社区的合作伙伴, 提升数据检索性能,提高数据检索实时性,持续降低检索服务成本。

当时的路线图将首个大版本定义为:

  • VSAG 1.0 将提供全面的图索引和倒排索引支持, 同时覆盖纯内存和内存+磁盘混合检索模式, 并提供较低的内存成本和卓越的检索性能。

以下是一些算法或功能的规划:

  • 支持常见的数据类型,满足不同场景的非结构化数据检索需求
    • FP32 向量:满足主流向量检索场景使用
    • INT8、BF16、FP16 向量:适配量化的 embedding 模型,避免额外的存储开销
    • 稀疏向量:扩展文本检索方式
  • 提供全面优化的核心索引类型,覆盖绝大部分检索场景
    • 图索引 HGraph:满足对高精度和低延迟的要求
    • 倒排索引 IVF:满足大 K 和批量查询的需求
  • 提供丰富的量化方式,满足内存/召回率的平衡
    • RabitQ(BQ):超高倍率的压缩,极少的内存使用
    • PQ:灵活的压缩倍率,适合低精度要求的场景
    • SQ4、SQ8:常规压缩方式,少量牺牲召回率获得内存和性能收益
  • 多平台指令集适配,减少系统集成分发工作量
    • x86_64 平台:SSE,AVX,AVX2,AVX512
    • ARM 平台:Neon,SVE
    • 可选的矩阵乘法加速库:intel-mkl,openblas
  • 支持资源隔离,提供细粒度的运行资源可配置
    • 内存资源:支持以索引为单位设置内存分配器, 以实现类似租户级内存管理
    • CPU 资源:支持注入线程池,从而提升写入吞吐和搜索吞吐

除此之外,我们还有很多功能特性会在开源社区讨论、开发和实现。 如果你对此感兴趣,请关注 VSAG 项目!

开源社区

VSAG 是一个由蚂蚁集团开源并持续在 GitHub 上维护的项目,欢迎开发者、研究者和使用者加入社区。

交流渠道

  • GitHub Issues:报告缺陷、提交特性请求、讨论设计方案。 https://github.com/antgroup/vsag/issues
  • GitHub Discussions(若开启):长期话题、用法问答、最佳实践。
  • Pull Request:任何代码、文档、示例改动都通过 PR 提交,详见 贡献到 VSAG
  • 钉钉 / 微信群:如社区公告给出入口,可在 README 顶部找到最新链接。

项目治理

  • 维护者团队负责代码评审、发版与路线图;
  • 所有 PR 需经过至少一次代码评审 + 必需的 CI 检查;
  • 每个 PR 必须附带 kind/*version/* 两类标签,由 Mergify 强制检查(见 贡献者指南)。

贡献方式

不止于写代码,以下都是欢迎的贡献形式:

  • 文档:修正错别字、补充示例、翻译页面;
  • 示例:在 examples/cpp/examples/python/ 新增端到端 demo;
  • Benchmark:提交新的测试机型 / 数据集结果,丰富 标准环境性能参考
  • 生态集成:为 VSAG 编写其他语言 / 数据库的绑定或适配层;
  • 科普文章:欢迎投稿到 docs/blog/(详见仓库 README)。

行为准则

社区遵循 Contributor Covenant Code of Conduct。 请以建设性、尊重的方式参与讨论。

相关项目

参见 关联项目

使用 AI Agent 创建 Issue

你可以借助 AI 编码 Agent(Claude Code、OpenCode 或 Codex)与 VSAG 仓库内置的 /create-issue 斜杠命令一起,为 VSAG 起草并提交一份高质量的 GitHub Issue。 Agent 会把你的需求映射到项目的 Issue 模板,自动填好必填字段,并通过 GitHub CLI 提交。

本页面介绍端到端的使用步骤。Agent 内部遵循的规范工作流位于 .github/agent-prompts/create-issue.md, 本页只关注用户侧的操作。

前置条件

  • 一个 GitHub 账号。
  • 本地已安装并配置好以下任意一个受支持的 AI 编码 Agent: Claude CodeOpenCode 或 Codex。
  • 本机可用的 git

1. 安装并登录 GitHub CLI(gh

先按官方快速入门在你的平台上安装 gh

https://docs.github.com/en/github-cli/github-cli/quickstart

然后在终端登录:

gh auth login

选择 GitHub.com,挑选认证协议(HTTPS 即可),并按浏览器提示完成登录。

2. 验证 gh 登录状态

gh auth status

确认 GitHub.com 已成功认证后再继续。

3. 克隆 VSAG 仓库

git clone https://github.com/antgroup/vsag.git
cd vsag

/create-issue 命令及其 Prompt 文件都在仓库内,因此 Agent 必须在 vsag/ 目录下启动,才能识别该命令。

4. 在仓库目录中启动 Agent

vsag/ 目录下启动其中一个受支持的 Agent:

# Claude Code
claude

# OpenCode
opencode

# Codex CLI
codex

5. 运行 /create-issue

在 Agent 对话中调用斜杠命令,并用自然语言描述你的需求。例如:

/create-issue HGraph 在 dim=0 时构建会崩溃;希望返回一个明确的错误。

Agent 将会:

  1. .github/ISSUE_TEMPLATE/ 中选择最合适的模板;
  2. 在必填字段缺失时主动追问;
  3. path:line 形式引用代码或文档,撰写 Issue 正文;
  4. 把最终草稿展示给你确认;
  5. 你确认后,通过 gh issue create 提交 Issue。

整个过程中你可以反复与 Agent 沟通——让它调整措辞、补充复现步骤、切换模板、 附加日志,再决定是否提交。

小贴士

  • 描述要具体:报 Bug 时附上索引类型、参数、数据集形状、报错信息以及运行平台。
  • 提需求时,描述使用场景以及期望的 API 或行为,Agent 会据此填好模板字段。
  • Issue 不需要 Signed-off-by:——DCO 仅适用于 commit。
  • 如果不想通过交互式 Agent 驱动整个流程,可参考仓库提供的 Shell 包装脚本 tools/issue-helper/new-issue.sh

参见

关联项目

本页收录与 VSAG 相关或集成了 VSAG 的上下游项目,便于用户快速搭建完整方案。

使用 VSAG 的项目

VSAG 的依赖与灵感来源

  • Faiss:Meta 的向量检索库;VSAG 在 IVF / 量化思路上有所借鉴。
  • SPANN / SPTAG:微软的大规模向量检索工程,为 VSAG 的大规模检索设计提供了灵感。

生态工具

绑定与语言支持

  • C++(原生)
  • Pythonpyvsag,源码位于 python_bindings/python/
  • Node.js / TypeScript:源码位于 typescript/,npm 包名 vsag

欢迎提交 PR 完善本列表。

科研论文

1. Effective and General Distance Computation for Approximate Nearest Neighbor Search [ICDE’25]

摘要(翻译):高维空间中的近似K最近邻(AKNN)搜索是一个关键且富有挑战性的问题。在AKNN搜索中,距离计算是主导运行时间的核心任务。现有方法通常使用近似距离来提升计算效率,但这往往以牺牲搜索精度为代价。为解决此问题,当前最先进的方法ADSampling采用随机投影来估计近似距离,并引入一个额外的距离校正过程以减轻精度损失。然而,ADSampling在有效性和通用性上均存在局限,这主要源于其距离近似和校正过程对随机投影的依赖。为了解决ADSampling在有效性上的局限,我们利用数据分布,通过正交投影来改进距离计算。此外,为了克服其在通用性上的局限,我们采用一种数据驱动的方法进行距离校正,将校正过程与距离近似过程解耦。大量的实验证明了我们所提方法的优越性和有效性。具体而言,与ADSampling相比,我们的方法在真实数据集上实现了1.6至2.1倍的加速,同时达到了更高的精度。

该功能已集成于 VSAG 库中,功能名为 BSA,帮助磁盘索引减少高精度重排数据量。

2. VSAG: An Optimized Search Framework for Graph-based Approximate Nearest Neighbor Search [VLDB’25]

摘要(翻译):近似最近邻搜索(ANNS)是向量数据库和人工智能基础设施中的一个基础问题。近期的基于图的ANNS算法在实现高搜索精度的同时,也达到了实用的效率。尽管取得了这些进展,但由于基于图的搜索所带来的随机内存访问模式以及向量距离计算的高昂开销,这些算法在生产环境中仍然面临性能瓶颈。此外,基于图的ANNS算法的性能对参数高度敏感,而选择最优参数的成本又极其高昂——例如,手动调参需要反复重建索引。本文介绍了VSAG,一个旨在提升基于图的ANNS算法在生产环境中性能的开源框架。VSAG已在蚂蚁集团的各项服务中大规模部署,并集成了三项关键优化:(i) 高效的内存访问:通过预取技术和缓存友好的向量组织方式,减少L3缓存未命中;(ii) 自动化参数调优:无需重建索引即可自动选择性能最优的参数;(iii) 高效的距离计算:利用现代硬件、标量量化技术,并智能地切换至低精度表示,从而显著降低距离计算的成本。我们在真实数据集上对VSAG进行了评估。实验结果表明,在保证同等精度的前提下,VSAG达到了业界顶尖的性能水平,且相比业界标准库HNSWlib,其加速比可高达4倍。

该功能已集成于 VSAG 库中,通过统一的 Tune 接口启用(历史上称为 “ELP Optimizer”,底层实现键为 use_elp_optimizer)。

3. EnhanceGraph: A Continuously Enhanced Graph-based Index for High-dimensional Approximate Nearest Neighbor Search [arxiv]

摘要(翻译):随着深度学习技术的飞速发展,高维向量空间中的近似最近邻搜索近年来受到了广泛关注。我们观察到,在基于图的索引的整个生命周期中,会产生大量的搜索日志与构建日志。然而,由于现有索引具有静态特性,这两类有价值的日志并未得到充分利用。本文提出了EnhanceGraph框架,该框架将这两类日志整合到一种名为“共轭图”(conjugate graph)的新型结构中,并利用该共轭图来提升搜索质量。通过对基于图的索引的局限性进行理论分析与观察,我们提出了若干优化方法。针对搜索日志,共轭图存储从局部最优解到全局最优解的边,以增强路由至最近邻的能力;针对构建日志,共轭图存储从邻近图(proximity graph)中被剪除的边,以增强对k最近邻的检索能力。我们在多个公开及真实的工业数据集上的实验结果表明,EnhanceGraph在不牺牲搜索效率的前提下,显著提升了搜索精度,其中召回率(recall)的最大提升幅度达到了从41.74%至93.42%。此外,我们的EnhanceGraph算法已被集成到蚂蚁集团的开源向量库VSAG中。

该功能已集成到 VSAG 库中,可通过 use_conjugate_graph 参数启用。

4. SINDI: an Efficient Index for Approximate Maximum Inner Product Search on Sparse Vectors [arxiv]

摘要(翻译): 稀疏向量最大内积搜索(MIPS)在面向检索增强生成(RAG)的多路检索中至关重要。近期的基于倒排索引和基于图的算法在实现高搜索精度的同时,也达到了实用的效率。然而,它们在生产环境中的性能常常受限于冗余的距离计算和频繁的随机内存访问。此外,稀疏向量的压缩存储格式也阻碍了SIMD加速技术的应用。本文提出了一种稀疏倒排非冗余距离索引(SINDI),该索引集成了三项关键优化:(i) 高效的内积计算:SINDI利用SIMD加速技术并消除了冗余的标识符查找,从而实现了批量的内积计算;(ii) 内存友好设计:SINDI将对原始向量的随机内存访问替换为对倒排列表的顺序访问,从而显著降低了内存访问延迟;(iii) 向量剪枝:SINDI仅保留向量中绝对值较大的非零项,从而在保持精度的同时提升了查询吞吐量。我们在多个真实数据集上对SINDI进行了评估。实验结果表明,SINDI在不同规模、语言和模型的数据集上均达到了业界顶尖的性能水平。在MsMarco数据集上,当Recall@50超过99%时,与SEISMIC和PyANNs相比,SINDI带来的单线程每秒查询率(QPS)提升了4.2至26.4倍。值得注意的是,SINDI已被集成到蚂蚁集团的开源向量检索引擎库VSAG中。

SINDI 是 VSAG 库中的一个索引类型。

贡献者列表

以下是 VSAG 项目的贡献者(更新于 2026-08-21),按照第一次贡献的时间排序: