Skip to main content

genegraph_storage/
lance_storage_graph.rs

1//! Lance storage backend for graph embeddings.
2//!
3//! Async-first implementation that matches the async `StorageBackend` trait:
4//! - All I/O is async, no internal `block_on` or runtime creation.
5//! - Callers (CLI, tests, services) are responsible for providing a Tokio runtime.
6
7use std::path::{Path, PathBuf};
8use std::sync::Arc;
9
10use arrow::array::{Array as ArrowArray, Float64Array, UInt32Array};
11use arrow::datatypes::{DataType, Field, Schema};
12use arrow::record_batch::RecordBatch;
13use log::{debug, info};
14use smartcore::linalg::basic::arrays::Array;
15use smartcore::linalg::basic::matrix::DenseMatrix;
16use sprs::CsMat;
17
18use crate::metadata::FileInfo;
19use crate::metadata::GeneMetadata;
20use crate::traits::backend::StorageBackend;
21use crate::traits::lance::LanceStorage;
22use crate::traits::metadata::Metadata;
23use crate::{StorageError, StorageResult};
24
25/// Lance-based storage backend for ArrowSpace graph embeddings.
26///
27/// Stores dense and sparse matrices as Lance datasets using a columnar format
28/// (`row`, `col`, `value` for sparse; `col_*` for dense) schema for efficient
29/// random and columnar access.
30#[derive(Debug, Clone)]
31pub struct LanceStorageGraph {
32    pub(crate) _base: String,
33    pub(crate) _name: String,
34}
35
36impl LanceStorageGraph {
37    /// Creates a new Lance storage backend.
38    ///
39    /// This is used for on-the-fly creation. For proper setup use `Genefold<...>::seed`.
40    ///
41    /// # Arguments
42    ///
43    /// * `_base` - Base directory path for all storage files
44    /// * `_name` - Name prefix for this storage instance
45    pub fn new(_base: String, _name: String) -> Self {
46        info!("Creating LanceStorage at base={}, name={}", _base, _name);
47        Self { _base, _name }
48    }
49
50    /// Spawn a LanceStorage from an existing seeded directory (with metadata.json)
51    pub async fn spawn(base_path: String) -> Result<(Self, GeneMetadata), StorageError> {
52        // Reuse the generic `exists` helper from the StorageBackend trait
53        let (exists, md_path) = Self::exists(&base_path);
54
55        // Replace assert! with proper error handling
56        if !exists || md_path.is_none() {
57            return Err(StorageError::Invalid(format!(
58                "Metadata does not exist in base path: {}",
59                base_path
60            )));
61        }
62
63        // Load metadata from the discovered metadata.json
64        let metadata = GeneMetadata::read(md_path.unwrap()).await?;
65
66        // Construct the LanceStorage using the metadata-provided nameid
67        let storage = Self::new(base_path.clone(), metadata.name_id.clone());
68        Ok((storage, metadata))
69    }
70}
71
72impl LanceStorage for LanceStorageGraph {}
73
74impl StorageBackend for LanceStorageGraph {
75    fn get_base(&self) -> String {
76        self._base.clone()
77    }
78
79    fn get_name(&self) -> String {
80        self._name.clone()
81    }
82
83    fn base_path(&self) -> PathBuf {
84        PathBuf::from(&self._base)
85    }
86
87    fn metadata_path(&self) -> PathBuf {
88        self.base_path()
89            .join(format!("{}_metadata.json", self._name))
90    }
91
92    /// Converts the base path for the store to a `file://` URI for Lance.
93    fn basepath_to_uri(&self) -> String {
94        Self::path_to_uri(PathBuf::from(self._base.clone()).as_path())
95    }
96
97    /// Save dense matrix using Lance-optimized vector format.
98    ///
99    /// Each row of the matrix becomes a FixedSizeList entry for efficient vector operations.
100    /// This format is optimized for vector search and enables Lance's full-zip encoding.
101    ///
102    /// # Arguments
103    /// * `filename` - any name
104    /// * `matrix` - Dense matrix to save (N rows × F cols)
105    /// * `md_path` - Metadata file path for validation
106    async fn save_dense(
107        &self,
108        key: &str,
109        matrix: &DenseMatrix<f64>,
110        md_path: &Path,
111    ) -> StorageResult<()> {
112        self.validate_initialized(md_path)?;
113        let path = self.file_path(key);
114        let (n_rows, n_cols) = matrix.shape();
115
116        info!(
117            "Saving dense {} matrix: {} x {} at {:?}",
118            key, n_rows, n_cols, path
119        );
120
121        // Convert to Lance-optimized RecordBatch (FixedSizeList format)
122        let batch = self.to_dense_record_batch(matrix)?;
123
124        // Verify batch has correct number of rows
125        if batch.num_rows() != n_rows {
126            return Err(StorageError::Invalid(format!(
127                "RecordBatch has {} rows but matrix has {} rows",
128                batch.num_rows(),
129                n_rows
130            )));
131        }
132
133        {
134            // Write to Lance
135            let uri = Self::path_to_uri(&path);
136            self.write_lance_batch_async(uri, batch).await?;
137            let mut md = self.load_metadata().await?;
138            md = md.add_file(
139                key,
140                FileInfo::new(
141                    format!("{}_{}.lance", self.get_name(), key),
142                    "dense",
143                    matrix.shape(),
144                    None,
145                    None,
146                ),
147            );
148            self.save_metadata(&md).await?;
149            info!("Dense {} matrix saved successfully", key);
150        }
151        Ok(())
152    }
153
154    /// Load dense matrix from Lance-optimized vector format.
155    ///
156    /// Reads FixedSizeList vectors and reconstructs a column-major DenseMatrix.
157    ///
158    /// # Arguments
159    /// * `filename` - any name previously assigned
160    ///
161    /// # Returns
162    /// Column-major DenseMatrix matching smartcore conventions
163    async fn load_dense(&self, key: &str) -> StorageResult<DenseMatrix<f64>> {
164        let path = self.file_path(key);
165        info!("Loading dense {} matrix from {:?}", key, path);
166
167        // Read all batches from Lance (may span multiple batches for large datasets)
168        let uri = Self::path_to_uri(&path);
169        let batch = self.read_lance_all_batches_async(uri).await?;
170
171        // Convert from FixedSizeList format to DenseMatrix
172        let matrix = self.from_dense_record_batch(&batch)?;
173
174        let (n_rows, n_cols) = matrix.shape();
175        info!("Loaded dense {} matrix: {} x {}", key, n_rows, n_cols);
176
177        Ok(matrix)
178    }
179
180    /// Load initial data using columnar format from a file path.
181    ///
182    /// Async test helper that avoids any internal blocking runtimes.
183    async fn load_dense_from_file(&self, path: &Path) -> StorageResult<DenseMatrix<f64>> {
184        info!("Loading dense matrix from file (async): {:?}", path);
185
186        if !path.exists() {
187            return Err(StorageError::Invalid(format!(
188                "Dense file does not exist: {:?}",
189                path
190            )));
191        }
192
193        let extension = path
194            .extension()
195            .and_then(|e| e.to_str())
196            .ok_or_else(|| StorageError::Invalid(format!("Invalid file path: {:?}", path)))?;
197
198        match extension {
199            "lance" => {
200                // Use a temporary LanceStorage rooted at the file's parent dir,
201                // same pattern as save_dense_to_file_async.
202                let parent = path
203                    .parent()
204                    .ok_or_else(|| {
205                        StorageError::Invalid(format!("Path has no parent: {:?}", path))
206                    })?
207                    .to_str()
208                    .ok_or_else(|| {
209                        StorageError::Invalid(format!("Non-UTF8 parent path for {:?}", path))
210                    })?
211                    .to_string();
212
213                let tmp_storage = Self::new(parent, String::from("tmp_storage"));
214
215                // Reuse the async Lance reader logic.
216                let uri = Self::path_to_uri(path);
217                let batch = tmp_storage.read_lance_all_batches_async(uri).await?;
218                let matrix = tmp_storage.from_dense_record_batch(&batch)?;
219                info!(
220                    "Loaded dense matrix from Lance: {} x {}",
221                    matrix.shape().0,
222                    matrix.shape().1
223                );
224                Ok(matrix)
225            }
226            "parquet" => {
227                use parquet::arrow::arrow_reader::ParquetRecordBatchReaderBuilder;
228                use std::fs::File;
229
230                // 1. Read from Parquet into a single RecordBatch
231                let file = File::open(path)
232                    .map_err(|e| StorageError::Io(format!("Failed to open parquet file: {}", e)))?;
233
234                let builder = ParquetRecordBatchReaderBuilder::try_new(file).map_err(|e| {
235                    StorageError::Parquet(format!("Failed to create parquet reader: {}", e))
236                })?;
237                let mut reader = builder.build().map_err(|e| {
238                    StorageError::Parquet(format!("Failed to build parquet reader: {}", e))
239                })?;
240
241                let mut batches = Vec::new();
242                #[allow(clippy::while_let_on_iterator)]
243                while let Some(batch) = reader.next() {
244                    let batch = batch.map_err(|e| {
245                        StorageError::Parquet(format!("Failed to read parquet batch: {}", e))
246                    })?;
247                    batches.push(batch);
248                }
249
250                if batches.is_empty() {
251                    return Err(StorageError::Invalid(format!(
252                        "Empty parquet dataset at {:?}",
253                        path
254                    )));
255                }
256
257                let schema = batches[0].schema();
258                let combined = arrow::compute::concat_batches(&schema, &batches).map_err(|e| {
259                    StorageError::Parquet(format!("Failed to concatenate parquet batches: {}", e))
260                })?;
261
262                // 2. Detect layout: vector (FixedSizeList) vs old wide columnar (col_* Float64)
263                let fields = schema.fields();
264                let is_vector = fields.len() == 1
265                    && matches!(
266                        fields[0].data_type(),
267                        DataType::FixedSizeList(inner, _)
268                            if matches!(inner.data_type(), DataType::Float64)
269                    );
270
271                let is_wide_col = !is_vector
272                    && !fields.is_empty()
273                    && fields
274                        .iter()
275                        .all(|f| matches!(f.data_type(), DataType::Float64))
276                    && fields.iter().any(|f| f.name().starts_with("col_"));
277
278                // 3. Build DenseMatrix from the RecordBatch
279                let matrix = if is_vector {
280                    // New format already: vector column (FixedSizeList<Float64>)
281                    // Reuse the same decoding as Lance.
282                    let parent = path
283                        .parent()
284                        .ok_or_else(|| {
285                            StorageError::Invalid(format!("Path has no parent: {:?}", path))
286                        })?
287                        .to_str()
288                        .ok_or_else(|| {
289                            StorageError::Invalid(format!("Non-UTF8 parent path for {:?}", path))
290                        })?
291                        .to_string();
292
293                    let tmp_storage = Self::new(parent, String::from("tmp_storage"));
294                    tmp_storage.from_dense_record_batch(&combined)?
295                } else if is_wide_col {
296                    // Old wide columnar: columns like col_0, col_1, ... as Float64
297                    let n_rows = combined.num_rows();
298                    let n_cols = combined.num_columns();
299                    if n_rows == 0 || n_cols == 0 {
300                        return Err(StorageError::Invalid(format!(
301                            "Cannot load empty wide-column parquet at {:?}",
302                            path
303                        )));
304                    }
305
306                    let mut data = Vec::with_capacity(n_rows * n_cols);
307                    for col_idx in 0..n_cols {
308                        let col = combined.column(col_idx);
309                        let arr = col.as_any().downcast_ref::<Float64Array>().ok_or_else(|| {
310                            StorageError::Invalid(format!(
311                                "Wide-column parquet expects Float64, got {:?} in column {}",
312                                col.data_type(),
313                                col_idx
314                            ))
315                        })?;
316                        // Build column-major storage: all rows for col 0, then col 1, ...
317                        for row_idx in 0..n_rows {
318                            data.push(arr.value(row_idx));
319                        }
320                    }
321
322                    DenseMatrix::new(n_rows, n_cols, data, true)
323                        .map_err(|e| StorageError::Invalid(e.to_string()))?
324                } else {
325                    return Err(StorageError::Invalid(format!(
326                        "Unsupported Parquet schema at {:?}: expected FixedSizeList<Float64> \
327                         or wide Float64 columns named col_*",
328                        path
329                    )));
330                };
331
332                info!(
333                    "Loaded dense matrix from Parquet: {} x {}",
334                    matrix.shape().0,
335                    matrix.shape().1
336                );
337
338                Ok(matrix)
339            }
340            _ => Err(StorageError::Invalid(format!(
341                "Unsupported file format: {}. Only .lance and .parquet are supported",
342                extension
343            ))),
344        }
345    }
346
347    fn file_path(&self, key: &str) -> PathBuf {
348        self.base_path()
349            .join(format!("{}_{}.lance", self._name, key))
350    }
351
352    // =========
353    // ASYNC API (matches StorageBackend)
354    // =========
355
356    async fn save_sparse(
357        &self,
358        key: &str,
359        matrix: &CsMat<f64>,
360        md_path: &Path,
361    ) -> StorageResult<()> {
362        self.validate_initialized(md_path)?;
363        let path = self.file_path(key);
364        info!(
365            "Saving sparse {} matrix: {} x {}, nnz={} at {:?}",
366            key,
367            matrix.rows(),
368            matrix.cols(),
369            matrix.nnz(),
370            path
371        );
372
373        let filetype = FileInfo::which_filetype(key);
374        {
375            let mut metadata = self.load_metadata().await?;
376            metadata = metadata.add_file(
377                key,
378                FileInfo::new(
379                    format!("{}_{}.lance", self.get_name(), key),
380                    filetype.as_str(),
381                    (matrix.rows(), matrix.cols()),
382                    Some(matrix.nnz()),
383                    None,
384                ),
385            );
386            self.save_metadata(&metadata).await?;
387
388            let batch = self.to_sparse_record_batch(matrix)?;
389            let uri = Self::path_to_uri(&path);
390            self.write_lance_batch_async(uri, batch).await?;
391        }
392        info!("Sparse matrix {} saved successfully", filetype);
393        Ok(())
394    }
395
396    async fn load_sparse(&self, key: &str) -> StorageResult<CsMat<f64>> {
397        info!("Loading sparse {} matrix", key);
398
399        let metadata = self.load_metadata().await?;
400        let filetype = FileInfo::which_filetype(key);
401        let file_info = metadata
402            .files
403            .get(key)
404            .ok_or_else(|| StorageError::Invalid(format!("{key} not found in metadata")))?;
405
406        let expected_rows = file_info.rows;
407        let expected_cols = file_info.cols;
408        debug!(
409            "Expected dimensions from storage metadata: {} x {}",
410            expected_rows, expected_cols
411        );
412
413        let path = self.file_path(key);
414        let uri = Self::path_to_uri(&path);
415        let batch = self.read_lance_first_batch_async(uri).await?;
416        let matrix = self.from_sparse_record_batch(batch, expected_rows, expected_cols)?;
417        info!(
418            "Sparse {} matrix loaded: {} x {}, nnz={}",
419            filetype,
420            matrix.rows(),
421            matrix.cols(),
422            matrix.nnz()
423        );
424        Ok(matrix)
425    }
426
427    async fn save_lambdas(&self, lambdas: &[f64], md_path: &Path) -> StorageResult<()> {
428        self.validate_initialized(md_path)?;
429        let key = "lambdas";
430        let path = self.file_path("lambdas");
431        info!("Saving {} lambda values", lambdas.len());
432
433        let schema = Schema::new(vec![Field::new("lambda", DataType::Float64, false)]);
434        let batch = RecordBatch::try_new(
435            Arc::new(schema),
436            vec![Arc::new(Float64Array::from(lambdas.to_vec())) as _],
437        )
438        .map_err(|e| StorageError::Lance(e.to_string()))?;
439
440        {
441            let mut metadata = self.load_metadata().await?;
442            metadata = metadata.add_file(
443                key,
444                FileInfo::new(
445                    format!("{}_{}.lance", self.get_name(), key),
446                    "vector",
447                    (lambdas.len(), 1),
448                    None,
449                    None,
450                ),
451            );
452            self.save_metadata(&metadata).await?;
453
454            let uri = Self::path_to_uri(&path);
455            self.write_lance_batch_async(uri, batch).await?;
456        }
457        info!("Lambda values saved successfully");
458        Ok(())
459    }
460
461    async fn load_lambdas(&self) -> StorageResult<Vec<f64>> {
462        let path = self.file_path("lambdas");
463        info!("Loading lambda values from {:?}", path);
464
465        let uri = Self::path_to_uri(&path);
466        let batch = self.read_lance_first_batch_async(uri).await?;
467        let arr = batch
468            .column(0)
469            .as_any()
470            .downcast_ref::<Float64Array>()
471            .ok_or_else(|| StorageError::Invalid("lambda column type mismatch".into()))?;
472
473        let lambdas: Vec<f64> = (0..arr.len()).map(|i| arr.value(i)).collect();
474        info!("Loaded {} lambda values", lambdas.len());
475        Ok(lambdas)
476    }
477
478    async fn save_vector(&self, key: &str, vector: &[f64], md_path: &Path) -> StorageResult<()> {
479        self.validate_initialized(md_path)?;
480        let path = self.file_path(key);
481        info!("Saving {} values for vector {}", vector.len(), key);
482
483        let schema = Schema::new(vec![Field::new("element", DataType::Float64, false)]);
484        let float64_array = Float64Array::from_iter_values::<Vec<f64>>(vector.into());
485        let batch = RecordBatch::try_new(Arc::new(schema), vec![Arc::new(float64_array) as _])
486            .map_err(|e| StorageError::Lance(e.to_string()))?;
487
488        {
489            let mut metadata = self.load_metadata().await?;
490            metadata = metadata.add_file(
491                key,
492                FileInfo::new(
493                    format!("{}_{}.lance", self.get_name(), key),
494                    "vector",
495                    (vector.len(), 1),
496                    None,
497                    None,
498                ),
499            );
500            self.save_metadata(&metadata).await?;
501
502            let uri = Self::path_to_uri(&path);
503            self.write_lance_batch_async(uri, batch).await?;
504        }
505        info!("Index {} saved successfully", key);
506        Ok(())
507    }
508
509    async fn save_index(&self, key: &str, vector: &[usize], md_path: &Path) -> StorageResult<()> {
510        self.validate_initialized(md_path)?;
511        let path = self.file_path(key);
512        info!("Saving {} values for index {}", vector.len(), key);
513
514        let schema = Schema::new(vec![Field::new("id", DataType::UInt32, false)]);
515        let uint32_array = UInt32Array::from_iter_values(vector.iter().map(|&x| x as u32));
516        let batch = RecordBatch::try_new(Arc::new(schema), vec![Arc::new(uint32_array) as _])
517            .map_err(|e| StorageError::Lance(e.to_string()))?;
518
519        {
520            let mut metadata = self.load_metadata().await?;
521            metadata = metadata.add_file(
522                key,
523                FileInfo::new(
524                    format!("{}_{}.lance", self.get_name(), key),
525                    "vector",
526                    (vector.len(), 1),
527                    None,
528                    None,
529                ),
530            );
531            self.save_metadata(&metadata).await?;
532
533            let uri = Self::path_to_uri(&path);
534            self.write_lance_batch_async(uri, batch).await?;
535        }
536        info!("Index {} saved successfully", key);
537        Ok(())
538    }
539
540    async fn load_vector(&self, filename: &str) -> StorageResult<Vec<f64>> {
541        let path = self.file_path(filename);
542        info!("Loading vector {} from {:?}", filename, path);
543
544        let uri = Self::path_to_uri(&path);
545        let batch = self.read_lance_first_batch_async(uri).await?;
546        let arr = batch
547            .column(0)
548            .as_any()
549            .downcast_ref::<Float64Array>()
550            .ok_or_else(|| StorageError::Invalid("column type mismatch".into()))?;
551
552        let vector: Vec<f64> = (0..arr.len()).map(|i| arr.value(i)).collect();
553        info!("Loaded {} vector values for {}", vector.len(), filename);
554        Ok(vector)
555    }
556
557    async fn load_index(&self, filename: &str) -> StorageResult<Vec<usize>> {
558        let path = self.file_path(filename);
559        info!("Loading vector {} from {:?}", filename, path);
560
561        let uri = Self::path_to_uri(&path);
562        let batch = self.read_lance_first_batch_async(uri).await?;
563        let arr = batch
564            .column(0)
565            .as_any()
566            .downcast_ref::<UInt32Array>()
567            .ok_or_else(|| StorageError::Invalid("column type mismatch".into()))?;
568
569        let vector: Vec<usize> = (0..arr.len()).map(|i| arr.value(i) as usize).collect();
570        info!("Loaded {} vector values for {}", vector.len(), filename);
571        Ok(vector)
572    }
573
574    /// Save dense matrix to file in columnar format (col_0, col_1, ..., col_N)
575    ///
576    /// Async test helper that avoids any internal blocking runtimes.
577    async fn save_dense_to_file(data: &DenseMatrix<f64>, path: &Path) -> StorageResult<()> {
578        use tokio::fs as tokio_fs;
579
580        info!("Saving dense matrix to file (async): {:?}", path);
581
582        // Ensure parent dir exists for the test file.
583        if let Some(parent) = path.parent() {
584            tokio_fs::try_exists(parent).await.map_err(|e| {
585                StorageError::Io(format!("Failed to create dir {:?}: {}", parent, e))
586            })?;
587        }
588
589        // Create a temporary storage only to store the file.
590        let tmp_storage = Self::new(
591            String::from(path.parent().unwrap().to_str().unwrap()),
592            String::from("tmp_storage"),
593        );
594
595        let extension = path
596            .extension()
597            .and_then(|e| e.to_str())
598            .ok_or_else(|| StorageError::Invalid(format!("Invalid file path: {:?}", path)))?;
599
600        let (n_rows, n_cols) = data.shape();
601        info!("Saving matrix: {} rows x {} cols", n_rows, n_cols);
602
603        match extension {
604            "lance" => {
605                let batch = tmp_storage.to_dense_record_batch(data)?;
606                debug!(
607                    "Created RecordBatch with {} rows for Lance",
608                    batch.num_rows()
609                );
610
611                // Verify all rows are in the batch
612                if batch.num_rows() != n_rows {
613                    return Err(StorageError::Invalid(format!(
614                        "RecordBatch has {} rows but matrix has {} rows",
615                        batch.num_rows(),
616                        n_rows
617                    )));
618                }
619
620                let uri = Self::path_to_uri(path);
621                tmp_storage.write_lance_batch_async(uri, batch).await?;
622                info!("Saved dense matrix to Lance: {} x {}", n_rows, n_cols);
623                Ok(())
624            }
625            "parquet" => {
626                use parquet::arrow::ArrowWriter;
627                use parquet::file::properties::WriterProperties;
628                use std::fs::File;
629
630                // For tests we still use sync parquet writer; directory was created with tokio_fs.
631                let batch = tmp_storage.to_dense_record_batch(data)?;
632                debug!(
633                    "Created RecordBatch with {} rows for Parquet",
634                    batch.num_rows()
635                );
636
637                if batch.num_rows() != n_rows {
638                    return Err(StorageError::Invalid(format!(
639                        "RecordBatch has {} rows but matrix has {} rows",
640                        batch.num_rows(),
641                        n_rows
642                    )));
643                }
644
645                let file = File::create(path).map_err(|e| {
646                    StorageError::Io(format!("Failed to create parquet file: {}", e))
647                })?;
648
649                let props = WriterProperties::builder()
650                    .set_compression(parquet::basic::Compression::SNAPPY)
651                    .build();
652
653                let mut writer =
654                    ArrowWriter::try_new(file, batch.schema(), Some(props)).map_err(|e| {
655                        StorageError::Parquet(format!("Failed to create parquet writer: {}", e))
656                    })?;
657
658                writer
659                    .write(&batch)
660                    .map_err(|e| StorageError::Parquet(format!("Failed to write batch: {}", e)))?;
661
662                writer
663                    .close()
664                    .map_err(|e| StorageError::Parquet(format!("Failed to close writer: {}", e)))?;
665
666                info!("Saved dense matrix to Parquet: {} x {}", n_rows, n_cols);
667                Ok(())
668            }
669            _ => Err(StorageError::Invalid(format!(
670                "Unsupported file format: {}. Only .lance and .parquet are supported",
671                extension
672            ))),
673        }
674    }
675
676    /// Save centroid_map (item-to-centroid assignments)
677    async fn save_centroid_map(&self, map: &[usize], md_path: &Path) -> StorageResult<()> {
678        self.validate_initialized(md_path)?;
679        let key = "centroid_map";
680        let path = self.file_path(key);
681        info!("Saving {} centroid map entries", map.len());
682
683        let schema = Schema::new(vec![Field::new("centroid_id", DataType::UInt32, false)]);
684        let uint32_array = UInt32Array::from_iter_values(map.iter().map(|&x| x as u32));
685        let batch = RecordBatch::try_new(Arc::new(schema), vec![Arc::new(uint32_array) as _])
686            .map_err(|e| StorageError::Lance(e.to_string()))?;
687
688        {
689            let mut metadata = self.load_metadata().await?;
690            metadata = metadata.add_file(
691                key,
692                FileInfo::new(
693                    format!("{}_{}.lance", self.get_name(), key),
694                    "vector",
695                    (map.len(), 1),
696                    None,
697                    None,
698                ),
699            );
700            self.save_metadata(&metadata).await?;
701
702            let uri = Self::path_to_uri(&path);
703            self.write_lance_batch_async(uri, batch).await?;
704        }
705        info!("Centroid map saved successfully");
706        Ok(())
707    }
708
709    /// Load centroid_map
710    async fn load_centroid_map(&self) -> StorageResult<Vec<usize>> {
711        let path = self.file_path("centroid_map");
712        info!("Loading centroid map from {:?}", path);
713
714        let uri = Self::path_to_uri(&path);
715        let batch = self.read_lance_first_batch_async(uri).await?;
716        let arr = batch
717            .column(0)
718            .as_any()
719            .downcast_ref::<UInt32Array>()
720            .ok_or_else(|| StorageError::Invalid("centroid_id column type mismatch".into()))?;
721
722        let map: Vec<usize> = (0..arr.len()).map(|i| arr.value(i) as usize).collect();
723        info!("Loaded {} centroid map entries", map.len());
724        Ok(map)
725    }
726
727    /// Save subcentroid_lambdas (tau values for subcentroids)
728    async fn save_subcentroid_lambdas(&self, lambdas: &[f64], md_path: &Path) -> StorageResult<()> {
729        self.validate_initialized(md_path)?;
730        let key = "subcentroid_lambdas";
731        let path = self.file_path(key);
732        info!("Saving {} subcentroid lambda values", lambdas.len());
733
734        let schema = Schema::new(vec![Field::new(
735            "subcentroid_lambda",
736            DataType::Float64,
737            false,
738        )]);
739        let batch = RecordBatch::try_new(
740            Arc::new(schema),
741            vec![Arc::new(Float64Array::from(lambdas.to_vec())) as _],
742        )
743        .map_err(|e| StorageError::Lance(e.to_string()))?;
744        {
745            let mut metadata = self.load_metadata().await?;
746            metadata = metadata.add_file(
747                key,
748                FileInfo::new(
749                    format!("{}_{}.lance", self.get_name(), key),
750                    "vector",
751                    (lambdas.len(), 1),
752                    None,
753                    None,
754                ),
755            );
756            self.save_metadata(&metadata).await?;
757
758            let uri = Self::path_to_uri(&path);
759            self.write_lance_batch_async(uri, batch).await?;
760        }
761        info!("Subcentroid lambda values saved successfully");
762        Ok(())
763    }
764
765    /// Load subcentroid_lambdas
766    async fn load_subcentroid_lambdas(&self) -> StorageResult<Vec<f64>> {
767        let path = self.file_path("subcentroid_lambdas");
768        info!("Loading subcentroid lambda values from {:?}", path);
769
770        let uri = Self::path_to_uri(&path);
771        let batch = self.read_lance_first_batch_async(uri).await?;
772        let arr = batch
773            .column(0)
774            .as_any()
775            .downcast_ref::<Float64Array>()
776            .ok_or_else(|| {
777                StorageError::Invalid("subcentroid_lambda column type mismatch".into())
778            })?;
779
780        let lambdas: Vec<f64> = (0..arr.len()).map(|i| arr.value(i)).collect();
781        info!("Loaded {} subcentroid lambda values", lambdas.len());
782        Ok(lambdas)
783    }
784
785    /// Save subcentroids (dense matrix)
786    async fn save_subcentroids(
787        &self,
788        subcentroids: &DenseMatrix<f64>,
789        md_path: &Path,
790    ) -> StorageResult<()> {
791        self.validate_initialized(md_path)?;
792        let key = "sub_centroids";
793        let path = self.file_path(key);
794        let (n_rows, n_cols) = subcentroids.shape();
795        info!(
796            "Saving subcentroids matrix {} x {} at {:?}",
797            n_rows, n_cols, path
798        );
799
800        let batch = self.to_dense_record_batch(subcentroids)?;
801        {
802            let mut metadata = self.load_metadata().await?;
803            metadata = metadata.add_file(
804                key,
805                FileInfo::new(
806                    format!("{}_{}.lance", self.get_name(), key),
807                    "vector",
808                    subcentroids.shape(),
809                    None,
810                    None,
811                ),
812            );
813            self.save_metadata(&metadata).await?;
814
815            let uri = Self::path_to_uri(&path);
816            self.write_lance_batch_async(uri, batch).await?;
817        }
818        debug!("Subcentroids matrix saved successfully");
819        Ok(())
820    }
821
822    /// Load subcentroids as Vec<Vec<f64>>
823    async fn load_subcentroids(&self) -> StorageResult<Vec<Vec<f64>>> {
824        let path = self.file_path("sub_centroids");
825        info!("Loading sub_centroids from {:?}", path);
826
827        let uri = Self::path_to_uri(&path);
828        let batch = self.read_lance_all_batches_async(uri).await?;
829        let matrix = self.from_dense_record_batch(&batch)?;
830
831        // Convert DenseMatrix to Vec<Vec<f64>>
832        let (n_rows, n_cols) = matrix.shape();
833        let mut result = Vec::with_capacity(n_rows);
834
835        for row_idx in 0..n_rows {
836            let row: Vec<f64> = (0..n_cols)
837                .map(|col_idx| *matrix.get((row_idx, col_idx)))
838                .collect();
839            result.push(row);
840        }
841
842        info!(
843            "Loaded sub_centroids: {} x {} as Vec<Vec<f64>>",
844            n_rows, n_cols
845        );
846        Ok(result)
847    }
848
849    /// Save item norms vector
850    async fn save_item_norms(&self, item_norms: &[f64], md_path: &Path) -> StorageResult<()> {
851        self.validate_initialized(md_path)?;
852        let key = "item_norms";
853        let path = self.file_path(key);
854        info!("Saving {} item norm values", item_norms.len());
855
856        let schema = Schema::new(vec![Field::new("norm", DataType::Float64, false)]);
857        let batch = RecordBatch::try_new(
858            Arc::new(schema),
859            vec![Arc::new(Float64Array::from(item_norms.to_vec())) as _],
860        )
861        .map_err(|e| StorageError::Lance(e.to_string()))?;
862
863        {
864            let mut metadata = self.load_metadata().await?;
865            metadata = metadata.add_file(
866                key,
867                FileInfo::new(
868                    format!("{}_{}.lance", self.get_name(), key),
869                    "vector",
870                    (item_norms.len(), 1),
871                    None,
872                    None,
873                ),
874            );
875            self.save_metadata(&metadata).await?;
876
877            let uri = Self::path_to_uri(&path);
878            self.write_lance_batch_async(uri, batch).await?;
879        }
880        info!("Item norms saved successfully");
881        Ok(())
882    }
883
884    /// Load item norms vector
885    async fn load_item_norms(&self) -> StorageResult<Vec<f64>> {
886        let path = self.file_path("item_norms");
887        info!("Loading item norms from {:?}", path);
888
889        let uri = Self::path_to_uri(&path);
890        let batch = self.read_lance_first_batch_async(uri).await?;
891        let arr = batch
892            .column(0)
893            .as_any()
894            .downcast_ref::<Float64Array>()
895            .ok_or_else(|| StorageError::Invalid("norm column type mismatch".into()))?;
896
897        let norms: Vec<f64> = (0..arr.len()).map(|i| arr.value(i)).collect();
898        info!("Loaded {} item norm values", norms.len());
899        Ok(norms)
900    }
901
902    async fn save_cluster_assignments(
903        &self,
904        assignments: &[Option<usize>],
905        md_path: &Path,
906    ) -> StorageResult<()> {
907        self.validate_initialized(md_path)?;
908        let key = "cluster_assignments";
909        let path = self.file_path(key);
910        info!("Saving {} cluster assignments", assignments.len());
911
912        // Convert Option<usize> to i64 (-1 for None)
913        let values: Vec<i64> = assignments
914            .iter()
915            .map(|opt| opt.map(|v| v as i64).unwrap_or(-1))
916            .collect();
917
918        use arrow::array::Int64Array;
919        let schema = Schema::new(vec![Field::new("cluster_id", DataType::Int64, false)]);
920        let batch = RecordBatch::try_new(
921            Arc::new(schema),
922            vec![Arc::new(Int64Array::from(values)) as _],
923        )
924        .map_err(|e| StorageError::Lance(e.to_string()))?;
925
926        {
927            let mut metadata = self.load_metadata().await?;
928            metadata = metadata.add_file(
929                key,
930                FileInfo::new(
931                    format!("{}_{}.lance", self.get_name(), key),
932                    "vector",
933                    (assignments.len(), 1),
934                    None,
935                    None,
936                ),
937            );
938            self.save_metadata(&metadata).await?;
939
940            let uri = Self::path_to_uri(&path);
941            self.write_lance_batch_async(uri, batch).await?;
942        }
943        info!("Cluster assignments saved successfully");
944        Ok(())
945    }
946
947    async fn load_cluster_assignments(&self) -> StorageResult<Vec<Option<usize>>> {
948        use arrow::array::Int64Array;
949        let path = self.file_path("cluster_assignments");
950        info!("Loading cluster assignments from {:?}", path);
951
952        let uri = Self::path_to_uri(&path);
953        let batch = self.read_lance_first_batch_async(uri).await?;
954        let arr = batch
955            .column(0)
956            .as_any()
957            .downcast_ref::<Int64Array>()
958            .ok_or_else(|| StorageError::Invalid("cluster_id column type mismatch".into()))?;
959
960        let assignments: Vec<Option<usize>> = (0..arr.len())
961            .map(|i| {
962                let v = arr.value(i);
963                if v < 0 { None } else { Some(v as usize) }
964            })
965            .collect();
966        info!("Loaded {} cluster assignments", assignments.len());
967        Ok(assignments)
968    }
969}