Skip to main content

ic_rig/embeddings/
builder.rs

1//! [`EmbeddingsBuilder`] — batch embedding with automatic chunking.
2//!
3//! The builder collects documents, extracts their texts via [`Embed`], chunks
4//! them to respect `MAX_DOCUMENTS`, calls the model in sequential batches,
5//! then reassembles the results back to the originating document.
6//!
7//! # Why sequential batching?
8//!
9//! Rig uses `buffer_unordered(10)` from the `futures` crate to parallelise
10//! batch requests. `irig` deliberately avoids the `futures` dependency and
11//! runs batches sequentially instead. On ICP, HTTP outcalls are already
12//! individually async — if you need parallelism at the application level,
13//! issue multiple agent/embedding calls from your canister code.
14//!
15//! # Example
16//!
17//! ```rust,ignore
18//! let results: Vec<(Article, Vec<Embedding>)> =
19//!     EmbeddingsBuilder::new(model)
20//!         .documents(articles)?
21//!         .build()
22//!         .await?;
23//!
24//! for (article, embeddings) in results {
25//!     // embeddings[0] = title vector, embeddings[1] = body vector
26//!     store.upsert(article.id, embeddings);
27//! }
28//! ```
29
30use super::{
31    Embed, EmbedError, Embedding, EmbeddingError, EmbeddingModel,
32    embed::TextEmbedder,
33};
34
35// ── Builder ───────────────────────────────────────────────────────────────────
36
37/// Accumulates documents, embeds them in efficient batches, and returns each
38/// document paired with its embedding(s).
39pub struct EmbeddingsBuilder<M, T> {
40    model: M,
41    /// Each entry is `(document, [texts_to_embed])`. One document may produce
42    /// multiple texts (and therefore multiple embeddings).
43    documents: Vec<(T, Vec<String>)>,
44}
45
46impl<M: EmbeddingModel, T: Embed> EmbeddingsBuilder<M, T> {
47    pub fn new(model: M) -> Self {
48        Self { model, documents: Vec::new() }
49    }
50
51    /// Add a single document.
52    pub fn document(mut self, doc: T) -> Result<Self, EmbedError> {
53        let mut embedder = TextEmbedder::default();
54        doc.embed(&mut embedder)?;
55        self.documents.push((doc, embedder.texts));
56        Ok(self)
57    }
58
59    /// Add multiple documents.
60    pub fn documents(self, docs: impl IntoIterator<Item = T>) -> Result<Self, EmbedError> {
61        docs.into_iter().try_fold(self, |b, doc| b.document(doc))
62    }
63
64    /// Embed all queued documents.
65    ///
66    /// Returns `Vec<(T, Vec<Embedding>)>`. Each `Vec<Embedding>` has one entry
67    /// per text the document pushed via [`Embed::embed`] — same order.
68    pub async fn build(self) -> Result<Vec<(T, Vec<Embedding>)>, EmbeddingError> {
69        // Flatten: (doc_index, text) pairs in insertion order.
70        let mut flat: Vec<(usize, String)> = Vec::new();
71        for (i, (_, texts)) in self.documents.iter().enumerate() {
72            for text in texts {
73                flat.push((i, text.clone()));
74            }
75        }
76
77        // Embed in chunks, sequentially.
78        // `embeddings_by_doc[i]` accumulates the Vec<Embedding> for document i.
79        let mut embeddings_by_doc: Vec<Vec<Embedding>> =
80            (0..self.documents.len()).map(|_| Vec::new()).collect();
81
82        for chunk in flat.chunks(M::MAX_DOCUMENTS) {
83            let (ids, texts): (Vec<usize>, Vec<String>) = chunk
84                .iter()
85                .cloned()
86                .unzip();
87
88            let batch = self
89                .model
90                .embed_texts(texts)
91                .await
92                .map_err(|e| EmbeddingError::Response(e.to_string()))?;
93
94            if batch.len() != ids.len() {
95                return Err(EmbeddingError::Response(format!(
96                    "model returned {} embeddings for {} inputs",
97                    batch.len(),
98                    ids.len(),
99                )));
100            }
101
102            for (doc_idx, embedding) in ids.into_iter().zip(batch) {
103                embeddings_by_doc[doc_idx].push(embedding);
104            }
105        }
106
107        // Pair each document with its embeddings.
108        let result = self
109            .documents
110            .into_iter()
111            .zip(embeddings_by_doc)
112            .map(|((doc, _), embeddings)| (doc, embeddings))
113            .collect();
114
115        Ok(result)
116    }
117}