1#![allow(dead_code)]
2use std::collections::HashMap;
6use std::f32;
7
8pub const EMBEDDING_DIM: usize = 1536; pub trait EmbeddingProvider: Send + Sync {
12 fn embed(&self, text: &str) -> Result<Vec<f32>, EmbeddingError>;
13 fn embed_batch(&self, texts: &[String]) -> Result<Vec<Vec<f32>>, EmbeddingError>;
14 fn dimension(&self) -> usize;
15}
16
17#[derive(Debug)]
18pub enum EmbeddingError {
19 ApiError(String),
20 InvalidDimension,
21 NetworkError,
22}
23
24impl std::fmt::Display for EmbeddingError {
25 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
26 match self {
27 EmbeddingError::ApiError(msg) => write!(f, "API error: {}", msg),
28 EmbeddingError::InvalidDimension => write!(f, "Invalid embedding dimension"),
29 EmbeddingError::NetworkError => write!(f, "Network error"),
30 }
31 }
32}
33
34pub struct EmbeddingCache {
36 embeddings: HashMap<String, Vec<f32>>,
37 dimension: usize,
38}
39
40impl EmbeddingCache {
41 pub fn new(dimension: usize) -> Self {
42 Self {
43 embeddings: HashMap::new(),
44 dimension,
45 }
46 }
47
48 pub fn insert(&mut self, text: String, embedding: Vec<f32>) {
49 if embedding.len() == self.dimension {
50 self.embeddings.insert(text, embedding);
51 }
52 }
53
54 pub fn get(&self, text: &str) -> Option<&Vec<f32>> {
55 self.embeddings.get(text)
56 }
57
58 pub fn contains(&self, text: &str) -> bool {
59 self.embeddings.contains_key(text)
60 }
61}
62
63#[cfg(target_arch = "x86_64")]
65pub fn cosine_similarity_simd(a: &[f32], b: &[f32]) -> f32 {
66 use std::arch::x86_64::*;
67
68 if a.len() != b.len() {
69 return 0.0;
70 }
71
72 unsafe {
73 let mut dot_sum = _mm256_setzero_ps();
74 let mut norm_a = _mm256_setzero_ps();
75 let mut norm_b = _mm256_setzero_ps();
76
77 let chunks = a.len() / 8;
78
79 for i in 0..chunks {
80 let va = _mm256_loadu_ps(a.as_ptr().add(i * 8));
81 let vb = _mm256_loadu_ps(b.as_ptr().add(i * 8));
82
83 dot_sum = _mm256_fmadd_ps(va, vb, dot_sum);
84 norm_a = _mm256_fmadd_ps(va, va, norm_a);
85 norm_b = _mm256_fmadd_ps(vb, vb, norm_b);
86 }
87
88 let dot = horizontal_sum_ps256(dot_sum);
90 let na = horizontal_sum_ps256(norm_a).sqrt();
91 let nb = horizontal_sum_ps256(norm_b).sqrt();
92
93 let mut dot_rem = 0.0;
95 let mut na_rem = 0.0;
96 let mut nb_rem = 0.0;
97
98 for i in (chunks * 8)..a.len() {
99 dot_rem += a[i] * b[i];
100 na_rem += a[i] * a[i];
101 nb_rem += b[i] * b[i];
102 }
103
104 (dot + dot_rem) / ((na + na_rem.sqrt()) * (nb + nb_rem.sqrt()))
105 }
106}
107
108#[cfg(not(target_arch = "x86_64"))]
109pub fn cosine_similarity_simd(a: &[f32], b: &[f32]) -> f32 {
110 cosine_similarity_scalar(a, b)
111}
112
113pub fn cosine_similarity_scalar(a: &[f32], b: &[f32]) -> f32 {
115 if a.len() != b.len() {
116 return 0.0;
117 }
118
119 let mut dot = 0.0;
120 let mut norm_a = 0.0;
121 let mut norm_b = 0.0;
122
123 for i in 0..a.len() {
124 dot += a[i] * b[i];
125 norm_a += a[i] * a[i];
126 norm_b += b[i] * b[i];
127 }
128
129 dot / (norm_a.sqrt() * norm_b.sqrt())
130}
131
132#[cfg(target_arch = "x86_64")]
133unsafe fn horizontal_sum_ps256(v: std::arch::x86_64::__m256) -> f32 {
134 use std::arch::x86_64::*;
135
136 let high = _mm256_extractf128_ps(v, 1);
137 let low = _mm256_castps256_ps128(v);
138 let sum = _mm_add_ps(high, low);
139 let shuf = _mm_shuffle_ps(sum, sum, 0x0E);
140 let sums = _mm_add_ps(sum, shuf);
141 let shuf2 = _mm_movehl_ps(sums, sums);
142 let result = _mm_add_ss(sums, shuf2);
143 _mm_cvtss_f32(result)
144}
145
146pub struct MockEmbeddingProvider {
148 dimension: usize,
149}
150
151impl MockEmbeddingProvider {
152 pub fn new(dimension: usize) -> Self {
153 Self { dimension }
154 }
155}
156
157impl EmbeddingProvider for MockEmbeddingProvider {
158 fn embed(&self, text: &str) -> Result<Vec<f32>, EmbeddingError> {
159 let mut embedding = vec![0.0; self.dimension];
161 let hash = text
162 .bytes()
163 .fold(0u64, |acc, b| acc.wrapping_mul(31).wrapping_add(b as u64));
164
165 for (i, slot) in embedding.iter_mut().enumerate() {
166 let val = ((hash.wrapping_mul(i as u64 + 1)) % 1000) as f32 / 1000.0;
167 *slot = val * 2.0 - 1.0; }
169
170 let norm: f32 = embedding.iter().map(|x| x * x).sum::<f32>().sqrt();
172 if norm > 0.0 {
173 for val in &mut embedding {
174 *val /= norm;
175 }
176 }
177
178 Ok(embedding)
179 }
180
181 fn embed_batch(&self, texts: &[String]) -> Result<Vec<Vec<f32>>, EmbeddingError> {
182 texts.iter().map(|t| self.embed(t)).collect()
183 }
184
185 fn dimension(&self) -> usize {
186 self.dimension
187 }
188}
189
190#[cfg(not(target_arch = "wasm32"))]
199pub struct PythonEmbeddingProvider {
200 child: std::sync::Mutex<std::process::Child>,
201 stdin: std::sync::Mutex<std::process::ChildStdin>,
202 rx: std::sync::Mutex<std::sync::mpsc::Receiver<Result<Vec<u8>, String>>>,
203 dim: usize,
204 timeout: std::time::Duration,
205}
206
207#[cfg(not(target_arch = "wasm32"))]
208impl PythonEmbeddingProvider {
209 pub fn new(model: &str) -> Result<Self, EmbeddingError> {
214 let python = std::env::var("MICROSCOPE_PYTHON").unwrap_or_else(|_| "python".to_string());
215 let script = std::env::var("MICROSCOPE_EMBED_SCRIPT").unwrap_or_else(|_| {
216 let manifest = format!("{}/embed.py", env!("CARGO_MANIFEST_DIR"));
217 if std::path::Path::new(&manifest).exists() {
218 manifest
219 } else {
220 std::env::var("MICROSCOPE_HOME")
221 .map(|h| format!("{}/embed.py", h))
222 .unwrap_or_else(|_| "embed.py".to_string())
223 }
224 });
225 let timeout_ms = std::env::var("MICROSCOPE_EMBED_TIMEOUT_MS")
226 .ok()
227 .and_then(|v| v.parse::<u64>().ok())
228 .unwrap_or(120_000);
229
230 let mut child = std::process::Command::new(&python)
231 .arg(&script)
232 .arg(model)
233 .stdin(std::process::Stdio::piped())
234 .stdout(std::process::Stdio::piped())
235 .stderr(std::process::Stdio::null())
236 .spawn()
237 .map_err(|e| EmbeddingError::ApiError(format!("spawn {} {}: {}", python, script, e)))?;
238 let stdin = child
239 .stdin
240 .take()
241 .ok_or_else(|| EmbeddingError::ApiError("embedding stdin unavailable".into()))?;
242 let mut stdout = child
243 .stdout
244 .take()
245 .ok_or_else(|| EmbeddingError::ApiError("embedding stdout unavailable".into()))?;
246
247 let (tx, rx) = std::sync::mpsc::channel();
249 std::thread::spawn(move || {
250 use std::io::Read;
251 let mut header = [0u8; 4];
252 let dim = match stdout.read_exact(&mut header) {
253 Ok(()) => u32::from_le_bytes(header) as usize,
254 Err(e) => {
255 let _ = tx.send(Err(format!("read dim header: {}", e)));
256 return;
257 }
258 };
259 let _ = tx.send(Ok(header.to_vec()));
260 let mut buf = vec![0u8; dim * 4];
261 loop {
262 match stdout.read_exact(&mut buf) {
263 Ok(()) => {
264 if tx.send(Ok(buf.clone())).is_err() {
265 break;
266 }
267 }
268 Err(e) => {
269 let _ = tx.send(Err(format!("read embedding: {}", e)));
270 break;
271 }
272 }
273 }
274 });
275
276 let timeout = std::time::Duration::from_millis(timeout_ms);
277 let header_msg = rx
278 .recv_timeout(timeout)
279 .map_err(|_| {
280 let _ = child.kill();
281 EmbeddingError::ApiError(format!(
282 "embedding model '{}' did not initialize within {} ms",
283 model, timeout_ms
284 ))
285 })?
286 .map_err(EmbeddingError::ApiError)?;
287 let dim = u32::from_le_bytes(header_msg[0..4].try_into().unwrap()) as usize;
288
289 Ok(Self {
290 child: std::sync::Mutex::new(child),
291 stdin: std::sync::Mutex::new(stdin),
292 rx: std::sync::Mutex::new(rx),
293 dim,
294 timeout,
295 })
296 }
297
298 fn read_vector(&self) -> Result<Vec<f32>, EmbeddingError> {
299 let rx = self
300 .rx
301 .lock()
302 .map_err(|_| EmbeddingError::ApiError("embedding receiver lock poisoned".into()))?;
303 match rx.recv_timeout(self.timeout) {
304 Ok(Ok(bytes)) => {
305 let mut v = Vec::with_capacity(bytes.len() / 4);
306 for chunk in bytes.as_chunks::<4>().0 {
307 v.push(f32::from_le_bytes(*chunk));
308 }
309 Ok(v)
310 }
311 Ok(Err(e)) => Err(EmbeddingError::ApiError(e)),
312 Err(_) => {
313 let mut child = self.child.lock().map_err(|_| {
314 EmbeddingError::ApiError("embedding child lock poisoned".into())
315 })?;
316 let _ = child.kill();
317 Err(EmbeddingError::ApiError(format!(
318 "embedding provider timed out after {} ms",
319 self.timeout.as_millis()
320 )))
321 }
322 }
323 }
324}
325
326#[cfg(not(target_arch = "wasm32"))]
327impl Drop for PythonEmbeddingProvider {
328 fn drop(&mut self) {
329 if let Ok(mut child) = self.child.lock() {
330 let _ = child.kill();
331 let _ = child.wait();
332 }
333 }
334}
335
336#[cfg(not(target_arch = "wasm32"))]
337impl EmbeddingProvider for PythonEmbeddingProvider {
338 fn embed(&self, text: &str) -> Result<Vec<f32>, EmbeddingError> {
339 use std::io::Write;
340 let mut stdin = self
341 .stdin
342 .lock()
343 .map_err(|_| EmbeddingError::ApiError("embedding stdin lock poisoned".into()))?;
344 writeln!(stdin, "{}", text)
345 .map_err(|e| EmbeddingError::ApiError(format!("write stdin: {}", e)))?;
346 stdin
347 .flush()
348 .map_err(|e| EmbeddingError::ApiError(format!("flush stdin: {}", e)))?;
349 drop(stdin);
350 self.read_vector()
351 }
352
353 fn embed_batch(&self, texts: &[String]) -> Result<Vec<Vec<f32>>, EmbeddingError> {
354 use std::io::Write;
355 let mut stdin = self
356 .stdin
357 .lock()
358 .map_err(|_| EmbeddingError::ApiError("embedding stdin lock poisoned".into()))?;
359 for t in texts {
360 writeln!(stdin, "{}", t)
361 .map_err(|e| EmbeddingError::ApiError(format!("write stdin: {}", e)))?;
362 }
363 stdin
364 .flush()
365 .map_err(|e| EmbeddingError::ApiError(format!("flush stdin: {}", e)))?;
366 drop(stdin);
367 let mut out = Vec::with_capacity(texts.len());
368 for _ in texts {
369 out.push(self.read_vector()?);
370 }
371 Ok(out)
372 }
373
374 fn dimension(&self) -> usize {
375 self.dim
376 }
377}
378
379pub fn with_cached_provider<R>(
395 cfg: &crate::config::Embedding,
396 idx_dim: usize,
397 f: impl FnOnce(&dyn EmbeddingProvider) -> R,
398) -> R {
399 type Cache = (String, String, usize, Box<dyn EmbeddingProvider>);
400 static CACHE: std::sync::OnceLock<std::sync::Mutex<Option<Cache>>> = std::sync::OnceLock::new();
401 let mut guard = CACHE
402 .get_or_init(|| std::sync::Mutex::new(None))
403 .lock()
404 .unwrap_or_else(|e| e.into_inner());
405 let rebuild = match guard.as_ref() {
406 Some((p, m, d, _)) => (p, m, d) != (&cfg.provider, &cfg.model, &idx_dim),
407 None => true,
408 };
409 if rebuild {
410 *guard = Some((
411 cfg.provider.clone(),
412 cfg.model.clone(),
413 idx_dim,
414 provider_from_config(cfg, idx_dim),
415 ));
416 }
417 match guard.as_ref() {
418 Some((_, _, _, provider)) => f(provider.as_ref()),
419 None => unreachable!("the provider was installed above"),
420 }
421}
422
423pub fn provider_from_config(
428 cfg: &crate::config::Embedding,
429 idx_dim: usize,
430) -> Box<dyn EmbeddingProvider> {
431 match cfg.provider.as_str() {
432 #[cfg(not(target_arch = "wasm32"))]
433 "python" => match PythonEmbeddingProvider::new(&cfg.model) {
434 Ok(p) => Box::new(p),
435 Err(e) => {
436 eprintln!(
437 " WARN: python embedding provider unavailable: {} — using mock",
438 e
439 );
440 Box::new(MockEmbeddingProvider::new(idx_dim))
441 }
442 },
443 "candle" => {
444 #[cfg(feature = "embeddings")]
445 {
446 match CandleEmbeddingProvider::new(&cfg.model) {
447 Ok(p) => Box::new(p),
448 Err(e) => {
449 eprintln!(
455 " ERROR: candle provider '{}' failed to load: {:?}\n \
456 Rebuild with --features \"native embeddings\", or set \
457 [embedding] provider = \"python\" / \"mock\" explicitly.\n \
458 Refusing to fall back to mock: it would silently \
459 return meaningless results.",
460 cfg.model, e
461 );
462 std::process::exit(2);
463 }
464 }
465 }
466 #[cfg(not(feature = "embeddings"))]
467 {
468 eprintln!(
469 " ERROR: [embedding] provider = \"candle\" requires the \
470 'embeddings' feature, but the binary was built without it.\n \
471 Rebuild with --features \"native embeddings\", or set \
472 [embedding] provider = \"python\" / \"mock\" explicitly.\n \
473 Refusing to fall back to mock: it would silently return \
474 meaningless results."
475 );
476 std::process::exit(2);
477 }
478 }
479 "none" | "mock" => Box::new(MockEmbeddingProvider::new(idx_dim)),
480 "bge-small" => {
481 #[cfg(feature = "embeddings")]
482 {
483 match CandleEmbeddingProvider::with_config(
486 "BAAI/bge-small-en-v1.5",
487 None,
488 384,
489 cfg.use_gpu,
490 ) {
491 Ok(p) => {
492 eprintln!(
493 " [bge-small] Provider ready (dim=384, use_gpu={})",
494 cfg.use_gpu
495 );
496 Box::new(p)
497 }
498 Err(EmbeddingError::NetworkError) => {
499 eprintln!(" [bge-small] Network/model download failed. Check internet / HuggingFace cache. Falling back to mock provider.");
500 Box::new(MockEmbeddingProvider::new(idx_dim))
501 }
502 Err(e) => {
503 eprintln!(
504 " [bge-small] Init failed: {}. Falling back to mock provider.",
505 e
506 );
507 Box::new(MockEmbeddingProvider::new(idx_dim))
508 }
509 }
510 }
511 #[cfg(not(feature = "embeddings"))]
512 {
513 eprintln!(" WARN: bge-small requires the 'embeddings' feature — using mock");
514 Box::new(MockEmbeddingProvider::new(idx_dim))
515 }
516 }
517 other => {
518 eprintln!(
519 " WARN: unknown embedding provider '{}' — using mock",
520 other
521 );
522 Box::new(MockEmbeddingProvider::new(idx_dim))
523 }
524 }
525}
526
527#[repr(C, packed)]
529pub struct EmbeddedBlockHeader {
530 pub x: f32,
532 pub y: f32,
533 pub z: f32,
534 pub zoom: f32,
535 pub depth: u8,
536 pub layer_id: u8,
537 pub data_offset: u32,
538 pub data_len: u16,
539 pub parent_idx: u32,
540 pub child_count: u16,
541 pub crc16: [u8; 2], pub embedding_offset: u32, pub has_embedding: bool, }
547
548#[cfg(feature = "embeddings")]
550pub struct CandleEmbeddingProvider {
551 model: candle_transformers::models::bert::BertModel,
552 tokenizer: tokenizers::Tokenizer,
553 dim: usize,
554 device: candle_core::Device,
555}
556
557#[cfg(feature = "embeddings")]
558impl CandleEmbeddingProvider {
559 pub fn new(model_id: &str) -> Result<Self, EmbeddingError> {
560 Self::with_config(model_id, None, 0, false)
566 }
567
568 pub fn with_config(
570 model_id: &str,
571 config_override: Option<candle_transformers::models::bert::Config>,
572 dim_override: usize,
573 use_gpu: bool,
574 ) -> Result<Self, EmbeddingError> {
575 use candle_core::Device;
576 use hf_hub::api::sync::Api;
577
578 let device = if use_gpu {
580 match Device::cuda_if_available(0) {
581 Ok(d) => {
582 eprintln!(" [bge] GPU device: {:?}", d);
583 d
584 }
585 Err(e) => {
586 eprintln!(" [bge] CUDA unavailable ({}) — falling back to CPU", e);
587 Device::Cpu
588 }
589 }
590 } else {
591 Device::Cpu
592 };
593 let api = Api::new().map_err(|_| EmbeddingError::NetworkError)?;
594 let repo = api.model(model_id.to_string());
595
596 let tokenizer_path = repo
598 .get("tokenizer.json")
599 .map_err(|_| EmbeddingError::NetworkError)?;
600 let tokenizer = tokenizers::Tokenizer::from_file(tokenizer_path)
601 .map_err(|e| EmbeddingError::ApiError(format!("tokenizer load: {}", e)))?;
602
603 let weights_path = repo
605 .get("model.safetensors")
606 .map_err(|_| EmbeddingError::NetworkError)?;
607 let vb = unsafe {
609 candle_nn::VarBuilder::from_mmaped_safetensors(
610 &[weights_path],
611 candle_core::DType::F32,
612 &device,
613 )
614 }
615 .map_err(|e| EmbeddingError::ApiError(format!("varbuilder: {}", e)))?;
616
617 let config = if let Some(cfg) = config_override {
619 cfg
620 } else {
621 let config_path = repo
623 .get("config.json")
624 .map_err(|_| EmbeddingError::NetworkError)?;
625 let config_str = std::fs::read_to_string(&config_path)
626 .map_err(|e| EmbeddingError::ApiError(format!("config read: {}", e)))?;
627 serde_json::from_str(&config_str)
628 .map_err(|e| EmbeddingError::ApiError(format!("config parse: {}", e)))?
629 };
630 let declared_dim: usize = if dim_override > 0 {
637 dim_override
638 } else {
639 let raw = repo
641 .get("config.json")
642 .ok()
643 .and_then(|p| std::fs::read_to_string(&p).ok())
644 .and_then(|s| serde_json::from_str::<serde_json::Value>(&s).ok())
645 .and_then(|v| v.get("hidden_size").and_then(|h| h.as_u64()))
646 .ok_or_else(|| {
647 EmbeddingError::ApiError(
648 "cannot determine embedding dimension: config.json has no \
649 hidden_size, and no dim override was given"
650 .into(),
651 )
652 })?;
653 raw as usize
654 };
655 if declared_dim == 0 {
656 return Err(EmbeddingError::ApiError(
657 "embedding dimension resolved to 0".into(),
658 ));
659 }
660
661 let model = candle_transformers::models::bert::BertModel::load(vb, &config)
662 .map_err(|e| EmbeddingError::ApiError(format!("model load: {}", e)))?;
663
664 Ok(Self {
665 model,
666 tokenizer,
667 dim: declared_dim,
668 device,
669 })
670 }
671
672 fn embed_inner(&self, text: &str) -> Result<Vec<f32>, EmbeddingError> {
673 use candle_core::Tensor;
674
675 let encoding = self
676 .tokenizer
677 .encode(text, true)
678 .map_err(|e| EmbeddingError::ApiError(format!("tokenize: {}", e)))?;
679
680 let mask = encoding.get_attention_mask();
696 let real_len = mask.iter().filter(|&&m| m != 0).count().max(1);
697 let all_ids = encoding.get_ids();
698 let all_type_ids = encoding.get_type_ids();
699 let ids = &all_ids[..real_len.min(all_ids.len())];
700 let type_ids = &all_type_ids[..real_len.min(all_type_ids.len())];
701 let len = ids.len();
702
703 if let Ok(path) = std::env::var("MICROSCOPE_EMBED_DEBUG") {
712 use std::io::Write as _;
713 if let Ok(mut f) = std::fs::OpenOptions::new()
714 .create(true)
715 .append(true)
716 .open(&path)
717 {
718 let _ = writeln!(f, "ids\t{:?}\ttext\t{:?}", ids, text);
719 }
720 }
721
722 let input_ids = Tensor::new(ids, &self.device)
723 .map_err(|e| EmbeddingError::ApiError(e.to_string()))?
724 .reshape((1, len))
725 .map_err(|e| EmbeddingError::ApiError(e.to_string()))?;
726 let token_type_ids = Tensor::new(type_ids, &self.device)
727 .map_err(|e| EmbeddingError::ApiError(e.to_string()))?
728 .reshape((1, len))
729 .map_err(|e| EmbeddingError::ApiError(e.to_string()))?;
730
731 let output = self
732 .model
733 .forward(&input_ids, &token_type_ids)
734 .map_err(|e| EmbeddingError::ApiError(format!("forward: {}", e)))?;
735
736 let pooled = output
738 .mean(1)
739 .map_err(|e| EmbeddingError::ApiError(format!("mean pool: {}", e)))?
740 .squeeze(0)
741 .map_err(|e| EmbeddingError::ApiError(format!("squeeze: {}", e)))?;
742
743 let mut embedding: Vec<f32> = pooled
744 .to_vec1()
745 .map_err(|e| EmbeddingError::ApiError(format!("to_vec: {}", e)))?;
746
747 let norm: f32 = embedding.iter().map(|x| x * x).sum::<f32>().sqrt();
749 if norm > 0.0 {
750 for val in &mut embedding {
751 *val /= norm;
752 }
753 }
754
755 Ok(embedding)
756 }
757}
758
759#[cfg(feature = "embeddings")]
760impl EmbeddingProvider for CandleEmbeddingProvider {
761 fn embed(&self, text: &str) -> Result<Vec<f32>, EmbeddingError> {
762 self.embed_inner(text)
763 }
764
765 fn embed_batch(&self, texts: &[String]) -> Result<Vec<Vec<f32>>, EmbeddingError> {
766 texts.iter().map(|t| self.embed_inner(t)).collect()
767 }
768
769 fn dimension(&self) -> usize {
770 self.dim
771 }
772}
773
774#[cfg(test)]
775mod tests {
776 use super::*;
777
778 #[test]
779 fn test_cosine_similarity() {
780 let a = vec![1.0, 0.0, 0.0];
781 let b = vec![1.0, 0.0, 0.0];
782 assert!((cosine_similarity_scalar(&a, &b) - 1.0).abs() < 0.001);
783
784 let c = vec![0.0, 1.0, 0.0];
785 assert!((cosine_similarity_scalar(&a, &c) - 0.0).abs() < 0.001);
786
787 let d = vec![-1.0, 0.0, 0.0];
788 assert!((cosine_similarity_scalar(&a, &d) - -1.0).abs() < 0.001);
789 }
790
791 #[test]
792 fn test_mock_embeddings() {
793 let provider = MockEmbeddingProvider::new(128);
794 let embedding = provider.embed("test text").unwrap();
795
796 assert_eq!(embedding.len(), 128);
797
798 let norm: f32 = embedding.iter().map(|x| x * x).sum::<f32>().sqrt();
800 assert!((norm - 1.0).abs() < 0.001);
801 }
802
803 #[test]
804 fn test_embedding_cache() {
805 let mut cache = EmbeddingCache::new(3);
806 let embedding = vec![1.0, 0.0, 0.0];
807
808 cache.insert("test".to_string(), embedding.clone());
809 assert!(cache.contains("test"));
810 assert_eq!(cache.get("test"), Some(&embedding));
811 }
812}