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:
Daniel Boros
2026-08-04 11:16:48 +02:00
committed by generall
parent 845ea36253
commit 8bd997e62a
2 changed files with 329 additions and 3 deletions
@@ -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,
+328 -2
View File
@@ -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]);
}
}