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
9 changes: 6 additions & 3 deletions docs/docs/en/src/api/dataset.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,9 +116,12 @@ if (result.has_value()) {
}
```

For KNN, `GetNumElements()` is `1` and the ids/distances arrays have length `k`. For range search,
the number of matches is reported through the result's dimension. See
[k-Nearest Neighbor Search](../guide/knn_search.md).
For single-query KNN, `GetNumElements()` is `1` and the ids/distances arrays have length `k`. HGraph
and IVF batched KNN returns a row-major `GetNumElements() x GetDim()` matrix; slot `i` of row `q` is
at `q * GetDim() + i`. Short rows are padded with `id == -1` and infinite distance. Batched KNN
rejects an index containing external label `-1` to keep this padding unambiguous. Range search
supports one query only.
See [k-Nearest Neighbor Search](../guide/knn_search.md).

## `SparseVector`

Expand Down
10 changes: 6 additions & 4 deletions docs/docs/en/src/api/index_class.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,13 +129,15 @@ See `examples/cpp/303_feature_remove.cpp`.

## Search

The recommended entry point is [`SearchWithRequest`](#searchwithrequest), which takes a single
The recommended entry point is [`SearchWithRequest`](#searchwithrequest), which takes a
[`SearchRequest`](search.md#searchrequest) carrying the query, mode, top-k / radius, and any filters.
The older per-argument `KnnSearch` / `RangeSearch` overloads remain for compatibility.

Every search returns a `DatasetPtr`: for KNN, `num_elements == 1` and `ids` / `distances` have
length `k`; for range search, the result length is the number of matches. See [Dataset](dataset.md)
for how to read results.
Every search returns a `DatasetPtr`: single-query KNN has `num_elements == 1`; HGraph and IVF
also support batched KNN, returning a row-major `num_elements x dim` matrix. `dim` is the returned
row width and can be clamped below the requested top-k. Missing entries are padded with `id == -1`;
batched KNN rejects an index containing external label `-1` to keep this padding unambiguous. Range
search accepts a single query only. See [Dataset](dataset.md).

### `SearchWithRequest`

Expand Down
20 changes: 8 additions & 12 deletions docs/docs/en/src/api/search.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ enum class SearchMode {

| Field | Type | Default | Meaning |
|-------|------|---------|---------|
| `query_` | `DatasetPtr` | `nullptr` | The query. IVF KNN requests support multiple query vectors; other requests allow one. |
| `query_` | `DatasetPtr` | `nullptr` | The query. HGraph and IVF support contiguous multi-query KNN batches; range search supports one query only. |
| `mode_` | `SearchMode` | `KNN_SEARCH` | KNN vs. range search. |
| `topk_` | `int64_t` | `10` | Neighbors to return (KNN mode). Must be positive. |
| `radius_` | `float` | `0.5` | Distance threshold (range mode). Non-negative. |
Expand Down Expand Up @@ -81,12 +81,12 @@ Reordering is automatically disabled in callback mode. Other indexes do not supp

### IVF bucket routing

IVF accepts `{"ivf":{"scan_buckets_count":N,"disable_bucket_scan":true}}` through
`params_str_`. This routing-only mode returns the `N` selected bucket IDs per query in the
result `Dataset` instead of vector labels. `NumElements()` equals the number of queries,
`Dim()` equals `scan_buckets_count`, `GetIds()` contains bucket IDs (with `-1` for empty
slots), and `GetDistances()` has distances to bucket centroids. No vector scan is performed,
so filters, `topk`, range limits, reordering, and reasoning options are ignored.
IVF accepts `{"ivf":{"scan_buckets_count":N,"disable_bucket_scan":true}}` in `params_str_`.
This routing-only mode returns `N` bucket IDs per query instead of vector labels.
`NumElements()` is the query count, `Dim()` is `scan_buckets_count`, `GetIds()` contains bucket IDs
(`-1` for empty slots), and `GetDistances()` contains distances to the corresponding bucket
centroids. It does not scan bucket vectors, so filters, `topk`, range limits, reordering, and
reasoning options are ignored.

### IVF bucket IDs bypass

Expand All @@ -101,13 +101,9 @@ selection and scans only the specified buckets.
**Semantics:**
- **Empty** (default): Use normal bucket routing via `ClassifyDatasForSearch`.
- **Non-empty**: Skip routing and scan only the provided bucket IDs.
- **Batch IVF KNN**: One outer `bucket_ids_` entry per query vector.
Result is rectangular: `NumElements()` is query count, `Dim()` is `topk_`.
Missing neighbors are `-1` with infinite distance.

**Constraints:**
- Batch IVF search supports KNN only; custom query distance and reasoning labels are unsupported.
- A non-empty outer vector must contain exactly one non-empty entry per query vector.
- Currently only single-query is supported; the outer vector must contain exactly one entry.
- Each bucket ID must be in range `[0, bucket_count)`.
- Duplicate bucket IDs are rejected.
- Incompatible with `disable_bucket_scan` mode.
Expand Down
6 changes: 4 additions & 2 deletions docs/docs/zh/src/api/dataset.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,8 +115,10 @@ if (result.has_value()) {
}
```

