use async_trait::async_trait;
use qql_core::error::QqlError;
use crate::sparse::{self, Bm25Params, SparseVector};
#[cfg(not(target_arch = "wasm32"))]
pub trait EmbedderBound: Send + Sync {}
#[cfg(not(target_arch = "wasm32"))]
impl<T: Send + Sync> EmbedderBound for T {}
#[cfg(target_arch = "wasm32")]
pub trait EmbedderBound {}
#[cfg(target_arch = "wasm32")]
impl<T> EmbedderBound for T {}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
pub trait Embedder: EmbedderBound {
async fn embed_dense(&self, text: &str, model: &str) -> Result<Vec<f32>, QqlError>;
async fn embed_sparse_query(&self, text: &str, model: &str) -> Result<SparseVector, QqlError> {
if !model.is_empty() && !model.eq_ignore_ascii_case("default") {
return Err(sparse_model_unsupported_error(model));
}
self.bm25_text_config().pipeline().embed_query(text)
}
async fn embed_sparse_document(
&self,
text: &str,
model: &str,
) -> Result<SparseVector, QqlError> {
if !model.is_empty() && !model.eq_ignore_ascii_case("default") {
return Err(sparse_model_unsupported_error(model));
}
self.bm25_text_config().pipeline().embed_document(text)
}
async fn embed_sparse_document_batch(
&self,
texts: &[String],
model: &str,
) -> Result<Vec<SparseVector>, QqlError> {
let mut results = Vec::with_capacity(texts.len());
for text in texts {
results.push(self.embed_sparse_document(text, model).await?);
}
Ok(results)
}
async fn embed_sparse_query_batch(
&self,
texts: &[String],
model: &str,
) -> Result<Vec<SparseVector>, QqlError> {
let mut results = Vec::with_capacity(texts.len());
for text in texts {
results.push(self.embed_sparse_query(text, model).await?);
}
Ok(results)
}
async fn embed_joint(&self, text: &str, model: &str) -> Result<JointEmbeddingOutput, QqlError> {
let dense = self.embed_dense(text, model).await?;
let sparse = self.embed_sparse_document(text, model).await?;
let multi = self.embed_multi(text, model).await?;
Ok(JointEmbeddingOutput {
dense: Some(dense),
sparse: Some(sparse),
multi: Some(multi),
})
}
async fn embed_joint_batch(
&self,
texts: &[String],
model: &str,
) -> Result<Vec<JointEmbeddingOutput>, QqlError> {
let mut results = Vec::with_capacity(texts.len());
for text in texts {
results.push(self.embed_joint(text, model).await?);
}
Ok(results)
}
fn dimension(&self) -> Option<usize> {
None
}
fn bm25_params(&self) -> Bm25Params {
Bm25Params::default()
}
fn bm25_text_config(&self) -> crate::Bm25TextConfig {
crate::Bm25TextConfig {
params: self.bm25_params(),
..crate::Bm25TextConfig::default()
}
}
fn multi_dimension(&self) -> Option<usize> {
None
}
fn accepts_model(&self, _model: &str) -> bool {
true
}
async fn embed_dense_batch(
&self,
texts: &[String],
model: &str,
) -> Result<Vec<Vec<f32>>, QqlError> {
let mut results = Vec::with_capacity(texts.len());
for text in texts {
results.push(self.embed_dense(text, model).await?);
}
Ok(results)
}
async fn embed_multi(&self, text: &str, model: &str) -> Result<Vec<Vec<f32>>, QqlError> {
let _ = text;
Err(multi_unsupported_error(model))
}
async fn embed_multi_batch(
&self,
texts: &[String],
model: &str,
) -> Result<Vec<Vec<Vec<f32>>>, QqlError> {
let mut results = Vec::with_capacity(texts.len());
for text in texts {
results.push(self.embed_multi(text, model).await?);
}
Ok(results)
}
async fn embed_image(&self, source: &str, model: &str) -> Result<Vec<f32>, QqlError> {
let _ = source;
Err(image_unsupported_error(model))
}
async fn embed_image_batch(
&self,
sources: &[String],
model: &str,
) -> Result<Vec<Vec<f32>>, QqlError> {
let mut results = Vec::with_capacity(sources.len());
for source in sources {
results.push(self.embed_image(source, model).await?);
}
Ok(results)
}
async fn rerank_pairs(
&self,
query: &str,
documents: &[String],
model: &str,
) -> Result<Vec<f32>, QqlError> {
let _ = (query, documents);
Err(cross_rerank_unsupported_error(model))
}
}
pub fn multi_unsupported_error(model: &str) -> QqlError {
let model_note = if model.is_empty() || model.eq_ignore_ascii_case("default") {
"no model specified".to_string()
} else {
format!("model='{model}'")
};
QqlError::execution(
"QQL-EMBEDDING-MULTI",
format!(
"multi-vector embedding is not available ({model_note}). \
Configure a multi embedder (multi_embedding_endpoint / multi_embedding_model, \
or edge multi_model for offline BGE-M3), pass precomputed VECTOR [[...], ...], \
or use UPSERT with explicit multivector bags."
),
None,
)
}
pub fn image_unsupported_error(model: &str) -> QqlError {
let model_note = if model.is_empty() || model.eq_ignore_ascii_case("default") {
"no model specified".to_string()
} else {
format!("model='{model}'")
};
QqlError::execution(
"QQL-EMBEDDING-IMAGE",
format!(
"image embedding is not available ({model_note}). \
Configure an image/CLIP vision embedder (image_embedding_model / edge image_model, \
or image_embedding_endpoint), pass a precomputed VECTOR [...], \
or use UPSERT USING IMAGE ON FIELD <path_field>."
),
None,
)
}
pub fn cross_rerank_unsupported_error(model: &str) -> QqlError {
let model_note = if model.is_empty() || model.eq_ignore_ascii_case("default") {
"no model specified".to_string()
} else {
format!("model='{model}'")
};
QqlError::execution(
"QQL-RERANK-CROSS",
format!(
"cross-encoder pair scoring is not available ({model_note}). \
Configure a rerank host (rerank_endpoint / rerank_model, or edge \
reranker_model for offline TextRerank / bge-reranker)."
),
None,
)
}
pub fn sparse_model_unsupported_error(model: &str) -> QqlError {
QqlError::execution(
"QQL-EMBEDDING-SPARSE",
format!(
"sparse model '{model}' is not available on this embedder. \
Omit the MODEL clause (or use MODEL 'default') for local \
wire-compatible BM25. To use model-aware sparse embedding \
(SPLADE / BGE-M3), configure a sparse embedding backend."
),
None,
)
}
pub fn dense_model_unsupported_error(model: &str) -> QqlError {
QqlError::execution(
"QQL-EMBEDDING",
format!(
"dense model '{model}' is not available on this embedder. \
Omit the MODEL clause (or use MODEL 'default') to use the \
configured dense model. To serve multiple dense models, \
configure a model-routing dense embedding backend."
),
None,
)
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct JointEmbeddingOutput {
pub dense: Option<Vec<f32>>,
pub sparse: Option<SparseVector>,
pub multi: Option<Vec<Vec<f32>>>,
}
pub struct SparseEmbedder;
impl SparseEmbedder {
pub fn embed_query(text: &str) -> SparseVector {
sparse::embed_query(text)
}
pub fn embed_document(text: &str) -> SparseVector {
sparse::embed_document(text)
}
pub fn embed_document_with(text: &str, params: &Bm25Params) -> SparseVector {
sparse::embed_document_with_params(text, params)
}
}