Summary
RvfStore::delete(ids) sets bits in deletion_bitmap. Those bits:
- Survive re-ingest of the same id —
ingest_batch inserts vector data but never clears the deletion bit.
- Cause
query() (and index paths) to skip the id while the bit remains set.
- Cause
compact() to treat the id as dead: it physically removes the vector — including a freshly re-ingested payload — then clears the bitmap.
So “delete then re-insert under the same id” is broken, and “delete → re-insert → compact” destroys the re-inserted data.
DeletionBitmap::clear_ids already exists in deletion.rs but is not called from the re-ingest path.
Affected versions
- Confirmed: 0.2.0 (
store.rs delete / ingest_batch / query / compact)
- Same interaction present in 0.3.0 (delete sets bitmap; query filters deleted; compact removes deleted_ids)
Repro
use rvf_runtime::{DistanceMetric, QueryOptions, RvfOptions, RvfStore};
fn main() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("del.rvf");
let opts = RvfOptions {
dimension: 2,
metric: DistanceMetric::L2,
..Default::default()
};
let mut s = RvfStore::create(&path, opts).unwrap();
let v = [1.0f32, 0.0];
s.ingest_batch(&[&v], &[42], None).unwrap();
s.delete(&[42]).unwrap();
let v2 = [0.0f32, 1.0];
s.ingest_batch(&[&v2], &[42], None).unwrap();
let hits = s.query(&[0.0, 1.0], 1, &QueryOptions::default()).unwrap();
// Actual: 42 not returned (still soft-deleted) — BUG
// Expected: 42 present with v2
s.compact().unwrap();
let hits2 = s.query(&[0.0, 1.0], 1, &QueryOptions::default()).unwrap();
// Actual: still absent — re-ingested payload reclaimed as dead — BUG
let _ = (hits, hits2);
}
Expected
On successful ingest_batch / upsert of an id, clear that id from deletion_bitmap (e.g. clear_ids). Document soft-delete vs re-ingest-as-undelete. compact should only drop ids still soft-deleted at compact time.
Downstream impact
WeftOS BranchableMemory::delete never calls RvfStore::delete on working tips; it uses a crate-level tombstone set so delete→re-ingest works and chain-walk query masking is correct. Promote-to-base still hits this sticky-bitmap path — a real correctness hazard.
Reported from WeftOS WEFT-662.
Summary
RvfStore::delete(ids)sets bits indeletion_bitmap. Those bits:ingest_batchinserts vector data but never clears the deletion bit.query()(and index paths) to skip the id while the bit remains set.compact()to treat the id as dead: it physically removes the vector — including a freshly re-ingested payload — then clears the bitmap.So “delete then re-insert under the same id” is broken, and “delete → re-insert → compact” destroys the re-inserted data.
DeletionBitmap::clear_idsalready exists indeletion.rsbut is not called from the re-ingest path.Affected versions
store.rsdelete / ingest_batch / query / compact)Repro
Expected
On successful
ingest_batch/ upsert of an id, clear that id fromdeletion_bitmap(e.g.clear_ids). Document soft-delete vs re-ingest-as-undelete.compactshould only drop ids still soft-deleted at compact time.Downstream impact
WeftOS
BranchableMemory::deletenever callsRvfStore::deleteon working tips; it uses a crate-level tombstone set so delete→re-ingest works and chain-walk query masking is correct. Promote-to-base still hits this sticky-bitmap path — a real correctness hazard.Reported from WeftOS WEFT-662.