对 KNN,`GetNumElements()` 为 `1`,ids/distances 数组长度为 `k`。对范围搜索,命中数通过结果的维度报告。
见 [k-近邻搜索](../guide/knn_search.md)。
对单查询 KNN,`GetNumElements()` 为 `1`,ids/distances 数组长度为 `k`。HGraph 和 IVF 的批量 KNN 返回
按行主序排列的 `GetNumElements() x GetDim()` 矩阵,行 `q` 的第 `i` 个槽位位于
`q * GetDim() + i`。不足一行的结果以 `id == -1` 和无穷距离填充。为保持填充语义明确,批量 KNN 会拒绝
包含外部 label `-1` 的索引。范围搜索仅支持单个查询。见 [k-近邻搜索](../guide/knn_search.md)。

## `SparseVector`

Expand Down
8 changes: 5 additions & 3 deletions docs/docs/zh/src/api/index_class.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,12 +127,14 @@ using WriteFuncType = std::function<void(OffsetType, SizeType, const void*)>;

## 搜索

推荐的入口是 [`SearchWithRequest`](#searchwithrequest),它接收单个
推荐的入口是 [`SearchWithRequest`](#searchwithrequest),它接收
[`SearchRequest`](search.md#searchrequest),其中携带查询、模式、top-k / 半径以及各类过滤器。较旧的
逐参数 `KnnSearch` / `RangeSearch` 重载为兼容性保留。

每次搜索都返回一个 `DatasetPtr`:对 KNN,`num_elements == 1`,`ids` / `distances` 长度为 `k`;对范围
搜索,结果长度即命中数。如何读取结果见 [Dataset](dataset.md)。
每次搜索都返回一个 `DatasetPtr`:单查询 KNN 的 `num_elements == 1`;HGraph 和 IVF 还支持批量 KNN,
返回按行主序排列的 `num_elements x dim` 矩阵。`dim` 是实际返回的行宽,可能小于请求的 top-k;缺失条目
以 `id == -1` 填充;为保持填充语义明确,批量 KNN 会拒绝包含外部 label `-1` 的索引。范围搜索只接受单个查询。如何读取结果见
[Dataset](dataset.md)。

### `SearchWithRequest`

Expand Down
10 changes: 3 additions & 7 deletions docs/docs/zh/src/api/search.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

```cpp
vsag::SearchRequest request;
request.query_ = query; // 含单个查询向量的 DatasetPtr
request.query_ = query; // 单个查询,或 HGraph/IVF 的连续 KNN 查询批次
request.mode_ = vsag::SearchMode::KNN_SEARCH;
request.topk_ = 10;
request.params_str_ = R"({"hgraph": {"ef_search": 100}})";
Expand All @@ -34,7 +34,7 @@ enum class SearchMode {

| 字段 | 类型 | 默认值 | 含义 |
|------|------|--------|------|
| `query_` | `DatasetPtr` | `nullptr` | 查询。IVF KNN 请求支持多个查询向量,其他请求只允许一个。 |
| `query_` | `DatasetPtr` | `nullptr` | 查询。HGraph 和 IVF KNN 支持连续的多查询批次;范围搜索只支持单个查询。 |
| `mode_` | `SearchMode` | `KNN_SEARCH` | KNN 还是范围搜索。 |
| `topk_` | `int64_t` | `10` | 要返回的邻居数(KNN 模式)。必须为正。 |
| `radius_` | `float` | `0.5` | 距离阈值(范围模式)。非负。 |
Expand Down Expand Up @@ -95,13 +95,9 @@ reasoning 选项均会被忽略。
**语义:**
- **空**(默认):使用正常的桶路由(`ClassifyDatasForSearch`)。
- **非空**:跳过路由,仅扫描提供的桶 ID。
- **批量 IVF KNN**:`bucket_ids_` 外层每项对应一个查询向量。
结果为矩形:`NumElements()` 是查询数,`Dim()` 是 `topk_`。
缺失邻居用 `-1` 和无穷距离填充。

**约束:**
- 批量 IVF 搜索仅支持 KNN;不支持自定义查询距离和 reasoning labels。
- 非空外层向量必须为每个查询向量提供一个非空条目。
- 当前仅支持单查询;外层向量必须恰好包含一个条目。
- 每个桶 ID 必须在 `[0, bucket_count)` 范围内。
- 重复的桶 ID 会被拒绝。
- 与 `disable_bucket_scan` 模式不兼容。
Expand Down
21 changes: 17 additions & 4 deletions include/vsag/index.h
Original file line number Diff line number Diff line change
Expand Up @@ -323,11 +323,24 @@ class Index {

/**
* @brief Performing search with request on index
*
*
* @param request @see SearchRequest
* @return result contains
* - num_elements: 1
* - ids, distances: length is (num_elements * k)
* @return result contains
* - single-query requests: num_elements = 1, dim = actual returned
* result count (<= request.topk_ for KNN, and <= request.limited_size_
* for RANGE_SEARCH when limited_size_ > 0). May be < topk_ when filters
* reject candidates.
* - batched KNN requests, when supported by the implementation:
* num_elements = query->GetNumElements(),
* dim = implementation-defined returned row width. HGraph clamps it to
* min(request.topk_, GetNumElements()), while IVF preserves
* request.topk_. Callers MUST read `dim` from the returned dataset.
Comment on lines +333 to +337
* ids/distances are stored row-major with length (num_elements * dim).
* Queries that yielded fewer than dim neighbors are padded with
* sentinel entries (id = -1, distance = +infinity). Batch KNN rejects
* an index containing external label -1 to keep this padding unambiguous.
* Callers MUST check `ids[i] == -1`
* to detect padding and MUST read `dim` from the returned dataset.
*/
[[nodiscard]] virtual tl::expected<DatasetPtr, Error>
SearchWithRequest(const SearchRequest& request) const {
Expand Down
23 changes: 17 additions & 6 deletions include/vsag/search_request.h
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,21 @@ enum class SearchMode {
class SearchRequest {
public:
// basic params
/**
/**
* @brief Query dataset containing the vector or vectors to search for
* @details This DatasetPtr holds the query vector used for similarity search.
* IVF KNN requests and supported AnalyzeIndexBySearch implementations accept
* multiple query vectors; other requests allow one.
* @details This DatasetPtr holds the query data used for similarity search.
* - Single query: Set NumElements to 1 with one vector. Supported by all
Comment thread
LHT129 marked this conversation as resolved.
* search modes (KNN_SEARCH, RANGE_SEARCH).
* - Batched KNN: Set NumElements to the number of queries, with vectors
* stored contiguously. Supported by HGraph::SearchWithRequest and
* IVF::SearchWithRequest; results are returned with NumElements =
* query_count and a row-major Dim determined by the implementation
* (which can be less than topk when the index is smaller). Queries that yield
* fewer neighbors than the returned Dim are padded with sentinel entries
* (id = -1, distance = +infinity). Batch KNN rejects an index containing external
* label -1 to keep this padding unambiguous.
* - Batched RANGE_SEARCH is not supported; implementations MUST reject
* NumElements > 1 for range mode.
*/
DatasetPtr query_{nullptr};

Expand Down Expand Up @@ -219,8 +229,9 @@ class SearchRequest {

Comment thread
LHT129 marked this conversation as resolved.
/**
* @brief Pre-selected bucket IDs for bypassing IVF bucket routing (ClassifyDatasForSearch)
* @details The outer vector contains one entry per query vector.
* Inner vector contains ordered bucket IDs (caller is responsible for ordering).
* @details Supports one ordered bucket-ID list per query vector. For a single query,
* the outer vector must contain exactly one entry.
* Inner vectors contain ordered bucket IDs (caller is responsible for ordering).
* When non-empty with at least one ID, skips ClassifyDatasForSearch and searches only
* the specified buckets. Empty means "use default bucket routing".
*/
Expand Down
19 changes: 15 additions & 4 deletions src/algorithm/hgraph/hgraph.h
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
#pragma once

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

[suggestion] The overflow check in get_data() is overly conservative.

The check if (index > std::numeric_limits<uint32_t>::max()) on an int64_t parameter will never be true for valid inputs, since uint32_t::max() is ~4.3 billion and the label table cannot grow that large. This adds a branch and a <limits> include without providing meaningful protection.

If the intent is to guard against negative indices (which would be UB when cast to uint32_t), a simpler and more direct check would be:

if (index < 0) {
    throw std::out_of_range("negative label id");
}

Alternatively, if the goal is to match the original uint32_t parameter semantics exactly, consider keeping the parameter as uint32_t and having callers perform the conversion where needed.


#include <atomic>
#include <limits>
#include <memory>
#include <mutex>
#include <optional>
Expand Down Expand Up @@ -267,17 +268,21 @@ class HGraph : public InnerIndexInterface {
* pointer is absent.
*/
const void*
get_data(const DatasetPtr& dataset, uint32_t index = 0) const {
get_data(const DatasetPtr& dataset, int64_t index = 0) const {
CHECK_ARGUMENT(index >= 0, "query index must be non-negative");
CHECK_ARGUMENT(data_type_ == DataTypes::DATA_TYPE_SPARSE ||
Comment thread
LHT129 marked this conversation as resolved.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

[suggestion] The overflow guard dim_ > 0 && index <= INT64_MAX / dim_ will reject any call when dim_ == 0 and data_type_ != DATA_TYPE_SPARSE, because the left side of || is false and dim_ > 0 short-circuits to false. For a non-sparse empty index (dim_ == 0), this CHECK_ARGUMENT would fire incorrectly, preventing legitimate queries that should return empty results.

Consider restructuring the condition so that dim_ == 0 is handled gracefully (e.g. return nullptr early, or use dim_ > 0 || data_type_ == DATA_TYPE_SPARSE and only apply the multiplication check when dim_ > 0).

(dim_ > 0 && index <= std::numeric_limits<int64_t>::max() / dim_),
"query offset exceeds int64_t range");
if (data_type_ == DataTypes::DATA_TYPE_FLOAT) {
auto* ptr = dataset->GetFloat32Vectors();
return ptr ? ptr + static_cast<int64_t>(index) * dim_ : nullptr;
return ptr ? ptr + index * dim_ : nullptr;
} else if (data_type_ == DataTypes::DATA_TYPE_INT8) {
auto* ptr = dataset->GetInt8Vectors();
return ptr ? ptr + static_cast<int64_t>(index) * dim_ : nullptr;
return ptr ? ptr + index * dim_ : nullptr;
} else if (data_type_ == DataTypes::DATA_TYPE_FP16 ||
data_type_ == DataTypes::DATA_TYPE_BF16) {
auto* ptr = dataset->GetFloat16Vectors();
return ptr ? ptr + static_cast<int64_t>(index) * dim_ : nullptr;
return ptr ? ptr + index * dim_ : nullptr;
} else if (data_type_ == DataTypes::DATA_TYPE_SPARSE) {
auto* ptr = dataset->GetSparseVectors();
return ptr ? ptr + index : nullptr;
Expand Down Expand Up @@ -626,6 +631,12 @@ class HGraph : public InnerIndexInterface {
QueryContext* ctx,
const std::optional<float>& threshold = std::nullopt) const;

DatasetPtr
search_range_with_request(const SearchRequest& request,
const HGraphSearchParameters& params,
const FilterPtr& filter,
QueryContext& ctx) const;

private:
/// Reorder the candidate heap using precise codes, updating in-place.
void
Expand Down
Loading
Loading