From f376f8d677a3c7cbeab62f3039ed02253ff3c389 Mon Sep 17 00:00:00 2001 From: LHT129 Date: Tue, 30 Jun 2026 12:47:54 +0800 Subject: [PATCH] fix(hgraph): expose skip_ratio and skip_strategy search parameters (v0.18 backport) Expose skip_ratio and skip_strategy as configurable search parameters for HGraph index. These parameters control the filter skip strategy during graph traversal, allowing users to tune the trade-off between search speed and recall when using filters. Key changes: - Add skip_ratio and skip_strategy_type fields to HGraphSearchParameters - Parse these parameters from JSON in FromJson() - Add validation for skip_ratio range [0.0, 1.0] - Pass parameters to InnerSearchParam in both KnnSearch and SearchWithRequest paths - Invert skip_ratio semantics to match parameter name meaning: skip_ratio now means "what percentage of invalid points to skip" - Remove special case for valid_ratio==1.0 in get_retain_ratio - Add unit tests for parameter parsing and validation - Update test comments with correct formula skip_ratio semantics (after inversion): - skip_ratio=0.2 (default): skip 20% of invalid points - retain_ratio = (1 - valid_ratio) * (1 - skip_ratio) - visit_ratio = valid_ratio + retain_ratio The underlying skip mechanism was already supported in BasicSearcher/ ParallelSearcher since v0.17.0, but was never exposed through the public API for HGraph. This change maintains backward compatibility by using the same default value (0.2) that was previously hardcoded. Fixes: #2368 Signed-off-by: LHT129 Assisted-by: opencode:qwen3.7-plus --- src/algorithm/hgraph.cpp | 6 ++++ src/algorithm/hgraph_parameter.cpp | 12 ++++++++ src/algorithm/hgraph_parameter.h | 4 +++ src/algorithm/hgraph_parameter_test.cpp | 29 +++++++++++++++++++ src/impl/inner_search_param.h | 2 +- src/index/hnsw_zparameters.h | 2 +- src/utils/filter_search_skip_strategy.cpp | 7 ++--- .../filter_search_skip_strategy_test.cpp | 16 +++++----- 8 files changed, 63 insertions(+), 15 deletions(-) diff --git a/src/algorithm/hgraph.cpp b/src/algorithm/hgraph.cpp index 679c27b4e5..09c415ef7c 100644 --- a/src/algorithm/hgraph.cpp +++ b/src/algorithm/hgraph.cpp @@ -943,6 +943,8 @@ HGraph::KnnSearch(const DatasetPtr& query, search_param.topk = static_cast(search_param.ef); search_param.consider_duplicate = this->label_table_->CompressDuplicateData(); search_param.parallel_search_thread_count = params.parallel_search_thread_count; + search_param.skip_ratio = params.skip_ratio; + search_param.skip_strategy_type = params.skip_strategy_type; search_result = this->search_one_graph(query_data, this->bottom_graph_, @@ -1153,6 +1155,8 @@ HGraph::RangeSearch(const DatasetPtr& query, search_param.consider_duplicate = true; search_param.range_search_limit_size = static_cast(limited_size); search_param.parallel_search_thread_count = params.parallel_search_thread_count; + search_param.skip_ratio = params.skip_ratio; + search_param.skip_strategy_type = params.skip_strategy_type; auto search_result = this->search_one_graph(raw_query, this->bottom_graph_, @@ -2197,6 +2201,8 @@ HGraph::SearchWithRequest(const SearchRequest& request) const { stats.is_timeout.store(false, std::memory_order_relaxed); } search_param.parallel_search_thread_count = params.parallel_search_thread_count; + search_param.skip_ratio = params.skip_ratio; + search_param.skip_strategy_type = params.skip_strategy_type; // hops_limit only takes effect when it's greater than ef_search if (params.hops_limit <= static_cast(params.ef_search)) { diff --git a/src/algorithm/hgraph_parameter.cpp b/src/algorithm/hgraph_parameter.cpp index 86949cf3ad..dd24cd84e5 100644 --- a/src/algorithm/hgraph_parameter.cpp +++ b/src/algorithm/hgraph_parameter.cpp @@ -220,6 +220,18 @@ HGraphSearchParameters::FromJson(const std::string& json_string) { params[INDEX_TYPE_HGRAPH][HGRAPH_USE_EXTRA_INFO_FILTER].GetBool(); } + 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)); + } + 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; } } // namespace vsag diff --git a/src/algorithm/hgraph_parameter.h b/src/algorithm/hgraph_parameter.h index 6b648a44f1..84429d6682 100644 --- a/src/algorithm/hgraph_parameter.h +++ b/src/algorithm/hgraph_parameter.h @@ -20,6 +20,7 @@ #include "data_type.h" #include "index_search_parameter.h" #include "inner_index_parameter.h" +#include "utils/filter_search_skip_strategy.h" #include "utils/pointer_define.h" #include "vsag/constants.h" @@ -81,6 +82,9 @@ class HGraphSearchParameters : public IndexSearchParameter { uint32_t hops_limit{std::numeric_limits::max()}; bool use_reorder{false}; bool use_extra_info_filter{false}; + float skip_ratio{0.2F}; + FilterSearchSkipStrategyType skip_strategy_type{ + FilterSearchSkipStrategyType::DETERMINISTIC_ACCUMULATIVE}; private: HGraphSearchParameters() = default; diff --git a/src/algorithm/hgraph_parameter_test.cpp b/src/algorithm/hgraph_parameter_test.cpp index 949256f127..78b649b176 100644 --- a/src/algorithm/hgraph_parameter_test.cpp +++ b/src/algorithm/hgraph_parameter_test.cpp @@ -228,3 +228,32 @@ TEST_CASE("HGraph maps label_remap_type to inner index parameter", "[ut][HGraphP REQUIRE(typed_param != nullptr); REQUIRE(typed_param->label_remap_type == vsag::LabelRemapType::ROBIN); } + +TEST_CASE("HGraphSearchParameters parses skip_ratio and skip_strategy", + "[ut][HGraphSearchParameters][skip_ratio]") { + 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": "random"}})"); + REQUIRE(params.skip_strategy_type == vsag::FilterSearchSkipStrategyType::RANDOM); + } + + SECTION("rejects out-of-range skip_ratio") { + REQUIRE_THROWS(vsag::HGraphSearchParameters::FromJson( + R"({"hgraph": {"ef_search": 32, "skip_ratio": 1.5}})")); + REQUIRE_THROWS(vsag::HGraphSearchParameters::FromJson( + R"({"hgraph": {"ef_search": 32, "skip_ratio": -0.1}})")); + } +} diff --git a/src/impl/inner_search_param.h b/src/impl/inner_search_param.h index 914abd3ed2..361b334be0 100644 --- a/src/impl/inner_search_param.h +++ b/src/impl/inner_search_param.h @@ -43,7 +43,7 @@ class InnerSearchParam { uint64_t ef{10}; uint32_t hops_limit{std::numeric_limits::max()}; FilterPtr is_inner_id_allowed{nullptr}; - float skip_ratio{0.8F}; + float skip_ratio{0.2F}; FilterSearchSkipStrategyType skip_strategy_type{ FilterSearchSkipStrategyType::DETERMINISTIC_ACCUMULATIVE}; InnerSearchMode search_mode{KNN_SEARCH}; diff --git a/src/index/hnsw_zparameters.h b/src/index/hnsw_zparameters.h index 4fe2cab177..c35d7afc06 100644 --- a/src/index/hnsw_zparameters.h +++ b/src/index/hnsw_zparameters.h @@ -62,7 +62,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}; bool use_conjugate_graph_search; diff --git a/src/utils/filter_search_skip_strategy.cpp b/src/utils/filter_search_skip_strategy.cpp index 1497fe9fb7..f87975ef99 100644 --- a/src/utils/filter_search_skip_strategy.cpp +++ b/src/utils/filter_search_skip_strategy.cpp @@ -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(1.0F - valid_ratio) * static_cast(skip_ratio); + return static_cast(1.0F - valid_ratio) * static_cast(1.0F - skip_ratio); } class RandomFilterSearchSkipStrategy : public FilterSearchSkipStrategy { @@ -39,7 +36,7 @@ class RandomFilterSearchSkipStrategy : public FilterSearchSkipStrategy { bool ShouldVisit() override { - return generator_.NextFloat() > visit_threshold_; + return generator_.NextFloat() >= visit_threshold_; } private: diff --git a/src/utils/filter_search_skip_strategy_test.cpp b/src/utils/filter_search_skip_strategy_test.cpp index cb92ccb912..85dacd9fc3 100644 --- a/src/utils/filter_search_skip_strategy_test.cpp +++ b/src/utils/filter_search_skip_strategy_test.cpp @@ -24,6 +24,8 @@ TEST_CASE("Filter search skip strategy parse", "[ut][filter_search_skip_strategy FilterSearchSkipStrategyType::RANDOM); REQUIRE(parse_filter_search_skip_strategy_type("deterministic_accumulative") == FilterSearchSkipStrategyType::DETERMINISTIC_ACCUMULATIVE); + REQUIRE(std::string(filter_search_skip_strategy_type_to_string( + FilterSearchSkipStrategyType::RANDOM)) == "random"); REQUIRE(std::string(filter_search_skip_strategy_type_to_string( FilterSearchSkipStrategyType::DETERMINISTIC_ACCUMULATIVE)) == "deterministic_accumulative"); @@ -32,7 +34,7 @@ TEST_CASE("Filter search skip strategy parse", "[ut][filter_search_skip_strategy TEST_CASE("Accumulative ShouldVisit is deterministic", "[ut][filter_search_skip_strategy]") { constexpr float valid_ratio = 0.5F; - constexpr float skip_ratio = 0.8F; + constexpr float skip_ratio = 0.2F; std::vector first_sequence; std::vector second_sequence; @@ -47,7 +49,7 @@ TEST_CASE("Accumulative ShouldVisit is deterministic", "[ut][filter_search_skip_ } REQUIRE(first_sequence == second_sequence); - // visit_ratio = 0.5 + 0.5*0.8 = 0.9 + // visit_ratio = 0.5 + (1-0.5)*(1-0.2) = 0.5 + 0.4 = 0.9 // Accumulative pattern: F,T,T,T,T,T,T,T,T,T repeated = 18/20 true std::vector expected = {false, true, true, true, true, true, true, true, true, true, false, true, true, true, true, true, true, true, true, true}; @@ -63,18 +65,16 @@ TEST_CASE("ShouldVisit edge cases", "[ut][filter_search_skip_strategy]") { } } - SECTION("skip ratio zero with low valid ratio visits less") { + SECTION("skip ratio zero means no skipping") { auto strategy = create_filter_search_skip_strategy( FilterSearchSkipStrategyType::DETERMINISTIC_ACCUMULATIVE, 0.5F, 0.0F); - // visit_ratio = 0.5 + 0.5*0 = 0.5 - // Accumulative pattern: F,T,F,T,... alternating = exactly 50/100 true + // visit_ratio = 0.5 + (1-0.5)*(1-0.0) = 0.5 + 0.5 = 1.0 + // Accumulative pattern: all true since visit_ratio = 1.0 std::vector sequence; for (uint64_t i = 0; i < 20; ++i) { sequence.emplace_back(strategy->ShouldVisit()); } - std::vector expected = {false, true, false, true, false, true, false, - true, false, true, false, true, false, true, - false, true, false, true, false, true}; + std::vector expected(20, true); // All true since we visit everything REQUIRE(sequence == expected); } }