Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 36 additions & 4 deletions docs/docs/en/src/resources/autotune.md
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,34 @@ truth and defines per-query `recall_at_k` as the intersection of the returned an
in their first `top_k` entries, divided by `top_k`; the reported value is the average over all
queries. Ground truth is optional when recall is neither a constraint nor the objective.

Typed HGraph requests may reproduce a filtered workload by providing either one `FilterPtr` or one
exclusion `BitsetPtr` per query:

```cpp
request.workload = {queries, filtered_ground_truth, 10, 48};
request.workload.query_filters = query_filters;
// Or: request.workload.query_invalid_bitsets = query_invalid_bitsets;
```

The two vectors are mutually exclusive. An empty vector means that filter form is absent; otherwise
its size must equal the query count, and a null entry leaves that query unfiltered. A set bit in
`query_invalid_bitsets` excludes the corresponding external index ID. Bitsets must not be mutated
during tuning and should contain only IDs from the evaluated index.

Every non-null `FilterPtr` is reused across candidates and may be called concurrently, so it must be
deterministic, reusable, and thread-safe. Its `ValidRatio()` must represent that query's actual
selectivity because HGraph uses it to choose search paths. This version uses
`CheckValid(int64_t)` and rejects `use_extra_info_filter=true`; the ID passed to `CheckValid` is the
index's external label.

Each query must admit at least `top_k` IDs, and ground truth must be computed using its corresponding
filter or bitset. The returned parameters must be used with a representative filtered workload of
the same kind. `TuneIndex` supports the same HGraph workload; when either filter form is present and
`index_spaces` is omitted, only HGraph candidates are generated. Filters and bitsets are not stored
in the index, so the caller must still pass the applicable value to each final `KnnSearch`.
Caller-provided filters are available only through the typed API and are not serialized into CLI
requests or reports. IVF and Pyramid filtered tuning are not supported in this version.

The CLI's `index_path` field remains an offline adapter: it uses the concrete create parameters to
create and deserialize an index, then enters the same search-only flow.

Expand All @@ -203,14 +231,17 @@ unnamed hierarchy. To select different `ef_search` values for different paths, r
AutoTune request per representative path workload, with the matching ground truth. V1 does not
aggregate path-specific recommendations into one result.

A complete example is available at
[`examples/cpp/327_feature_autotune_existing_index.cpp`][existing-index-example].
The `FilterPtr` example is available at
[`examples/cpp/327_feature_autotune_existing_index.cpp`][existing-index-example]. For direct
per-query exclusion bitsets, see
[`examples/cpp/330_feature_autotune_existing_index_bitset.cpp`][bitset-example].
For Pyramid path tuning, see
[`examples/cpp/328_feature_autotune_existing_pyramid.cpp`][pyramid-example].
It tunes the same Pyramid index separately for 512-vector and 4096-vector leaf subgraphs under the
same recall target, illustrating why different paths may need different `ef_search` values.

[existing-index-example]: https://github.com/antgroup/vsag/blob/main/examples/cpp/327_feature_autotune_existing_index.cpp
[bitset-example]: https://github.com/antgroup/vsag/blob/main/examples/cpp/330_feature_autotune_existing_index_bitset.cpp
[pyramid-example]: https://github.com/antgroup/vsag/blob/main/examples/cpp/328_feature_autotune_existing_pyramid.cpp

## Metrics
Expand Down Expand Up @@ -260,5 +291,6 @@ result.
## V1 Boundaries

V1 evaluates one KNN workload and performs a full sweep except for the supported HGraph
`ef_search` adaptive search. It does not provide filtered or range-search workloads, adaptive query
sampling, cross-request build cache, or model-based candidate generation.
`ef_search` adaptive search. The typed API supports per-query ID filters for HGraph; the CLI and
other index types do not yet support filtered workloads. V1 does not provide range-search
workloads, adaptive query sampling, cross-request build cache, or model-based candidate generation.
36 changes: 32 additions & 4 deletions docs/docs/zh/src/resources/autotune.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,31 @@ if (result.has_value() && result->status == vsag::autotune::TuneStatus::SUCCESS)
返回结果和 ground truth 前 `top_k` 个 ID 的交集大小除以 `top_k`,最终指标是所有 query
的平均值。recall 既不是约束也不是目标时,ground truth 可省略。

typed HGraph 请求可以为每条 query 提供一个 `FilterPtr`,或者一个表示排除集合的
`BitsetPtr`,以复现带过滤的 workload:

```cpp
request.workload = {queries, filtered_ground_truth, 10, 48};
request.workload.query_filters = query_filters;
// 或:request.workload.query_invalid_bitsets = query_invalid_bitsets;
```

两种 vector 互斥。vector 为空表示未使用该种过滤方式;否则数量必须与 query 数量相同,
其中空指针表示该 query 不做过滤。`query_invalid_bitsets` 中置位的 bit 表示排除对应的索引
外部 ID。调优期间不能修改 bitset,并且其中应只包含待评测索引中的 ID。

