迭代式搜索
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.cpp、examples/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 的调用。
SearchRequestAPI。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不能在多线程间并发使用;不同查询应各自持有独立上下文。
原生 HGraph SearchSession(实验性)
OpenSearchSession(query, allocator = nullptr) 仅固定查询、索引和资源,返回单消费者、有所有权的会话,
通过 SUPPORT_CONTINUE_SEARCH_SESSION 检查支持情况。此 API 独立于 IteratorContext,旧迭代器实现保持不变。
Open 深拷贝查询,不创建计算器或执行路由/距离计算。首次 Next(SearchSessionNextOptions)
创建可复用计算器并执行上层路由,后续调用从保存的底层图 frontier 继续扩展,复用距离缓存、访问/扩展状态和计算器。
每次至少扩展 max(ef_search, max_candidates) 个剩余顶点,或耗尽 frontier;
过滤条件严格时可继续扩展以填满当前页。这不是预先计算大 KNN 结果后分页。
页内按距离排序,不保证跨页全局顺序;耗尽指可达图耗尽,不保证枚举不连通顶点。
过滤前 HasMore() 可保守返回 true。
SearchSessionNextOptions 默认使用 HGraph JSON,因为目前仅 HGraph 支持会话;未来后端必须说明其接受的参数。
Next 的 max_candidates、filter(nullptr 表示全部接受)和 search_parameters 仅对本次调用生效。
可在调用之间更换过滤器或修改同一过滤器对象,但调用期间条件必须稳定。每次重新检查所有已发现、未交付的 ID,
包括此前被过滤器或 threshold 拒绝的候选及普通重复向量别名。threshold 可收紧或放宽;精排可开关,
每个 ID 的精确距离延迟计算一次并单独缓存。改变 ef、需求量或条件不会重新路由或重复计算已有距离,已交付 ID 不再返回。
HasMore() 独立于当前过滤条件:尚未初始化、frontier 非空或存在已发现未交付 ID 时为 true。
空批次不关闭会话;即使遍历已耗尽,之后放宽过滤器仍可得到结果。当前请求必须在空批次时停止,
不能在无法满足的过滤条件下仅依赖 while (HasMore()),否则可能无限循环。
统计 session_traversal_exhausted、session_undelivered_nodes 区分遍历耗尽与保留的拒绝候选。
永久无法返回的 NaN ID 在不存在可挽救它的其他输出 cell 时不再计入 HasMore:没有独立精排 cell,或两份已缓存距离均为 NaN;仍保留为遍历桥梁。尚未计算的其他精排 cell 会保守保留该 ID。
上层非有限距离路由恢复每层最多访问 65536 个顶点;超过安全上限会返回执行错误并关闭会话,而不是静默截断路由。
参数是完整替换,不是 JSON 增量补丁。此 POC 每次扫描 O(N) 稠密 ID 域并对合格候选排序,持有 O(N) 距离和状态数组。
非终态调用中,非法参数或超出 [1, INT64_MAX] 的需求量可重试,不破坏会话。
需求量是返回数量上限,不是分配大小;仅为实际结果分配缓冲区。实际结果大小溢出、分配失败或其他遍历异常会关闭会话。
Open 前已标记删除的 ID 永久不参与交付,但仍作为遍历桥梁;用户过滤器拒绝的候选仍可在后续调用重新参与。
auto opened = index->OpenSearchSession(query);
vsag::SearchSessionNextOptions options{10, R"({"hgraph":{"ef_search":64}})", nullptr};
if (opened.has_value()) {
auto session = std::move(opened.value());
while (session->HasMore()) {
auto page = session->Next(options);
if (!page.has_value() || page.value()->GetDim() == 0) break;
// 消费本批次;下次调用前可修改 options。
}
}
旧的 OpenSearchSession(query, k_per_call, parameters, filter, allocator) 和 Next(max) 仍作为便捷接口,
使用 Open 指定的默认条件,最小扩展量为 max(k_per_call, ef_search)。
显式 Next(options) 仅覆盖本次条件,不修改后续旧接口调用的默认值。
会话打开期间不能修改索引、查询或距离度量,也不能并发调用会话方法。Open 后调用方可以销毁查询和 公开索引对象,会话会保留后端资源。自定义 allocator 以及索引 Resource 借用的外部 allocator/thread pool 必须活得比会话久。返回结果独立拥有缓冲区, 在 Close、会话析构和索引析构后仍有效。Close 可重复调用;耗尽或关闭后的 Next 返回空结果,保留累计工作统计,本轮工作量为零,且不校验请求数量。 Close 释放遍历状态存储。
支持普通标签/extra-info 过滤、返回 extra info、输出距离 threshold 和普通精排。被过滤顶点仍作为
图遍历桥梁。不支持并明确拒绝:共享重复向量存储、强制删除、启用的 MCI/共轭图搜索、特殊 RaBitQ 搜索、
并行/超时/factor/暴力回退/跳数限制及显式跳过策略。frontier 和分数缓存使用标准库分配,
可选 allocator 用于查询拷贝和邻居临时存储,并不覆盖所有会话分配。
累计统计 session_routing_runs、session_computers、session_scored、session_reordered、
session_expanded 可用于检查真实遍历工作量。