mirror of
https://github.com/qdrant/qdrant.git
synced 2026-09-21 13:37:46 -05:00
feat: open dispatcher for VectorStorageReadEnum (#9337)
* feat: read-only open for ReadOnlyChunkedDenseVectorStorage Add ReadOnlyChunkedDenseVectorStorage::open, the read-only counterpart of open_appendable_memmap_vector_storage_impl: open the chunked vectors/ directory via ChunkedVectorsRead::open and materialize the deleted/ flags into an owned bitvec via DynamicStoredFlags::load_bitvec, threading every file open through the UniversalRead backend without writing anything. Share the vectors/ and deleted/ directory names with the writable storage by exposing them pub(crate). Covered by a round-trip test: write and delete through the writable storage, reopen read-only, and assert vectors, deletions and counts match. Part of the read-only vector-storage open constructors (#9241). * feat: read-only open for ReadOnlyChunkedMultiDenseVectorStorage Add ReadOnlyChunkedMultiDenseVectorStorage::open, the read-only counterpart of open_appendable_memmap_multi_vector_storage_impl: open the chunked vectors/ and offsets/ directories via ChunkedVectorsRead::open and materialize the deleted/ flags into an owned bitvec via DynamicStoredFlags::load_bitvec, threading every file open through the UniversalRead backend without writing anything. Share the vectors/, offsets/ and deleted/ directory names with the writable storage by exposing them pub(crate). Covered by a round-trip test: write and delete multivectors through the writable storage, reopen read-only, and assert per-point multivector contents, deletions and counts match. Part of the read-only vector-storage open constructors (#9241). * feat: read-only open for ReadOnlySparseVectorStorage Add ReadOnlySparseVectorStorage::open, the read-only counterpart of MmapSparseVectorStorage::open: open the Gridstore store/ directory via GridstoreReader::open and materialize the deleted/ flags into an owned bitvec via DynamicStoredFlags::load_bitvec, threading every file open through the UniversalRead backend without writing anything. next_point_offset is reconstructed the same way the writable storage does on reopen (highest deleted id or the Gridstore pointer count). Share the store/ and deleted/ directory names with the writable storage by exposing them pub(crate). Covered by a round-trip test: write and delete sparse vectors through the writable storage, reopen read-only, and assert live contents, deletions and the reconstructed point count match. Part of the read-only vector-storage open constructors (#9241). * feat: open dispatcher for VectorStorageReadEnum Add VectorStorageReadEnum::open, the read-only counterpart of open_vector_storage: route a VectorDataConfig to the matching read-only variant. The storage type selects the on-disk layout (mmap -> immutable DenseVectorStorageImpl, chunked-mmap -> appendable chunked storage), the datatype selects the element type, and a multivector config routes to the chunked multi-dense storage (mmap multivectors are appendable-only). advice/populate are derived from the storage type exactly as the writable path does. Expose open_dense_vector_storage_impl as pub(crate) so the dispatcher can build the immutable Dense* variants. Sparse storage keeps its own ReadOnlySparseVectorStorage::open and is wrapped at the call site. Covered by tests asserting each config routes to the expected variant and round-trips. Part of the read-only vector-storage open constructors (#9241).
This commit is contained in:
@@ -181,7 +181,7 @@ pub fn open_dense_vector_storage_byte(
|
||||
Ok(VectorStorageEnum::DenseMemmapByte(Box::new(mmap_storage)))
|
||||
}
|
||||
|
||||
fn open_dense_vector_storage_impl<T, S>(
|
||||
pub(crate) fn open_dense_vector_storage_impl<T, S>(
|
||||
fs: S::Fs,
|
||||
path: &Path,
|
||||
dim: usize,
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
use std::path::Path;
|
||||
|
||||
use common::bitvec::BitSlice;
|
||||
use common::generic_consts::AccessPattern;
|
||||
use common::mmap::{Advice, AdviceSetting};
|
||||
use common::types::PointOffsetType;
|
||||
use common::universal_io::UniversalRead;
|
||||
|
||||
use crate::common::operation_error::{OperationError, OperationResult};
|
||||
use crate::data_types::named_vectors::CowVector;
|
||||
use crate::data_types::vectors::{VectorElementType, VectorElementTypeByte, VectorElementTypeHalf};
|
||||
use crate::types::{Distance, VectorStorageDatatype};
|
||||
use crate::types::{Distance, VectorDataConfig, VectorStorageDatatype, VectorStorageType};
|
||||
use crate::vector_storage::VectorStorageRead;
|
||||
use crate::vector_storage::dense::dense_vector_storage::DenseVectorStorageImpl;
|
||||
use crate::vector_storage::dense::dense_vector_storage::{
|
||||
DenseVectorStorageImpl, open_dense_vector_storage_impl,
|
||||
};
|
||||
use crate::vector_storage::dense::read_only::chunked_vector_storage::ReadOnlyChunkedDenseVectorStorage;
|
||||
use crate::vector_storage::multi_dense::read_only::chunked_vector_storage::ReadOnlyChunkedMultiDenseVectorStorage;
|
||||
use crate::vector_storage::sparse::read_only::sparse_vector_storage::ReadOnlySparseVectorStorage;
|
||||
@@ -29,6 +35,120 @@ pub enum VectorStorageReadEnum<S: UniversalRead> {
|
||||
Sparse(Box<ReadOnlySparseVectorStorage<S>>),
|
||||
}
|
||||
|
||||
impl<S: UniversalRead> VectorStorageReadEnum<S> {
|
||||
/// Open the read-only counterpart of a dense vector storage from its
|
||||
/// `VectorDataConfig`, mirroring `open_vector_storage`. Sparse storages are
|
||||
/// opened separately via `ReadOnlySparseVectorStorage::open`.
|
||||
#[allow(dead_code)] // pending: read-only segment constructor will use this
|
||||
pub fn open(
|
||||
fs: &S::Fs,
|
||||
vector_config: &VectorDataConfig,
|
||||
path: &Path,
|
||||
) -> OperationResult<Option<Self>>
|
||||
where
|
||||
S::Fs: Clone,
|
||||
{
|
||||
let dim = vector_config.size;
|
||||
let distance = vector_config.distance;
|
||||
let datatype = vector_config.datatype.unwrap_or_default();
|
||||
|
||||
let (advice, populate, chunked) = match vector_config.storage_type {
|
||||
VectorStorageType::Mmap => (AdviceSetting::Global, false, false),
|
||||
VectorStorageType::InRamMmap => (AdviceSetting::from(Advice::Normal), true, false),
|
||||
VectorStorageType::ChunkedMmap => (AdviceSetting::Global, false, true),
|
||||
VectorStorageType::InRamChunkedMmap => {
|
||||
(AdviceSetting::from(Advice::Normal), true, true)
|
||||
}
|
||||
// No on-disk data to open for these storage types: no-op.
|
||||
VectorStorageType::Memory | VectorStorageType::Empty => return Ok(None),
|
||||
};
|
||||
|
||||
// Multivectors always use the appendable chunked layout.
|
||||
if vector_config.multivector_config.is_some() {
|
||||
return Ok(Some(match datatype {
|
||||
VectorStorageDatatype::Float32 => {
|
||||
Self::MultiDenseChunked(Box::new(ReadOnlyChunkedMultiDenseVectorStorage::open(
|
||||
fs, path, dim, distance, advice, populate,
|
||||
)?))
|
||||
}
|
||||
VectorStorageDatatype::Uint8 => Self::MultiDenseChunkedByte(Box::new(
|
||||
ReadOnlyChunkedMultiDenseVectorStorage::open(
|
||||
fs, path, dim, distance, advice, populate,
|
||||
)?,
|
||||
)),
|
||||
VectorStorageDatatype::Float16 => Self::MultiDenseChunkedHalf(Box::new(
|
||||
ReadOnlyChunkedMultiDenseVectorStorage::open(
|
||||
fs, path, dim, distance, advice, populate,
|
||||
)?,
|
||||
)),
|
||||
VectorStorageDatatype::Turbo4 => {
|
||||
return Err(OperationError::service_error(
|
||||
"Turbo4 datatype storage is not yet supported",
|
||||
));
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
// chunked-mmap is appendable; plain mmap is the immutable storage.
|
||||
Ok(Some(if chunked {
|
||||
match datatype {
|
||||
VectorStorageDatatype::Float32 => {
|
||||
Self::DenseChunked(Box::new(ReadOnlyChunkedDenseVectorStorage::open(
|
||||
fs, path, dim, distance, advice, populate,
|
||||
)?))
|
||||
}
|
||||
VectorStorageDatatype::Uint8 => {
|
||||
Self::DenseChunkedByte(Box::new(ReadOnlyChunkedDenseVectorStorage::open(
|
||||
fs, path, dim, distance, advice, populate,
|
||||
)?))
|
||||
}
|
||||
VectorStorageDatatype::Float16 => {
|
||||
Self::DenseChunkedHalf(Box::new(ReadOnlyChunkedDenseVectorStorage::open(
|
||||
fs, path, dim, distance, advice, populate,
|
||||
)?))
|
||||
}
|
||||
VectorStorageDatatype::Turbo4 => {
|
||||
return Err(OperationError::service_error(
|
||||
"Turbo4 datatype storage is not yet supported",
|
||||
));
|
||||
}
|
||||
}
|
||||
} 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::Turbo4 => {
|
||||
return Err(OperationError::service_error(
|
||||
"Turbo4 datatype storage is not yet supported",
|
||||
));
|
||||
}
|
||||
}
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: UniversalRead> VectorStorageRead for VectorStorageReadEnum<S> {
|
||||
fn distance(&self) -> Distance {
|
||||
match self {
|
||||
@@ -188,3 +308,209 @@ impl<S: UniversalRead> VectorStorageRead for VectorStorageReadEnum<S> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use common::counter::hardware_counter::HardwareCounterCell;
|
||||
use common::generic_consts::Random;
|
||||
use common::mmap::AdviceSetting;
|
||||
use common::universal_io::{MmapFile, MmapFs};
|
||||
use rand::rngs::StdRng;
|
||||
use rand::{RngExt, SeedableRng};
|
||||
use tempfile::Builder;
|
||||
|
||||
use super::*;
|
||||
use crate::data_types::vectors::{
|
||||
DenseVector, MultiDenseVectorInternal, TypedMultiDenseVectorRef, VectorRef,
|
||||
};
|
||||
use crate::types::{Indexes, MultiVectorConfig};
|
||||
use crate::vector_storage::VectorStorage;
|
||||
use crate::vector_storage::dense::appendable_dense_vector_storage::open_appendable_memmap_vector_storage_impl;
|
||||
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::multi_dense::appendable_mmap_multi_dense_vector_storage::open_appendable_memmap_multi_vector_storage_impl;
|
||||
|
||||
const DIM: usize = 16;
|
||||
|
||||
fn dense_config(
|
||||
storage_type: VectorStorageType,
|
||||
multivector_config: Option<MultiVectorConfig>,
|
||||
) -> VectorDataConfig {
|
||||
VectorDataConfig {
|
||||
size: DIM,
|
||||
distance: Distance::Dot,
|
||||
storage_type,
|
||||
index: Indexes::Plain {},
|
||||
quantization_config: None,
|
||||
multivector_config,
|
||||
datatype: Some(VectorStorageDatatype::Float32),
|
||||
}
|
||||
}
|
||||
|
||||
fn rand_vec(rng: &mut StdRng) -> DenseVector {
|
||||
std::iter::repeat_with(|| rng.random_range(-1.0..1.0))
|
||||
.take(DIM)
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// `Memory` and `Empty` storage types are a no-op: nothing to open read-only.
|
||||
#[test]
|
||||
fn open_memory_and_empty_are_noop() {
|
||||
let dir = Builder::new().prefix("disp_noop").tempdir().unwrap();
|
||||
for storage_type in [VectorStorageType::Memory, VectorStorageType::Empty] {
|
||||
let opened = VectorStorageReadEnum::<MmapFile>::open(
|
||||
&MmapFs,
|
||||
&dense_config(storage_type, None),
|
||||
dir.path(),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(opened.is_none(), "{storage_type:?} should be a no-op");
|
||||
}
|
||||
}
|
||||
|
||||
/// `ChunkedMmap` single-dense routes to the chunked read-only storage.
|
||||
#[test]
|
||||
fn open_routes_chunked_mmap_to_dense_chunked() {
|
||||
let dir = Builder::new().prefix("disp_chunked").tempdir().unwrap();
|
||||
let mut rng = StdRng::seed_from_u64(1);
|
||||
let hw = HardwareCounterCell::disposable();
|
||||
let vectors: Vec<DenseVector> = (0..300).map(|_| rand_vec(&mut rng)).collect();
|
||||
|
||||
{
|
||||
let mut storage = open_appendable_memmap_vector_storage_impl::<VectorElementType>(
|
||||
dir.path(),
|
||||
DIM,
|
||||
Distance::Dot,
|
||||
AdviceSetting::Global,
|
||||
false,
|
||||
)
|
||||
.unwrap();
|
||||
for (id, vector) in vectors.iter().enumerate() {
|
||||
storage
|
||||
.insert_vector(id as PointOffsetType, VectorRef::from(vector), &hw)
|
||||
.unwrap();
|
||||
}
|
||||
storage.flusher()().unwrap();
|
||||
}
|
||||
|
||||
let storage = VectorStorageReadEnum::<MmapFile>::open(
|
||||
&MmapFs,
|
||||
&dense_config(VectorStorageType::ChunkedMmap, None),
|
||||
dir.path(),
|
||||
)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
assert!(matches!(storage, VectorStorageReadEnum::DenseChunked(_)));
|
||||
assert_eq!(storage.total_vector_count(), vectors.len());
|
||||
let got: DenseVector = storage
|
||||
.get_vector::<Random>(7)
|
||||
.to_owned()
|
||||
.try_into()
|
||||
.unwrap();
|
||||
assert_eq!(got, vectors[7]);
|
||||
}
|
||||
|
||||
/// `Mmap` single-dense config routes to the immutable `DenseVectorStorageImpl`.
|
||||
#[test]
|
||||
fn open_routes_mmap_to_dense() {
|
||||
let dir = Builder::new().prefix("disp_mmap").tempdir().unwrap();
|
||||
let mut rng = StdRng::seed_from_u64(2);
|
||||
let hw = HardwareCounterCell::disposable();
|
||||
let vectors: Vec<DenseVector> = (0..3).map(|_| rand_vec(&mut rng)).collect();
|
||||
|
||||
{
|
||||
// The immutable mmap storage is built by copying from another storage.
|
||||
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();
|
||||
}
|
||||
let mut iter = (0..vectors.len() as PointOffsetType).map(|i| {
|
||||
(
|
||||
staging.get_vector::<Random>(i),
|
||||
staging.is_deleted_vector(i),
|
||||
)
|
||||
});
|
||||
storage.update_from(&mut iter, &Default::default()).unwrap();
|
||||
storage.flusher()().unwrap();
|
||||
}
|
||||
|
||||
let storage = VectorStorageReadEnum::<MmapFile>::open(
|
||||
&MmapFs,
|
||||
&dense_config(VectorStorageType::Mmap, None),
|
||||
dir.path(),
|
||||
)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
assert!(matches!(storage, VectorStorageReadEnum::Dense(_)));
|
||||
assert_eq!(storage.total_vector_count(), vectors.len());
|
||||
let got: DenseVector = storage
|
||||
.get_vector::<Random>(1)
|
||||
.to_owned()
|
||||
.try_into()
|
||||
.unwrap();
|
||||
assert_eq!(got, vectors[1]);
|
||||
}
|
||||
|
||||
/// A multivector config routes to the chunked multi-dense read-only storage.
|
||||
#[test]
|
||||
fn open_routes_multivector_to_multi_dense_chunked() {
|
||||
let dir = Builder::new().prefix("disp_multi").tempdir().unwrap();
|
||||
let mut rng = StdRng::seed_from_u64(3);
|
||||
let hw = HardwareCounterCell::disposable();
|
||||
let multis: Vec<MultiDenseVectorInternal> = (0..200)
|
||||
.map(|_| {
|
||||
let inner = rng.random_range(1..=3);
|
||||
let vectors = std::iter::repeat_with(|| rand_vec(&mut rng))
|
||||
.take(inner)
|
||||
.collect::<Vec<_>>();
|
||||
MultiDenseVectorInternal::try_from(vectors).unwrap()
|
||||
})
|
||||
.collect();
|
||||
|
||||
{
|
||||
let mut storage =
|
||||
open_appendable_memmap_multi_vector_storage_impl::<VectorElementType>(
|
||||
dir.path(),
|
||||
DIM,
|
||||
Distance::Dot,
|
||||
MultiVectorConfig::default(),
|
||||
AdviceSetting::Global,
|
||||
false,
|
||||
)
|
||||
.unwrap();
|
||||
for (id, multivec) in multis.iter().enumerate() {
|
||||
storage
|
||||
.insert_vector(id as PointOffsetType, VectorRef::from(multivec), &hw)
|
||||
.unwrap();
|
||||
}
|
||||
storage.flusher()().unwrap();
|
||||
}
|
||||
|
||||
let storage = VectorStorageReadEnum::<MmapFile>::open(
|
||||
&MmapFs,
|
||||
&dense_config(
|
||||
VectorStorageType::ChunkedMmap,
|
||||
Some(MultiVectorConfig::default()),
|
||||
),
|
||||
dir.path(),
|
||||
)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
assert!(matches!(
|
||||
storage,
|
||||
VectorStorageReadEnum::MultiDenseChunked(_)
|
||||
));
|
||||
assert_eq!(storage.total_vector_count(), multis.len());
|
||||
let stored = storage.get_vector::<Random>(5);
|
||||
let multi: TypedMultiDenseVectorRef<VectorElementType> =
|
||||
stored.as_vec_ref().try_into().unwrap();
|
||||
assert_eq!(multi.to_owned(), multis[5]);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user