1use std::path::Path;
2use std::sync::Arc;
3
4use ndarray::{Array2, s};
5use rust_hdf5::{DatatypeMessage, H5Dataset, H5File, Hdf5Error};
6use zarrs::array::{Array, ArrayCreateError, ArraySubset};
7use zarrs::filesystem::FilesystemStore;
8use zarrs::storage::ReadableListableStorage;
9
10use crate::dtype::{EmbeddingDtype, dtype_of_array, read_subset_as_f32};
11use crate::error::{EcpError, Result, ResultExt};
12
13fn hdf5_err(e: Hdf5Error, context: &str) -> EcpError {
17 if matches!(&e, Hdf5Error::NotFound(_)) {
18 EcpError::NotFound(format!("{context}: {e}"))
19 } else {
20 EcpError::Store(format!("{context}: {e}"))
21 }
22}
23
24fn open_zarr_array(
27 store: &ReadableListableStorage,
28 path: &str,
29) -> Result<Array<dyn zarrs::storage::ReadableListableStorageTraits>> {
30 Array::open(store.clone(), path).map_err(|e| match e {
31 ArrayCreateError::MissingMetadata => EcpError::NotFound(format!("no array at {path}")),
32 other => EcpError::Store(format!("failed to open {path}: {other}")),
33 })
34}
35
36pub enum EmbeddingsSource {
40 Hdf5(H5Dataset),
41 Zarr {
42 store: ReadableListableStorage,
43 path: String,
44 },
45 Memory(Array2<f32>),
47}
48
49fn hdf5_dtype(dataset: &H5Dataset) -> Result<EmbeddingDtype> {
51 match dataset
52 .datatype()
53 .map_err(|e| hdf5_err(e, "failed to read HDF5 dataset datatype"))?
54 {
55 DatatypeMessage::FloatingPoint { size: 2, .. } => Ok(EmbeddingDtype::F16),
56 DatatypeMessage::FloatingPoint { size: 4, .. } => Ok(EmbeddingDtype::F32),
57 DatatypeMessage::FixedPoint {
58 size: 1,
59 signed: false,
60 ..
61 } => Ok(EmbeddingDtype::UInt8),
62 DatatypeMessage::FixedPoint {
63 size: 1,
64 signed: true,
65 ..
66 } => Ok(EmbeddingDtype::Int8),
67 other => Err(EcpError::InvalidInput(format!(
68 "unsupported embeddings dtype: {other:?} (use float32, float16, uint8 or int8)"
69 ))),
70 }
71}
72
73impl EmbeddingsSource {
74 pub fn open(path: &Path, name: &str) -> Result<Self> {
77 if !path.exists() {
78 return Err(EcpError::NotFound(format!(
79 "{} does not exist",
80 path.display()
81 )));
82 }
83 match path.extension().and_then(|ext| ext.to_str()) {
84 Some("h5") => {
85 let file =
86 H5File::open(path).map_err(|e| hdf5_err(e, "failed to open HDF5 file"))?;
87 let dataset = file
88 .dataset(name)
89 .map_err(|e| hdf5_err(e, "failed to open HDF5 dataset"))?;
90 Ok(EmbeddingsSource::Hdf5(dataset))
91 }
92 Some("zarr") => {
93 let store: ReadableListableStorage =
94 Arc::new(FilesystemStore::new(path).store_err("failed to open zarr store")?);
95 Ok(EmbeddingsSource::Zarr {
96 store,
97 path: format!("/{name}"),
98 })
99 }
100 other => Err(EcpError::InvalidInput(format!(
101 "unsupported embeddings file format: {other:?} (use \"h5\" or \"zarr\")"
102 ))),
103 }
104 }
105
106 pub fn from_zarr(store: ReadableListableStorage, path: String) -> Self {
108 EmbeddingsSource::Zarr { store, path }
109 }
110
111 pub fn shape(&self) -> Result<(usize, usize)> {
113 Ok(match self {
114 EmbeddingsSource::Hdf5(dataset) => {
115 let shape = dataset.shape();
116 (shape[0], shape[1])
117 }
118 EmbeddingsSource::Zarr { store, path } => {
119 let array = open_zarr_array(store, path)?;
120 let shape = array.shape();
121 (shape[0] as usize, shape[1] as usize)
122 }
123 EmbeddingsSource::Memory(embeddings) => (embeddings.nrows(), embeddings.ncols()),
124 })
125 }
126
127 pub fn natural_chunk_vecs(&self, fallback: usize) -> Result<usize> {
130 Ok(match self {
131 EmbeddingsSource::Hdf5(dataset) => {
132 dataset.chunk_dims().map(|dims| dims[0]).unwrap_or_else(|| {
133 log::warn!(
134 "HDF5 dataset has no chunking; using the fallback batch size of \
135 {fallback} rows"
136 );
137 fallback
138 })
139 }
140 EmbeddingsSource::Zarr { store, path } => {
141 let array = open_zarr_array(store, path)?;
142 array
143 .chunk_shape_usize(&[0, 0])
144 .store_err("failed to read zarr chunk shape")?[0]
145 }
146 EmbeddingsSource::Memory(_) => fallback,
147 })
148 }
149
150 pub fn chunk_aligned_batch_vecs(
154 &self,
155 memory_floor_vecs: usize,
156 fallback: usize,
157 ) -> Result<usize> {
158 let chunk_vecs = self.natural_chunk_vecs(fallback)?.max(1);
159 Ok(memory_floor_vecs.max(1).div_ceil(chunk_vecs) * chunk_vecs)
160 }
161
162 pub fn native_dtype(&self) -> Result<EmbeddingDtype> {
164 match self {
165 EmbeddingsSource::Hdf5(dataset) => hdf5_dtype(dataset),
166 EmbeddingsSource::Zarr { store, path } => {
167 let array = open_zarr_array(store, path)?;
168 dtype_of_array(&array, path)
169 }
170 EmbeddingsSource::Memory(_) => Ok(EmbeddingDtype::F32),
171 }
172 }
173
174 pub fn read_vecs(&self, start: usize, end: usize) -> Result<Array2<f32>> {
176 match self {
177 EmbeddingsSource::Hdf5(dataset) => {
178 let dim = dataset.shape()[1];
179 let rows = end - start;
180 let flat: Vec<f32> = match hdf5_dtype(dataset)? {
184 EmbeddingDtype::F16 | EmbeddingDtype::F32 => dataset
185 .read_numeric_slice_as::<f32>(&[start, 0], &[rows, dim])
186 .map_err(|e| hdf5_err(e, "failed to read HDF5 vec range"))?,
187 EmbeddingDtype::UInt8 => dataset
188 .read_numeric_slice_as::<u8>(&[start, 0], &[rows, dim])
189 .map_err(|e| hdf5_err(e, "failed to read HDF5 vec range"))?
190 .into_iter()
191 .map(|v| v as f32)
192 .collect(),
193 EmbeddingDtype::Int8 => dataset
194 .read_numeric_slice_as::<i8>(&[start, 0], &[rows, dim])
195 .map_err(|e| hdf5_err(e, "failed to read HDF5 vec range"))?
196 .into_iter()
197 .map(|v| v as f32)
198 .collect(),
199 };
200 Ok(Array2::from_shape_vec((rows, dim), flat)
201 .expect("HDF5 vec range didn't match its declared shape"))
202 }
203 EmbeddingsSource::Zarr { store, path } => {
204 let array = open_zarr_array(store, path)?;
205 let dim = array.shape()[1];
206 let subset = ArraySubset::new_with_ranges(&[start as u64..end as u64, 0..dim]);
207 read_subset_as_f32(&array, &subset, path)
208 }
209 EmbeddingsSource::Memory(embeddings) => {
210 Ok(embeddings.slice(s![start..end, ..]).to_owned())
211 }
212 }
213 }
214}
215
216#[cfg(test)]
217#[path = "utests/source.rs"]
218mod tests;