llm_kernel/embedding/
types.rs1use crate::error::{KernelError, Result};
4
5#[derive(Debug, Clone)]
7pub struct EmbeddingResult {
8 pub vector: Vec<f32>,
10 pub text_preview: String,
12}
13
14impl EmbeddingResult {
15 pub fn dim(&self) -> usize {
17 self.vector.len()
18 }
19
20 pub fn cosine_similarity(&self, other: &EmbeddingResult) -> f64 {
22 cosine_similarity(&self.vector, &other.vector)
23 }
24}
25
26#[allow(dead_code)]
30pub(crate) fn text_preview(text: &str) -> String {
31 match text.char_indices().nth(64) {
32 Some((i, _)) => format!("{}…", &text[..i]),
33 None => text.to_string(),
34 }
35}
36
37pub fn normalize(v: &mut [f32]) {
41 let norm: f32 = v.iter().map(|x| x * x).sum::<f32>().sqrt();
42 if norm > 0.0 {
43 v.iter_mut().for_each(|x| *x /= norm);
44 }
45}
46
47pub fn cosine_similarity(a: &[f32], b: &[f32]) -> f64 {
53 if a.len() != b.len() || a.is_empty() {
54 return 0.0;
55 }
56 let (dot, sum_a, sum_b) =
57 a.iter()
58 .zip(b.iter())
59 .fold((0.0f64, 0.0f64, 0.0f64), |(dot, sa, sb), (&x, &y)| {
60 let x = x as f64;
61 let y = y as f64;
62 (dot + x * y, sa + x * x, sb + y * y)
63 });
64 let denom = sum_a.sqrt() * sum_b.sqrt();
65 if denom == 0.0 { 0.0 } else { dot / denom }
66}
67
68pub trait EmbeddingProvider: Send + Sync {
72 fn dim(&self) -> usize;
74
75 fn embed(&self, text: &str) -> Result<EmbeddingResult>;
82
83 fn embed_document(&self, text: &str) -> Result<EmbeddingResult> {
88 self.embed(text)
89 }
90
91 fn embed_batch(&self, texts: &[&str]) -> Result<Vec<EmbeddingResult>> {
98 texts.iter().map(|t| self.embed(t)).collect()
99 }
100
101 fn embed_documents(&self, texts: &[&str]) -> Result<Vec<EmbeddingResult>> {
106 self.embed_batch(texts)
107 }
108
109 fn name(&self) -> &str;
111}
112
113pub fn chunk_batch<'a>(texts: &[&'a str], max_batch_size: usize) -> Result<Vec<Vec<&'a str>>> {
131 if max_batch_size == 0 {
132 return Err(KernelError::Embedding("max_batch_size must be > 0".into()));
133 }
134 Ok(texts.chunks(max_batch_size).map(|c| c.to_vec()).collect())
135}
136
137#[cfg(test)]
138mod tests {
139 use super::*;
140
141 #[test]
142 fn cosine_similarity_identical() {
143 let v = vec![1.0, 0.0, 0.0];
144 assert!((cosine_similarity(&v, &v) - 1.0).abs() < 1e-6);
145 }
146
147 #[test]
148 fn cosine_similarity_orthogonal() {
149 let a = vec![1.0, 0.0];
150 let b = vec![0.0, 1.0];
151 assert!((cosine_similarity(&a, &b)).abs() < 1e-6);
152 }
153
154 #[test]
155 fn cosine_similarity_opposite() {
156 let a = vec![1.0, 0.0];
157 let b = vec![-1.0, 0.0];
158 assert!((cosine_similarity(&a, &b) + 1.0).abs() < 1e-6);
159 }
160
161 #[test]
162 fn cosine_similarity_empty() {
163 assert_eq!(cosine_similarity(&[], &[]), 0.0f64);
164 }
165
166 #[test]
167 fn cosine_similarity_unequal_len() {
168 assert_eq!(cosine_similarity(&[1.0], &[1.0, 2.0]), 0.0f64);
169 }
170
171 #[test]
172 fn embedding_result_similarity() {
173 let a = EmbeddingResult {
174 vector: vec![1.0, 0.0],
175 text_preview: "a".into(),
176 };
177 let b = EmbeddingResult {
178 vector: vec![0.0, 1.0],
179 text_preview: "b".into(),
180 };
181 assert!((a.cosine_similarity(&b)).abs() < 1e-6);
182 }
183
184 #[test]
185 fn embedding_result_dim() {
186 let e = EmbeddingResult {
187 vector: vec![1.0, 0.0, 0.5],
188 text_preview: "test".into(),
189 };
190 assert_eq!(e.dim(), 3);
191 }
192
193 #[test]
197 fn cosine_similarity_f64_precision_high_dim() {
198 let scale = (512f64).sqrt().recip() as f32;
199 let v: Vec<f32> = vec![scale; 512];
200 let sim = cosine_similarity(&v, &v);
201 assert!(
202 (sim - 1.0).abs() < 1e-10,
203 "self-similarity too far from 1.0: {sim}"
204 );
205 }
206
207 #[test]
210 fn cosine_similarity_ranking_preserved() {
211 let n = 384;
212 let base: Vec<f32> = vec![1.0f32; n];
213 let mut nudged = base.clone();
214 nudged[0] = 1.0 + 1e-4;
215 let sim_exact = cosine_similarity(&base, &base);
216 let sim_off = cosine_similarity(&base, &nudged);
217 assert!(
218 sim_exact > sim_off,
219 "ranking flip: self-sim {sim_exact} <= nudged {sim_off}"
220 );
221 }
222
223 #[test]
224 fn chunk_batch_splits_evenly() {
225 let chunks = chunk_batch(&["a", "b", "c", "d"], 2).unwrap();
226 assert_eq!(chunks, vec![vec!["a", "b"], vec!["c", "d"]]);
227 }
228
229 #[test]
230 fn chunk_batch_splits_with_remainder() {
231 let chunks = chunk_batch(&["a", "b", "c", "d", "e"], 2).unwrap();
232 assert_eq!(chunks, vec![vec!["a", "b"], vec!["c", "d"], vec!["e"]]);
233 }
234
235 #[test]
236 fn chunk_batch_empty_input() {
237 let chunks: Vec<Vec<&str>> = chunk_batch(&[], 5).unwrap();
238 assert!(chunks.is_empty());
239 }
240
241 #[test]
242 fn chunk_batch_size_larger_than_input() {
243 let chunks = chunk_batch(&["a", "b"], 10).unwrap();
244 assert_eq!(chunks, vec![vec!["a", "b"]]);
245 }
246
247 #[test]
248 fn chunk_batch_zero_size_errors() {
249 assert!(chunk_batch(&["a"], 0).is_err());
250 }
251}