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
20 changes: 15 additions & 5 deletions docs/docs/en/src/advanced/enhance_graph.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ At build time:

```json
{
"hnsw": {
"index_param": {
"base_quantization_type": "fp32",
"max_degree": 32,
"ef_construction": 400,
"use_conjugate_graph": true
Expand All @@ -23,7 +24,7 @@ At search time, toggle it via the `use_conjugate_graph_search` key in the search

```cpp
std::string search_param_json = R"({
"hnsw": {
"hgraph": {
"ef_search": 100,
"use_conjugate_graph_search": true
}
Expand All @@ -33,9 +34,16 @@ auto result = index->KnnSearch(query, k, search_param_json);

## How It Works

The conjugate graph is built by inverting "failure paths" over the training data on the original
graph and then used as additional candidate edges during greedy expansion at search time. It is a
lightweight patch on the main graph, typically below 10% of the main graph's size.
Call `Feedback(query, k, search_parameters, global_optimum_id)` after a hard query when the exact
nearest label is known. Omitting `global_optimum_id` makes HGraph compute it by an exact scan.
For offline enhancement, `Pretrain(base_ids, k, search_parameters)` generates queries between the
chosen float32 base vectors and their neighbors, then feeds the resulting failures back. Both
methods return the number of newly inserted conjugate edges; redundant feedback returns zero.

The conjugate graph maps local-result labels to known global optima and contributes extra
candidates after HGraph's normal traversal. `use_conjugate_graph` is disabled by default; calling
`Feedback` or `Pretrain` without it returns `UNSUPPORTED_INDEX_OPERATION`. Search enhancement is
enabled by default for an enabled graph and can be disabled per search.

## Example

Expand All @@ -52,4 +60,6 @@ recall end-to-end.

- Build time increases slightly when enabled.
- Conjugate-graph data is serialized together with the index.
- `UpdateId` updates conjugate edges as well as the HGraph label table.
- `Pretrain` currently supports float32 HGraph indexes; `Feedback` supports all HGraph dtypes.
- It can be combined with `Tune` — they target route quality and runtime parameters respectively.
1 change: 1 addition & 0 deletions docs/docs/en/src/indexes/hgraph.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ most users need; the exhaustive list is in [Index Parameters](../resources/index
| `base_direct_read` / `precise_direct_read` | bool | `false` | With `uring_io`, open the corresponding file using direct IO instead of the page cache. |
| `hgraph_init_capacity` | int | `100` | Initial capacity hint (doesn't cap the final size) |
| `persist_source_id` | bool | `false` | Persist source-ID metadata during serialization so a restored index can later export a reusable build cache. |
| `use_conjugate_graph` | bool | `false` | Enable `Feedback`/`Pretrain` graph enhancement; see [Graph Index Enhancement](../advanced/enhance_graph.md). |
| `resize_increase_count_bit` | int | `10` | `log2` of the slot-growth batch. Valid range is `1` to `31`; `1` grows in 2-slot batches and `10` in 1,024-slot batches. Smaller values reduce preallocation but can increase reallocations. |

`use_reverse_edges` is intended for workloads that need fast incoming-neighbor inspection, graph
Expand Down
3 changes: 3 additions & 0 deletions docs/docs/en/src/resources/index_parameters.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ HGraph places its build parameters under the generic `index_param` key (see
| `label_remap_type` | `pg` | Label-map implementation: `pg` (default) or `robin` |
| `reorder_source` | `precise` | Reorder from the `precise` store or directly from `base`; RaBitQ x+y split, including `tq_chain="mrle, rabitq"`, selects `base` automatically |
| `persist_source_id` | `false` | Include HGraph source-ID metadata in serialization; useful when a restored index must later export a build cache |
| `use_conjugate_graph` | `false` | Enable HGraph feedback/pretraining and persist the auxiliary conjugate graph |
| `mrle_dim` | `0` | MRLE output dimension in `[0, dim]`; `0` means input dimension |
| `fast_encode_rabitq` | `true` | Use fast multi-bit RaBitQ encoding; `false` restores the exact encoder |
| `fast_encode_rabitq_rounds` | `6` | Fast-encoder refinement rounds in `[1, 32]` |
Expand All @@ -64,6 +65,8 @@ At search time:

`ef_search` accepts any positive signed 64-bit integer. It is no longer capped relative to
`topk`; very large values can substantially increase latency and memory used by the frontier.
`use_conjugate_graph_search` is a boolean (default `true`) that uses learned conjugate edges when
the index was built with `use_conjugate_graph: true`.

The `hgraph` search-param object also accepts `brute_force_threshold` (a float
in `[0.0, 1.0]`, default `0.0`). When set above zero and the request carries a
Expand Down
18 changes: 14 additions & 4 deletions docs/docs/zh/src/advanced/enhance_graph.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ VSAG 通过 **Conjugate Graph**(共轭图)机制对这类查询进行在线/

```json
{
"hnsw": {
"index_param": {
"base_quantization_type": "fp32",
"max_degree": 32,
"ef_construction": 400,
"use_conjugate_graph": true
Expand All @@ -23,7 +24,7 @@ VSAG 通过 **Conjugate Graph**(共轭图)机制对这类查询进行在线/

```cpp
std::string search_param_json = R"({
"hnsw": {
"hgraph": {
"ef_search": 100,
"use_conjugate_graph_search": true
}
Expand All @@ -33,8 +34,15 @@ auto result = index->KnnSearch(query, k, search_param_json);

## 工作原理

共轭图由原图在训练数据上的"失败路径"反向构建而成,在搜索时作为补充的候选边参与贪心扩展。
它相当于对主图的一层轻量索引补丁,典型体积 < 主图 10%。
已知困难查询的精确最近邻标签时,可调用
`Feedback(query, k, search_parameters, global_optimum_id)`;省略 `global_optimum_id` 时,
HGraph 会通过精确扫描计算。离线增强可调用 `Pretrain(base_ids, k, search_parameters)`,
它会在选定的 float32 基础向量及其近邻之间生成查询并反馈失败路径。两个方法均返回新插入的
共轭边数量,重复反馈返回 0。

共轭图保存从局部结果标签到已知全局最优标签的映射,并在 HGraph 常规遍历后补充候选。
`use_conjugate_graph` 默认关闭;未开启时调用 `Feedback` 或 `Pretrain` 会返回
`UNSUPPORTED_INDEX_OPERATION`。索引启用共轭图后,搜索增强默认开启,也可按次搜索关闭。

## 示例

Expand All @@ -50,4 +58,6 @@ auto result = index->KnnSearch(query, k, search_param_json);

- 启用后构建时间会略有增加。
- 共轭图数据会随索引一并序列化。
- `UpdateId` 会同时更新 HGraph 标签表与共轭边。
- `Pretrain` 当前支持 float32 HGraph;`Feedback` 支持 HGraph 的全部数据类型。
- 与 `Tune` 可以叠加使用,分别作用于路由质量与运行期参数。
1 change: 1 addition & 0 deletions docs/docs/zh/src/indexes/hgraph.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ auto result = index->KnnSearch(
| `base_direct_read` / `precise_direct_read` | bool | `false` | 使用 `uring_io` 时,以 direct IO 打开对应文件而非经过页缓存 |
| `hgraph_init_capacity` | int | `100` | 初始容量提示(不会限制最终规模) |
| `persist_source_id` | bool | `false` | 序列化时保留 Source ID 元数据,使恢复后的索引仍可导出可复用的构建缓存 |
| `use_conjugate_graph` | bool | `false` | 启用 `Feedback`/`Pretrain` 图增强;详见[图索引增强](../advanced/enhance_graph.md) |
| `resize_increase_count_bit` | int | `10` | 扩容批次 slot 数的 `log2`,取值范围为 `1` 到 `31`。`1` 表示每次按 2 个 slot 对齐,`10` 表示按 1024 个 slot 对齐。较小取值减少预分配,但可能增加重分配次数。 |

`use_reverse_edges` 面向需要快速检查入邻居、图分析或图维护算法的负载。维护反向邻接表会让边
Expand Down
3 changes: 3 additions & 0 deletions docs/docs/zh/src/resources/index_parameters.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ HGraph 的构建参数使用通用的 `index_param` 键(参见 `examples/cpp/1
| `label_remap_type` | `pg` | label map 实现:默认 `pg`,或 `robin` |
| `reorder_source` | `precise` | 从 `precise` 存储或直接从 `base` 重排;RaBitQ x+y split(包括 `tq_chain="mrle, rabitq"`)会自动选择 `base` |
| `persist_source_id` | `false` | 序列化 HGraph 时保留 Source ID 元数据;适用于恢复索引后继续导出构建缓存 |
| `use_conjugate_graph` | `false` | 启用 HGraph 反馈/预训练并持久化辅助共轭图 |
| `mrle_dim` | `0` | MRLE 输出维度,范围 `[0, dim]`;`0` 表示输入维度 |
| `fast_encode_rabitq` | `true` | 使用多 bit RaBitQ 快速编码;设为 `false` 恢复精确编码器 |
| `fast_encode_rabitq_rounds` | `6` | 快速编码器微调轮数,范围 `[1, 32]` |
Expand All @@ -61,6 +62,8 @@ HGraph 的构建参数使用通用的 `index_param` 键(参见 `examples/cpp/1

`ef_search` 接受任意正的有符号 64 位整数,不再存在与 `topk` 相关的上限。非常大的取值会
明显增加延迟和搜索前沿占用的内存。
`use_conjugate_graph_search` 为布尔值(默认 `true`);当构建时设置
`use_conjugate_graph: true` 后,它控制是否使用已学习的共轭边。

`hgraph` 搜索参数还接受 `brute_force_threshold`(`[0.0, 1.0]` 区间的 float,
默认 `0.0`)。当取值 `> 0` 且当前请求的 filter 的 `ValidRatio()` 不超过该
Expand Down
174 changes: 174 additions & 0 deletions examples/cpp/304_feature_enhance_graph.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@

// Copyright 2024-present the vsag project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#include <vsag/vsag.h>

#include <iostream>

int
main(int argc, char** argv) {
/******************* Prepare Base Dataset *****************/
int dim = 128;
int base_elements = 2000;
int query_elements = 1000;
int ef_search = 10;
Comment thread
wxyucs marked this conversation as resolved.

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.

[suggestion] The variable int ef_search = 10 is declared but never used. The search parameters in the JSON strings below hardcode "ef_search": 10 directly. Either remove this unused variable or use it in the parameter strings (e.g., via string formatting) to make the example easier to modify.

int64_t k = 10;

auto base = vsag::Dataset::Make();
std::shared_ptr<int64_t[]> base_ids(new int64_t[base_elements]);
std::shared_ptr<float[]> base_data(new float[dim * base_elements]);
std::mt19937 rng;
rng.seed(47);
std::uniform_real_distribution<float> distribution_real(-1, 1);
for (int i = 0; i < base_elements; i++) {
base_ids[i] = i;

for (int d = 0; d < dim; d++) {
base_data[d + i * dim] = distribution_real(rng);
}
}
base->Dim(dim)
->NumElements(base_elements)
->Ids(base_ids.get())
->Float32Vectors(base_data.get())
->Owner(false);

/******************* Build HGraph Index *****************/
// When you want to use EnhanceGraph, the use_conjugate_graph must be set to true
auto hgraph_build_parameters = R"(
{
"dtype": "float32",
"metric_type": "l2",
"dim": 128,
"index_param": {
"base_quantization_type": "fp32",
"max_degree": 16,
"ef_construction": 100,
"use_conjugate_graph": true
}
}
)";
std::shared_ptr<vsag::Index> hgraph;
if (auto index = vsag::Factory::CreateIndex("hgraph", hgraph_build_parameters);
index.has_value()) {
hgraph = index.value();
} else {
std::cerr << "Failed to create HGraph: " << index.error().message << std::endl;
return -1;
}

if (const auto build_result = hgraph->Build(base); build_result.has_value()) {
std::cout << "After Build(), Index contains: " << hgraph->GetNumElements() << std::endl;
} else {
std::cerr << "Failed to build index: " << build_result.error().message << std::endl;
exit(-1);
}

/******************* Search HGraph Index without Conjugate Graph *****************/
// record the failed ids
std::set<std::pair<int, int64_t>> failed_queries;
// use_conjugate_graph_search indicates whether to use information from the conjugate_graph to enhance the search results.
auto before_enhance_parameters = R"(
{
"hgraph": {
"ef_search": 10,
"use_conjugate_graph_search": false
}
}
)";
{
int correct = 0;
std::cout << "====Search Stage====" << std::endl;

for (int i = 0; i < query_elements; i++) {
auto query = vsag::Dataset::Make();
query->Dim(dim)
->Float32Vectors(base_data.get() + i * dim)
->NumElements(1)
->Owner(false);

auto result = hgraph->KnnSearch(query, k, before_enhance_parameters);
int64_t global_optimum = i; // global optimum is itself
if (result.has_value()) {
int64_t local_optimum = result.value()->GetIds()[0];
if (local_optimum == global_optimum) {
correct++;
} else {
failed_queries.emplace(i, global_optimum);
}
} else {
std::cerr << "Search Error: " << result.error().message << std::endl;
}
}
std::cout << "Recall: " << correct / (1.0 * query_elements) << std::endl;
}

/******************* Enhance Phase *****************/
//
{
uint32_t error_fixed = 0;
std::cout << "====Feedback Stage====" << std::endl;
for (auto item : failed_queries) {
Comment thread
wxyucs marked this conversation as resolved.
auto query = vsag::Dataset::Make();
query->Dim(dim)
->Float32Vectors(base_data.get() + item.first * dim)
->NumElements(1)
->Owner(false);
auto feedback = hgraph->Feedback(query, 1, before_enhance_parameters, item.second);
if (not feedback.has_value()) {
std::cerr << "Feedback Error: " << feedback.error().message << std::endl;
return -1;
}
error_fixed += feedback.value();
}
std::cout << "Fixed queries num: " << error_fixed << std::endl;
}

/******************* Search HGraph Index with Conjugate Graph *****************/
auto after_enhance_parameters = R"(
{
"hgraph": {
"ef_search": 10,
"use_conjugate_graph_search": true
}
}
)";
{
int correct = 0;
std::cout << "====Enhanced Search Stage====" << std::endl;

for (int i = 0; i < query_elements; i++) {
auto query = vsag::Dataset::Make();
query->Dim(dim)
->Float32Vectors(base_data.get() + i * dim)
->NumElements(1)
->Owner(false);

auto result = hgraph->KnnSearch(query, k, after_enhance_parameters);
int64_t global_optimum = i; // global optimum is itself
if (result.has_value()) {
int64_t local_optimum = result.value()->GetIds()[0];
if (local_optimum == global_optimum) {
correct++;
}
} else {
std::cerr << "Search Error: " << result.error().message << std::endl;
}
}
std::cout << "Enhanced Recall: " << correct / (1.0 * query_elements) << std::endl;
}

return 0;
}
3 changes: 3 additions & 0 deletions examples/cpp/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ target_link_libraries(302_feature_range_search vsag)
add_executable (303_feature_remove 303_feature_remove.cpp)
target_link_libraries (303_feature_remove vsag)

add_executable(304_feature_enhance_graph 304_feature_enhance_graph.cpp)
target_link_libraries(304_feature_enhance_graph vsag)

# add_executable(305_feature_update 305_feature_update.cpp)
# target_link_libraries(305_feature_update vsag)

Expand Down
2 changes: 2 additions & 0 deletions include/vsag/constants.h
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ extern const char* const PARAMETER_REPR;
extern const char* const REPR_DENSE;
extern const char* const REPR_SPARSE;
extern const char* const REPR_MULTI_VECTOR;
extern const char* const PARAMETER_USE_CONJUGATE_GRAPH;
extern const char* const PARAMETER_USE_CONJUGATE_GRAPH_SEARCH;
extern const char* const PARAMETER_USE_OLD_SERIAL_FORMAT;

extern const char* const ODESCENT_PARAMETER_ALPHA;
Expand Down
1 change: 1 addition & 0 deletions src/algorithm/hgraph/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ set(HGRAPH_SRCS
hgraph_param_mapping.cpp
hgraph_search.cpp
hgraph_serialize.cpp
hgraph_enhance.cpp
)

add_library(hgraph OBJECT ${HGRAPH_SRCS})
Expand Down
10 changes: 9 additions & 1 deletion src/algorithm/hgraph/hgraph.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,8 @@ HGraph::HGraph(const HGraphParameterPtr& hgraph_param, const vsag::IndexCommonPa
graph_type_(hgraph_param->graph_type),
hierarchical_datacell_param_(hgraph_param->hierarchical_graph_param),
mci_parameters_(hgraph_param->mci_parameters),
use_old_serial_format_(common_param.use_old_serial_format_) {
use_old_serial_format_(common_param.use_old_serial_format_),
use_conjugate_graph_(hgraph_param->use_conjugate_graph) {
this->support_duplicate_ = hgraph_param->support_duplicate;
this->deduplicate_storage_ = hgraph_param->deduplicate_storage;
const bool is_dense_vector = common_param.repr_ == RecordRepr::DENSE &&
Expand Down Expand Up @@ -92,6 +93,9 @@ HGraph::HGraph(const HGraphParameterPtr& hgraph_param, const vsag::IndexCommonPa
if (this->mci_parameters_.enabled) {
this->mci_cliques_ = std::make_shared<CliqueDataCell>(common_param.allocator_.get());
}
if (this->use_conjugate_graph_) {
Comment thread
wxyucs marked this conversation as resolved.
this->conjugate_graph_ = std::make_shared<ConjugateGraph>(common_param.allocator_.get());
}

this->bottom_graph_ =
GraphInterface::MakeInstance(hgraph_param->bottom_graph_param, common_param);
Expand Down Expand Up @@ -839,6 +843,10 @@ HGraph::cal_memory_usage() {
if (this->mci_cliques_ != nullptr) {
memory += this->mci_cliques_->GetMemoryUsage();
}
if (this->conjugate_graph_ != nullptr) {
std::shared_lock graph_lock(this->conjugate_graph_mutex_);
memory += this->conjugate_graph_->GetMemoryUsage();
}

std::unique_lock lock(this->memory_usage_mutex_);
this->current_memory_usage_.store(memory);
Expand Down
Loading
Loading