Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions docs/docs/en/src/guide/knn_search.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,31 @@ for (int64_t i = 0; i < result->GetDim(); ++i) {
```

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.

This PR's threshold support is limited to the maintained indexes: BruteForce, HGraph, IVF,
Pyramid, SINDI, and SIMQ. HNSW and DiskANN are deprecated and are explicit unsupported
non-goals; this option does not add or change behavior for either legacy index.

KNN search accepts an optional top-level `threshold` in the JSON search parameters:
Comment thread
wxyucs marked this conversation as resolved.

```json
{
"threshold": 4.0,
"hgraph": { "ef_search": 64 }
}
```

When present, the result contains at most `k` neighbors and every returned distance is less than
or equal to `threshold`. The boundary is inclusive, results remain sorted by ascending distance,
and fewer than `k` results—or no results—may be returned. When omitted, KNN behavior is unchanged.
The value uses the index metric's returned distance: squared L2 for `l2`, `1 - inner_product` for
`ip`, and `1 - cosine_similarity` for `cosine`. Therefore negative thresholds can be meaningful
for `ip`; the threshold must be a finite JSON number.

The same option is available through `SearchRequest::threshold_` on BruteForce, HGraph, and IVF;
Pyramid, SINDI, and SIMQ support the option through their KNN JSON parameters. These are the six
maintained indexes supported by this feature. It only limits KNN results; use `RangeSearch` and
its `radius` argument for range-search requests. Existing index-specific search parameters can be
supplied alongside `threshold`.
25 changes: 24 additions & 1 deletion docs/docs/zh/src/guide/knn_search.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,30 @@ BruteForce 索引支持用 Build 和 Add 方法写入数据,这里我们用 Ad

搜索请求至多返回 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。"


本 PR 的阈值支持范围仅包括仍在维护的索引:BruteForce、HGraph、IVF、Pyramid、SINDI 和
SIMQ。HNSW 和 DiskANN 已弃用,是明确不在范围内的目标;本选项不会为这两个旧索引新增或修改行为。

KNN 搜索参数支持在 JSON 顶层设置可选的 `threshold`:

```json
{
"threshold": 4.0,
"hgraph": { "ef_search": 64 }
}
```

设置后,结果至多返回 `k` 个邻居,并且每个返回距离都小于或等于 `threshold`。
边界值会被包含,结果仍按距离升序排列,因此可能返回少于 `k` 个结果,甚至没有结果。
不设置该字段时,KNN 行为保持不变。阈值使用索引返回的距离语义:`l2` 为 L2 平方距离,
`ip` 为 `1 - inner_product`,`cosine` 为 `1 - cosine_similarity`。因此 `ip` 可以使用负阈值;
阈值必须是有限的 JSON 数字。

通过统一请求接口时,BruteForce、HGraph 和 IVF 支持使用 `SearchRequest::threshold_`;
Pyramid、SINDI 和 SIMQ 通过 KNN JSON 参数支持该选项。这六种索引是本功能支持的仍在维护的索引。
该选项只限制 KNN 结果;范围搜索请使用 `RangeSearch` 及其 `radius` 参数。`threshold` 可以和现有索引专用搜索参数同时传入。

```bash
results:
6519: 13.855
Expand All @@ -105,4 +129,3 @@ results:
8703: 16.1161
5583: 16.1256
```

9 changes: 9 additions & 0 deletions include/vsag/search_request.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
#pragma once

#include <cstdint>
#include <optional>
#include <string>
#include <vector>

Expand Down Expand Up @@ -183,6 +184,14 @@ class SearchRequest {
* Default is empty (no reasoning enabled).
*/
std::vector<int64_t> expected_labels_{};

/**
* @brief Optional inclusive distance threshold for KNN search mode
* @details When set, at most topk_ results are returned and every returned
* distance is less than or equal to this threshold. An unset value
* preserves the default KNN behavior.
*/
std::optional<float> threshold_{std::nullopt};
};

} // namespace vsag
15 changes: 14 additions & 1 deletion src/algorithm/bruteforce/bruteforce.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
#include "bruteforce.h"

#include <atomic>
#include <cmath>
#include <cstring>
#include <mutex>
#include <new>
Expand All @@ -36,6 +37,7 @@
#include "storage/serialization_tags.h"
#include "storage/tlv_section.h"
#include "typing.h"
#include "utils/search_threshold.h"
#include "utils/slow_task_timer.h"
#include "utils/util_functions.h"
namespace vsag {
Expand Down Expand Up @@ -360,6 +362,7 @@ BruteForce::KnnSearch(const DatasetPtr& query,
req.query_ = query;
req.topk_ = k;
req.params_str_ = parameters;
req.threshold_ = ParseSearchThreshold(parameters);
Comment thread
wxyucs marked this conversation as resolved.
if (filter != nullptr) {
req.filter_ = filter;
}
Expand All @@ -368,6 +371,7 @@ BruteForce::KnnSearch(const DatasetPtr& query,

DatasetPtr
BruteForce::SearchWithRequest(const SearchRequest& request) const {
ValidateSearchThreshold(request.threshold_);
std::shared_lock read_lock(this->global_mutex_);

auto computer = this->make_search_computer(request.query_);
Expand Down Expand Up @@ -470,7 +474,9 @@ BruteForce::SearchWithRequest(const SearchRequest& request) const {
if (is_range and dist > radius) {
continue;
}
cur_heap->Push(dist, i);
if (not request.threshold_.has_value() || std::isfinite(dist)) {
cur_heap->Push(dist, i);
}
} else {
if (reasoning != nullptr) {
reasoning->RecordFilterReject(i);
Expand Down Expand Up @@ -506,6 +512,13 @@ BruteForce::SearchWithRequest(const SearchRequest& request) const {
}
}

if (not is_range) {
filter_search_result_by_threshold(
heap,
request.threshold_,
select_query_allocator(request.search_allocator_, this->allocator_));
}

// Collect result inner IDs before pack_knn_result_with_extra_info consumes the heap,
// so we can call MarkResult for reasoning analysis.
Vector<InnerIdType> result_inner_ids(this->allocator_);
Expand Down
Loading
Loading