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>;
77
78 fn embed_batch(&self, texts: &[&str]) -> Result<Vec<EmbeddingResult>> {
85 texts.iter().map(|t| self.embed(t)).collect()
86 }
87
88 fn name(&self) -> &str;
90}
91
92pub fn chunk_batch<'a>(texts: &[&'a str], max_batch_size: usize) -> Result<Vec<Vec<&'a str>>> {
110 if max_batch_size == 0 {
111 return Err(KernelError::Embedding("max_batch_size must be > 0".into()));
112 }
113 Ok(texts.chunks(max_batch_size).map(|c| c.to_vec()).collect())
114}
115
116#[cfg(test)]
117mod tests {
118 use super::*;
119
120 #[test]
121 fn cosine_similarity_identical() {
122 let v = vec![1.0, 0.0, 0.0];
123 assert!((cosine_similarity(&v, &v) - 1.0).abs() < 1e-6);
124 }
125
126 #[test]
127 fn cosine_similarity_orthogonal() {
128 let a = vec![1.0, 0.0];
129 let b = vec![0.0, 1.0];
130 assert!((cosine_similarity(&a, &b)).abs() < 1e-6);
131 }
132
133 #[test]
134 fn cosine_similarity_opposite() {
135 let a = vec![1.0, 0.0];
136 let b = vec![-1.0, 0.0];
137 assert!((cosine_similarity(&a, &b) + 1.0).abs() < 1e-6);
138 }
139
140 #[test]
141 fn cosine_similarity_empty() {
142 assert_eq!(cosine_similarity(&[], &[]), 0.0f64);
143 }
144
145 #[test]
146 fn cosine_similarity_unequal_len() {
147 assert_eq!(cosine_similarity(&[1.0], &[1.0, 2.0]), 0.0f64);
148 }
149
150 #[test]
151 fn embedding_result_similarity() {
152 let a = EmbeddingResult {
153 vector: vec![1.0, 0.0],
154 text_preview: "a".into(),
155 };
156 let b = EmbeddingResult {
157 vector: vec![0.0, 1.0],
158 text_preview: "b".into(),
159 };
160 assert!((a.cosine_similarity(&b)).abs() < 1e-6);
161 }
162
163 #[test]
164 fn embedding_result_dim() {
165 let e = EmbeddingResult {
166 vector: vec![1.0, 0.0, 0.5],
167 text_preview: "test".into(),
168 };
169 assert_eq!(e.dim(), 3);
170 }
171
172 #[test]
176 fn cosine_similarity_f64_precision_high_dim() {
177 let scale = (512f64).sqrt().recip() as f32;
178 let v: Vec<f32> = vec![scale; 512];
179 let sim = cosine_similarity(&v, &v);
180 assert!(
181 (sim - 1.0).abs() < 1e-10,
182 "self-similarity too far from 1.0: {sim}"
183 );
184 }
185
186 #[test]
189 fn cosine_similarity_ranking_preserved() {
190 let n = 384;
191 let base: Vec<f32> = vec![1.0f32; n];
192 let mut nudged = base.clone();
193 nudged[0] = 1.0 + 1e-4;
194 let sim_exact = cosine_similarity(&base, &base);
195 let sim_off = cosine_similarity(&base, &nudged);
196 assert!(
197 sim_exact > sim_off,
198 "ranking flip: self-sim {sim_exact} <= nudged {sim_off}"
199 );
200 }
201
202 #[test]
203 fn chunk_batch_splits_evenly() {
204 let chunks = chunk_batch(&["a", "b", "c", "d"], 2).unwrap();
205 assert_eq!(chunks, vec![vec!["a", "b"], vec!["c", "d"]]);
206 }
207
208 #[test]
209 fn chunk_batch_splits_with_remainder() {
210 let chunks = chunk_batch(&["a", "b", "c", "d", "e"], 2).unwrap();
211 assert_eq!(chunks, vec![vec!["a", "b"], vec!["c", "d"], vec!["e"]]);
212 }
213
214 #[test]
215 fn chunk_batch_empty_input() {
216 let chunks: Vec<Vec<&str>> = chunk_batch(&[], 5).unwrap();
217 assert!(chunks.is_empty());
218 }
219
220 #[test]
221 fn chunk_batch_size_larger_than_input() {
222 let chunks = chunk_batch(&["a", "b"], 10).unwrap();
223 assert_eq!(chunks, vec![vec!["a", "b"]]);
224 }
225
226 #[test]
227 fn chunk_batch_zero_size_errors() {
228 assert!(chunk_batch(&["a"], 0).is_err());
229 }
230}