1use async_trait::async_trait;
2use qql_core::error::QqlError;
3
4use crate::sparse::{self, SparseVector};
5
6#[cfg(not(target_arch = "wasm32"))]
7pub trait EmbedderBound: Send + Sync {}
9#[cfg(not(target_arch = "wasm32"))]
10impl<T: Send + Sync> EmbedderBound for T {}
11
12#[cfg(target_arch = "wasm32")]
13pub trait EmbedderBound {}
15#[cfg(target_arch = "wasm32")]
16impl<T> EmbedderBound for T {}
17
18#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
27#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
28pub trait Embedder: EmbedderBound {
29 async fn embed_dense(&self, text: &str, model: &str) -> Result<Vec<f32>, QqlError>;
31
32 async fn embed_sparse_query(&self, text: &str, model: &str) -> Result<SparseVector, QqlError> {
39 if !model.is_empty() && !model.eq_ignore_ascii_case("default") {
40 return Err(sparse_model_unsupported_error(model));
41 }
42 Ok(sparse::embed_query(text))
43 }
44
45 async fn embed_sparse_document(
53 &self,
54 text: &str,
55 model: &str,
56 ) -> Result<SparseVector, QqlError> {
57 if !model.is_empty() && !model.eq_ignore_ascii_case("default") {
58 return Err(sparse_model_unsupported_error(model));
59 }
60 Ok(sparse::embed_document(text))
61 }
62
63 async fn embed_sparse_document_batch(
66 &self,
67 texts: &[String],
68 model: &str,
69 ) -> Result<Vec<SparseVector>, QqlError> {
70 let mut results = Vec::with_capacity(texts.len());
71 for text in texts {
72 results.push(self.embed_sparse_document(text, model).await?);
73 }
74 Ok(results)
75 }
76
77 async fn embed_joint(&self, text: &str, model: &str) -> Result<JointEmbeddingOutput, QqlError> {
83 let dense = self.embed_dense(text, model).await?;
84 let sparse = self.embed_sparse_document(text, model).await?;
85 let multi = self.embed_multi(text, model).await?;
86 Ok(JointEmbeddingOutput {
87 dense: Some(dense),
88 sparse: Some(sparse),
89 multi: Some(multi),
90 })
91 }
92
93 async fn embed_joint_batch(
95 &self,
96 texts: &[String],
97 model: &str,
98 ) -> Result<Vec<JointEmbeddingOutput>, QqlError> {
99 let mut results = Vec::with_capacity(texts.len());
100 for text in texts {
101 results.push(self.embed_joint(text, model).await?);
102 }
103 Ok(results)
104 }
105
106 fn dimension(&self) -> Option<usize> {
109 None
110 }
111
112 fn multi_dimension(&self) -> Option<usize> {
114 None
115 }
116
117 fn accepts_model(&self, _model: &str) -> bool {
120 true
121 }
122
123 async fn embed_dense_batch(
126 &self,
127 texts: &[String],
128 model: &str,
129 ) -> Result<Vec<Vec<f32>>, QqlError> {
130 let mut results = Vec::with_capacity(texts.len());
131 for text in texts {
132 results.push(self.embed_dense(text, model).await?);
133 }
134 Ok(results)
135 }
136
137 async fn embed_multi(&self, text: &str, model: &str) -> Result<Vec<Vec<f32>>, QqlError> {
142 let _ = text;
143 Err(multi_unsupported_error(model))
144 }
145
146 async fn embed_multi_batch(
148 &self,
149 texts: &[String],
150 model: &str,
151 ) -> Result<Vec<Vec<Vec<f32>>>, QqlError> {
152 let mut results = Vec::with_capacity(texts.len());
153 for text in texts {
154 results.push(self.embed_multi(text, model).await?);
155 }
156 Ok(results)
157 }
158
159 async fn embed_image(&self, source: &str, model: &str) -> Result<Vec<f32>, QqlError> {
164 let _ = source;
165 Err(image_unsupported_error(model))
166 }
167
168 async fn embed_image_batch(
170 &self,
171 sources: &[String],
172 model: &str,
173 ) -> Result<Vec<Vec<f32>>, QqlError> {
174 let mut results = Vec::with_capacity(sources.len());
175 for source in sources {
176 results.push(self.embed_image(source, model).await?);
177 }
178 Ok(results)
179 }
180
181 async fn rerank_pairs(
187 &self,
188 query: &str,
189 documents: &[String],
190 model: &str,
191 ) -> Result<Vec<f32>, QqlError> {
192 let _ = (query, documents);
193 Err(cross_rerank_unsupported_error(model))
194 }
195}
196
197pub fn multi_unsupported_error(model: &str) -> QqlError {
199 let model_note = if model.is_empty() || model.eq_ignore_ascii_case("default") {
200 "no model specified".to_string()
201 } else {
202 format!("model='{model}'")
203 };
204 QqlError::execution(
205 "QQL-EMBEDDING-MULTI",
206 format!(
207 "multi-vector embedding is not available ({model_note}). \
208 Configure a multi embedder (multi_embedding_endpoint / multi_embedding_model, \
209 or edge multi_model for offline BGE-M3), pass precomputed VECTOR [[...], ...], \
210 or use UPSERT with explicit multivector bags."
211 ),
212 None,
213 )
214}
215
216pub fn image_unsupported_error(model: &str) -> QqlError {
218 let model_note = if model.is_empty() || model.eq_ignore_ascii_case("default") {
219 "no model specified".to_string()
220 } else {
221 format!("model='{model}'")
222 };
223 QqlError::execution(
224 "QQL-EMBEDDING-IMAGE",
225 format!(
226 "image embedding is not available ({model_note}). \
227 Configure an image/CLIP vision embedder (image_embedding_model / edge image_model, \
228 or image_embedding_endpoint), pass a precomputed VECTOR [...], \
229 or use UPSERT USING IMAGE ON FIELD <path_field>."
230 ),
231 None,
232 )
233}
234
235pub fn cross_rerank_unsupported_error(model: &str) -> QqlError {
237 let model_note = if model.is_empty() || model.eq_ignore_ascii_case("default") {
238 "no model specified".to_string()
239 } else {
240 format!("model='{model}'")
241 };
242 QqlError::execution(
243 "QQL-RERANK-CROSS",
244 format!(
245 "cross-encoder pair scoring is not available ({model_note}). \
246 Configure a rerank host (rerank_endpoint / rerank_model, or edge \
247 reranker_model for offline TextRerank / bge-reranker)."
248 ),
249 None,
250 )
251}
252
253pub fn sparse_model_unsupported_error(model: &str) -> QqlError {
255 QqlError::execution(
256 "QQL-EMBEDDING-SPARSE",
257 format!(
258 "sparse model '{model}' is not available on this embedder. \
259 Omit the MODEL clause (or use MODEL 'default') for local \
260 wire-compatible BM25. To use model-aware sparse embedding \
261 (SPLADE / BGE-M3), configure a sparse embedding backend."
262 ),
263 None,
264 )
265}
266
267#[derive(Debug, Clone, Default, PartialEq)]
269pub struct JointEmbeddingOutput {
270 pub dense: Option<Vec<f32>>,
272 pub sparse: Option<SparseVector>,
274 pub multi: Option<Vec<Vec<f32>>>,
276}
277
278pub struct SparseEmbedder;
280
281impl SparseEmbedder {
282 pub fn embed_query(text: &str) -> SparseVector {
284 sparse::embed_query(text)
285 }
286
287 pub fn embed_document(text: &str) -> SparseVector {
289 sparse::embed_document(text)
290 }
291}