ragfs_embed/
noop.rs

1//! No-op embedder for testing without Candle.
2//!
3//! This module provides a [`NoopEmbedder`] that returns zero-vectors for all embeddings.
4//! It's useful for:
5//! - Testing without the Candle ML stack
6//! - Development builds with faster compilation
7//! - Stubbing embeddings in unit tests
8
9use async_trait::async_trait;
10use ragfs_core::{EmbedError, Embedder, EmbeddingConfig, EmbeddingOutput, Modality};
11
12/// No-op embedder that returns zero-vectors.
13///
14/// This embedder is always available, even without the `candle` feature.
15/// It returns 384-dimensional zero-vectors for all inputs, making it useful
16/// for testing and development.
17///
18/// # Example
19///
20/// ```rust
21/// use ragfs_embed::NoopEmbedder;
22/// use ragfs_core::{Embedder, EmbeddingConfig};
23///
24/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
25/// let embedder = NoopEmbedder::new();
26/// let config = EmbeddingConfig::default();
27/// let texts: Vec<&str> = vec!["Hello", "World"];
28/// let outputs = embedder.embed_text(&texts, &config).await?;
29///
30/// assert_eq!(outputs.len(), 2);
31/// assert_eq!(outputs[0].embedding.len(), 384);
32/// assert!(outputs[0].embedding.iter().all(|&v| v == 0.0));
33/// # Ok(())
34/// # }
35/// ```
36pub struct NoopEmbedder {
37    dimension: usize,
38    modalities: Vec<Modality>,
39}
40
41impl NoopEmbedder {
42    /// Create a new no-op embedder with default dimension (384).
43    #[must_use]
44    pub fn new() -> Self {
45        Self {
46            dimension: 384,
47            modalities: vec![Modality::Text],
48        }
49    }
50
51    /// Create a new no-op embedder with custom dimension.
52    #[must_use]
53    pub fn with_dimension(dimension: usize) -> Self {
54        Self {
55            dimension,
56            modalities: vec![Modality::Text],
57        }
58    }
59}
60
61impl Default for NoopEmbedder {
62    fn default() -> Self {
63        Self::new()
64    }
65}
66
67#[async_trait]
68impl Embedder for NoopEmbedder {
69    fn model_name(&self) -> &str {
70        "noop"
71    }
72
73    fn dimension(&self) -> usize {
74        self.dimension
75    }
76
77    fn max_tokens(&self) -> usize {
78        512
79    }
80
81    fn modalities(&self) -> &[Modality] {
82        &self.modalities
83    }
84
85    async fn embed_text(
86        &self,
87        texts: &[&str],
88        _config: &EmbeddingConfig,
89    ) -> Result<Vec<EmbeddingOutput>, EmbedError> {
90        Ok(texts
91            .iter()
92            .map(|_| EmbeddingOutput {
93                embedding: vec![0.0; self.dimension],
94                token_count: 0,
95            })
96            .collect())
97    }
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103
104    #[test]
105    fn test_noop_new() {
106        let embedder = NoopEmbedder::new();
107        assert_eq!(embedder.dimension(), 384);
108        assert_eq!(embedder.model_name(), "noop");
109    }
110
111    #[test]
112    fn test_noop_default() {
113        let embedder = NoopEmbedder::default();
114        assert_eq!(embedder.dimension(), 384);
115    }
116
117    #[test]
118    fn test_noop_with_dimension() {
119        let embedder = NoopEmbedder::with_dimension(768);
120        assert_eq!(embedder.dimension(), 768);
121    }
122
123    #[test]
124    fn test_noop_modalities() {
125        let embedder = NoopEmbedder::new();
126        let modalities = embedder.modalities();
127        assert_eq!(modalities.len(), 1);
128        assert!(matches!(modalities[0], Modality::Text));
129    }
130
131    #[test]
132    fn test_noop_max_tokens() {
133        let embedder = NoopEmbedder::new();
134        assert_eq!(embedder.max_tokens(), 512);
135    }
136
137    #[tokio::test]
138    async fn test_noop_embed_text() {
139        let embedder = NoopEmbedder::new();
140        let texts: Vec<&str> = vec!["Hello", "World"];
141        let config = EmbeddingConfig::default();
142
143        let outputs = embedder.embed_text(&texts, &config).await.unwrap();
144
145        assert_eq!(outputs.len(), 2);
146        assert_eq!(outputs[0].embedding.len(), 384);
147        assert_eq!(outputs[1].embedding.len(), 384);
148        assert!(outputs[0].embedding.iter().all(|&v| v == 0.0));
149        assert_eq!(outputs[0].token_count, 0);
150    }
151
152    #[tokio::test]
153    async fn test_noop_embed_empty() {
154        let embedder = NoopEmbedder::new();
155        let texts: Vec<&str> = vec![];
156        let config = EmbeddingConfig::default();
157
158        let outputs = embedder.embed_text(&texts, &config).await.unwrap();
159
160        assert!(outputs.is_empty());
161    }
162
163    #[tokio::test]
164    async fn test_noop_embed_custom_dimension() {
165        let embedder = NoopEmbedder::with_dimension(768);
166        let texts: Vec<&str> = vec!["Test"];
167        let config = EmbeddingConfig::default();
168
169        let outputs = embedder.embed_text(&texts, &config).await.unwrap();
170
171        assert_eq!(outputs[0].embedding.len(), 768);
172    }
173}