每个非空 `FilterPtr` 会跨候选重复使用,并且可能被并发调用,因此必须确定、可重复使用且
线程安全。`ValidRatio()` 必须能代表该 query 的实际选择率,因为 HGraph 会用它选择搜索路径。
本版本使用 `CheckValid(int64_t)` 并拒绝 `use_extra_info_filter=true`;`CheckValid` 收到的 ID
是索引的外部 label。

每条 query 必须至少允许 `top_k` 个 ID 通过,ground truth 必须使用对应 filter 或 bitset
生成;推荐参数应继续用于具有同类代表性过滤分布的 workload。`TuneIndex` 同样支持这种
HGraph workload;存在任一种过滤输入且省略 `index_spaces` 时只生成 HGraph 候选。filter 和
bitset 都不会保存到索引中,调用方在最终的每次 `KnnSearch` 中仍需传入对应值。调用方提供的
过滤输入只支持 typed API,不会写入 CLI 请求或报告。本版本不支持 IVF 和 Pyramid 的
filtered tuning。

CLI 的 `index_path` 仍是离线适配:它先使用具体 create 参数创建并反序列化 Index,再进入
同一条 search-only 流程。

Expand All @@ -192,14 +217,17 @@ Pyramid 原生的默认/root 搜索。typed 请求直接从 `workload.queries->G
发起一次 typed AutoTune 请求,并提供与该 path 对应的 ground truth。V1 不把多个 path 的
推荐聚合成一条结果。

完整示例见
[`examples/cpp/327_feature_autotune_existing_index.cpp`][existing-index-example]。
`FilterPtr` 完整示例见
[`examples/cpp/327_feature_autotune_existing_index.cpp`][existing-index-example];直接传入每条
query 的排除 bitset 见
[`examples/cpp/330_feature_autotune_existing_index_bitset.cpp`][bitset-example]。
Pyramid path 调优示例见
[`examples/cpp/328_feature_autotune_existing_pyramid.cpp`][pyramid-example]。
它对同一个 Pyramid 索引中的 512-vector 和 4096-vector 叶子子图使用相同 recall 目标分别
调优,用于展示不同 path 可能需要不同的 `ef_search`。

[existing-index-example]: https://github.com/antgroup/vsag/blob/main/examples/cpp/327_feature_autotune_existing_index.cpp
[bitset-example]: https://github.com/antgroup/vsag/blob/main/examples/cpp/330_feature_autotune_existing_index_bitset.cpp
[pyramid-example]: https://github.com/antgroup/vsag/blob/main/examples/cpp/328_feature_autotune_existing_pyramid.cpp

## 指标
Expand Down Expand Up @@ -245,5 +273,5 @@ typed `TuneIndex` 和 `TuneSearch` 绝不会写报告文件,而是通过返回
## V1 边界

V1 评测一个 KNN workload;除已支持的 HGraph `ef_search` 自适应搜索外,其他候选仍完整遍历。
它暂不支持过滤或范围查询 workload、query sampling、跨请求 build cache,以及基于模型的
候选生成
typed API 支持 HGraph 的逐 query ID filter;CLI 和其他索引类型暂不支持 filtered workload。
V1 仍不支持范围查询 workload、query sampling、跨请求 build cache,以及基于模型的候选生成
32 changes: 31 additions & 1 deletion examples/cpp/327_feature_autotune_existing_index.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,35 @@

#include <cstdint>
#include <iostream>
#include <memory>
#include <string>
#include <vector>

#include "autotune.h"

namespace {

class ParityFilter : public vsag::Filter {
public:
explicit ParityFilter(int64_t parity) : parity_(parity) {
}

bool
CheckValid(int64_t id) const override {
return id % 2 == parity_;
}

float
ValidRatio() const override {
return 0.5F;
}

private:
int64_t parity_;
};

} // namespace

int
main() {
constexpr int64_t DIM = 4;
Expand Down Expand Up @@ -57,6 +81,11 @@ main() {
->Dim(1)
->Ids(ground_truth_ids.data())
->Owner(false);
std::vector<vsag::FilterPtr> filters;
filters.reserve(QUERY_COUNT);
for (int64_t i = 0; i < QUERY_COUNT; ++i) {
filters.emplace_back(std::make_shared<ParityFilter>(ground_truth_ids[i] % 2));
}

const std::string create_params = R"(
{
Expand Down Expand Up @@ -84,6 +113,7 @@ main() {
vsag::autotune::SearchRequest request;
request.index = index;
request.workload = {queries, ground_truth, 1, 1};
request.workload.query_filters = filters;
request.parameter_space = R"({"hgraph":{"ef_search":[4,8,16]}})";
request.constraints = {{vsag::autotune::Metric::RECALL_AT_K, 1.0}};
request.objective = vsag::autotune::Metric::LATENCY_AVG_MS;
Expand All @@ -110,7 +140,7 @@ main() {
->Dim(DIM)
->Float32Vectors(query_vectors.data())
->Owner(false);
auto neighbors = index->KnnSearch(query, 1, result.parameters);
auto neighbors = index->KnnSearch(query, 1, result.parameters, filters[0]);
if (!neighbors.has_value()) {
std::cerr << "Search failed: " << neighbors.error().message << std::endl;
return 1;
Expand Down
132 changes: 132 additions & 0 deletions examples/cpp/330_feature_autotune_existing_index_bitset.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
// Copyright 2024-present the vsag project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#include <vsag/vsag.h>

#include <cstdint>
#include <iostream>
#include <string>
#include <utility>
#include <vector>

#include "autotune.h"

int
main() {
constexpr int64_t DIM = 4;
constexpr int64_t BASE_COUNT = 16;
constexpr int64_t QUERY_COUNT = 4;
vsag::Options::Instance().set_block_size_limit(2UL * 1024 * 1024);

std::vector<int64_t> base_ids(BASE_COUNT);
std::vector<float> base_vectors(BASE_COUNT * DIM);
for (int64_t i = 0; i < BASE_COUNT; ++i) {
base_ids[i] = 1000 + i;
for (int64_t j = 0; j < DIM; ++j) {
base_vectors[i * DIM + j] = static_cast<float>(i * DIM + j);
}
}

std::vector<float> query_vectors(base_vectors.begin(),
base_vectors.begin() + QUERY_COUNT * DIM);
std::vector<int64_t> ground_truth_ids(base_ids.begin(), base_ids.begin() + QUERY_COUNT);

auto base = vsag::Dataset::Make()
->NumElements(BASE_COUNT)
->Dim(DIM)
->Ids(base_ids.data())
->Float32Vectors(base_vectors.data())
->Owner(false);
auto queries = vsag::Dataset::Make()
->NumElements(QUERY_COUNT)
->Dim(DIM)
->Float32Vectors(query_vectors.data())
->Owner(false);
auto ground_truth = vsag::Dataset::Make()
->NumElements(QUERY_COUNT)
->Dim(1)
->Ids(ground_truth_ids.data())
->Owner(false);

std::vector<vsag::BitsetPtr> invalid_bitsets;
invalid_bitsets.reserve(QUERY_COUNT);
for (int64_t i = 0; i < QUERY_COUNT; ++i) {
auto invalid = vsag::Bitset::Make();
for (const auto id : base_ids) {
invalid->Set(id, id % 2 != ground_truth_ids[i] % 2);
}
invalid_bitsets.emplace_back(std::move(invalid));
}

const std::string create_params = R"(
{
"dim": 4,
"dtype": "float32",
"metric_type": "l2",
"index_param": {
"base_quantization_type": "fp32",
"max_degree": 8,
"ef_construction": 40
}
})";
auto created = vsag::Factory::CreateIndex("hgraph", create_params);
if (!created.has_value()) {
std::cerr << "Failed to create index: " << created.error().message << std::endl;
return 1;
}
auto index = created.value();
auto built = index->Build(base);
if (!built.has_value()) {
std::cerr << "Failed to build index: " << built.error().message << std::endl;
return 1;
}

vsag::autotune::SearchRequest request;
request.index = index;
request.workload = {queries, ground_truth, 1, 1};
request.workload.query_invalid_bitsets = invalid_bitsets;
request.parameter_space = R"({"hgraph":{"ef_search":[4,8,16]}})";
request.constraints = {{vsag::autotune::Metric::RECALL_AT_K, 1.0}};
request.objective = vsag::autotune::Metric::LATENCY_AVG_MS;
request.config.max_trials = 3;

const auto tuned = vsag::autotune::TuneSearch(request);
if (!tuned.has_value()) {
std::cerr << "AutoTune failed: " << tuned.error().message << std::endl;
return 1;
}
if (tuned->status == vsag::autotune::TuneStatus::NO_FEASIBLE_CANDIDATE) {
std::cerr << "No candidate satisfied the constraints. Best effort:\n"
<< tuned->best_effort.dump(2) << std::endl;
return 2;
}

const auto& result = tuned.value();
std::cout << "recommended search_params: " << result.parameters << '\n'
<< "validated metrics: " << result.metrics.dump() << '\n'
<< "trials evaluated: " << result.report["trials"].size() << std::endl;

auto query = vsag::Dataset::Make()
->NumElements(1)
->Dim(DIM)
->Float32Vectors(query_vectors.data())
->Owner(false);
auto neighbors = index->KnnSearch(query, 1, result.parameters, invalid_bitsets[0]);
if (!neighbors.has_value()) {
std::cerr << "Search failed: " << neighbors.error().message << std::endl;
return 1;
}
std::cout << "first neighbor id: " << neighbors.value()->GetIds()[0] << std::endl;
return 0;
}
4 changes: 4 additions & 0 deletions examples/cpp/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,10 @@ if (TARGET vsag::autotune)
add_executable (328_feature_autotune_existing_pyramid
328_feature_autotune_existing_pyramid.cpp)
target_link_libraries (328_feature_autotune_existing_pyramid vsag::autotune)

add_executable (330_feature_autotune_existing_index_bitset
330_feature_autotune_existing_index_bitset.cpp)
target_link_libraries (330_feature_autotune_existing_index_bitset vsag::autotune)
endif ()

add_executable(329_feature_ivf_precise_bucket 329_feature_ivf_precise_bucket.cpp)
Expand Down
Loading
Loading