mod build;
mod builder;
pub use builder::MultiVectorTextEmbeddingBuilder;
use candle_core::DType;
use candle_core::Device;
use super::METAL_FORWARD_LOCK;
use super::batching;
use crate::error::Result;
use crate::models::EncoderInput;
use crate::models::MultiVectorModel;
use crate::multi_vector::MultiVectorEmbedding;
use crate::registry::multi_vector::MultiVectorDescriptor;
use crate::tokenizer::TextTokenizer;
#[derive(Clone, Copy)]
enum Role {
Query,
Document,
}
pub struct MultiVectorTextEmbedding {
model: Box<dyn MultiVectorModel>,
tokenizer: TextTokenizer,
query_tokenizer: TextTokenizer,
descriptor: MultiVectorDescriptor,
device: Device,
dtype: DType,
batch_size: usize,
}
impl MultiVectorTextEmbedding {
pub fn builder(spec: impl Into<String>) -> MultiVectorTextEmbeddingBuilder {
MultiVectorTextEmbeddingBuilder::new(spec)
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn new(
model: Box<dyn MultiVectorModel>,
tokenizer: TextTokenizer,
query_tokenizer: TextTokenizer,
descriptor: MultiVectorDescriptor,
device: Device,
dtype: DType,
batch_size: usize,
) -> Self {
Self {
model,
tokenizer,
query_tokenizer,
descriptor,
device,
dtype,
batch_size,
}
}
pub fn descriptor(&self) -> &MultiVectorDescriptor {
&self.descriptor
}
pub fn max_length(&self) -> usize {
self.tokenizer.max_length()
}
pub fn device(&self) -> &Device {
&self.device
}
pub fn dtype(&self) -> DType {
self.dtype
}
pub fn weights_dtype(&self) -> DType {
self.model.weights_dtype()
}
pub fn token_count<S: AsRef<str>>(&self, texts: &[S]) -> Result<Vec<usize>> {
let refs: Vec<&str> = texts.iter().map(|t| t.as_ref()).collect();
self.tokenizer.count(&refs)
}
pub fn embed<S: AsRef<str>>(
&self,
texts: &[S],
batch_size: Option<usize>,
) -> Result<Vec<MultiVectorEmbedding>> {
self.passage_embed(texts, batch_size)
}
pub fn passage_embed<S: AsRef<str>>(
&self,
texts: &[S],
batch_size: Option<usize>,
) -> Result<Vec<MultiVectorEmbedding>> {
self.embed_with(texts, batch_size, Role::Document)
}
pub fn query_embed<S: AsRef<str>>(
&self,
texts: &[S],
batch_size: Option<usize>,
) -> Result<Vec<MultiVectorEmbedding>> {
self.embed_with(texts, batch_size, Role::Query)
}
fn embed_with<S: AsRef<str>>(
&self,
texts: &[S],
batch_size: Option<usize>,
role: Role,
) -> Result<Vec<MultiVectorEmbedding>> {
if texts.is_empty() {
return Ok(Vec::new());
}
let batch_size = batch_size.unwrap_or(self.batch_size).max(1);
let formatted = self.formatted(role, texts);
let refs: Vec<&str> = formatted.iter().map(String::as_str).collect();
let tokenizer = self.tokenizer_for(role);
let lengths = tokenizer.count(&refs)?;
let order = batching::sorted_indices(&lengths);
let mut rows = Vec::with_capacity(texts.len());
for chunk in order.chunks(batch_size) {
let chunk_texts: Vec<&str> = chunk.iter().map(|i| refs[*i]).collect();
rows.extend(self.embed_chunk(&chunk_texts, tokenizer, role)?);
}
Ok(batching::restore(&order, rows))
}
fn formatted<S: AsRef<str>>(&self, role: Role, texts: &[S]) -> Vec<String> {
let marker = match role {
Role::Query => self.descriptor.query_marker.as_deref(),
Role::Document => self.descriptor.document_marker.as_deref(),
};
texts
.iter()
.map(|t| match marker {
Some(marker) => format!("{marker}{}", t.as_ref()),
None => t.as_ref().to_string(),
})
.collect()
}
fn tokenizer_for(&self, role: Role) -> &TextTokenizer {
match role {
Role::Query => &self.query_tokenizer,
Role::Document => &self.tokenizer,
}
}
fn embed_chunk(
&self,
texts: &[&str],
tokenizer: &TextTokenizer,
role: Role,
) -> Result<Vec<MultiVectorEmbedding>> {
if tokenizer.count(texts)?.iter().all(|&count| count == 0) {
let empty = MultiVectorEmbedding::from_rows(self.model.dim(), Vec::<Vec<f32>>::new());
return Ok(vec![empty; texts.len()]);
}
let _guard = self.device.is_metal().then(|| {
METAL_FORWARD_LOCK
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
});
let batch = tokenizer.encode(texts, &self.device)?;
let input = EncoderInput {
input_ids: &batch.input_ids,
attention_mask: &batch.attention_mask,
token_type_ids: &batch.token_type_ids,
lengths: &batch.lengths,
};
match role {
Role::Query => self.model.forward_query(&input),
Role::Document => self.model.forward(&input),
}
}
}
#[cfg(test)]
mod tests;