Skip to content

Implement SVSIndex::getDataByLabel (MOD-17706) - #1033

Merged
nonirosenfeldredis merged 5 commits into
mainfrom
sharon-svs-getdatabylabel
Sep 8, 2026
Merged

Implement SVSIndex::getDataByLabel (MOD-17706)#1033
nonirosenfeldredis merged 5 commits into
mainfrom
sharon-svs-getdatabylabel

Conversation

@nonirosenfeldredis

@nonirosenfeldredis nonirosenfeldredis commented Sep 6, 2026

Copy link
Copy Markdown
Collaborator

Describe the changes in the pull request

SVSIndex::getDataByLabel was a stub that always reported nothing where VecSimTieredIndex::getDataByLabel had a bypass for SVS backend.

It now reads the stored vectors directly from the underlying SVS index (single-value via
get_datum, multi-value via get_label_to_external_lookup + get_parent_index), refusing only for
compressed/LeanVec-reduced storage (storage_traits_t::is_compressed()) -- where it reports nothing.

The read logic is shared with the existing test-only getStoredVectorDataByLabel via a new private
appendStoredDataByLabel helper, so the single/multi-value branching and the raw memcpy live in one
place.

Tiered read logic

VecSimTieredIndex::getDataByLabel always reads the flat buffer first, then decides whether the
backend also needs reading: for a single-value label the buffer alone is the whole answer once it
has contributed anything, but a multi-value label's vectors are routinely split across the tiers
while an ingest job is pending, so the backend is always consulted too in that case.

A compressed SVS backend complicates that decision: it appends nothing from getDataByLabel
regardless of whether it holds part of the label -- it cannot report values, which is not the
same as "doesn't have any". Unlike HNSW, whose tiered backend can never be quantized, SVS's
routinely is, so a multi-value label split between a non-empty buffer contribution and such a
backend would otherwise come back as just the buffer's subset, indistinguishable from the complete
answer. Before trusting that subset, the tiered read checks backend membership via
SVSIndexBase::isLabelExists -- cheap because it's metadata, not the value read compression rules
out -- and if the backend is compressed and holds part of the label, discards the buffer's
contribution too and reports nothing for the whole label, the same rule SVSIndex::getDataByLabel
already applies within a single tier.

Which issues this PR fixes

  1. MOD-17706

Main objects this PR modified

  1. src/VecSim/algorithms/svs/svs.h
  2. src/VecSim/vec_sim_tiered_index.h
  3. tests/unit/test_svs.cpp
  4. tests/unit/test_svs_tiered.cpp

Mark if applicable

  • This PR introduces API changes
  • This PR introduces serialization changes

Note

Medium Risk
Changes observable getDataByLabel behavior for SVS and tiered SVS (including RediSearch “holds vectors” paths); logic for compressed backends and multi-value labels split across tiers is easy to get wrong.

Overview
Implements SVSIndex::getDataByLabel so uncompressed SVS indexes return the stored vectors for a label (single-value via get_datum, multi-value via label lookup + parent index). Compressed or LeanVec-reduced indexes still append nothing, matching HNSW—no dequantized bytes that could fool equality checks.

The read path is centralized in a new appendStoredDataByLabel helper, also used by the test-only getStoredVectorDataByLabel, with compression gated on storage_traits_t::is_compressed() instead of separate quant flags.

Tiered getDataByLabel no longer skips all backend reads for SVS. It always consults the flat buffer first, then the backend when needed (multi-value or label not fully in the buffer). For a compressed SVS backend, if the buffer contributed vectors but the backend also holds that label (isLabelExists), the tiered read drops the buffer result and returns nothing so a partial subset is not mistaken for the full label.

Unit tests cover uncompressed vs compressed, single vs multi, absent labels, tiered flat-only reads, tiered backend reads after ingest, and the compressed split-label case.

Reviewed by Cursor Bugbot for commit ee9745e. Bugbot is set up for automated code reviews on this repo. Configure here.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale Bugbot comment from a previous run.

Comment thread src/VecSim/vec_sim_tiered_index.h
@codecov

codecov Bot commented Sep 6, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.05882% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 97.40%. Comparing base (08b33bf) to head (ee9745e).

Files with missing lines Patch % Lines
src/VecSim/vec_sim_tiered_index.h 92.85% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1033      +/-   ##
==========================================
- Coverage   97.41%   97.40%   -0.01%     
==========================================
  Files         141      141              
  Lines        8688     8698      +10     
==========================================
+ Hits         8463     8472       +9     
- Misses        225      226       +1     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

