feat: add HGraph MCI clique companion index#2385
Conversation
Merge Protections🟢 All 3 merge protections satisfied — ready to merge. Show 3 satisfied protections🟢 Require kind label
🟢 Require version label
🟢 Require linked issue for feature/bug PRs
|
|
Warning Gemini encountered an error creating the review. You can try again by commenting |
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request introduces the HGraph MCI (Maximal Clique Index) Companion feature, which optimizes filtered KNN searches by building and storing clique metadata alongside the HGraph index. The changes span documentation, examples, build configuration updates, and the core algorithms for MCI building, searching, and serialization. The code review identified several critical issues in the new implementation, including a memory corruption bug in ListLinearHeap::initRaw due to an unupdated size variable, a severe query-time performance bottleneck from allocating visited_nodes per search, a data race on num_cliques_per_node in the parallel builder, and potential undefined behavior in changeToCoreOrder upon heap pop failure. Additionally, improvements were suggested to replace raw memory allocations with RAII containers and standard printf statements with the library's logging system.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
fed41a2 to
78b29f3
Compare
b8eeedc to
25830d9
Compare
|
/label S-waiting-on-review |
Add an optional HGraph MCI companion that stores clique metadata with HGraph, routes filtered search through an MCI searcher, supports serialization and add maintenance, and documents the companion workflow with a self-contained example. Signed-off-by: zhuangye.yxw <2510035537@qq.com> Assisted-by: Codex:gpt-5
Signed-off-by: zhuangye.yxw <2510035537@qq.com> Assisted-by: Codex:GPT-5
Remove public build-time use_mci and hgraph_valid_ratio_threshold handling from HGraph MCI options, default the search routing threshold to 0.05, and rename MCI source files to drop the v3 file-name suffix. Signed-off-by: zhuangye.yxw <2510035537@qq.com> Assisted-by: Codex:GPT-5
Signed-off-by: zhuangye.yxw <2510035537@qq.com> Assisted-by: Codex:GPT-5
Signed-off-by: zhuangye.yxw <2510035537@qq.com> Assisted-by: Codex:GPT-5
Signed-off-by: zhuangye.yxw <2510035537@qq.com> Assisted-by: Codex:GPT-5
Signed-off-by: zhuangye.yxw <2510035537@qq.com> Assisted-by: Codex:GPT-5
468cc1a to
64c1082
Compare
Signed-off-by: zhuangye.yxw <2510035537@qq.com> Assisted-by: Codex:GPT-5
|
/review |
|
Automated pull request review completed. Review effort: Submitted 8 inline comments. |
vsag-bot
left a comment
There was a problem hiding this comment.
Automated inline review completed.
Review effort: high (8275 changed lines across 45 files).
Submitted 8 inline comments.
Reviewed commit 5914e06.
| g.edges[g.m] = g.edges[i]; | ||
| } | ||
| } | ||
| g.m++; |
There was a problem hiding this comment.
[P1] Guard the zero-edge graph before incrementing m
When edge is empty, this unconditional increment creates one default (0, 0) edge while g.n remains zero. build_csr() then writes p_idx[1] into a one-element vector; an ASan probe reproduces the heap-buffer-overflow. The optimized builder can reach this state when a seed's nearest candidate is a duplicate (distance limit zero) while the remaining candidates are distinct, so return no cliques before building the CSR when the edge list is empty.
| re_id.resize(g.n); | ||
| std::fill(re_id.begin(), re_id.begin() + g.n, g.n); | ||
|
|
||
| if (in_c.capacity() < g.max_d) |
There was a problem hiding this comment.
[P1] Resize reusable scratch vectors by size, not capacity
Checking capacity() does not ensure that indices below g.max_d are within the vector's logical size. This is especially immediate for vector<bool>, whose first small resize commonly leaves a capacity of 64: a later seed with a larger degree skips resize(), and this fill plus subsequent indexing run past size(). Reusing one runner for graphs of sizes 5, 8, and 9 aborts under _GLIBCXX_DEBUG; the same issue exists for re_id above, so both checks must use size() or resize unconditionally.
| mci_param.metric = this->metric_; | ||
| mci_param.used_precise_float_csr = &mci_used_precise_float_csr; | ||
| } | ||
| search_result = this->mci_searcher_->Search( |
There was a problem hiding this comment.
[P1] Do not route executor-backed attribute filters to MCI
Attribute filtering is stored in search_param.executors, but both MCI implementations only check is_inner_id_allowed and never run these executors. A request combining a selective user filter with enable_attribute_filter_ can therefore route here and return IDs that violate the attribute expression. Either incorporate the executor-produced filter into MCI's validity check or keep such requests on the HGraph path.
| if (row_stride != nullptr) { | ||
| *row_stride = this->code_size_ / sizeof(float); | ||
| } | ||
| return reinterpret_cast<const float*>(memory_io->GetReadOnlyRawData()); |
There was a problem hiding this comment.
[P1] Pin the raw MemoryIO buffer for the duration of MCI search
This returns an unpinned pointer that MemoryIO::ResizeImpl or WriteImpl may invalidate through Reallocate. HGraph search's shared global_mutex_ does not protect insert_persistent_codes(), which runs before Add takes that mutex, so concurrent Add can reallocate the FP32 buffer while search_precise_float_csr() is dereferencing it. The fast path needs a lifetime/read lock spanning the search, or it must be disabled while the index is mutable.
| clique_ids.clear(); | ||
| cliques->CollectNodeCliqueIds(candidate->inner_id, clique_ids); | ||
| for (auto clique_id : clique_ids) { | ||
| if (visited_cliques.Get(clique_id)) { |
There was a problem hiding this comment.
[P1] Keep the clique-count snapshot stable during traversal
The visit-mark array is sized from an unlocked TotalLogicalCliqueCount() snapshot, while each later clique lookup takes a separate lock. During a batched concurrent Add, AppendNewClique() can insert another clique between those operations, allowing CollectNodeCliqueIds() to return an ID equal to or above the marks size; visited_cliques.Get() then indexes out of bounds. Hold a stable cell snapshot across traversal, retry on a version change, or grow/check the marks before indexing.
| max_norm_error); | ||
|
|
||
| auto distance_from_dot = [&](InnerIdType lhs, InnerIdType rhs, float dot) { | ||
| const float distance = |
There was a problem hiding this comment.
[P2] Honor the configured metric in optimized clique distances
The float fast path always constructs local edges from squared-L2 distance, even when HGraph is configured for cosine or inner product; it receives no metric parameter. This disagrees with the generic path's ComputePairVectors() reference behavior, and raw cosine build vectors are not necessarily normalized, so magnitude can completely change clique topology and filtered recall. Pass the metric and reproduce its distance semantics, or restrict this optimized path to L2.
| const auto candidate_limit = | ||
| std::min<uint64_t>({params.candidate_limit, graph.row_stride, total - 1}); | ||
| const auto clique_threshold = | ||
| std::max<uint64_t>(2, std::min<uint64_t>({params.clique_max, candidate_limit + 1, total})); |
There was a problem hiding this comment.
[P2] Enforce clique_max as an upper bound
clique_max is used here as a minimum enumeration threshold, and no later step truncates saved cliques to that size. For example, clique_max: 4 can store all nine vertices of a complete local graph, contrary to the documented maximum and with unexpectedly larger memory and query fan-out. Apply an actual upper bound when saving cliques, or correct the public parameter contract if minimum size is intended.
| } | ||
| } | ||
| if (not seed_list_provided) { | ||
| for (InnerIdType seed = 0; seed < total and seeds < seed_target; ++seed) { |
There was a problem hiding this comment.
[P2] Avoid full-index seed scans for callback filters
A normal callback-based Filter does not implement GetValidIds(), so MCI falls back to scanning inner IDs from zero until it finds the requested number of valid seeds. With the sparse filters that trigger MCI, valid IDs near the end can make every query execute millions of filter callbacks, turning the intended sublinear search into an O(total) scan. Use bounded sampling or a reusable valid-ID source, or fall back to HGraph when seed enumeration is unavailable.
Summary
Add an optional HGraph MCI companion for filtered KNN search. The companion stores clique metadata inside the HGraph index, shares HGraph vector storage, and is not exposed as a standalone public index type.
Changes
src/algorithm/mci/module,CliqueDataCell, and MCI searcher used by HGraph filtered search.GetStats().index_param.mcienables build-time MCI by presence,hgraph_valid_ratio_thresholdis search-only, MCI search seed parameters aremci_seed_countandmci_seed_ratio, and the defaultmci_seed_countis 500.src/simd, remove themci_v3_file-name prefix, rename the MCI namespace tovsag::mci, and align internal naming/style with VSAG conventions.MemoryIOraw-buffer fast path for contiguous FP32 storage.Add()is for small increments on an existing initial index and should not be used to build an MCI index from empty.fp32_simd_test.cppmacro expansion in one large Catch2 test to lower GCC debug-info variable tracking pressure in CI.Tests
Compatibility Impact
index_param.mci;hgraph_valid_ratio_thresholdbelongs to search parameters.Performance and Concurrency Impact
Related
Closes: #2386