1use crate::config::EmbeddingConfig;
8use crate::error::MemoryError;
9use std::future::Future;
10use std::hash::{Hash, Hasher};
11use std::pin::Pin;
12
13pub type EmbedFuture<'a> = Pin<Box<dyn Future<Output = Result<Vec<f32>, MemoryError>> + Send + 'a>>;
15
16pub type EmbedBatchFuture<'a> =
18 Pin<Box<dyn Future<Output = Result<Vec<Vec<f32>>, MemoryError>> + Send + 'a>>;
19
20pub trait Embedder: Send + Sync {
24 fn embed<'a>(&'a self, text: &'a str) -> EmbedFuture<'a>;
26
27 fn embed_batch<'a>(&'a self, texts: Vec<String>) -> EmbedBatchFuture<'a>;
31
32 fn model_name(&self) -> &str;
34
35 fn dimensions(&self) -> usize;
37}
38
39pub struct OllamaEmbedder {
43 client: reqwest::Client,
44 base_url: String,
45 model: String,
46 dimensions: usize,
47 batch_size: usize,
48}
49
50impl OllamaEmbedder {
51 pub fn try_new(config: &EmbeddingConfig) -> Result<Self, MemoryError> {
56 let client = reqwest::Client::builder()
57 .timeout(std::time::Duration::from_secs(config.timeout_secs))
58 .build()
59 .map_err(|e| {
60 MemoryError::EmbedderUnavailable(format!("failed to build HTTP client: {e}"))
61 })?;
62
63 Ok(Self {
64 client,
65 base_url: config.ollama_url.trim_end_matches('/').to_string(),
66 model: config.model.clone(),
67 dimensions: config.dimensions,
68 batch_size: config.batch_size,
69 })
70 }
71
72 }
74
75impl Embedder for OllamaEmbedder {
76 fn embed<'a>(&'a self, text: &'a str) -> EmbedFuture<'a> {
77 Box::pin(async move {
78 let mut results = self.embed_batch(vec![text.to_string()]).await?;
79 results.pop().ok_or_else(|| {
80 MemoryError::Other("Ollama returned empty embeddings for single text".to_string())
81 })
82 })
83 }
84
85 fn embed_batch<'a>(&'a self, texts: Vec<String>) -> EmbedBatchFuture<'a> {
86 Box::pin(async move {
87 let mut all_embeddings = Vec::with_capacity(texts.len());
88
89 for batch in texts.chunks(self.batch_size) {
90 let input: Vec<&str> = batch.iter().map(|s| s.as_str()).collect();
91 let body = serde_json::json!({
92 "model": self.model,
93 "input": input
94 });
95
96 let url = format!("{}/api/embed", self.base_url);
97 let response = self
98 .client
99 .post(&url)
100 .json(&body)
101 .send()
102 .await
103 .map_err(|e| {
104 if e.is_connect() {
105 MemoryError::EmbedderUnavailable(format!(
106 "Ollama not running at {}",
107 self.base_url
108 ))
109 } else if e.is_timeout() {
110 MemoryError::EmbedderUnavailable(format!(
111 "Ollama embedding timed out: {}",
112 e
113 ))
114 } else {
115 MemoryError::EmbeddingRequest(e)
116 }
117 })?;
118
119 if response.status() == reqwest::StatusCode::NOT_FOUND {
120 return Err(MemoryError::EmbedderUnavailable(format!(
121 "Model '{}' not available in Ollama. Run: ollama pull {}",
122 self.model, self.model
123 )));
124 }
125
126 if !response.status().is_success() {
127 let status = response.status();
128 let body = response
129 .text()
130 .await
131 .map_err(|err| format!("failed to read Ollama error body: {err}"));
132 return Err(format_ollama_http_error(status, body));
133 }
134
135 let resp_body: serde_json::Value = response.json().await?;
136 let batch_embeddings = parse_embedding_response(&resp_body, self.dimensions)?;
137 all_embeddings.extend(batch_embeddings);
138 }
139
140 Ok(all_embeddings)
141 })
142 }
143
144 fn model_name(&self) -> &str {
145 &self.model
146 }
147
148 fn dimensions(&self) -> usize {
149 self.dimensions
150 }
151}
152
153#[doc(hidden)]
154pub fn format_ollama_http_error(
155 status: reqwest::StatusCode,
156 body: Result<String, String>,
157) -> MemoryError {
158 match body {
159 Ok(body) => MemoryError::Other(format!(
160 "Ollama returned HTTP {}: {}",
161 status,
162 &body[..body.len().min(500)]
163 )),
164 Err(err) => MemoryError::Other(format!("Ollama returned HTTP {status}; {err}")),
165 }
166}
167
168#[doc(hidden)]
172pub fn parse_embedding_response(
173 body: &serde_json::Value,
174 expected_dims: usize,
175) -> Result<Vec<Vec<f32>>, MemoryError> {
176 let embeddings = body["embeddings"].as_array().ok_or_else(|| {
177 MemoryError::Other("Ollama response missing 'embeddings' field".to_string())
178 })?;
179
180 let mut result = Vec::with_capacity(embeddings.len());
181 for embedding_val in embeddings {
182 let raw_array = embedding_val
183 .as_array()
184 .ok_or_else(|| MemoryError::Other("Embedding is not an array".to_string()))?;
185
186 let mut embedding = Vec::with_capacity(raw_array.len());
187 for (i, v) in raw_array.iter().enumerate() {
188 let val = v.as_f64().ok_or_else(|| {
189 MemoryError::Other(format!(
190 "Embedding dimension {} contains non-numeric value: {}",
191 i, v
192 ))
193 })?;
194 embedding.push(val as f32);
195 }
196
197 if embedding.len() != expected_dims {
198 return Err(MemoryError::DimensionMismatch {
199 expected: expected_dims,
200 actual: embedding.len(),
201 });
202 }
203
204 result.push(embedding);
205 }
206
207 Ok(result)
208}
209
210pub struct MockEmbedder {
217 dimensions: usize,
218}
219
220impl MockEmbedder {
221 pub fn new(dimensions: usize) -> Self {
223 Self { dimensions }
224 }
225}
226
227impl Embedder for MockEmbedder {
228 fn embed<'a>(&'a self, text: &'a str) -> EmbedFuture<'a> {
229 let embedding = deterministic_embedding(text, self.dimensions);
230 Box::pin(async move { Ok(embedding) })
231 }
232
233 fn embed_batch<'a>(&'a self, texts: Vec<String>) -> EmbedBatchFuture<'a> {
234 let embeddings: Vec<Vec<f32>> = texts
235 .iter()
236 .map(|t| deterministic_embedding(t, self.dimensions))
237 .collect();
238 Box::pin(async move { Ok(embeddings) })
239 }
240
241 fn model_name(&self) -> &str {
242 "mock-embedder"
243 }
244
245 fn dimensions(&self) -> usize {
246 self.dimensions
247 }
248}
249
250fn deterministic_embedding(text: &str, dimensions: usize) -> Vec<f32> {
252 let mut hasher = std::hash::DefaultHasher::new();
253 text.hash(&mut hasher);
254 let mut state = hasher.finish();
255 if state == 0 {
256 state = 1;
257 }
258
259 let mut values = Vec::with_capacity(dimensions);
260 for _ in 0..dimensions {
261 state ^= state << 13;
263 state ^= state >> 7;
264 state ^= state << 17;
265 let val = ((state as f64) / (u64::MAX as f64)) * 2.0 - 1.0;
266 values.push(val as f32);
267 }
268
269 let magnitude: f32 = values.iter().map(|v| v * v).sum::<f32>().sqrt();
271 if magnitude > 0.0 {
272 for v in &mut values {
273 *v /= magnitude;
274 }
275 }
276
277 values
278}
279
280pub type MultiEmbedFuture<'a> =
284 Pin<Box<dyn Future<Output = Result<MultiFunctionEmbedding, MemoryError>> + Send + 'a>>;
285
286pub type MultiEmbedBatchFuture<'a> =
288 Pin<Box<dyn Future<Output = Result<Vec<MultiFunctionEmbedding>, MemoryError>> + Send + 'a>>;
289
290#[derive(Debug, Clone, PartialEq)]
295pub struct SparseWeights {
296 pub entries: Vec<(usize, f32)>,
298}
299
300impl SparseWeights {
301 #[must_use]
308 pub fn from_dense(vec: &[f32], top_k: usize, min_weight: f32) -> Self {
309 let mut entries: Vec<(usize, f32)> = vec
310 .iter()
311 .enumerate()
312 .map(|(i, &v)| (i, v))
313 .filter(|(_, v)| v.abs() >= min_weight)
314 .collect();
315 entries.sort_by(|a, b| {
316 b.1.abs()
317 .partial_cmp(&a.1.abs())
318 .unwrap_or(std::cmp::Ordering::Equal)
319 });
320 entries.truncate(top_k);
321 Self { entries }
322 }
323
324 #[must_use]
326 pub fn from_entries(mut entries: Vec<(usize, f32)>) -> Self {
327 entries.sort_by(|a, b| {
328 b.1.abs()
329 .partial_cmp(&a.1.abs())
330 .unwrap_or(std::cmp::Ordering::Equal)
331 });
332 Self { entries }
333 }
334
335 pub fn len(&self) -> usize {
337 self.entries.len()
338 }
339
340 pub fn is_empty(&self) -> bool {
342 self.entries.is_empty()
343 }
344
345 pub fn dot(&self, other: &SparseWeights) -> f32 {
351 use std::collections::HashMap;
352 let map: HashMap<usize, f32> = other.entries.iter().copied().collect();
353 self.entries
354 .iter()
355 .map(|(idx, w)| w * map.get(idx).copied().unwrap_or(0.0))
356 .sum()
357 }
358}
359
360#[derive(Debug, Clone, PartialEq)]
365pub struct MultiVectorEmbedding {
366 pub token_vectors: Vec<Vec<f32>>,
368}
369
370impl MultiVectorEmbedding {
371 #[must_use]
378 pub fn from_dense_chunked(vec: &[f32], num_tokens: usize) -> Self {
379 if vec.is_empty() || num_tokens == 0 {
380 return Self {
381 token_vectors: Vec::new(),
382 };
383 }
384 let chunk_size = (vec.len() + num_tokens - 1) / num_tokens; let token_vectors = vec
386 .chunks(chunk_size)
387 .map(|chunk| {
388 let mut v = chunk.to_vec();
390 v.resize(chunk_size, 0.0);
391 v
392 })
393 .collect();
394 Self { token_vectors }
395 }
396
397 #[must_use]
399 pub fn from_token_vectors(token_vectors: Vec<Vec<f32>>) -> Self {
400 Self { token_vectors }
401 }
402
403 pub fn len(&self) -> usize {
405 self.token_vectors.len()
406 }
407
408 pub fn is_empty(&self) -> bool {
410 self.token_vectors.is_empty()
411 }
412}
413
414#[derive(Debug, Clone)]
417pub struct MultiFunctionEmbedding {
418 pub dense: Vec<f32>,
420 pub sparse: SparseWeights,
422 pub multi_vec: MultiVectorEmbedding,
424}
425
426pub trait MultiFunctionEmbedder: Send + Sync {
433 fn embed_multi<'a>(&'a self, text: &'a str) -> MultiEmbedFuture<'a>;
435
436 fn embed_batch_multi<'a>(&'a self, texts: Vec<String>) -> MultiEmbedBatchFuture<'a>;
440
441 fn model_name(&self) -> &str;
443
444 fn dimensions(&self) -> usize;
446}
447
448#[derive(Debug, Clone)]
451pub struct BgeM3DeriveConfig {
452 pub sparse_top_k: usize,
454 pub sparse_min_weight: f32,
456 pub num_multi_vec_tokens: usize,
458}
459
460impl Default for BgeM3DeriveConfig {
461 fn default() -> Self {
462 Self {
463 sparse_top_k: 128,
464 sparse_min_weight: 0.01,
465 num_multi_vec_tokens: 32,
466 }
467 }
468}
469
470pub struct BgeM3Embedder {
489 client: reqwest::Client,
490 base_url: String,
491 model: String,
492 dimensions: usize,
493 batch_size: usize,
494 derive_config: BgeM3DeriveConfig,
495}
496
497impl BgeM3Embedder {
498 pub fn try_new(config: &EmbeddingConfig) -> Result<Self, MemoryError> {
503 Self::try_new_with_derive(config, BgeM3DeriveConfig::default())
504 }
505
506 pub fn try_new_with_derive(
508 config: &EmbeddingConfig,
509 derive_config: BgeM3DeriveConfig,
510 ) -> Result<Self, MemoryError> {
511 let client = reqwest::Client::builder()
512 .timeout(std::time::Duration::from_secs(config.timeout_secs))
513 .build()
514 .map_err(|e| {
515 MemoryError::EmbedderUnavailable(format!("failed to build HTTP client: {e}"))
516 })?;
517
518 Ok(Self {
519 client,
520 base_url: config.ollama_url.trim_end_matches('/').to_string(),
521 model: config.model.clone(),
522 dimensions: config.dimensions,
523 batch_size: config.batch_size,
524 derive_config,
525 })
526 }
527
528 pub fn with_params(
531 base_url: &str,
532 model: &str,
533 dimensions: usize,
534 batch_size: usize,
535 timeout_secs: u64,
536 derive_config: BgeM3DeriveConfig,
537 ) -> Result<Self, MemoryError> {
538 let client = reqwest::Client::builder()
539 .timeout(std::time::Duration::from_secs(timeout_secs))
540 .build()
541 .map_err(|e| {
542 MemoryError::EmbedderUnavailable(format!("failed to build HTTP client: {e}"))
543 })?;
544
545 Ok(Self {
546 client,
547 base_url: base_url.trim_end_matches('/').to_string(),
548 model: model.to_string(),
549 dimensions,
550 batch_size,
551 derive_config,
552 })
553 }
554
555 fn derive_multi_function(
557 &self,
558 dense_embeddings: Vec<Vec<f32>>,
559 ) -> Vec<MultiFunctionEmbedding> {
560 dense_embeddings
561 .into_iter()
562 .map(|dense| {
563 let sparse = SparseWeights::from_dense(
564 &dense,
565 self.derive_config.sparse_top_k,
566 self.derive_config.sparse_min_weight,
567 );
568 let multi_vec = MultiVectorEmbedding::from_dense_chunked(
569 &dense,
570 self.derive_config.num_multi_vec_tokens,
571 );
572 MultiFunctionEmbedding {
573 dense,
574 sparse,
575 multi_vec,
576 }
577 })
578 .collect()
579 }
580
581 async fn fetch_dense(&self, texts: &[String]) -> Result<Vec<Vec<f32>>, MemoryError> {
583 let mut all_embeddings = Vec::with_capacity(texts.len());
584
585 for batch in texts.chunks(self.batch_size) {
586 let input: Vec<&str> = batch.iter().map(|s| s.as_str()).collect();
587 let body = serde_json::json!({
588 "model": self.model,
589 "input": input
590 });
591
592 let url = format!("{}/api/embed", self.base_url);
593 let response = self
594 .client
595 .post(&url)
596 .json(&body)
597 .send()
598 .await
599 .map_err(|e| {
600 if e.is_connect() {
601 MemoryError::EmbedderUnavailable(format!(
602 "Ollama not running at {}",
603 self.base_url
604 ))
605 } else if e.is_timeout() {
606 MemoryError::EmbedderUnavailable(format!(
607 "Ollama embedding timed out: {}",
608 e
609 ))
610 } else {
611 MemoryError::EmbeddingRequest(e)
612 }
613 })?;
614
615 if response.status() == reqwest::StatusCode::NOT_FOUND {
616 return Err(MemoryError::EmbedderUnavailable(format!(
617 "Model '{}' not available in Ollama. Run: ollama pull {}",
618 self.model, self.model
619 )));
620 }
621
622 if !response.status().is_success() {
623 let status = response.status();
624 let body = response
625 .text()
626 .await
627 .map_err(|err| format!("failed to read Ollama error body: {err}"));
628 return Err(format_ollama_http_error(status, body));
629 }
630
631 let resp_body: serde_json::Value = response.json().await?;
632 let batch_embeddings = parse_embedding_response(&resp_body, self.dimensions)?;
633 all_embeddings.extend(batch_embeddings);
634 }
635
636 Ok(all_embeddings)
637 }
638}
639
640impl MultiFunctionEmbedder for BgeM3Embedder {
641 fn embed_multi<'a>(&'a self, text: &'a str) -> MultiEmbedFuture<'a> {
642 Box::pin(async move {
643 let mut results = self
644 .fetch_dense(&[text.to_string()])
645 .await?;
646 let dense = results.pop().ok_or_else(|| {
647 MemoryError::Other("Ollama returned empty embeddings for single text".to_string())
648 })?;
649 let sparse = SparseWeights::from_dense(
650 &dense,
651 self.derive_config.sparse_top_k,
652 self.derive_config.sparse_min_weight,
653 );
654 let multi_vec = MultiVectorEmbedding::from_dense_chunked(
655 &dense,
656 self.derive_config.num_multi_vec_tokens,
657 );
658 Ok(MultiFunctionEmbedding {
659 dense,
660 sparse,
661 multi_vec,
662 })
663 })
664 }
665
666 fn embed_batch_multi<'a>(&'a self, texts: Vec<String>) -> MultiEmbedBatchFuture<'a> {
667 Box::pin(async move {
668 let dense_embeddings = self.fetch_dense(&texts).await?;
669 Ok(self.derive_multi_function(dense_embeddings))
670 })
671 }
672
673 fn model_name(&self) -> &str {
674 &self.model
675 }
676
677 fn dimensions(&self) -> usize {
678 self.dimensions
679 }
680}
681
682impl Embedder for BgeM3Embedder {
685 fn embed<'a>(&'a self, text: &'a str) -> EmbedFuture<'a> {
686 Box::pin(async move {
687 let mut results = self.fetch_dense(&[text.to_string()]).await?;
688 results.pop().ok_or_else(|| {
689 MemoryError::Other("Ollama returned empty embeddings for single text".to_string())
690 })
691 })
692 }
693
694 fn embed_batch<'a>(&'a self, texts: Vec<String>) -> EmbedBatchFuture<'a> {
695 Box::pin(async move { self.fetch_dense(&texts).await })
696 }
697
698 fn model_name(&self) -> &str {
699 &self.model
700 }
701
702 fn dimensions(&self) -> usize {
703 self.dimensions
704 }
705}
706
707#[cfg(feature = "candle-embedder")]
718pub struct CandleEmbedder {
719 model: candle_transformers::models::nomic_bert::NomicBertModel,
720 tokenizer: tokenizers::Tokenizer,
721 device: candle_core::Device,
722 model_id: String,
723 dimensions: usize,
724 max_seq_len: usize,
725}
726
727#[cfg(feature = "candle-embedder")]
728impl CandleEmbedder {
729 pub fn try_new(config: &EmbeddingConfig) -> Result<Self, MemoryError> {
731 Self::try_new_with_model(
732 "nomic-ai/nomic-embed-text-v1.5",
733 config,
734 )
735 }
736
737 pub fn try_new_with_model(model_id: &str, config: &EmbeddingConfig) -> Result<Self, MemoryError> {
742 let device = candle_core::Device::Cpu;
743 let dimensions = config.dimensions;
744 let max_seq_len = 8192; let (owner, name) = match model_id.split_once('/') {
750 Some((o, n)) => (o, n),
751 None => ("nomic-ai", model_id),
752 };
753
754 let api = hf_hub::HFClientSync::new().map_err(|e| {
755 MemoryError::EmbedderUnavailable(format!("failed to create HF Hub client: {e}"))
756 })?;
757 let repo = api.model(owner, name);
758
759 let config_path = download_hf_file(&repo, "config.json")?;
761 let tokenizer_path = download_hf_file(&repo, "tokenizer.json")?;
762
763 let weights_path = download_hf_file(&repo, "model.safetensors")
765 .or_else(|_| download_hf_file(&repo, "pytorch_model.bin"))?;
766
767 let tokenizer = tokenizers::Tokenizer::from_file(&tokenizer_path).map_err(|e| {
769 MemoryError::EmbedderUnavailable(format!("failed to load tokenizer from {}: {e}", tokenizer_path.display()))
770 })?;
771
772 let config_str = std::fs::read_to_string(&config_path).map_err(|e| {
774 MemoryError::EmbedderUnavailable(format!("failed to read config.json: {e}"))
775 })?;
776 let model_config: candle_transformers::models::nomic_bert::Config =
777 serde_json::from_str(&config_str).map_err(|e| {
778 MemoryError::EmbedderUnavailable(format!("failed to parse model config: {e}"))
779 })?;
780
781 if model_config.n_embd != dimensions {
783 return Err(MemoryError::DimensionMismatch {
784 expected: dimensions,
785 actual: model_config.n_embd,
786 });
787 }
788
789 let dtype = candle_core::DType::F32;
791 let weights_bytes = std::fs::read(&weights_path).map_err(|e| {
794 MemoryError::EmbedderUnavailable(format!("failed to read weights file {}: {e}", weights_path.display()))
795 })?;
796 let vb = candle_nn::VarBuilder::from_buffered_safetensors(weights_bytes, dtype, &device)
797 .map_err(|e| {
798 MemoryError::EmbedderUnavailable(format!("failed to load model weights: {e}"))
799 })?;
800
801 let model = candle_transformers::models::nomic_bert::NomicBertModel::load(vb, &model_config)
802 .map_err(|e| {
803 MemoryError::EmbedderUnavailable(format!("failed to build NomicBert model: {e}"))
804 })?;
805
806 Ok(Self {
807 model,
808 tokenizer,
809 device,
810 model_id: model_id.to_string(),
811 dimensions,
812 max_seq_len,
813 })
814 }
815
816 fn embed_batch_sync(&self, texts: &[String], _query_mode: bool) -> Result<Vec<Vec<f32>>, MemoryError> {
818 use candle_core::Tensor;
819 use candle_transformers::models::nomic_bert::{mean_pooling, l2_normalize};
820
821 let mut all_embeddings = Vec::with_capacity(texts.len());
822
823 for text in texts {
831 let prefixed: &str = text;
832
833 let encoding = self.tokenizer.encode(prefixed, true).map_err(|e| {
834 MemoryError::Other(format!("tokenizer error: {e}"))
835 })?;
836
837 let input_ids = encoding.get_ids();
838 let attention_mask = encoding.get_attention_mask();
839
840 let seq_len = input_ids.len().min(self.max_seq_len);
842 let input_ids = &input_ids[..seq_len];
843 let attention_mask = &attention_mask[..seq_len];
844
845 let input_ids_tensor = Tensor::new(input_ids, &self.device)?
846 .unsqueeze(0)?; let attention_mask_tensor = Tensor::new(attention_mask, &self.device)?
848 .unsqueeze(0)?; let token_type_ids = input_ids_tensor.zeros_like()?;
853 let hidden_states = self.model.forward(
854 &input_ids_tensor,
855 Some(&token_type_ids),
856 Some(&attention_mask_tensor),
857 )?;
858
859 let pooled = mean_pooling(&hidden_states, &attention_mask_tensor)?;
861 let normalized = l2_normalize(&pooled)?;
862
863 let embedding_vec = normalized.to_vec2::<f32>()?;
865 let embedding = embedding_vec.into_iter().next().ok_or_else(|| {
866 MemoryError::Other("model returned empty embedding".to_string())
867 })?;
868
869 if embedding.len() != self.dimensions {
870 return Err(MemoryError::DimensionMismatch {
871 expected: self.dimensions,
872 actual: embedding.len(),
873 });
874 }
875
876 all_embeddings.push(embedding);
877 }
878
879 Ok(all_embeddings)
880 }
881}
882
883#[cfg(feature = "candle-embedder")]
886fn download_hf_file(
887 repo: &hf_hub::HFRepositorySync<hf_hub::repository::RepoTypeModel>,
888 filename: &str,
889) -> Result<std::path::PathBuf, MemoryError> {
890 repo.download_file()
891 .filename(filename.to_string())
892 .send()
893 .map_err(|e| {
894 MemoryError::EmbedderUnavailable(format!("failed to download '{filename}': {e}"))
895 })
896}
897
898#[cfg(feature = "candle-embedder")]
899impl Embedder for CandleEmbedder {
900 fn embed<'a>(&'a self, text: &'a str) -> EmbedFuture<'a> {
901 let result = self.embed_batch_sync(&[text.to_string()], true);
902 Box::pin(async move {
903 let mut results = result?;
904 results.pop().ok_or_else(|| {
905 MemoryError::Other("Candle embedder returned empty results".to_string())
906 })
907 })
908 }
909
910 fn embed_batch<'a>(&'a self, texts: Vec<String>) -> EmbedBatchFuture<'a> {
911 let result = self.embed_batch_sync(&texts, false);
912 Box::pin(async move { result })
913 }
914
915 fn model_name(&self) -> &str {
916 &self.model_id
917 }
918
919 fn dimensions(&self) -> usize {
920 self.dimensions
921 }
922}
923
924#[cfg(test)]
927mod bge_m3_tests {
928 use super::*;
929
930 #[test]
933 fn sparse_from_dense_keeps_top_k_by_abs_weight() {
934 let dense = vec![0.5, -0.9, 0.01, 0.8, -0.3, 0.001];
935 let sparse = SparseWeights::from_dense(&dense, 3, 0.05);
936 assert_eq!(sparse.len(), 3);
937 assert_eq!(sparse.entries[0], (1, -0.9));
939 assert_eq!(sparse.entries[1], (3, 0.8));
940 assert_eq!(sparse.entries[2], (0, 0.5));
941 }
942
943 #[test]
944 fn sparse_from_dense_filters_below_threshold() {
945 let dense = vec![0.5, 0.001, 0.9, 0.002];
946 let sparse = SparseWeights::from_dense(&dense, 100, 0.05);
947 assert_eq!(sparse.len(), 2);
948 assert_eq!(sparse.entries[0], (2, 0.9));
949 assert_eq!(sparse.entries[1], (0, 0.5));
950 }
951
952 #[test]
953 fn sparse_from_dense_empty_input() {
954 let sparse = SparseWeights::from_dense(&[], 10, 0.0);
955 assert!(sparse.is_empty());
956 }
957
958 #[test]
959 fn sparse_from_dense_truncates_to_top_k() {
960 let dense = vec![1.0; 100];
961 let sparse = SparseWeights::from_dense(&dense, 10, 0.0);
962 assert_eq!(sparse.len(), 10);
963 }
964
965 #[test]
966 fn sparse_dot_product() {
967 let a = SparseWeights::from_entries(vec![(0, 1.0), (2, 2.0), (5, 3.0)]);
968 let b = SparseWeights::from_entries(vec![(0, 0.5), (2, 1.0), (3, 10.0)]);
969 let result = a.dot(&b);
971 assert!((result - 2.5).abs() < 1e-6);
972 }
973
974 #[test]
975 fn sparse_dot_product_no_overlap() {
976 let a = SparseWeights::from_entries(vec![(0, 1.0), (1, 2.0)]);
977 let b = SparseWeights::from_entries(vec![(2, 3.0), (3, 4.0)]);
978 assert_eq!(a.dot(&b), 0.0);
979 }
980
981 #[test]
982 fn sparse_dot_product_self() {
983 let a = SparseWeights::from_entries(vec![(0, 3.0), (1, 4.0)]);
984 assert!((a.dot(&a) - 25.0).abs() < 1e-6);
986 }
987
988 #[test]
989 fn sparse_from_entries_sorts_by_abs_weight() {
990 let entries = vec![(0, 0.1), (1, -0.5), (2, 0.3)];
991 let sparse = SparseWeights::from_entries(entries);
992 assert_eq!(sparse.entries[0], (1, -0.5));
993 assert_eq!(sparse.entries[1], (2, 0.3));
994 assert_eq!(sparse.entries[2], (0, 0.1));
995 }
996
997 #[test]
1000 fn multi_vec_from_dense_chunked() {
1001 let dense = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
1002 let mv = MultiVectorEmbedding::from_dense_chunked(&dense, 3);
1003 assert_eq!(mv.len(), 3);
1004 assert_eq!(mv.token_vectors[0], vec![1.0, 2.0]);
1006 assert_eq!(mv.token_vectors[1], vec![3.0, 4.0]);
1007 assert_eq!(mv.token_vectors[2], vec![5.0, 6.0]);
1008 }
1009
1010 #[test]
1011 fn multi_vec_from_dense_chunked_with_padding() {
1012 let dense = vec![1.0, 2.0, 3.0, 4.0, 5.0];
1013 let mv = MultiVectorEmbedding::from_dense_chunked(&dense, 2);
1014 assert_eq!(mv.len(), 2);
1015 assert_eq!(mv.token_vectors[0], vec![1.0, 2.0, 3.0]);
1017 assert_eq!(mv.token_vectors[1], vec![4.0, 5.0, 0.0]); }
1019
1020 #[test]
1021 fn multi_vec_from_dense_chunked_empty() {
1022 let mv = MultiVectorEmbedding::from_dense_chunked(&[], 4);
1023 assert!(mv.is_empty());
1024 }
1025
1026 #[test]
1027 fn multi_vec_from_dense_chunked_zero_tokens() {
1028 let mv = MultiVectorEmbedding::from_dense_chunked(&[1.0, 2.0], 0);
1029 assert!(mv.is_empty());
1030 }
1031
1032 #[test]
1033 fn multi_vec_from_token_vectors() {
1034 let tokens = vec![vec![1.0, 0.0], vec![0.0, 1.0], vec![1.0, 1.0]];
1035 let mv = MultiVectorEmbedding::from_token_vectors(tokens.clone());
1036 assert_eq!(mv.len(), 3);
1037 assert_eq!(mv.token_vectors, tokens);
1038 }
1039
1040 #[test]
1041 fn multi_vec_consistent_chunk_sizes() {
1042 let dense: Vec<f32> = (0..1024).map(|i| i as f32).collect();
1043 let mv = MultiVectorEmbedding::from_dense_chunked(&dense, 32);
1044 assert_eq!(mv.len(), 32);
1045 let len0 = mv.token_vectors[0].len();
1047 for tv in &mv.token_vectors {
1048 assert_eq!(tv.len(), len0);
1049 }
1050 }
1051
1052 #[test]
1055 fn multi_function_embedding_holds_all_three() {
1056 let dense = vec![0.1, 0.5, 0.9, 0.3];
1057 let sparse = SparseWeights::from_dense(&dense, 2, 0.1);
1058 let multi_vec = MultiVectorEmbedding::from_dense_chunked(&dense, 2);
1059 let mfe = MultiFunctionEmbedding {
1060 dense: dense.clone(),
1061 sparse: sparse.clone(),
1062 multi_vec: multi_vec.clone(),
1063 };
1064 assert_eq!(mfe.dense, dense);
1065 assert_eq!(mfe.sparse, sparse);
1066 assert_eq!(mfe.multi_vec, multi_vec);
1067 }
1068
1069 #[test]
1072 fn derive_config_default_values() {
1073 let cfg = BgeM3DeriveConfig::default();
1074 assert_eq!(cfg.sparse_top_k, 128);
1075 assert_eq!(cfg.sparse_min_weight, 0.01);
1076 assert_eq!(cfg.num_multi_vec_tokens, 32);
1077 }
1078
1079 #[test]
1082 fn bge_m3_embedder_with_params_constructs() {
1083 let embedder = BgeM3Embedder::with_params(
1084 "http://localhost:11434/",
1085 "bge-m3",
1086 1024,
1087 32,
1088 30,
1089 BgeM3DeriveConfig::default(),
1090 );
1091 assert!(embedder.is_ok());
1092 let embedder = embedder.unwrap();
1093 assert_eq!(Embedder::model_name(&embedder), "bge-m3");
1094 assert_eq!(Embedder::dimensions(&embedder), 1024);
1095 }
1096
1097 #[test]
1098 fn bge_m3_embedder_try_new_from_config() {
1099 let config = EmbeddingConfig {
1100 ollama_url: "http://localhost:11434".to_string(),
1101 model: "bge-m3".to_string(),
1102 dimensions: 1024,
1103 batch_size: 16,
1104 timeout_secs: 60,
1105 };
1106 let embedder = BgeM3Embedder::try_new(&config);
1107 assert!(embedder.is_ok());
1108 let embedder = embedder.unwrap();
1109 assert_eq!(Embedder::model_name(&embedder), "bge-m3");
1110 assert_eq!(Embedder::dimensions(&embedder), 1024);
1111 }
1112
1113 #[test]
1114 fn bge_m3_embedder_try_new_with_custom_derive() {
1115 let config = EmbeddingConfig {
1116 ollama_url: "http://localhost:11434".to_string(),
1117 model: "bge-m3".to_string(),
1118 dimensions: 1024,
1119 batch_size: 16,
1120 timeout_secs: 60,
1121 };
1122 let derive = BgeM3DeriveConfig {
1123 sparse_top_k: 64,
1124 sparse_min_weight: 0.05,
1125 num_multi_vec_tokens: 16,
1126 };
1127 let embedder = BgeM3Embedder::try_new_with_derive(&config, derive);
1128 assert!(embedder.is_ok());
1129 }
1130
1131 #[test]
1134 fn derive_multi_function_produces_correct_lengths() {
1135 let embedder = BgeM3Embedder::with_params(
1136 "http://localhost:11434",
1137 "bge-m3",
1138 1024,
1139 32,
1140 30,
1141 BgeM3DeriveConfig {
1142 sparse_top_k: 64,
1143 sparse_min_weight: 0.0,
1144 num_multi_vec_tokens: 16,
1145 },
1146 )
1147 .unwrap();
1148
1149 let dense_vec: Vec<f32> = (0..1024).map(|i| (i as f32) / 1024.0).collect();
1150 let results = embedder.derive_multi_function(vec![dense_vec.clone()]);
1151 assert_eq!(results.len(), 1);
1152
1153 let mfe = &results[0];
1154 assert_eq!(mfe.dense.len(), 1024);
1155 assert_eq!(mfe.sparse.len(), 64); assert_eq!(mfe.multi_vec.len(), 16); }
1158
1159 #[test]
1160 fn derive_multi_function_handles_multiple_inputs() {
1161 let embedder = BgeM3Embedder::with_params(
1162 "http://localhost:11434",
1163 "bge-m3",
1164 8,
1165 4,
1166 30,
1167 BgeM3DeriveConfig::default(),
1168 )
1169 .unwrap();
1170
1171 let inputs: Vec<Vec<f32>> = (0..5).map(|i| vec![i as f32; 8]).collect();
1172 let results = embedder.derive_multi_function(inputs);
1173 assert_eq!(results.len(), 5);
1174 for mfe in &results {
1175 assert_eq!(mfe.dense.len(), 8);
1176 }
1177 }
1178
1179 #[test]
1180 fn derive_multi_function_empty_input() {
1181 let embedder = BgeM3Embedder::with_params(
1182 "http://localhost:11434",
1183 "bge-m3",
1184 8,
1185 4,
1186 30,
1187 BgeM3DeriveConfig::default(),
1188 )
1189 .unwrap();
1190
1191 let results = embedder.derive_multi_function(vec![]);
1192 assert!(results.is_empty());
1193 }
1194
1195 #[tokio::test]
1198 #[ignore = "requires Ollama running with bge-m3 model pulled"]
1199 async fn bge_m3_embed_multi_live() {
1200 let embedder = BgeM3Embedder::with_params(
1201 "http://127.0.0.1:11434",
1202 "bge-m3",
1203 1024,
1204 32,
1205 60,
1206 BgeM3DeriveConfig::default(),
1207 )
1208 .unwrap();
1209
1210 let result = embedder.embed_multi("hello world").await;
1211 assert!(result.is_ok(), "Ollama call failed: {:?}", result.err());
1212 let mfe = result.unwrap();
1213 assert_eq!(mfe.dense.len(), 1024);
1214 assert!(!mfe.sparse.is_empty());
1215 assert!(!mfe.multi_vec.is_empty());
1216 }
1217
1218 #[tokio::test]
1219 #[ignore = "requires Ollama running with bge-m3 model pulled"]
1220 async fn bge_m3_embed_batch_multi_live() {
1221 let embedder = BgeM3Embedder::with_params(
1222 "http://127.0.0.1:11434",
1223 "bge-m3",
1224 1024,
1225 32,
1226 60,
1227 BgeM3DeriveConfig::default(),
1228 )
1229 .unwrap();
1230
1231 let texts = vec!["hello".to_string(), "world".to_string(), "test".to_string()];
1232 let results = embedder.embed_batch_multi(texts).await;
1233 assert!(results.is_ok(), "Ollama batch call failed: {:?}", results.err());
1234 let embeddings = results.unwrap();
1235 assert_eq!(embeddings.len(), 3);
1236 for mfe in &embeddings {
1237 assert_eq!(mfe.dense.len(), 1024);
1238 }
1239 }
1240
1241 #[tokio::test]
1242 #[ignore = "requires Ollama running with bge-m3 model pulled"]
1243 async fn bge_m3_embedder_as_standard_embedder_live() {
1244 let embedder = BgeM3Embedder::with_params(
1245 "http://127.0.0.1:11434",
1246 "bge-m3",
1247 1024,
1248 32,
1249 60,
1250 BgeM3DeriveConfig::default(),
1251 )
1252 .unwrap();
1253
1254 let result = embedder.embed("hello world").await;
1255 assert!(result.is_ok());
1256 let dense = result.unwrap();
1257 assert_eq!(dense.len(), 1024);
1258 }
1259}