convergio_knowledge/
embedder.rs1use fastembed::{EmbeddingModel, InitOptions, TextEmbedding};
7use std::sync::OnceLock;
8use tokio::sync::Mutex;
9use tracing::{info, warn};
10
11static MODEL: OnceLock<Mutex<TextEmbedding>> = OnceLock::new();
13
14fn get_model() -> &'static Mutex<TextEmbedding> {
15 MODEL.get_or_init(|| {
16 info!("loading embedding model AllMiniLML6V2...");
17 let model = TextEmbedding::try_new(
18 InitOptions::new(EmbeddingModel::AllMiniLML6V2).with_show_download_progress(true),
19 )
20 .expect("failed to load embedding model");
21 info!("embedding model loaded (384 dims)");
22 Mutex::new(model)
23 })
24}
25
26pub async fn embed(text: &str) -> Vec<f32> {
28 let model = get_model();
29 let mut guard = model.lock().await;
30 match guard.embed(vec![text.to_string()], None) {
31 Ok(mut results) if !results.is_empty() => results.remove(0),
32 Ok(_) => {
33 warn!("embedding returned empty results");
34 vec![0.0; 384]
35 }
36 Err(e) => {
37 warn!(error = %e, "embedding failed");
38 vec![0.0; 384]
39 }
40 }
41}
42
43pub async fn embed_batch(texts: &[String]) -> Vec<Vec<f32>> {
45 if texts.is_empty() {
46 return vec![];
47 }
48 let model = get_model();
49 let mut guard = model.lock().await;
50 match guard.embed(texts, None) {
51 Ok(results) => results,
52 Err(e) => {
53 warn!(error = %e, "batch embedding failed");
54 texts.iter().map(|_| vec![0.0; 384]).collect()
55 }
56 }
57}
58
59pub const fn embedding_dim() -> usize {
61 384
62}
63
64#[cfg(test)]
65mod tests {
66 use super::*;
67
68 #[tokio::test]
69 async fn embed_produces_384_dims() {
70 let v = embed("test convergio knowledge store").await;
71 assert_eq!(v.len(), 384);
72 let nonzero = v.iter().any(|x| *x != 0.0);
74 assert!(nonzero, "embedding should have non-zero values");
75 }
76
77 #[tokio::test]
78 async fn similar_texts_have_high_similarity() {
79 let a = embed("rate limiter fix for authenticated agents").await;
80 let b = embed("fixing the rate limit for auth users").await;
81 let c = embed("chocolate cake recipe with vanilla frosting").await;
82
83 let sim_ab = cosine(&a, &b);
84 let sim_ac = cosine(&a, &c);
85 assert!(
86 sim_ab > sim_ac,
87 "similar texts should score higher: ab={sim_ab:.3} ac={sim_ac:.3}"
88 );
89 }
90
91 fn cosine(a: &[f32], b: &[f32]) -> f32 {
92 let dot: f32 = a.iter().zip(b).map(|(x, y)| x * y).sum();
93 let na: f32 = a.iter().map(|x| x * x).sum::<f32>().sqrt();
94 let nb: f32 = b.iter().map(|x| x * x).sum::<f32>().sqrt();
95 if na == 0.0 || nb == 0.0 {
96 0.0
97 } else {
98 dot / (na * nb)
99 }
100 }
101}