mirror of
https://github.com/qdrant/qdrant.git
synced 2026-07-23 11:11:00 -05:00
fix: fold appended points' deletion into read-only live_reload
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
use std::path::Path;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use common::bitvec::{BitSlice, BitVec};
|
||||
use common::mmap::AdviceSetting;
|
||||
@@ -14,10 +14,15 @@ use crate::common::operation_error::{OperationError, OperationResult};
|
||||
/// In-memory counterpart of `BitvecFlags`: persisted flags materialized into an
|
||||
/// owned `BitVec`, no write path.
|
||||
///
|
||||
/// Only consumer is the vector storages' `deleted` set, where the live-reload
|
||||
/// delta is authoritative (deleted offsets set, appended offsets live so absent).
|
||||
/// So unlike `ReadOnlyRoaringFlags` it keeps no file handle and never reopens —
|
||||
/// the owned bitvec is the whole state.
|
||||
/// The only consumer is the vector storages' `deleted` set. On live-reload the
|
||||
/// id-tracker delta supplies whole-point deletions ([`Self::insert_all`]), but
|
||||
/// an appended point can also carry a per-vector deletion recorded only in the
|
||||
/// on-disk flags file — a missing named vector is stored as a placeholder and
|
||||
/// its slot deleted. So the backing directory is retained and
|
||||
/// [`Self::reload_appended`] reopens the flags file to read the persisted bit
|
||||
/// of each appended offset. The set only ever grows, so a whole-point deletion
|
||||
/// already folded in but not yet flushed to the flags file is never lost to a
|
||||
/// re-read.
|
||||
#[derive(Debug)]
|
||||
#[allow(dead_code)] // pending: read-only vector storages will hold `deleted` as this
|
||||
pub struct InMemoryBitvecFlags {
|
||||
@@ -25,6 +30,9 @@ pub struct InMemoryBitvecFlags {
|
||||
bitvec: BitVec,
|
||||
/// Set-flag count, kept in sync with `bitvec`.
|
||||
count: usize,
|
||||
/// Backing directory of the dynamic flags file, so [`Self::reload_appended`]
|
||||
/// can reopen it. `None` for flags built via [`Self::from_bitvec`].
|
||||
directory: Option<PathBuf>,
|
||||
}
|
||||
|
||||
/// Read-only mmap options: never writable, lazily paged, nothing populated.
|
||||
@@ -90,7 +98,11 @@ impl InMemoryBitvecFlags {
|
||||
})?;
|
||||
let count = bitvec.count_ones();
|
||||
|
||||
Ok(Self { bitvec, count })
|
||||
Ok(Self {
|
||||
bitvec,
|
||||
count,
|
||||
directory: Some(directory.to_path_buf()),
|
||||
})
|
||||
}
|
||||
|
||||
/// Wrap an already-materialized deletion `bitvec`, computing the set-flag
|
||||
@@ -98,7 +110,11 @@ impl InMemoryBitvecFlags {
|
||||
/// flags read by [`Self::open`] (e.g. the immutable dense `deleted.dat`).
|
||||
pub fn from_bitvec(bitvec: BitVec) -> Self {
|
||||
let count = bitvec.count_ones();
|
||||
Self { bitvec, count }
|
||||
Self {
|
||||
bitvec,
|
||||
count,
|
||||
directory: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the flag at `key` is set; out-of-range keys read as unset.
|
||||
@@ -129,6 +145,43 @@ impl InMemoryBitvecFlags {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Fold the persisted deletion bit of each appended offset into the set.
|
||||
///
|
||||
/// An appended point can carry a deleted vector slot whose flag lives only
|
||||
/// in the on-disk flags file, not the id-tracker delta (see the type doc).
|
||||
/// The flags file is reopened and the persisted bit of every `new_point` is
|
||||
/// read back; live offsets read unset and change nothing. A no-op for flags
|
||||
/// built via [`Self::from_bitvec`], which have no dynamic flags file.
|
||||
pub fn reload_appended<S: UniversalRead>(
|
||||
&mut self,
|
||||
fs: &impl UniversalReadFs<File = S>,
|
||||
new_points: &[PointOffsetType],
|
||||
) -> OperationResult<()> {
|
||||
let Some(directory) = self.directory.clone() else {
|
||||
return Ok(());
|
||||
};
|
||||
if new_points.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let flags = StoredBitSlice::<S>::open(
|
||||
fs,
|
||||
&directory.join(FLAGS_FILE),
|
||||
bitslice_open_options(Populate::No),
|
||||
Default::default(),
|
||||
)?;
|
||||
|
||||
let mut deleted = Vec::new();
|
||||
for &point in new_points {
|
||||
if flags.get_bit(u64::from(point))?.unwrap_or(false) {
|
||||
deleted.push(point);
|
||||
}
|
||||
}
|
||||
self.insert_all(&deleted);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::default_constructed_unit_structs)]
|
||||
|
||||
@@ -13,8 +13,9 @@ impl<T: PrimitiveVectorElement, S: UniversalRead> LiveReload
|
||||
{
|
||||
type Fs = S::Fs;
|
||||
|
||||
/// Reload the chunked vectors and apply `deleted_points`; appended points are
|
||||
/// served from the refreshed chunks, so `new_points` is unused.
|
||||
/// Reload the chunked vectors, apply `deleted_points`, and fold in the
|
||||
/// persisted deletion of each appended offset — a live point may have a
|
||||
/// deleted vector slot recorded only on disk.
|
||||
fn live_reload(
|
||||
&mut self,
|
||||
fs: &S::Fs,
|
||||
@@ -25,6 +26,7 @@ impl<T: PrimitiveVectorElement, S: UniversalRead> LiveReload
|
||||
self.vectors
|
||||
.live_reload(fs, deleted_points, new_points, hw_counter)?;
|
||||
self.deleted.insert_all(deleted_points);
|
||||
self.deleted.reload_appended::<S>(fs, new_points)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -194,4 +194,63 @@ mod tests {
|
||||
}
|
||||
assert!(!reader.is_deleted_vector(0));
|
||||
}
|
||||
|
||||
/// A point can be appended live while one of its named vectors is missing:
|
||||
/// the writer stores a placeholder and deletes that vector slot. The
|
||||
/// deletion is recorded only in the on-disk flags file, never in the
|
||||
/// id-tracker delta, so `live_reload` must read it back for the appended
|
||||
/// offset instead of assuming every appended point is live.
|
||||
#[test]
|
||||
fn live_reload_picks_up_appended_vector_deletion() {
|
||||
const DIM: usize = 4;
|
||||
let dir = Builder::new()
|
||||
.prefix("ro_dense_appended_deleted")
|
||||
.tempdir()
|
||||
.unwrap();
|
||||
let hw = HardwareCounterCell::disposable();
|
||||
|
||||
let mut writer = open_appendable_memmap_vector_storage_impl::<VectorElementType>(
|
||||
dir.path(),
|
||||
DIM,
|
||||
Distance::Dot,
|
||||
AdviceSetting::Global,
|
||||
false,
|
||||
)
|
||||
.unwrap();
|
||||
writer
|
||||
.insert_vector(0, VectorRef::from(&vec![1.0; DIM]), &hw)
|
||||
.unwrap();
|
||||
writer.flusher()().unwrap();
|
||||
|
||||
let mut reader = ReadOnlyChunkedDenseVectorStorage::<VectorElementType, MmapFile>::open(
|
||||
&MmapFs,
|
||||
dir.path(),
|
||||
DIM,
|
||||
Distance::Dot,
|
||||
AdviceSetting::Global,
|
||||
Populate::No,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Append offset 1 as a placeholder, then delete its vector slot.
|
||||
writer
|
||||
.insert_vector(1, VectorRef::from(&vec![0.0; DIM]), &hw)
|
||||
.unwrap();
|
||||
writer.delete_vector(1).unwrap();
|
||||
writer.flusher()().unwrap();
|
||||
|
||||
let deleted_ids: Vec<PointOffsetType> = vec![];
|
||||
let new_ids: Vec<PointOffsetType> = vec![1];
|
||||
reader
|
||||
.live_reload(
|
||||
&MmapFs,
|
||||
&SortedSlice::new(&deleted_ids).unwrap(),
|
||||
&SortedSlice::new(&new_ids).unwrap(),
|
||||
&hw,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(reader.total_vector_count(), 2);
|
||||
assert!(reader.is_deleted_vector(1));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,8 +13,9 @@ impl<T: PrimitiveVectorElement, S: UniversalRead> LiveReload
|
||||
{
|
||||
type Fs = S::Fs;
|
||||
|
||||
/// Reload the vectors and offsets and apply `deleted_points`; appended points
|
||||
/// are served from the refreshed chunks, so `new_points` is unused.
|
||||
/// Reload the vectors and offsets, apply `deleted_points`, and fold in the
|
||||
/// persisted deletion of each appended offset — a live point may have a
|
||||
/// deleted vector slot recorded only on disk.
|
||||
fn live_reload(
|
||||
&mut self,
|
||||
fs: &S::Fs,
|
||||
@@ -27,6 +28,7 @@ impl<T: PrimitiveVectorElement, S: UniversalRead> LiveReload
|
||||
self.offsets
|
||||
.live_reload(fs, deleted_points, new_points, hw_counter)?;
|
||||
self.deleted.insert_all(deleted_points);
|
||||
self.deleted.reload_appended::<S>(fs, new_points)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -257,4 +257,69 @@ mod tests {
|
||||
}
|
||||
assert!(!reader.is_deleted_vector(0));
|
||||
}
|
||||
|
||||
/// An appended multivector point can have its slot deleted (missing named
|
||||
/// vector → placeholder + delete); the deletion is recorded only in the
|
||||
/// on-disk flags file, never the id-tracker delta, so `live_reload` must
|
||||
/// read it back for the appended offset.
|
||||
#[test]
|
||||
fn live_reload_picks_up_appended_vector_deletion() {
|
||||
const DIM: usize = 8;
|
||||
let dir = Builder::new()
|
||||
.prefix("ro_multi_appended_deleted")
|
||||
.tempdir()
|
||||
.unwrap();
|
||||
let hw = HardwareCounterCell::disposable();
|
||||
|
||||
let multi = |value: VectorElementType| {
|
||||
MultiDenseVectorInternal::try_from(vec![vec![value; DIM]]).unwrap()
|
||||
};
|
||||
|
||||
let mut writer = open_appendable_memmap_multi_vector_storage_impl::<VectorElementType>(
|
||||
dir.path(),
|
||||
DIM,
|
||||
Distance::Dot,
|
||||
MultiVectorConfig::default(),
|
||||
AdviceSetting::Global,
|
||||
false,
|
||||
)
|
||||
.unwrap();
|
||||
writer
|
||||
.insert_vector(0, VectorRef::from(&multi(1.0)), &hw)
|
||||
.unwrap();
|
||||
writer.flusher()().unwrap();
|
||||
|
||||
let mut reader =
|
||||
ReadOnlyChunkedMultiDenseVectorStorage::<VectorElementType, MmapFile>::open(
|
||||
&MmapFs,
|
||||
dir.path(),
|
||||
DIM,
|
||||
Distance::Dot,
|
||||
MultiVectorConfig::default(),
|
||||
AdviceSetting::Global,
|
||||
Populate::No,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Append offset 1 as a placeholder, then delete its vector slot.
|
||||
writer
|
||||
.insert_vector(1, VectorRef::from(&multi(0.0)), &hw)
|
||||
.unwrap();
|
||||
writer.delete_vector(1).unwrap();
|
||||
writer.flusher()().unwrap();
|
||||
|
||||
let deleted_ids: Vec<PointOffsetType> = vec![];
|
||||
let new_ids: Vec<PointOffsetType> = vec![1];
|
||||
reader
|
||||
.live_reload(
|
||||
&MmapFs,
|
||||
&SortedSlice::new(&deleted_ids).unwrap(),
|
||||
&SortedSlice::new(&new_ids).unwrap(),
|
||||
&hw,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(reader.total_vector_count(), 2);
|
||||
assert!(reader.is_deleted_vector(1));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,18 +10,18 @@ use crate::common::operation_error::OperationResult;
|
||||
impl<S: UniversalRead> LiveReload for ReadOnlySparseVectorStorage<S> {
|
||||
type Fs = S::Fs;
|
||||
|
||||
/// Reload the Gridstore, apply `deleted_points`, and recompute
|
||||
/// `next_point_offset`; appended points come from the Gridstore, so
|
||||
/// `new_points` is unused.
|
||||
/// Reload the Gridstore, apply `deleted_points`, fold in the persisted
|
||||
/// deletion of each appended offset, and recompute `next_point_offset`.
|
||||
fn live_reload(
|
||||
&mut self,
|
||||
fs: &S::Fs,
|
||||
deleted_points: &SortedSlice<'_, PointOffsetType>,
|
||||
_new_points: &SortedSlice<'_, PointOffsetType>,
|
||||
new_points: &SortedSlice<'_, PointOffsetType>,
|
||||
_hw_counter: &HardwareCounterCell,
|
||||
) -> OperationResult<()> {
|
||||
self.storage.live_reload(fs)?;
|
||||
self.deleted.insert_all(deleted_points);
|
||||
self.deleted.reload_appended::<S>(fs, new_points)?;
|
||||
|
||||
self.next_point_offset = self
|
||||
.deleted
|
||||
|
||||
@@ -235,4 +235,53 @@ mod tests {
|
||||
}
|
||||
assert!(!reader.is_deleted_vector(0));
|
||||
}
|
||||
|
||||
/// An appended sparse point can have its slot deleted; the deletion is
|
||||
/// recorded only in the on-disk flags file, never the id-tracker delta, so
|
||||
/// `live_reload` must read it back for the appended offset.
|
||||
#[test]
|
||||
fn live_reload_picks_up_appended_vector_deletion() {
|
||||
let dir = Builder::new()
|
||||
.prefix("ro_sparse_appended_deleted")
|
||||
.tempdir()
|
||||
.unwrap();
|
||||
let hw = HardwareCounterCell::disposable();
|
||||
|
||||
fn make(id: usize) -> SparseVector {
|
||||
SparseVector {
|
||||
indices: vec![1, 5, 9],
|
||||
values: vec![id as f32 + 0.1, id as f32 + 0.2, id as f32 + 0.3],
|
||||
}
|
||||
}
|
||||
|
||||
let mut writer = MmapSparseVectorStorage::open_or_create(dir.path()).unwrap();
|
||||
writer
|
||||
.insert_vector(0, VectorRef::from(&make(0)), &hw)
|
||||
.unwrap();
|
||||
writer.flusher()().unwrap();
|
||||
|
||||
let mut reader =
|
||||
ReadOnlySparseVectorStorage::<MmapFile>::open(&MmapFs, dir.path(), Populate::No)
|
||||
.unwrap();
|
||||
|
||||
// Append offset 1, then delete it.
|
||||
writer
|
||||
.insert_vector(1, VectorRef::from(&make(1)), &hw)
|
||||
.unwrap();
|
||||
writer.delete_vector(1).unwrap();
|
||||
writer.flusher()().unwrap();
|
||||
|
||||
let deleted_ids: Vec<PointOffsetType> = vec![];
|
||||
let new_ids: Vec<PointOffsetType> = vec![1];
|
||||
reader
|
||||
.live_reload(
|
||||
&MmapFs,
|
||||
&SortedSlice::new(&deleted_ids).unwrap(),
|
||||
&SortedSlice::new(&new_ids).unwrap(),
|
||||
&hw,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert!(reader.is_deleted_vector(1));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,9 @@ impl<S: UniversalRead> LiveReload for ReadOnlyTurboVectorStorage<S> {
|
||||
type Fs = S::Fs;
|
||||
|
||||
/// Pick up vectors a writer appended (chunked backend only; the single-file
|
||||
/// layout is immutable) and patch the in-memory deletion flags.
|
||||
/// layout is immutable), patch the in-memory deletion flags, and fold in the
|
||||
/// persisted deletion of each appended offset — a live point may have a
|
||||
/// deleted vector slot recorded only on disk.
|
||||
fn live_reload(
|
||||
&mut self,
|
||||
fs: &S::Fs,
|
||||
@@ -23,6 +25,7 @@ impl<S: UniversalRead> LiveReload for ReadOnlyTurboVectorStorage<S> {
|
||||
storage.live_reload(fs, deleted_points, new_points, hw_counter)?;
|
||||
}
|
||||
self.deleted.insert_all(deleted_points);
|
||||
self.deleted.reload_appended::<S>(fs, new_points)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -30,8 +33,10 @@ impl<S: UniversalRead> LiveReload for ReadOnlyTurboVectorStorage<S> {
|
||||
impl<S: UniversalRead> LiveReload for ReadOnlyTurboMultiVectorStorage<S> {
|
||||
type Fs = S::Fs;
|
||||
|
||||
/// Pick up multivectors a writer appended (records + offsets) and patch the
|
||||
/// in-memory deletion flags.
|
||||
/// Pick up multivectors a writer appended (records + offsets), patch the
|
||||
/// in-memory deletion flags, and fold in the persisted deletion of each
|
||||
/// appended offset — a live point may have a deleted vector slot recorded
|
||||
/// only on disk.
|
||||
fn live_reload(
|
||||
&mut self,
|
||||
fs: &S::Fs,
|
||||
@@ -44,6 +49,7 @@ impl<S: UniversalRead> LiveReload for ReadOnlyTurboMultiVectorStorage<S> {
|
||||
self.offsets
|
||||
.live_reload(fs, deleted_points, new_points, hw_counter)?;
|
||||
self.deleted.insert_all(deleted_points);
|
||||
self.deleted.reload_appended::<S>(fs, new_points)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user