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(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(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 deterministic_embedding(text: &str, dimensions: usize) -> Vec<f32> {
287 let mut hasher = std::hash::DefaultHasher::new();
288 text.hash(&mut hasher);
289 let mut state = hasher.finish();
290 if state == 0 {
291 state = 1;
292 }
293
294 let mut values = Vec::with_capacity(dimensions);
295 for _ in 0..dimensions {
296 state ^= state << 13;
298 state ^= state >> 7;
299 state ^= state << 17;
300 let val = ((state as f64) / (u64::MAX as f64)) * 2.0 - 1.0;
301 values.push(val as f32);
302 }
303
304 let magnitude: f32 = values.iter().map(|v| v * v).sum::<f32>().sqrt();
306 if magnitude > 0.0 {
307 for v in &mut values {
308 *v /= magnitude;
309 }
310 }
311
312 values
313}
314
315pub type MultiEmbedFuture<'a> =
319 Pin<Box<dyn Future<Output = Result<MultiFunctionEmbedding, MemoryError>> + Send + 'a>>;
320
321pub type MultiEmbedBatchFuture<'a> =
323 Pin<Box<dyn Future<Output = Result<Vec<MultiFunctionEmbedding>, MemoryError>> + Send + 'a>>;
324
325#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
330pub struct SparseWeights {
331 pub entries: Vec<(usize, f32)>,
333}
334
335impl SparseWeights {
336 #[must_use]
343 pub fn from_dense(vec: &[f32], top_k: usize, min_weight: f32) -> Self {
344 let mut entries: Vec<(usize, f32)> = vec
345 .iter()
346 .enumerate()
347 .map(|(i, &v)| (i, v))
348 .filter(|(_, v)| v.abs() >= min_weight)
349 .collect();
350 entries.sort_by(|a, b| {
351 b.1.abs()
352 .partial_cmp(&a.1.abs())
353 .unwrap_or(std::cmp::Ordering::Equal)
354 });
355 entries.truncate(top_k);
356 Self { entries }
357 }
358
359 #[must_use]
361 pub fn from_entries(mut entries: Vec<(usize, f32)>) -> Self {
362 entries.sort_by(|a, b| {
363 b.1.abs()
364 .partial_cmp(&a.1.abs())
365 .unwrap_or(std::cmp::Ordering::Equal)
366 });
367 Self { entries }
368 }
369
370 pub fn len(&self) -> usize {
372 self.entries.len()
373 }
374
375 pub fn is_empty(&self) -> bool {
377 self.entries.is_empty()
378 }
379
380 pub fn dot(&self, other: &SparseWeights) -> f32 {
386 use std::collections::HashMap;
387 let map: HashMap<usize, f32> = other.entries.iter().copied().collect();
388 self.entries
389 .iter()
390 .map(|(idx, w)| w * map.get(idx).copied().unwrap_or(0.0))
391 .sum()
392 }
393}
394
395#[derive(Debug, Clone, PartialEq)]
400pub struct MultiVectorEmbedding {
401 pub token_vectors: Vec<Vec<f32>>,
403}
404
405impl MultiVectorEmbedding {
406 #[must_use]
413 pub fn from_dense_chunked(vec: &[f32], num_tokens: usize) -> Self {
414 if vec.is_empty() || num_tokens == 0 {
415 return Self {
416 token_vectors: Vec::new(),
417 };
418 }
419 let chunk_size = (vec.len() + num_tokens - 1) / num_tokens; let token_vectors = vec
421 .chunks(chunk_size)
422 .map(|chunk| {
423 let mut v = chunk.to_vec();
425 v.resize(chunk_size, 0.0);
426 v
427 })
428 .collect();
429 Self { token_vectors }
430 }
431
432 #[must_use]
434 pub fn from_token_vectors(token_vectors: Vec<Vec<f32>>) -> Self {
435 Self { token_vectors }
436 }
437
438 pub fn len(&self) -> usize {
440 self.token_vectors.len()
441 }
442
443 pub fn is_empty(&self) -> bool {
445 self.token_vectors.is_empty()
446 }
447}
448
449#[derive(Debug, Clone)]
452pub struct MultiFunctionEmbedding {
453 pub dense: Vec<f32>,
455 pub sparse: SparseWeights,
457 pub multi_vec: MultiVectorEmbedding,
459}
460
461pub trait MultiFunctionEmbedder: Send + Sync {
468 fn embed_multi<'a>(&'a self, text: &'a str) -> MultiEmbedFuture<'a>;
470
471 fn embed_batch_multi<'a>(&'a self, texts: Vec<String>) -> MultiEmbedBatchFuture<'a>;
475
476 fn model_name(&self) -> &str;
478
479 fn dimensions(&self) -> usize;
481}
482
483#[derive(Debug, Clone)]
486pub struct BgeM3DeriveConfig {
487 pub sparse_top_k: usize,
489 pub sparse_min_weight: f32,
491 pub num_multi_vec_tokens: usize,
493}
494
495impl Default for BgeM3DeriveConfig {
496 fn default() -> Self {
497 Self {
498 sparse_top_k: 128,
499 sparse_min_weight: 0.01,
500 num_multi_vec_tokens: 32,
501 }
502 }
503}
504
505pub struct BgeM3Embedder {
524 client: reqwest::Client,
525 base_url: String,
526 model: String,
527 dimensions: usize,
528 batch_size: usize,
529 derive_config: BgeM3DeriveConfig,
530}
531
532impl BgeM3Embedder {
533 pub fn try_new(config: &EmbeddingConfig) -> Result<Self, MemoryError> {
538 Self::try_new_with_derive(config, BgeM3DeriveConfig::default())
539 }
540
541 pub fn try_new_with_derive(
543 config: &EmbeddingConfig,
544 derive_config: BgeM3DeriveConfig,
545 ) -> Result<Self, MemoryError> {
546 let client = reqwest::Client::builder()
547 .timeout(std::time::Duration::from_secs(config.timeout_secs))
548 .build()
549 .map_err(|e| {
550 MemoryError::EmbedderUnavailable(format!("failed to build HTTP client: {e}"))
551 })?;
552
553 Ok(Self {
554 client,
555 base_url: config.ollama_url.trim_end_matches('/').to_string(),
556 model: config.model.clone(),
557 dimensions: config.dimensions,
558 batch_size: config.batch_size,
559 derive_config,
560 })
561 }
562
563 pub fn with_params(
566 base_url: &str,
567 model: &str,
568 dimensions: usize,
569 batch_size: usize,
570 timeout_secs: u64,
571 derive_config: BgeM3DeriveConfig,
572 ) -> Result<Self, MemoryError> {
573 let client = reqwest::Client::builder()
574 .timeout(std::time::Duration::from_secs(timeout_secs))
575 .build()
576 .map_err(|e| {
577 MemoryError::EmbedderUnavailable(format!("failed to build HTTP client: {e}"))
578 })?;
579
580 Ok(Self {
581 client,
582 base_url: base_url.trim_end_matches('/').to_string(),
583 model: model.to_string(),
584 dimensions,
585 batch_size,
586 derive_config,
587 })
588 }
589
590 fn derive_multi_function(
592 &self,
593 dense_embeddings: Vec<Vec<f32>>,
594 ) -> Vec<MultiFunctionEmbedding> {
595 dense_embeddings
596 .into_iter()
597 .map(|dense| {
598 let sparse = SparseWeights::from_dense(
599 &dense,
600 self.derive_config.sparse_top_k,
601 self.derive_config.sparse_min_weight,
602 );
603 let multi_vec = MultiVectorEmbedding::from_dense_chunked(
604 &dense,
605 self.derive_config.num_multi_vec_tokens,
606 );
607 MultiFunctionEmbedding {
608 dense,
609 sparse,
610 multi_vec,
611 }
612 })
613 .collect()
614 }
615
616 async fn fetch_dense(&self, texts: &[String]) -> Result<Vec<Vec<f32>>, MemoryError> {
618 let mut all_embeddings = Vec::with_capacity(texts.len());
619
620 for batch in texts.chunks(self.batch_size) {
621 let input: Vec<&str> = batch.iter().map(|s| s.as_str()).collect();
622 let body = serde_json::json!({
623 "model": self.model,
624 "input": input
625 });
626
627 let url = format!("{}/api/embed", self.base_url);
628 let response = self
629 .client
630 .post(&url)
631 .json(&body)
632 .send()
633 .await
634 .map_err(|e| {
635 if e.is_connect() {
636 MemoryError::EmbedderUnavailable(format!(
637 "Ollama not running at {}",
638 self.base_url
639 ))
640 } else if e.is_timeout() {
641 MemoryError::EmbedderUnavailable(format!(
642 "Ollama embedding timed out: {}",
643 e
644 ))
645 } else {
646 MemoryError::EmbeddingRequest(e)
647 }
648 })?;
649
650 if response.status() == reqwest::StatusCode::NOT_FOUND {
651 return Err(MemoryError::EmbedderUnavailable(format!(
652 "Model '{}' not available in Ollama. Run: ollama pull {}",
653 self.model, self.model
654 )));
655 }
656
657 if !response.status().is_success() {
658 let status = response.status();
659 let body = response
660 .text()
661 .await
662 .map_err(|err| format!("failed to read Ollama error body: {err}"));
663 return Err(format_ollama_http_error(status, body));
664 }
665
666 let resp_body: serde_json::Value = response.json().await?;
667 let batch_embeddings = parse_embedding_response(&resp_body, self.dimensions)?;
668 all_embeddings.extend(batch_embeddings);
669 }
670
671 Ok(all_embeddings)
672 }
673}
674
675impl MultiFunctionEmbedder for BgeM3Embedder {
676 fn embed_multi<'a>(&'a self, text: &'a str) -> MultiEmbedFuture<'a> {
677 Box::pin(async move {
678 let mut results = self.fetch_dense(&[text.to_string()]).await?;
679 let dense = results.pop().ok_or_else(|| {
680 MemoryError::Other("Ollama returned empty embeddings for single text".to_string())
681 })?;
682 let sparse = SparseWeights::from_dense(
683 &dense,
684 self.derive_config.sparse_top_k,
685 self.derive_config.sparse_min_weight,
686 );
687 let multi_vec = MultiVectorEmbedding::from_dense_chunked(
688 &dense,
689 self.derive_config.num_multi_vec_tokens,
690 );
691 Ok(MultiFunctionEmbedding {
692 dense,
693 sparse,
694 multi_vec,
695 })
696 })
697 }
698
699 fn embed_batch_multi<'a>(&'a self, texts: Vec<String>) -> MultiEmbedBatchFuture<'a> {
700 Box::pin(async move {
701 let dense_embeddings = self.fetch_dense(&texts).await?;
702 Ok(self.derive_multi_function(dense_embeddings))
703 })
704 }
705
706 fn model_name(&self) -> &str {
707 &self.model
708 }
709
710 fn dimensions(&self) -> usize {
711 self.dimensions
712 }
713}
714
715impl Embedder for BgeM3Embedder {
718 fn embed<'a>(&'a self, text: &'a str) -> EmbedFuture<'a> {
719 Box::pin(async move {
720 let mut results = self.fetch_dense(&[text.to_string()]).await?;
721 results.pop().ok_or_else(|| {
722 MemoryError::Other("Ollama returned empty embeddings for single text".to_string())
723 })
724 })
725 }
726
727 fn embed_batch<'a>(&'a self, texts: Vec<String>) -> EmbedBatchFuture<'a> {
728 Box::pin(async move { self.fetch_dense(&texts).await })
729 }
730
731 fn model_name(&self) -> &str {
732 &self.model
733 }
734
735 fn dimensions(&self) -> usize {
736 self.dimensions
737 }
738
739 fn embed_multi_optional<'a>(&'a self, text: &'a str) -> OptionalMultiEmbedFuture<'a> {
740 Box::pin(async move {
741 MultiFunctionEmbedder::embed_multi(self, text)
742 .await
743 .map(Some)
744 })
745 }
746
747 fn embed_batch_multi_optional<'a>(
748 &'a self,
749 texts: Vec<String>,
750 ) -> OptionalMultiEmbedBatchFuture<'a> {
751 Box::pin(async move {
752 MultiFunctionEmbedder::embed_batch_multi(self, texts)
753 .await
754 .map(Some)
755 })
756 }
757}
758
759#[cfg(feature = "candle-embedder")]
770pub struct CandleEmbedder {
771 model: candle_transformers::models::nomic_bert::NomicBertModel,
772 tokenizer: tokenizers::Tokenizer,
773 device: candle_core::Device,
774 model_id: String,
775 dimensions: usize,
776 max_seq_len: usize,
777}
778
779#[cfg(feature = "candle-embedder")]
780impl CandleEmbedder {
781 pub fn try_new(config: &EmbeddingConfig) -> Result<Self, MemoryError> {
783 Self::try_new_with_model("nomic-ai/nomic-embed-text-v1.5", config)
784 }
785
786 pub fn try_new_with_model(
791 model_id: &str,
792 config: &EmbeddingConfig,
793 ) -> Result<Self, MemoryError> {
794 let device = candle_core::Device::Cpu;
795 let dimensions = config.dimensions;
796 let max_seq_len = 8192; let (owner, name) = match model_id.split_once('/') {
802 Some((o, n)) => (o, n),
803 None => ("nomic-ai", model_id),
804 };
805
806 let api = hf_hub::HFClientSync::new().map_err(|e| {
807 MemoryError::EmbedderUnavailable(format!("failed to create HF Hub client: {e}"))
808 })?;
809 let repo = api.model(owner, name);
810
811 let config_path = download_hf_file(&repo, "config.json")?;
813 let tokenizer_path = download_hf_file(&repo, "tokenizer.json")?;
814
815 let weights_path = download_hf_file(&repo, "model.safetensors")
817 .or_else(|_| download_hf_file(&repo, "pytorch_model.bin"))?;
818
819 let tokenizer = tokenizers::Tokenizer::from_file(&tokenizer_path).map_err(|e| {
821 MemoryError::EmbedderUnavailable(format!(
822 "failed to load tokenizer from {}: {e}",
823 tokenizer_path.display()
824 ))
825 })?;
826
827 let config_str = std::fs::read_to_string(&config_path).map_err(|e| {
829 MemoryError::EmbedderUnavailable(format!("failed to read config.json: {e}"))
830 })?;
831 let model_config: candle_transformers::models::nomic_bert::Config =
832 serde_json::from_str(&config_str).map_err(|e| {
833 MemoryError::EmbedderUnavailable(format!("failed to parse model config: {e}"))
834 })?;
835
836 if model_config.n_embd != dimensions {
838 return Err(MemoryError::DimensionMismatch {
839 expected: dimensions,
840 actual: model_config.n_embd,
841 });
842 }
843
844 let dtype = candle_core::DType::F32;
846 let weights_bytes = std::fs::read(&weights_path).map_err(|e| {
849 MemoryError::EmbedderUnavailable(format!(
850 "failed to read weights file {}: {e}",
851 weights_path.display()
852 ))
853 })?;
854 let vb = candle_nn::VarBuilder::from_buffered_safetensors(weights_bytes, dtype, &device)
855 .map_err(|e| {
856 MemoryError::EmbedderUnavailable(format!("failed to load model weights: {e}"))
857 })?;
858
859 let model =
860 candle_transformers::models::nomic_bert::NomicBertModel::load(vb, &model_config)
861 .map_err(|e| {
862 MemoryError::EmbedderUnavailable(format!(
863 "failed to build NomicBert model: {e}"
864 ))
865 })?;
866
867 Ok(Self {
868 model,
869 tokenizer,
870 device,
871 model_id: model_id.to_string(),
872 dimensions,
873 max_seq_len,
874 })
875 }
876
877 fn embed_batch_sync(
879 &self,
880 texts: &[String],
881 _query_mode: bool,
882 ) -> Result<Vec<Vec<f32>>, MemoryError> {
883 use candle_core::Tensor;
884 use candle_transformers::models::nomic_bert::{l2_normalize, mean_pooling};
885
886 let mut all_embeddings = Vec::with_capacity(texts.len());
887
888 for text in texts {
896 let prefixed: &str = text;
897
898 let encoding = self
899 .tokenizer
900 .encode(prefixed, true)
901 .map_err(|e| MemoryError::Other(format!("tokenizer error: {e}")))?;
902
903 let input_ids = encoding.get_ids();
904 let attention_mask = encoding.get_attention_mask();
905
906 let seq_len = input_ids.len().min(self.max_seq_len);
908 let input_ids = &input_ids[..seq_len];
909 let attention_mask = &attention_mask[..seq_len];
910
911 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()?;
917 let hidden_states = self.model.forward(
918 &input_ids_tensor,
919 Some(&token_type_ids),
920 Some(&attention_mask_tensor),
921 )?;
922
923 let pooled = mean_pooling(&hidden_states, &attention_mask_tensor)?;
925 let normalized = l2_normalize(&pooled)?;
926
927 let embedding_vec = normalized.to_vec2::<f32>()?;
929 let embedding = embedding_vec
930 .into_iter()
931 .next()
932 .ok_or_else(|| MemoryError::Other("model returned empty embedding".to_string()))?;
933
934 if embedding.len() != self.dimensions {
935 return Err(MemoryError::DimensionMismatch {
936 expected: self.dimensions,
937 actual: embedding.len(),
938 });
939 }
940
941 all_embeddings.push(embedding);
942 }
943
944 Ok(all_embeddings)
945 }
946}
947
948#[cfg(feature = "candle-embedder")]
951fn download_hf_file(
952 repo: &hf_hub::HFRepositorySync<hf_hub::repository::RepoTypeModel>,
953 filename: &str,
954) -> Result<std::path::PathBuf, MemoryError> {
955 repo.download_file()
956 .filename(filename.to_string())
957 .send()
958 .map_err(|e| {
959 MemoryError::EmbedderUnavailable(format!("failed to download '{filename}': {e}"))
960 })
961}
962
963#[cfg(feature = "candle-embedder")]
964impl Embedder for CandleEmbedder {
965 fn embed<'a>(&'a self, text: &'a str) -> EmbedFuture<'a> {
966 let result = self.embed_batch_sync(&[text.to_string()], true);
967 Box::pin(async move {
968 let mut results = result?;
969 results.pop().ok_or_else(|| {
970 MemoryError::Other("Candle embedder returned empty results".to_string())
971 })
972 })
973 }
974
975 fn embed_batch<'a>(&'a self, texts: Vec<String>) -> EmbedBatchFuture<'a> {
976 let result = self.embed_batch_sync(&texts, false);
977 Box::pin(async move { result })
978 }
979
980 fn model_name(&self) -> &str {
981 &self.model_id
982 }
983
984 fn dimensions(&self) -> usize {
985 self.dimensions
986 }
987}
988
989#[cfg(test)]
992mod bge_m3_tests {
993 use super::*;
994
995 #[test]
998 fn sparse_from_dense_keeps_top_k_by_abs_weight() {
999 let dense = vec![0.5, -0.9, 0.01, 0.8, -0.3, 0.001];
1000 let sparse = SparseWeights::from_dense(&dense, 3, 0.05);
1001 assert_eq!(sparse.len(), 3);
1002 assert_eq!(sparse.entries[0], (1, -0.9));
1004 assert_eq!(sparse.entries[1], (3, 0.8));
1005 assert_eq!(sparse.entries[2], (0, 0.5));
1006 }
1007
1008 #[test]
1009 fn sparse_from_dense_filters_below_threshold() {
1010 let dense = vec![0.5, 0.001, 0.9, 0.002];
1011 let sparse = SparseWeights::from_dense(&dense, 100, 0.05);
1012 assert_eq!(sparse.len(), 2);
1013 assert_eq!(sparse.entries[0], (2, 0.9));
1014 assert_eq!(sparse.entries[1], (0, 0.5));
1015 }
1016
1017 #[test]
1018 fn sparse_from_dense_empty_input() {
1019 let sparse = SparseWeights::from_dense(&[], 10, 0.0);
1020 assert!(sparse.is_empty());
1021 }
1022
1023 #[test]
1024 fn sparse_from_dense_truncates_to_top_k() {
1025 let dense = vec![1.0; 100];
1026 let sparse = SparseWeights::from_dense(&dense, 10, 0.0);
1027 assert_eq!(sparse.len(), 10);
1028 }
1029
1030 #[test]
1031 fn sparse_dot_product() {
1032 let a = SparseWeights::from_entries(vec![(0, 1.0), (2, 2.0), (5, 3.0)]);
1033 let b = SparseWeights::from_entries(vec![(0, 0.5), (2, 1.0), (3, 10.0)]);
1034 let result = a.dot(&b);
1036 assert!((result - 2.5).abs() < 1e-6);
1037 }
1038
1039 #[test]
1040 fn sparse_dot_product_no_overlap() {
1041 let a = SparseWeights::from_entries(vec![(0, 1.0), (1, 2.0)]);
1042 let b = SparseWeights::from_entries(vec![(2, 3.0), (3, 4.0)]);
1043 assert_eq!(a.dot(&b), 0.0);
1044 }
1045
1046 #[test]
1047 fn sparse_dot_product_self() {
1048 let a = SparseWeights::from_entries(vec![(0, 3.0), (1, 4.0)]);
1049 assert!((a.dot(&a) - 25.0).abs() < 1e-6);
1051 }
1052
1053 #[test]
1054 fn sparse_from_entries_sorts_by_abs_weight() {
1055 let entries = vec![(0, 0.1), (1, -0.5), (2, 0.3)];
1056 let sparse = SparseWeights::from_entries(entries);
1057 assert_eq!(sparse.entries[0], (1, -0.5));
1058 assert_eq!(sparse.entries[1], (2, 0.3));
1059 assert_eq!(sparse.entries[2], (0, 0.1));
1060 }
1061
1062 #[test]
1065 fn multi_vec_from_dense_chunked() {
1066 let dense = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
1067 let mv = MultiVectorEmbedding::from_dense_chunked(&dense, 3);
1068 assert_eq!(mv.len(), 3);
1069 assert_eq!(mv.token_vectors[0], vec![1.0, 2.0]);
1071 assert_eq!(mv.token_vectors[1], vec![3.0, 4.0]);
1072 assert_eq!(mv.token_vectors[2], vec![5.0, 6.0]);
1073 }
1074
1075 #[test]
1076 fn multi_vec_from_dense_chunked_with_padding() {
1077 let dense = vec![1.0, 2.0, 3.0, 4.0, 5.0];
1078 let mv = MultiVectorEmbedding::from_dense_chunked(&dense, 2);
1079 assert_eq!(mv.len(), 2);
1080 assert_eq!(mv.token_vectors[0], vec![1.0, 2.0, 3.0]);
1082 assert_eq!(mv.token_vectors[1], vec![4.0, 5.0, 0.0]); }
1084
1085 #[test]
1086 fn multi_vec_from_dense_chunked_empty() {
1087 let mv = MultiVectorEmbedding::from_dense_chunked(&[], 4);
1088 assert!(mv.is_empty());
1089 }
1090
1091 #[test]
1092 fn multi_vec_from_dense_chunked_zero_tokens() {
1093 let mv = MultiVectorEmbedding::from_dense_chunked(&[1.0, 2.0], 0);
1094 assert!(mv.is_empty());
1095 }
1096
1097 #[test]
1098 fn multi_vec_from_token_vectors() {
1099 let tokens = vec![vec![1.0, 0.0], vec![0.0, 1.0], vec![1.0, 1.0]];
1100 let mv = MultiVectorEmbedding::from_token_vectors(tokens.clone());
1101 assert_eq!(mv.len(), 3);
1102 assert_eq!(mv.token_vectors, tokens);
1103 }
1104
1105 #[test]
1106 fn multi_vec_consistent_chunk_sizes() {
1107 let dense: Vec<f32> = (0..1024).map(|i| i as f32).collect();
1108 let mv = MultiVectorEmbedding::from_dense_chunked(&dense, 32);
1109 assert_eq!(mv.len(), 32);
1110 let len0 = mv.token_vectors[0].len();
1112 for tv in &mv.token_vectors {
1113 assert_eq!(tv.len(), len0);
1114 }
1115 }
1116
1117 #[test]
1120 fn multi_function_embedding_holds_all_three() {
1121 let dense = vec![0.1, 0.5, 0.9, 0.3];
1122 let sparse = SparseWeights::from_dense(&dense, 2, 0.1);
1123 let multi_vec = MultiVectorEmbedding::from_dense_chunked(&dense, 2);
1124 let mfe = MultiFunctionEmbedding {
1125 dense: dense.clone(),
1126 sparse: sparse.clone(),
1127 multi_vec: multi_vec.clone(),
1128 };
1129 assert_eq!(mfe.dense, dense);
1130 assert_eq!(mfe.sparse, sparse);
1131 assert_eq!(mfe.multi_vec, multi_vec);
1132 }
1133
1134 #[test]
1137 fn derive_config_default_values() {
1138 let cfg = BgeM3DeriveConfig::default();
1139 assert_eq!(cfg.sparse_top_k, 128);
1140 assert_eq!(cfg.sparse_min_weight, 0.01);
1141 assert_eq!(cfg.num_multi_vec_tokens, 32);
1142 }
1143
1144 #[test]
1147 fn bge_m3_embedder_with_params_constructs() {
1148 let embedder = BgeM3Embedder::with_params(
1149 "http://localhost:11434/",
1150 "bge-m3",
1151 1024,
1152 32,
1153 30,
1154 BgeM3DeriveConfig::default(),
1155 );
1156 assert!(embedder.is_ok());
1157 let embedder = embedder.unwrap();
1158 assert_eq!(Embedder::model_name(&embedder), "bge-m3");
1159 assert_eq!(Embedder::dimensions(&embedder), 1024);
1160 }
1161
1162 #[test]
1163 fn bge_m3_embedder_try_new_from_config() {
1164 let config = EmbeddingConfig {
1165 ollama_url: "http://localhost:11434".to_string(),
1166 model: "bge-m3".to_string(),
1167 dimensions: 1024,
1168 batch_size: 16,
1169 timeout_secs: 60,
1170 };
1171 let embedder = BgeM3Embedder::try_new(&config);
1172 assert!(embedder.is_ok());
1173 let embedder = embedder.unwrap();
1174 assert_eq!(Embedder::model_name(&embedder), "bge-m3");
1175 assert_eq!(Embedder::dimensions(&embedder), 1024);
1176 }
1177
1178 #[test]
1179 fn bge_m3_embedder_try_new_with_custom_derive() {
1180 let config = EmbeddingConfig {
1181 ollama_url: "http://localhost:11434".to_string(),
1182 model: "bge-m3".to_string(),
1183 dimensions: 1024,
1184 batch_size: 16,
1185 timeout_secs: 60,
1186 };
1187 let derive = BgeM3DeriveConfig {
1188 sparse_top_k: 64,
1189 sparse_min_weight: 0.05,
1190 num_multi_vec_tokens: 16,
1191 };
1192 let embedder = BgeM3Embedder::try_new_with_derive(&config, derive);
1193 assert!(embedder.is_ok());
1194 }
1195
1196 #[test]
1199 fn derive_multi_function_produces_correct_lengths() {
1200 let embedder = BgeM3Embedder::with_params(
1201 "http://localhost:11434",
1202 "bge-m3",
1203 1024,
1204 32,
1205 30,
1206 BgeM3DeriveConfig {
1207 sparse_top_k: 64,
1208 sparse_min_weight: 0.0,
1209 num_multi_vec_tokens: 16,
1210 },
1211 )
1212 .unwrap();
1213
1214 let dense_vec: Vec<f32> = (0..1024).map(|i| (i as f32) / 1024.0).collect();
1215 let results = embedder.derive_multi_function(vec![dense_vec.clone()]);
1216 assert_eq!(results.len(), 1);
1217
1218 let mfe = &results[0];
1219 assert_eq!(mfe.dense.len(), 1024);
1220 assert_eq!(mfe.sparse.len(), 64); assert_eq!(mfe.multi_vec.len(), 16); }
1223
1224 #[test]
1225 fn derive_multi_function_handles_multiple_inputs() {
1226 let embedder = BgeM3Embedder::with_params(
1227 "http://localhost:11434",
1228 "bge-m3",
1229 8,
1230 4,
1231 30,
1232 BgeM3DeriveConfig::default(),
1233 )
1234 .unwrap();
1235
1236 let inputs: Vec<Vec<f32>> = (0..5).map(|i| vec![i as f32; 8]).collect();
1237 let results = embedder.derive_multi_function(inputs);
1238 assert_eq!(results.len(), 5);
1239 for mfe in &results {
1240 assert_eq!(mfe.dense.len(), 8);
1241 }
1242 }
1243
1244 #[test]
1245 fn derive_multi_function_empty_input() {
1246 let embedder = BgeM3Embedder::with_params(
1247 "http://localhost:11434",
1248 "bge-m3",
1249 8,
1250 4,
1251 30,
1252 BgeM3DeriveConfig::default(),
1253 )
1254 .unwrap();
1255
1256 let results = embedder.derive_multi_function(vec![]);
1257 assert!(results.is_empty());
1258 }
1259
1260 #[tokio::test]
1263 #[ignore = "requires Ollama running with bge-m3 model pulled"]
1264 async fn bge_m3_embed_multi_live() {
1265 let embedder = BgeM3Embedder::with_params(
1266 "http://127.0.0.1:11434",
1267 "bge-m3",
1268 1024,
1269 32,
1270 60,
1271 BgeM3DeriveConfig::default(),
1272 )
1273 .unwrap();
1274
1275 let result = embedder.embed_multi("hello world").await;
1276 assert!(result.is_ok(), "Ollama call failed: {:?}", result.err());
1277 let mfe = result.unwrap();
1278 assert_eq!(mfe.dense.len(), 1024);
1279 assert!(!mfe.sparse.is_empty());
1280 assert!(!mfe.multi_vec.is_empty());
1281 }
1282
1283 #[tokio::test]
1284 #[ignore = "requires Ollama running with bge-m3 model pulled"]
1285 async fn bge_m3_embed_batch_multi_live() {
1286 let embedder = BgeM3Embedder::with_params(
1287 "http://127.0.0.1:11434",
1288 "bge-m3",
1289 1024,
1290 32,
1291 60,
1292 BgeM3DeriveConfig::default(),
1293 )
1294 .unwrap();
1295
1296 let texts = vec!["hello".to_string(), "world".to_string(), "test".to_string()];
1297 let results = embedder.embed_batch_multi(texts).await;
1298 assert!(
1299 results.is_ok(),
1300 "Ollama batch call failed: {:?}",
1301 results.err()
1302 );
1303 let embeddings = results.unwrap();
1304 assert_eq!(embeddings.len(), 3);
1305 for mfe in &embeddings {
1306 assert_eq!(mfe.dense.len(), 1024);
1307 }
1308 }
1309
1310 #[tokio::test]
1311 #[ignore = "requires Ollama running with bge-m3 model pulled"]
1312 async fn bge_m3_embedder_as_standard_embedder_live() {
1313 let embedder = BgeM3Embedder::with_params(
1314 "http://127.0.0.1:11434",
1315 "bge-m3",
1316 1024,
1317 32,
1318 60,
1319 BgeM3DeriveConfig::default(),
1320 )
1321 .unwrap();
1322
1323 let result = embedder.embed("hello world").await;
1324 assert!(result.is_ok());
1325 let dense = result.unwrap();
1326 assert_eq!(dense.len(), 1024);
1327 }
1328}