1use async_trait::async_trait;
2use qql_core::error::QqlError;
3
4use crate::sparse::{self, Bm25Params, 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(
54 &self,
55 text: &str,
56 model: &str,
57 ) -> Result<SparseVector, QqlError> {
58 if !model.is_empty() && !model.eq_ignore_ascii_case("default") {
59 return Err(sparse_model_unsupported_error(model));
60 }
61 Ok(sparse::embed_document_with_params(
62 text,
63 &self.bm25_params(),
64 ))
65 }
66
67 async fn embed_sparse_document_batch(
70 &self,
71 texts: &[String],
72 model: &str,
73 ) -> Result<Vec<SparseVector>, QqlError> {
74 let mut results = Vec::with_capacity(texts.len());
75 for text in texts {
76 results.push(self.embed_sparse_document(text, model).await?);
77 }
78 Ok(results)
79 }
80
81 async fn embed_sparse_query_batch(
85 &self,
86 texts: &[String],
87 model: &str,
88 ) -> Result<Vec<SparseVector>, QqlError> {
89 let mut results = Vec::with_capacity(texts.len());
90 for text in texts {
91 results.push(self.embed_sparse_query(text, model).await?);
92 }
93 Ok(results)
94 }
95
96 async fn embed_joint(&self, text: &str, model: &str) -> Result<JointEmbeddingOutput, QqlError> {
102 let dense = self.embed_dense(text, model).await?;
103 let sparse = self.embed_sparse_document(text, model).await?;
104 let multi = self.embed_multi(text, model).await?;
105 Ok(JointEmbeddingOutput {
106 dense: Some(dense),
107 sparse: Some(sparse),
108 multi: Some(multi),
109 })
110 }
111
112 async fn embed_joint_batch(
114 &self,
115 texts: &[String],
116 model: &str,
117 ) -> Result<Vec<JointEmbeddingOutput>, QqlError> {
118 let mut results = Vec::with_capacity(texts.len());
119 for text in texts {
120 results.push(self.embed_joint(text, model).await?);
121 }
122 Ok(results)
123 }
124
125 fn dimension(&self) -> Option<usize> {
128 None
129 }
130
131 fn bm25_params(&self) -> Bm25Params {
142 Bm25Params::default()
143 }
144
145 fn multi_dimension(&self) -> Option<usize> {
147 None
148 }
149
150 fn accepts_model(&self, _model: &str) -> bool {
153 true
154 }
155
156 async fn embed_dense_batch(
159 &self,
160 texts: &[String],
161 model: &str,
162 ) -> Result<Vec<Vec<f32>>, QqlError> {
163 let mut results = Vec::with_capacity(texts.len());
164 for text in texts {
165 results.push(self.embed_dense(text, model).await?);
166 }
167 Ok(results)
168 }
169
170 async fn embed_multi(&self, text: &str, model: &str) -> Result<Vec<Vec<f32>>, QqlError> {
175 let _ = text;
176 Err(multi_unsupported_error(model))
177 }
178
179 async fn embed_multi_batch(
181 &self,
182 texts: &[String],
183 model: &str,
184 ) -> Result<Vec<Vec<Vec<f32>>>, QqlError> {
185 let mut results = Vec::with_capacity(texts.len());
186 for text in texts {
187 results.push(self.embed_multi(text, model).await?);
188 }
189 Ok(results)
190 }
191
192 async fn embed_image(&self, source: &str, model: &str) -> Result<Vec<f32>, QqlError> {
197 let _ = source;
198 Err(image_unsupported_error(model))
199 }
200
201 async fn embed_image_batch(
203 &self,
204 sources: &[String],
205 model: &str,
206 ) -> Result<Vec<Vec<f32>>, QqlError> {
207 let mut results = Vec::with_capacity(sources.len());
208 for source in sources {
209 results.push(self.embed_image(source, model).await?);
210 }
211 Ok(results)
212 }
213
214 async fn rerank_pairs(
220 &self,
221 query: &str,
222 documents: &[String],
223 model: &str,
224 ) -> Result<Vec<f32>, QqlError> {
225 let _ = (query, documents);
226 Err(cross_rerank_unsupported_error(model))
227 }
228}
229
230pub fn multi_unsupported_error(model: &str) -> QqlError {
232 let model_note = if model.is_empty() || model.eq_ignore_ascii_case("default") {
233 "no model specified".to_string()
234 } else {
235 format!("model='{model}'")
236 };
237 QqlError::execution(
238 "QQL-EMBEDDING-MULTI",
239 format!(
240 "multi-vector embedding is not available ({model_note}). \
241 Configure a multi embedder (multi_embedding_endpoint / multi_embedding_model, \
242 or edge multi_model for offline BGE-M3), pass precomputed VECTOR [[...], ...], \
243 or use UPSERT with explicit multivector bags."
244 ),
245 None,
246 )
247}
248
249pub fn image_unsupported_error(model: &str) -> QqlError {
251 let model_note = if model.is_empty() || model.eq_ignore_ascii_case("default") {
252 "no model specified".to_string()
253 } else {
254 format!("model='{model}'")
255 };
256 QqlError::execution(
257 "QQL-EMBEDDING-IMAGE",
258 format!(
259 "image embedding is not available ({model_note}). \
260 Configure an image/CLIP vision embedder (image_embedding_model / edge image_model, \
261 or image_embedding_endpoint), pass a precomputed VECTOR [...], \
262 or use UPSERT USING IMAGE ON FIELD <path_field>."
263 ),
264 None,
265 )
266}
267
268pub fn cross_rerank_unsupported_error(model: &str) -> QqlError {
270 let model_note = if model.is_empty() || model.eq_ignore_ascii_case("default") {
271 "no model specified".to_string()
272 } else {
273 format!("model='{model}'")
274 };
275 QqlError::execution(
276 "QQL-RERANK-CROSS",
277 format!(
278 "cross-encoder pair scoring is not available ({model_note}). \
279 Configure a rerank host (rerank_endpoint / rerank_model, or edge \
280 reranker_model for offline TextRerank / bge-reranker)."
281 ),
282 None,
283 )
284}
285
286pub fn sparse_model_unsupported_error(model: &str) -> QqlError {
288 QqlError::execution(
289 "QQL-EMBEDDING-SPARSE",
290 format!(
291 "sparse model '{model}' is not available on this embedder. \
292 Omit the MODEL clause (or use MODEL 'default') for local \
293 wire-compatible BM25. To use model-aware sparse embedding \
294 (SPLADE / BGE-M3), configure a sparse embedding backend."
295 ),
296 None,
297 )
298}
299
300pub fn dense_model_unsupported_error(model: &str) -> QqlError {
306 QqlError::execution(
307 "QQL-EMBEDDING",
308 format!(
309 "dense model '{model}' is not available on this embedder. \
310 Omit the MODEL clause (or use MODEL 'default') to use the \
311 configured dense model. To serve multiple dense models, \
312 configure a model-routing dense embedding backend."
313 ),
314 None,
315 )
316}
317
318#[derive(Debug, Clone, Default, PartialEq)]
320pub struct JointEmbeddingOutput {
321 pub dense: Option<Vec<f32>>,
323 pub sparse: Option<SparseVector>,
325 pub multi: Option<Vec<Vec<f32>>>,
327}
328
329pub struct SparseEmbedder;
331
332impl SparseEmbedder {
333 pub fn embed_query(text: &str) -> SparseVector {
335 sparse::embed_query(text)
336 }
337
338 pub fn embed_document(text: &str) -> SparseVector {
341 sparse::embed_document(text)
342 }
343
344 pub fn embed_document_with(text: &str, params: &Bm25Params) -> SparseVector {
346 sparse::embed_document_with_params(text, params)
347 }
348}