Skip to content

feat: add threshold filtering to knn search - #2531

Open
wxyucs wants to merge 1 commit into
antgroup:mainfrom
wxyucs:codex/issue-2081-knn-threshold
Open

feat: add threshold filtering to knn search#2531
wxyucs wants to merge 1 commit into
antgroup:mainfrom
wxyucs:codex/issue-2081-knn-threshold

Conversation

@wxyucs

@wxyucs wxyucs commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator

Summary

Add inclusive top-level distance threshold filtering for KNN in the maintained indexes only:

  • BruteForce
  • HGraph
  • IVF
  • Pyramid
  • SINDI
  • SIMQ

HNSW and DiskANN are deprecated and explicitly unsupported non-goals for this PR. Their source, test, and documentation paths are absent from the full PR diff.

Threshold filtering excludes non-finite values before bounded result selection and allows eligible finite candidates to backfill. Non-reorder IVF filters during bucket selection, while reordered IVF filters exact distances after reranking. HGraph iterator search consumes rejected pages and retained discard state internally. Ordinary KNN preserves infinite results and may use infinite intermediate graph nodes as traversal seeds. Threshold scratch storage uses the active request allocator.

Validation

  • One Conventional Commit with DCO and Assisted-by trailers.
  • clang-format-15, direct clang-tidy-15, bounded debug build, and git diff --check passed.
  • Bounded threshold regressions passed: 104 assertions in 8 cases.
  • Bounded full unit suite passed: 85,543,385 assertions in 636 cases.
  • Bounded SIMQ maintained search validation passed: 1,619 assertions.
  • The full PR path diff has zero HNSW/DiskANN source, test, or documentation paths.
  • All 38 review threads were answered inline and resolved.

Fixes: #2081

Copilot AI review requested due to automatic review settings July 24, 2026 07:51
@wxyucs wxyucs added kind/feature Brand-new functionality or capabilities 引入全新的功能、新特性或新能力 version/1.1 labels Jul 24, 2026
@vsag-bot

vsag-bot commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator

/label status/waiting-for-review
/waiting-on reviewer
/request-review @jiaweizone
/request-review @inabao

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@vsag-bot
vsag-bot self-requested a review July 24, 2026 07:51
@vsag-bot

vsag-bot commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator

Automated pull request review completed.

Review effort: high (1177 changed lines across 27 files).

Submitted 4 inline comments.
Review: #2531 (review)

@mergify

mergify Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Merge Protections

🟢 All 3 merge protections satisfied — ready to merge.

Show 3 satisfied protections

🟢 Require kind label

  • label~=^kind/

🟢 Require version label

  • label~=^version/

