feat(sindi): implement value-aware term pruning - #2542
Conversation
|
/label status/blocked |
|
Automated pull request review completed. Review effort: Submitted 5 inline comments. |
Merge Protections🟢 All 2 merge protections satisfied — ready to merge. Show 2 satisfied protections🟢 Require kind label
🟢 Require version label
|
vsag-bot
left a comment
There was a problem hiding this comment.
Automated inline review completed.
Review effort: high (600 changed lines across 28 files).
Submitted 5 inline comments.
Reviewed commit 0aa6524.
0aa6524 to
dc9a5c2
Compare
LHT129
left a comment
There was a problem hiding this comment.
Reviewed the latest commit (dc9a5c2) for the value-aware term pruning feature.
Summary: The implementation correctly replaces the flat term_prune_ratio with a nested term_prune object supporting independent ratio and threshold limits. The SortByValue() integration in Add() and build_immutable() is correctly placed before compaction/serialization. The analyzer paths (collect_coarse_candidates, collect_doc_prune_candidates) correctly pass window_num to SparseTermComputer. Test coverage is thorough across parameter parsing, sort correctness, scan count computation, and end-to-end search with pruning.
Existing review threads that still need attention:
-
CalcDistanceById(single) andCalDistanceById(batch) insindi.cppstill constructSparseTermComputerwithoutwindow_num, defaulting towindow_num=1. This meansterm_prune.thresholdis not divided across windows in these code paths, which could cause distance calculations to scan more postings than expected when a threshold is configured. -
The
SortByValue()call after everyAdd()batch sorts all terms in affected windows even when no term pruning is configured, which can regress streaming workloads.
No new blocking issues found beyond those already raised in existing review threads.
dc9a5c2 to
bc414f2
Compare
bc414f2 to
7417fd7
Compare
|
Maintenance update for
The inline thread was answered and resolved. A fresh CI run is starting. |
|
Fixed the CircleCI failure in |
|
test comment |
LHT129
left a comment
There was a problem hiding this comment.
Code review for feat(sindi): implement value-aware term pruning
Signed-off-by: Roxanne0321 <liruoxvan020321@qq.com> Assisted-by: Codex:gpt-5
|
Updated DMQ persistence handling in |
LHT129
left a comment
There was a problem hiding this comment.
Code Review Summary
This PR implements value-aware term pruning for SINDI sparse index. Overall code quality is good — most previous review comments have been addressed, the analyzer refactoring correctly delegates to Query(), and thread safety appears correct with proper lock usage. A few items to consider:
Key Findings
-
[note]
sort_posting_list_by_valueduplicates logic withSparseTermDataCell::SortByValue— The free function insindi.cppand the method insparse_term_datacell.cppshare the same sorting algorithm. Consider consolidating to reduce maintenance burden. -
[note]
CalcDistanceById/CalDistanceByIddon't passwindow_num— Insindi.cpp,SparseTermComputeris constructed withoutwindow_num, defaulting to 1. Sinceterm_prune_ratiowas removed (set to 0) andterm_prune_thresholddefaults to 0 (disabled), no pruning actually occurs in these paths, so this is functionally correct. However, if pruning parameters are ever enabled for these code paths, thewindow_numwould need to be passed. -
[note]
sort_posting_list_by_valueis outside thevsagnamespace — The free function insindi.cppis defined outside thevsagnamespace while all other code in the file is inside it.
What Was Addressed from Previous Reviews
- The nested
term_pruneJSON object design was changed to flat parameters ✅ - The
FromJsondefault-reset issue was addressed ✅ - The
uint64_t→uint32_tcast inGetTermScanCountwas addressed with properstatic_cast✅
What Remains
- The
CalcDistanceById/CalDistanceByIdwindow_numissue flagged by LHT129 is still present (though functionally harmless with current defaults)
vsag-bot
left a comment
There was a problem hiding this comment.
Automated inline review completed.
Review effort: high (1114 changed lines across 30 files).
Submitted 3 inline comments.
Reviewed commit 5a0d7ec.
Signed-off-by: Roxanne0321 <liruoxvan020321@qq.com> Assisted-by: Codex:gpt-5
Signed-off-by: Roxanne0321 <liruoxvan020321@qq.com> Assisted-by: Codex:gpt-5
Signed-off-by: Roxanne0321 <liruoxvan020321@qq.com> Assisted-by: Codex:gpt-5
Signed-off-by: Roxanne0321 <liruoxvan020321@qq.com> Assisted-by: Codex:gpt-5
LHT129
left a comment
There was a problem hiding this comment.
Code review for commit 96472c3 (test(sindi): simplify positive sparse fixtures).
The cleanup is well-executed. Consolidating "positive_sparse" into "sparse" with a 0.1F min value simplifies the test infrastructure without losing coverage. The NaN test simplification to only check SortByValue rejection is correct since NaN is now rejected at sort time, not insertion time.
A few observations:
-
[suggestion] The free function
sort_posting_list_by_valueinsindi.cppduplicates the same sorting logic asSparseTermDataCell::SortByValue(). Consider extracting the common in-place permutation algorithm into a shared utility to reduce duplication and maintenance burden. -
[note] The
SortByValuetest callsdata_cell.SortByValue()twice in a row without an assertion or comment. If this is testing idempotency, a brief comment would help future readers understand the intent. -
[note] The
sindi_dmq_datacell.cppTODO about resetting the serialization version is a good reminder — the version was bumped to 6 for the sorting change and should be reset in a dedicated format migration.
Overall the PR looks solid. The parameter validation changes (doc_prune_ratio, query_prune_ratio, term_prune_ratio all moving to [0, 1)) are consistent and well-tested. The test coverage for term pruning, incremental partial windows, threshold division across windows, and legacy deserialization sorting is comprehensive.
| REQUIRE(*data_cell.term_ids_[term] == Vector<uint16_t>({1, 2, 3, 0}, allocator.get())); | ||
| std::vector<float> sorted_values(values.size()); | ||
| const auto* data = data_cell.term_datas_[term]->data(); | ||
| if (quantization_type == SparseValueQuantizationType::SQ8) { |
There was a problem hiding this comment.
[note] SortByValue() is called twice in a row here. If this is testing idempotency (i.e., sorting an already-sorted list produces the same result), a brief comment would help future readers understand the intent.
Signed-off-by: Roxanne0321 <liruoxvan020321@qq.com> Assisted-by: Codex:gpt-5
LHT129
left a comment
There was a problem hiding this comment.
Review Summary
This is a follow-up review on the latest commit 2cb63679. The PR implements value-aware term pruning for SINDI with posting-list sorting, threshold-based scan limits, and legacy data normalization.
Previously reported issues still present
[critical] CalcDistanceById and CalDistanceById still construct SparseTermComputer without window_num (previously reported by @LHT129 in discussion_r3656456807)
In sindi.cpp:
- Line 1981 (
CalcDistanceById):std::make_shared<SparseTermComputer>(sparse_query, search_param, allocator_)— defaults towindow_num=1 - Line 2067 (
CalDistanceById):std::make_shared<SparseTermComputer>(mapped_query, search_param, allocator_)— defaults towindow_num=1
Both KnnSearch (line 626) and RangeSearch (line 1145) correctly pass the actual window count. When term_retain_threshold is configured, these two code paths will not divide the threshold across windows, potentially scanning more postings than expected.
Resolved issues
- Posting-list sorting now uses
SortPostingListByValuewith in-place permutation, avoiding extra allocation for sorted copies. SortByValueinAdd()now correctly handles the case wherelast_affected_windowremains-1when all input vectors are rejected.deserialize_immutable_windownormalizes legacy posting lists on load viaSortPostingListByValue.trim_deserialized_trailing_windowscorrectly handles both mutable and immutable paths.sindi_analyzer.cppcorrectly passeswindow_numtoSparseTermComputer.
Overall
The implementation is solid. The only remaining concern is the window_num omission in CalcDistanceById/CalDistanceById, which was already flagged in a prior review.
LHT129
left a comment
There was a problem hiding this comment.
Review Summary (follow-up on 2cb6367)
[critical] CalcDistanceById and CalDistanceById still construct SparseTermComputer without window_num (previously reported by @LHT129)
In sindi.cpp line 1981 and line 2067, SparseTermComputer is constructed with the default window_num=1, while KnnSearch (line 626) and RangeSearch (line 1145) correctly pass the actual window count. When term_retain_threshold is configured, these two code paths will not divide the threshold across windows.
Other previously reported issues have been resolved. No new issues found in this commit.
LHT129
left a comment
There was a problem hiding this comment.
Review Summary (follow-up on commit 2cb6367)
[critical] CalcDistanceById and CalDistanceById still construct SparseTermComputer without window_num (previously reported by @LHT129)
In sindi.cpp line 1981 and line 2067, SparseTermComputer is constructed with the default window_num=1, while KnnSearch (line 626) and RangeSearch (line 1145) correctly pass the actual window count. When term_retain_threshold is configured, these two code paths will not divide the threshold across windows.
Other previously reported issues have been resolved. No new issues found in this commit.
vsag-bot
left a comment
There was a problem hiding this comment.
Automated inline review completed.
Review effort: high (982 changed lines across 25 files).
Submitted 5 inline comments.
Reviewed commit 2cb6367.
| window_term_list_.resize(populated_window_count); | ||
| } | ||
| for (int64_t window = first_affected_window; window <= last_affected_window; ++window) { | ||
| window_term_list_[window]->SortByValue(); |
There was a problem hiding this comment.
[P1] Avoid sorting complete windows after every Add
This invokes SortByValue() after every Add, and that method scans every term and runs a full std::sort over every non-empty posting list. Repeated single-document Adds sharing a term therefore redo prefixes of lengths 1 through N even when pruning is disabled; a 2,000-document debug probe took 1,426 ms versus 9 ms for one batch, and the allowed 60,000-document window magnifies this quadratic behavior. Track dirty suffixes or otherwise avoid rebuilding complete posting lists on each incremental Add.
| if (sparse_value_quant_type_ == SparseValueQuantizationType::SQ8) { | ||
| computer->ScanForAccumulateSQ8( | ||
| it, term_ids_[term]->data(), term_datas_[term]->data(), term_size, global_dists); | ||
| computer->ScanForAccumulateSQ8(it, term_ids, term_data, term_count, global_dists); |
There was a problem hiding this comment.
[P2] Choose the retained end from the query sign
Posting lists are descending, and this always scans from data(), although signed sparse values remain accepted. With base values 3 and -4, query value -1, and threshold 1, the unpruned search returns the -4 document while this path scans 3 and returns no result. Either reject signed indexed/query values before mutation or scan the low-value suffix for negative queries; immutable and heap-insertion paths need the same rule.
| } | ||
| } | ||
| CHECK_ARGUMENT( | ||
| std::none_of(values.begin(), values.end(), [](float value) { return std::isnan(value); }), |
There was a problem hiding this comment.
[P1] Validate NaN before committing Add state
Add reaches this check only during its post-insertion SortByValue() call, after postings, labels, element count, and optional rerank data have been committed. A one-term FP32 NaN Add returns an error but leaves GetNumElements() equal to 1, and subsequent normalization continues encountering the poisoned posting. Validate original values before SQ8 training/remapping/insertion, or roll back every committed structure when sorting fails.
| continue; | ||
| } | ||
|
|
||
| auto& ids = *term_ids_[term]; |
There was a problem hiding this comment.
[P1] Validate deserialized posting metadata before dereferencing
Deserialize() reads term_sizes_ without verifying that its length and non-empty entries match term_ids_, term_datas_, and their byte counts, then enters this loop. A payload with capacity 1, an empty term 0, and serialized sizes {0,1} reaches term 1 and dereferences term_ids_[1]; the focused probe exited with SIGSEGV. Reject inconsistent mutable payloads before calling SortByValue() instead of trusting these vectors to be aligned.
| } | ||
| } | ||
|
|
||
| SortByValue(); |
There was a problem hiding this comment.
[P2] Do not re-sort every current payload during load
Every payload written by this revision is already sorted because Add/Build normalizes postings, yet every mutable load unconditionally runs full sorting here; immutable loading has the same unconditional per-term normalization. Thus ordinary round trips, including configurations with pruning disabled, change from linear restoration to sum(m log m) comparisons and scratch work. Persist ordering metadata or perform a linear orderedness check so only legacy unsorted lists are normalized.
What
sindi.term_prune_ratioandsindi.term_prune_thresholdas peer search parametersterm_prune_threshold: 0as disabled; divide positive thresholds across populated index windows withthreshold / window_numAddavoids quadratic full-prefix reordering without incorrect pruningBuild, normalize deserialized posting lists, and trim historical trailing empty windows during legacy and streaming deserializationhgraph-90.yml,hgraph-95.yml, andhgraph-99.ymlso those configurations contain only HGraph casesDefaults
term_prune_ratio:0.0term_prune_threshold:0(disabled)Validation
unittestsandfunctestsSparseTermDatacell Basic Test: 230 assertionsSINDI DMQ Rerank Large Term ID Fallback Test: 3 assertionsSINDI DMQ Streaming Serialization Test: 22 assertionsSparseDmqDataCell implements FlattenInterface: 18 assertions[sindi]functional filter: 8 test cases and 498,982 assertionsgit diff --check