-
Notifications
You must be signed in to change notification settings - Fork 105
support min_distance for large limit query #2408
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -534,7 +534,8 @@ HierarchicalNSW::searchBaseLayerST(InnerIdType ep_id, | |
| const float skip_ratio, | ||
| vsag::FilterSearchSkipStrategyType skip_strategy_type, | ||
| vsag::Allocator* allocator, | ||
| vsag::IteratorFilterContext* iter_ctx) const { | ||
| vsag::IteratorFilterContext* iter_ctx, | ||
| float min_distance) const { | ||
| VisitedListPtr vl = visited_list_pool_->getFreeVisitedList(); | ||
| vl_type* visited_array = vl->mass; | ||
| vl_type visited_array_tag = vl->curV; | ||
|
|
@@ -562,14 +563,16 @@ HierarchicalNSW::searchBaseLayerST(InnerIdType ep_id, | |
| iter_ctx->PopDiscard(); | ||
| } | ||
| } else { | ||
| lower_bound = std::numeric_limits<float>::max(); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [suggestion] When Consider adding a if (iter_ctx->CheckPoint(cur_inner_id) && cur_dist > min_distance + vsag::THRESHOLD_ERROR) {
top_candidates.emplace(cur_dist, cur_inner_id);
...
} |
||
| if ((!has_deletions || !isMarkedDeleted(ep_id)) && | ||
| ((!is_id_allowed) || is_id_allowed->CheckValid(getExternalLabel(ep_id)))) { | ||
| float dist = fstdistfunc_(data_point, getDataByInternalId(ep_id), dist_func_param_); | ||
| lower_bound = dist; | ||
| top_candidates.emplace(dist, ep_id); | ||
| candidate_set.emplace(-dist, ep_id); | ||
| if (dist > min_distance + vsag::THRESHOLD_ERROR) { | ||
| top_candidates.emplace(dist, ep_id); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [suggestion] In the In the original code, |
||
| lower_bound = dist; | ||
| } | ||
| } else { | ||
| lower_bound = std::numeric_limits<float>::max(); | ||
| candidate_set.emplace(-lower_bound, ep_id); | ||
| } | ||
| visited_array[ep_id] = visited_array_tag; | ||
|
|
@@ -639,6 +642,9 @@ HierarchicalNSW::searchBaseLayerST(InnerIdType ep_id, | |
| if (iter_ctx != nullptr && !iter_ctx->CheckPoint(candidate_id)) { | ||
| continue; | ||
| } | ||
| if (dist <= min_distance + vsag::THRESHOLD_ERROR) { | ||
| continue; | ||
| } | ||
| top_candidates.emplace(dist, candidate_id); | ||
| } | ||
|
|
||
|
|
@@ -667,7 +673,8 @@ HierarchicalNSW::searchBaseLayerST(InnerIdType ep_id, | |
| const void* data_point, | ||
| float radius, | ||
| int64_t ef, | ||
| const vsag::FilterPtr is_id_allowed) const { | ||
| const vsag::FilterPtr is_id_allowed, | ||
| float min_distance) const { | ||
| VisitedListPtr vl = visited_list_pool_->getFreeVisitedList(); | ||
| vl_type* visited_array = vl->mass; | ||
| vl_type visited_array_tag = vl->curV; | ||
|
|
@@ -680,7 +687,7 @@ HierarchicalNSW::searchBaseLayerST(InnerIdType ep_id, | |
| ((!is_id_allowed) || is_id_allowed->CheckValid(getExternalLabel(ep_id)))) { | ||
| float dist = fstdistfunc_(data_point, getDataByInternalId(ep_id), dist_func_param_); | ||
| lower_bound = dist; | ||
| if (dist <= radius + vsag::THRESHOLD_ERROR) | ||
| if (dist <= radius + vsag::THRESHOLD_ERROR && dist > min_distance + vsag::THRESHOLD_ERROR) | ||
| top_candidates.emplace(dist, ep_id); | ||
| candidate_set.emplace(-dist, ep_id); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [suggestion] In the This is just a note for consistency — the first overload (with |
||
| } else { | ||
|
|
@@ -740,8 +747,10 @@ HierarchicalNSW::searchBaseLayerST(InnerIdType ep_id, | |
|
|
||
| if ((!has_deletions || !isMarkedDeleted(candidate_id)) && | ||
| ((!is_id_allowed) || | ||
| is_id_allowed->CheckValid(getExternalLabel(candidate_id)))) | ||
| top_candidates.emplace(dist, candidate_id); | ||
| is_id_allowed->CheckValid(getExternalLabel(candidate_id)))) { | ||
| if (dist > min_distance + vsag::THRESHOLD_ERROR) | ||
| top_candidates.emplace(dist, candidate_id); | ||
| } | ||
|
|
||
| if (not top_candidates.empty()) | ||
| lower_bound = top_candidates.top().first; | ||
|
|
@@ -1689,7 +1698,8 @@ HierarchicalNSW::searchKnn(const void* query_data, | |
| vsag::FilterSearchSkipStrategyType skip_strategy_type, | ||
| vsag::Allocator* allocator, | ||
| vsag::IteratorFilterContext* iter_ctx, | ||
| bool is_last_filter) const { | ||
| bool is_last_filter, | ||
| float min_distance) const { | ||
| std::shared_lock resize_lock(resize_mutex_); | ||
| std::priority_queue<std::pair<float, LabelType>> result; | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [suggestion] The if (is_last_filter) {
while (!iter_ctx->Empty()) {
uint32_t cur_inner_id = iter_ctx->GetTopID();
float cur_dist = iter_ctx->GetTopDist();
result.emplace(cur_dist, getExternalLabel(cur_inner_id)); // no min_distance check
iter_ctx->PopDiscard();
}
return result;
}This means that in the last filter iteration of a multi-filter search, results with distance <= if (cur_dist > min_distance + vsag::THRESHOLD_ERROR) {
result.emplace(cur_dist, getExternalLabel(cur_inner_id));
} |
||
| if (cur_element_count_ == 0) | ||
|
|
@@ -1718,7 +1728,8 @@ HierarchicalNSW::searchKnn(const void* query_data, | |
| skip_ratio, | ||
| skip_strategy_type, | ||
| allocator, | ||
| iter_ctx); | ||
| iter_ctx, | ||
| min_distance); | ||
| } else { | ||
| int64_t currObj; | ||
| int max_level_copy; | ||
|
|
@@ -1767,7 +1778,8 @@ HierarchicalNSW::searchKnn(const void* query_data, | |
| skip_ratio, | ||
| skip_strategy_type, | ||
| allocator, | ||
| iter_ctx); | ||
| iter_ctx, | ||
| min_distance); | ||
| } else { | ||
| top_candidates = searchBaseLayerST<true, true>(currObj, | ||
| query_data, | ||
|
|
@@ -1776,7 +1788,8 @@ HierarchicalNSW::searchKnn(const void* query_data, | |
| skip_ratio, | ||
| skip_strategy_type, | ||
| allocator, | ||
| iter_ctx); | ||
| iter_ctx, | ||
| min_distance); | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -1901,14 +1914,16 @@ HierarchicalNSW::searchBaseLayerST<false, false>( | |
| const float skip_ratio, | ||
| vsag::FilterSearchSkipStrategyType skip_strategy_type, | ||
| vsag::Allocator* allocator, | ||
| vsag::IteratorFilterContext* iter_ctx) const; | ||
| vsag::IteratorFilterContext* iter_ctx, | ||
| float min_distance) const; | ||
|
|
||
| template MaxHeap | ||
| HierarchicalNSW::searchBaseLayerST<false, false>(InnerIdType ep_id, | ||
| const void* data_point, | ||
| float radius, | ||
| int64_t ef, | ||
| const vsag::FilterPtr is_id_allowed) const; | ||
| const vsag::FilterPtr is_id_allowed, | ||
| float min_distance) const; | ||
|
|
||
| void | ||
| HierarchicalNSW::setImmutable() { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -67,6 +67,9 @@ class InnerSearchParam { | |
| // use in search process with duplicate ids | ||
| bool consider_duplicate{false}; | ||
|
|
||
| // skip results with dist <= min_distance (for search iterator) | ||
| float min_distance{std::numeric_limits<float>::lowest()}; | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [note] The However, if a user sets
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [note] This PR adds a new
Since |
||
|
|
||
| // time record | ||
| std::shared_ptr<Timer> time_cost{nullptr}; | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -177,7 +177,9 @@ BasicSearcher::search_impl(const GraphInterfacePtr& graph, | |
| flatten->Query(&cur_dist, computer, &cur_inner_id, 1, ctx); | ||
| // Sign convention: top_candidates stores positive distances (nearest = smallest); | ||
| // candidate_set is a max-heap, so distances are negated (nearest = largest, popped first). | ||
| top_candidates->Push(cur_dist, cur_inner_id); | ||
| if (cur_dist > inner_search_param.min_distance + THRESHOLD_ERROR) { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [suggestion] In the No action needed, just confirming the pattern is intentional.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [suggestion] In the This is the same class of issue as the Consider setting |
||
| top_candidates->Push(cur_dist, cur_inner_id); | ||
| } | ||
| candidate_set->Push(-cur_dist, cur_inner_id); | ||
| if constexpr (mode == InnerSearchMode::RANGE_SEARCH) { | ||
| if (cur_dist > inner_search_param.radius and not top_candidates->Empty()) { | ||
|
|
@@ -205,7 +207,8 @@ BasicSearcher::search_impl(const GraphInterfacePtr& graph, | |
| } else { | ||
| flatten->Query(&dist, computer, &ep, 1, ctx); | ||
| } | ||
| if (not is_id_allowed || is_id_allowed->CheckValid(ep)) { | ||
| if ((not is_id_allowed || is_id_allowed->CheckValid(ep)) and | ||
| dist > inner_search_param.min_distance + THRESHOLD_ERROR) { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [suggestion] When the entry point is valid (passes In the original code, This is the same concern as in |
||
| top_candidates->Push(dist, ep); | ||
| lower_bound = top_candidates->Top().first; | ||
| } | ||
|
|
@@ -283,7 +286,7 @@ BasicSearcher::search_impl(const GraphInterfacePtr& graph, | |
| } | ||
| candidate_set->Push(-dist, cur_id); | ||
| flatten->Prefetch(candidate_set->Top().second); | ||
| if (id_allowed) { | ||
| if (id_allowed && dist > inner_search_param.min_distance + THRESHOLD_ERROR) { | ||
| top_candidates->Push(dist, cur_id); | ||
| } | ||
|
|
||
|
|
@@ -381,7 +384,7 @@ BasicSearcher::search_impl(const GraphInterfacePtr& graph, | |
| flatten->Query(&dist, computer, &ep, 1, ctx); | ||
| } | ||
| ++dist_cmp; | ||
| if (check_func(ep)) { | ||
| if (check_func(ep) && dist > inner_search_param.min_distance + THRESHOLD_ERROR) { | ||
| top_candidates->Push(dist, ep); | ||
| lower_bound = top_candidates->Top().first; | ||
| } | ||
|
|
@@ -478,15 +481,17 @@ BasicSearcher::search_impl(const GraphInterfacePtr& graph, | |
| (mode == RANGE_SEARCH && dist <= inner_search_param.radius)) { | ||
| candidate_set->Push(-dist, cur_id); | ||
| // flatten->Prefetch(candidate_set->Top().second); | ||
| if (check_func(cur_id)) { | ||
| if (check_func(cur_id) && | ||
| dist > inner_search_param.min_distance + THRESHOLD_ERROR) { | ||
| top_candidates->Push(dist, cur_id); | ||
| } else if (reasoning != nullptr) { | ||
| reasoning->RecordFilterReject(cur_id); | ||
| } | ||
| if (inner_search_param.consider_duplicate) { | ||
| const auto duplicate_ids = graph->GetDuplicateIds(cur_id); | ||
| for (const auto& item : duplicate_ids) { | ||
| if (check_func(item)) { | ||
| if (check_func(item) && | ||
| dist > inner_search_param.min_distance + THRESHOLD_ERROR) { | ||
| top_candidates->Push(dist, item); | ||
| } | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -323,13 +323,15 @@ ParallelSearcher::search_impl(const GraphInterfacePtr& graph, | |
| if (top_candidates->Size() < ef || lower_bound > dist || | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [suggestion] The entry point in Current code (line 179-182): if (check_func(ep)) {
top_candidates->Push(dist, ep);
lower_bound = top_candidates->Top().first;
}Suggested fix: if (check_func(ep) && dist > inner_search_param.min_distance + THRESHOLD_ERROR) {
top_candidates->Push(dist, ep);
lower_bound = top_candidates->Top().first;
}Without this check, the parallel searcher path will include results with distance <= |
||
| (mode == RANGE_SEARCH && dist <= inner_search_param.radius)) { | ||
| candidate_set->Push(-dist, cur_id); | ||
| if (check_func(cur_id)) { | ||
| if (check_func(cur_id) && | ||
| dist > inner_search_param.min_distance + THRESHOLD_ERROR) { | ||
| top_candidates->Push(dist, cur_id); | ||
| } | ||
| if (inner_search_param.consider_duplicate) { | ||
| const auto duplicate_ids = graph->GetDuplicateIds(cur_id); | ||
| for (const auto& item : duplicate_ids) { | ||
| if (check_func(item)) { | ||
| if (check_func(item) && | ||
| dist > inner_search_param.min_distance + THRESHOLD_ERROR) { | ||
| top_candidates->Push(dist, item); | ||
| } | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[note] The
min_distanceparameter is plumbed through thesearchKnnvirtual interface inalgorithm_interface.hwith a default value ofstd::numeric_limits<float>::lowest(). This is backward-compatible for existing callers. However, there is no correspondingmin_distanceparameter added tosearchRangein the same interface. If range search also needsmin_distancesupport in the future, it would need a separate change.Also, the
bruteForcemethod does not receivemin_distance. If brute-force fallback is used (e.g., viabrute_force_thresholdin HGraph), results belowmin_distancewill not be filtered. Consider whether this is intentional or ifbruteForceshould also respectmin_distance.