#[cfg(feature = "native")]
mod cached;
#[cfg(feature = "native")]
mod native;
#[cfg(test)]
mod tests;
use crate::error::{EmbedError, Result};
use crate::model::{EmbeddingModel, ModelConfig};
use async_trait::async_trait;
#[cfg(test)]
std::thread_local! {
static VALIDATE_TEXTS_CALLS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
}
#[cfg(feature = "native")]
pub use cached::CachedEmbeddingService;
#[cfg(feature = "native")]
pub use native::NativeEmbeddingService;
pub const DEFAULT_MAX_BATCH_SIZE: usize = 1000;
pub const MAX_TEXT_BYTES: usize = 32768;
#[deprecated(
since = "0.7.0",
note = "use MAX_TEXT_BYTES; this limit counts UTF-8 bytes, not chars"
)]
pub const MAX_TEXT_CHARS: usize = MAX_TEXT_BYTES;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum EmbeddingRole {
Query,
Passage,
Generic,
}
impl EmbeddingRole {
#[inline]
pub(crate) const fn cache_tag(self) -> &'static str {
match self {
EmbeddingRole::Query => "role:query",
EmbeddingRole::Passage => "role:passage",
EmbeddingRole::Generic => "role:generic",
}
}
#[inline]
pub(crate) const fn instruction(self, model: EmbeddingModel) -> Option<&'static str> {
match self {
EmbeddingRole::Query => model.query_instruction(),
EmbeddingRole::Passage => model.document_instruction(),
EmbeddingRole::Generic => None,
}
}
}
pub(crate) fn validate_texts(texts: &[String]) -> Result<()> {
#[cfg(test)]
VALIDATE_TEXTS_CALLS.set(VALIDATE_TEXTS_CALLS.get() + 1);
validate_texts_bounded(texts, MAX_TEXT_BYTES)
}
#[cfg(test)]
pub(crate) fn reset_validate_texts_calls() {
VALIDATE_TEXTS_CALLS.set(0);
}
#[cfg(test)]
pub(crate) fn validate_texts_calls() -> usize {
VALIDATE_TEXTS_CALLS.get()
}
pub(crate) fn validate_texts_bounded<T: AsRef<str>>(texts: &[T], max_bytes: usize) -> Result<()> {
if texts.is_empty() {
return Err(EmbedError::InvalidInput("no texts provided".into()));
}
if texts.len() > DEFAULT_MAX_BATCH_SIZE {
return Err(EmbedError::InvalidInput(format!(
"batch size {} exceeds maximum {}",
texts.len(),
DEFAULT_MAX_BATCH_SIZE
)));
}
for text in texts {
let text = text.as_ref();
if text.len() > max_bytes {
return Err(EmbedError::TextTooLong {
length: text.len(),
max: max_bytes,
});
}
}
Ok(())
}
enum ValidatedTextBatchInner<'a> {
Contiguous(&'a [String]),
Borrowed(&'a [&'a str]),
}
#[doc(hidden)]
pub struct ValidatedTextBatch<'a> {
inner: ValidatedTextBatchInner<'a>,
}
impl<'a> ValidatedTextBatch<'a> {
pub(in crate::service) fn new(texts: &'a [String]) -> Result<Self> {
validate_texts(texts)?;
Ok(Self {
inner: ValidatedTextBatchInner::Contiguous(texts),
})
}
pub(in crate::service) fn borrowed_subset<'b>(
&'b self,
texts: &'b [&'b str],
) -> ValidatedTextBatch<'b> {
ValidatedTextBatch {
inner: ValidatedTextBatchInner::Borrowed(texts),
}
}
pub(in crate::service) fn len(&self) -> usize {
match self.inner {
ValidatedTextBatchInner::Contiguous(texts) => texts.len(),
ValidatedTextBatchInner::Borrowed(texts) => texts.len(),
}
}
pub(in crate::service) fn get(&self, index: usize) -> &str {
match self.inner {
ValidatedTextBatchInner::Contiguous(texts) => texts[index].as_str(),
ValidatedTextBatchInner::Borrowed(texts) => texts[index],
}
}
fn contiguous(&self) -> Option<&'a [String]> {
match self.inner {
ValidatedTextBatchInner::Contiguous(texts) => Some(texts),
ValidatedTextBatchInner::Borrowed(_) => None,
}
}
pub(in crate::service) fn borrowed(&self) -> Option<&'a [&'a str]> {
match self.inner {
ValidatedTextBatchInner::Contiguous(_) => None,
ValidatedTextBatchInner::Borrowed(texts) => Some(texts),
}
}
pub(in crate::service) fn to_owned_with_prefix(&self, prefix: Option<&str>) -> Vec<String> {
(0..self.len())
.map(|index| {
let text = self.get(index);
match prefix {
None => text.to_owned(),
Some(prefix) => {
let mut prepared = String::with_capacity(prefix.len() + text.len());
prepared.push_str(prefix);
prepared.push_str(text);
prepared
}
}
})
.collect()
}
}
#[async_trait]
pub trait EmbeddingService: Send + Sync {
async fn embed(&self, texts: &[String], model: EmbeddingModel) -> Result<Vec<Vec<f32>>>;
async fn embed_one(&self, text: &str, model: EmbeddingModel) -> Result<Vec<f32>> {
let texts = vec![text.to_string()];
let mut embeddings = self.embed(&texts, model).await?;
embeddings
.pop()
.ok_or_else(|| EmbedError::Internal("no embedding generated".into()))
}
async fn embed_with_role(
&self,
texts: &[String],
model: EmbeddingModel,
role: EmbeddingRole,
) -> Result<Vec<Vec<f32>>> {
validate_texts(texts)?;
let prepared = apply_prefix(texts, role.instruction(model));
self.embed(&prepared, model).await
}
#[doc(hidden)]
async fn embed_with_role_prevalidated(
&self,
texts: ValidatedTextBatch<'_>,
model: EmbeddingModel,
role: EmbeddingRole,
) -> Result<Vec<Vec<f32>>> {
if let Some(contiguous) = texts.contiguous() {
return self.embed_with_role(contiguous, model, role).await;
}
let owned = texts.to_owned_with_prefix(None);
self.embed_with_role(&owned, model, role).await
}
async fn embed_query(&self, texts: &[String], model: EmbeddingModel) -> Result<Vec<Vec<f32>>> {
self.embed_with_role(texts, model, EmbeddingRole::Query)
.await
}
async fn embed_passage(
&self,
texts: &[String],
model: EmbeddingModel,
) -> Result<Vec<Vec<f32>>> {
self.embed_with_role(texts, model, EmbeddingRole::Passage)
.await
}
fn model_config(&self, model: EmbeddingModel) -> ModelConfig {
ModelConfig::new(model)
}
fn supports_model(&self, model: EmbeddingModel) -> bool;
fn name(&self) -> &'static str;
}
pub(crate) fn apply_prefix(texts: &[String], prefix: Option<&str>) -> Vec<String> {
match prefix {
None => texts.to_vec(),
Some(p) => texts.iter().map(|t| format!("{p}{t}")).collect(),
}
}