Skip to main content

ipfrs_semantic/
dynamic.rs

1//! Dynamic embedding updates for evolving embedding spaces
2//!
3//! This module provides mechanisms for:
4//! - Online embedding updates
5//! - Version migration
6//! - Incremental fine-tuning
7//! - Embedding space evolution tracking
8
9use crate::VectorIndex;
10use ipfrs_core::{Cid, Error, Result};
11use serde::{Deserialize, Serialize};
12use std::collections::HashMap;
13use std::sync::{Arc, RwLock};
14
15/// Version of an embedding model
16#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
17pub struct ModelVersion {
18    /// Major version
19    pub major: u32,
20    /// Minor version
21    pub minor: u32,
22    /// Patch version
23    pub patch: u32,
24    /// Optional tag (e.g., "alpha", "beta")
25    pub tag: Option<String>,
26}
27
28impl ModelVersion {
29    /// Create a new model version
30    pub fn new(major: u32, minor: u32, patch: u32) -> Self {
31        Self {
32            major,
33            minor,
34            patch,
35            tag: None,
36        }
37    }
38
39    /// Create a version with a tag
40    pub fn with_tag(mut self, tag: String) -> Self {
41        self.tag = Some(tag);
42        self
43    }
44
45    /// Check if this version is compatible with another (same major version)
46    pub fn is_compatible_with(&self, other: &ModelVersion) -> bool {
47        self.major == other.major
48    }
49}
50
51impl std::fmt::Display for ModelVersion {
52    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
53        write!(f, "{}.{}.{}", self.major, self.minor, self.patch)?;
54        if let Some(tag) = &self.tag {
55            write!(f, "-{}", tag)?;
56        }
57        Ok(())
58    }
59}
60
61/// Embedding transformation for migrating between versions
62#[derive(Debug, Clone, Serialize, Deserialize)]
63pub struct EmbeddingTransform {
64    /// Source version
65    pub from_version: ModelVersion,
66    /// Target version
67    pub to_version: ModelVersion,
68    /// Transformation matrix (if dimensions change)
69    pub transform_matrix: Option<Vec<Vec<f32>>>,
70    /// Bias vector
71    pub bias: Option<Vec<f32>>,
72}
73
74impl EmbeddingTransform {
75    /// Create an identity transform (no change)
76    pub fn identity(version: ModelVersion) -> Self {
77        Self {
78            from_version: version.clone(),
79            to_version: version,
80            transform_matrix: None,
81            bias: None,
82        }
83    }
84
85    /// Create a linear transformation
86    pub fn linear(
87        from_version: ModelVersion,
88        to_version: ModelVersion,
89        matrix: Vec<Vec<f32>>,
90    ) -> Self {
91        Self {
92            from_version,
93            to_version,
94            transform_matrix: Some(matrix),
95            bias: None,
96        }
97    }
98
99    /// Apply transformation to an embedding
100    pub fn apply(&self, embedding: &[f32]) -> Vec<f32> {
101        let mut result = embedding.to_vec();
102
103        // Apply matrix transformation if present
104        if let Some(matrix) = &self.transform_matrix {
105            let out_dim = matrix[0].len();
106            let mut transformed = vec![0.0; out_dim];
107
108            for (i, row) in matrix.iter().enumerate() {
109                if i >= embedding.len() {
110                    break;
111                }
112                for (j, &val) in row.iter().enumerate() {
113                    transformed[j] += embedding[i] * val;
114                }
115            }
116
117            result = transformed;
118        }
119
120        // Apply bias if present
121        if let Some(bias) = &self.bias {
122            for (i, &b) in bias.iter().enumerate() {
123                if i < result.len() {
124                    result[i] += b;
125                }
126            }
127        }
128
129        result
130    }
131}
132
133/// Dynamic index that supports multiple embedding versions
134pub struct DynamicIndex {
135    /// Indices for each version
136    indices: Arc<RwLock<HashMap<ModelVersion, VectorIndex>>>,
137    /// Current active version
138    active_version: Arc<RwLock<ModelVersion>>,
139    /// Transformations between versions
140    transforms: Arc<RwLock<HashMap<(ModelVersion, ModelVersion), EmbeddingTransform>>>,
141    /// Embedding dimension
142    dimension: usize,
143}
144
145impl DynamicIndex {
146    /// Create a new dynamic index
147    pub fn new(initial_version: ModelVersion, dimension: usize) -> Result<Self> {
148        let mut indices = HashMap::new();
149        let index = VectorIndex::with_defaults(dimension)?;
150        indices.insert(initial_version.clone(), index);
151
152        Ok(Self {
153            indices: Arc::new(RwLock::new(indices)),
154            active_version: Arc::new(RwLock::new(initial_version)),
155            transforms: Arc::new(RwLock::new(HashMap::new())),
156            dimension,
157        })
158    }
159
160    /// Get the current active version
161    pub fn active_version(&self) -> ModelVersion {
162        self.active_version
163            .read()
164            .unwrap_or_else(|e| e.into_inner())
165            .clone()
166    }
167
168    /// Add a new version with optional transform from previous version
169    pub fn add_version(
170        &self,
171        version: ModelVersion,
172        transform: Option<EmbeddingTransform>,
173    ) -> Result<()> {
174        let mut indices = self.indices.write().unwrap_or_else(|e| e.into_inner());
175
176        if indices.contains_key(&version) {
177            return Err(Error::InvalidInput(format!(
178                "Version {} already exists",
179                version
180            )));
181        }
182
183        let index = VectorIndex::with_defaults(self.dimension)?;
184        indices.insert(version.clone(), index);
185
186        // Add transform if provided
187        if let Some(t) = transform {
188            let mut transforms = self.transforms.write().unwrap_or_else(|e| e.into_inner());
189            transforms.insert((t.from_version.clone(), t.to_version.clone()), t);
190        }
191
192        Ok(())
193    }
194
195    /// Set the active version
196    pub fn set_active_version(&self, version: ModelVersion) -> Result<()> {
197        let indices = self.indices.read().unwrap_or_else(|e| e.into_inner());
198
199        if !indices.contains_key(&version) {
200            return Err(Error::InvalidInput(format!(
201                "Version {} does not exist",
202                version
203            )));
204        }
205
206        let mut active = self
207            .active_version
208            .write()
209            .unwrap_or_else(|e| e.into_inner());
210        *active = version;
211
212        Ok(())
213    }
214
215    /// Insert an embedding for a specific version
216    pub fn insert(
217        &self,
218        cid: &Cid,
219        embedding: &[f32],
220        version: Option<ModelVersion>,
221    ) -> Result<()> {
222        let version = version.unwrap_or_else(|| self.active_version());
223
224        let mut indices = self.indices.write().unwrap_or_else(|e| e.into_inner());
225        let index = indices
226            .get_mut(&version)
227            .ok_or_else(|| Error::InvalidInput(format!("Version {} does not exist", version)))?;
228
229        index.insert(cid, embedding)?;
230        Ok(())
231    }
232
233    /// Update an existing embedding
234    pub fn update(
235        &self,
236        cid: &Cid,
237        new_embedding: &[f32],
238        version: Option<ModelVersion>,
239    ) -> Result<()> {
240        let version = version.unwrap_or_else(|| self.active_version());
241
242        let mut indices = self.indices.write().unwrap_or_else(|e| e.into_inner());
243        let index = indices
244            .get_mut(&version)
245            .ok_or_else(|| Error::InvalidInput(format!("Version {} does not exist", version)))?;
246
247        // First delete the old embedding
248        index.delete(cid)?;
249        // Then insert the new one
250        index.insert(cid, new_embedding)?;
251
252        Ok(())
253    }
254
255    /// Migrate embeddings from one version to another
256    pub fn migrate(&self, from: &ModelVersion, to: &ModelVersion) -> Result<usize> {
257        let transforms = self.transforms.read().unwrap_or_else(|e| e.into_inner());
258        let transform = transforms
259            .get(&(from.clone(), to.clone()))
260            .ok_or_else(|| Error::InvalidInput(format!("No transform from {} to {}", from, to)))?
261            .clone();
262        drop(transforms);
263
264        // Get all embeddings from source version
265        let indices = self.indices.read().unwrap_or_else(|e| e.into_inner());
266        let source_index = indices.get(from).ok_or_else(|| {
267            Error::InvalidInput(format!("Source version {} does not exist", from))
268        })?;
269
270        // Ensure target version exists
271        if !indices.contains_key(to) {
272            return Err(Error::InvalidInput(format!(
273                "Target version {} does not exist",
274                to
275            )));
276        }
277
278        // Get all embeddings from source index
279        let embeddings = source_index.get_all_embeddings();
280        drop(indices);
281
282        // Apply transformation and insert into target index
283        let mut migrated_count = 0;
284        for (cid, embedding) in embeddings {
285            // Apply transformation
286            let transformed = transform.apply(&embedding);
287
288            // Insert into target index
289            let mut indices = self.indices.write().unwrap_or_else(|e| e.into_inner());
290            if let Some(target_index) = indices.get_mut(to) {
291                // Only insert if not already present
292                if !target_index.contains(&cid) {
293                    target_index.insert(&cid, &transformed)?;
294                    migrated_count += 1;
295                }
296            }
297            drop(indices);
298        }
299
300        Ok(migrated_count)
301    }
302
303    /// Get statistics for all versions
304    pub fn version_stats(&self) -> HashMap<ModelVersion, VersionStats> {
305        let indices = self.indices.read().unwrap_or_else(|e| e.into_inner());
306
307        indices
308            .iter()
309            .map(|(version, index)| {
310                let stats = VersionStats {
311                    version: version.clone(),
312                    num_embeddings: index.len(),
313                    is_active: version == &self.active_version(),
314                };
315                (version.clone(), stats)
316            })
317            .collect()
318    }
319}
320
321/// Statistics for a specific version
322#[derive(Debug, Clone, Serialize, Deserialize)]
323pub struct VersionStats {
324    /// Version identifier
325    pub version: ModelVersion,
326    /// Number of embeddings in this version
327    pub num_embeddings: usize,
328    /// Whether this is the active version
329    pub is_active: bool,
330}
331
332/// Online updater for incremental fine-tuning
333pub struct OnlineUpdater {
334    /// Learning rate for updates
335    learning_rate: f32,
336    /// Momentum factor
337    momentum: f32,
338    /// Velocity (for momentum)
339    velocity: Arc<RwLock<HashMap<Cid, Vec<f32>>>>,
340}
341
342impl OnlineUpdater {
343    /// Create a new online updater
344    pub fn new(learning_rate: f32, momentum: f32) -> Self {
345        Self {
346            learning_rate,
347            momentum,
348            velocity: Arc::new(RwLock::new(HashMap::new())),
349        }
350    }
351
352    /// Update an embedding with a gradient
353    pub fn update(&self, cid: &Cid, embedding: &[f32], gradient: &[f32]) -> Vec<f32> {
354        let mut velocity = self.velocity.write().unwrap_or_else(|e| e.into_inner());
355
356        // Get or initialize velocity for this CID
357        let v = velocity
358            .entry(*cid)
359            .or_insert_with(|| vec![0.0; embedding.len()]);
360
361        // Update velocity with momentum
362        for i in 0..embedding.len().min(gradient.len()) {
363            v[i] = self.momentum * v[i] - self.learning_rate * gradient[i];
364        }
365
366        // Apply velocity to embedding
367        embedding
368            .iter()
369            .zip(v.iter())
370            .map(|(&e, &vel)| e + vel)
371            .collect()
372    }
373
374    /// Clear velocity history
375    pub fn reset(&self) {
376        let mut velocity = self.velocity.write().unwrap_or_else(|e| e.into_inner());
377        velocity.clear();
378    }
379
380    /// Get statistics
381    pub fn stats(&self) -> OnlineUpdaterStats {
382        let velocity = self.velocity.read().unwrap_or_else(|e| e.into_inner());
383
384        OnlineUpdaterStats {
385            learning_rate: self.learning_rate,
386            momentum: self.momentum,
387            num_tracked: velocity.len(),
388        }
389    }
390}
391
392/// Statistics for online updater
393#[derive(Debug, Clone, Serialize, Deserialize)]
394pub struct OnlineUpdaterStats {
395    /// Learning rate
396    pub learning_rate: f32,
397    /// Momentum
398    pub momentum: f32,
399    /// Number of tracked embeddings
400    pub num_tracked: usize,
401}
402
403#[cfg(test)]
404mod tests {
405    use super::*;
406
407    #[test]
408    fn test_model_version() {
409        let v1 = ModelVersion::new(1, 0, 0);
410        let v2 = ModelVersion::new(1, 1, 0);
411        let v3 = ModelVersion::new(2, 0, 0);
412
413        assert!(v1.is_compatible_with(&v2));
414        assert!(!v1.is_compatible_with(&v3));
415
416        assert_eq!(v1.to_string(), "1.0.0");
417        assert_eq!(v1.with_tag("alpha".into()).to_string(), "1.0.0-alpha");
418    }
419
420    #[test]
421    fn test_embedding_transform() {
422        let v1 = ModelVersion::new(1, 0, 0);
423        let v2 = ModelVersion::new(1, 1, 0);
424
425        // Identity transform
426        let identity = EmbeddingTransform::identity(v1.clone());
427        let embedding = vec![1.0, 2.0, 3.0];
428        let result = identity.apply(&embedding);
429        assert_eq!(result, embedding);
430
431        // Linear transform (2x2 -> 2x2)
432        let matrix = vec![vec![1.0, 0.0], vec![0.0, 2.0]];
433        let transform = EmbeddingTransform::linear(v1, v2, matrix);
434        let embedding = vec![1.0, 2.0];
435        let result = transform.apply(&embedding);
436        assert_eq!(result, vec![1.0, 4.0]);
437    }
438
439    #[test]
440    fn test_dynamic_index_creation() {
441        let version = ModelVersion::new(1, 0, 0);
442        let index = DynamicIndex::new(version.clone(), 128)
443            .expect("test: DynamicIndex creation should succeed");
444
445        assert_eq!(index.active_version(), version);
446    }
447
448    #[test]
449    fn test_add_version() {
450        let v1 = ModelVersion::new(1, 0, 0);
451        let v2 = ModelVersion::new(1, 1, 0);
452
453        let index =
454            DynamicIndex::new(v1.clone(), 128).expect("test: DynamicIndex creation should succeed");
455        index
456            .add_version(v2.clone(), None)
457            .expect("test: add_version should succeed");
458
459        let stats = index.version_stats();
460        assert_eq!(stats.len(), 2);
461        assert!(stats.contains_key(&v1));
462        assert!(stats.contains_key(&v2));
463    }
464
465    #[test]
466    fn test_set_active_version() {
467        let v1 = ModelVersion::new(1, 0, 0);
468        let v2 = ModelVersion::new(1, 1, 0);
469
470        let index =
471            DynamicIndex::new(v1.clone(), 128).expect("test: DynamicIndex creation should succeed");
472        index
473            .add_version(v2.clone(), None)
474            .expect("test: add_version should succeed");
475
476        assert_eq!(index.active_version(), v1);
477
478        index
479            .set_active_version(v2.clone())
480            .expect("test: set_active_version should succeed");
481        assert_eq!(index.active_version(), v2);
482    }
483
484    #[test]
485    fn test_insert_and_update() {
486        use multihash_codetable::{Code, MultihashDigest};
487
488        let version = ModelVersion::new(1, 0, 0);
489        let index =
490            DynamicIndex::new(version, 3).expect("test: DynamicIndex creation should succeed");
491
492        let data = "test_embedding";
493        let hash = Code::Sha2_256.digest(data.as_bytes());
494        let cid = Cid::new_v1(0x55, hash);
495
496        let embedding = vec![1.0, 2.0, 3.0];
497        index
498            .insert(&cid, &embedding, None)
499            .expect("test: insert should succeed");
500
501        let stats = index.version_stats();
502        assert_eq!(
503            stats
504                .values()
505                .next()
506                .expect("test: stats should have at least one entry")
507                .num_embeddings,
508            1
509        );
510
511        // Update the embedding
512        let new_embedding = vec![4.0, 5.0, 6.0];
513        index
514            .update(&cid, &new_embedding, None)
515            .expect("test: update should succeed");
516
517        let stats = index.version_stats();
518        assert_eq!(
519            stats
520                .values()
521                .next()
522                .expect("test: stats should have at least one entry")
523                .num_embeddings,
524            1
525        );
526    }
527
528    #[test]
529    fn test_online_updater() {
530        use multihash_codetable::{Code, MultihashDigest};
531
532        let updater = OnlineUpdater::new(0.1, 0.9);
533
534        let data = "test";
535        let hash = Code::Sha2_256.digest(data.as_bytes());
536        let cid = Cid::new_v1(0x55, hash);
537
538        let embedding = vec![1.0, 1.0, 1.0];
539        let gradient = vec![0.1, 0.1, 0.1];
540
541        let updated = updater.update(&cid, &embedding, &gradient);
542
543        // With learning_rate=0.1, gradient should decrease embedding
544        assert!(updated[0] < 1.0);
545        assert_eq!(updated.len(), 3);
546
547        let stats = updater.stats();
548        assert_eq!(stats.num_tracked, 1);
549    }
550
551    #[test]
552    fn test_updater_momentum() {
553        use multihash_codetable::{Code, MultihashDigest};
554
555        let updater = OnlineUpdater::new(0.1, 0.9);
556
557        let data = "test";
558        let hash = Code::Sha2_256.digest(data.as_bytes());
559        let cid = Cid::new_v1(0x55, hash);
560
561        let embedding = vec![1.0];
562        let gradient = vec![0.1];
563
564        // First update
565        let updated1 = updater.update(&cid, &embedding, &gradient);
566
567        // Second update with same gradient
568        let updated2 = updater.update(&cid, &updated1, &gradient);
569
570        // With momentum, second update should have larger magnitude
571        let delta1 = (embedding[0] - updated1[0]).abs();
572        let delta2 = (updated1[0] - updated2[0]).abs();
573        assert!(delta2 > delta1);
574    }
575}