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 {}
8#[cfg(not(target_arch = "wasm32"))]
9impl<T: Send + Sync> EmbedderBound for T {}
10
11#[cfg(target_arch = "wasm32")]
12pub trait EmbedderBound {}
13#[cfg(target_arch = "wasm32")]
14impl<T> EmbedderBound for T {}
15
16#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
22#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
23pub trait Embedder: EmbedderBound {
24 async fn embed_dense(&self, text: &str, model: &str) -> Result<Vec<f32>, QqlError>;
25
26 async fn embed_sparse(&self, text: &str, model: &str) -> Result<SparseVector, QqlError> {
32 if !model.is_empty() && !model.eq_ignore_ascii_case("default") {
33 return Err(sparse_model_unsupported_error(model));
34 }
35 Ok(sparse::build_query_default(text))
36 }
37
38 async fn embed_sparse_batch(
40 &self,
41 texts: &[String],
42 model: &str,
43 ) -> Result<Vec<SparseVector>, QqlError> {
44 let mut results = Vec::with_capacity(texts.len());
45 for text in texts {
46 results.push(self.embed_sparse(text, model).await?);
47 }
48 Ok(results)
49 }
50
51 async fn embed_joint(&self, text: &str, model: &str) -> Result<JointEmbeddingOutput, QqlError> {
57 let dense = self.embed_dense(text, model).await?;
58 let sparse = self.embed_sparse(text, model).await?;
59 let multi = self.embed_multi(text, model).await?;
60 Ok(JointEmbeddingOutput {
61 dense: Some(dense),
62 sparse: Some(sparse),
63 multi: Some(multi),
64 })
65 }
66
67 async fn embed_joint_batch(
69 &self,
70 texts: &[String],
71 model: &str,
72 ) -> Result<Vec<JointEmbeddingOutput>, QqlError> {
73 let mut results = Vec::with_capacity(texts.len());
74 for text in texts {
75 results.push(self.embed_joint(text, model).await?);
76 }
77 Ok(results)
78 }
79
80 fn dimension(&self) -> Option<usize> {
83 None
84 }
85
86 fn multi_dimension(&self) -> Option<usize> {
88 None
89 }
90
91 fn accepts_model(&self, _model: &str) -> bool {
94 true
95 }
96
97 async fn embed_dense_batch(
100 &self,
101 texts: &[String],
102 model: &str,
103 ) -> Result<Vec<Vec<f32>>, QqlError> {
104 let mut results = Vec::with_capacity(texts.len());
105 for text in texts {
106 results.push(self.embed_dense(text, model).await?);
107 }
108 Ok(results)
109 }
110
111 async fn embed_multi(&self, text: &str, model: &str) -> Result<Vec<Vec<f32>>, QqlError> {
116 let _ = text;
117 Err(multi_unsupported_error(model))
118 }
119
120 async fn embed_multi_batch(
122 &self,
123 texts: &[String],
124 model: &str,
125 ) -> Result<Vec<Vec<Vec<f32>>>, QqlError> {
126 let mut results = Vec::with_capacity(texts.len());
127 for text in texts {
128 results.push(self.embed_multi(text, model).await?);
129 }
130 Ok(results)
131 }
132
133 async fn embed_image(&self, source: &str, model: &str) -> Result<Vec<f32>, QqlError> {
138 let _ = source;
139 Err(image_unsupported_error(model))
140 }
141
142 async fn embed_image_batch(
144 &self,
145 sources: &[String],
146 model: &str,
147 ) -> Result<Vec<Vec<f32>>, QqlError> {
148 let mut results = Vec::with_capacity(sources.len());
149 for source in sources {
150 results.push(self.embed_image(source, model).await?);
151 }
152 Ok(results)
153 }
154
155 async fn rerank_pairs(
161 &self,
162 query: &str,
163 documents: &[String],
164 model: &str,
165 ) -> Result<Vec<f32>, QqlError> {
166 let _ = (query, documents);
167 Err(cross_rerank_unsupported_error(model))
168 }
169}
170
171pub fn multi_unsupported_error(model: &str) -> QqlError {
173 let model_note = if model.is_empty() || model.eq_ignore_ascii_case("default") {
174 "no model specified".to_string()
175 } else {
176 format!("model='{model}'")
177 };
178 QqlError::execution(
179 "QQL-EMBEDDING-MULTI",
180 format!(
181 "multi-vector embedding is not available ({model_note}). \
182 Configure a multi embedder (multi_embedding_endpoint / multi_embedding_model, \
183 or edge multi_model for offline BGE-M3), pass precomputed VECTOR [[...], ...], \
184 or use UPSERT with explicit multivector bags."
185 ),
186 None,
187 )
188}
189
190pub fn image_unsupported_error(model: &str) -> QqlError {
192 let model_note = if model.is_empty() || model.eq_ignore_ascii_case("default") {
193 "no model specified".to_string()
194 } else {
195 format!("model='{model}'")
196 };
197 QqlError::execution(
198 "QQL-EMBEDDING-IMAGE",
199 format!(
200 "image embedding is not available ({model_note}). \
201 Configure an image/CLIP vision embedder (image_embedding_model / edge image_model, \
202 or image_embedding_endpoint), pass a precomputed VECTOR [...], \
203 or use UPSERT USING IMAGE ON FIELD <path_field>."
204 ),
205 None,
206 )
207}
208
209pub fn cross_rerank_unsupported_error(model: &str) -> QqlError {
211 let model_note = if model.is_empty() || model.eq_ignore_ascii_case("default") {
212 "no model specified".to_string()
213 } else {
214 format!("model='{model}'")
215 };
216 QqlError::execution(
217 "QQL-RERANK-CROSS",
218 format!(
219 "cross-encoder pair scoring is not available ({model_note}). \
220 Configure a rerank host (rerank_endpoint / rerank_model, or edge \
221 reranker_model for offline TextRerank / bge-reranker)."
222 ),
223 None,
224 )
225}
226
227pub fn sparse_model_unsupported_error(model: &str) -> QqlError {
229 QqlError::execution(
230 "QQL-EMBEDDING-SPARSE",
231 format!(
232 "sparse model '{model}' is not available on this embedder. \
233 Omit the MODEL clause (or use MODEL 'default') for local BM25-style \
234 hashing. To use model-aware sparse embedding (SPLADE / BGE-M3), \
235 configure a sparse embedding backend."
236 ),
237 None,
238 )
239}
240
241#[derive(Debug, Clone, Default, PartialEq)]
243pub struct JointEmbeddingOutput {
244 pub dense: Option<Vec<f32>>,
245 pub sparse: Option<SparseVector>,
246 pub multi: Option<Vec<Vec<f32>>>,
247}
248
249pub struct SparseEmbedder;
251
252impl SparseEmbedder {
253 pub fn embed_sparse(text: &str) -> SparseVector {
255 sparse::build_query_default(text)
256 }
257}