refactor(hgraph): centralize regular search routing - #2606
Conversation
|
/label status/waiting-for-review |
|
Automated pull request review completed. Review effort: Submitted 1 inline comment. |
Merge Protections🟢 All 2 merge protections satisfied — ready to merge. Show 2 satisfied protections🟢 Require kind label
🟢 Require version label
|
There was a problem hiding this comment.
Pull request overview
Note
Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.
Centralizes HGraph “regular” search route selection into a single internal selector while preserving existing routing priorities (brute-force vs MCI vs bottom-graph) and adding regression coverage for route selection.
Changes:
- Added an internal
SearchPlanselector (select_search_plan) and filter seed-source helpers to consolidate routing logic. - Refactored
HGraph::SearchWithRequestto route viaswitchon the selected plan (brute-force / MCI / bottom-graph). - Added tests to confirm brute-force precedence and that range search selects the bottom graph route.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| src/algorithm/hgraph/hgraph_search.cpp | Introduces centralized plan selection + helper functions and refactors routing in SearchWithRequest. |
| src/algorithm/hgraph/hgraph_mci_test.cpp | Adds coverage for brute-force precedence and range-search routing outcome. |
72eb877 to
5fa0e2f
Compare
5fa0e2f to
2e8bfa7
Compare
LHT129
left a comment
There was a problem hiding this comment.
Thanks for this refactoring. The extraction of SearchPlan, SearchPlanInput, and the helper functions into a dedicated decision module makes the routing logic much clearer and easier to test.
Summary of changes reviewed:
src/algorithm/hgraph/hgraph_search.cpp: +108/-16 — extractedSearchPlanenum,SearchPlanInputstruct, and helpers (is_mci_available,select_search_plan,has_valid_id_source,has_bitset_source) into an anonymous namespace; replaced the inline if-else routing with aswitchonselect_search_plan.src/algorithm/hgraph/hgraph_mci_test.cpp: +28/-0 — added 3 test cases covering brute-force precedence (KNN), MCI routing, and range search selecting the bottom graph.
What looks good:
- The
select_search_planfunction now takesmci_availableas a parameter, avoiding the redundantis_mci_availablecall noted earlier. - The
SearchPlanInputconstruction uses positional aggregate init (no C++20 designated initializers), maintaining C++17 compatibility. has_bitset_sourceincludes an explicit nullptr guard, consistent withhas_valid_id_source.- All three routing paths (brute force, MCI, bottom graph) are covered by tests.
- The
[[fallthrough]]fromkMCItokBottomGraphcorrectly handles the case wheretry_mci_searchfails to produce an MCI result. - The
mci_result.routeis correctly set to"hgraph"only when MCI was available but the bottom graph was chosen; it stays as"disabled"when MCI was never a candidate — preserving the existing statistics semantics.
One minor note posted as an inline comment about the fragility of positional aggregate initialization with 10 fields.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.
Suppressed comments (1)
src/algorithm/hgraph/hgraph_search.cpp:88
has_valid_id_source()/has_bitset_source()(and related MCI eligibility checks) are now duplicated betweenhgraph_search.cppandhgraph_mci.cpp(seesrc/algorithm/hgraph/hgraph_mci.cpp:77-92). This raises drift risk if future MCI eligibility semantics change in one place but not the other, which is exactly what this refactor is trying to avoid. Consider extracting these helpers into a shared internal header (e.g.,src/algorithm/hgraph/hgraph_mci_utils.h) and including it from both translation units so there is a single source of truth for seed-source detection.
bool
has_valid_id_source(const FilterPtr& filter) {
if (filter == nullptr) {
return false;
}
const int64_t* valid_ids = nullptr;
int64_t valid_count = 0;
filter->GetValidIds(&valid_ids, valid_count);
return valid_ids != nullptr and valid_count > 0;
}
bool
has_bitset_source(const FilterPtr& filter) {
if (filter == nullptr) {
return false;
}
const auto bitset_filter = std::dynamic_pointer_cast<BlackListFilter>(filter);
return bitset_filter != nullptr and bitset_filter->IsBitsetFilter();
}
LHT129
left a comment
There was a problem hiding this comment.
This revision addresses all previously raised comments:
- Switched from C++20 designated initializers to positional aggregate initialization for
SearchPlanInput. - Extracted
is_mci_availableresult into a local variable to avoid redundant calls. - Added explicit nullptr guard in
has_bitset_source. - Added test coverage for all three routing paths (MCI, brute-force, bottom graph).
The refactoring cleanly centralizes search routing logic into select_search_plan with a clear switch-based dispatch. No new issues found.
2e8bfa7 to
11b334f
Compare
| search_param.time_cost = std::make_shared<Timer>(); | ||
| search_param.time_cost->SetThreshold(params.timeout_ms); | ||
| stats.is_timeout.store(false, std::memory_order_relaxed); | ||
| ctx->stats->is_timeout.store(false, std::memory_order_relaxed); |
There was a problem hiding this comment.
[suggestion] configure_bottom_graph_search dereferences ctx->stats at line 602 (ctx->stats->is_timeout.store(...)) without any null check. While the current sole caller (SearchWithRequest) guarantees ctx->stats is non-null, this function is a public static method and could be called from other contexts in the future.
Consider either:
- Adding an assertion like
assert(ctx->stats != nullptr)before the dereference, or - Guarding with
if (ctx->stats != nullptr)before theis_timeout.storecall.
| } | ||
|
|
||
| /***** Step 9: Trim, pack, and annotate the final dataset. *****/ | ||
| return this->pack_search_result( |
There was a problem hiding this comment.
[suggestion] search_result is passed by value to pack_search_result at line 971-972, but it is a local variable in SearchWithRequest that is not used after this call. Passing with std::move(search_result) would avoid an unnecessary reference count increment/decrement on the shared pointer.
// Current:
return this->pack_search_result(
request, k, search_result, ctx, mci_result, stats, reasoning_ctx);
// Suggested:
return this->pack_search_result(
request, k, std::move(search_result), ctx, mci_result, stats, reasoning_ctx);
LHT129
left a comment
There was a problem hiding this comment.
This refactoring cleanly decomposes SearchWithRequest into well-named helper methods, making the search pipeline much easier to follow. The SearchPlan selector centralizes routing logic that was previously scattered.
I left three inline comments covering minor points:
- A dead-code guard in
search_route_graphsthat can never trigger in the current call path. - A potential null-pointer dereference of
ctx->statsinconfigure_bottom_graph_searchwhen called from future contexts. - A missed
std::moveopportunity when passingsearch_resulttopack_search_result.
None of these are blocking — the logic is correct and the test coverage for brute-force precedence and range search routing is solid.
|
|
||
| static void | ||
| configure_bottom_graph_search(const SearchRequest& request, | ||
| const HGraphSearchParameters& params, |
There was a problem hiding this comment.
[note] The QueryContext* ctx parameter in configure_bottom_graph_search is declared but never used in the function body (see hgraph_search.cpp implementation). If it was added for future extensibility, consider adding a comment explaining its intended purpose. Otherwise, it can be removed to keep the interface minimal.
LHT129
left a comment
There was a problem hiding this comment.
Code Review for PR #2606
[suggestion] select_search_plan called twice in SearchWithRequest
In hgraph_search.cpp, select_search_plan is called twice (lines 877-878 and 898). The first call with mci_available=false serves as a pre-check to determine whether brute force should be used regardless of MCI eligibility, so that MCI availability computation can be skipped when brute force is already selected.
This double-call pattern makes the control flow harder to follow. Consider computing MCI availability unconditionally first (the has_bitset_source / has_valid_id_source calls are cheap), then calling select_search_plan once with the real mci_available value. This would eliminate the pre-check and simplify the logic.
// Compute MCI availability unconditionally, then call select_search_plan once
const auto bitset_seed_source = has_bitset_source(request.filter_);
bool valid_id_seed_source = false;
if (params.use_mci and this->mci_parameters_.enabled and search_param.executors.empty()) {
valid_id_seed_source = has_valid_id_source(request.filter_);
if (valid_id_seed_source or bitset_seed_source) {
plan_input.mci_has_clique_index = ...;
}
}
plan_input.has_valid_id_source = valid_id_seed_source;
plan_input.has_bitset_source = bitset_seed_source;
const bool mci_available = is_mci_available(plan_input);
switch (select_search_plan(plan_input, mci_available)) { ... }[note] Brute force KNN search now receives request.threshold_
At line 905, the brute force KNN search call now passes request.threshold_ as an additional parameter:
search_result = this->brute_force_search<InnerSearchMode::KNN_SEARCH>(
raw_query, ft, k, 0.0F, &ctx, request.threshold_);The original code did not pass a threshold to brute force KNN search. This is a behavioral change that enables threshold-based filtering in the brute force path. Please confirm this is intentional and not an accidental inclusion from the refactoring.
[note] Redundant INVALID_ENTRY_POINT check in search_route_graphs
search_route_graphs (line 549) checks search_param.ep == INVALID_ENTRY_POINT and returns early. However, the caller SearchWithRequest already validates entry_point == INVALID_ENTRY_POINT at line 819 and returns before calling search_route_graphs. This defensive check is harmless but redundant.
Summary
The refactoring is well-structured and significantly improves readability by extracting clear sub-functions (create_query_context, search_route_graphs, configure_bottom_graph_search, pack_search_result, parse_and_validate_search_params, initialize_reasoning_context) from the monolithic SearchWithRequest. The VisitedListGuard RAII wrapper is a nice addition. The test coverage for brute-force precedence and range search routing is good.
The main suggestion is simplifying the double select_search_plan call pattern. The brute force threshold parameter addition should be confirmed as intentional.
LHT129
left a comment
There was a problem hiding this comment.
Code Review for PR #2606
[suggestion] select_search_plan called twice in SearchWithRequest
In hgraph_search.cpp, select_search_plan is called twice (lines 877-878 and 898). The first call with mci_available=false serves as a pre-check to determine whether brute force should be used regardless of MCI eligibility, so that MCI availability computation can be skipped when brute force is already selected.
This double-call pattern makes the control flow harder to follow. Consider computing MCI availability unconditionally first (the has_bitset_source / has_valid_id_source calls are cheap), then calling select_search_plan once with the real mci_available value. This would eliminate the pre-check and simplify the logic.
// Compute MCI availability unconditionally, then call select_search_plan once
const auto bitset_seed_source = has_bitset_source(request.filter_);
bool valid_id_seed_source = false;
if (params.use_mci and this->mci_parameters_.enabled and search_param.executors.empty()) {
valid_id_seed_source = has_valid_id_source(request.filter_);
if (valid_id_seed_source or bitset_seed_source) {
plan_input.mci_has_clique_index = ...;
}
}
plan_input.has_valid_id_source = valid_id_seed_source;
plan_input.has_bitset_source = bitset_seed_source;
const bool mci_available = is_mci_available(plan_input);
switch (select_search_plan(plan_input, mci_available)) { ... }[note] Brute force KNN search now receives request.threshold_
At line 905, the brute force KNN search call now passes request.threshold_ as an additional parameter:
search_result = this->brute_force_search<InnerSearchMode::KNN_SEARCH>(
raw_query, ft, k, 0.0F, &ctx, request.threshold_);The original code did not pass a threshold to brute force KNN search. This is a behavioral change that enables threshold-based filtering in the brute force path. Please confirm this is intentional and not an accidental inclusion from the refactoring.
[note] Redundant INVALID_ENTRY_POINT check in search_route_graphs
search_route_graphs (line 549) checks search_param.ep == INVALID_ENTRY_POINT and returns early. However, the caller SearchWithRequest already validates entry_point == INVALID_ENTRY_POINT at line 819 and returns before calling search_route_graphs. This defensive check is harmless but redundant.
Summary
The refactoring is well-structured and significantly improves readability by extracting clear sub-functions (create_query_context, search_route_graphs, configure_bottom_graph_search, pack_search_result, parse_and_validate_search_params, initialize_reasoning_context) from the monolithic SearchWithRequest. The VisitedListGuard RAII wrapper is a nice addition. The test coverage for brute-force precedence and range search routing is good.
The main suggestion is simplifying the double select_search_plan call pattern. The brute force threshold parameter addition should be confirmed as intentional.
| @@ -534,82 +547,60 @@ HGraph::SearchWithRequest(const SearchRequest& request) const { | |||
| search_param.distance_batch_size = request.distance_batch_size_; | |||
There was a problem hiding this comment.
[note] search_route_graphs accepts const HGraphSearchParameters& params but never uses it in the function body. The parameter can be removed to simplify the signature.
| search_param.consider_duplicate = true; | ||
| search_param.enable_rabitq_one_bit_search = | ||
| use_custom_distance ? false : params.rabitq_one_bit_search; | ||
| search_param.parallel_search_thread_count = params.parallel_search_thread_count; |
There was a problem hiding this comment.
[note] configure_bottom_graph_search unconditionally dereferences ctx->stats (ctx->stats->bottom_graph_search_count++), but create_query_context may create a context with a null stats pointer when params->stats is null. If configure_bottom_graph_search is only ever called after a stats-capable path, consider adding a // stats is always non-null here comment, or add a null guard.
| @@ -534,82 +547,60 @@ HGraph::SearchWithRequest(const SearchRequest& request) const { | |||
| search_param.distance_batch_size = request.distance_batch_size_; | |||
There was a problem hiding this comment.
[note] search_route_graphs accepts const HGraphSearchParameters& params but never uses it in the function body. The parameter can be removed to simplify the signature.
| search_param.consider_duplicate = true; | ||
| search_param.enable_rabitq_one_bit_search = | ||
| use_custom_distance ? false : params.rabitq_one_bit_search; | ||
| search_param.parallel_search_thread_count = params.parallel_search_thread_count; |
There was a problem hiding this comment.
[note] configure_bottom_graph_search unconditionally dereferences ctx->stats (ctx->stats->bottom_graph_search_count++), but create_query_context may create a context with a null stats pointer when params->stats is null. If configure_bottom_graph_search is only ever called after a stats-capable path, consider adding a // stats is always non-null here comment, or add a null guard.
| bool mci_available = false; | ||
| if (not brute_force_route) { | ||
| // MCI seeds use the original external-label filter; ft wraps it for inner-ID search. | ||
| const auto bitset_seed_source = has_bitset_source(request.filter_); |
There was a problem hiding this comment.
[suggestion] MCI seed-source detection (has_valid_id_source, has_bitset_source) operates on request.filter_, but the actual search filter and mci_result are constructed from ft (which may be a wrapped/augmented filter via create_search_filter). When params.use_extra_info_filter is true, ft can differ from request.filter_, causing the routing decision to be based on a different filter instance than the one used for MCI seed extraction and bottom-graph search.
Consider either:
- Running
has_valid_id_source/has_bitset_sourceonftinstead ofrequest.filter_, or - Explicitly unwrapping
ftto the original filter before seed-source detection, with a comment explaining why the unwrapped filter is correct for seed purposes.
| QueryContext* ctx, | ||
| InnerSearchParam& search_param); | ||
|
|
||
| [[nodiscard]] DatasetPtr |
There was a problem hiding this comment.
[note] configure_bottom_graph_search is declared static in the header (hgraph.h:838) and defined as a static member function, but it mutates ctx->stats->is_timeout (line 576 in the .cpp). While technically valid since ctx is passed as a parameter, the static qualifier is semantically misleading — it suggests the method does not depend on or mutate instance state, yet the stats object it modifies is typically owned by the calling SearchWithRequest instance flow.
Consider removing static and making it a regular const member function. This would make the dependency on ctx (which carries instance-derived state) more explicit and consistent with other helper methods like search_route_graphs and create_query_context which are non-static const members.
| @@ -20,6 +20,7 @@ | |||
| #include "attr/argparse.h" | |||
| CHECK_ARGUMENT(request.distance_batch_size_ > 0, | ||
| "distance_batch_size must be greater than 0"); | ||
| CHECK_ARGUMENT(not is_range, "HGraph custom distance only supports KNN search"); | ||
| } |
There was a problem hiding this comment.
[suggestion] configure_bottom_graph_search dereferences ctx->stats at line 602 without a null check on ctx. While the single call site in SearchWithRequest always passes a valid pointer (&ctx where ctx is a stack variable), this is a static method that could be called from other contexts in the future. Consider adding a null guard for defensive safety.
if (params.enable_time_record) {
search_param.time_cost = std::make_shared<Timer>();
search_param.time_cost->SetThreshold(params.timeout_ms);
if (ctx != nullptr and ctx->stats != nullptr) {
ctx->stats->is_timeout.store(false, std::memory_order_relaxed);
}
}| CHECK_ARGUMENT(request.distance_batch_size_ > 0, | ||
| "distance_batch_size must be greater than 0"); | ||
| CHECK_ARGUMENT(not is_range, "HGraph custom distance only supports KNN search"); | ||
| } |
There was a problem hiding this comment.
[suggestion] configure_bottom_graph_search dereferences ctx->stats without a null check on ctx. While the single call site in SearchWithRequest always passes a valid pointer (&ctx where ctx is a stack variable), this is a static method that could be called from other contexts in the future. Consider adding a null guard for defensive safety.
if (params.enable_time_record) {
search_param.time_cost = std::make_shared<Timer>();
search_param.time_cost->SetThreshold(params.timeout_ms);
if (ctx != nullptr and ctx->stats != nullptr) {
ctx->stats->is_timeout.store(false, std::memory_order_relaxed);
}
}| if (params.enable_time_record) { | ||
| search_param.time_cost = std::make_shared<Timer>(); | ||
| search_param.time_cost->SetThreshold(params.timeout_ms); | ||
| stats.is_timeout.store(false, std::memory_order_relaxed); |
There was a problem hiding this comment.
[suggestion] configure_bottom_graph_search dereferences ctx->stats without a null check on ctx. While the single call site in SearchWithRequest always passes a valid pointer (&ctx where ctx is a stack variable), this is a static method that could be called from other contexts in the future. Consider adding a null guard for defensive safety.
if (params.enable_time_record) {
search_param.time_cost = std::make_shared<Timer>();
search_param.time_cost->SetThreshold(params.timeout_ms);
if (ctx != nullptr and ctx->stats != nullptr) {
ctx->stats->is_timeout.store(false, std::memory_order_relaxed);
}
}| search_param.time_cost = std::make_shared<Timer>(); | ||
| search_param.time_cost->SetThreshold(params.timeout_ms); | ||
| stats.is_timeout.store(false, std::memory_order_relaxed); | ||
| ctx->stats->is_timeout.store(false, std::memory_order_relaxed); |
There was a problem hiding this comment.
[suggestion] configure_bottom_graph_search dereferences ctx->stats without a null check on ctx. While the single call site in SearchWithRequest always passes a valid pointer (&ctx where ctx is a stack variable), this is a static method that could be called from other contexts in the future. Consider adding a null guard for defensive safety.
if (params.enable_time_record) {
search_param.time_cost = std::make_shared<Timer>();
search_param.time_cost->SetThreshold(params.timeout_ms);
if (ctx != nullptr and ctx->stats != nullptr) {
ctx->stats->is_timeout.store(false, std::memory_order_relaxed);
}
}| dataset_results->Reasoning(reasoning_ctx->GenerateReport()); | ||
| } | ||
|
|
||
| return std::move(dataset_results); |
There was a problem hiding this comment.
[suggestion] Returning std::move(dataset_results) prevents copy elision (NRVO). Since dataset_results is a local variable, the compiler can elide the copy/move entirely if returned without std::move. The explicit std::move forces a move construction, which is strictly worse than the guaranteed copy elision that NRVO provides.
// Current:
return std::move(dataset_results);
// Suggested:
return dataset_results;| plan_input.use_mci = params.use_mci; | ||
| plan_input.mci_enabled = this->mci_parameters_.enabled; | ||
| plan_input.has_attribute_executor = not search_param.executors.empty(); | ||
| plan_input.mci_valid_ratio_threshold = params.mci_hgraph_valid_ratio_threshold; |
There was a problem hiding this comment.
[suggestion] plan_input.mci_valid_ratio_threshold is set before the brute_force_route check (line 876), but mci_has_clique_index, has_valid_id_source, and has_bitset_source are only populated inside the if (not brute_force_route) block (lines 880-895). This means the first select_search_plan(plan_input, false) call on line 878 operates on a partially-populated plan_input where mci_has_clique_index, has_valid_id_source, and has_bitset_source still hold their default false values. While this is intentional (the first call only checks for brute-force routing which doesn't depend on those fields), it creates a subtle coupling between the population order and the select_search_plan logic. Consider either:
- Populating all fields before the first
select_search_plancall, or - Adding a comment noting that the first call intentionally uses a partially-populated struct.
| bool brute_force_used = false; | ||
| MCIHybridSearchResult mci_result(params, ft); | ||
| DistHeapPtr search_result; | ||
| if (not use_custom_distance) { |
There was a problem hiding this comment.
[note] The if (not use_custom_distance) block at line 868 now spans ~70 lines (868-930) containing the entire search plan selection and execution logic. The else branch (lines 931-938) is just a single search_one_graph call. Consider extracting the if block body into a dedicated private method (e.g., execute_search_plan) to keep SearchWithRequest at a consistent abstraction level, matching the extraction pattern already applied to search_route_graphs, configure_bottom_graph_search, etc.
|
[suggestion] In |
LHT129
left a comment
There was a problem hiding this comment.
Thanks for this clean refactoring of the HGraph search routing logic. The extraction of SearchWithRequest into well-named helper methods (create_query_context, search_route_graphs, configure_bottom_graph_search, pack_search_result, parse_and_validate_search_params, initialize_reasoning_context) significantly improves readability and maintainability.
A few observations from the review:
-
The
VisitedListGuardRAII class is a nice improvement over the previous local struct — extracting it to file scope makes it reusable and testable. -
The
select_search_plan/is_mci_availablesplit with the two-phase check (brute force first, then MCI eligibility) is a sensible optimization that avoids computing MCI clique index state when brute force would win anyway. -
The early
vt_guard.Release()before reorder (step 7) is a good performance improvement — returning the visited list to the pool early allows other concurrent searches to reuse it during the reorder phase. -
The NaN-aware MCI threshold comparison (
not(valid_ratio >= threshold)) with the explicit comment is the correct approach for preserving MCI attempt behavior with NaN ratios.
One suggestion (posted as a separate PR comment): the initialize_reasoning_context method signature takes QueryContext* ctx but ctx->reasoning_ctx is null during the call. Consider narrowing the parameter to reflect the actual contract.
The new tests in hgraph_mci_test.cpp provide good coverage for brute-force precedence, NaN ratio handling, and range search bottom-graph fallback. The refactoring preserves all existing behavior while making the search routing logic much easier to follow.
|
|
||
| namespace vsag { | ||
|
|
||
| enum class search_plan { |
There was a problem hiding this comment.
[note] The search_plan enum and search_plan_input struct use snake_case naming, which differs from the project convention of PascalCase for types (e.g., InnerSearchMode, QueryContext, HGraphSearchParameters). The enum values also mix a K_ prefix with UPPER_SNAKE_CASE (K_BOTTOM_GRAPH), while existing enums like InnerSearchMode use plain UPPER_SNAKE_CASE (KNN_SEARCH).
Consider renaming for consistency:
enum class SearchPlan { kBottomGraph, kBruteForce, kMCI };
struct SearchPlanInput { ... };or following the existing InnerSearchMode pattern:
enum class SearchPlan { BOTTOM_GRAPH, BRUTE_FORCE, MCI };
LHT129
left a comment
There was a problem hiding this comment.
Overall this is a well-executed refactoring that cleanly decomposes SearchWithRequest into focused, testable sub-methods. The routing logic extraction into select_search_plan / is_mci_available is sound, and the VisitedListGuard RAII wrapper is a nice improvement over the previous ad-hoc struct.
Summary of changes:
- Extracted
parse_and_validate_search_params,create_query_context,initialize_reasoning_context,search_route_graphs,configure_bottom_graph_search, andpack_search_resultfromSearchWithRequest. - Introduced
search_plan/search_plan_input/select_search_planto centralize brute-force/MCI/bottom-graph routing decisions. - Added
VisitedListGuardRAII class for visited list lifecycle management. - Added test coverage for brute-force precedence, NaN ratio handling, and range search selecting bottom graph.
- Fixed
size_tunderflow risk inroute_graphs_.size() - 1by casting before subtraction.
Behavior preservation: The refactoring correctly preserves all existing behaviors including brute-force priority, MCI eligibility checks, route statistics annotation, bitset/valid-id seed source detection, and the NaN-ratio MCI fallback path. The two-phase select_search_plan call (first checking brute-force alone, then full routing) is a deliberate optimization to avoid computing MCI availability when brute-force already wins.
One minor style note was left inline regarding type naming conventions.
|
|
||
| namespace vsag { | ||
|
|
||
| enum class search_plan { |
There was a problem hiding this comment.
[suggestion] The search_plan enum, search_plan_input struct, VisitedListGuard class, and the helper functions (is_mci_available, select_search_plan, has_valid_id_source, has_bitset_source) are defined at namespace vsag scope in this .cpp file, giving them external linkage. If another translation unit also defines symbols with these names in namespace vsag, it would cause an ODR violation.
The previous iteration of this PR used an anonymous namespace for these symbols. Consider either wrapping them back in an anonymous namespace or marking them static to give them internal linkage, since they are only used within this translation unit.
| plan_input.mci_valid_ratio_threshold = params.mci_hgraph_valid_ratio_threshold; | ||
| const bool brute_force_route = | ||
| select_search_plan(plan_input, false) == search_plan::K_BRUTE_FORCE; | ||
| bool mci_available = false; |
There was a problem hiding this comment.
[note] select_search_plan(plan_input, false) is called before plan_input.mci_has_clique_index, plan_input.has_valid_id_source, and plan_input.has_bitset_source are initialized (they are set later in the if (not brute_force_route) block).
This is currently safe because mci_available=false prevents select_search_plan from reading those fields, but it creates a fragile dependency on the internal implementation of select_search_plan. If someone later modifies select_search_plan to read those fields unconditionally (e.g., for logging or assertions), it would read uninitialized values.
Consider either initializing all plan_input fields before the first select_search_plan call, or adding a comment noting that the first call intentionally uses incomplete input and only checks brute-force eligibility.
LHT129
left a comment
There was a problem hiding this comment.
I have completed a thorough review of this PR (commit 0de14d4f). The refactoring cleanly extracts SearchWithRequest into focused, well-named helper methods, and the centralized select_search_plan / search_plan_input routing logic is a clear improvement over the previous inline conditionals.
Summary of findings:
The PR has already received extensive review feedback (30+ comments) covering the key areas: C++20 compatibility, MCI availability computation, NaN handling, filter mismatch between request.filter_ and ft, naming conventions, and test coverage. Most of the actionable issues have been addressed in this revision.
Remaining notes (already flagged in existing comments):
- The
search_plan/search_plan_inputnaming uses snake_case while the VSAG convention is PascalCase — noted as file-local types, low priority. mci_result.routedefaults to"disabled"and is only set to"hgraph"whenmci_availableis true — the author has confirmed this is intentional to preserve existing callback-only filter regression behavior.- MCI seed-source detection uses
request.filter_whilemci_resultis constructed withft(the wrapped filter) — the comment on L881 explains this is intentional since MCI seeds use the original external-label filter.
Overall assessment: The refactoring is well-executed, preserves existing behavior, and adds meaningful test coverage for the routing decision paths. No blocking issues found.
LHT129
left a comment
There was a problem hiding this comment.
[suggestion] In search_route_graphs (hgraph_search.cpp:393), the params and use_custom_distance parameters are never read in the function body. These appear to be leftovers from the extraction — the original inline routing code did not use params either. Removing these unused parameters would simplify the signature.
Additionally, configure_bottom_graph_search is declared static in the header, which is correct since it only operates on its parameters. However, the function mutates ctx->stats->is_timeout — this is fine but worth noting that the "static" label is slightly misleading since the function has side effects through the ctx pointer.
Summary
Closes #2605