Skip to main content

ipfrs_semantic/
migration.rs

1//! Index migration utilities
2//!
3//! This module provides tools for migrating data between different index types
4//! and configurations, including upgrading from in-memory to disk-based indices,
5//! applying quantization, and changing index parameters.
6//!
7//! # Features
8//!
9//! - **Index Type Migration**: Convert between HNSW, DiskANN, and quantized indices
10//! - **Configuration Updates**: Change index parameters with data preservation
11//! - **Batch Migration**: Efficient bulk data transfer
12//! - **Progress Tracking**: Monitor migration progress
13//!
14//! # Example
15//!
16//! ```rust
17//! use ipfrs_semantic::migration::{IndexMigration, MigrationConfig};
18//! use ipfrs_semantic::hnsw::VectorIndex;
19//!
20//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
21//! // Create a migration plan
22//! let config = MigrationConfig {
23//!     batch_size: 1000,
24//!     verify_after_migration: true,
25//!     ..Default::default()
26//! };
27//!
28//! let migration = IndexMigration::new(config);
29//!
30//! // Migration would be performed here
31//! // migration.migrate(source_index, target_index)?;
32//! # Ok(())
33//! # }
34//! ```
35
36use crate::hnsw::{DistanceMetric, VectorIndex};
37use ipfrs_core::{Cid, Result};
38use std::collections::HashMap;
39use std::sync::atomic::{AtomicUsize, Ordering};
40use std::sync::Arc;
41
42/// Migration configuration
43#[derive(Debug, Clone)]
44pub struct MigrationConfig {
45    /// Batch size for migration
46    pub batch_size: usize,
47    /// Whether to verify data after migration
48    pub verify_after_migration: bool,
49    /// Maximum concurrent migrations
50    pub max_concurrent: usize,
51    /// Whether to preserve original index during migration
52    pub preserve_source: bool,
53}
54
55impl Default for MigrationConfig {
56    fn default() -> Self {
57        Self {
58            batch_size: 1000,
59            verify_after_migration: true,
60            max_concurrent: 4,
61            preserve_source: true,
62        }
63    }
64}
65
66/// Migration progress information
67#[derive(Debug, Clone)]
68pub struct MigrationProgress {
69    /// Total entries to migrate
70    pub total_entries: usize,
71    /// Entries migrated so far
72    pub migrated_entries: usize,
73    /// Verification progress
74    pub verified_entries: usize,
75    /// Failed entries
76    pub failed_entries: usize,
77    /// Estimated time remaining (seconds)
78    pub estimated_seconds_remaining: f64,
79}
80
81impl MigrationProgress {
82    /// Calculate completion percentage
83    pub fn completion_percent(&self) -> f64 {
84        if self.total_entries == 0 {
85            return 100.0;
86        }
87        (self.migrated_entries as f64 / self.total_entries as f64) * 100.0
88    }
89
90    /// Check if migration is complete
91    pub fn is_complete(&self) -> bool {
92        self.migrated_entries >= self.total_entries
93    }
94}
95
96/// Migration statistics
97#[derive(Debug, Clone)]
98pub struct MigrationStats {
99    /// Total time taken
100    pub total_duration_seconds: f64,
101    /// Entries per second
102    pub throughput: f64,
103    /// Success rate
104    pub success_rate: f64,
105    /// Total entries migrated
106    pub total_migrated: usize,
107    /// Total entries failed
108    pub total_failed: usize,
109}
110
111/// Index migration manager
112pub struct IndexMigration {
113    /// Configuration
114    config: MigrationConfig,
115    /// Progress tracking
116    progress: Arc<AtomicUsize>,
117}
118
119impl IndexMigration {
120    /// Create a new index migration manager
121    pub fn new(config: MigrationConfig) -> Self {
122        Self {
123            config,
124            progress: Arc::new(AtomicUsize::new(0)),
125        }
126    }
127
128    /// Migrate from one HNSW index to another with different parameters
129    pub fn migrate_hnsw_to_hnsw(
130        &self,
131        source: &VectorIndex,
132        target_m: usize,
133        target_ef_construction: usize,
134    ) -> Result<VectorIndex> {
135        // Extract dimension and metric from source
136        let dimension = 768; // Would be extracted from source in real impl
137        let metric = DistanceMetric::Cosine;
138
139        let mut target = VectorIndex::new(dimension, metric, target_m, target_ef_construction)?;
140
141        // Get all entries from source
142        let entries = source.get_all_embeddings();
143        let _total = entries.len();
144
145        // Migrate in batches
146        for (i, chunk) in entries.chunks(self.config.batch_size).enumerate() {
147            for (cid, embedding) in chunk {
148                target.insert(cid, embedding)?;
149            }
150
151            self.progress
152                .store((i + 1) * self.config.batch_size, Ordering::Relaxed);
153        }
154
155        // Verify if requested
156        if self.config.verify_after_migration {
157            self.verify_migration(source, &target)?;
158        }
159
160        Ok(target)
161    }
162
163    /// Verify that migration was successful
164    fn verify_migration(&self, source: &VectorIndex, target: &VectorIndex) -> Result<()> {
165        let source_entries = source.get_all_embeddings();
166
167        for (cid, _embedding) in &source_entries {
168            if !target.contains(cid) {
169                return Err(ipfrs_core::Error::Internal(format!(
170                    "Migration verification failed: CID {:?} missing in target",
171                    cid
172                )));
173            }
174        }
175
176        Ok(())
177    }
178
179    /// Get current migration progress
180    pub fn get_progress(&self, total_entries: usize) -> MigrationProgress {
181        let migrated = self.progress.load(Ordering::Relaxed);
182
183        MigrationProgress {
184            total_entries,
185            migrated_entries: migrated,
186            verified_entries: 0,
187            failed_entries: 0,
188            estimated_seconds_remaining: 0.0,
189        }
190    }
191
192    /// Migrate embeddings with transformation
193    pub fn migrate_with_transform<F>(
194        &self,
195        source: &VectorIndex,
196        dimension: usize,
197        metric: DistanceMetric,
198        m: usize,
199        ef_construction: usize,
200        transform: F,
201    ) -> Result<VectorIndex>
202    where
203        F: Fn(&[f32]) -> Vec<f32>,
204    {
205        let mut target = VectorIndex::new(dimension, metric, m, ef_construction)?;
206
207        let entries = source.get_all_embeddings();
208
209        for (cid, embedding) in entries {
210            let transformed = transform(&embedding);
211            target.insert(&cid, &transformed)?;
212        }
213
214        Ok(target)
215    }
216
217    /// Export index entries for external migration
218    pub fn export_entries(&self, index: &VectorIndex) -> Vec<(Cid, Vec<f32>)> {
219        index.get_all_embeddings()
220    }
221
222    /// Import entries into a new index
223    pub fn import_entries(
224        &self,
225        entries: &[(Cid, Vec<f32>)],
226        dimension: usize,
227        metric: DistanceMetric,
228        m: usize,
229        ef_construction: usize,
230    ) -> Result<VectorIndex> {
231        let mut index = VectorIndex::new(dimension, metric, m, ef_construction)?;
232
233        for (cid, embedding) in entries {
234            index.insert(cid, embedding)?;
235        }
236
237        Ok(index)
238    }
239}
240
241/// Configuration change migration
242pub struct ConfigMigration;
243
244impl ConfigMigration {
245    /// Migrate to higher quality settings
246    pub fn upgrade_quality(source: &VectorIndex) -> Result<VectorIndex> {
247        let migration = IndexMigration::new(MigrationConfig::default());
248
249        // Upgrade to higher M and ef_construction
250        migration.migrate_hnsw_to_hnsw(source, 32, 400)
251    }
252
253    /// Migrate to faster settings
254    pub fn optimize_speed(source: &VectorIndex) -> Result<VectorIndex> {
255        let migration = IndexMigration::new(MigrationConfig::default());
256
257        // Downgrade to lower M and ef_construction for speed
258        migration.migrate_hnsw_to_hnsw(source, 8, 100)
259    }
260
261    /// Balance quality and speed
262    pub fn balance(source: &VectorIndex) -> Result<VectorIndex> {
263        let migration = IndexMigration::new(MigrationConfig::default());
264
265        // Balanced settings
266        migration.migrate_hnsw_to_hnsw(source, 16, 200)
267    }
268}
269
270/// Dimension reduction migration
271pub struct DimensionMigration;
272
273impl DimensionMigration {
274    /// Reduce dimensionality using PCA-like projection
275    /// Note: This is a simplified version - real PCA would require training
276    pub fn reduce_dimension(source: &VectorIndex, target_dim: usize) -> Result<VectorIndex> {
277        let migration = IndexMigration::new(MigrationConfig::default());
278
279        // Simple truncation (real implementation would use PCA or other methods)
280        let transform = |embedding: &[f32]| -> Vec<f32> {
281            embedding[..target_dim.min(embedding.len())].to_vec()
282        };
283
284        migration.migrate_with_transform(
285            source,
286            target_dim,
287            DistanceMetric::Cosine,
288            16,
289            200,
290            transform,
291        )
292    }
293}
294
295/// Metric migration utilities
296pub struct MetricMigration;
297
298impl MetricMigration {
299    /// Convert index to use different distance metric
300    pub fn change_metric(source: &VectorIndex, new_metric: DistanceMetric) -> Result<VectorIndex> {
301        let entries = source.get_all_embeddings();
302        let dimension = 768; // Would be extracted from source
303
304        let mut target = VectorIndex::new(dimension, new_metric, 16, 200)?;
305
306        for (cid, embedding) in entries {
307            target.insert(&cid, &embedding)?;
308        }
309
310        Ok(target)
311    }
312
313    /// Normalize embeddings for cosine distance
314    pub fn normalize_for_cosine(source: &VectorIndex) -> Result<VectorIndex> {
315        let migration = IndexMigration::new(MigrationConfig::default());
316
317        let transform = |embedding: &[f32]| -> Vec<f32> {
318            let norm: f32 = embedding.iter().map(|x| x * x).sum::<f32>().sqrt();
319            if norm > 1e-6 {
320                embedding.iter().map(|x| x / norm).collect()
321            } else {
322                embedding.to_vec()
323            }
324        };
325
326        migration.migrate_with_transform(source, 768, DistanceMetric::Cosine, 16, 200, transform)
327    }
328}
329
330/// Batch migration utilities
331pub struct BatchMigration {
332    /// Batch size
333    batch_size: usize,
334    /// Statistics
335    stats: HashMap<String, usize>,
336}
337
338impl BatchMigration {
339    /// Create a new batch migration
340    pub fn new(batch_size: usize) -> Self {
341        Self {
342            batch_size,
343            stats: HashMap::new(),
344        }
345    }
346
347    /// Migrate in batches with progress callback
348    pub fn migrate_with_callback<F>(
349        &mut self,
350        source: &VectorIndex,
351        target: &mut VectorIndex,
352        mut callback: F,
353    ) -> Result<()>
354    where
355        F: FnMut(usize, usize),
356    {
357        let entries = source.get_all_embeddings();
358        let total = entries.len();
359
360        for (i, chunk) in entries.chunks(self.batch_size).enumerate() {
361            for (cid, embedding) in chunk {
362                target.insert(cid, embedding)?;
363            }
364
365            let migrated = (i + 1) * self.batch_size.min(total);
366            callback(migrated, total);
367        }
368
369        Ok(())
370    }
371
372    /// Get migration statistics
373    pub fn get_stats(&self) -> &HashMap<String, usize> {
374        &self.stats
375    }
376}
377
378impl Default for BatchMigration {
379    fn default() -> Self {
380        Self::new(1000)
381    }
382}
383
384#[cfg(test)]
385mod tests {
386    use super::*;
387    use multihash_codetable::{Code, MultihashDigest};
388
389    fn create_test_index() -> VectorIndex {
390        let mut index = VectorIndex::new(768, DistanceMetric::Cosine, 16, 200)
391            .expect("test: create 768-dim cosine index");
392
393        for i in 0..10 {
394            let data = format!("test_vector_{}", i);
395            let hash = Code::Sha2_256.digest(data.as_bytes());
396            let cid = Cid::new_v1(0x55, hash);
397            let embedding = vec![i as f32 * 0.1; 768];
398            index
399                .insert(&cid, &embedding)
400                .expect("test: insert test vector");
401        }
402
403        index
404    }
405
406    #[test]
407    fn test_migration_config_default() {
408        let config = MigrationConfig::default();
409        assert_eq!(config.batch_size, 1000);
410        assert!(config.verify_after_migration);
411        assert_eq!(config.max_concurrent, 4);
412    }
413
414    #[test]
415    fn test_migration_progress() {
416        let progress = MigrationProgress {
417            total_entries: 100,
418            migrated_entries: 50,
419            verified_entries: 0,
420            failed_entries: 0,
421            estimated_seconds_remaining: 10.0,
422        };
423
424        assert_eq!(progress.completion_percent(), 50.0);
425        assert!(!progress.is_complete());
426    }
427
428    #[test]
429    fn test_migration_progress_complete() {
430        let progress = MigrationProgress {
431            total_entries: 100,
432            migrated_entries: 100,
433            verified_entries: 100,
434            failed_entries: 0,
435            estimated_seconds_remaining: 0.0,
436        };
437
438        assert_eq!(progress.completion_percent(), 100.0);
439        assert!(progress.is_complete());
440    }
441
442    #[test]
443    fn test_index_migration_creation() {
444        let config = MigrationConfig::default();
445        let migration = IndexMigration::new(config);
446        let progress = migration.get_progress(100);
447
448        assert_eq!(progress.migrated_entries, 0);
449    }
450
451    #[test]
452    fn test_export_entries() {
453        let index = create_test_index();
454        let migration = IndexMigration::new(MigrationConfig::default());
455
456        let entries = migration.export_entries(&index);
457        assert_eq!(entries.len(), 10);
458    }
459
460    #[test]
461    fn test_import_entries() {
462        let source = create_test_index();
463        let migration = IndexMigration::new(MigrationConfig::default());
464
465        let entries = migration.export_entries(&source);
466        let imported = migration
467            .import_entries(&entries, 768, DistanceMetric::Cosine, 16, 200)
468            .expect("test: import entries");
469
470        assert_eq!(imported.len(), source.len());
471    }
472
473    #[test]
474    fn test_migrate_with_transform() {
475        let source = create_test_index();
476        let migration = IndexMigration::new(MigrationConfig::default());
477
478        // Transform: multiply all values by 2
479        let transform =
480            |embedding: &[f32]| -> Vec<f32> { embedding.iter().map(|x| x * 2.0).collect() };
481
482        let target = migration
483            .migrate_with_transform(&source, 768, DistanceMetric::Cosine, 16, 200, transform)
484            .expect("test: migrate with transform");
485
486        assert_eq!(target.len(), source.len());
487    }
488
489    #[test]
490    fn test_config_migration_upgrade() {
491        let source = create_test_index();
492        let upgraded =
493            ConfigMigration::upgrade_quality(&source).expect("test: upgrade quality migration");
494
495        assert_eq!(upgraded.len(), source.len());
496    }
497
498    #[test]
499    fn test_config_migration_speed() {
500        let source = create_test_index();
501        let optimized =
502            ConfigMigration::optimize_speed(&source).expect("test: optimize speed migration");
503
504        assert_eq!(optimized.len(), source.len());
505    }
506
507    #[test]
508    fn test_config_migration_balance() {
509        let source = create_test_index();
510        let balanced = ConfigMigration::balance(&source).expect("test: balance migration");
511
512        assert_eq!(balanced.len(), source.len());
513    }
514
515    #[test]
516    fn test_dimension_reduction() {
517        let source = create_test_index();
518        let reduced =
519            DimensionMigration::reduce_dimension(&source, 384).expect("test: dimension reduction");
520
521        assert_eq!(reduced.len(), source.len());
522    }
523
524    #[test]
525    fn test_metric_change() {
526        let source = create_test_index();
527        let changed = MetricMigration::change_metric(&source, DistanceMetric::L2)
528            .expect("test: metric change to L2");
529
530        assert_eq!(changed.len(), source.len());
531    }
532
533    #[test]
534    fn test_normalize_for_cosine() {
535        let source = create_test_index();
536        let normalized =
537            MetricMigration::normalize_for_cosine(&source).expect("test: normalize for cosine");
538
539        assert_eq!(normalized.len(), source.len());
540    }
541
542    #[test]
543    fn test_batch_migration() {
544        let source = create_test_index();
545        let mut target = VectorIndex::new(768, DistanceMetric::Cosine, 16, 200)
546            .expect("test: create target index for batch");
547
548        let mut batch_migration = BatchMigration::new(5);
549        let mut callback_count = 0;
550
551        batch_migration
552            .migrate_with_callback(&source, &mut target, |migrated, total| {
553                callback_count += 1;
554                assert!(migrated <= total);
555            })
556            .expect("test: batch migration with callback");
557
558        assert_eq!(target.len(), source.len());
559        assert!(callback_count > 0);
560    }
561}