1use crate::config::EmbeddingConfig;
8use crate::error::MemoryError;
9use std::future::Future;
10use std::hash::{Hash, Hasher};
11use std::pin::Pin;
12
13pub type OptionalMultiEmbedFuture<'a> =
15 Pin<Box<dyn Future<Output = Result<Option<MultiFunctionEmbedding>, MemoryError>> + Send + 'a>>;
16
17pub type OptionalMultiEmbedBatchFuture<'a> = Pin<
19 Box<dyn Future<Output = Result<Option<Vec<MultiFunctionEmbedding>>, MemoryError>> + Send + 'a>,
20>;
21
22pub type EmbedFuture<'a> = Pin<Box<dyn Future<Output = Result<Vec<f32>, MemoryError>> + Send + 'a>>;
24
25pub type EmbedBatchFuture<'a> =
27 Pin<Box<dyn Future<Output = Result<Vec<Vec<f32>>, MemoryError>> + Send + 'a>>;
28
29pub trait Embedder: Send + Sync {
33 fn embed<'a>(&'a self, text: &'a str) -> EmbedFuture<'a>;
35
36 fn embed_batch<'a>(&'a self, texts: Vec<String>) -> EmbedBatchFuture<'a>;
40
41 fn model_name(&self) -> &str;
43
44 fn dimensions(&self) -> usize;
46
47 fn embed_multi_optional<'a>(&'a self, _text: &'a str) -> OptionalMultiEmbedFuture<'a> {
53 Box::pin(async { Ok(None) })
54 }
55
56 fn embed_batch_multi_optional<'a>(
58 &'a self,
59 texts: Vec<String>,
60 ) -> OptionalMultiEmbedBatchFuture<'a> {
61 Box::pin(async move {
62 let mut output = Vec::with_capacity(texts.len());
63 for text in &texts {
64 let Some(multi) = self.embed_multi_optional(text).await? else {
65 return Ok(None);
66 };
67 output.push(multi);
68 }
69 Ok(Some(output))
70 })
71 }
72}
73
74pub struct OllamaEmbedder {
78 client: reqwest::Client,
79 base_url: String,
80 model: String,
81 dimensions: usize,
82 batch_size: usize,
83}
84
85impl OllamaEmbedder {
86 pub fn try_new(config: &EmbeddingConfig) -> Result<Self, MemoryError> {
91 let client = reqwest::Client::builder()
92 .timeout(std::time::Duration::from_secs(config.timeout_secs))
93 .build()
94 .map_err(|e| {
95 MemoryError::EmbedderUnavailable(format!("failed to build HTTP client: {e}"))
96 })?;
97
98 Ok(Self {
99 client,
100 base_url: config.ollama_url.trim_end_matches('/').to_string(),
101 model: config.model.clone(),
102 dimensions: config.dimensions,
103 batch_size: config.batch_size,
104 })
105 }
106
107 }
109
110impl Embedder for OllamaEmbedder {
111 fn embed<'a>(&'a self, text: &'a str) -> EmbedFuture<'a> {
112 Box::pin(async move {
113 let mut results = self.embed_batch(vec![text.to_string()]).await?;
114 results.pop().ok_or_else(|| {
115 MemoryError::Other("Ollama returned empty embeddings for single text".to_string())
116 })
117 })
118 }
119
120 fn embed_batch<'a>(&'a self, texts: Vec<String>) -> EmbedBatchFuture<'a> {
121 Box::pin(async move {
122 let mut all_embeddings = Vec::with_capacity(texts.len());
123
124 for batch in texts.chunks(self.batch_size) {
125 let input: Vec<&str> = batch.iter().map(|s| s.as_str()).collect();
126 let body = serde_json::json!({
127 "model": self.model,
128 "input": input
129 });
130
131 let url = format!("{}/api/embed", self.base_url);
132 let response = self
133 .client
134 .post(&url)
135 .json(&body)
136 .send()
137 .await
138 .map_err(|e| {
139 if e.is_connect() {
140 MemoryError::EmbedderUnavailable(format!(
141 "Ollama not running at {}",
142 self.base_url
143 ))
144 } else if e.is_timeout() {
145 MemoryError::EmbedderUnavailable(format!(
146 "Ollama embedding timed out: {}",
147 e
148 ))
149 } else {
150 MemoryError::EmbeddingRequest(e)
151 }
152 })?;
153
154 if response.status() == reqwest::StatusCode::NOT_FOUND {
155 return Err(MemoryError::EmbedderUnavailable(format!(
156 "Model '{}' not available in Ollama. Run: ollama pull {}",
157 self.model, self.model
158 )));
159 }
160
161 if !response.status().is_success() {
162 let status = response.status();
163 let body = response
164 .text()
165 .await
166 .map_err(|err| format!("failed to read Ollama error body: {err}"));
167 return Err(format_ollama_http_error(status, body));
168 }
169
170 let resp_body: serde_json::Value = response.json().await?;
171 let batch_embeddings = parse_embedding_response(&resp_body, self.dimensions)?;
172 all_embeddings.extend(batch_embeddings);
173 }
174
175 Ok(all_embeddings)
176 })
177 }
178
179 fn model_name(&self) -> &str {
180 &self.model
181 }
182
183 fn dimensions(&self) -> usize {
184 self.dimensions
185 }
186}
187
188#[doc(hidden)]
189pub fn format_ollama_http_error(
190 status: reqwest::StatusCode,
191 body: Result<String, String>,
192) -> MemoryError {
193 match body {
194 Ok(body) => {
195 let preview: String = body.chars().take(500).collect();
196 MemoryError::Other(format!("Ollama returned HTTP {}: {}", status, preview))
197 }
198 Err(err) => MemoryError::Other(format!("Ollama returned HTTP {status}; {err}")),
199 }
200}
201
202#[doc(hidden)]
206pub fn parse_embedding_response(
207 body: &serde_json::Value,
208 expected_dims: usize,
209) -> Result<Vec<Vec<f32>>, MemoryError> {
210 let embeddings = body["embeddings"].as_array().ok_or_else(|| {
211 MemoryError::Other("Ollama response missing 'embeddings' field".to_string())
212 })?;
213
214 let mut result = Vec::with_capacity(embeddings.len());
215 for embedding_val in embeddings {
216 let raw_array = embedding_val
217 .as_array()
218 .ok_or_else(|| MemoryError::Other("Embedding is not an array".to_string()))?;
219
220 let mut embedding = Vec::with_capacity(raw_array.len());
221 for (i, v) in raw_array.iter().enumerate() {
222 let val = v.as_f64().ok_or_else(|| {
223 MemoryError::Other(format!(
224 "Embedding dimension {} contains non-numeric value: {}",
225 i, v
226 ))
227 })?;
228 embedding.push(val as f32);
229 }
230
231 if embedding.len() != expected_dims {
232 return Err(MemoryError::DimensionMismatch {
233 expected: expected_dims,
234 actual: embedding.len(),
235 });
236 }
237
238 result.push(embedding);
239 }
240
241 Ok(result)
242}
243
244pub struct MockEmbedder {
251 dimensions: usize,
252}
253
254impl MockEmbedder {
255 pub fn new(dimensions: usize) -> Self {
257 Self { dimensions }
258 }
259}
260
261impl Embedder for MockEmbedder {
262 fn embed<'a>(&'a self, text: &'a str) -> EmbedFuture<'a> {
263 let embedding = deterministic_embedding(mock_semantic_text(text), self.dimensions);
264 Box::pin(async move { Ok(embedding) })
265 }
266
267 fn embed_batch<'a>(&'a self, texts: Vec<String>) -> EmbedBatchFuture<'a> {
268 let embeddings: Vec<Vec<f32>> = texts
269 .iter()
270 .map(|t| deterministic_embedding(mock_semantic_text(t), self.dimensions))
271 .collect();
272 Box::pin(async move { Ok(embeddings) })
273 }
274
275 fn model_name(&self) -> &str {
276 "mock-embedder"
277 }
278
279 fn dimensions(&self) -> usize {
280 self.dimensions
281 }
282}
283
284fn mock_semantic_text(text: &str) -> &str {
285 text.strip_prefix("search_query: ")
286 .or_else(|| text.strip_prefix("search_document: "))
287 .unwrap_or(text)
288}
289
290fn deterministic_embedding(text: &str, dimensions: usize) -> Vec<f32> {
292 let mut hasher = std::hash::DefaultHasher::new();
293 text.hash(&mut hasher);
294 let mut state = hasher.finish();
295 if state == 0 {
296 state = 1;
297 }
298
299 let mut values = Vec::with_capacity(dimensions);
300 for _ in 0..dimensions {
301 state ^= state << 13;
303 state ^= state >> 7;
304 state ^= state << 17;
305 let val = ((state as f64) / (u64::MAX as f64)) * 2.0 - 1.0;
306 values.push(val as f32);
307 }
308
309 let magnitude: f32 = values.iter().map(|v| v * v).sum::<f32>().sqrt();
311 if magnitude > 0.0 {
312 for v in &mut values {
313 *v /= magnitude;
314 }
315 }
316
317 values
318}
319
320pub type MultiEmbedFuture<'a> =
324 Pin<Box<dyn Future<Output = Result<MultiFunctionEmbedding, MemoryError>> + Send + 'a>>;
325
326pub type MultiEmbedBatchFuture<'a> =
328 Pin<Box<dyn Future<Output = Result<Vec<MultiFunctionEmbedding>, MemoryError>> + Send + 'a>>;
329
330#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
335pub struct SparseWeights {
336 pub entries: Vec<(usize, f32)>,
338}
339
340impl SparseWeights {
341 #[must_use]
348 pub fn from_dense(vec: &[f32], top_k: usize, min_weight: f32) -> Self {
349 let mut entries: Vec<(usize, f32)> = vec
350 .iter()
351 .enumerate()
352 .map(|(i, &v)| (i, v))
353 .filter(|(_, v)| v.abs() >= min_weight)
354 .collect();
355 entries.sort_by(|a, b| {
356 b.1.abs()
357 .partial_cmp(&a.1.abs())
358 .unwrap_or(std::cmp::Ordering::Equal)
359 });
360 entries.truncate(top_k);
361 Self { entries }
362 }
363
364 #[must_use]
366 pub fn from_entries(mut entries: Vec<(usize, f32)>) -> Self {
367 entries.sort_by(|a, b| {
368 b.1.abs()
369 .partial_cmp(&a.1.abs())
370 .unwrap_or(std::cmp::Ordering::Equal)
371 });
372 Self { entries }
373 }
374
375 pub fn len(&self) -> usize {
377 self.entries.len()
378 }
379
380 pub fn is_empty(&self) -> bool {
382 self.entries.is_empty()
383 }
384
385 pub fn dot(&self, other: &SparseWeights) -> f32 {
391 use std::collections::HashMap;
392 let map: HashMap<usize, f32> = other.entries.iter().copied().collect();
393 self.entries
394 .iter()
395 .map(|(idx, w)| w * map.get(idx).copied().unwrap_or(0.0))
396 .sum()
397 }
398}
399
400#[derive(Debug, Clone, PartialEq)]
405pub struct MultiVectorEmbedding {
406 pub token_vectors: Vec<Vec<f32>>,
408}
409
410impl MultiVectorEmbedding {
411 #[must_use]
418 pub fn from_dense_chunked(vec: &[f32], num_tokens: usize) -> Self {
419 if vec.is_empty() || num_tokens == 0 {
420 return Self {
421 token_vectors: Vec::new(),
422 };
423 }
424 let chunk_size = (vec.len() + num_tokens - 1) / num_tokens; let token_vectors = vec
426 .chunks(chunk_size)
427 .map(|chunk| {
428 let mut v = chunk.to_vec();
430 v.resize(chunk_size, 0.0);
431 v
432 })
433 .collect();
434 Self { token_vectors }
435 }
436
437 #[must_use]
439 pub fn from_token_vectors(token_vectors: Vec<Vec<f32>>) -> Self {
440 Self { token_vectors }
441 }
442
443 pub fn len(&self) -> usize {
445 self.token_vectors.len()
446 }
447
448 pub fn is_empty(&self) -> bool {
450 self.token_vectors.is_empty()
451 }
452}
453
454#[derive(Debug, Clone)]
457pub struct MultiFunctionEmbedding {
458 pub dense: Vec<f32>,
460 pub sparse: SparseWeights,
462 pub multi_vec: MultiVectorEmbedding,
464}
465
466pub trait MultiFunctionEmbedder: Send + Sync {
473 fn embed_multi<'a>(&'a self, text: &'a str) -> MultiEmbedFuture<'a>;
475
476 fn embed_batch_multi<'a>(&'a self, texts: Vec<String>) -> MultiEmbedBatchFuture<'a>;
480
481 fn model_name(&self) -> &str;
483
484 fn dimensions(&self) -> usize;
486}
487
488#[derive(Debug, Clone)]
491pub struct BgeM3DeriveConfig {
492 pub sparse_top_k: usize,
494 pub sparse_min_weight: f32,
496 pub num_multi_vec_tokens: usize,
498}
499
500impl Default for BgeM3DeriveConfig {
501 fn default() -> Self {
502 Self {
503 sparse_top_k: 128,
504 sparse_min_weight: 0.01,
505 num_multi_vec_tokens: 32,
506 }
507 }
508}
509
510pub struct BgeM3Embedder {
529 client: reqwest::Client,
530 base_url: String,
531 model: String,
532 dimensions: usize,
533 batch_size: usize,
534 derive_config: BgeM3DeriveConfig,
535}
536
537impl BgeM3Embedder {
538 pub fn try_new(config: &EmbeddingConfig) -> Result<Self, MemoryError> {
543 Self::try_new_with_derive(config, BgeM3DeriveConfig::default())
544 }
545
546 pub fn try_new_with_derive(
548 config: &EmbeddingConfig,
549 derive_config: BgeM3DeriveConfig,
550 ) -> Result<Self, MemoryError> {
551 let client = reqwest::Client::builder()
552 .timeout(std::time::Duration::from_secs(config.timeout_secs))
553 .build()
554 .map_err(|e| {
555 MemoryError::EmbedderUnavailable(format!("failed to build HTTP client: {e}"))
556 })?;
557
558 Ok(Self {
559 client,
560 base_url: config.ollama_url.trim_end_matches('/').to_string(),
561 model: config.model.clone(),
562 dimensions: config.dimensions,
563 batch_size: config.batch_size,
564 derive_config,
565 })
566 }
567
568 pub fn with_params(
571 base_url: &str,
572 model: &str,
573 dimensions: usize,
574 batch_size: usize,
575 timeout_secs: u64,
576 derive_config: BgeM3DeriveConfig,
577 ) -> Result<Self, MemoryError> {
578 let client = reqwest::Client::builder()
579 .timeout(std::time::Duration::from_secs(timeout_secs))
580 .build()
581 .map_err(|e| {
582 MemoryError::EmbedderUnavailable(format!("failed to build HTTP client: {e}"))
583 })?;
584
585 Ok(Self {
586 client,
587 base_url: base_url.trim_end_matches('/').to_string(),
588 model: model.to_string(),
589 dimensions,
590 batch_size,
591 derive_config,
592 })
593 }
594
595 fn derive_multi_function(
597 &self,
598 dense_embeddings: Vec<Vec<f32>>,
599 ) -> Vec<MultiFunctionEmbedding> {
600 dense_embeddings
601 .into_iter()
602 .map(|dense| {
603 let sparse = SparseWeights::from_dense(
604 &dense,
605 self.derive_config.sparse_top_k,
606 self.derive_config.sparse_min_weight,
607 );
608 let multi_vec = MultiVectorEmbedding::from_dense_chunked(
609 &dense,
610 self.derive_config.num_multi_vec_tokens,
611 );
612 MultiFunctionEmbedding {
613 dense,
614 sparse,
615 multi_vec,
616 }
617 })
618 .collect()
619 }
620
621 async fn fetch_dense(&self, texts: &[String]) -> Result<Vec<Vec<f32>>, MemoryError> {
623 let mut all_embeddings = Vec::with_capacity(texts.len());
624
625 for batch in texts.chunks(self.batch_size) {
626 let input: Vec<&str> = batch.iter().map(|s| s.as_str()).collect();
627 let body = serde_json::json!({
628 "model": self.model,
629 "input": input
630 });
631
632 let url = format!("{}/api/embed", self.base_url);
633 let response = self
634 .client
635 .post(&url)
636 .json(&body)
637 .send()
638 .await
639 .map_err(|e| {
640 if e.is_connect() {
641 MemoryError::EmbedderUnavailable(format!(
642 "Ollama not running at {}",
643 self.base_url
644 ))
645 } else if e.is_timeout() {
646 MemoryError::EmbedderUnavailable(format!(
647 "Ollama embedding timed out: {}",
648 e
649 ))
650 } else {
651 MemoryError::EmbeddingRequest(e)
652 }
653 })?;
654
655 if response.status() == reqwest::StatusCode::NOT_FOUND {
656 return Err(MemoryError::EmbedderUnavailable(format!(
657 "Model '{}' not available in Ollama. Run: ollama pull {}",
658 self.model, self.model
659 )));
660 }
661
662 if !response.status().is_success() {
663 let status = response.status();
664 let body = response
665 .text()
666 .await
667 .map_err(|err| format!("failed to read Ollama error body: {err}"));
668 return Err(format_ollama_http_error(status, body));
669 }
670
671 let resp_body: serde_json::Value = response.json().await?;
672 let batch_embeddings = parse_embedding_response(&resp_body, self.dimensions)?;
673 all_embeddings.extend(batch_embeddings);
674 }
675
676 Ok(all_embeddings)
677 }
678}
679
680impl MultiFunctionEmbedder for BgeM3Embedder {
681 fn embed_multi<'a>(&'a self, text: &'a str) -> MultiEmbedFuture<'a> {
682 Box::pin(async move {
683 let mut results = self.fetch_dense(&[text.to_string()]).await?;
684 let dense = results.pop().ok_or_else(|| {
685 MemoryError::Other("Ollama returned empty embeddings for single text".to_string())
686 })?;
687 let sparse = SparseWeights::from_dense(
688 &dense,
689 self.derive_config.sparse_top_k,
690 self.derive_config.sparse_min_weight,
691 );
692 let multi_vec = MultiVectorEmbedding::from_dense_chunked(
693 &dense,
694 self.derive_config.num_multi_vec_tokens,
695 );
696 Ok(MultiFunctionEmbedding {
697 dense,
698 sparse,
699 multi_vec,
700 })
701 })
702 }
703
704 fn embed_batch_multi<'a>(&'a self, texts: Vec<String>) -> MultiEmbedBatchFuture<'a> {
705 Box::pin(async move {
706 let dense_embeddings = self.fetch_dense(&texts).await?;
707 Ok(self.derive_multi_function(dense_embeddings))
708 })
709 }
710
711 fn model_name(&self) -> &str {
712 &self.model
713 }
714
715 fn dimensions(&self) -> usize {
716 self.dimensions
717 }
718}
719
720impl Embedder for BgeM3Embedder {
723 fn embed<'a>(&'a self, text: &'a str) -> EmbedFuture<'a> {
724 Box::pin(async move {
725 let mut results = self.fetch_dense(&[text.to_string()]).await?;
726 results.pop().ok_or_else(|| {
727 MemoryError::Other("Ollama returned empty embeddings for single text".to_string())
728 })
729 })
730 }
731
732 fn embed_batch<'a>(&'a self, texts: Vec<String>) -> EmbedBatchFuture<'a> {
733 Box::pin(async move { self.fetch_dense(&texts).await })
734 }
735
736 fn model_name(&self) -> &str {
737 &self.model
738 }
739
740 fn dimensions(&self) -> usize {
741 self.dimensions
742 }
743
744 fn embed_multi_optional<'a>(&'a self, text: &'a str) -> OptionalMultiEmbedFuture<'a> {
745 Box::pin(async move {
746 MultiFunctionEmbedder::embed_multi(self, text)
747 .await
748 .map(Some)
749 })
750 }
751
752 fn embed_batch_multi_optional<'a>(
753 &'a self,
754 texts: Vec<String>,
755 ) -> OptionalMultiEmbedBatchFuture<'a> {
756 Box::pin(async move {
757 MultiFunctionEmbedder::embed_batch_multi(self, texts)
758 .await
759 .map(Some)
760 })
761 }
762}
763
764#[cfg(feature = "candle-embedder")]
775pub struct CandleEmbedder {
776 model: candle_transformers::models::nomic_bert::NomicBertModel,
777 tokenizer: tokenizers::Tokenizer,
778 device: candle_core::Device,
779 model_id: String,
780 dimensions: usize,
781 max_seq_len: usize,
782}
783
784#[cfg(feature = "candle-embedder")]
785impl CandleEmbedder {
786 pub fn try_new(config: &EmbeddingConfig) -> Result<Self, MemoryError> {
788 Self::try_new_with_model("nomic-ai/nomic-embed-text-v1.5", config)
789 }
790
791 pub fn try_new_with_model(
796 model_id: &str,
797 config: &EmbeddingConfig,
798 ) -> Result<Self, MemoryError> {
799 let device = candle_core::Device::Cpu;
800 let dimensions = config.dimensions;
801 let max_seq_len = 8192; let (owner, name) = match model_id.split_once('/') {
807 Some((o, n)) => (o, n),
808 None => ("nomic-ai", model_id),
809 };
810
811 let api = hf_hub::HFClientSync::new().map_err(|e| {
812 MemoryError::EmbedderUnavailable(format!("failed to create HF Hub client: {e}"))
813 })?;
814 let repo = api.model(owner, name);
815
816 let config_path = download_hf_file(&repo, "config.json")?;
818 let tokenizer_path = download_hf_file(&repo, "tokenizer.json")?;
819
820 let weights_path = download_hf_file(&repo, "model.safetensors")
822 .or_else(|_| download_hf_file(&repo, "pytorch_model.bin"))?;
823
824 let tokenizer = tokenizers::Tokenizer::from_file(&tokenizer_path).map_err(|e| {
826 MemoryError::EmbedderUnavailable(format!(
827 "failed to load tokenizer from {}: {e}",
828 tokenizer_path.display()
829 ))
830 })?;
831
832 let config_str = std::fs::read_to_string(&config_path).map_err(|e| {
834 MemoryError::EmbedderUnavailable(format!("failed to read config.json: {e}"))
835 })?;
836 let model_config: candle_transformers::models::nomic_bert::Config =
837 serde_json::from_str(&config_str).map_err(|e| {
838 MemoryError::EmbedderUnavailable(format!("failed to parse model config: {e}"))
839 })?;
840
841 if model_config.n_embd != dimensions {
843 return Err(MemoryError::DimensionMismatch {
844 expected: dimensions,
845 actual: model_config.n_embd,
846 });
847 }
848
849 let dtype = candle_core::DType::F32;
851 let weights_bytes = std::fs::read(&weights_path).map_err(|e| {
854 MemoryError::EmbedderUnavailable(format!(
855 "failed to read weights file {}: {e}",
856 weights_path.display()
857 ))
858 })?;
859 let vb = candle_nn::VarBuilder::from_buffered_safetensors(weights_bytes, dtype, &device)
860 .map_err(|e| {
861 MemoryError::EmbedderUnavailable(format!("failed to load model weights: {e}"))
862 })?;
863
864 let model =
865 candle_transformers::models::nomic_bert::NomicBertModel::load(vb, &model_config)
866 .map_err(|e| {
867 MemoryError::EmbedderUnavailable(format!(
868 "failed to build NomicBert model: {e}"
869 ))
870 })?;
871
872 Ok(Self {
873 model,
874 tokenizer,
875 device,
876 model_id: model_id.to_string(),
877 dimensions,
878 max_seq_len,
879 })
880 }
881
882 fn embed_batch_sync(
884 &self,
885 texts: &[String],
886 _query_mode: bool,
887 ) -> Result<Vec<Vec<f32>>, MemoryError> {
888 use candle_core::Tensor;
889 use candle_transformers::models::nomic_bert::{l2_normalize, mean_pooling};
890
891 let mut all_embeddings = Vec::with_capacity(texts.len());
892
893 for text in texts {
901 let prefixed: &str = text;
902
903 let encoding = self
904 .tokenizer
905 .encode(prefixed, true)
906 .map_err(|e| MemoryError::Other(format!("tokenizer error: {e}")))?;
907
908 let input_ids = encoding.get_ids();
909 let attention_mask = encoding.get_attention_mask();
910
911 let seq_len = input_ids.len().min(self.max_seq_len);
913 let input_ids = &input_ids[..seq_len];
914 let attention_mask = &attention_mask[..seq_len];
915
916 let input_ids_tensor = Tensor::new(input_ids, &self.device)?.unsqueeze(0)?; let attention_mask_tensor = Tensor::new(attention_mask, &self.device)?.unsqueeze(0)?; let token_type_ids = input_ids_tensor.zeros_like()?;
922 let hidden_states = self.model.forward(
923 &input_ids_tensor,
924 Some(&token_type_ids),
925 Some(&attention_mask_tensor),
926 )?;
927
928 let pooled = mean_pooling(&hidden_states, &attention_mask_tensor)?;
930 let normalized = l2_normalize(&pooled)?;
931
932 let embedding_vec = normalized.to_vec2::<f32>()?;
934 let embedding = embedding_vec
935 .into_iter()
936 .next()
937 .ok_or_else(|| MemoryError::Other("model returned empty embedding".to_string()))?;
938
939 if embedding.len() != self.dimensions {
940 return Err(MemoryError::DimensionMismatch {
941 expected: self.dimensions,
942 actual: embedding.len(),
943 });
944 }
945
946 all_embeddings.push(embedding);
947 }
948
949 Ok(all_embeddings)
950 }
951}
952
953#[cfg(feature = "candle-embedder")]
956fn download_hf_file(
957 repo: &hf_hub::HFRepositorySync<hf_hub::repository::RepoTypeModel>,
958 filename: &str,
959) -> Result<std::path::PathBuf, MemoryError> {
960 repo.download_file()
961 .filename(filename.to_string())
962 .send()
963 .map_err(|e| {
964 MemoryError::EmbedderUnavailable(format!("failed to download '{filename}': {e}"))
965 })
966}
967
968#[cfg(feature = "candle-embedder")]
969impl Embedder for CandleEmbedder {
970 fn embed<'a>(&'a self, text: &'a str) -> EmbedFuture<'a> {
971 let result = self.embed_batch_sync(&[text.to_string()], true);
972 Box::pin(async move {
973 let mut results = result?;
974 results.pop().ok_or_else(|| {
975 MemoryError::Other("Candle embedder returned empty results".to_string())
976 })
977 })
978 }
979
980 fn embed_batch<'a>(&'a self, texts: Vec<String>) -> EmbedBatchFuture<'a> {
981 let result = self.embed_batch_sync(&texts, false);
982 Box::pin(async move { result })
983 }
984
985 fn model_name(&self) -> &str {
986 &self.model_id
987 }
988
989 fn dimensions(&self) -> usize {
990 self.dimensions
991 }
992}
993
994#[cfg(test)]
997mod bge_m3_tests {
998 use super::*;
999
1000 #[test]
1003 fn sparse_from_dense_keeps_top_k_by_abs_weight() {
1004 let dense = vec![0.5, -0.9, 0.01, 0.8, -0.3, 0.001];
1005 let sparse = SparseWeights::from_dense(&dense, 3, 0.05);
1006 assert_eq!(sparse.len(), 3);
1007 assert_eq!(sparse.entries[0], (1, -0.9));
1009 assert_eq!(sparse.entries[1], (3, 0.8));
1010 assert_eq!(sparse.entries[2], (0, 0.5));
1011 }
1012
1013 #[test]
1014 fn sparse_from_dense_filters_below_threshold() {
1015 let dense = vec![0.5, 0.001, 0.9, 0.002];
1016 let sparse = SparseWeights::from_dense(&dense, 100, 0.05);
1017 assert_eq!(sparse.len(), 2);
1018 assert_eq!(sparse.entries[0], (2, 0.9));
1019 assert_eq!(sparse.entries[1], (0, 0.5));
1020 }
1021
1022 #[test]
1023 fn sparse_from_dense_empty_input() {
1024 let sparse = SparseWeights::from_dense(&[], 10, 0.0);
1025 assert!(sparse.is_empty());
1026 }
1027
1028 #[test]
1029 fn sparse_from_dense_truncates_to_top_k() {
1030 let dense = vec![1.0; 100];
1031 let sparse = SparseWeights::from_dense(&dense, 10, 0.0);
1032 assert_eq!(sparse.len(), 10);
1033 }
1034
1035 #[test]
1036 fn sparse_dot_product() {
1037 let a = SparseWeights::from_entries(vec![(0, 1.0), (2, 2.0), (5, 3.0)]);
1038 let b = SparseWeights::from_entries(vec![(0, 0.5), (2, 1.0), (3, 10.0)]);
1039 let result = a.dot(&b);
1041 assert!((result - 2.5).abs() < 1e-6);
1042 }
1043
1044 #[test]
1045 fn sparse_dot_product_no_overlap() {
1046 let a = SparseWeights::from_entries(vec![(0, 1.0), (1, 2.0)]);
1047 let b = SparseWeights::from_entries(vec![(2, 3.0), (3, 4.0)]);
1048 assert_eq!(a.dot(&b), 0.0);
1049 }
1050
1051 #[test]
1052 fn sparse_dot_product_self() {
1053 let a = SparseWeights::from_entries(vec![(0, 3.0), (1, 4.0)]);
1054 assert!((a.dot(&a) - 25.0).abs() < 1e-6);
1056 }
1057
1058 #[test]
1059 fn sparse_from_entries_sorts_by_abs_weight() {
1060 let entries = vec![(0, 0.1), (1, -0.5), (2, 0.3)];
1061 let sparse = SparseWeights::from_entries(entries);
1062 assert_eq!(sparse.entries[0], (1, -0.5));
1063 assert_eq!(sparse.entries[1], (2, 0.3));
1064 assert_eq!(sparse.entries[2], (0, 0.1));
1065 }
1066
1067 #[test]
1070 fn multi_vec_from_dense_chunked() {
1071 let dense = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
1072 let mv = MultiVectorEmbedding::from_dense_chunked(&dense, 3);
1073 assert_eq!(mv.len(), 3);
1074 assert_eq!(mv.token_vectors[0], vec![1.0, 2.0]);
1076 assert_eq!(mv.token_vectors[1], vec![3.0, 4.0]);
1077 assert_eq!(mv.token_vectors[2], vec![5.0, 6.0]);
1078 }
1079
1080 #[test]
1081 fn multi_vec_from_dense_chunked_with_padding() {
1082 let dense = vec![1.0, 2.0, 3.0, 4.0, 5.0];
1083 let mv = MultiVectorEmbedding::from_dense_chunked(&dense, 2);
1084 assert_eq!(mv.len(), 2);
1085 assert_eq!(mv.token_vectors[0], vec![1.0, 2.0, 3.0]);
1087 assert_eq!(mv.token_vectors[1], vec![4.0, 5.0, 0.0]); }
1089
1090 #[test]
1091 fn multi_vec_from_dense_chunked_empty() {
1092 let mv = MultiVectorEmbedding::from_dense_chunked(&[], 4);
1093 assert!(mv.is_empty());
1094 }
1095
1096 #[test]
1097 fn multi_vec_from_dense_chunked_zero_tokens() {
1098 let mv = MultiVectorEmbedding::from_dense_chunked(&[1.0, 2.0], 0);
1099 assert!(mv.is_empty());
1100 }
1101
1102 #[test]
1103 fn multi_vec_from_token_vectors() {
1104 let tokens = vec![vec![1.0, 0.0], vec![0.0, 1.0], vec![1.0, 1.0]];
1105 let mv = MultiVectorEmbedding::from_token_vectors(tokens.clone());
1106 assert_eq!(mv.len(), 3);
1107 assert_eq!(mv.token_vectors, tokens);
1108 }
1109
1110 #[test]
1111 fn multi_vec_consistent_chunk_sizes() {
1112 let dense: Vec<f32> = (0..1024).map(|i| i as f32).collect();
1113 let mv = MultiVectorEmbedding::from_dense_chunked(&dense, 32);
1114 assert_eq!(mv.len(), 32);
1115 let len0 = mv.token_vectors[0].len();
1117 for tv in &mv.token_vectors {
1118 assert_eq!(tv.len(), len0);
1119 }
1120 }
1121
1122 #[test]
1125 fn multi_function_embedding_holds_all_three() {
1126 let dense = vec![0.1, 0.5, 0.9, 0.3];
1127 let sparse = SparseWeights::from_dense(&dense, 2, 0.1);
1128 let multi_vec = MultiVectorEmbedding::from_dense_chunked(&dense, 2);
1129 let mfe = MultiFunctionEmbedding {
1130 dense: dense.clone(),
1131 sparse: sparse.clone(),
1132 multi_vec: multi_vec.clone(),
1133 };
1134 assert_eq!(mfe.dense, dense);
1135 assert_eq!(mfe.sparse, sparse);
1136 assert_eq!(mfe.multi_vec, multi_vec);
1137 }
1138
1139 #[test]
1142 fn derive_config_default_values() {
1143 let cfg = BgeM3DeriveConfig::default();
1144 assert_eq!(cfg.sparse_top_k, 128);
1145 assert_eq!(cfg.sparse_min_weight, 0.01);
1146 assert_eq!(cfg.num_multi_vec_tokens, 32);
1147 }
1148
1149 #[test]
1152 fn bge_m3_embedder_with_params_constructs() {
1153 let embedder = BgeM3Embedder::with_params(
1154 "http://localhost:11434/",
1155 "bge-m3",
1156 1024,
1157 32,
1158 30,
1159 BgeM3DeriveConfig::default(),
1160 );
1161 assert!(embedder.is_ok());
1162 let embedder = embedder.unwrap();
1163 assert_eq!(Embedder::model_name(&embedder), "bge-m3");
1164 assert_eq!(Embedder::dimensions(&embedder), 1024);
1165 }
1166
1167 #[test]
1168 fn bge_m3_embedder_try_new_from_config() {
1169 let config = EmbeddingConfig {
1170 ollama_url: "http://localhost:11434".to_string(),
1171 model: "bge-m3".to_string(),
1172 dimensions: 1024,
1173 batch_size: 16,
1174 timeout_secs: 60,
1175 };
1176 let embedder = BgeM3Embedder::try_new(&config);
1177 assert!(embedder.is_ok());
1178 let embedder = embedder.unwrap();
1179 assert_eq!(Embedder::model_name(&embedder), "bge-m3");
1180 assert_eq!(Embedder::dimensions(&embedder), 1024);
1181 }
1182
1183 #[test]
1184 fn bge_m3_embedder_try_new_with_custom_derive() {
1185 let config = EmbeddingConfig {
1186 ollama_url: "http://localhost:11434".to_string(),
1187 model: "bge-m3".to_string(),
1188 dimensions: 1024,
1189 batch_size: 16,
1190 timeout_secs: 60,
1191 };
1192 let derive = BgeM3DeriveConfig {
1193 sparse_top_k: 64,
1194 sparse_min_weight: 0.05,
1195 num_multi_vec_tokens: 16,
1196 };
1197 let embedder = BgeM3Embedder::try_new_with_derive(&config, derive);
1198 assert!(embedder.is_ok());
1199 }
1200
1201 #[test]
1204 fn derive_multi_function_produces_correct_lengths() {
1205 let embedder = BgeM3Embedder::with_params(
1206 "http://localhost:11434",
1207 "bge-m3",
1208 1024,
1209 32,
1210 30,
1211 BgeM3DeriveConfig {
1212 sparse_top_k: 64,
1213 sparse_min_weight: 0.0,
1214 num_multi_vec_tokens: 16,
1215 },
1216 )
1217 .unwrap();
1218
1219 let dense_vec: Vec<f32> = (0..1024).map(|i| (i as f32) / 1024.0).collect();
1220 let results = embedder.derive_multi_function(vec![dense_vec.clone()]);
1221 assert_eq!(results.len(), 1);
1222
1223 let mfe = &results[0];
1224 assert_eq!(mfe.dense.len(), 1024);
1225 assert_eq!(mfe.sparse.len(), 64); assert_eq!(mfe.multi_vec.len(), 16); }
1228
1229 #[test]
1230 fn derive_multi_function_handles_multiple_inputs() {
1231 let embedder = BgeM3Embedder::with_params(
1232 "http://localhost:11434",
1233 "bge-m3",
1234 8,
1235 4,
1236 30,
1237 BgeM3DeriveConfig::default(),
1238 )
1239 .unwrap();
1240
1241 let inputs: Vec<Vec<f32>> = (0..5).map(|i| vec![i as f32; 8]).collect();
1242 let results = embedder.derive_multi_function(inputs);
1243 assert_eq!(results.len(), 5);
1244 for mfe in &results {
1245 assert_eq!(mfe.dense.len(), 8);
1246 }
1247 }
1248
1249 #[test]
1250 fn derive_multi_function_empty_input() {
1251 let embedder = BgeM3Embedder::with_params(
1252 "http://localhost:11434",
1253 "bge-m3",
1254 8,
1255 4,
1256 30,
1257 BgeM3DeriveConfig::default(),
1258 )
1259 .unwrap();
1260
1261 let results = embedder.derive_multi_function(vec![]);
1262 assert!(results.is_empty());
1263 }
1264
1265 #[tokio::test]
1268 #[ignore = "requires Ollama running with bge-m3 model pulled"]
1269 async fn bge_m3_embed_multi_live() {
1270 let embedder = BgeM3Embedder::with_params(
1271 "http://127.0.0.1:11434",
1272 "bge-m3",
1273 1024,
1274 32,
1275 60,
1276 BgeM3DeriveConfig::default(),
1277 )
1278 .unwrap();
1279
1280 let result = embedder.embed_multi("hello world").await;
1281 assert!(result.is_ok(), "Ollama call failed: {:?}", result.err());
1282 let mfe = result.unwrap();
1283 assert_eq!(mfe.dense.len(), 1024);
1284 assert!(!mfe.sparse.is_empty());
1285 assert!(!mfe.multi_vec.is_empty());
1286 }
1287
1288 #[tokio::test]
1289 #[ignore = "requires Ollama running with bge-m3 model pulled"]
1290 async fn bge_m3_embed_batch_multi_live() {
1291 let embedder = BgeM3Embedder::with_params(
1292 "http://127.0.0.1:11434",
1293 "bge-m3",
1294 1024,
1295 32,
1296 60,
1297 BgeM3DeriveConfig::default(),
1298 )
1299 .unwrap();
1300
1301 let texts = vec!["hello".to_string(), "world".to_string(), "test".to_string()];
1302 let results = embedder.embed_batch_multi(texts).await;
1303 assert!(
1304 results.is_ok(),
1305 "Ollama batch call failed: {:?}",
1306 results.err()
1307 );
1308 let embeddings = results.unwrap();
1309 assert_eq!(embeddings.len(), 3);
1310 for mfe in &embeddings {
1311 assert_eq!(mfe.dense.len(), 1024);
1312 }
1313 }
1314
1315 #[tokio::test]
1316 #[ignore = "requires Ollama running with bge-m3 model pulled"]
1317 async fn bge_m3_embedder_as_standard_embedder_live() {
1318 let embedder = BgeM3Embedder::with_params(
1319 "http://127.0.0.1:11434",
1320 "bge-m3",
1321 1024,
1322 32,
1323 60,
1324 BgeM3DeriveConfig::default(),
1325 )
1326 .unwrap();
1327
1328 let result = embedder.embed("hello world").await;
1329 assert!(result.is_ok());
1330 let dense = result.unwrap();
1331 assert_eq!(dense.len(), 1024);
1332 }
1333}