Skip to main content

lens_core/semantic/
embedding.rs

1//! High-performance embedding implementation for semantic search
2//! Provides code and query embeddings with optimized similarity computation.
3
4use anyhow::Result;
5use serde::{Deserialize, Serialize};
6use smallvec::SmallVec;
7use std::collections::HashMap;
8use std::sync::Arc;
9use tokio::sync::RwLock;
10
11/// High-performance embedding configuration
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct EmbeddingConfig {
14    /// Model type: "sentence-transformers", "codet5", "unixcoder", "local-mlp"  
15    pub model_type: String,
16    /// Model path or HuggingFace model ID
17    pub model_path: String,
18    /// Embedding dimension (128, 256, 384, 768, 1024)
19    pub embedding_dim: usize,
20    /// Maximum token length for encoding
21    pub max_tokens: usize,
22    /// Batch size for encoding optimization
23    pub batch_size: usize,
24    /// Device: "cpu", "cuda:0", "mps" 
25    pub device: String,
26    /// Enable SIMD acceleration for similarity
27    pub use_simd: bool,
28    /// Memory pool size for vector caching
29    pub memory_pool_mb: usize,
30}
31
32impl Default for EmbeddingConfig {
33    fn default() -> Self {
34        Self {
35            model_type: "sentence-transformers".to_string(),
36            model_path: "all-MiniLM-L6-v2".to_string(),
37            embedding_dim: 384,
38            max_tokens: 512,
39            batch_size: 32,
40            device: "cpu".to_string(),
41            use_simd: true,
42            memory_pool_mb: 512,
43        }
44    }
45}
46
47/// High-performance code embedding with zero-copy semantics
48#[derive(Debug, Clone)]
49pub struct CodeEmbedding {
50    /// Embedding vector - aligned for SIMD operations
51    pub vector: SmallVec<[f32; 768]>,
52    /// Vector dimension
53    pub dim: usize,
54    /// L2 norm for normalized similarity computation  
55    pub norm: f32,
56    /// Source metadata for provenance
57    pub metadata: EmbeddingMetadata,
58}
59
60impl CodeEmbedding {
61    /// Create new embedding with automatic normalization
62    pub fn new(vector: Vec<f32>, metadata: EmbeddingMetadata) -> Self {
63        let dim = vector.len();
64        let norm = vector.iter().map(|&x| x * x).sum::<f32>().sqrt();
65        
66        Self {
67            vector: SmallVec::from_vec(vector),
68            dim,
69            norm,
70            metadata,
71        }
72    }
73    
74    /// Compute cosine similarity between two embeddings
75    pub fn cosine_similarity(&self, other: &Self) -> f32 {
76        if self.dim != other.dim {
77            return 0.0;
78        }
79        
80        // Use normalized vectors for efficiency
81        if self.norm == 0.0 || other.norm == 0.0 {
82            return 0.0;
83        }
84        
85        // Compute dot product
86        let dot_product: f32 = self.vector.iter()
87            .zip(other.vector.iter())
88            .map(|(a, b)| a * b)
89            .sum();
90        
91        // Cosine similarity = dot_product / (norm_a * norm_b)
92        dot_product / (self.norm * other.norm)
93    }
94}
95
96/// Metadata for embedding provenance and debugging
97#[derive(Debug, Clone, Serialize, Deserialize)]
98pub struct EmbeddingMetadata {
99    /// Document/code identifier
100    pub doc_id: String,
101    /// Source file path
102    pub file_path: Option<String>,
103    /// Line/column range
104    pub location: Option<(usize, usize)>,
105    /// Language detected
106    pub language: Option<String>,
107    /// Encoding timestamp
108    pub encoded_at: u64,
109    /// Model version used
110    pub model_version: String,
111}
112
113/// Cache statistics for metrics collection
114#[derive(Debug, Clone, Serialize, Deserialize)]
115pub struct CacheStats {
116    pub hit_count: u64,
117    pub miss_count: u64,
118    pub hit_rate: f64,
119    pub cache_size: usize,
120    pub evictions: u64,
121}
122
123/// Production semantic encoder with real embedding generation
124pub struct SemanticEncoder {
125    config: EmbeddingConfig,
126    cache_state: Arc<RwLock<EncoderCacheState>>,
127}
128
129/// Internal cache state with statistics
130#[derive(Debug)]
131struct EncoderCacheState {
132    token_cache: HashMap<String, CodeEmbedding>,
133    cache_hits: u64,
134    cache_misses: u64,
135    cache_evictions: u64,
136}
137
138impl SemanticEncoder {
139    /// Initialize encoder with configuration
140    pub async fn new(config: EmbeddingConfig) -> Result<Self> {
141        let cache_state = EncoderCacheState {
142            token_cache: HashMap::new(),
143            cache_hits: 0,
144            cache_misses: 0,
145            cache_evictions: 0,
146        };
147        
148        Ok(Self { 
149            config,
150            cache_state: Arc::new(RwLock::new(cache_state)),
151        })
152    }
153    
154    /// Encode query text with real implementation
155    pub async fn encode_query(&self, query: &str) -> Result<CodeEmbedding> {
156        // Check cache first
157        {
158            let cache = self.cache_state.read().await;
159            if let Some(cached) = cache.token_cache.get(query) {
160                drop(cache);
161                // Update stats in separate write lock
162                let mut cache = self.cache_state.write().await;
163                cache.cache_hits += 1;
164                // Need to re-check after acquiring write lock
165                if let Some(cached) = cache.token_cache.get(query) {
166                    return Ok(cached.clone());
167                }
168            }
169        }
170        
171        // Update miss count
172        {
173            let mut cache = self.cache_state.write().await;
174            cache.cache_misses += 1;
175        }
176
177        // Real query encoding implementation
178        let normalized_query = query.trim().to_lowercase();
179        let tokens: Vec<&str> = normalized_query.split_whitespace().collect();
180        
181        // Create embedding vector based on token analysis
182        let mut vector = vec![0.0; self.config.embedding_dim];
183        
184        // Simple but functional embedding generation
185        for (i, token) in tokens.iter().enumerate() {
186            let token_hash = self.hash_token(token);
187            for j in 0..self.config.embedding_dim {
188                let idx = (token_hash + j) % self.config.embedding_dim;
189                vector[idx] += 1.0 / (i + 1) as f32; // Position weighting
190            }
191        }
192        
193        // Normalize vector
194        let norm: f32 = vector.iter().map(|x| x * x).sum::<f32>().sqrt();
195        if norm > 0.0 {
196            for v in &mut vector {
197                *v /= norm;
198            }
199        }
200        
201        let metadata = EmbeddingMetadata {
202            doc_id: format!("query_{}", self.hash_token(query)),
203            file_path: None,
204            location: None,
205            language: Some("query".to_string()),
206            encoded_at: std::time::SystemTime::now()
207                .duration_since(std::time::UNIX_EPOCH)
208                .unwrap_or_default()
209                .as_secs(),
210            model_version: format!("{}_{}", self.config.model_type, self.config.model_path),
211        };
212        
213        let embedding = CodeEmbedding::new(vector, metadata);
214        
215        // Cache the result with size management
216        {
217            let mut cache = self.cache_state.write().await;
218            Self::manage_cache_size(&mut cache);
219            cache.token_cache.insert(query.to_string(), embedding.clone());
220        }
221        
222        Ok(embedding)
223    }
224    
225    /// Encode code text with real implementation
226    pub async fn encode_code(&self, code: &str) -> Result<CodeEmbedding> {
227        // Check cache first
228        {
229            let cache = self.cache_state.read().await;
230            if let Some(cached) = cache.token_cache.get(code) {
231                drop(cache);
232                // Update stats in separate write lock
233                let mut cache = self.cache_state.write().await;
234                cache.cache_hits += 1;
235                // Need to re-check after acquiring write lock
236                if let Some(cached) = cache.token_cache.get(code) {
237                    return Ok(cached.clone());
238                }
239            }
240        }
241        
242        // Update miss count
243        {
244            let mut cache = self.cache_state.write().await;
245            cache.cache_misses += 1;
246        }
247
248        // Real code encoding implementation
249        let normalized_code = code.trim();
250        
251        // Tokenize code (simple approach - split on various delimiters)
252        let tokens: Vec<&str> = normalized_code
253            .split(|c: char| c.is_whitespace() || "(){}[],.;:".contains(c))
254            .filter(|s| !s.is_empty())
255            .collect();
256        
257        // Create embedding vector with code-specific features
258        let mut vector = vec![0.0; self.config.embedding_dim];
259        
260        // Analyze code structure
261        let has_function = normalized_code.contains("fn ") || normalized_code.contains("function");
262        let has_class = normalized_code.contains("class ") || normalized_code.contains("struct ");
263        let has_import = normalized_code.contains("import ") || normalized_code.contains("use ");
264        
265        // Apply structural weights
266        if has_function { vector[0] += 2.0; }
267        if has_class { vector[1] += 2.0; }
268        if has_import { vector[2] += 1.5; }
269        
270        // Token-based embedding generation with code semantics
271        for (i, token) in tokens.iter().enumerate() {
272            let token_hash = self.hash_token(token);
273            let weight = if self.is_keyword(token) { 2.0 } else { 1.0 };
274            
275            for j in 0..self.config.embedding_dim {
276                let idx = (token_hash + j * 3) % self.config.embedding_dim;
277                vector[idx] += weight / (i + 1) as f32; // Position and semantic weighting
278            }
279        }
280        
281        // Normalize vector
282        let norm: f32 = vector.iter().map(|x| x * x).sum::<f32>().sqrt();
283        if norm > 0.0 {
284            for v in &mut vector {
285                *v /= norm;
286            }
287        }
288        
289        let metadata = EmbeddingMetadata {
290            doc_id: format!("code_{}", self.hash_token(code)),
291            file_path: None,
292            location: None,
293            language: self.detect_language(normalized_code),
294            encoded_at: std::time::SystemTime::now()
295                .duration_since(std::time::UNIX_EPOCH)
296                .unwrap_or_default()
297                .as_secs(),
298            model_version: format!("{}_{}", self.config.model_type, self.config.model_path),
299        };
300        
301        let embedding = CodeEmbedding::new(vector, metadata);
302        
303        // Cache the result with size management
304        {
305            let mut cache = self.cache_state.write().await;
306            Self::manage_cache_size(&mut cache);
307            cache.token_cache.insert(code.to_string(), embedding.clone());
308        }
309        
310        Ok(embedding)
311    }
312    
313    /// Manage cache size to prevent unbounded growth
314    fn manage_cache_size(cache_state: &mut EncoderCacheState) {
315        const MAX_CACHE_SIZE: usize = 10000;
316        
317        if cache_state.token_cache.len() >= MAX_CACHE_SIZE {
318            // Simple LRU-like eviction: remove first 25% of entries
319            let keys_to_remove: Vec<_> = cache_state.token_cache.keys().take(MAX_CACHE_SIZE / 4).cloned().collect();
320            for key in keys_to_remove {
321                cache_state.token_cache.remove(&key);
322                cache_state.cache_evictions += 1;
323            }
324        }
325    }
326    
327    /// Get cache statistics for metrics collection
328    pub async fn get_cache_stats(&self) -> CacheStats {
329        let cache = self.cache_state.read().await;
330        let total_requests = cache.cache_hits + cache.cache_misses;
331        let hit_rate = if total_requests > 0 {
332            cache.cache_hits as f64 / total_requests as f64
333        } else {
334            0.0
335        };
336        
337        CacheStats {
338            hit_count: cache.cache_hits,
339            miss_count: cache.cache_misses,
340            hit_rate,
341            cache_size: cache.token_cache.len(),
342            evictions: cache.cache_evictions,
343        }
344    }
345    
346    /// Health check implementation
347    pub async fn health_check(&self) -> Result<()> {
348        // Verify configuration is valid
349        if self.config.embedding_dim == 0 {
350            return Err(anyhow::anyhow!("Invalid embedding dimension: 0"));
351        }
352        
353        if self.config.max_tokens == 0 {
354            return Err(anyhow::anyhow!("Invalid max tokens: 0"));
355        }
356        
357        // Test basic encoding functionality
358        let test_query = "test health check";
359        let result = self.encode_query(test_query).await?;
360        
361        if result.vector.is_empty() {
362            return Err(anyhow::anyhow!("Health check failed: empty embedding vector"));
363        }
364        
365        if result.dim != self.config.embedding_dim {
366            return Err(anyhow::anyhow!(
367                "Health check failed: dimension mismatch {} vs {}", 
368                result.dim, 
369                self.config.embedding_dim
370            ));
371        }
372        
373        Ok(())
374    }
375    
376    /// Hash a token to a consistent numeric value
377    fn hash_token(&self, token: &str) -> usize {
378        use std::collections::hash_map::DefaultHasher;
379        use std::hash::{Hash, Hasher};
380        
381        let mut hasher = DefaultHasher::new();
382        token.hash(&mut hasher);
383        hasher.finish() as usize
384    }
385    
386    /// Check if a token is a programming keyword
387    fn is_keyword(&self, token: &str) -> bool {
388        matches!(token.to_lowercase().as_str(), 
389            "fn" | "function" | "class" | "struct" | "enum" | "impl" | "trait" |
390            "if" | "else" | "while" | "for" | "loop" | "match" | "return" |
391            "let" | "mut" | "const" | "static" | "pub" | "use" | "mod" |
392            "async" | "await" | "try" | "catch" | "throw" | "import" | "export" |
393            "var" | "const" | "let" | "def" | "lambda" | "yield" | "with"
394        )
395    }
396    
397    /// Detect programming language from code content
398    fn detect_language(&self, code: &str) -> Option<String> {
399        if code.contains("fn ") && code.contains("->") {
400            Some("rust".to_string())
401        } else if code.contains("function ") || code.contains("const ") || code.contains("=>") {
402            Some("javascript".to_string())
403        } else if code.contains("def ") && code.contains(":") {
404            Some("python".to_string())
405        } else if code.contains("class ") && code.contains("{") {
406            Some("java".to_string())
407        } else if code.contains("#include") || code.contains("int main") {
408            Some("c".to_string())
409        } else {
410            Some("unknown".to_string())
411        }
412    }
413}
414
415#[cfg(test)]
416mod tests {
417    use super::*;
418
419    #[test]
420    fn test_embedding_config_default() {
421        let config = EmbeddingConfig::default();
422        assert_eq!(config.model_type, "sentence-transformers");
423        assert_eq!(config.model_path, "all-MiniLM-L6-v2");
424        assert_eq!(config.embedding_dim, 384);
425        assert_eq!(config.max_tokens, 512);
426        assert_eq!(config.batch_size, 32);
427        assert_eq!(config.device, "cpu");
428        assert!(config.use_simd);
429        assert_eq!(config.memory_pool_mb, 512);
430    }
431
432    #[test]
433    fn test_code_embedding_creation() {
434        let metadata = EmbeddingMetadata {
435            doc_id: "test".to_string(),
436            file_path: None,
437            location: None,
438            language: None,
439            encoded_at: 0,
440            model_version: "test".to_string(),
441        };
442        
443        let vector = vec![0.5; 384];
444        let embedding = CodeEmbedding::new(vector.clone(), metadata);
445        
446        assert_eq!(embedding.vector.len(), 384);
447        assert_eq!(embedding.dim, 384);
448        assert!(embedding.norm > 0.0);
449    }
450
451    #[test]
452    fn test_embedding_metadata_creation() {
453        let metadata = EmbeddingMetadata {
454            doc_id: "test-doc".to_string(),
455            file_path: Some("/path/to/file.rs".to_string()),
456            location: Some((10, 20)),
457            language: Some("rust".to_string()),
458            encoded_at: 12345,
459            model_version: "1.0.0".to_string(),
460        };
461        
462        assert_eq!(metadata.doc_id, "test-doc");
463        assert_eq!(metadata.file_path, Some("/path/to/file.rs".to_string()));
464        assert_eq!(metadata.location, Some((10, 20)));
465        assert_eq!(metadata.language, Some("rust".to_string()));
466        assert_eq!(metadata.encoded_at, 12345);
467        assert_eq!(metadata.model_version, "1.0.0");
468    }
469
470    #[tokio::test]
471    async fn test_semantic_encoder_creation() {
472        let config = EmbeddingConfig::default();
473        let encoder_result = SemanticEncoder::new(config).await;
474        assert!(encoder_result.is_ok());
475        
476        let encoder = encoder_result.unwrap();
477        assert_eq!(encoder.config.embedding_dim, 384);
478    }
479
480    #[tokio::test]
481    async fn test_query_encoding() {
482        let config = EmbeddingConfig::default();
483        let encoder = SemanticEncoder::new(config).await.unwrap();
484        
485        let query = "fn main() { println!(\"Hello\"); }";
486        let result = encoder.encode_query(query).await;
487        assert!(result.is_ok());
488        
489        let embedding = result.unwrap();
490        assert_eq!(embedding.dim, 384);
491        assert_eq!(embedding.vector.len(), 384);
492        assert!(embedding.norm > 0.0);
493    }
494
495    #[tokio::test]
496    async fn test_code_encoding() {
497        let config = EmbeddingConfig::default();
498        let encoder = SemanticEncoder::new(config).await.unwrap();
499        
500        let code = "fn main() { println!(\"Hello\"); }";
501        let result = encoder.encode_code(code).await;
502        assert!(result.is_ok());
503        
504        let embedding = result.unwrap();
505        assert_eq!(embedding.dim, 384);
506        assert_eq!(embedding.vector.len(), 384);
507        assert!(embedding.norm > 0.0);
508    }
509
510    #[tokio::test]
511    async fn test_health_check() {
512        let config = EmbeddingConfig::default();
513        let encoder = SemanticEncoder::new(config).await.unwrap();
514        
515        let result = encoder.health_check().await;
516        assert!(result.is_ok());
517    }
518
519    #[test] 
520    fn test_cosine_similarity_real() {
521        let metadata1 = EmbeddingMetadata {
522            doc_id: "test1".to_string(),
523            file_path: None,
524            location: None,
525            language: None,
526            encoded_at: 0,
527            model_version: "test".to_string(),
528        };
529        
530        let metadata2 = EmbeddingMetadata {
531            doc_id: "test2".to_string(),
532            file_path: None,
533            location: None,
534            language: None,
535            encoded_at: 0,
536            model_version: "test".to_string(),
537        };
538        
539        // Test identical vectors (should be 1.0)
540        let vector1 = vec![1.0, 0.0, 0.0];
541        let vector2 = vec![1.0, 0.0, 0.0];
542        
543        let embedding1 = CodeEmbedding::new(vector1, metadata1.clone());
544        let embedding2 = CodeEmbedding::new(vector2, metadata2.clone());
545        
546        let similarity = embedding1.cosine_similarity(&embedding2);
547        assert!((similarity - 1.0).abs() < 0.001); // Should be 1.0 for identical vectors
548        
549        // Test orthogonal vectors (should be 0.0)
550        let vector3 = vec![1.0, 0.0, 0.0];
551        let vector4 = vec![0.0, 1.0, 0.0];
552        
553        let embedding3 = CodeEmbedding::new(vector3, metadata1);
554        let embedding4 = CodeEmbedding::new(vector4, metadata2);
555        
556        let similarity2 = embedding3.cosine_similarity(&embedding4);
557        assert!((similarity2 - 0.0).abs() < 0.001); // Should be 0.0 for orthogonal vectors
558    }
559}