Skip to main content

lc_embeddings/
lib.rs

1#![warn(missing_docs)]
2// lc-embeddings/src/lib.rs
3//! Embedding model implementations for LangChainRust.
4//!
5//! Provides embedding generation via multiple backends:
6//! - OpenAI (`text-embedding-ada-002`, `text-embedding-3-small/large`)
7//! - DeepSeek
8//! - Qwen (Alibaba Cloud / DashScope)
9//! - Local: `BagOfWordsEmbeddings` (always available) and `LocalEmbeddings` (ONNX, feature-gated)
10//! - `MockEmbeddings` for testing
11//!
12//! Multimodal (image+text shared space) backends live behind
13//! [`VisionEmbeddings`]: Cohere Embed v4 and DashScope `multimodal-embedding-v1`.
14
15mod cohere;
16mod deepseek;
17mod local;
18mod mock;
19mod openai;
20pub mod openai_compat;
21mod qwen;
22mod retry;
23pub mod token_level;
24mod vision;
25mod vision_cohere;
26mod vision_qwen;
27
28#[cfg(test)]
29mod test_support;
30
31#[cfg(feature = "fastembed")]
32mod fastembed_emb;
33
34#[cfg(feature = "local-candle")]
35mod candle_emb;
36
37pub use cohere::{
38    CohereEmbedInputType, CohereEmbeddings, CohereEmbeddingsConfig, COHERE_EMBED_BASE_URL,
39    COHERE_EMBED_MODEL,
40};
41pub use deepseek::{DeepSeekEmbeddings, DeepSeekEmbeddingsConfig, DEEPSEEK_EMBED_MODEL};
42pub use local::BagOfWordsEmbeddings;
43// As of 1.0, `LocalEmbeddings` is not available without the `local-embeddings` feature (the
44// old fallback alias was removed); users must explicitly choose — `BagOfWordsEmbeddings` or
45// the ONNX version via the feature.
46#[cfg(feature = "local-embeddings")]
47pub use local::{LocalEmbeddings, LocalEmbeddingsBuilder};
48pub use mock::MockEmbeddings;
49pub use openai::{OpenAIEmbeddings, OpenAIEmbeddingsConfig};
50pub use qwen::{QwenEmbeddings, QwenEmbeddingsConfig, QWEN_EMBED_MODEL};
51pub use token_level::{
52    char_spans_to_byte_spans, LocalTokenLevelEmbeddings, TokenEmbedding, TokenLevelEmbeddings,
53    TokenSpan,
54};
55// B7 (v0.22.4): cross-modal vision embeddings — trait, mock, and the Cohere
56// Embed v4 / DashScope multimodal-embedding-v1 backends.
57pub use vision::{ImageInput, MockVisionEmbeddings, VisionEmbeddings};
58pub use vision_cohere::{
59    CohereVisionEmbeddings, CohereVisionEmbeddingsConfig, COHERE_VISION_DIMENSION,
60    COHERE_VISION_EMBED_MODEL,
61};
62pub use vision_qwen::{
63    QwenVisionEmbeddings, QwenVisionEmbeddingsConfig, QWEN_VISION_BASE_URL, QWEN_VISION_DIMENSION,
64    QWEN_VISION_EMBED_MODEL,
65};
66
67#[cfg(feature = "fastembed")]
68pub use fastembed_emb::FastEmbedEmbeddings;
69
70#[cfg(feature = "local-candle")]
71pub use candle_emb::CandleEmbeddings;
72
73/// Debug wrapper that always prints `[REDACTED]`, never the wrapped value.
74///
75/// M-12: provider `Config` structs keep `pub api_key: String` (public API contract) but must
76/// not leak the key into logs via a derived `Debug`. Configs implement `Debug` manually and
77/// pass their key field through this wrapper so the secret never appears in output.
78pub(crate) struct RedactedDebug<T>(pub T);
79
80impl<T> std::fmt::Debug for RedactedDebug<T> {
81    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
82        f.write_str("[REDACTED]")
83    }
84}
85
86use async_trait::async_trait;
87
88/// Embedding error type
89#[derive(Debug, thiserror::Error)]
90#[non_exhaustive]
91pub enum EmbeddingError {
92    /// HTTP request error
93    #[error("HTTP error: {0}")]
94    HttpError(String),
95
96    /// API error
97    #[error("API error: {0}")]
98    ApiError(String),
99
100    /// Parse error
101    #[error("Parse error: {0}")]
102    ParseError(String),
103
104    /// Configuration error (e.g. empty API key, unknown model dimension) — fails fast at construction.
105    #[error("Configuration error: {0}")]
106    Config(String),
107
108    /// Empty input
109    #[error("Input is empty")]
110    EmptyInput,
111
112    /// Batch embedding misalignment: for N requested texts, the provider returned a vector
113    /// count or index outside the expected range (a chunk missing entries / out of order).
114    ///
115    /// P0-1: reject silently misaligned data — never treat a missing vector as "dissimilar".
116    #[error("Embedding batch mismatch: expected {expected} vectors, got position {actual}")]
117    BatchMismatch {
118        /// The expected number of vectors
119        expected: usize,
120        /// The position index where the misalignment occurred
121        actual: usize,
122    },
123
124    /// Some text in a batch got no vector (the provider returned fewer embeddings than requested).
125    ///
126    /// P0-1: reject silently empty vectors — a missing vector is an explicit error, not a zero vector.
127    #[error("Embedding batch contains an empty vector (provider returned fewer embeddings than requested)")]
128    EmptyVectorInBatch,
129}
130
131/// Embedding model trait
132///
133/// Defines the interface for generating text embedding vectors.
134///
135/// # Normalization contract (P2-8)
136///
137/// All returned vectors are **L2-normalized** unit vectors (except zero vectors),
138/// regardless of whether the provider normalizes internally. This guarantees downstream
139/// cosine, dot-product, or L2-distance results do not drift across providers. HTTP
140/// providers call [`l2_normalize`] uniformly before returning.
141#[async_trait]
142pub trait Embeddings: Send + Sync {
143    /// Generate an embedding vector for a single text.
144    ///
145    /// # Arguments
146    /// * `text` - Input text
147    ///
148    /// # Returns
149    /// Embedding vector (typically 1536 dimensions or higher)
150    async fn embed_query(&self, text: &str) -> Result<Vec<f32>, EmbeddingError>;
151
152    /// Generate embedding vectors for multiple documents.
153    ///
154    /// # Arguments
155    /// * `texts` - List of input texts
156    ///
157    /// # Returns
158    /// List of embedding vectors
159    ///
160    /// # Semantic contract (P1-1)
161    ///
162    /// - Any empty or all-whitespace text (`trim().is_empty()`) → `Err(EmbeddingError::EmptyInput)`;
163    /// - An empty slice `&[]` is treated as "no text to embed" → `Ok(vec![])` (nothing to do is not an error).
164    ///
165    /// The default implementation loops over [`Self::embed_query`] with a uniform emptiness
166    /// check up front; providers that override it must follow the same contract and not diverge.
167    async fn embed_documents(&self, texts: &[&str]) -> Result<Vec<Vec<f32>>, EmbeddingError> {
168        if texts.iter().any(|t| t.trim().is_empty()) {
169            return Err(EmbeddingError::EmptyInput);
170        }
171        let mut embeddings = Vec::new();
172        for text in texts {
173            embeddings.push(self.embed_query(text).await?);
174        }
175        Ok(embeddings)
176    }
177
178    /// Get the embedding vector dimension.
179    fn dimension(&self) -> usize;
180
181    /// Get the model name.
182    fn model_name(&self) -> &str;
183}
184
185/// Compute cosine similarity between two vectors.
186///
187/// Re-exported from [`lc_core::math::cosine_similarity`].
188pub use lc_core::math::cosine_similarity;
189
190/// In-place L2 normalization: scale `vec` to unit length.
191///
192/// P2-8: providers normalize returned vectors differently (OpenAI normalizes, BOW
193/// self-normalizes, remote providers such as Cohere may not); downstream results using
194/// dot-product/L2 distance instead of cosine would drift across providers. This function
195/// is the **single** normalization implementation; HTTP providers call it uniformly before
196/// returning, ensuring `Embeddings` always produces unit-length vectors.
197///
198/// A zero vector stays zero (no NaN is produced).
199pub fn l2_normalize(vec: &mut [f32]) {
200    let norm: f32 = vec.iter().map(|x| x * x).sum::<f32>().sqrt();
201    if norm > 0.0 {
202        for v in vec.iter_mut() {
203            *v /= norm;
204        }
205    }
206}
207
208/// Mutex for synchronizing environment-variable mutations in tests.
209///
210/// Tests that set/remove env vars must acquire this lock to avoid data races
211/// when running tests in parallel (the default for `cargo test`).
212#[cfg(test)]
213static ENV_TEST_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
214
215#[cfg(test)]
216mod tests {
217    use super::*;
218
219    #[test]
220    fn test_cosine_similarity() {
221        // Identical vectors
222        let a = vec![1.0, 0.0, 0.0];
223        let b = vec![1.0, 0.0, 0.0];
224        assert!((cosine_similarity(&a, &b).unwrap() - 1.0).abs() < 0.0001);
225
226        // Orthogonal vectors
227        let a = vec![1.0, 0.0, 0.0];
228        let b = vec![0.0, 1.0, 0.0];
229        assert!((cosine_similarity(&a, &b).unwrap() - 0.0).abs() < 0.0001);
230
231        // Opposite vectors
232        let a = vec![1.0, 0.0, 0.0];
233        let b = vec![-1.0, 0.0, 0.0];
234        assert!((cosine_similarity(&a, &b).unwrap() - (-1.0)).abs() < 0.0001);
235    }
236
237    #[test]
238    fn test_cosine_similarity_different_lengths() {
239        let a = vec![1.0, 0.0];
240        let b = vec![1.0, 0.0, 0.0];
241        assert!(cosine_similarity(&a, &b).is_err());
242    }
243}