pub mod llama;
#[cfg(feature = "native")]
pub mod native;
pub mod openai;
#[cfg(feature = "ort")]
pub mod ort;
pub mod stub;
use async_trait::async_trait;
pub use llama::LlamaServerEmbedder;
#[cfg(feature = "native")]
pub use native::NativeEmbedder;
pub use openai::OpenAiCompatEmbedder;
#[cfg(feature = "ort")]
pub use ort::OrtEmbedder;
pub use stub::StubEmbedder;
use crate::error::{RagError, Result};
pub const QWEN3_QUERY_INSTRUCTION: &str =
"Instruct: Given a web search query, retrieve relevant passages that answer the query\nQuery: ";
pub const MATRYOSHKA_512_DIM: usize = 512;
#[must_use]
pub fn format_query_or_passage(model_id: &str, text: &str, is_query: bool) -> String {
let lower = model_id.to_ascii_lowercase();
if lower.contains("qwen3") && lower.contains("embed") {
if is_query {
format!("{QWEN3_QUERY_INSTRUCTION}{text}")
} else {
text.to_string()
}
} else if lower.contains("e5") {
let prefix = if is_query { "query: " } else { "passage: " };
format!("{prefix}{text}")
} else {
text.to_string()
}
}
pub fn truncate_and_normalize_mrl(v: &mut Vec<f32>, target_dim: usize) {
if v.len() > target_dim {
v.truncate(target_dim);
}
aphrody_rag_core::fusion::l2_normalize(v);
}
#[async_trait]
pub trait Embedder: Send + Sync {
fn model_id(&self) -> &str;
fn dimension(&self) -> usize;
async fn embed(&self, texts: &[String], is_query: bool) -> Result<Vec<Vec<f32>>>;
async fn embed_query(&self, query: &str) -> Result<Vec<f32>> {
let res = self.embed(&[query.to_string()], true).await?;
res.into_iter().next().ok_or_else(|| {
RagError::EmbeddingFailed("empty embedding response for query".to_string())
})
}
async fn embed_batch(&self, texts: &[String]) -> Result<Vec<Vec<f32>>> {
self.embed(texts, false).await
}
}
#[cfg(test)]
mod tests {
use super::*;
use aphrody_rag_core::fusion::dot;
#[test]
fn test_format_query_or_passage_qwen3() {
let q = "how to run hybrid rag";
let formatted_q = format_query_or_passage("qwen3-embedding-0.6b", q, true);
assert_eq!(
formatted_q,
"Instruct: Given a web search query, retrieve relevant passages that answer the query\nQuery: how to run hybrid rag"
);
let doc = "hybrid rag combines bm25 and vector search";
let formatted_doc = format_query_or_passage("qwen3-embedding-0.6b", doc, false);
assert_eq!(formatted_doc, doc);
}
#[test]
fn test_format_query_or_passage_e5() {
let q = "search query";
assert_eq!(
format_query_or_passage("multilingual-e5-small", q, true),
"query: search query"
);
let doc = "doc body";
assert_eq!(
format_query_or_passage("multilingual-e5-small", doc, false),
"passage: doc body"
);
}
#[test]
fn test_truncate_and_normalize_mrl() {
let mut vec = vec![1.0_f32; 1024];
truncate_and_normalize_mrl(&mut vec, 512);
assert_eq!(vec.len(), 512);
let norm_sq = dot(&vec, &vec);
assert!((norm_sq - 1.0).abs() < 1e-5);
}
}