mod build;
mod builder;
pub use builder::SparseTextEmbeddingBuilder;
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::SparseModel;
use crate::registry::sparse::SparseDescriptor;
use crate::sparse::SparseEmbedding;
use crate::tokenizer::TextTokenizer;
pub struct SparseTextEmbedding {
model: Box<dyn SparseModel>,
tokenizer: TextTokenizer,
descriptor: SparseDescriptor,
device: Device,
dtype: DType,
batch_size: usize,
}
impl SparseTextEmbedding {
pub fn builder(spec: impl Into<String>) -> SparseTextEmbeddingBuilder {
SparseTextEmbeddingBuilder::new(spec)
}
pub(crate) fn new(
model: Box<dyn SparseModel>,
tokenizer: TextTokenizer,
descriptor: SparseDescriptor,
device: Device,
dtype: DType,
batch_size: usize,
) -> Self {
Self {
model,
tokenizer,
descriptor,
device,
dtype,
batch_size,
}
}
pub fn descriptor(&self) -> &SparseDescriptor {
&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<SparseEmbedding>> {
self.passage_embed(texts, batch_size)
}
pub fn passage_embed<S: AsRef<str>>(
&self,
texts: &[S],
batch_size: Option<usize>,
) -> Result<Vec<SparseEmbedding>> {
self.embed_with(texts, batch_size, |model, input| model.forward(input))
}
pub fn query_embed<S: AsRef<str>>(
&self,
texts: &[S],
batch_size: Option<usize>,
) -> Result<Vec<SparseEmbedding>> {
self.embed_with(texts, batch_size, |model, input| model.forward_query(input))
}
fn embed_with<S: AsRef<str>>(
&self,
texts: &[S],
batch_size: Option<usize>,
forward: fn(&dyn SparseModel, &EncoderInput) -> Result<Vec<SparseEmbedding>>,
) -> Result<Vec<SparseEmbedding>> {
if texts.is_empty() {
return Ok(Vec::new());
}
let batch_size = batch_size.unwrap_or(self.batch_size).max(1);
let refs: Vec<&str> = texts.iter().map(|t| t.as_ref()).collect();
let lengths = self.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, forward)?);
}
Ok(batching::restore(&order, rows))
}
fn embed_chunk(
&self,
texts: &[&str],
forward: fn(&dyn SparseModel, &EncoderInput) -> Result<Vec<SparseEmbedding>>,
) -> Result<Vec<SparseEmbedding>> {
if self.tokenizer.count(texts)?.iter().all(|&count| count == 0) {
return Ok(vec![SparseEmbedding::from_pairs([]); texts.len()]);
}
let _guard = self.device.is_metal().then(|| {
METAL_FORWARD_LOCK
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
});
let batch = self.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,
};
forward(self.model.as_ref(), &input)
}
}
#[cfg(test)]
mod tests;