1use std::collections::BTreeMap;
8use std::path::{Path, PathBuf};
9use std::sync::Arc;
10
11use arrow::array::{Array as ArrowArray, ArrayRef, Float32Array, Float64Array, UInt32Array};
12use arrow::datatypes::{DataType, Field, Float64Type, Int64Type, Schema, UInt32Type};
13use arrow::record_batch::RecordBatch;
14use log::{debug, info};
15use smartcore::linalg::basic::arrays::Array;
16use smartcore::linalg::basic::matrix::DenseMatrix;
17use sprs::CsMat;
18
19use crate::graph::{GraphEdge, GraphReadOptions, GraphWriteOptions, StoredGraph};
20use crate::metadata::FileInfo;
21use crate::metadata::GeneMetadata;
22use crate::traits::backend::StorageBackend;
23use crate::traits::lance::{
24 LanceStorage, graph_record_batch, stored_graph_from_batch_with_options,
25 validate_vector_space_schema, vector_space_collection_batch,
26};
27use crate::{StorageError, StorageResult};
28
29fn checked_u32_values(values: &[usize], what: &str) -> StorageResult<Vec<u32>> {
34 values
35 .iter()
36 .map(|&v| {
37 u32::try_from(v).map_err(|_| {
38 StorageError::Overflow(format!(
39 "{} value {} exceeds u32::MAX and would be silently truncated",
40 what, v
41 ))
42 })
43 })
44 .collect()
45}
46
47async fn ensure_parent_dir(path: &Path) -> StorageResult<()> {
50 let parent = path
51 .parent()
52 .filter(|p| !p.as_os_str().is_empty())
53 .ok_or_else(|| StorageError::Invalid(format!("path has no parent directory: {path:?}")))?;
54 tokio::fs::create_dir_all(parent)
55 .await
56 .map_err(|e| StorageError::Io(format!("create dir {parent:?}: {e}")))
57}
58
59#[derive(Debug, Clone)]
113pub struct LanceStorageGraph {
114 pub(crate) base: String,
115 pub(crate) name: String,
116}
117
118impl LanceStorageGraph {
119 pub fn new(base: String, name: String) -> StorageResult<Self> {
133 crate::generations::validate_logical_name(&name)?;
134 Ok(Self::from_parts(base, name))
135 }
136
137 fn from_parts(base: String, name: String) -> Self {
141 info!("Creating LanceStorage at base={}, name={}", base, name);
142 Self { base, name }
143 }
144
145 pub async fn spawn(base_path: String) -> Result<(Self, GeneMetadata), StorageError> {
147 let (exists, md_path) = Self::exists(&base_path);
149
150 if !exists || md_path.is_none() {
152 return Err(StorageError::Invalid(format!(
153 "Metadata does not exist in base path: {}",
154 base_path
155 )));
156 }
157
158 let metadata = GeneMetadata::read(md_path.unwrap()).await?;
160
161 let storage = Self::from_parts(base_path.clone(), metadata.name_id.clone());
163 Ok((storage, metadata))
164 }
165
166 pub fn scoped_generation(&self, generation: u64) -> Self {
172 let logical = crate::generations::logical_name(&self.name);
173 Self::from_parts(
174 self.base.clone(),
175 crate::generations::generation_name(logical, generation),
176 )
177 }
178}
179
180impl LanceStorage for LanceStorageGraph {}
181
182fn parent_dir_of(path: &Path) -> StorageResult<String> {
189 path.parent()
190 .ok_or_else(|| StorageError::Invalid(format!("Path has no parent: {:?}", path)))?
191 .to_str()
192 .map(str::to_string)
193 .ok_or_else(|| StorageError::Invalid(format!("Non-UTF8 parent path for {:?}", path)))
194}
195
196async fn load_dense_lance(path: &Path) -> StorageResult<DenseMatrix<f64>> {
199 use crate::traits::backend::StorageBackend;
200 use crate::traits::lance::LanceStorage;
201
202 let parent = parent_dir_of(path)?;
203 let tmp_storage = LanceStorageGraph::from_parts(parent, String::from("tmp_storage"));
204
205 let uri = LanceStorageGraph::path_to_uri(path)?;
206 let batch = tmp_storage.read_lance_all_batches_async(uri).await?;
207 let matrix = tmp_storage.from_dense_record_batch(&batch)?;
208 info!(
209 "Loaded dense matrix from Lance: {} x {}",
210 matrix.shape().0,
211 matrix.shape().1
212 );
213 Ok(matrix)
214}
215
216async fn read_parquet_batches(path: &Path) -> StorageResult<RecordBatch> {
222 use parquet::arrow::arrow_reader::ParquetRecordBatchReaderBuilder;
223
224 let owned_path = path.to_path_buf();
225 let combined = tokio::task::spawn_blocking(move || -> StorageResult<RecordBatch> {
226 let file = std::fs::File::open(&owned_path)
227 .map_err(|e| StorageError::Io(format!("Failed to open parquet file: {}", e)))?;
228
229 let builder = ParquetRecordBatchReaderBuilder::try_new(file).map_err(|e| {
230 StorageError::Parquet(format!("Failed to create parquet reader: {}", e))
231 })?;
232 let reader = builder
233 .build()
234 .map_err(|e| StorageError::Parquet(format!("Failed to build parquet reader: {}", e)))?;
235
236 let batches: Vec<RecordBatch> = reader
237 .collect::<Result<Vec<_>, _>>()
238 .map_err(|e| StorageError::Parquet(format!("Failed to read parquet batch: {}", e)))?;
239
240 if batches.is_empty() {
241 return Err(StorageError::Invalid(format!(
242 "Empty parquet dataset at {:?}",
243 owned_path
244 )));
245 }
246
247 let schema = batches[0].schema();
248 arrow::compute::concat_batches(&schema, &batches).map_err(|e| {
249 StorageError::Parquet(format!("Failed to concatenate parquet batches: {}", e))
250 })
251 })
252 .await
253 .map_err(|e| StorageError::Io(format!("Parquet reader task failed: {}", e)))??;
254 Ok(combined)
255}
256
257fn dense_matrix_from_parquet(
263 path: &Path,
264 combined: &RecordBatch,
265) -> StorageResult<DenseMatrix<f64>> {
266 use crate::traits::backend::StorageBackend;
267
268 let schema = combined.schema();
269 let fields = schema.fields();
270 let is_vector = fields.len() == 1
271 && matches!(
272 fields[0].data_type(),
273 DataType::FixedSizeList(inner, _)
274 if matches!(inner.data_type(), DataType::Float64)
275 );
276
277 let is_wide_col = !is_vector
278 && !fields.is_empty()
279 && fields
280 .iter()
281 .all(|f| matches!(f.data_type(), DataType::Float64))
282 && fields.iter().any(|f| f.name().starts_with("col_"));
283
284 let matrix = if is_vector {
285 let parent = parent_dir_of(path)?;
288 let tmp_storage = LanceStorageGraph::from_parts(parent, String::from("tmp_storage"));
289 tmp_storage.from_dense_record_batch(combined)?
290 } else if is_wide_col {
291 let n_rows = combined.num_rows();
293 let n_cols = combined.num_columns();
294 if n_rows == 0 || n_cols == 0 {
295 return Err(StorageError::Invalid(format!(
296 "Cannot load empty wide-column parquet at {:?}",
297 path
298 )));
299 }
300
301 let mut data = Vec::with_capacity(n_rows * n_cols);
302 for col_idx in 0..n_cols {
303 let col = combined.column(col_idx);
304 let arr = col.as_any().downcast_ref::<Float64Array>().ok_or_else(|| {
305 StorageError::Invalid(format!(
306 "Wide-column parquet expects Float64, got {:?} in column {}",
307 col.data_type(),
308 col_idx
309 ))
310 })?;
311 for row_idx in 0..n_rows {
313 data.push(arr.value(row_idx));
314 }
315 }
316
317 DenseMatrix::new(n_rows, n_cols, data, true)
318 .map_err(|e| StorageError::Invalid(e.to_string()))?
319 } else {
320 return Err(StorageError::Invalid(format!(
321 "Unsupported Parquet schema at {:?}: expected FixedSizeList<Float64> \
322 or wide Float64 columns named col_*",
323 path
324 )));
325 };
326
327 info!(
328 "Loaded dense matrix from Parquet: {} x {}",
329 matrix.shape().0,
330 matrix.shape().1
331 );
332 Ok(matrix)
333}
334
335fn checked_dense_batch(
338 storage: &LanceStorageGraph,
339 data: &DenseMatrix<f64>,
340) -> StorageResult<RecordBatch> {
341 use crate::traits::backend::StorageBackend;
342
343 let batch = storage.to_dense_record_batch(data)?;
344 let (n_rows, _) = data.shape();
345 if batch.num_rows() != n_rows {
346 return Err(StorageError::Invalid(format!(
347 "RecordBatch has {} rows but matrix has {} rows",
348 batch.num_rows(),
349 n_rows
350 )));
351 }
352 Ok(batch)
353}
354
355async fn save_dense_lance(
357 parent_dir: String,
358 data: &DenseMatrix<f64>,
359 path: &Path,
360) -> StorageResult<()> {
361 use crate::traits::backend::StorageBackend;
362 use crate::traits::lance::LanceStorage;
363
364 let tmp_storage = LanceStorageGraph::from_parts(parent_dir, String::from("tmp_storage"));
365 let batch = checked_dense_batch(&tmp_storage, data)?;
366 debug!(
367 "Created RecordBatch with {} rows for Lance",
368 batch.num_rows()
369 );
370
371 let uri = LanceStorageGraph::path_to_uri(path)?;
372 tmp_storage.write_lance_batch_async(uri, batch).await?;
373 let (n_rows, n_cols) = data.shape();
374 info!("Saved dense matrix to Lance: {} x {}", n_rows, n_cols);
375 Ok(())
376}
377
378async fn save_dense_parquet(
384 parent_dir: String,
385 data: &DenseMatrix<f64>,
386 path: &Path,
387) -> StorageResult<()> {
388 use parquet::arrow::ArrowWriter;
389 use parquet::file::properties::WriterProperties;
390 use std::fs::File;
391
392 let tmp_storage = LanceStorageGraph::from_parts(parent_dir, String::from("tmp_storage"));
393 let batch = checked_dense_batch(&tmp_storage, data)?;
394 debug!(
395 "Created RecordBatch with {} rows for Parquet",
396 batch.num_rows()
397 );
398
399 let owned_path = path.to_path_buf();
400 tokio::task::spawn_blocking(move || -> StorageResult<()> {
401 let file = File::create(&owned_path)
402 .map_err(|e| StorageError::Io(format!("Failed to create parquet file: {}", e)))?;
403
404 let props = WriterProperties::builder()
405 .set_compression(parquet::basic::Compression::SNAPPY)
406 .build();
407
408 let mut writer = ArrowWriter::try_new(file, batch.schema(), Some(props)).map_err(|e| {
409 StorageError::Parquet(format!("Failed to create parquet writer: {}", e))
410 })?;
411
412 writer
413 .write(&batch)
414 .map_err(|e| StorageError::Parquet(format!("Failed to write batch: {}", e)))?;
415
416 writer
417 .close()
418 .map_err(|e| StorageError::Parquet(format!("Failed to close writer: {}", e)))?;
419
420 Ok(())
421 })
422 .await
423 .map_err(|e| StorageError::Io(format!("parquet writer task failed: {}", e)))??;
424
425 let (n_rows, n_cols) = data.shape();
426 info!("Saved dense matrix to Parquet: {} x {}", n_rows, n_cols);
427 Ok(())
428}
429
430impl StorageBackend for LanceStorageGraph {
431 fn get_base(&self) -> String {
432 self.base.clone()
433 }
434
435 fn get_name(&self) -> String {
436 self.name.clone()
437 }
438
439 fn base_path(&self) -> PathBuf {
440 PathBuf::from(&self.base)
441 }
442
443 fn metadata_path(&self) -> PathBuf {
444 crate::generations::metadata_file_path(&self.base_path(), &self.name)
445 }
446
447 fn basepath_to_uri(&self) -> StorageResult<String> {
449 Self::path_to_uri(PathBuf::from(self.base.clone()).as_path())
450 }
451
452 async fn save_dense(
462 &self,
463 key: &str,
464 matrix: &DenseMatrix<f64>,
465 md_path: &Path,
466 ) -> StorageResult<()> {
467 self.validate_initialized(md_path)?;
468 let path = self.file_path(key);
469 let (n_rows, n_cols) = matrix.shape();
470
471 info!(
472 "Saving dense {} matrix: {} x {} at {:?}",
473 key, n_rows, n_cols, path
474 );
475
476 let batch = self.to_dense_record_batch(matrix)?;
478
479 if batch.num_rows() != n_rows {
481 return Err(StorageError::Invalid(format!(
482 "RecordBatch has {} rows but matrix has {} rows",
483 batch.num_rows(),
484 n_rows
485 )));
486 }
487
488 {
489 let uri = Self::path_to_uri(&path)?;
491 self.write_lance_batch_async(uri, batch).await?;
492 crate::commit::with_commit_actor(&self.metadata_path(), || async {
496 let mut md = self.load_metadata().await?;
497 md = md.add_file(
498 key,
499 FileInfo::new(
500 crate::generations::artifact_file_name(self.get_name().as_str(), key),
501 "dense",
502 matrix.shape(),
503 None,
504 None,
505 )?,
506 );
507 self.save_metadata(&md).await
508 })
509 .await?;
510 info!("Dense {} matrix saved successfully", key);
511 }
512 Ok(())
513 }
514
515 async fn load_dense(&self, key: &str) -> StorageResult<DenseMatrix<f64>> {
525 let path = self.file_path(key);
526 info!("Loading dense {} matrix from {:?}", key, path);
527
528 let uri = Self::path_to_uri(&path)?;
530 let batch = self.read_lance_all_batches_async(uri).await?;
531
532 let matrix = self.from_dense_record_batch(&batch)?;
534
535 let (n_rows, n_cols) = matrix.shape();
536 info!("Loaded dense {} matrix: {} x {}", key, n_rows, n_cols);
537
538 Ok(matrix)
539 }
540
541 async fn load_dense_from_file(&self, path: &Path) -> StorageResult<DenseMatrix<f64>> {
546 info!("Loading dense matrix from file (async): {:?}", path);
547
548 if !path.exists() {
549 return Err(StorageError::Invalid(format!(
550 "Dense file does not exist: {:?}",
551 path
552 )));
553 }
554
555 let extension = path
556 .extension()
557 .and_then(|e| e.to_str())
558 .ok_or_else(|| StorageError::Invalid(format!("Invalid file path: {:?}", path)))?;
559
560 match extension {
561 "lance" => load_dense_lance(path).await,
562 "parquet" => {
563 let combined = read_parquet_batches(path).await?;
564 dense_matrix_from_parquet(path, &combined)
565 }
566 _ => Err(StorageError::Invalid(format!(
567 "Unsupported file format: {}. Only .lance and .parquet are supported",
568 extension
569 ))),
570 }
571 }
572
573 fn file_path(&self, key: &str) -> PathBuf {
574 crate::generations::artifact_file_path(&self.base_path(), &self.name, key)
575 }
576
577 async fn save_sparse(
582 &self,
583 key: &str,
584 matrix: &CsMat<f64>,
585 md_path: &Path,
586 ) -> StorageResult<()> {
587 self.validate_initialized(md_path)?;
588 if matrix.nnz() == 0 {
594 return Err(StorageError::Invalid(format!(
595 "sparse '{key}' is fully disconnected: adjacency nnz=0 ({}x{}) — \
596 raise --eps/--k or gate the input before persisting",
597 matrix.rows(),
598 matrix.cols()
599 )));
600 }
601 let path = self.file_path(key);
602 info!(
603 "Saving sparse {} matrix: {} x {}, nnz={} at {:?}",
604 key,
605 matrix.rows(),
606 matrix.cols(),
607 matrix.nnz(),
608 path
609 );
610
611 let filetype = FileInfo::which_filetype(key)?;
612 {
613 crate::commit::with_commit_actor(&self.metadata_path(), || async {
614 let mut metadata = self.load_metadata().await?;
615 metadata = metadata.add_file(
616 key,
617 FileInfo::new(
618 crate::generations::artifact_file_name(self.get_name().as_str(), key),
619 filetype.as_str(),
620 (matrix.rows(), matrix.cols()),
621 Some(matrix.nnz()),
622 None,
623 )?,
624 );
625 self.save_metadata(&metadata).await
626 })
627 .await?;
628
629 let batch = self.to_sparse_record_batch(matrix)?;
630 let uri = Self::path_to_uri(&path)?;
631 self.write_lance_batch_async(uri, batch).await?;
632 }
633 info!("Sparse matrix {} saved successfully", filetype);
634 Ok(())
635 }
636
637 async fn load_sparse(&self, key: &str) -> StorageResult<CsMat<f64>> {
638 info!("Loading sparse {} matrix", key);
639
640 let metadata = self.load_metadata().await?;
641 let filetype = FileInfo::which_filetype(key)?;
642 let file_info = metadata
643 .files
644 .get(key)
645 .ok_or_else(|| StorageError::Invalid(format!("{key} not found in metadata")))?;
646
647 let expected_rows = file_info.rows;
648 let expected_cols = file_info.cols;
649 debug!(
650 "Expected dimensions from storage metadata: {} x {}",
651 expected_rows, expected_cols
652 );
653
654 let path = self.file_path(key);
655 let uri = Self::path_to_uri(&path)?;
656 let batch = self.read_lance_all_batches_async(uri).await?;
657 let matrix = self.from_sparse_record_batch(batch, expected_rows, expected_cols)?;
658 info!(
659 "Sparse {} matrix loaded: {} x {}, nnz={}",
660 filetype,
661 matrix.rows(),
662 matrix.cols(),
663 matrix.nnz()
664 );
665 Ok(matrix)
666 }
667
668 async fn save_lambdas(&self, lambdas: &[f64], md_path: &Path) -> StorageResult<()> {
669 info!("Saving {} lambda values", lambdas.len());
670 self.save_primitive_column::<Float64Type>("lambdas", "lambda", lambdas.to_vec(), md_path)
671 .await
672 }
673
674 async fn load_lambdas(&self) -> StorageResult<Vec<f64>> {
675 let path = self.file_path("lambdas");
676 info!("Loading lambda values from {:?}", path);
677
678 let uri = Self::path_to_uri(&path)?;
679 let batch = self.read_lance_all_batches_async(uri).await?;
680 let arr = batch
681 .column(0)
682 .as_any()
683 .downcast_ref::<Float64Array>()
684 .ok_or_else(|| StorageError::Invalid("lambda column type mismatch".into()))?;
685
686 let lambdas: Vec<f64> = (0..arr.len()).map(|i| arr.value(i)).collect();
687 info!("Loaded {} lambda values", lambdas.len());
688 Ok(lambdas)
689 }
690
691 async fn save_vector(&self, key: &str, vector: &[f64], md_path: &Path) -> StorageResult<()> {
692 info!("Saving {} values for vector {}", vector.len(), key);
693 self.save_primitive_column::<Float64Type>(key, "element", vector.to_vec(), md_path)
694 .await
695 }
696
697 async fn save_index(&self, key: &str, vector: &[usize], md_path: &Path) -> StorageResult<()> {
698 info!("Saving {} values for index {}", vector.len(), key);
699 let values = checked_u32_values(vector, "index")?;
702 self.save_primitive_column::<UInt32Type>(key, "id", values, md_path)
703 .await
704 }
705
706 async fn load_vector(&self, filename: &str) -> StorageResult<Vec<f64>> {
707 let path = self.file_path(filename);
708 info!("Loading vector {} from {:?}", filename, path);
709
710 let uri = Self::path_to_uri(&path)?;
711 let batch = self.read_lance_all_batches_async(uri).await?;
712 let arr = batch
713 .column(0)
714 .as_any()
715 .downcast_ref::<Float64Array>()
716 .ok_or_else(|| StorageError::Invalid("column type mismatch".into()))?;
717
718 let vector: Vec<f64> = (0..arr.len()).map(|i| arr.value(i)).collect();
719 info!("Loaded {} vector values for {}", vector.len(), filename);
720 Ok(vector)
721 }
722
723 async fn load_index(&self, filename: &str) -> StorageResult<Vec<usize>> {
724 let path = self.file_path(filename);
725 info!("Loading vector {} from {:?}", filename, path);
726
727 let uri = Self::path_to_uri(&path)?;
728 let batch = self.read_lance_all_batches_async(uri).await?;
729 let arr = batch
730 .column(0)
731 .as_any()
732 .downcast_ref::<UInt32Array>()
733 .ok_or_else(|| StorageError::Invalid("column type mismatch".into()))?;
734
735 let vector: Vec<usize> = (0..arr.len()).map(|i| arr.value(i) as usize).collect();
736 info!("Loaded {} vector values for {}", vector.len(), filename);
737 Ok(vector)
738 }
739
740 async fn save_dense_to_file(data: &DenseMatrix<f64>, path: &Path) -> StorageResult<()> {
744 info!("Saving dense matrix to file (async): {:?}", path);
745
746 let parent = path
749 .parent()
750 .filter(|p| !p.as_os_str().is_empty())
751 .ok_or_else(|| {
752 StorageError::Invalid(format!("path has no parent directory: {:?}", path))
753 })?;
754 tokio::fs::create_dir_all(parent)
755 .await
756 .map_err(|e| StorageError::Io(format!("create dir {:?}: {}", parent, e)))?;
757 let parent_str = parent
758 .to_str()
759 .ok_or_else(|| StorageError::Invalid(format!("non-UTF8 parent path for {:?}", path)))?
760 .to_string();
761
762 let extension = path
763 .extension()
764 .and_then(|e| e.to_str())
765 .ok_or_else(|| StorageError::Invalid(format!("Invalid file path: {:?}", path)))?;
766
767 let (n_rows, n_cols) = data.shape();
768 info!("Saving matrix: {} rows x {} cols", n_rows, n_cols);
769
770 match extension {
771 "lance" => save_dense_lance(parent_str, data, path).await,
772 "parquet" => save_dense_parquet(parent_str, data, path).await,
773 _ => Err(StorageError::Invalid(format!(
774 "Unsupported file format: {}. Only .lance and .parquet are supported",
775 extension
776 ))),
777 }
778 }
779
780 async fn save_vectors_with(
785 &self,
786 name: &str,
787 batch: &RecordBatch,
788 properties: &BTreeMap<String, String>,
789 md_path: &Path,
790 ) -> StorageResult<()> {
791 self.validate_initialized(md_path)?;
792 let (batch, dim) = vector_space_collection_batch(batch, properties)?;
793 let rows = batch.num_rows();
794
795 let path = self.file_path(name);
796 info!(
797 "Saving vector-space collection '{}' ({} rows, vector dim {}) at {:?}",
798 name, rows, dim, path
799 );
800 let uri = Self::path_to_uri(&path)?;
801 self.write_lance_batch_async(uri, batch).await?;
810 crate::commit::with_commit_actor(&self.metadata_path(), || async {
811 let mut md = self.load_metadata().await?;
812 let mut info = FileInfo::new(
813 crate::generations::artifact_file_name(self.get_name().as_str(), name),
814 "vectors",
815 (rows, dim as usize),
816 None,
817 None,
818 )?;
819 info.properties = properties.clone();
820 md = md.add_file(name, info);
821 self.save_metadata(&md).await
822 })
823 .await?;
824 info!("Vector-space collection '{}' saved successfully", name);
825 Ok(())
826 }
827
828 async fn load_vectors(&self, name: &str) -> StorageResult<RecordBatch> {
829 info!("Loading vector-space collection '{}'", name);
830 self.load_vectors_from_path(&self.file_path(name)).await
831 }
832
833 async fn save_graph_with(
834 &self,
835 name: &str,
836 edges: &[GraphEdge],
837 options: &GraphWriteOptions,
838 md_path: &Path,
839 ) -> StorageResult<()> {
840 self.validate_initialized(md_path)?;
841 let (batch, num_nodes) = graph_record_batch(edges, options)?;
842 let num_nodes_usize = usize::try_from(num_nodes)
843 .map_err(|_| StorageError::Overflow(format!("node count {num_nodes} exceeds usize")))?;
844 let weighted = edges[0].weight.is_some();
845 let width = options.node_id_width;
846
847 let path = self.file_path(name);
848 info!(
849 "Saving graph collection '{}' ({} edges, {} nodes, width {:?}, weight width {:?}, weighted {}) at {:?}",
850 name,
851 edges.len(),
852 num_nodes,
853 width,
854 options.weight_type,
855 weighted,
856 path
857 );
858 let uri = Self::path_to_uri(&path)?;
859 self.write_lance_batch_async(uri, batch).await?;
867 crate::commit::with_commit_actor(&self.metadata_path(), || async {
868 let mut md = self.load_metadata().await?;
869 let mut info = FileInfo::new(
870 crate::generations::artifact_file_name(self.get_name().as_str(), name),
871 "graph",
872 (num_nodes_usize, num_nodes_usize),
873 Some(edges.len()),
874 None,
875 )?;
876 info.properties = options.properties.clone();
877 info.properties
878 .insert("node_id_width".to_string(), width.as_str().to_string());
879 info.properties
880 .insert("weighted".to_string(), weighted.to_string());
881 info.properties
882 .insert("num_nodes".to_string(), num_nodes.to_string());
883 info.properties.insert(
884 "weight_type".to_string(),
885 options.weight_type.as_str().to_string(),
886 );
887 md = md.add_file(name, info);
888 self.save_metadata(&md).await
889 })
890 .await?;
891 info!("Graph collection '{}' saved successfully", name);
892 Ok(())
893 }
894
895 async fn load_graph(&self, name: &str) -> StorageResult<StoredGraph> {
896 info!("Loading graph collection '{}'", name);
897 self.load_graph_from_path(&self.file_path(name)).await
898 }
899
900 async fn save_vectors_to_path(
905 &self,
906 path: &Path,
907 batch: &RecordBatch,
908 properties: &BTreeMap<String, String>,
909 ) -> StorageResult<()> {
910 let (batch, dim) = vector_space_collection_batch(batch, properties)?;
911 ensure_parent_dir(path).await?;
912 info!(
913 "Saving vector-space collection ({} rows, vector dim {}) at {:?} (registry-free)",
914 batch.num_rows(),
915 dim,
916 path
917 );
918 let uri = Self::path_to_uri(path)?;
919 self.write_lance_batch_async(uri, batch).await?;
920 info!("Vector-space collection saved successfully (registry-free)");
921 Ok(())
922 }
923
924 async fn load_vectors_from_path(&self, path: &Path) -> StorageResult<RecordBatch> {
925 info!(
926 "Loading vector-space collection from {:?} (registry-free)",
927 path
928 );
929 let uri = Self::path_to_uri(path)?;
930 let batch = self.read_lance_all_batches_async(uri).await?;
931 validate_vector_space_schema(batch.schema().as_ref())?;
932 info!(
933 "Loaded vector-space collection: {} rows (registry-free)",
934 batch.num_rows()
935 );
936 Ok(batch)
937 }
938
939 async fn save_graph_to_path(
940 &self,
941 path: &Path,
942 edges: &[GraphEdge],
943 options: &GraphWriteOptions,
944 ) -> StorageResult<()> {
945 let (batch, num_nodes) = graph_record_batch(edges, options)?;
946 ensure_parent_dir(path).await?;
947 info!(
948 "Saving graph collection ({} edges, {} nodes, width {:?}, weight width {:?}, weighted {}) at {:?} (registry-free)",
949 edges.len(),
950 num_nodes,
951 options.node_id_width,
952 options.weight_type,
953 edges.first().map(|e| e.weight.is_some()).unwrap_or(false),
954 path
955 );
956 let uri = Self::path_to_uri(path)?;
957 self.write_lance_batch_async(uri, batch).await?;
958 info!("Graph collection saved successfully (registry-free)");
959 Ok(())
960 }
961
962 async fn load_graph_from_path(&self, path: &Path) -> StorageResult<StoredGraph> {
963 self.load_graph_from_path_with_options(path, &GraphReadOptions::default())
964 .await
965 }
966
967 async fn load_graph_from_path_with_options(
968 &self,
969 path: &Path,
970 options: &GraphReadOptions,
971 ) -> StorageResult<StoredGraph> {
972 info!(
973 "Loading graph collection from {:?} (registry-free, strict={})",
974 path, options.strict
975 );
976 let uri = Self::path_to_uri(path)?;
977 let batch = self.read_lance_all_batches_async(uri).await?;
978 let graph = stored_graph_from_batch_with_options(batch, options)?;
979 info!(
980 "Loaded graph collection: {} edges, {} nodes, width {:?}, weight width {:?} (registry-free)",
981 graph.edges.len(),
982 graph.num_nodes,
983 graph.node_id_width,
984 graph.weight_type
985 );
986 Ok(graph)
987 }
988
989 async fn load_scalars(&self, name: &str) -> StorageResult<Vec<f64>> {
990 self.load_scalars_from_path(&self.file_path(name)).await
991 }
992
993 async fn save_scalars_to_path(&self, path: &Path, values: &[f64]) -> StorageResult<()> {
994 if values.is_empty() {
995 return Err(StorageError::Invalid(
996 "empty scalar collections are not supported".into(),
997 ));
998 }
999 ensure_parent_dir(path).await?;
1000 info!(
1001 "Saving scalar collection ({} values) at {:?} (registry-free)",
1002 values.len(),
1003 path
1004 );
1005 let schema =
1011 Schema::new(vec![Field::new("lambda", DataType::Float64, false)]).with_metadata(
1012 std::collections::HashMap::from([("kind".to_string(), "vector-space".to_string())]),
1013 );
1014 let batch = RecordBatch::try_new(
1015 Arc::new(schema),
1016 vec![Arc::new(Float64Array::from(values.to_vec())) as ArrayRef],
1017 )
1018 .map_err(|e| StorageError::Lance(e.to_string()))?;
1019 let uri = Self::path_to_uri(path)?;
1020 self.write_lance_batch_async(uri, batch).await?;
1021 info!("Scalar collection saved successfully (registry-free)");
1022 Ok(())
1023 }
1024
1025 async fn load_scalars_from_path(&self, path: &Path) -> StorageResult<Vec<f64>> {
1026 info!("Loading scalar collection from {:?} (registry-free)", path);
1027 let uri = Self::path_to_uri(path)?;
1028 let batch = self.read_lance_all_batches_async(uri).await?;
1029
1030 let schema = batch.schema();
1031 match schema.metadata().get("kind").map(String::as_str) {
1036 Some("vector-space") => {}
1037 other => {
1038 return Err(StorageError::Invalid(format!(
1039 "scalar collection at {path:?} has dataset kind {other:?}, \
1040 expected 'vector-space'"
1041 )));
1042 }
1043 }
1044 if schema.fields().len() != 1 {
1045 return Err(StorageError::Invalid(format!(
1046 "scalar collection expects exactly one column, found {}",
1047 schema.fields().len()
1048 )));
1049 }
1050 let field = schema.field(0);
1051 if field.is_nullable() {
1052 return Err(StorageError::Invalid(format!(
1053 "scalar collection column '{}' is nullable",
1054 field.name()
1055 )));
1056 }
1057 let column = batch.column(0);
1058 if column.null_count() != 0 {
1059 return Err(StorageError::Invalid(
1060 "scalar collection column contains nulls".into(),
1061 ));
1062 }
1063 let values: Vec<f64> = match field.data_type() {
1064 DataType::Float64 => {
1065 let a = column
1066 .as_any()
1067 .downcast_ref::<Float64Array>()
1068 .ok_or_else(|| StorageError::Invalid("scalar column type mismatch".into()))?;
1069 (0..a.len()).map(|i| a.value(i)).collect()
1070 }
1071 DataType::Float32 => {
1072 let a = column
1074 .as_any()
1075 .downcast_ref::<Float32Array>()
1076 .ok_or_else(|| StorageError::Invalid("scalar column type mismatch".into()))?;
1077 (0..a.len()).map(|i| f64::from(a.value(i))).collect()
1078 }
1079 other => {
1080 return Err(StorageError::Invalid(format!(
1081 "scalar collection column must be Float64|Float32, found {other:?}"
1082 )));
1083 }
1084 };
1085 info!("Loaded {} scalar values (registry-free)", values.len());
1086 Ok(values)
1087 }
1088
1089 async fn collection_schema_from_path(&self, path: &Path) -> StorageResult<Schema> {
1090 info!("Reading collection schema from {:?} (registry-free)", path);
1091 let path = path.to_path_buf();
1092 tokio::task::spawn_blocking(move || crate::lancefmt::read_schema(&path))
1093 .await
1094 .map_err(|e| StorageError::Io(format!("lancefmt read_schema task failed: {e}")))?
1095 }
1096}
1097
1098impl crate::traits::clustering::ClusteringArtifacts for LanceStorageGraph {
1099 async fn save_centroid_map(&self, map: &[usize], md_path: &Path) -> StorageResult<()> {
1101 info!("Saving {} centroid map entries", map.len());
1102 let values = checked_u32_values(map, "centroid map")?;
1105 self.save_primitive_column::<UInt32Type>("centroid_map", "centroid_id", values, md_path)
1106 .await
1107 }
1108
1109 async fn load_centroid_map(&self) -> StorageResult<Vec<usize>> {
1111 let path = self.file_path("centroid_map");
1112 info!("Loading centroid map from {:?}", path);
1113
1114 let uri = Self::path_to_uri(&path)?;
1115 let batch = self.read_lance_all_batches_async(uri).await?;
1116 let arr = batch
1117 .column(0)
1118 .as_any()
1119 .downcast_ref::<UInt32Array>()
1120 .ok_or_else(|| StorageError::Invalid("centroid_id column type mismatch".into()))?;
1121
1122 let map: Vec<usize> = (0..arr.len()).map(|i| arr.value(i) as usize).collect();
1123 info!("Loaded {} centroid map entries", map.len());
1124 Ok(map)
1125 }
1126
1127 async fn save_subcentroid_lambdas(&self, lambdas: &[f64], md_path: &Path) -> StorageResult<()> {
1129 info!("Saving {} subcentroid lambda values", lambdas.len());
1130 self.save_primitive_column::<Float64Type>(
1131 "subcentroid_lambdas",
1132 "subcentroid_lambda",
1133 lambdas.to_vec(),
1134 md_path,
1135 )
1136 .await
1137 }
1138
1139 async fn load_subcentroid_lambdas(&self) -> StorageResult<Vec<f64>> {
1141 let path = self.file_path("subcentroid_lambdas");
1142 info!("Loading subcentroid lambda values from {:?}", path);
1143
1144 let uri = Self::path_to_uri(&path)?;
1145 let batch = self.read_lance_all_batches_async(uri).await?;
1146 let arr = batch
1147 .column(0)
1148 .as_any()
1149 .downcast_ref::<Float64Array>()
1150 .ok_or_else(|| {
1151 StorageError::Invalid("subcentroid_lambda column type mismatch".into())
1152 })?;
1153
1154 let lambdas: Vec<f64> = (0..arr.len()).map(|i| arr.value(i)).collect();
1155 info!("Loaded {} subcentroid lambda values", lambdas.len());
1156 Ok(lambdas)
1157 }
1158
1159 async fn save_subcentroids(
1161 &self,
1162 subcentroids: &DenseMatrix<f64>,
1163 md_path: &Path,
1164 ) -> StorageResult<()> {
1165 self.validate_initialized(md_path)?;
1166 let key = "sub_centroids";
1167 let path = self.file_path(key);
1168 let (n_rows, n_cols) = subcentroids.shape();
1169 info!(
1170 "Saving subcentroids matrix {} x {} at {:?}",
1171 n_rows, n_cols, path
1172 );
1173
1174 let batch = self.to_dense_record_batch(subcentroids)?;
1175 {
1176 crate::commit::with_commit_actor(&self.metadata_path(), || async {
1177 let mut metadata = self.load_metadata().await?;
1178 metadata = metadata.add_file(
1179 key,
1180 FileInfo::new(
1181 crate::generations::artifact_file_name(self.get_name().as_str(), key),
1182 "vector",
1183 subcentroids.shape(),
1184 None,
1185 None,
1186 )?,
1187 );
1188 self.save_metadata(&metadata).await
1189 })
1190 .await?;
1191
1192 let uri = Self::path_to_uri(&path)?;
1193 self.write_lance_batch_async(uri, batch).await?;
1194 }
1195 debug!("Subcentroids matrix saved successfully");
1196 Ok(())
1197 }
1198
1199 async fn load_subcentroids(&self) -> StorageResult<Vec<Vec<f64>>> {
1201 let path = self.file_path("sub_centroids");
1202 info!("Loading sub_centroids from {:?}", path);
1203
1204 let uri = Self::path_to_uri(&path)?;
1205 let batch = self.read_lance_all_batches_async(uri).await?;
1206 let matrix = self.from_dense_record_batch(&batch)?;
1207
1208 let (n_rows, n_cols) = matrix.shape();
1210 let mut result = Vec::with_capacity(n_rows);
1211
1212 for row_idx in 0..n_rows {
1213 let row: Vec<f64> = (0..n_cols)
1214 .map(|col_idx| *matrix.get((row_idx, col_idx)))
1215 .collect();
1216 result.push(row);
1217 }
1218
1219 info!(
1220 "Loaded sub_centroids: {} x {} as Vec<Vec<f64>>",
1221 n_rows, n_cols
1222 );
1223 Ok(result)
1224 }
1225
1226 async fn save_item_norms(&self, item_norms: &[f64], md_path: &Path) -> StorageResult<()> {
1228 info!("Saving {} item norm values", item_norms.len());
1229 self.save_primitive_column::<Float64Type>(
1230 "item_norms",
1231 "norm",
1232 item_norms.to_vec(),
1233 md_path,
1234 )
1235 .await
1236 }
1237
1238 async fn load_item_norms(&self) -> StorageResult<Vec<f64>> {
1240 let path = self.file_path("item_norms");
1241 info!("Loading item norms from {:?}", path);
1242
1243 let uri = Self::path_to_uri(&path)?;
1244 let batch = self.read_lance_all_batches_async(uri).await?;
1245 let arr = batch
1246 .column(0)
1247 .as_any()
1248 .downcast_ref::<Float64Array>()
1249 .ok_or_else(|| StorageError::Invalid("norm column type mismatch".into()))?;
1250
1251 let norms: Vec<f64> = (0..arr.len()).map(|i| arr.value(i)).collect();
1252 info!("Loaded {} item norm values", norms.len());
1253 Ok(norms)
1254 }
1255
1256 async fn save_cluster_assignments(
1257 &self,
1258 assignments: &[Option<usize>],
1259 md_path: &Path,
1260 ) -> StorageResult<()> {
1261 info!("Saving {} cluster assignments", assignments.len());
1262
1263 let values: Vec<i64> = assignments
1265 .iter()
1266 .map(|opt| opt.map(|v| v as i64).unwrap_or(-1))
1267 .collect();
1268
1269 self.save_primitive_column::<Int64Type>(
1270 "cluster_assignments",
1271 "cluster_id",
1272 values,
1273 md_path,
1274 )
1275 .await
1276 }
1277
1278 async fn load_cluster_assignments(&self) -> StorageResult<Vec<Option<usize>>> {
1279 use arrow::array::Int64Array;
1280 let path = self.file_path("cluster_assignments");
1281 info!("Loading cluster assignments from {:?}", path);
1282
1283 let uri = Self::path_to_uri(&path)?;
1284 let batch = self.read_lance_all_batches_async(uri).await?;
1285 let arr = batch
1286 .column(0)
1287 .as_any()
1288 .downcast_ref::<Int64Array>()
1289 .ok_or_else(|| StorageError::Invalid("cluster_id column type mismatch".into()))?;
1290
1291 let assignments: Vec<Option<usize>> = (0..arr.len())
1292 .map(|i| {
1293 let v = arr.value(i);
1294 if v < 0 { None } else { Some(v as usize) }
1295 })
1296 .collect();
1297 info!("Loaded {} cluster assignments", assignments.len());
1298 Ok(assignments)
1299 }
1300}