Skip to content
Merged
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
54 changes: 52 additions & 2 deletions docs/docs/en/src/indexes/hgraph.md
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,8 @@ Search-time parameters live under the `hgraph` sub-object:
|-----------|------|---------|-------------|
| `ef_search` | int | — (required) | Size of the search frontier. Larger = higher recall, slower query. |
| `hops_limit` | int | unlimited | Hard cap on the number of hops the beam search performs before returning the current frontier. |
| `skip_ratio` | float | `0.2` | Performance tuning parameter for filtered search. Controls the ratio of invalid points to skip, in range `[0.0, 1.0]`. `skip_ratio=0.2` means skip 20% of invalid points and only check 80%. Higher values improve performance but may reduce recall. Only applies to searches with filters. See [Filter Skip Strategy](#filter-skip-strategy-skip_ratio-and-skip_strategy) below. |
| `skip_strategy` | string | `"deterministic_accumulative"` | Strategy for filter skipping. Options: `"random"` (random skipping) or `"deterministic_accumulative"` (deterministic cumulative skipping). See [Filter Skip Strategy](#filter-skip-strategy-skip_ratio-and-skip_strategy) below. |
Comment thread
LHT129 marked this conversation as resolved.
| `brute_force_threshold` | float | `0.0` | Selectivity-aware brute-force fallback. When `> 0` and the supplied filter's `ValidRatio()` is `≤ brute_force_threshold`, the search **bypasses the graph traversal entirely** and runs an exact scan over the valid ids using the best available flatten codes (see the section below). Must lie in `[0.0, 1.0]`; the default `0.0` disables the feature and preserves legacy behavior. |
| `rabitq_one_bit_search` | bool | `false` | Enables the RaBitQ filter/lower-bound path. On an x+y split index it uses all x filter bits; see [RaBitQ x+y Split](../quantization/rabitq_split.md). |
| `rabitq_error_rate` | float | index default | Positive lower-bound error multiplier for this search. It can be tuned without rebuilding the split index. |
Expand Down Expand Up @@ -262,8 +264,56 @@ Picking a value:
is visited to check `CheckValid`). The benefit grows when graph search would
otherwise need a much larger `ef_search` to recover recall.

A runnable example is
[`322_feature_hgraph_brute_force_threshold.cpp`](https://github.com/antgroup/vsag/blob/main/examples/cpp/322_feature_hgraph_brute_force_threshold.cpp).
See
[`322_feature_hgraph_brute_force_threshold.cpp`](https://github.com/antgroup/vsag/blob/main/examples/cpp/322_feature_hgraph_brute_force_threshold.cpp)
for a runnable brute-force fallback example.

### Filter Skip Strategy (skip_ratio and skip_strategy)

When searching with a filter, HGraph needs to frequently call Filter::CheckValid() during graph traversal to verify whether each candidate point is valid. This check can be expensive (especially for complex filter logic). skip_ratio and skip_strategy provide a probabilistic optimization: they skip some filter checks to speed up the search, but may reduce recall.

#### How It Works

This is a probabilistic optimization strategy: we don't know in advance which points are valid, so we decide probabilistically whether to visit each point.
Comment thread
LHT129 marked this conversation as resolved.

- skip_ratio (default 0.2): Controls the aggressiveness of skipping filter checks. skip_ratio=0.2 means skip 20% of candidate checks and only check 80%. Higher values skip more, making search faster but potentially reducing recall.
- skip_strategy (default "deterministic_accumulative"): Determines how skipping is distributed:
- "random": Random skipping. Each point is visited independently with probability `visit_ratio = valid_ratio + (1 - valid_ratio) * (1 - skip_ratio)`, so roughly a `1 - skip_ratio` fraction of invalid points are skipped.
- "deterministic_accumulative": Deterministic cumulative skipping. Emits visit decisions at fixed intervals so that the long-run visit ratio matches the target `visit_ratio`, with lower variance than the random strategy.

The specific formula:
- Let valid_ratio be the filter's global validity rate (from Filter::ValidRatio())
- Probability of visiting each point = valid_ratio + (1 - valid_ratio) * (1 - skip_ratio)
- In expectation, this targets skipping about skip_ratio of invalid candidate checks when Filter::ValidRatio() is accurate

#### Usage Examples

```cpp
// Conservative setting: skip 10% of invalid candidate checks, suitable for high-recall
// scenarios where latency is less critical
auto params = R"({"hgraph": {"ef_search": 200, "skip_ratio": 0.1}})";
auto result = index->KnnSearch(query, topk, params, my_filter).value();

// Use random strategy
auto params = R"({"hgraph": {"ef_search": 200, "skip_ratio": 0.2, "skip_strategy": "random"}})";
auto result = index->KnnSearch(query, topk, params, my_filter).value();

// Aggressive skipping: skip 50% of invalid candidate checks for lower latency
auto params = R"({"hgraph": {"ef_search": 200, "skip_ratio": 0.5}})";
auto result = index->KnnSearch(query, topk, params, my_filter).value();
```

#### Choosing Values

- Default 0.2: Suitable for most scenarios, balancing performance and recall.
- 0.1 or lower: Conservative setting, suitable for scenarios with high recall requirements where latency is less critical.
- 0.5 or higher: Aggressive skipping, suitable for latency-sensitive scenarios where recall degradation is acceptable (e.g., real-time recommendation systems).
- 0.0: Don't skip any points, equivalent to disabling this optimization (all points will be checked).

Important notes:
- Only applies to searches with filters. These parameters are ignored when no filter is present.
- Performance optimization works better when Filter::ValidRatio() is accurately estimated.
- Can be used together with brute_force_threshold: when the filter is very strict (ValidRatio is very small), brute_force_threshold will trigger brute-force fallback; otherwise, graph traversal + skip optimization is used.

## When to use HGraph

Expand Down
7 changes: 7 additions & 0 deletions docs/docs/en/src/resources/index_parameters.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,13 @@ LazyHGraph only supports `dtype: "float32"`. Search parameters use the `hgraph`
object, for example `{"hgraph": {"ef_search": 100}}`. See the
[LazyHGraph index page](../indexes/lazy_hgraph.md) for details.

The `hgraph` search-param object also accepts the following filter-related parameters:

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `skip_ratio` | float | `0.2` | Controls the ratio of filtered-search candidate checks to skip, in range `[0.0, 1.0]`. Higher values mean more aggressive skipping, faster search, and potentially lower recall. |
| `skip_strategy` | string | `"deterministic_accumulative"` | Skip strategy. Supports `"random"` and `"deterministic_accumulative"`. |

## IVF

```json
Expand Down
48 changes: 48 additions & 0 deletions docs/docs/zh/src/indexes/hgraph.md
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,8 @@ base->NumElements(num_vectors)->Dim(dim)->Ids(ids)
|------|------|--------|------|
| `ef_search` | int | —(必填) | 搜索前沿候选集的大小,越大召回越高、查询越慢。 |
| `hops_limit` | int | 不限 | beam search 在返回当前前沿前允许的最大跳数。 |
| `skip_ratio` | float | `0.2` | 过滤场景下的性能调优参数。控制跳过无效点的比例,取值范围 `[0.0, 1.0]`。`skip_ratio=0.2` 表示跳过 20% 的无效点,只检查 80% 的无效点。值越大性能越好但召回率可能越低。仅在带 filter 的搜索中生效。详见下文[过滤跳过策略](#过滤跳过策略skip_ratio-与-skip_strategy)。 |
| `skip_strategy` | string | `"deterministic_accumulative"` | 过滤跳过的策略。可选值:`"random"`(随机跳过)或 `"deterministic_accumulative"`(确定性累积跳过)。详见下文[过滤跳过策略](#过滤跳过策略skip_ratio-与-skip_strategy)。 |
| `brute_force_threshold` | float | `0.0` | 选择率感知的暴搜回退开关。当取值 `> 0` 且当前 filter 的 `ValidRatio()` 小于等于 `brute_force_threshold` 时,搜索会**完全跳过图遍历**,直接在通过过滤的 id 上用最佳精度的 flatten 编码做一次暴力扫描(细节见下一节)。取值范围 `[0.0, 1.0]`;默认 `0.0` 表示关闭,保持原有行为。 |
| `rabitq_one_bit_search` | bool | `false` | 启用 RaBitQ filter/lower-bound 路径;对 x+y split 索引会使用全部 x 个 filter bits,详见 [RaBitQ x+y Split](../quantization/rabitq_split.md)。 |
| `rabitq_error_rate` | float | 索引默认值 | 本次搜索使用的正数 lower-bound 误差倍率;调整它不需要重建 split 索引。 |
Expand Down Expand Up @@ -243,6 +245,52 @@ auto result = index->KnnSearch(query, topk, params, my_filter).value();
可运行示例:
[`322_feature_hgraph_brute_force_threshold.cpp`](https://github.com/antgroup/vsag/blob/main/examples/cpp/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 比例的无效候选检查

#### 使用示例

```cpp
// 保守设置:跳过 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 向量。
Expand Down
7 changes: 7 additions & 0 deletions docs/docs/zh/src/resources/index_parameters.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,13 @@ LazyHGraph 的构建参数可以放在顶层 `lazy_hgraph` 对象中(推荐,
LazyHGraph 只支持 `dtype: "float32"`。搜索参数使用 `hgraph` 对象,例如
`{"hgraph": {"ef_search": 100}}`。详见 [LazyHGraph 索引文档](../indexes/lazy_hgraph.md)。

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

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

## IVF

```json
Expand Down
12 changes: 12 additions & 0 deletions src/algorithm/hgraph/hgraph_parameter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,18 @@ HGraphSearchParameters::FromJson(const std::string& json_string) {
fmt::format("rabitq_error_rate must be finite and positive, got {}",
obj.rabitq_error_rate));
}
if (params[INDEX_TYPE_HGRAPH].Contains(HNSW_PARAMETER_SKIP_RATIO)) {
obj.skip_ratio = params[INDEX_TYPE_HGRAPH][HNSW_PARAMETER_SKIP_RATIO].GetFloat();
CHECK_ARGUMENT((0.0F <= obj.skip_ratio) and (obj.skip_ratio <= 1.0F), // NOLINT
fmt::format("skip_ratio({}) must be in range [0.0, 1.0]", obj.skip_ratio));
}
Comment thread
LHT129 marked this conversation as resolved.
Comment thread
LHT129 marked this conversation as resolved.
if (params[INDEX_TYPE_HGRAPH].Contains(HNSW_PARAMETER_SKIP_STRATEGY)) {
CHECK_ARGUMENT(
params[INDEX_TYPE_HGRAPH][HNSW_PARAMETER_SKIP_STRATEGY].IsString(),
fmt::format("parameters[{}] must be string type", HNSW_PARAMETER_SKIP_STRATEGY));
obj.skip_strategy_type = parse_filter_search_skip_strategy_type(
params[INDEX_TYPE_HGRAPH][HNSW_PARAMETER_SKIP_STRATEGY].GetString());
}

return obj;
}
Expand Down
4 changes: 4 additions & 0 deletions src/algorithm/hgraph/hgraph_parameter.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
#include "../index_search_parameter.h"
#include "../inner_index_parameter.h"
#include "data_type.h"
#include "utils/filter_search_skip_strategy.h"
#include "utils/pointer_define.h"
#include "vsag/constants.h"

Expand Down Expand Up @@ -90,6 +91,9 @@ class HGraphSearchParameters : public IndexSearchParameter {
// preserves existing behaviour.
float brute_force_threshold{0.0F};
float rabitq_error_rate{std::numeric_limits<float>::quiet_NaN()};
float skip_ratio{0.2F};
FilterSearchSkipStrategyType skip_strategy_type{
FilterSearchSkipStrategyType::DETERMINISTIC_ACCUMULATIVE};

private:
HGraphSearchParameters() = default;
Expand Down
64 changes: 64 additions & 0 deletions src/algorithm/hgraph/hgraph_parameter_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,70 @@ TEST_CASE("HGraphSearchParameters parses brute_force_threshold",
}
}

TEST_CASE("HGraphSearchParameters parses skip_ratio and skip_strategy",
"[ut][HGraphSearchParameters][skip_ratio]") {
Comment thread
LHT129 marked this conversation as resolved.
SECTION("default values") {
auto params = vsag::HGraphSearchParameters::FromJson(R"({"hgraph": {"ef_search": 32}})");
REQUIRE(params.skip_ratio == 0.2F);
REQUIRE(params.skip_strategy_type ==
vsag::FilterSearchSkipStrategyType::DETERMINISTIC_ACCUMULATIVE);
}

SECTION("custom skip_ratio") {
auto params = vsag::HGraphSearchParameters::FromJson(
R"({"hgraph": {"ef_search": 32, "skip_ratio": 0.5}})");
REQUIRE(params.skip_ratio == 0.5F);
}

SECTION("custom skip_strategy") {
auto params = vsag::HGraphSearchParameters::FromJson(
R"({"hgraph": {"ef_search": 32, "skip_strategy": "deterministic_accumulative"}})");
REQUIRE(params.skip_strategy_type ==
vsag::FilterSearchSkipStrategyType::DETERMINISTIC_ACCUMULATIVE);
}

SECTION("both skip_ratio and skip_strategy") {
auto params = vsag::HGraphSearchParameters::FromJson(
R"(
{
"hgraph": {
"ef_search": 32,
"skip_ratio": 0.3,
"skip_strategy": "deterministic_accumulative"
}
})");
REQUIRE(params.skip_ratio == 0.3F);
REQUIRE(params.skip_strategy_type ==
vsag::FilterSearchSkipStrategyType::DETERMINISTIC_ACCUMULATIVE);
}

SECTION("random skip_strategy") {
auto params = vsag::HGraphSearchParameters::FromJson(
R"({"hgraph": {"ef_search": 32, "skip_strategy": "random"}})");
REQUIRE(params.skip_strategy_type == vsag::FilterSearchSkipStrategyType::RANDOM);
}

SECTION("skip_ratio out of range - too high") {
REQUIRE_THROWS(vsag::HGraphSearchParameters::FromJson(
R"({"hgraph": {"ef_search": 32, "skip_ratio": 1.5}})"));
}
Comment thread
LHT129 marked this conversation as resolved.

SECTION("skip_ratio out of range - negative") {
REQUIRE_THROWS(vsag::HGraphSearchParameters::FromJson(
R"({"hgraph": {"ef_search": 32, "skip_ratio": -0.1}})"));
}

SECTION("skip_strategy rejects unknown values") {
REQUIRE_THROWS(vsag::HGraphSearchParameters::FromJson(
R"({"hgraph": {"ef_search": 32, "skip_strategy": "unknown"}})"));
}

SECTION("skip_strategy rejects non-string values") {
REQUIRE_THROWS(vsag::HGraphSearchParameters::FromJson(
R"({"hgraph": {"ef_search": 32, "skip_strategy": 123}})"));
}
}
Comment thread
LHT129 marked this conversation as resolved.

TEST_CASE("HGraph maps support_force_remove to inner parameter", "[ut][HGraphParameter]") {
auto param = vsag::JsonType::Parse(R"({
"base_quantization_type": "fp32",
Expand Down
5 changes: 5 additions & 0 deletions src/algorithm/hgraph/hgraph_search.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,8 @@ HGraph::KnnSearch(const DatasetPtr& query,
search_param.parallel_search_thread_count = params.parallel_search_thread_count;
search_param.enable_reorder = params.enable_reorder;
search_param.enable_rabitq_one_bit_search = params.rabitq_one_bit_search;
search_param.skip_ratio = params.skip_ratio;
search_param.skip_strategy_type = params.skip_strategy_type;

DistanceRecordVector rabitq_lower_bound_candidates(ctx.alloc);
auto* rabitq_lower_bound_candidates_ptr =
Expand Down Expand Up @@ -509,6 +511,9 @@ HGraph::SearchWithRequest(const SearchRequest& request) const {
}
}

search_param.skip_ratio = params.skip_ratio;
search_param.skip_strategy_type = params.skip_strategy_type;

DistanceRecordVector rabitq_lower_bound_candidates(ctx.alloc);
auto* rabitq_lower_bound_candidates_ptr =
search_param.enable_rabitq_one_bit_search and use_reorder_ and
Expand Down
2 changes: 1 addition & 1 deletion src/impl/inner_search_param.h
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ class InnerSearchParam {
uint64_t ef{10};
uint32_t hops_limit{std::numeric_limits<uint32_t>::max()};
FilterPtr is_inner_id_allowed{nullptr};
float skip_ratio{0.8F};
float skip_ratio{0.2F};
Comment thread
LHT129 marked this conversation as resolved.
Comment thread
LHT129 marked this conversation as resolved.
FilterSearchSkipStrategyType skip_strategy_type{
FilterSearchSkipStrategyType::DETERMINISTIC_ACCUMULATIVE};
InnerSearchMode search_mode{KNN_SEARCH};
Expand Down
2 changes: 1 addition & 1 deletion src/index/hnsw_zparameters.h
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ struct HnswSearchParameters {
public:
// required vars
int64_t ef_search;
float skip_ratio{0.9};
float skip_ratio{0.1F};
FilterSearchSkipStrategyType skip_strategy_type{
FilterSearchSkipStrategyType::DETERMINISTIC_ACCUMULATIVE};
Comment thread
LHT129 marked this conversation as resolved.
bool use_conjugate_graph_search;
Expand Down
7 changes: 2 additions & 5 deletions src/utils/filter_search_skip_strategy.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,7 @@ constexpr const char* DETERMINISTIC_ACCUMULATIVE_SKIP_STRATEGY = "deterministic_

double
get_retain_ratio(float valid_ratio, float skip_ratio) {
if (valid_ratio == 1.0F) {
return 1.0;
}
return static_cast<double>(1.0F - valid_ratio) * static_cast<double>(skip_ratio);
return static_cast<double>(1.0F - valid_ratio) * static_cast<double>(1.0F - skip_ratio);
}
Comment thread
LHT129 marked this conversation as resolved.

class RandomFilterSearchSkipStrategy : public FilterSearchSkipStrategy {
Expand All @@ -39,7 +36,7 @@ class RandomFilterSearchSkipStrategy : public FilterSearchSkipStrategy {

bool
ShouldVisit() override {
return generator_.NextFloat() > visit_threshold_;
return generator_.NextFloat() >= visit_threshold_;
}

private:
Expand Down
Loading
Loading