🟢 Require linked issue for feature/bug PRs

  • body~=(?im)(?:^|[\s\-\*])(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s*:?\s+(?:#\d+|[\w.\-]+/[\w.\-]+#\d+|https?://github\.com/[\w.\-]+/[\w.\-]+/issues/\d+)

LHT129

This comment was marked as resolved.

@vsag-bot vsag-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated inline review completed.

Review effort: medium (246 changed lines across 13 files).
Submitted 3 inline comments.

Reviewed commit 9b6cf96.

Comment thread src/algorithm/ivf/ivf.cpp Outdated
Comment thread include/vsag/search_request.h Outdated
Comment thread src/utils/search_threshold.h Outdated
Copilot AI review requested due to automatic review settings July 24, 2026 11:12
@wxyucs
wxyucs force-pushed the codex/issue-2081-knn-threshold branch from 9b6cf96 to e718e38 Compare July 24, 2026 11:12

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@vsag-bot
vsag-bot self-requested a review July 24, 2026 11:12

@vsag-bot vsag-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated inline review completed.

Review effort: high (397 changed lines across 15 files).
Submitted 7 inline comments.

Reviewed commit e718e38.

Comment thread src/utils/search_threshold.h
Comment thread src/utils/search_threshold.h Outdated
Comment thread src/algorithm/inner_index_interface.cpp Outdated
Comment thread src/algorithm/hgraph/hgraph_search.cpp Outdated
Comment thread src/algorithm/ivf/ivf.cpp
Comment thread src/algorithm/ivf/ivf.cpp
Comment thread docs/docs/en/src/guide/knn_search.md
@wxyucs
wxyucs force-pushed the codex/issue-2081-knn-threshold branch from e718e38 to d05a524 Compare July 27, 2026 04:04
Copilot AI review requested due to automatic review settings July 27, 2026 04:04
@vsag-bot
vsag-bot self-requested a review July 27, 2026 04:05

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Comment thread src/utils/search_threshold.h

@LHT129 LHT129 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This update addresses all six issues raised in the previous review round:

  1. IVF reorder threshold — Threshold is now applied after exact-distance reorder, with FilterDatasetByThreshold capping at topk_. The candidate pool is retained at full size when threshold is present.
  2. Non-finite threshold validationValidateSearchThreshold rejects NaN/Inf in SearchWithRequest paths, and index_impl.h validates inside SAFE_CALL for consistent error handling.
  3. Allocator preservationFilterDatasetByThreshold accepts an Allocator*, allocates through it, and sets Owner(true, allocator). HNSW and SINDI pass their caller allocator; a regression test verifies balanced deallocation.
  4. route_buckets_only threshold — IVF now explicitly rejects threshold with disable_bucket_scan via CHECK_ARGUMENT.
  5. IVF reasoning reportAttachReasoningReport is called after FilterDatasetByThreshold in the reorder path.
  6. Pyramid/SIMQ threshold — Both now parse and apply threshold via FilterDatasetByThreshold with their allocators and k-capping.

Additional observations:

  • The #include <unordered_set> addition in test_brute_force.cpp fixes a missing-include bug in the existing code (it was used but not included).
  • ParseSearchThreshold now validates JSON type via IsNumber() before calling GetFloat(), preventing silent coercion of string values.
  • The SetOFFFirstUsed() call in HGraph when threshold filtering empties the result is a correct fix to maintain iterator filter state consistency.
  • FilterDatasetByThreshold correctly preserves statistics and reasoning from the input dataset.

One minor suggestion was posted inline about the two-pass iteration in FilterDatasetByThreshold.

@wxyucs
wxyucs force-pushed the codex/issue-2081-knn-threshold branch from 0990cfd to 44a70f2 Compare July 28, 2026 04:14
@vsag-bot
vsag-bot self-requested a review July 28, 2026 04:14

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@vsag-bot vsag-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated inline review completed.

Review effort: high (651 changed lines across 23 files).
Submitted 4 inline comments.

Reviewed commit 44a70f2.

Comment thread src/impl/searcher/basic_searcher.cpp Outdated
Comment thread src/algorithm/bruteforce/bruteforce.cpp Outdated
Comment thread src/algorithm/ivf/ivf.cpp
Comment thread docs/docs/en/src/guide/knn_search.md Outdated
Comment thread src/utils/search_threshold.h
Comment thread src/utils/search_threshold.h

@LHT129 LHT129 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review Summary

This PR adds inclusive top-level distance threshold filtering for KNN search across all maintained indexes (BruteForce, HGraph, IVF, Pyramid, SINDI, SIMQ). The implementation is thorough and well-tested with 628 additions across 23 files.

What was reviewed

  • New src/utils/search_threshold.h with ParseSearchThreshold, ValidateSearchThreshold, FilterDatasetByThreshold, and AllocateThresholdArray
  • Threshold integration in all 6 maintained indexes
  • Non-finite distance filtering in MemmoveHeap, BasicSearcher, ParallelSearcher, and HGraph
  • JSON parsing support (JsonWrapper::IsNumber)
  • SearchRequest::threshold_ field
  • IndexImpl validation layer across all KnnSearch overloads and SearchWithRequest
  • Documentation updates (EN + ZH)
  • Comprehensive tests for BruteForce, HGraph, IVF, Pyramid, and SIMQ

Key observations

  1. The non-finite distance defense is layered at multiple levels (heap Push, searcher entry points, HGraph result filtering) — good defense-in-depth.
  2. The HGraph iterator path correctly consumes threshold-rejected candidates via SetPoint so pagination state is maintained.
  3. IVF reorder path correctly retains the full candidate pool for exact-distance threshold filtering.
  4. FilterDatasetByThreshold exception safety was improved from earlier rounds — allocations are now attached to the owning Dataset before subsequent allocations.

Minor suggestions

  • FilterDatasetByThreshold iterates 3 times over the input (count, copy ids+dists, copy extra-infos). The second and third passes could be merged.
  • When result_count == 0 with extra info present, the filtered Dataset has ExtraInfoSize > 0 but ExtraInfos == nullptr. Downstream consumers should be null-safe on this pointer.

Overall

The implementation is solid, well-tested, and addresses all critical issues from previous review rounds. No blocking concerns.

Copilot AI review requested due to automatic review settings July 28, 2026 09:22
@wxyucs
wxyucs force-pushed the codex/issue-2081-knn-threshold branch from 44a70f2 to c429a73 Compare July 28, 2026 09:22

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@vsag-bot
vsag-bot self-requested a review July 28, 2026 09:22

@vsag-bot vsag-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated inline review completed.

Review effort: high (728 changed lines across 22 files).
Submitted 2 inline comments.

Reviewed commit c429a73.

Comment thread src/impl/searcher/basic_searcher.cpp Outdated
Comment thread src/algorithm/inner_index_interface.cpp Outdated
Copilot AI review requested due to automatic review settings July 29, 2026 07:06
@wxyucs
wxyucs force-pushed the codex/issue-2081-knn-threshold branch from c429a73 to 632fa56 Compare July 29, 2026 07:06
@vsag-bot
vsag-bot self-requested a review July 29, 2026 07:06

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@vsag-bot vsag-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated inline review completed.

Review effort: high (787 changed lines across 22 files).
Submitted 4 inline comments.

Reviewed commit 632fa56.

Comment thread src/utils/search_threshold.h Outdated
Comment thread src/algorithm/ivf/ivf.cpp
Comment thread src/algorithm/hgraph/hgraph_search.cpp Outdated
Comment thread src/impl/searcher/basic_searcher.cpp

@LHT129 LHT129 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the latest commit (632fa56). All previously flagged issues have been properly addressed:

  • JSON type validation before GetFloat (IsNumber check)
  • Route-only IVF explicitly rejects threshold with INVALID_ARGUMENT
  • IVF reasoning report attached after threshold filtering
  • Pyramid and SIMQ threshold filtering implemented with regressions
  • IndexImpl validates threshold JSON before empty-index shortcuts
  • Non-finite distance handling in searchers (basic and parallel)

The implementation is thorough with good test coverage across all six maintained indexes. No new issues found.

Signed-off-by: Xiangyu Wang <wxy407827@antgroup.com>
Assisted-by: OpenAI Codex:GPT-5
Copilot AI review requested due to automatic review settings July 29, 2026 11:54
@wxyucs
wxyucs force-pushed the codex/issue-2081-knn-threshold branch from 632fa56 to b35bbc3 Compare July 29, 2026 11:54

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@vsag-bot
vsag-bot self-requested a review July 29, 2026 11:54
The result contains up to `k` neighbors sorted by ascending distance to the query.

## Threshold Filtering

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[note] The documentation references "This PR's threshold support" which ties the docs to a specific pull request. User-facing documentation should describe the feature itself without referencing the PR that added it. Consider rephrasing to something like "Threshold filtering is supported for the following maintained indexes: BruteForce, HGraph, IVF, Pyramid, SINDI, and SIMQ."

The same issue appears in the Chinese documentation.


搜索请求至多返回 k 个结果,这些结果按照最近邻和查询向量的距离升序排序。输出的结果类似于:

## 按阈值过滤

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[note] Same issue as the English docs: "本 PR 的阈值支持范围仅包括" references the PR itself. Consider rephrasing to describe the feature directly, e.g. "阈值过滤支持以下仍在维护的索引:BruteForce、HGraph、IVF、Pyramid、SINDI 和 SIMQ。"

@vsag-bot vsag-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated inline review completed.

Review effort: high (1177 changed lines across 27 files).
Submitted 4 inline comments.

Reviewed commit b35bbc3.

for (const auto& record : finite_records) {
search_result->Push(record);
}
filter_search_result_by_threshold(search_result, request.threshold_, ctx.alloc);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Filter non-finite HGraph candidates before ef pruning

This filtering happens only after the default single-threaded graph search has bounded candidates to ef/topk. With IP vectors {FLT_MAX, 0}, query FLT_MAX, k = ef_search = 1, and threshold 1, the computed -inf candidate occupies the sole result slot and the qualifying distance-1 candidate is discarded, so this returns empty. Exclude non-finite candidates during bounded result selection while retaining them only as traversal seeds.

for (uint32_t i = 0; i < count_no_visited; i++) {
dist = line_dists[i];
const auto cur_id = to_be_visited_id[i];
if (not std::isfinite(dist)) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Keep NaN intermediate nodes as traversal seeds

This branch enqueues infinities for traversal but drops NaN nodes entirely. In a topology finite entry -> NaN-valued bridge -> finite target, the public HGraph iterator can no longer reach the target, even without a threshold; the previous path enqueued that bridge. Normalize NaN to a safe traversal priority while excluding it from the result heap.

if (std::isinf(dist)) {
if constexpr (mode == KNN_SEARCH) {
if (not is_id_allowed || is_id_allowed->CheckValid(cur_id)) {
top_candidates->Push(dist, cur_id);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Maintain the ef bound when retaining infinite results

This push bypasses the top_candidates->Size() > ef eviction and lower-bound update used by the finite branch. Once more than ef connected nodes have overflowed L2 distances, the heap remains oversized, the equality-based termination condition is disabled, and iterator search can traverse the full graph while retaining O(N) candidates. Preserve the infinity as a traversal seed/result without violating the ef bound; the parallel implementation has the same pattern.

for (const auto& [distance, _] : reranked) {
if (not threshold.has_value() or
(std::isfinite(distance) and distance <= threshold.value())) {
++result_count;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Handle zero k before counting SIMQ results

For k == 0, result_count is incremented to 1 before the equality check, so it can never equal zero and the loop counts every reranked candidate. SIMQ therefore returns all candidates, whereas the previous min(k, size) path returned none. Validate that k is positive or retain an explicit zero-result fast path.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

kind/feature Brand-new functionality or capabilities 引入全新的功能、新特性或新能力 module/api module/docs module/index module/testing size/XXL version/1.1

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[feat](other): support threshold filtering in knn search

4 participants