Skip to main content

cel_memory/
embedder.rs

1//! Embedder trait and reference implementations.
2//!
3//! The [`Embedder`] trait abstracts over local (fastembed/ONNX) and cloud
4//! (OpenAI/Voyage) embedding backends. Storage backends take a
5//! `Arc<dyn Embedder>` at construction and use it for write-time embedding
6//! and retrieval-time query embedding.
7//!
8//! v1 ships [`MockEmbedder`] — deterministic small-dim vectors for tests.
9//! Production embedders (e.g. `FastEmbedEmbedder` in `cel-memory-sqlite`)
10//! live in backend crates.
11
12use async_trait::async_trait;
13use thiserror::Error;
14
15/// Errors produced by an [`Embedder`].
16///
17/// Distinct from [`crate::MemoryError`] so the trait can be implemented in
18/// backend crates without coupling to the full memory error surface. Callers
19/// map these at the provider boundary via [`From<EmbedderError>`].
20#[derive(Debug, Error)]
21pub enum EmbedderError {
22    /// The embedder's upstream client failed (model load, rate limit, etc.).
23    #[error("provider error: {0}")]
24    Provider(String),
25
26    /// The embedder produced a vector with the wrong dimensionality.
27    #[error("embedding dim mismatch: expected {expected}, got {actual}")]
28    DimMismatch {
29        /// Declared [`Embedder::dim`].
30        expected: usize,
31        /// Length of the vector actually returned.
32        actual: usize,
33    },
34
35    /// An unexpected internal error. Indicates a bug.
36    #[error("internal error: {0}")]
37    Internal(String),
38}
39
40/// Result alias for embedder operations.
41pub type EmbedderResult<T> = std::result::Result<T, EmbedderError>;
42
43/// An embedder turns text into a fixed-dimension vector.
44///
45/// Implementations must produce vectors of [`Embedder::dim`] length on
46/// every call; the storage layer validates and rejects mismatches.
47#[async_trait]
48pub trait Embedder: Send + Sync {
49    /// Vector dimensionality.
50    fn dim(&self) -> usize;
51
52    /// Stable model identifier (e.g., `"bge-small-en-v1.5"`).
53    fn model_name(&self) -> &str;
54
55    /// Embed one piece of text.
56    async fn embed(&self, text: &str) -> EmbedderResult<Vec<f32>>;
57
58    /// Embed a batch of texts. Default implementation calls [`embed`]
59    /// sequentially; production embedders should override for batching.
60    ///
61    /// [`embed`]: Embedder::embed
62    async fn embed_batch(&self, texts: &[String]) -> EmbedderResult<Vec<Vec<f32>>> {
63        let mut out = Vec::with_capacity(texts.len());
64        for t in texts {
65            out.push(self.embed(t).await?);
66        }
67        Ok(out)
68    }
69}
70
71/// Deterministic test embedder. Hashes the input text to a small vector
72/// of pseudo-random floats. **Never use in production** — produces
73/// meaningless vectors.
74///
75/// Useful for unit tests of storage backends where we just need *some*
76/// vector to round-trip through vector tables.
77#[derive(Debug, Clone)]
78pub struct MockEmbedder {
79    dim: usize,
80    model: String,
81}
82
83impl MockEmbedder {
84    /// Mock embedder with the default `384` dimension, matching common
85    /// local embedding models.
86    pub fn new() -> Self {
87        Self {
88            dim: 384,
89            model: "mock-384".into(),
90        }
91    }
92
93    /// Mock embedder with an arbitrary dim. Use only for tests that
94    /// override the migration schema.
95    pub fn with_dim(dim: usize) -> Self {
96        Self {
97            dim,
98            model: format!("mock-{dim}"),
99        }
100    }
101}
102
103impl Default for MockEmbedder {
104    fn default() -> Self {
105        Self::new()
106    }
107}
108
109#[async_trait]
110impl Embedder for MockEmbedder {
111    fn dim(&self) -> usize {
112        self.dim
113    }
114
115    fn model_name(&self) -> &str {
116        &self.model
117    }
118
119    async fn embed(&self, text: &str) -> EmbedderResult<Vec<f32>> {
120        let mut seed: u64 = 0xcbf29ce484222325;
121        for b in text.bytes() {
122            seed ^= b as u64;
123            seed = seed.wrapping_mul(0x100000001b3);
124        }
125        let mut out = Vec::with_capacity(self.dim);
126        for i in 0..self.dim {
127            seed = seed
128                .wrapping_mul(6364136223846793005)
129                .wrapping_add(1442695040888963407);
130            let f = ((seed >> (i % 32)) as i32) as f32 / i32::MAX as f32;
131            out.push(f.clamp(-1.0, 1.0));
132        }
133        Ok(out)
134    }
135}
136
137impl From<EmbedderError> for crate::MemoryError {
138    fn from(e: EmbedderError) -> Self {
139        match e {
140            EmbedderError::DimMismatch { expected, actual } => crate::MemoryError::InvalidArgument(
141                format!("embedding dim mismatch: expected {expected}, got {actual}"),
142            ),
143            EmbedderError::Provider(msg) => crate::MemoryError::Provider(msg),
144            EmbedderError::Internal(msg) => crate::MemoryError::Internal(msg),
145        }
146    }
147}
148
149#[cfg(test)]
150mod tests {
151    use super::*;
152
153    #[tokio::test]
154    async fn mock_embedder_deterministic() {
155        let e = MockEmbedder::new();
156        let a = e.embed("hello").await.unwrap();
157        let b = e.embed("hello").await.unwrap();
158        assert_eq!(a, b);
159        assert_eq!(a.len(), 384);
160    }
161
162    #[tokio::test]
163    async fn mock_embedder_different_for_different_input() {
164        let e = MockEmbedder::new();
165        let a = e.embed("hello").await.unwrap();
166        let b = e.embed("world").await.unwrap();
167        assert_ne!(a, b);
168    }
169
170    #[tokio::test]
171    async fn mock_embedder_with_dim_honors_dim() {
172        let e = MockEmbedder::with_dim(8);
173        let v = e.embed("hi").await.unwrap();
174        assert_eq!(v.len(), 8);
175        assert_eq!(e.dim(), 8);
176    }
177
178    #[tokio::test]
179    async fn batch_default_works() {
180        let e = MockEmbedder::new();
181        let v = e
182            .embed_batch(&["a".to_string(), "b".to_string()])
183            .await
184            .unwrap();
185        assert_eq!(v.len(), 2);
186        assert_eq!(v[0].len(), 384);
187    }
188}