1use anyhow::Result;
5use serde::{Deserialize, Serialize};
6use smallvec::SmallVec;
7use std::collections::HashMap;
8use std::sync::Arc;
9use tokio::sync::RwLock;
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct EmbeddingConfig {
14 pub model_type: String,
16 pub model_path: String,
18 pub embedding_dim: usize,
20 pub max_tokens: usize,
22 pub batch_size: usize,
24 pub device: String,
26 pub use_simd: bool,
28 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#[derive(Debug, Clone)]
49pub struct CodeEmbedding {
50 pub vector: SmallVec<[f32; 768]>,
52 pub dim: usize,
54 pub norm: f32,
56 pub metadata: EmbeddingMetadata,
58}
59
60impl CodeEmbedding {
61 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 pub fn cosine_similarity(&self, other: &Self) -> f32 {
76 if self.dim != other.dim {
77 return 0.0;
78 }
79
80 if self.norm == 0.0 || other.norm == 0.0 {
82 return 0.0;
83 }
84
85 let dot_product: f32 = self.vector.iter()
87 .zip(other.vector.iter())
88 .map(|(a, b)| a * b)
89 .sum();
90
91 dot_product / (self.norm * other.norm)
93 }
94}
95
96#[derive(Debug, Clone, Serialize, Deserialize)]
98pub struct EmbeddingMetadata {
99 pub doc_id: String,
101 pub file_path: Option<String>,
103 pub location: Option<(usize, usize)>,
105 pub language: Option<String>,
107 pub encoded_at: u64,
109 pub model_version: String,
111}
112
113#[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
123pub struct SemanticEncoder {
125 config: EmbeddingConfig,
126 cache_state: Arc<RwLock<EncoderCacheState>>,
127}
128
129#[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 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 pub async fn encode_query(&self, query: &str) -> Result<CodeEmbedding> {
156 {
158 let cache = self.cache_state.read().await;
159 if let Some(cached) = cache.token_cache.get(query) {
160 drop(cache);
161 let mut cache = self.cache_state.write().await;
163 cache.cache_hits += 1;
164 if let Some(cached) = cache.token_cache.get(query) {
166 return Ok(cached.clone());
167 }
168 }
169 }
170
171 {
173 let mut cache = self.cache_state.write().await;
174 cache.cache_misses += 1;
175 }
176
177 let normalized_query = query.trim().to_lowercase();
179 let tokens: Vec<&str> = normalized_query.split_whitespace().collect();
180
181 let mut vector = vec![0.0; self.config.embedding_dim];
183
184 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; }
191 }
192
193 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 {
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 pub async fn encode_code(&self, code: &str) -> Result<CodeEmbedding> {
227 {
229 let cache = self.cache_state.read().await;
230 if let Some(cached) = cache.token_cache.get(code) {
231 drop(cache);
232 let mut cache = self.cache_state.write().await;
234 cache.cache_hits += 1;
235 if let Some(cached) = cache.token_cache.get(code) {
237 return Ok(cached.clone());
238 }
239 }
240 }
241
242 {
244 let mut cache = self.cache_state.write().await;
245 cache.cache_misses += 1;
246 }
247
248 let normalized_code = code.trim();
250
251 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 let mut vector = vec![0.0; self.config.embedding_dim];
259
260 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 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 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; }
279 }
280
281 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 {
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 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 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 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 pub async fn health_check(&self) -> Result<()> {
348 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 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 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 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 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 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); 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); }
559}