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) => MemoryError::Other(format!(
195 "Ollama returned HTTP {}: {}",
196 status,
197 &body[..body.len().min(500)]
198 )),
199 Err(err) => MemoryError::Other(format!("Ollama returned HTTP {status}; {err}")),
200 }
201}
202
203#[doc(hidden)]
207pub fn parse_embedding_response(
208 body: &serde_json::Value,
209 expected_dims: usize,
210) -> Result<Vec<Vec<f32>>, MemoryError> {
211 let embeddings = body["embeddings"].as_array().ok_or_else(|| {
212 MemoryError::Other("Ollama response missing 'embeddings' field".to_string())
213 })?;
214
215 let mut result = Vec::with_capacity(embeddings.len());
216 for embedding_val in embeddings {
217 let raw_array = embedding_val
218 .as_array()
219 .ok_or_else(|| MemoryError::Other("Embedding is not an array".to_string()))?;
220
221 let mut embedding = Vec::with_capacity(raw_array.len());
222 for (i, v) in raw_array.iter().enumerate() {
223 let val = v.as_f64().ok_or_else(|| {
224 MemoryError::Other(format!(
225 "Embedding dimension {} contains non-numeric value: {}",
226 i, v
227 ))
228 })?;
229 embedding.push(val as f32);
230 }
231
232 if embedding.len() != expected_dims {
233 return Err(MemoryError::DimensionMismatch {
234 expected: expected_dims,
235 actual: embedding.len(),
236 });
237 }
238
239 result.push(embedding);
240 }
241
242 Ok(result)
243}
244
245pub struct MockEmbedder {
252 dimensions: usize,
253}
254
255impl MockEmbedder {
256 pub fn new(dimensions: usize) -> Self {
258 Self { dimensions }
259 }
260}
261
262impl Embedder for MockEmbedder {
263 fn embed<'a>(&'a self, text: &'a str) -> EmbedFuture<'a> {
264 let embedding = deterministic_embedding(mock_semantic_text(text), self.dimensions);
265 Box::pin(async move { Ok(embedding) })
266 }
267
268 fn embed_batch<'a>(&'a self, texts: Vec<String>) -> EmbedBatchFuture<'a> {
269 let embeddings: Vec<Vec<f32>> = texts
270 .iter()
271 .map(|t| deterministic_embedding(mock_semantic_text(t), self.dimensions))
272 .collect();
273 Box::pin(async move { Ok(embeddings) })
274 }
275
276 fn model_name(&self) -> &str {
277 "mock-embedder"
278 }
279
280 fn dimensions(&self) -> usize {
281 self.dimensions
282 }
283}
284
285fn mock_semantic_text(text: &str) -> &str {
286 text.strip_prefix("search_query: ")
287 .or_else(|| text.strip_prefix("search_document: "))
288 .unwrap_or(text)
289}
290
291fn deterministic_embedding(text: &str, dimensions: usize) -> Vec<f32> {
293 let mut hasher = std::hash::DefaultHasher::new();
294 text.hash(&mut hasher);
295 let mut state = hasher.finish();
296 if state == 0 {
297 state = 1;
298 }
299
300 let mut values = Vec::with_capacity(dimensions);
301 for _ in 0..dimensions {
302 state ^= state << 13;
304 state ^= state >> 7;
305 state ^= state << 17;
306 let val = ((state as f64) / (u64::MAX as f64)) * 2.0 - 1.0;
307 values.push(val as f32);
308 }
309
310 let magnitude: f32 = values.iter().map(|v| v * v).sum::<f32>().sqrt();
312 if magnitude > 0.0 {
313 for v in &mut values {
314 *v /= magnitude;
315 }
316 }
317
318 values
319}
320
321pub type MultiEmbedFuture<'a> =
325 Pin<Box<dyn Future<Output = Result<MultiFunctionEmbedding, MemoryError>> + Send + 'a>>;
326
327pub type MultiEmbedBatchFuture<'a> =
329 Pin<Box<dyn Future<Output = Result<Vec<MultiFunctionEmbedding>, MemoryError>> + Send + 'a>>;
330
331#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
336pub struct SparseWeights {
337 pub entries: Vec<(usize, f32)>,
339}
340
341impl SparseWeights {
342 #[must_use]
349 pub fn from_dense(vec: &[f32], top_k: usize, min_weight: f32) -> Self {
350 let mut entries: Vec<(usize, f32)> = vec
351 .iter()
352 .enumerate()
353 .map(|(i, &v)| (i, v))
354 .filter(|(_, v)| v.abs() >= min_weight)
355 .collect();
356 entries.sort_by(|a, b| {
357 b.1.abs()
358 .partial_cmp(&a.1.abs())
359 .unwrap_or(std::cmp::Ordering::Equal)
360 });
361 entries.truncate(top_k);
362 Self { entries }
363 }
364
365 #[must_use]
367 pub fn from_entries(mut entries: Vec<(usize, f32)>) -> Self {
368 entries.sort_by(|a, b| {
369 b.1.abs()
370 .partial_cmp(&a.1.abs())
371 .unwrap_or(std::cmp::Ordering::Equal)
372 });
373 Self { entries }
374 }
375
376 pub fn len(&self) -> usize {
378 self.entries.len()
379 }
380
381 pub fn is_empty(&self) -> bool {
383 self.entries.is_empty()
384 }
385
386 pub fn dot(&self, other: &SparseWeights) -> f32 {
392 use std::collections::HashMap;
393 let map: HashMap<usize, f32> = other.entries.iter().copied().collect();
394 self.entries
395 .iter()
396 .map(|(idx, w)| w * map.get(idx).copied().unwrap_or(0.0))
397 .sum()
398 }
399}
400
401#[derive(Debug, Clone, PartialEq)]
406pub struct MultiVectorEmbedding {
407 pub token_vectors: Vec<Vec<f32>>,
409}
410
411impl MultiVectorEmbedding {
412 #[must_use]
419 pub fn from_dense_chunked(vec: &[f32], num_tokens: usize) -> Self {
420 if vec.is_empty() || num_tokens == 0 {
421 return Self {
422 token_vectors: Vec::new(),
423 };
424 }
425 let chunk_size = (vec.len() + num_tokens - 1) / num_tokens; let token_vectors = vec
427 .chunks(chunk_size)
428 .map(|chunk| {
429 let mut v = chunk.to_vec();
431 v.resize(chunk_size, 0.0);
432 v
433 })
434 .collect();
435 Self { token_vectors }
436 }
437
438 #[must_use]
440 pub fn from_token_vectors(token_vectors: Vec<Vec<f32>>) -> Self {
441 Self { token_vectors }
442 }
443
444 pub fn len(&self) -> usize {
446 self.token_vectors.len()
447 }
448
449 pub fn is_empty(&self) -> bool {
451 self.token_vectors.is_empty()
452 }
453}
454
455#[derive(Debug, Clone)]
458pub struct MultiFunctionEmbedding {
459 pub dense: Vec<f32>,
461 pub sparse: SparseWeights,
463 pub multi_vec: MultiVectorEmbedding,
465}
466
467pub trait MultiFunctionEmbedder: Send + Sync {
474 fn embed_multi<'a>(&'a self, text: &'a str) -> MultiEmbedFuture<'a>;
476
477 fn embed_batch_multi<'a>(&'a self, texts: Vec<String>) -> MultiEmbedBatchFuture<'a>;
481
482 fn model_name(&self) -> &str;
484
485 fn dimensions(&self) -> usize;
487}
488
489#[derive(Debug, Clone)]
492pub struct BgeM3DeriveConfig {
493 pub sparse_top_k: usize,
495 pub sparse_min_weight: f32,
497 pub num_multi_vec_tokens: usize,
499}
500
501impl Default for BgeM3DeriveConfig {
502 fn default() -> Self {
503 Self {
504 sparse_top_k: 128,
505 sparse_min_weight: 0.01,
506 num_multi_vec_tokens: 32,
507 }
508 }
509}
510
511pub struct BgeM3Embedder {
530 client: reqwest::Client,
531 base_url: String,
532 model: String,
533 dimensions: usize,
534 batch_size: usize,
535 derive_config: BgeM3DeriveConfig,
536}
537
538impl BgeM3Embedder {
539 pub fn try_new(config: &EmbeddingConfig) -> Result<Self, MemoryError> {
544 Self::try_new_with_derive(config, BgeM3DeriveConfig::default())
545 }
546
547 pub fn try_new_with_derive(
549 config: &EmbeddingConfig,
550 derive_config: BgeM3DeriveConfig,
551 ) -> Result<Self, MemoryError> {
552 let client = reqwest::Client::builder()
553 .timeout(std::time::Duration::from_secs(config.timeout_secs))
554 .build()
555 .map_err(|e| {
556 MemoryError::EmbedderUnavailable(format!("failed to build HTTP client: {e}"))
557 })?;
558
559 Ok(Self {
560 client,
561 base_url: config.ollama_url.trim_end_matches('/').to_string(),
562 model: config.model.clone(),
563 dimensions: config.dimensions,
564 batch_size: config.batch_size,
565 derive_config,
566 })
567 }
568
569 pub fn with_params(
572 base_url: &str,
573 model: &str,
574 dimensions: usize,
575 batch_size: usize,
576 timeout_secs: u64,
577 derive_config: BgeM3DeriveConfig,
578 ) -> Result<Self, MemoryError> {
579 let client = reqwest::Client::builder()
580 .timeout(std::time::Duration::from_secs(timeout_secs))
581 .build()
582 .map_err(|e| {
583 MemoryError::EmbedderUnavailable(format!("failed to build HTTP client: {e}"))
584 })?;
585
586 Ok(Self {
587 client,
588 base_url: base_url.trim_end_matches('/').to_string(),
589 model: model.to_string(),
590 dimensions,
591 batch_size,
592 derive_config,
593 })
594 }
595
596 fn derive_multi_function(
598 &self,
599 dense_embeddings: Vec<Vec<f32>>,
600 ) -> Vec<MultiFunctionEmbedding> {
601 dense_embeddings
602 .into_iter()
603 .map(|dense| {
604 let sparse = SparseWeights::from_dense(
605 &dense,
606 self.derive_config.sparse_top_k,
607 self.derive_config.sparse_min_weight,
608 );
609 let multi_vec = MultiVectorEmbedding::from_dense_chunked(
610 &dense,
611 self.derive_config.num_multi_vec_tokens,
612 );
613 MultiFunctionEmbedding {
614 dense,
615 sparse,
616 multi_vec,
617 }
618 })
619 .collect()
620 }
621
622 async fn fetch_dense(&self, texts: &[String]) -> Result<Vec<Vec<f32>>, MemoryError> {
624 let mut all_embeddings = Vec::with_capacity(texts.len());
625
626 for batch in texts.chunks(self.batch_size) {
627 let input: Vec<&str> = batch.iter().map(|s| s.as_str()).collect();
628 let body = serde_json::json!({
629 "model": self.model,
630 "input": input
631 });
632
633 let url = format!("{}/api/embed", self.base_url);
634 let response = self
635 .client
636 .post(&url)
637 .json(&body)
638 .send()
639 .await
640 .map_err(|e| {
641 if e.is_connect() {
642 MemoryError::EmbedderUnavailable(format!(
643 "Ollama not running at {}",
644 self.base_url
645 ))
646 } else if e.is_timeout() {
647 MemoryError::EmbedderUnavailable(format!(
648 "Ollama embedding timed out: {}",
649 e
650 ))
651 } else {
652 MemoryError::EmbeddingRequest(e)
653 }
654 })?;
655
656 if response.status() == reqwest::StatusCode::NOT_FOUND {
657 return Err(MemoryError::EmbedderUnavailable(format!(
658 "Model '{}' not available in Ollama. Run: ollama pull {}",
659 self.model, self.model
660 )));
661 }
662
663 if !response.status().is_success() {
664 let status = response.status();
665 let body = response
666 .text()
667 .await
668 .map_err(|err| format!("failed to read Ollama error body: {err}"));
669 return Err(format_ollama_http_error(status, body));
670 }
671
672 let resp_body: serde_json::Value = response.json().await?;
673 let batch_embeddings = parse_embedding_response(&resp_body, self.dimensions)?;
674 all_embeddings.extend(batch_embeddings);
675 }
676
677 Ok(all_embeddings)
678 }
679}
680
681impl MultiFunctionEmbedder for BgeM3Embedder {
682 fn embed_multi<'a>(&'a self, text: &'a str) -> MultiEmbedFuture<'a> {
683 Box::pin(async move {
684 let mut results = self.fetch_dense(&[text.to_string()]).await?;
685 let dense = results.pop().ok_or_else(|| {
686 MemoryError::Other("Ollama returned empty embeddings for single text".to_string())
687 })?;
688 let sparse = SparseWeights::from_dense(
689 &dense,
690 self.derive_config.sparse_top_k,
691 self.derive_config.sparse_min_weight,
692 );
693 let multi_vec = MultiVectorEmbedding::from_dense_chunked(
694 &dense,
695 self.derive_config.num_multi_vec_tokens,
696 );
697 Ok(MultiFunctionEmbedding {
698 dense,
699 sparse,
700 multi_vec,
701 })
702 })
703 }
704
705 fn embed_batch_multi<'a>(&'a self, texts: Vec<String>) -> MultiEmbedBatchFuture<'a> {
706 Box::pin(async move {
707 let dense_embeddings = self.fetch_dense(&texts).await?;
708 Ok(self.derive_multi_function(dense_embeddings))
709 })
710 }
711
712 fn model_name(&self) -> &str {
713 &self.model
714 }
715
716 fn dimensions(&self) -> usize {
717 self.dimensions
718 }
719}
720
721impl Embedder for BgeM3Embedder {
724 fn embed<'a>(&'a self, text: &'a str) -> EmbedFuture<'a> {
725 Box::pin(async move {
726 let mut results = self.fetch_dense(&[text.to_string()]).await?;
727 results.pop().ok_or_else(|| {
728 MemoryError::Other("Ollama returned empty embeddings for single text".to_string())
729 })
730 })
731 }
732
733 fn embed_batch<'a>(&'a self, texts: Vec<String>) -> EmbedBatchFuture<'a> {
734 Box::pin(async move { self.fetch_dense(&texts).await })
735 }
736
737 fn model_name(&self) -> &str {
738 &self.model
739 }
740
741 fn dimensions(&self) -> usize {
742 self.dimensions
743 }
744
745 fn embed_multi_optional<'a>(&'a self, text: &'a str) -> OptionalMultiEmbedFuture<'a> {
746 Box::pin(async move {
747 MultiFunctionEmbedder::embed_multi(self, text)
748 .await
749 .map(Some)
750 })
751 }
752
753 fn embed_batch_multi_optional<'a>(
754 &'a self,
755 texts: Vec<String>,
756 ) -> OptionalMultiEmbedBatchFuture<'a> {
757 Box::pin(async move {
758 MultiFunctionEmbedder::embed_batch_multi(self, texts)
759 .await
760 .map(Some)
761 })
762 }
763}
764
765#[cfg(feature = "candle-embedder")]
776pub struct CandleEmbedder {
777 model: candle_transformers::models::nomic_bert::NomicBertModel,
778 tokenizer: tokenizers::Tokenizer,
779 device: candle_core::Device,
780 model_id: String,
781 dimensions: usize,
782 max_seq_len: usize,
783}
784
785#[cfg(feature = "candle-embedder")]
786impl CandleEmbedder {
787 pub fn try_new(config: &EmbeddingConfig) -> Result<Self, MemoryError> {
789 Self::try_new_with_model("nomic-ai/nomic-embed-text-v1.5", config)
790 }
791
792 pub fn try_new_with_model(
797 model_id: &str,
798 config: &EmbeddingConfig,
799 ) -> Result<Self, MemoryError> {
800 let device = candle_core::Device::Cpu;
801 let dimensions = config.dimensions;
802 let max_seq_len = 512;
806
807 let (owner, name) = match model_id.split_once('/') {
811 Some((o, n)) => (o, n),
812 None => ("nomic-ai", model_id),
813 };
814
815 let api = hf_hub::HFClientSync::new().map_err(|e| {
816 MemoryError::EmbedderUnavailable(format!("failed to create HF Hub client: {e}"))
817 })?;
818 let repo = api.model(owner, name);
819
820 let config_path = download_hf_file(&repo, "config.json")?;
822 let tokenizer_path = download_hf_file(&repo, "tokenizer.json")?;
823
824 let weights_path = download_hf_file(&repo, "model.safetensors")
826 .or_else(|_| download_hf_file(&repo, "pytorch_model.bin"))?;
827
828 let tokenizer = tokenizers::Tokenizer::from_file(&tokenizer_path).map_err(|e| {
830 MemoryError::EmbedderUnavailable(format!(
831 "failed to load tokenizer from {}: {e}",
832 tokenizer_path.display()
833 ))
834 })?;
835
836 let config_str = std::fs::read_to_string(&config_path).map_err(|e| {
838 MemoryError::EmbedderUnavailable(format!("failed to read config.json: {e}"))
839 })?;
840 let model_config: candle_transformers::models::nomic_bert::Config =
841 serde_json::from_str(&config_str).map_err(|e| {
842 MemoryError::EmbedderUnavailable(format!("failed to parse model config: {e}"))
843 })?;
844
845 if model_config.n_embd != dimensions {
847 return Err(MemoryError::DimensionMismatch {
848 expected: dimensions,
849 actual: model_config.n_embd,
850 });
851 }
852
853 let dtype = candle_core::DType::F32;
855 let weights_bytes = std::fs::read(&weights_path).map_err(|e| {
858 MemoryError::EmbedderUnavailable(format!(
859 "failed to read weights file {}: {e}",
860 weights_path.display()
861 ))
862 })?;
863 let vb = candle_nn::VarBuilder::from_buffered_safetensors(weights_bytes, dtype, &device)
864 .map_err(|e| {
865 MemoryError::EmbedderUnavailable(format!("failed to load model weights: {e}"))
866 })?;
867
868 let model =
869 candle_transformers::models::nomic_bert::NomicBertModel::load(vb, &model_config)
870 .map_err(|e| {
871 MemoryError::EmbedderUnavailable(format!(
872 "failed to build NomicBert model: {e}"
873 ))
874 })?;
875
876 Ok(Self {
877 model,
878 tokenizer,
879 device,
880 model_id: model_id.to_string(),
881 dimensions,
882 max_seq_len,
883 })
884 }
885
886 fn embed_batch_sync(
888 &self,
889 texts: &[String],
890 _query_mode: bool,
891 ) -> Result<Vec<Vec<f32>>, MemoryError> {
892 use candle_core::Tensor;
893 use candle_transformers::models::nomic_bert::{l2_normalize, mean_pooling};
894
895 let mut all_embeddings = Vec::with_capacity(texts.len());
896
897 for text in texts {
905 let prefixed: &str = text;
906
907 let encoding = self
908 .tokenizer
909 .encode(prefixed, true)
910 .map_err(|e| MemoryError::Other(format!("tokenizer error: {e}")))?;
911
912 let input_ids = encoding.get_ids();
913 let attention_mask = encoding.get_attention_mask();
914
915 let seq_len = input_ids.len().min(self.max_seq_len);
917 let input_ids = &input_ids[..seq_len];
918 let attention_mask = &attention_mask[..seq_len];
919
920 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()?;
926 let hidden_states = self.model.forward(
927 &input_ids_tensor,
928 Some(&token_type_ids),
929 Some(&attention_mask_tensor),
930 )?;
931
932 let pooled = mean_pooling(&hidden_states, &attention_mask_tensor)?;
934 let normalized = l2_normalize(&pooled)?;
935
936 let embedding_vec = normalized.to_vec2::<f32>()?;
938 let embedding = embedding_vec
939 .into_iter()
940 .next()
941 .ok_or_else(|| MemoryError::Other("model returned empty embedding".to_string()))?;
942
943 if embedding.len() != self.dimensions {
944 return Err(MemoryError::DimensionMismatch {
945 expected: self.dimensions,
946 actual: embedding.len(),
947 });
948 }
949
950 all_embeddings.push(embedding);
951 }
952
953 Ok(all_embeddings)
954 }
955}
956
957#[cfg(feature = "candle-embedder")]
960fn download_hf_file(
961 repo: &hf_hub::HFRepositorySync<hf_hub::repository::RepoTypeModel>,
962 filename: &str,
963) -> Result<std::path::PathBuf, MemoryError> {
964 repo.download_file()
965 .filename(filename.to_string())
966 .send()
967 .map_err(|e| {
968 MemoryError::EmbedderUnavailable(format!("failed to download '{filename}': {e}"))
969 })
970}
971
972#[cfg(feature = "candle-embedder")]
973impl Embedder for CandleEmbedder {
974 fn embed<'a>(&'a self, text: &'a str) -> EmbedFuture<'a> {
975 let result = self.embed_batch_sync(&[text.to_string()], true);
976 Box::pin(async move {
977 let mut results = result?;
978 results.pop().ok_or_else(|| {
979 MemoryError::Other("Candle embedder returned empty results".to_string())
980 })
981 })
982 }
983
984 fn embed_batch<'a>(&'a self, texts: Vec<String>) -> EmbedBatchFuture<'a> {
985 let result = self.embed_batch_sync(&texts, false);
986 Box::pin(async move { result })
987 }
988
989 fn model_name(&self) -> &str {
990 &self.model_id
991 }
992
993 fn dimensions(&self) -> usize {
994 self.dimensions
995 }
996}
997
998#[cfg(test)]
1001mod bge_m3_tests {
1002 use super::*;
1003
1004 #[test]
1007 fn sparse_from_dense_keeps_top_k_by_abs_weight() {
1008 let dense = vec![0.5, -0.9, 0.01, 0.8, -0.3, 0.001];
1009 let sparse = SparseWeights::from_dense(&dense, 3, 0.05);
1010 assert_eq!(sparse.len(), 3);
1011 assert_eq!(sparse.entries[0], (1, -0.9));
1013 assert_eq!(sparse.entries[1], (3, 0.8));
1014 assert_eq!(sparse.entries[2], (0, 0.5));
1015 }
1016
1017 #[test]
1018 fn sparse_from_dense_filters_below_threshold() {
1019 let dense = vec![0.5, 0.001, 0.9, 0.002];
1020 let sparse = SparseWeights::from_dense(&dense, 100, 0.05);
1021 assert_eq!(sparse.len(), 2);
1022 assert_eq!(sparse.entries[0], (2, 0.9));
1023 assert_eq!(sparse.entries[1], (0, 0.5));
1024 }
1025
1026 #[test]
1027 fn sparse_from_dense_empty_input() {
1028 let sparse = SparseWeights::from_dense(&[], 10, 0.0);
1029 assert!(sparse.is_empty());
1030 }
1031
1032 #[test]
1033 fn sparse_from_dense_truncates_to_top_k() {
1034 let dense = vec![1.0; 100];
1035 let sparse = SparseWeights::from_dense(&dense, 10, 0.0);
1036 assert_eq!(sparse.len(), 10);
1037 }
1038
1039 #[test]
1040 fn sparse_dot_product() {
1041 let a = SparseWeights::from_entries(vec![(0, 1.0), (2, 2.0), (5, 3.0)]);
1042 let b = SparseWeights::from_entries(vec![(0, 0.5), (2, 1.0), (3, 10.0)]);
1043 let result = a.dot(&b);
1045 assert!((result - 2.5).abs() < 1e-6);
1046 }
1047
1048 #[test]
1049 fn sparse_dot_product_no_overlap() {
1050 let a = SparseWeights::from_entries(vec![(0, 1.0), (1, 2.0)]);
1051 let b = SparseWeights::from_entries(vec![(2, 3.0), (3, 4.0)]);
1052 assert_eq!(a.dot(&b), 0.0);
1053 }
1054
1055 #[test]
1056 fn sparse_dot_product_self() {
1057 let a = SparseWeights::from_entries(vec![(0, 3.0), (1, 4.0)]);
1058 assert!((a.dot(&a) - 25.0).abs() < 1e-6);
1060 }
1061
1062 #[test]
1063 fn sparse_from_entries_sorts_by_abs_weight() {
1064 let entries = vec![(0, 0.1), (1, -0.5), (2, 0.3)];
1065 let sparse = SparseWeights::from_entries(entries);
1066 assert_eq!(sparse.entries[0], (1, -0.5));
1067 assert_eq!(sparse.entries[1], (2, 0.3));
1068 assert_eq!(sparse.entries[2], (0, 0.1));
1069 }
1070
1071 #[test]
1074 fn multi_vec_from_dense_chunked() {
1075 let dense = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
1076 let mv = MultiVectorEmbedding::from_dense_chunked(&dense, 3);
1077 assert_eq!(mv.len(), 3);
1078 assert_eq!(mv.token_vectors[0], vec![1.0, 2.0]);
1080 assert_eq!(mv.token_vectors[1], vec![3.0, 4.0]);
1081 assert_eq!(mv.token_vectors[2], vec![5.0, 6.0]);
1082 }
1083
1084 #[test]
1085 fn multi_vec_from_dense_chunked_with_padding() {
1086 let dense = vec![1.0, 2.0, 3.0, 4.0, 5.0];
1087 let mv = MultiVectorEmbedding::from_dense_chunked(&dense, 2);
1088 assert_eq!(mv.len(), 2);
1089 assert_eq!(mv.token_vectors[0], vec![1.0, 2.0, 3.0]);
1091 assert_eq!(mv.token_vectors[1], vec![4.0, 5.0, 0.0]); }
1093
1094 #[test]
1095 fn multi_vec_from_dense_chunked_empty() {
1096 let mv = MultiVectorEmbedding::from_dense_chunked(&[], 4);
1097 assert!(mv.is_empty());
1098 }
1099
1100 #[test]
1101 fn multi_vec_from_dense_chunked_zero_tokens() {
1102 let mv = MultiVectorEmbedding::from_dense_chunked(&[1.0, 2.0], 0);
1103 assert!(mv.is_empty());
1104 }
1105
1106 #[test]
1107 fn multi_vec_from_token_vectors() {
1108 let tokens = vec![vec![1.0, 0.0], vec![0.0, 1.0], vec![1.0, 1.0]];
1109 let mv = MultiVectorEmbedding::from_token_vectors(tokens.clone());
1110 assert_eq!(mv.len(), 3);
1111 assert_eq!(mv.token_vectors, tokens);
1112 }
1113
1114 #[test]
1115 fn multi_vec_consistent_chunk_sizes() {
1116 let dense: Vec<f32> = (0..1024).map(|i| i as f32).collect();
1117 let mv = MultiVectorEmbedding::from_dense_chunked(&dense, 32);
1118 assert_eq!(mv.len(), 32);
1119 let len0 = mv.token_vectors[0].len();
1121 for tv in &mv.token_vectors {
1122 assert_eq!(tv.len(), len0);
1123 }
1124 }
1125
1126 #[test]
1129 fn multi_function_embedding_holds_all_three() {
1130 let dense = vec![0.1, 0.5, 0.9, 0.3];
1131 let sparse = SparseWeights::from_dense(&dense, 2, 0.1);
1132 let multi_vec = MultiVectorEmbedding::from_dense_chunked(&dense, 2);
1133 let mfe = MultiFunctionEmbedding {
1134 dense: dense.clone(),
1135 sparse: sparse.clone(),
1136 multi_vec: multi_vec.clone(),
1137 };
1138 assert_eq!(mfe.dense, dense);
1139 assert_eq!(mfe.sparse, sparse);
1140 assert_eq!(mfe.multi_vec, multi_vec);
1141 }
1142
1143 #[test]
1146 fn derive_config_default_values() {
1147 let cfg = BgeM3DeriveConfig::default();
1148 assert_eq!(cfg.sparse_top_k, 128);
1149 assert_eq!(cfg.sparse_min_weight, 0.01);
1150 assert_eq!(cfg.num_multi_vec_tokens, 32);
1151 }
1152
1153 #[test]
1156 fn bge_m3_embedder_with_params_constructs() {
1157 let embedder = BgeM3Embedder::with_params(
1158 "http://localhost:11434/",
1159 "bge-m3",
1160 1024,
1161 32,
1162 30,
1163 BgeM3DeriveConfig::default(),
1164 );
1165 assert!(embedder.is_ok());
1166 let embedder = embedder.unwrap();
1167 assert_eq!(Embedder::model_name(&embedder), "bge-m3");
1168 assert_eq!(Embedder::dimensions(&embedder), 1024);
1169 }
1170
1171 #[test]
1172 fn bge_m3_embedder_try_new_from_config() {
1173 let config = EmbeddingConfig {
1174 ollama_url: "http://localhost:11434".to_string(),
1175 model: "bge-m3".to_string(),
1176 dimensions: 1024,
1177 batch_size: 16,
1178 timeout_secs: 60,
1179 };
1180 let embedder = BgeM3Embedder::try_new(&config);
1181 assert!(embedder.is_ok());
1182 let embedder = embedder.unwrap();
1183 assert_eq!(Embedder::model_name(&embedder), "bge-m3");
1184 assert_eq!(Embedder::dimensions(&embedder), 1024);
1185 }
1186
1187 #[test]
1188 fn bge_m3_embedder_try_new_with_custom_derive() {
1189 let config = EmbeddingConfig {
1190 ollama_url: "http://localhost:11434".to_string(),
1191 model: "bge-m3".to_string(),
1192 dimensions: 1024,
1193 batch_size: 16,
1194 timeout_secs: 60,
1195 };
1196 let derive = BgeM3DeriveConfig {
1197 sparse_top_k: 64,
1198 sparse_min_weight: 0.05,
1199 num_multi_vec_tokens: 16,
1200 };
1201 let embedder = BgeM3Embedder::try_new_with_derive(&config, derive);
1202 assert!(embedder.is_ok());
1203 }
1204
1205 #[test]
1208 fn derive_multi_function_produces_correct_lengths() {
1209 let embedder = BgeM3Embedder::with_params(
1210 "http://localhost:11434",
1211 "bge-m3",
1212 1024,
1213 32,
1214 30,
1215 BgeM3DeriveConfig {
1216 sparse_top_k: 64,
1217 sparse_min_weight: 0.0,
1218 num_multi_vec_tokens: 16,
1219 },
1220 )
1221 .unwrap();
1222
1223 let dense_vec: Vec<f32> = (0..1024).map(|i| (i as f32) / 1024.0).collect();
1224 let results = embedder.derive_multi_function(vec![dense_vec.clone()]);
1225 assert_eq!(results.len(), 1);
1226
1227 let mfe = &results[0];
1228 assert_eq!(mfe.dense.len(), 1024);
1229 assert_eq!(mfe.sparse.len(), 64); assert_eq!(mfe.multi_vec.len(), 16); }
1232
1233 #[test]
1234 fn derive_multi_function_handles_multiple_inputs() {
1235 let embedder = BgeM3Embedder::with_params(
1236 "http://localhost:11434",
1237 "bge-m3",
1238 8,
1239 4,
1240 30,
1241 BgeM3DeriveConfig::default(),
1242 )
1243 .unwrap();
1244
1245 let inputs: Vec<Vec<f32>> = (0..5).map(|i| vec![i as f32; 8]).collect();
1246 let results = embedder.derive_multi_function(inputs);
1247 assert_eq!(results.len(), 5);
1248 for mfe in &results {
1249 assert_eq!(mfe.dense.len(), 8);
1250 }
1251 }
1252
1253 #[test]
1254 fn derive_multi_function_empty_input() {
1255 let embedder = BgeM3Embedder::with_params(
1256 "http://localhost:11434",
1257 "bge-m3",
1258 8,
1259 4,
1260 30,
1261 BgeM3DeriveConfig::default(),
1262 )
1263 .unwrap();
1264
1265 let results = embedder.derive_multi_function(vec![]);
1266 assert!(results.is_empty());
1267 }
1268
1269 #[tokio::test]
1272 #[ignore = "requires Ollama running with bge-m3 model pulled"]
1273 async fn bge_m3_embed_multi_live() {
1274 let embedder = BgeM3Embedder::with_params(
1275 "http://127.0.0.1:11434",
1276 "bge-m3",
1277 1024,
1278 32,
1279 60,
1280 BgeM3DeriveConfig::default(),
1281 )
1282 .unwrap();
1283
1284 let result = embedder.embed_multi("hello world").await;
1285 assert!(result.is_ok(), "Ollama call failed: {:?}", result.err());
1286 let mfe = result.unwrap();
1287 assert_eq!(mfe.dense.len(), 1024);
1288 assert!(!mfe.sparse.is_empty());
1289 assert!(!mfe.multi_vec.is_empty());
1290 }
1291
1292 #[tokio::test]
1293 #[ignore = "requires Ollama running with bge-m3 model pulled"]
1294 async fn bge_m3_embed_batch_multi_live() {
1295 let embedder = BgeM3Embedder::with_params(
1296 "http://127.0.0.1:11434",
1297 "bge-m3",
1298 1024,
1299 32,
1300 60,
1301 BgeM3DeriveConfig::default(),
1302 )
1303 .unwrap();
1304
1305 let texts = vec!["hello".to_string(), "world".to_string(), "test".to_string()];
1306 let results = embedder.embed_batch_multi(texts).await;
1307 assert!(
1308 results.is_ok(),
1309 "Ollama batch call failed: {:?}",
1310 results.err()
1311 );
1312 let embeddings = results.unwrap();
1313 assert_eq!(embeddings.len(), 3);
1314 for mfe in &embeddings {
1315 assert_eq!(mfe.dense.len(), 1024);
1316 }
1317 }
1318
1319 #[tokio::test]
1320 #[ignore = "requires Ollama running with bge-m3 model pulled"]
1321 async fn bge_m3_embedder_as_standard_embedder_live() {
1322 let embedder = BgeM3Embedder::with_params(
1323 "http://127.0.0.1:11434",
1324 "bge-m3",
1325 1024,
1326 32,
1327 60,
1328 BgeM3DeriveConfig::default(),
1329 )
1330 .unwrap();
1331
1332 let result = embedder.embed("hello world").await;
1333 assert!(result.is_ok());
1334 let dense = result.unwrap();
1335 assert_eq!(dense.len(), 1024);
1336 }
1337}