clawft_kernel/
embedding_e5.rs1use std::path::{Path, PathBuf};
20
21use async_trait::async_trait;
22
23use crate::embedding::{EmbeddingError, EmbeddingProvider};
24use crate::embedding_onnx::OnnxEmbeddingProvider;
25
26pub const E5_MODEL_NAME: &str = "e5-small-v2";
28
29pub const E5_DIMS: usize = 384;
31
32pub const E5_MAX_TOKENS: usize = 512;
34
35pub const PASSAGE_PREFIX: &str = "passage: ";
37
38pub const QUERY_PREFIX: &str = "query: ";
40
41pub struct E5EmbeddingProvider {
43 inner: OnnxEmbeddingProvider,
44 model_name: String,
46}
47
48impl E5EmbeddingProvider {
49 pub fn new(model_path: impl Into<PathBuf>) -> Self {
55 let model_path = model_path.into();
56 let inner =
57 OnnxEmbeddingProvider::with_config(model_path, E5_DIMS, E5_MAX_TOKENS);
58 let model_name = if inner.is_runtime_available() {
59 E5_MODEL_NAME.to_string()
60 } else {
61 format!("{E5_MODEL_NAME}-hash-fallback")
62 };
63 Self { inner, model_name }
64 }
65
66 pub fn is_runtime_available(&self) -> bool {
68 self.inner.is_runtime_available()
69 }
70
71 pub fn model_path(&self) -> &Path {
73 self.inner.model_path()
74 }
75
76 pub fn prefix_passage(text: &str) -> String {
78 if text.starts_with(PASSAGE_PREFIX) || text.starts_with(QUERY_PREFIX) {
79 text.to_string()
80 } else {
81 format!("{PASSAGE_PREFIX}{text}")
82 }
83 }
84
85 pub fn prefix_query(text: &str) -> String {
87 if text.starts_with(QUERY_PREFIX) || text.starts_with(PASSAGE_PREFIX) {
88 text.to_string()
89 } else {
90 format!("{QUERY_PREFIX}{text}")
91 }
92 }
93}
94
95#[async_trait]
96impl EmbeddingProvider for E5EmbeddingProvider {
97 async fn embed(&self, text: &str) -> Result<Vec<f32>, EmbeddingError> {
98 self.inner.embed(&Self::prefix_passage(text)).await
100 }
101
102 async fn embed_batch(&self, texts: &[&str]) -> Result<Vec<Vec<f32>>, EmbeddingError> {
103 let mut results = Vec::with_capacity(texts.len());
104 for text in texts {
105 results.push(self.embed(text).await?);
106 }
107 Ok(results)
108 }
109
110 async fn embed_query(&self, query: &str) -> Result<Vec<f32>, EmbeddingError> {
111 self.inner.embed(&Self::prefix_query(query)).await
113 }
114
115 fn dimensions(&self) -> usize {
116 self.inner.dimensions()
117 }
118
119 fn model_name(&self) -> &str {
120 &self.model_name
121 }
122}
123
124#[cfg(test)]
125mod tests {
126 use super::*;
127
128 #[test]
129 fn prefix_passage_is_idempotent() {
130 let once = E5EmbeddingProvider::prefix_passage("hello");
131 assert_eq!(once, "passage: hello");
132 assert_eq!(E5EmbeddingProvider::prefix_passage(&once), once);
133 }
134
135 #[test]
136 fn prefix_query_is_idempotent() {
137 let once = E5EmbeddingProvider::prefix_query("find deploys");
138 assert_eq!(once, "query: find deploys");
139 assert_eq!(E5EmbeddingProvider::prefix_query(&once), once);
140 }
141
142 #[test]
143 fn degraded_provider_reports_hash_fallback_name() {
144 let p = E5EmbeddingProvider::new("/nonexistent/e5-small-v2/model.onnx");
146 assert!(!p.is_runtime_available());
147 assert_eq!(p.model_name(), "e5-small-v2-hash-fallback");
148 assert_eq!(p.dimensions(), E5_DIMS);
149 }
150
151 #[tokio::test]
152 async fn embed_and_embed_query_differ_under_hash_fallback() {
153 let p = E5EmbeddingProvider::new("/nonexistent/e5-small-v2/model.onnx");
156 let doc = p.embed("the deploy OOM'd on model load").await.unwrap();
157 let qry = p.embed_query("the deploy OOM'd on model load").await.unwrap();
158 assert_eq!(doc.len(), E5_DIMS);
159 assert_eq!(qry.len(), E5_DIMS);
160 assert_ne!(
161 doc, qry,
162 "query: vs passage: prefixes must change the hash-fallback vector"
163 );
164 }
165
166 #[tokio::test]
167 async fn embed_is_deterministic() {
168 let p = E5EmbeddingProvider::new("/nonexistent/e5-small-v2/model.onnx");
169 let a = p.embed("stable input").await.unwrap();
170 let b = p.embed("stable input").await.unwrap();
171 assert_eq!(a, b);
172 }
173}