lc_embeddings/lib.rs
1// lc-embeddings/src/lib.rs
2//! Embedding model implementations for LangChainRust.
3//!
4//! Provides embedding generation via multiple backends:
5//! - OpenAI (`text-embedding-ada-002`, `text-embedding-3-small/large`)
6//! - DeepSeek
7//! - Qwen (Alibaba Cloud / DashScope)
8//! - Local: `BagOfWordsEmbeddings` (always available) and `LocalEmbeddings` (ONNX, feature-gated)
9//! - `MockEmbeddings` for testing
10
11mod cohere;
12mod deepseek;
13mod local;
14mod mock;
15mod openai;
16pub mod openai_compat;
17mod qwen;
18mod retry;
19
20#[cfg(test)]
21mod test_support;
22
23#[cfg(feature = "fastembed")]
24mod fastembed_emb;
25
26pub use cohere::{
27 CohereEmbedInputType, CohereEmbeddings, CohereEmbeddingsConfig, COHERE_EMBED_BASE_URL,
28 COHERE_EMBED_MODEL,
29};
30pub use deepseek::{DeepSeekEmbeddings, DeepSeekEmbeddingsConfig, DEEPSEEK_EMBED_MODEL};
31// P2-1: 无 `local-embeddings` feature 时 `LocalEmbeddings` 是已弃用的
32// BagOfWordsEmbeddings 别名(静默降级);`#[allow(deprecated)]` 豁免重导出警告。
33#[allow(deprecated)]
34pub use local::{BagOfWordsEmbeddings, LocalEmbeddings};
35pub use mock::MockEmbeddings;
36pub use openai::{OpenAIEmbeddings, OpenAIEmbeddingsConfig};
37pub use qwen::{QwenEmbeddings, QwenEmbeddingsConfig, QWEN_EMBED_MODEL};
38
39#[cfg(feature = "fastembed")]
40pub use fastembed_emb::FastEmbedEmbeddings;
41
42use async_trait::async_trait;
43
44/// Embedding error type
45#[derive(Debug, thiserror::Error)]
46pub enum EmbeddingError {
47 /// HTTP request error
48 #[error("HTTP error: {0}")]
49 HttpError(String),
50
51 /// API error
52 #[error("API error: {0}")]
53 ApiError(String),
54
55 /// Parse error
56 #[error("Parse error: {0}")]
57 ParseError(String),
58
59 /// 配置错误(如 API key 为空、模型维度未知)——构造期 fail fast。
60 #[error("Configuration error: {0}")]
61 Config(String),
62
63 /// Empty input
64 #[error("Input is empty")]
65 EmptyInput,
66
67 /// 批量 embedding 数量错位:请求 N 条文本,服务端返回的向量数量或 index
68 /// 超出了预期范围(某 chunk 少返回/乱序导致)。
69 ///
70 /// P0-1: 拒绝静默错数据——绝不把缺失向量当成"不相似"。
71 #[error("Embedding batch mismatch: expected {expected} vectors, got position {actual}")]
72 BatchMismatch { expected: usize, actual: usize },
73
74 /// 批量 embedding 中某条文本未取到向量(服务端返回量 < 请求量)。
75 ///
76 /// P0-1: 拒绝静默空向量——缺失即显式报错,而非留下零向量。
77 #[error("Embedding batch contains an empty vector (provider returned fewer embeddings than requested)")]
78 EmptyVectorInBatch,
79}
80
81/// Embedding model trait
82///
83/// Defines the interface for generating text embedding vectors.
84///
85/// # 归一化契约(P2-8)
86///
87/// 所有返回的向量都是 **L2 归一化**的单位向量(零向量除外),与 provider 内部
88/// 是否已归一化无关。这保证下游无论用 cosine、点积还是 L2 距离,结果都不随
89/// provider 漂移。HTTP provider 在返回前统一调用 [`l2_normalize`]。
90#[async_trait]
91pub trait Embeddings: Send + Sync {
92 /// Generate an embedding vector for a single text.
93 ///
94 /// # Arguments
95 /// * `text` - Input text
96 ///
97 /// # Returns
98 /// Embedding vector (typically 1536 dimensions or higher)
99 async fn embed_query(&self, text: &str) -> Result<Vec<f32>, EmbeddingError>;
100
101 /// Generate embedding vectors for multiple documents.
102 ///
103 /// # Arguments
104 /// * `texts` - List of input texts
105 ///
106 /// # Returns
107 /// List of embedding vectors
108 ///
109 /// # 语义约定(P1-1)
110 ///
111 /// - 任一文本为空或全空白(`trim().is_empty()`)→ `Err(EmbeddingError::EmptyInput)`;
112 /// - 空切片 `&[]` 视为"没有要嵌入的文本"→ `Ok(vec![])`(无事可做不算错误)。
113 ///
114 /// 默认实现循环调用 [`Self::embed_query`],并前置统一判空;各 provider
115 /// 覆写时须遵循同样的契约,不得再各自分裂。
116 async fn embed_documents(&self, texts: &[&str]) -> Result<Vec<Vec<f32>>, EmbeddingError> {
117 if texts.iter().any(|t| t.trim().is_empty()) {
118 return Err(EmbeddingError::EmptyInput);
119 }
120 let mut embeddings = Vec::new();
121 for text in texts {
122 embeddings.push(self.embed_query(text).await?);
123 }
124 Ok(embeddings)
125 }
126
127 /// Get the embedding vector dimension.
128 fn dimension(&self) -> usize;
129
130 /// Get the model name.
131 fn model_name(&self) -> &str;
132}
133
134/// Compute cosine similarity between two vectors.
135///
136/// Re-exported from [`lc_core::math::cosine_similarity`].
137pub use lc_core::math::cosine_similarity;
138
139/// In-place L2 normalization: scale `vec` to unit length.
140///
141/// P2-8: 各 provider 返回向量的归一化口径不一(OpenAI 已归一化、BOW 自归一化、
142/// Cohere 等远程 provider 可能不),下游一旦用点积/L2 距离而非 cosine,结果会因
143/// provider 漂移。本函数是**唯一**的归一化实现,HTTP provider 在返回前统一调用,
144/// 保证 `Embeddings` 产出的向量恒为单位长度。
145///
146/// 零向量保持零向量(不产生 NaN)。
147pub fn l2_normalize(vec: &mut [f32]) {
148 let norm: f32 = vec.iter().map(|x| x * x).sum::<f32>().sqrt();
149 if norm > 0.0 {
150 for v in vec.iter_mut() {
151 *v /= norm;
152 }
153 }
154}
155
156/// Mutex for synchronizing environment-variable mutations in tests.
157///
158/// Tests that set/remove env vars must acquire this lock to avoid data races
159/// when running tests in parallel (the default for `cargo test`).
160#[cfg(test)]
161static ENV_TEST_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
162
163#[cfg(test)]
164mod tests {
165 use super::*;
166
167 #[test]
168 fn test_cosine_similarity() {
169 // Identical vectors
170 let a = vec![1.0, 0.0, 0.0];
171 let b = vec![1.0, 0.0, 0.0];
172 assert!((cosine_similarity(&a, &b).unwrap() - 1.0).abs() < 0.0001);
173
174 // Orthogonal vectors
175 let a = vec![1.0, 0.0, 0.0];
176 let b = vec![0.0, 1.0, 0.0];
177 assert!((cosine_similarity(&a, &b).unwrap() - 0.0).abs() < 0.0001);
178
179 // Opposite vectors
180 let a = vec![1.0, 0.0, 0.0];
181 let b = vec![-1.0, 0.0, 0.0];
182 assert!((cosine_similarity(&a, &b).unwrap() - (-1.0)).abs() < 0.0001);
183 }
184
185 #[test]
186 fn test_cosine_similarity_different_lengths() {
187 let a = vec![1.0, 0.0];
188 let b = vec![1.0, 0.0, 0.0];
189 assert!(cosine_similarity(&a, &b).is_err());
190 }
191}