mirror of
https://github.com/qdrant/qdrant.git
synced 2026-08-05 17:40:55 -05:00
feat: add live-reload for immutable dense vector (#9392)
This commit is contained in:
@@ -66,6 +66,14 @@ impl InMemoryBitvecFlags {
|
||||
Ok(Self { bitvec, count })
|
||||
}
|
||||
|
||||
/// Wrap an already-materialized deletion `bitvec`, computing the set-flag
|
||||
/// count. For flags coming from an on-disk format other than the dynamic
|
||||
/// 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 }
|
||||
}
|
||||
|
||||
/// Whether the flag at `key` is set; out-of-range keys read as unset.
|
||||
pub fn get(&self, key: PointOffsetType) -> bool {
|
||||
self.bitvec.get(key as usize).is_some_and(|bit| *bit)
|
||||
|
||||
@@ -25,8 +25,8 @@ use crate::vector_storage::{
|
||||
DenseVectorStorage, DenseVectorStorageRead, VectorStorage, VectorStorageEnum, VectorStorageRead,
|
||||
};
|
||||
|
||||
const VECTORS_PATH: &str = "matrix.dat";
|
||||
const DELETED_PATH: &str = "deleted.dat";
|
||||
pub(crate) const VECTORS_PATH: &str = "matrix.dat";
|
||||
pub(crate) const DELETED_PATH: &str = "deleted.dat";
|
||||
|
||||
/// Stores all dense vectors in mem-mapped file
|
||||
///
|
||||
@@ -216,7 +216,7 @@ where
|
||||
S: UniversalRead,
|
||||
{
|
||||
fn vector_dim(&self) -> usize {
|
||||
self.vectors.as_ref().unwrap().dim
|
||||
self.vectors.as_ref().unwrap().dim()
|
||||
}
|
||||
|
||||
fn get_dense<P: AccessPattern>(&self, key: PointOffsetType) -> Cow<'_, [T]> {
|
||||
@@ -244,7 +244,7 @@ where
|
||||
stopped: &AtomicBool,
|
||||
) -> OperationResult<Range<PointOffsetType>> {
|
||||
let dim = self.vector_dim();
|
||||
let start_index = self.vectors.as_ref().unwrap().num_vectors as PointOffsetType;
|
||||
let start_index = self.vectors.as_ref().unwrap().num_vectors() as PointOffsetType;
|
||||
let mut end_index = start_index;
|
||||
|
||||
// Extend vectors file, write other vectors into it
|
||||
@@ -318,7 +318,7 @@ where
|
||||
}
|
||||
|
||||
fn total_vector_count(&self) -> usize {
|
||||
self.vectors.as_ref().unwrap().num_vectors
|
||||
self.vectors.as_ref().unwrap().num_vectors()
|
||||
}
|
||||
|
||||
fn get_vector<P: AccessPattern>(&self, key: PointOffsetType) -> CowVector<'_> {
|
||||
|
||||
@@ -26,9 +26,11 @@ const HEADER_SIZE: usize = 4;
|
||||
const VECTORS_HEADER: &[u8; HEADER_SIZE] = b"data";
|
||||
const DELETED_HEADER: &[u8; HEADER_SIZE] = b"drop";
|
||||
|
||||
/// Immutable storage for dense vectors.
|
||||
/// Immutable dense vector blob, shared by the writable [`ImmutableDenseVectors`]
|
||||
/// and the read-only dense storage. Provides typed read access for `T` through
|
||||
/// the [`UniversalRead`] backend `S`; holds no deletion flags.
|
||||
#[derive(Debug)]
|
||||
pub struct ImmutableDenseVectors<T, S = MmapFile>
|
||||
pub struct ImmutableDenseVectorData<T, S = MmapFile>
|
||||
where
|
||||
T: PrimitiveVectorElement,
|
||||
S: UniversalRead,
|
||||
@@ -37,24 +39,17 @@ where
|
||||
pub num_vectors: usize,
|
||||
/// Vector data storage, providing typed read access for `T`.
|
||||
storage: TypedStorage<ReadOnly<S>, T>,
|
||||
/// Memory mapped deletion flags
|
||||
deleted: MmapBitSlice,
|
||||
/// Current number of deleted vectors.
|
||||
pub deleted_count: usize,
|
||||
}
|
||||
|
||||
impl<T: PrimitiveVectorElement, S: UniversalRead> ImmutableDenseVectors<T, S> {
|
||||
impl<T: PrimitiveVectorElement, S: UniversalRead> ImmutableDenseVectorData<T, S> {
|
||||
/// Open the immutable vector blob read-only through `fs`. The file must
|
||||
/// already exist (the writer creates it); nothing is created here.
|
||||
pub fn open(
|
||||
fs: &S::Fs,
|
||||
vectors_path: &Path,
|
||||
deleted_path: &Path,
|
||||
dim: usize,
|
||||
populate: bool,
|
||||
) -> OperationResult<Self> {
|
||||
// Allocate/open vectors file
|
||||
ensure_mmap_file_size(vectors_path, VECTORS_HEADER, None)
|
||||
.describe("Create mmap data file")?;
|
||||
|
||||
let file_len = fs_err::metadata(vectors_path)?.len() as usize;
|
||||
let num_vectors = file_len.saturating_sub(HEADER_SIZE) / dim / size_of::<T>();
|
||||
|
||||
@@ -73,36 +68,13 @@ impl<T: PrimitiveVectorElement, S: UniversalRead> ImmutableDenseVectors<T, S> {
|
||||
})?;
|
||||
let storage = TypedStorage::<ReadOnly<S>, T>::wrap(read_only);
|
||||
|
||||
// Allocate/open deleted mmap
|
||||
let deleted_mmap_size = deleted_mmap_size(num_vectors);
|
||||
ensure_mmap_file_size(deleted_path, DELETED_HEADER, Some(deleted_mmap_size as u64))
|
||||
.describe("Create mmap deleted file")?;
|
||||
let deleted_mmap = mmap::open_write_mmap(deleted_path, AdviceSetting::Global, false)
|
||||
.describe("Open mmap deleted for writing")?;
|
||||
|
||||
// Advise kernel that we'll need this page soon so the kernel can prepare
|
||||
#[cfg(unix)]
|
||||
if let Err(err) = deleted_mmap.advise(memmap2::Advice::WillNeed) {
|
||||
log::error!("Failed to advise MADV_WILLNEED for deleted flags: {err}");
|
||||
}
|
||||
|
||||
// Transform into mmap BitSlice
|
||||
let deleted = MmapBitSlice::try_from(deleted_mmap, deleted_mmap_data_start())?;
|
||||
let deleted_count = deleted.count_ones();
|
||||
|
||||
Ok(Self {
|
||||
dim,
|
||||
num_vectors,
|
||||
storage,
|
||||
deleted,
|
||||
deleted_count,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn flusher(&self) -> MmapFlusher {
|
||||
self.deleted.flusher()
|
||||
}
|
||||
|
||||
/// Returns the byte offset within the file at which the vector for `key` begins.
|
||||
///
|
||||
/// File layout:
|
||||
@@ -201,6 +173,97 @@ impl<T: PrimitiveVectorElement, S: UniversalRead> ImmutableDenseVectors<T, S> {
|
||||
.expect("vectors read");
|
||||
}
|
||||
|
||||
pub fn populate(&self) {
|
||||
if let Err(err) = self.storage.populate() {
|
||||
log::error!("Failed to populate vector storage: {err}");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clear_cache(&self) -> OperationResult<()> {
|
||||
self.storage.clear_ram_cache()?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Immutable storage for dense vectors.
|
||||
///
|
||||
/// Wraps the shared [`ImmutableDenseVectorData`] blob with a writable deletion
|
||||
/// bitmap, so it can mark vectors as removed even though the vector data itself
|
||||
/// is append-only and can only be constructed from another storage.
|
||||
#[derive(Debug)]
|
||||
pub struct ImmutableDenseVectors<T, S = MmapFile>
|
||||
where
|
||||
T: PrimitiveVectorElement,
|
||||
S: UniversalRead,
|
||||
{
|
||||
/// Vector data blob, read-only through `S`.
|
||||
data: ImmutableDenseVectorData<T, S>,
|
||||
/// Memory mapped deletion flags
|
||||
deleted: MmapBitSlice,
|
||||
/// Current number of deleted vectors.
|
||||
pub deleted_count: usize,
|
||||
}
|
||||
|
||||
impl<T: PrimitiveVectorElement, S: UniversalRead> ImmutableDenseVectors<T, S> {
|
||||
pub fn open(
|
||||
fs: &S::Fs,
|
||||
vectors_path: &Path,
|
||||
deleted_path: &Path,
|
||||
dim: usize,
|
||||
populate: bool,
|
||||
) -> OperationResult<Self> {
|
||||
// Allocate/open vectors file
|
||||
ensure_mmap_file_size(vectors_path, VECTORS_HEADER, None)
|
||||
.describe("Create mmap data file")?;
|
||||
|
||||
let data = ImmutableDenseVectorData::open(fs, vectors_path, dim, populate)?;
|
||||
let num_vectors = data.num_vectors;
|
||||
|
||||
// Allocate/open deleted mmap
|
||||
let deleted_mmap_size = deleted_mmap_size(num_vectors);
|
||||
ensure_mmap_file_size(deleted_path, DELETED_HEADER, Some(deleted_mmap_size as u64))
|
||||
.describe("Create mmap deleted file")?;
|
||||
let deleted_mmap = mmap::open_write_mmap(deleted_path, AdviceSetting::Global, false)
|
||||
.describe("Open mmap deleted for writing")?;
|
||||
|
||||
// Advise kernel that we'll need this page soon so the kernel can prepare
|
||||
#[cfg(unix)]
|
||||
if let Err(err) = deleted_mmap.advise(memmap2::Advice::WillNeed) {
|
||||
log::error!("Failed to advise MADV_WILLNEED for deleted flags: {err}");
|
||||
}
|
||||
|
||||
// Transform into mmap BitSlice
|
||||
let deleted = MmapBitSlice::try_from(deleted_mmap, deleted_mmap_data_start())?;
|
||||
let deleted_count = deleted.count_ones();
|
||||
|
||||
Ok(Self {
|
||||
data,
|
||||
deleted,
|
||||
deleted_count,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn dim(&self) -> usize {
|
||||
self.data.dim
|
||||
}
|
||||
|
||||
pub fn num_vectors(&self) -> usize {
|
||||
self.data.num_vectors
|
||||
}
|
||||
|
||||
pub fn flusher(&self) -> MmapFlusher {
|
||||
self.deleted.flusher()
|
||||
}
|
||||
|
||||
/// Returns an optional vector data by key
|
||||
pub fn get_vector_opt<P: AccessPattern>(&self, key: PointOffsetType) -> Option<Cow<'_, [T]>> {
|
||||
self.data.get_vector_opt::<P>(key)
|
||||
}
|
||||
|
||||
pub fn for_each_in_batch<F: FnMut(usize, &[T])>(&self, keys: &[PointOffsetType], f: F) {
|
||||
self.data.for_each_in_batch(keys, f);
|
||||
}
|
||||
|
||||
/// Marks the key as deleted.
|
||||
///
|
||||
/// Returns true if the key was not deleted before, and it is now deleted.
|
||||
@@ -225,21 +288,12 @@ impl<T: PrimitiveVectorElement, S: UniversalRead> ImmutableDenseVectors<T, S> {
|
||||
}
|
||||
|
||||
pub fn populate(&self) {
|
||||
if let Err(err) = self.storage.populate() {
|
||||
log::error!("Failed to populate vector storage: {err}");
|
||||
}
|
||||
self.data.populate();
|
||||
}
|
||||
|
||||
pub fn clear_cache(&self) -> OperationResult<()> {
|
||||
let Self {
|
||||
dim: _,
|
||||
num_vectors: _,
|
||||
storage,
|
||||
deleted,
|
||||
deleted_count: _,
|
||||
} = self;
|
||||
storage.clear_ram_cache()?;
|
||||
deleted.clear_cache()?;
|
||||
self.data.clear_cache()?;
|
||||
self.deleted.clear_cache()?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -273,7 +327,7 @@ fn ensure_mmap_file_size(path: &Path, header: &[u8], size: Option<u64>) -> Opera
|
||||
|
||||
/// Get start position of flags `BitSlice` in deleted mmap.
|
||||
#[inline]
|
||||
const fn deleted_mmap_data_start() -> usize {
|
||||
pub(crate) const fn deleted_mmap_data_start() -> usize {
|
||||
let align = mem::align_of::<usize>();
|
||||
HEADER_SIZE.div_ceil(align) * align
|
||||
}
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
use std::path::Path;
|
||||
|
||||
use common::bitvec::BitVec;
|
||||
use common::mmap::AdviceSetting;
|
||||
use common::stored_bitslice::StoredBitSlice;
|
||||
use common::universal_io::{OpenOptions, Populate, UniversalRead};
|
||||
|
||||
use super::ReadOnlyImmutableDenseVectorStorage;
|
||||
use crate::common::flags::in_memory_bitvec_flags::InMemoryBitvecFlags;
|
||||
use crate::common::operation_error::{OperationError, OperationResult};
|
||||
use crate::data_types::primitive::PrimitiveVectorElement;
|
||||
use crate::types::Distance;
|
||||
use crate::vector_storage::dense::dense_vector_storage::{DELETED_PATH, VECTORS_PATH};
|
||||
use crate::vector_storage::dense::immutable_dense_vectors::{
|
||||
ImmutableDenseVectorData, deleted_mmap_data_start,
|
||||
};
|
||||
|
||||
/// Read-only mmap options: never writable, lazily paged, nothing populated.
|
||||
const READ_ONLY_OPTIONS: OpenOptions = OpenOptions {
|
||||
writeable: false,
|
||||
need_sequential: false,
|
||||
populate: Populate::No,
|
||||
advice: AdviceSetting::Global,
|
||||
};
|
||||
|
||||
impl<T: PrimitiveVectorElement, S: UniversalRead> ReadOnlyImmutableDenseVectorStorage<T, S> {
|
||||
/// Open the read-only counterpart of the immutable dense storage at `path`,
|
||||
/// threading every file open through `fs`; reads the existing layout but
|
||||
/// creates and writes nothing. `populate` warms the vector data.
|
||||
pub fn open(
|
||||
fs: &S::Fs,
|
||||
path: &Path,
|
||||
dim: usize,
|
||||
distance: Distance,
|
||||
populate: bool,
|
||||
) -> OperationResult<Self> {
|
||||
let vectors = ImmutableDenseVectorData::open(fs, &path.join(VECTORS_PATH), dim, populate)?;
|
||||
let deleted = open_deleted_flags::<S>(fs, &path.join(DELETED_PATH), vectors.num_vectors)?;
|
||||
|
||||
Ok(Self {
|
||||
vectors,
|
||||
deleted,
|
||||
distance,
|
||||
populate,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Read the immutable storage's `deleted.dat` through `fs` into an in-memory flag
|
||||
/// set. The file holds a header padded to `deleted_mmap_data_start()` bytes
|
||||
/// followed by the deletion `BitSlice`; the leading header bits are dropped and
|
||||
/// `num_vectors` deletion flags are kept.
|
||||
fn open_deleted_flags<S: UniversalRead>(
|
||||
fs: &S::Fs,
|
||||
deleted_path: &Path,
|
||||
num_vectors: usize,
|
||||
) -> OperationResult<InMemoryBitvecFlags> {
|
||||
let stored =
|
||||
StoredBitSlice::<S>::open(fs, deleted_path, READ_ONLY_OPTIONS, Default::default())?;
|
||||
let all = stored.read_all()?;
|
||||
|
||||
let start = deleted_mmap_data_start() * 8;
|
||||
let bits = all.get(start..start + num_vectors).ok_or_else(|| {
|
||||
OperationError::service_error(format!(
|
||||
"Deleted flags file {} holds fewer than {num_vectors} bits",
|
||||
deleted_path.display(),
|
||||
))
|
||||
})?;
|
||||
|
||||
Ok(InMemoryBitvecFlags::from_bitvec(BitVec::from_bitslice(
|
||||
bits,
|
||||
)))
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
use common::counter::hardware_counter::HardwareCounterCell;
|
||||
use common::sorted_slice::SortedSlice;
|
||||
use common::types::PointOffsetType;
|
||||
use common::universal_io::UniversalRead;
|
||||
|
||||
use super::ReadOnlyImmutableDenseVectorStorage;
|
||||
use crate::common::live_reload::LiveReload;
|
||||
use crate::common::operation_error::OperationResult;
|
||||
use crate::data_types::primitive::PrimitiveVectorElement;
|
||||
|
||||
impl<T: PrimitiveVectorElement, S: UniversalRead> LiveReload
|
||||
for ReadOnlyImmutableDenseVectorStorage<T, S>
|
||||
{
|
||||
type Fs = S::Fs;
|
||||
|
||||
/// Vector data is immutable, so only the in-memory deletion flags are patched
|
||||
/// from the authoritative `deleted_points`; `fs` and `new_points` are unused.
|
||||
fn live_reload(
|
||||
&mut self,
|
||||
_fs: &S::Fs,
|
||||
deleted_points: &SortedSlice<'_, PointOffsetType>,
|
||||
_new_points: &SortedSlice<'_, PointOffsetType>,
|
||||
_hw_counter: &HardwareCounterCell,
|
||||
) -> OperationResult<()> {
|
||||
self.deleted.insert_all(deleted_points);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
181
lib/segment/src/vector_storage/dense/read_only/immutable/mod.rs
Normal file
181
lib/segment/src/vector_storage/dense/read_only/immutable/mod.rs
Normal file
@@ -0,0 +1,181 @@
|
||||
use common::universal_io::UniversalRead;
|
||||
|
||||
use crate::common::flags::in_memory_bitvec_flags::InMemoryBitvecFlags;
|
||||
use crate::data_types::primitive::PrimitiveVectorElement;
|
||||
use crate::types::Distance;
|
||||
use crate::vector_storage::dense::immutable_dense_vectors::ImmutableDenseVectorData;
|
||||
|
||||
mod lifecycle;
|
||||
mod live_reload;
|
||||
mod read_ops;
|
||||
|
||||
/// Read-only counterpart of the immutable (mmap) dense vector storage.
|
||||
///
|
||||
/// Vector data is immutable, so it is read straight from the shared
|
||||
/// [`ImmutableDenseVectorData`] blob; deletions are tracked in memory and folded
|
||||
/// from the live-reload delta.
|
||||
#[derive(Debug)]
|
||||
pub struct ReadOnlyImmutableDenseVectorStorage<T: PrimitiveVectorElement, S: UniversalRead> {
|
||||
vectors: ImmutableDenseVectorData<T, S>,
|
||||
/// Flags marking deleted vectors.
|
||||
deleted: InMemoryBitvecFlags,
|
||||
distance: Distance,
|
||||
/// Whether vector data is populated into RAM (drives `is_on_disk`).
|
||||
populate: bool,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use common::counter::hardware_counter::HardwareCounterCell;
|
||||
use common::generic_consts::Random;
|
||||
use common::sorted_slice::SortedSlice;
|
||||
use common::types::PointOffsetType;
|
||||
use common::universal_io::{MmapFile, MmapFs};
|
||||
use rand::rngs::StdRng;
|
||||
use rand::{RngExt, SeedableRng};
|
||||
use tempfile::Builder;
|
||||
|
||||
use super::*;
|
||||
use crate::common::live_reload::LiveReload;
|
||||
use crate::data_types::vectors::{DenseVector, VectorElementType, VectorRef};
|
||||
use crate::segment_constructor::batched_reader::merge_from_single_source;
|
||||
use crate::vector_storage::dense::dense_vector_storage::open_dense_vector_storage;
|
||||
use crate::vector_storage::dense::volatile_dense_vector_storage::new_volatile_dense_vector_storage;
|
||||
use crate::vector_storage::{VectorStorage, VectorStorageRead};
|
||||
|
||||
fn rand_vec(rng: &mut StdRng, dim: usize) -> DenseVector {
|
||||
std::iter::repeat_with(|| rng.random_range(-1.0..1.0))
|
||||
.take(dim)
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Build the immutable storage (deleting some) from a staging storage, then
|
||||
/// reopen the same directory read-only and assert it mirrors the state.
|
||||
#[test]
|
||||
fn read_only_immutable_dense_round_trip() {
|
||||
const DIM: usize = 32;
|
||||
const POINT_COUNT: PointOffsetType = 300;
|
||||
|
||||
let dir = Builder::new()
|
||||
.prefix("ro_immutable_dense")
|
||||
.tempdir()
|
||||
.unwrap();
|
||||
let mut rng = StdRng::seed_from_u64(42);
|
||||
let hw = HardwareCounterCell::disposable();
|
||||
|
||||
let vectors: Vec<DenseVector> = (0..POINT_COUNT).map(|_| rand_vec(&mut rng, DIM)).collect();
|
||||
|
||||
let mut deleted_ids = Vec::new();
|
||||
{
|
||||
let mut storage =
|
||||
open_dense_vector_storage(dir.path(), DIM, Distance::Dot, false).unwrap();
|
||||
let mut staging = new_volatile_dense_vector_storage(DIM, Distance::Dot);
|
||||
for (id, vector) in vectors.iter().enumerate() {
|
||||
staging
|
||||
.insert_vector(id as PointOffsetType, VectorRef::from(vector), &hw)
|
||||
.unwrap();
|
||||
}
|
||||
for id in (0..POINT_COUNT).step_by(11) {
|
||||
staging.delete_vector(id).unwrap();
|
||||
deleted_ids.push(id);
|
||||
}
|
||||
merge_from_single_source(&mut storage, &staging, POINT_COUNT).unwrap();
|
||||
storage.flusher()().unwrap();
|
||||
}
|
||||
|
||||
let storage = ReadOnlyImmutableDenseVectorStorage::<VectorElementType, MmapFile>::open(
|
||||
&MmapFs,
|
||||
dir.path(),
|
||||
DIM,
|
||||
Distance::Dot,
|
||||
false,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(storage.total_vector_count(), POINT_COUNT as usize);
|
||||
assert_eq!(storage.distance(), Distance::Dot);
|
||||
assert_eq!(storage.deleted_vector_count(), deleted_ids.len());
|
||||
|
||||
// The immutable storage keeps deleted vectors' data, so every vector reads back.
|
||||
for id in 0..POINT_COUNT {
|
||||
assert_eq!(storage.is_deleted_vector(id), deleted_ids.contains(&id));
|
||||
let got: DenseVector = storage
|
||||
.get_vector::<Random>(id)
|
||||
.to_owned()
|
||||
.try_into()
|
||||
.unwrap();
|
||||
assert_eq!(got, vectors[id as usize], "vector {id} mismatch");
|
||||
}
|
||||
}
|
||||
|
||||
/// After `live_reload`, deletions made through the writer are reflected;
|
||||
/// vector data is immutable so nothing else changes.
|
||||
#[test]
|
||||
fn live_reload_picks_up_deletions() {
|
||||
const DIM: usize = 24;
|
||||
const POINT_COUNT: PointOffsetType = 200;
|
||||
|
||||
let dir = Builder::new()
|
||||
.prefix("ro_immutable_dense_reload")
|
||||
.tempdir()
|
||||
.unwrap();
|
||||
let mut rng = StdRng::seed_from_u64(7);
|
||||
let hw = HardwareCounterCell::disposable();
|
||||
|
||||
let vectors: Vec<DenseVector> = (0..POINT_COUNT).map(|_| rand_vec(&mut rng, DIM)).collect();
|
||||
|
||||
let mut writer = open_dense_vector_storage(dir.path(), DIM, Distance::Dot, false).unwrap();
|
||||
{
|
||||
let mut staging = new_volatile_dense_vector_storage(DIM, Distance::Dot);
|
||||
for (id, vector) in vectors.iter().enumerate() {
|
||||
staging
|
||||
.insert_vector(id as PointOffsetType, VectorRef::from(vector), &hw)
|
||||
.unwrap();
|
||||
}
|
||||
merge_from_single_source(&mut writer, &staging, POINT_COUNT).unwrap();
|
||||
writer.flusher()().unwrap();
|
||||
}
|
||||
|
||||
let mut reader = ReadOnlyImmutableDenseVectorStorage::<VectorElementType, MmapFile>::open(
|
||||
&MmapFs,
|
||||
dir.path(),
|
||||
DIM,
|
||||
Distance::Dot,
|
||||
false,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(reader.deleted_vector_count(), 0);
|
||||
|
||||
// Delete points through the writer, then feed the delta to the reader.
|
||||
let deleted_ids: Vec<PointOffsetType> = vec![1, 7, 42, 199];
|
||||
for &id in &deleted_ids {
|
||||
writer.delete_vector(id).unwrap();
|
||||
}
|
||||
writer.flusher()().unwrap();
|
||||
|
||||
reader
|
||||
.live_reload(
|
||||
&MmapFs,
|
||||
&SortedSlice::new(&deleted_ids).unwrap(),
|
||||
&SortedSlice::new(&[]).unwrap(),
|
||||
&hw,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(reader.total_vector_count(), POINT_COUNT as usize);
|
||||
assert_eq!(reader.deleted_vector_count(), deleted_ids.len());
|
||||
for &id in &deleted_ids {
|
||||
assert!(reader.is_deleted_vector(id));
|
||||
}
|
||||
assert!(!reader.is_deleted_vector(0));
|
||||
assert!(!reader.is_deleted_vector(2));
|
||||
|
||||
// Vector data is immutable: an untouched point still reads back correctly.
|
||||
let got: DenseVector = reader
|
||||
.get_vector::<Random>(2)
|
||||
.to_owned()
|
||||
.try_into()
|
||||
.unwrap();
|
||||
assert_eq!(got, vectors[2]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
use std::borrow::Cow;
|
||||
|
||||
use common::bitvec::BitSlice;
|
||||
use common::generic_consts::AccessPattern;
|
||||
use common::types::PointOffsetType;
|
||||
use common::universal_io::UniversalRead;
|
||||
|
||||
use super::ReadOnlyImmutableDenseVectorStorage;
|
||||
use crate::data_types::named_vectors::CowVector;
|
||||
use crate::data_types::primitive::PrimitiveVectorElement;
|
||||
use crate::types::{Distance, VectorStorageDatatype};
|
||||
use crate::vector_storage::{DenseVectorStorageRead, VectorStorageRead};
|
||||
|
||||
impl<T: PrimitiveVectorElement, S: UniversalRead> DenseVectorStorageRead<T>
|
||||
for ReadOnlyImmutableDenseVectorStorage<T, S>
|
||||
{
|
||||
fn vector_dim(&self) -> usize {
|
||||
self.vectors.dim
|
||||
}
|
||||
|
||||
fn get_dense<P: AccessPattern>(&self, key: PointOffsetType) -> Cow<'_, [T]> {
|
||||
self.vectors
|
||||
.get_vector_opt::<P>(key)
|
||||
.expect("vector not found")
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: PrimitiveVectorElement, S: UniversalRead> VectorStorageRead
|
||||
for ReadOnlyImmutableDenseVectorStorage<T, S>
|
||||
{
|
||||
fn size_of_available_vectors_in_bytes(&self) -> usize {
|
||||
self.available_vector_count() * self.vector_dim() * std::mem::size_of::<T>()
|
||||
}
|
||||
|
||||
fn distance(&self) -> Distance {
|
||||
self.distance
|
||||
}
|
||||
|
||||
fn datatype(&self) -> VectorStorageDatatype {
|
||||
T::datatype()
|
||||
}
|
||||
|
||||
fn is_on_disk(&self) -> bool {
|
||||
!self.populate
|
||||
}
|
||||
|
||||
fn total_vector_count(&self) -> usize {
|
||||
self.vectors.num_vectors
|
||||
}
|
||||
|
||||
fn get_vector<P: AccessPattern>(&self, key: PointOffsetType) -> CowVector<'_> {
|
||||
self.vectors
|
||||
.get_vector_opt::<P>(key)
|
||||
.map(|vector| T::slice_to_float_cow(vector).into())
|
||||
.expect("Vector not found")
|
||||
}
|
||||
|
||||
fn read_vectors<P: AccessPattern, U: Copy>(
|
||||
&self,
|
||||
keys: impl IntoIterator<Item = (U, PointOffsetType)>,
|
||||
mut callback: impl FnMut(U, PointOffsetType, CowVector<'_>),
|
||||
) {
|
||||
// Split into parallel arrays in one pass: `for_each_in_batch` needs an
|
||||
// offsets slice, but we still want `user_data[idx]` in the callback.
|
||||
let (user_data, point_offsets): (Vec<U>, Vec<PointOffsetType>) = keys.into_iter().unzip();
|
||||
|
||||
self.vectors
|
||||
.for_each_in_batch(&point_offsets, |idx, vector| {
|
||||
let vector = CowVector::from(T::slice_to_float_cow(Cow::Borrowed(vector)));
|
||||
callback(user_data[idx], point_offsets[idx], vector);
|
||||
});
|
||||
}
|
||||
|
||||
fn get_vector_opt<P: AccessPattern>(&self, key: PointOffsetType) -> Option<CowVector<'_>> {
|
||||
self.vectors
|
||||
.get_vector_opt::<P>(key)
|
||||
.map(|vector| T::slice_to_float_cow(vector).into())
|
||||
}
|
||||
|
||||
fn is_deleted_vector(&self, key: PointOffsetType) -> bool {
|
||||
self.deleted.get(key)
|
||||
}
|
||||
|
||||
fn deleted_vector_count(&self) -> usize {
|
||||
self.deleted.count()
|
||||
}
|
||||
|
||||
fn deleted_vector_bitslice(&self) -> &BitSlice {
|
||||
self.deleted.as_bitslice()
|
||||
}
|
||||
}
|
||||
@@ -5,10 +5,13 @@ use crate::data_types::primitive::PrimitiveVectorElement;
|
||||
use crate::types::Distance;
|
||||
use crate::vector_storage::chunked_vectors::ChunkedVectorsRead;
|
||||
|
||||
mod immutable;
|
||||
mod lifecycle;
|
||||
mod live_reload;
|
||||
mod read_ops;
|
||||
|
||||
pub use immutable::ReadOnlyImmutableDenseVectorStorage;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ReadOnlyChunkedDenseVectorStorage<T: PrimitiveVectorElement, S: UniversalRead> {
|
||||
vectors: ChunkedVectorsRead<T, S>,
|
||||
|
||||
@@ -5,10 +5,10 @@ use common::universal_io::UniversalRead;
|
||||
|
||||
use super::VectorStorageReadEnum;
|
||||
use crate::common::operation_error::{OperationError, OperationResult};
|
||||
use crate::data_types::vectors::{VectorElementType, VectorElementTypeByte, VectorElementTypeHalf};
|
||||
use crate::types::{VectorDataConfig, VectorStorageDatatype, VectorStorageType};
|
||||
use crate::vector_storage::dense::dense_vector_storage::open_dense_vector_storage_impl;
|
||||
use crate::vector_storage::dense::read_only::ReadOnlyChunkedDenseVectorStorage;
|
||||
use crate::vector_storage::dense::read_only::{
|
||||
ReadOnlyChunkedDenseVectorStorage, ReadOnlyImmutableDenseVectorStorage,
|
||||
};
|
||||
use crate::vector_storage::multi_dense::read_only::ReadOnlyChunkedMultiDenseVectorStorage;
|
||||
|
||||
impl<S: UniversalRead> VectorStorageReadEnum<S> {
|
||||
@@ -20,10 +20,7 @@ impl<S: UniversalRead> VectorStorageReadEnum<S> {
|
||||
fs: &S::Fs,
|
||||
vector_config: &VectorDataConfig,
|
||||
path: &Path,
|
||||
) -> OperationResult<Option<Self>>
|
||||
where
|
||||
S::Fs: Clone,
|
||||
{
|
||||
) -> OperationResult<Option<Self>> {
|
||||
let dim = vector_config.size;
|
||||
let distance = vector_config.distance;
|
||||
let datatype = vector_config.datatype.unwrap_or_default();
|
||||
@@ -109,30 +106,15 @@ impl<S: UniversalRead> VectorStorageReadEnum<S> {
|
||||
}
|
||||
} else {
|
||||
match datatype {
|
||||
VectorStorageDatatype::Float32 => {
|
||||
Self::Dense(Box::new(open_dense_vector_storage_impl::<
|
||||
VectorElementType,
|
||||
S,
|
||||
>(
|
||||
fs.clone(), path, dim, distance, populate
|
||||
)?))
|
||||
}
|
||||
VectorStorageDatatype::Uint8 => {
|
||||
Self::DenseByte(Box::new(open_dense_vector_storage_impl::<
|
||||
VectorElementTypeByte,
|
||||
S,
|
||||
>(
|
||||
fs.clone(), path, dim, distance, populate
|
||||
)?))
|
||||
}
|
||||
VectorStorageDatatype::Float16 => {
|
||||
Self::DenseHalf(Box::new(open_dense_vector_storage_impl::<
|
||||
VectorElementTypeHalf,
|
||||
S,
|
||||
>(
|
||||
fs.clone(), path, dim, distance, populate
|
||||
)?))
|
||||
}
|
||||
VectorStorageDatatype::Float32 => Self::Dense(Box::new(
|
||||
ReadOnlyImmutableDenseVectorStorage::open(fs, path, dim, distance, populate)?,
|
||||
)),
|
||||
VectorStorageDatatype::Uint8 => Self::DenseByte(Box::new(
|
||||
ReadOnlyImmutableDenseVectorStorage::open(fs, path, dim, distance, populate)?,
|
||||
)),
|
||||
VectorStorageDatatype::Float16 => Self::DenseHalf(Box::new(
|
||||
ReadOnlyImmutableDenseVectorStorage::open(fs, path, dim, distance, populate)?,
|
||||
)),
|
||||
VectorStorageDatatype::Turbo4 => {
|
||||
return Err(OperationError::service_error(
|
||||
"Turbo4 datatype storage is not yet supported",
|
||||
|
||||
@@ -18,12 +18,14 @@ impl<S: UniversalRead> LiveReload for VectorStorageReadEnum<S> {
|
||||
hw_counter: &HardwareCounterCell,
|
||||
) -> OperationResult<()> {
|
||||
match self {
|
||||
// Immutable dense (mmap) live-reload is postponed: needs the deleted
|
||||
// flags threaded through `ImmutableDenseVectors` (Dense* step).
|
||||
VectorStorageReadEnum::Dense(_)
|
||||
| VectorStorageReadEnum::DenseByte(_)
|
||||
| VectorStorageReadEnum::DenseHalf(_) => {
|
||||
todo!("live_reload for immutable dense (mmap) storage is not yet implemented")
|
||||
VectorStorageReadEnum::Dense(s) => {
|
||||
s.live_reload(fs, deleted_points, new_points, hw_counter)
|
||||
}
|
||||
VectorStorageReadEnum::DenseByte(s) => {
|
||||
s.live_reload(fs, deleted_points, new_points, hw_counter)
|
||||
}
|
||||
VectorStorageReadEnum::DenseHalf(s) => {
|
||||
s.live_reload(fs, deleted_points, new_points, hw_counter)
|
||||
}
|
||||
VectorStorageReadEnum::DenseChunked(s) => {
|
||||
s.live_reload(fs, deleted_points, new_points, hw_counter)
|
||||
|
||||
@@ -5,8 +5,9 @@ use crate::common::operation_error::OperationResult;
|
||||
use crate::data_types::vectors::{
|
||||
QueryVector, VectorElementType, VectorElementTypeByte, VectorElementTypeHalf,
|
||||
};
|
||||
use crate::vector_storage::dense::dense_vector_storage::DenseVectorStorageImpl;
|
||||
use crate::vector_storage::dense::read_only::ReadOnlyChunkedDenseVectorStorage;
|
||||
use crate::vector_storage::dense::read_only::{
|
||||
ReadOnlyChunkedDenseVectorStorage, ReadOnlyImmutableDenseVectorStorage,
|
||||
};
|
||||
use crate::vector_storage::multi_dense::read_only::ReadOnlyChunkedMultiDenseVectorStorage;
|
||||
use crate::vector_storage::sparse::read_only::ReadOnlySparseVectorStorage;
|
||||
use crate::vector_storage::{
|
||||
@@ -22,9 +23,9 @@ mod read_ops;
|
||||
/// Wraps each on-disk storage type with its [`super`] read-only variant.
|
||||
/// Volatile, empty and test-only variants are intentionally absent.
|
||||
pub enum VectorStorageReadEnum<S: UniversalRead> {
|
||||
Dense(Box<DenseVectorStorageImpl<VectorElementType, S>>),
|
||||
DenseByte(Box<DenseVectorStorageImpl<VectorElementTypeByte, S>>),
|
||||
DenseHalf(Box<DenseVectorStorageImpl<VectorElementTypeHalf, S>>),
|
||||
Dense(Box<ReadOnlyImmutableDenseVectorStorage<VectorElementType, S>>),
|
||||
DenseByte(Box<ReadOnlyImmutableDenseVectorStorage<VectorElementTypeByte, S>>),
|
||||
DenseHalf(Box<ReadOnlyImmutableDenseVectorStorage<VectorElementTypeHalf, S>>),
|
||||
DenseChunked(Box<ReadOnlyChunkedDenseVectorStorage<VectorElementType, S>>),
|
||||
DenseChunkedByte(Box<ReadOnlyChunkedDenseVectorStorage<VectorElementTypeByte, S>>),
|
||||
DenseChunkedHalf(Box<ReadOnlyChunkedDenseVectorStorage<VectorElementTypeHalf, S>>),
|
||||
|
||||
Reference in New Issue
Block a user