Skip to main content

uni_xervo/traits/
sparse.rs

1//! Learned-sparse text embedding types and the [`SparseEmbeddingModel`] trait.
2//!
3//! Sparse embeddings represent text as a small set of weighted vocabulary terms
4//! rather than a single dense vector. They power hybrid first-stage retrieval:
5//! a learned-sparse model (SPLADE family, the BGE-M3 sparse head) keeps the
6//! inverted-index compatibility of BM25 while learning term weights — and term
7//! expansion, for SPLADE — that beat BM25 in and out of domain.
8//!
9//! This trait sits alongside [`EmbeddingModel`](crate::traits::EmbeddingModel):
10//! it is a separate, narrow capability so a provider opts into it only when it
11//! can produce term-weight vectors. The downstream inverted index and impact
12//! scoring are deliberately out of scope — this is the *producer* side only.
13
14use crate::error::Result;
15use crate::traits::{ModelInfo, TokenUsage};
16use async_trait::async_trait;
17
18/// A single learned-sparse vector as `(term_id, weight)` pairs.
19///
20/// Each `term_id` indexes into the model's term space (see
21/// [`SparseEmbeddingModel::vocab_size`]); `weight` is the non-negative learned
22/// importance of that term. Entries are sparse: only non-zero terms appear.
23/// Term ids are only comparable within the *same* model — two models with
24/// different vocabularies produce incompatible sparse vectors.
25pub type SparseVector = Vec<(u32, f32)>;
26
27/// Result of a sparse-embedding call, carrying one [`SparseVector`] per input.
28///
29/// Mirrors [`EmbedResult`](crate::traits::EmbedResult): `usage` is `Some` when a
30/// remote provider reports token counts and `None` for local providers where the
31/// concept does not apply.
32#[derive(Debug, Clone)]
33pub struct SparseEmbedResult {
34    /// One sparse vector per input, in input order.
35    pub vectors: Vec<SparseVector>,
36    /// Token usage reported by the provider, if any.
37    pub usage: Option<TokenUsage>,
38}
39
40/// A model that produces learned-sparse term-weight vectors from text.
41///
42/// Use cases include SPLADE-style retrieval and the BGE-M3 sparse head for
43/// hybrid dense + sparse search. One [`SparseVector`] is returned per input
44/// text. For scoring two sparse vectors, see
45/// [`sparse_dot`](crate::score::sparse_dot).
46#[async_trait]
47pub trait SparseEmbeddingModel: ModelInfo {
48    /// Embed a batch of text strings into learned-sparse vectors.
49    ///
50    /// Returns one [`SparseVector`] per input, in input order.
51    ///
52    /// # Errors
53    /// Returns an error if tokenization fails, the model session errors, or the
54    /// upstream API rejects the request.
55    async fn embed(&self, texts: &[&str]) -> Result<SparseEmbedResult>;
56
57    /// The size of the term space the produced `term_id`s index into.
58    ///
59    /// For SPLADE this is the model's full vocabulary (term expansion); for the
60    /// BGE-M3 lexical head it is the tokenizer vocabulary the input tokens map to.
61    fn vocab_size(&self) -> u32;
62
63    /// Optional warmup hook (e.g. load weights into memory on first access).
64    /// The default is a no-op.
65    ///
66    /// # Errors
67    /// Returns an error if the underlying model fails to initialize.
68    async fn warmup(&self) -> Result<()> {
69        Ok(())
70    }
71}
72
73#[cfg(test)]
74mod tests {
75    use super::*;
76    use std::sync::Arc;
77
78    // Dyn-safety smoke check: fails to compile if the trait becomes non-object-safe.
79    #[test]
80    fn sparse_embedding_model_is_dyn_safe() {
81        fn _accept(_: Arc<dyn SparseEmbeddingModel>) {}
82    }
83}