From ac61bba438da91fcbc4c5e1491df0480694a5557 Mon Sep 17 00:00:00 2001 From: vsag-bot <276218163+vsag-bot@users.noreply.github.com> Date: Thu, 17 Sep 2026 11:00:14 +0000 Subject: [PATCH] fix(hgraph): handle empty dirty-vector candidates Signed-off-by: vsag-bot <276218163+vsag-bot@users.noreply.github.com> Assisted-by: Codex:codex-default --- src/algorithm/hgraph.cpp | 53 +++++++++++++++++++----------- src/algorithm/hgraph_add_test.cpp | 42 +++++++++++++++++++++++ src/algorithm/pyramid.cpp | 4 +-- src/impl/pruning_strategy.cpp | 8 +++-- src/impl/pruning_strategy_test.cpp | 15 +++++++++ 5 files changed, 99 insertions(+), 23 deletions(-) diff --git a/src/algorithm/hgraph.cpp b/src/algorithm/hgraph.cpp index b788eef8e0..12ebc1570a 100644 --- a/src/algorithm/hgraph.cpp +++ b/src/algorithm/hgraph.cpp @@ -968,7 +968,9 @@ HGraph::KnnSearch(const DatasetPtr& query, search_param, (VisitedListPtr) nullptr, &ctx); - search_param.ep = result->Top().second; + if (not result->Empty()) { + search_param.ep = result->Top().second; + } } } @@ -1180,7 +1182,9 @@ HGraph::RangeSearch(const DatasetPtr& query, search_param, (VisitedListPtr) nullptr, &ctx); - search_param.ep = result->Top().second; + if (not result->Empty()) { + search_param.ep = result->Top().second; + } } CHECK_ARGUMENT((1 <= params.ef_search) and (params.ef_search <= 1000), // NOLINT @@ -1676,6 +1680,7 @@ HGraph::add_one_point(const void* data, int level, InnerIdType inner_id) { std::unique_lock add_lock(add_mutex_); if (level >= static_cast(this->route_graphs_.size()) || bottom_graph_->TotalCount() == 0) { std::scoped_lock wlock(this->global_mutex_); + const auto previous_route_graph_count = this->route_graphs_.size(); // level maybe a negative number(-1) for (auto j = static_cast(this->route_graphs_.size()); j <= level; ++j) { this->route_graphs_.emplace_back(this->generate_one_route_graph()); @@ -1684,7 +1689,7 @@ HGraph::add_one_point(const void* data, int level, InnerIdType inner_id) { if (insert_success) { entry_point_id_ = inner_id; } else { - this->route_graphs_.pop_back(); + this->route_graphs_.resize(previous_route_graph_count); } add_lock.unlock(); } else { @@ -1712,7 +1717,9 @@ HGraph::graph_add_one(const void* data, int level, InnerIdType inner_id) { for (auto j = this->route_graphs_.size() - 1; j > level; --j) { result = search_one_graph( data, route_graphs_[j], flatten_codes, param, (VisitedListPtr) nullptr, nullptr); - param.ep = result->Top().second; + if (not result->Empty()) { + param.ep = result->Top().second; + } } param.ef = this->ef_construct_; @@ -1736,13 +1743,16 @@ HGraph::graph_add_one(const void* data, int level, InnerIdType inner_id) { label_table_->SetDuplicateId(static_cast(param.duplicate_id), inner_id); return false; } - mutually_connect_new_element(inner_id, - result, - this->bottom_graph_, - flatten_codes, - neighbors_mutex_, - allocator_, - alpha_); + const auto next_entry_point = mutually_connect_new_element(inner_id, + result, + this->bottom_graph_, + flatten_codes, + neighbors_mutex_, + allocator_, + alpha_); + if (next_entry_point == INVALID_ENTRY_POINT) { + return false; + } } else { bottom_graph_->InsertNeighborsById(inner_id, Vector(allocator_)); } @@ -1756,13 +1766,16 @@ HGraph::graph_add_one(const void* data, int level, InnerIdType inner_id) { // to specify which overloaded function to call (VisitedListPtr) nullptr, nullptr); - mutually_connect_new_element(inner_id, - result, - route_graphs_[j], - flatten_codes, - neighbors_mutex_, - allocator_, - alpha_); + const auto next_entry_point = mutually_connect_new_element(inner_id, + result, + route_graphs_[j], + flatten_codes, + neighbors_mutex_, + allocator_, + alpha_); + if (next_entry_point == INVALID_ENTRY_POINT) { + return false; + } } else { route_graphs_[j]->InsertNeighborsById(inner_id, Vector(allocator_)); } @@ -2257,7 +2270,9 @@ HGraph::SearchWithRequest(const SearchRequest& request) const { for (auto i = static_cast(this->route_graphs_.size() - 1); i >= 0; --i) { auto result = this->search_one_graph( raw_query, this->route_graphs_[i], this->basic_flatten_codes_, search_param, vt, &ctx); - search_param.ep = result->Top().second; + if (not result->Empty()) { + search_param.ep = result->Top().second; + } } FilterPtr ft = nullptr; diff --git a/src/algorithm/hgraph_add_test.cpp b/src/algorithm/hgraph_add_test.cpp index af17e66077..1860013126 100644 --- a/src/algorithm/hgraph_add_test.cpp +++ b/src/algorithm/hgraph_add_test.cpp @@ -16,6 +16,7 @@ #include #include #include +#include #include #include @@ -139,3 +140,44 @@ TEST_CASE("HGraph Tune accepts an incomplete source after Add failure", REQUIRE(tune_result.has_value()); CHECK(tune_result.value()); } + +TEST_CASE("HGraph search handles an empty route result", "[ut][hgraph][search][nonfinite]") { + constexpr int64_t dim = 4; + constexpr int64_t base_count = 64; + + auto common_param = MakeCommonParam(dim); + auto hgraph_json = vsag::JsonType::Parse(R"({ + "base_quantization_type": "fp32", + "max_degree": 8, + "ef_construction": 32, + "build_thread_count": 1 + })"); + auto index = std::make_shared>(hgraph_json, common_param); + + std::vector base_vectors(base_count * dim); + std::vector base_ids(base_count); + for (int64_t i = 0; i < base_count; ++i) { + base_ids[i] = i; + for (int64_t j = 0; j < dim; ++j) { + base_vectors[i * dim + j] = static_cast(i + j); + } + } + auto base = MakeFloatDataset(base_vectors, base_ids, dim, base_count); + REQUIRE(index->Build(base).has_value()); + + std::vector query_vectors(dim, std::numeric_limits::quiet_NaN()); + std::vector query_ids = {base_count}; + auto query = MakeFloatDataset(query_vectors, query_ids, dim, 1); + auto add_result = index->Add(query); + REQUIRE(add_result.has_value()); + REQUIRE(add_result.value().empty()); + + vsag::SearchRequest request; + request.query_ = query; + request.topk_ = 10; + request.params_str_ = R"({"hgraph":{"ef_search":32}})"; + + auto result = index->SearchWithRequest(request); + REQUIRE(result.has_value()); + CHECK(result.value()->GetDim() == 0); +} diff --git a/src/algorithm/pyramid.cpp b/src/algorithm/pyramid.cpp index 6d47125269..c8a2817551 100644 --- a/src/algorithm/pyramid.cpp +++ b/src/algorithm/pyramid.cpp @@ -897,9 +897,9 @@ Pyramid::add_one_point(const std::shared_ptr& node, inner_id); return; } - mutually_connect_new_element( + const auto next_entry_point = mutually_connect_new_element( inner_id, results, node->graph_, codes, points_mutex_, allocator_, alpha_); - if (update_entry_point) { + if (update_entry_point and next_entry_point != INVALID_ENTRY_POINT) { node->entry_point_ = inner_id; } } diff --git a/src/impl/pruning_strategy.cpp b/src/impl/pruning_strategy.cpp index c4f8f43c8f..78e378b9a5 100644 --- a/src/impl/pruning_strategy.cpp +++ b/src/impl/pruning_strategy.cpp @@ -15,6 +15,7 @@ #include "pruning_strategy.h" +#include "common.h" #include "datacell/flatten_datacell.h" #include "datacell/graph_interface.h" #include "impl/heap/standard_heap.h" @@ -87,9 +88,12 @@ mutually_connect_new_element(InnerIdType cur_c, top_candidates->Pop(); } - InnerIdType next_closest_entry_point = selected_neighbors.back(); - graph->InsertNeighborsById(cur_c, selected_neighbors); + if (selected_neighbors.empty()) { + return INVALID_ENTRY_POINT; + } + + InnerIdType next_closest_entry_point = selected_neighbors.back(); for (auto selected_neighbor : selected_neighbors) { if (selected_neighbor == cur_c) { diff --git a/src/impl/pruning_strategy_test.cpp b/src/impl/pruning_strategy_test.cpp index 4d8d7511d1..c4e9a75f97 100644 --- a/src/impl/pruning_strategy_test.cpp +++ b/src/impl/pruning_strategy_test.cpp @@ -21,6 +21,7 @@ #include #include +#include "common.h" #include "datacell/flatten_datacell.h" #include "datacell/flatten_datacell_parameter.h" #include "datacell/graph_datacell_parameter.h" @@ -194,6 +195,20 @@ TEST_CASE("Pruning Strategy Select Edges With Heuristic", "[ut][pruning_strategy graph->GetNeighbors(0, neighbors_0); REQUIRE(neighbors_0.size() == 1); } + + SECTION("Mutual connection supports no candidates") { + auto graph_param = std::make_shared(); + graph_param->io_parameter_ = std::make_shared(); + graph_param->max_degree_ = 4; + auto graph = GraphInterface::MakeInstance(graph_param, common_param); + + auto candidates = std::make_shared>(allocator.get(), -1); + auto mutexes = std::make_shared(); + auto entry_point = + mutually_connect_new_element(0, candidates, graph, flatten, mutexes, allocator.get()); + + REQUIRE(entry_point == INVALID_ENTRY_POINT); + } } } // namespace vsag