// Whether the buffer held it, measured rather than read off emptiness, so the tier
// decision does not depend on an assertion that only exists in test builds.
if (this->backendIndex->isMultiValue() || vectors_output.size() == before_flat) {
std::shared_lock<std::shared_mutex> main_lock(this->mainIndexGuard);

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.

For a compressed SVS backend with no buffer contribution, this acquires mainIndexGuard even though the result must be empty. It can wait behind a batch’s exclusive addVectors lock, then immediately return nothing from SVSIndex::getDataByLabel. While waiting, it also holds flatIndexGuard shared, potentially delaying buffer writes.
Could we resolve svs_backend before acquiring the main lock and return early when isCompressed() && vectors_output.size() == before_flat?
The multi-value case with a buffer contribution should retain the locked isLabelExists check, since we still need to determine whether the buffer represents the complete label.

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.

done

Comment thread src/VecSim/algorithms/svs/svs.h Outdated
Comment on lines +759 to +761
std::vector<OutputElement> vec_data(this->getStoredDataSize() / sizeof(OutputElement));
const char *data_ptr = reinterpret_cast<const char *>(indexed_span.data());
std::memcpy(vec_data.data(), data_ptr, this->getStoredDataSize());

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.

Consider adding a same type optimization (if OutputElement and DataType),
We can create vec_data directly in vectors_output.
Something like:

auto append_datum = [&](auto indexed_span) {
    if constexpr (std::is_same_v<OutputElement, DataType>) {
        vectors_output.emplace_back(indexed_span.begin(), indexed_span.end());
    } else {
        // Existing implementation, preserved for FP16 and byte output.
        std::vector<OutputElement> vec_data(
            this->getStoredDataSize() / sizeof(OutputElement));
        std::memcpy(vec_data.data(), indexed_span.data(), this->getStoredDataSize());
        vectors_output.push_back(std::move(vec_data));
    }
};

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.

done

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit fa5b616. Configure here.

Comment thread src/VecSim/vec_sim_tiered_index.h
nonirosenfeldredis and others added 5 commits September 8, 2026 15:42
SVSIndex::getDataByLabel was a stub that always reported nothing, so a
tiered SVS index could never learn a label's stored vectors -- the special
case documented on VecSimTieredIndex::getDataByLabel and the TODO left on
the stub itself.

It now reads the stored vectors directly from the underlying SVS index
(single-value via get_datum, multi-value via
get_label_to_external_lookup + get_parent_index), refusing only for
compressed/LeanVec-reduced storage, per storage_traits_t::is_compressed()
-- the same "report nothing rather than dequantize" rule the HNSW
implementations already follow. That lets VecSimTieredIndex::getDataByLabel
drop its SVSIndexBase bypass and read both tiers unconditionally.

The read logic is shared with the existing test-only
getStoredVectorDataByLabel via a new private appendStoredDataByLabel
helper, so the single/multi-value branching and the raw memcpy live in one
place.

Tests cover both isMulti and quant_bits variants: compressed storage
reports nothing, uncompressed single- and multi-value report the stored
vectors in insertion order, and an absent label reports nothing. The
tiered test now expects the SVS backend to answer once a vector has been
ingested into it, plus the existing flat-buffer coverage.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
clang-format flagged two lines in appendStoredDataByLabel's comment and
the vec_data construction added in the previous commit.
Codex review on this PR: removing the SVS bypass in
VecSimTieredIndex::getDataByLabel made it always read the flat buffer, then
the backend for multi-value labels. A compressed SVS backend appends
nothing regardless of whether it holds part of the label -- it cannot
report values, not "doesn't have any" -- so a multi-value label split
across a non-empty buffer contribution and such a backend came back as
just the buffer's subset, indistinguishable from the complete answer.
Unlike HNSW, whose tiered backend can never be quantized, SVS's routinely
is, so this case is reachable in practice.

`SVSIndexBase::isLabelExists` is metadata, not the value read compression
rules out, so it is cheap to check membership before trusting the buffer
alone: if the backend is compressed and holds part of this label, the
whole answer becomes "cannot tell" instead of a partial one -- the same
rule `SVSIndex::getDataByLabel` already applies within a single tier.

Covered by a new multi-value + Quant_8 combination (not in SVSDataTypeSet,
which only pairs compression with single-value) that ingests one vector
into the compressed backend and leaves a second one for the same label in
the flat buffer, then asserts `getDataByLabel` reports nothing. Checked
against a build with the fix disabled before trusting it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Review comment on this PR: appendStoredDataByLabel's byte-copy path is
only needed when OutputElement differs from the stored span's element
type (DataType) -- FP16's vecsim_dt mapping, or the test-only char output.
For FLOAT32/FLOAT64, both getDataByLabel's real callers, OutputElement
*is* DataType, so the span can construct the output vector directly via
its begin()/end() iterators instead of a raw memcpy.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Review comment on this PR: with no buffer contribution, a compressed SVS
backend's getDataByLabel always appends nothing regardless of whether it
holds the label, so the eventual answer is already known empty. Waiting
on mainIndexGuard behind a batch update's exclusive hold just to confirm
that is pure overhead -- and the wait happens while still holding
flatIndexGuard shared, which a buffer writer needs exclusively.

svs_backend is now resolved before either lock, and a compressed backend
with no buffer contribution returns early right after the frontend read.
The multi-value, buffer-contribution case keeps the locked isLabelExists
check, since that's the one case where the buffer alone isn't known to
be the complete answer.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@nonirosenfeldredis
nonirosenfeldredis added this pull request to the merge queue Sep 8, 2026
Merged via the queue into main with commit fd9fe8d Sep 8, 2026
16 checks passed
@nonirosenfeldredis
nonirosenfeldredis deleted the sharon-svs-getdatabylabel branch September 8, 2026 15:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants