Skip to main content

mnemo_core/embedding/
mod.rs

1pub mod onnx;
2pub mod openai;
3
4use crate::error::Result;
5
6#[async_trait::async_trait]
7pub trait EmbeddingProvider: Send + Sync {
8    async fn embed(&self, text: &str) -> Result<Vec<f32>>;
9    async fn embed_batch(&self, texts: &[&str]) -> Result<Vec<Vec<f32>>>;
10    fn dimensions(&self) -> usize;
11
12    /// Whether this provider produces real query vectors usable for semantic
13    /// (dense-similarity) recall. Defaults to `true`; the no-op provider — which
14    /// returns all-zero vectors — overrides this to `false` so the recall path
15    /// can refuse semantic/hybrid queries with a typed error instead of silently
16    /// returning an empty result set. Real providers (OpenAI, ONNX) inherit
17    /// `true`.
18    fn is_semantic_capable(&self) -> bool {
19        true
20    }
21}
22
23pub struct NoopEmbedding {
24    dimensions: usize,
25}
26
27impl NoopEmbedding {
28    pub fn new(dimensions: usize) -> Self {
29        Self { dimensions }
30    }
31}
32
33#[async_trait::async_trait]
34impl EmbeddingProvider for NoopEmbedding {
35    async fn embed(&self, _text: &str) -> Result<Vec<f32>> {
36        Ok(vec![0.0; self.dimensions])
37    }
38
39    async fn embed_batch(&self, texts: &[&str]) -> Result<Vec<Vec<f32>>> {
40        Ok(texts.iter().map(|_| vec![0.0; self.dimensions]).collect())
41    }
42
43    fn dimensions(&self) -> usize {
44        self.dimensions
45    }
46
47    /// The no-op embedder returns all-zero vectors — it cannot back semantic
48    /// recall. Reporting `false` makes the recall path fail loud instead of
49    /// silently returning empty.
50    fn is_semantic_capable(&self) -> bool {
51        false
52    }
53}
54
55/// A deterministic, offline **bag-of-words hashing** embedder for tests,
56/// examples, and demos.
57///
58/// Each whitespace token is FNV-1a hashed into a bucket and the resulting
59/// vector is L2-normalized, so two texts that share tokens are close under
60/// cosine similarity. It needs no model file and no network and — unlike
61/// [`NoopEmbedding`] — produces real, non-zero vectors, so it reports
62/// `is_semantic_capable() == true` and can back semantic/hybrid recall.
63///
64/// It is **not** a production-quality semantic model (there is no learned
65/// meaning, only lexical hashing); use OpenAI (HTTP) or ONNX embeddings for real
66/// semantic recall. Its purpose is a reproducible, dependency-free stand-in so
67/// tests and examples can exercise the vector path without an API key or model.
68pub struct DeterministicEmbedding {
69    dimensions: usize,
70}
71
72impl DeterministicEmbedding {
73    pub fn new(dimensions: usize) -> Self {
74        Self { dimensions }
75    }
76
77    fn embed_one(&self, text: &str) -> Vec<f32> {
78        let mut v = vec![0f32; self.dimensions];
79        for tok in text.split_whitespace() {
80            // FNV-1a over the token, mapped into a bucket.
81            let mut h = 0xcbf29ce484222325u64;
82            for b in tok.bytes() {
83                h ^= b as u64;
84                h = h.wrapping_mul(0x100000001b3);
85            }
86            let idx = (h as usize) % self.dimensions.max(1);
87            v[idx] += 1.0;
88        }
89        let norm = v.iter().map(|x| x * x).sum::<f32>().sqrt();
90        if norm > 0.0 {
91            for x in &mut v {
92                *x /= norm;
93            }
94        }
95        v
96    }
97}
98
99#[async_trait::async_trait]
100impl EmbeddingProvider for DeterministicEmbedding {
101    async fn embed(&self, text: &str) -> Result<Vec<f32>> {
102        Ok(self.embed_one(text))
103    }
104
105    async fn embed_batch(&self, texts: &[&str]) -> Result<Vec<Vec<f32>>> {
106        Ok(texts.iter().map(|t| self.embed_one(t)).collect())
107    }
108
109    fn dimensions(&self) -> usize {
110        self.dimensions
111    }
112
113    // Inherits `is_semantic_capable() == true` — it emits real, non-zero vectors.
114}
115
116#[cfg(test)]
117mod tests {
118    use super::*;
119
120    #[tokio::test]
121    async fn noop_is_not_semantic_capable_but_deterministic_is() {
122        assert!(!NoopEmbedding::new(8).is_semantic_capable());
123        assert!(DeterministicEmbedding::new(8).is_semantic_capable());
124    }
125
126    #[tokio::test]
127    async fn deterministic_embedding_is_stable_and_nonzero() {
128        let e = DeterministicEmbedding::new(16);
129        let a = e.embed("clinician adjusted the dosage").await.unwrap();
130        let b = e.embed("clinician adjusted the dosage").await.unwrap();
131        assert_eq!(a, b, "same text embeds identically");
132        assert_eq!(a.len(), 16);
133        assert!(a.iter().any(|x| *x != 0.0), "produces non-zero vectors");
134    }
135}