1use std::{cmp::max, ops::Range};
6
7use futures::{StreamExt, stream};
8
9use crate::{
10 completion::Usage,
11 embeddings::{
12 Embed, EmbedError, Embedding, EmbeddingError, EmbeddingModel, EmbeddingResponse,
13 embed::TextEmbedder,
14 },
15};
16
17pub struct EmbeddingsBuilder<M, T>
52where
53 M: EmbeddingModel,
54 T: Embed,
55{
56 model: M,
57 documents: Vec<(T, Vec<String>)>,
58}
59
60impl<M, T> EmbeddingsBuilder<M, T>
61where
62 M: EmbeddingModel,
63 T: Embed,
64{
65 pub fn new(model: M) -> Self {
67 Self {
68 model,
69 documents: vec![],
70 }
71 }
72
73 pub fn document(mut self, document: T) -> Result<Self, EmbedError> {
75 let mut embedder = TextEmbedder::default();
76 document.embed(&mut embedder)?;
77
78 self.documents.push((document, embedder.texts));
79
80 Ok(self)
81 }
82
83 pub fn documents(self, documents: impl IntoIterator<Item = T>) -> Result<Self, EmbedError> {
86 let builder = documents
87 .into_iter()
88 .try_fold(self, |builder, doc| builder.document(doc))?;
89
90 Ok(builder)
91 }
92}
93
94impl<M, T> EmbeddingsBuilder<M, T>
95where
96 M: EmbeddingModel,
97 T: Embed + Send,
98{
99 pub async fn build(self) -> Result<Vec<(T, Vec<Embedding>)>, EmbeddingError> {
140 let (result, _usage) = self.build_with_usage().await?;
141 Ok(result)
142 }
143
144 pub async fn build_with_usage(
153 self,
154 ) -> Result<(Vec<(T, Vec<Embedding>)>, Usage), EmbeddingError> {
155 use stream::TryStreamExt;
156
157 let mut docs: Vec<T> = Vec::with_capacity(self.documents.len());
170 let mut spans: Vec<Range<usize>> = Vec::with_capacity(self.documents.len());
171 let mut texts: Vec<String> = Vec::new();
172
173 for (doc, doc_texts) in self.documents {
174 let start = texts.len();
175 texts.extend(doc_texts);
176 spans.push(start..texts.len());
177 docs.push(doc);
178 }
179
180 let total_texts = texts.len();
181
182 let (slots, usage) = stream::iter(texts.into_iter().enumerate())
184 .chunks(M::MAX_DOCUMENTS)
186 .map(|chunk| async {
188 let (slots, batch): (Vec<usize>, Vec<String>) = chunk.into_iter().unzip();
189
190 let response: EmbeddingResponse = self.model.embed_texts_with_usage(batch).await?;
191 Ok::<_, EmbeddingError>((
192 slots
193 .into_iter()
194 .zip(response.embeddings)
195 .collect::<Vec<_>>(),
196 response.usage,
197 ))
198 })
199 .buffer_unordered(max(1, 1024 / M::MAX_DOCUMENTS))
201 .try_fold(
204 (
205 (0..total_texts)
206 .map(|_| None)
207 .collect::<Vec<Option<Embedding>>>(),
208 Usage::default(),
209 ),
210 |(mut slots, mut usage_acc), (chunk_embeddings, chunk_usage)| async move {
211 for (slot, embedding) in chunk_embeddings {
212 if let Some(place) = slots.get_mut(slot) {
219 *place = Some(embedding);
220 }
221 }
222 usage_acc += chunk_usage;
223 Ok((slots, usage_acc))
224 },
225 )
226 .await?;
227
228 let mut slots = slots.into_iter();
231 let mut result = Vec::with_capacity(docs.len());
232
233 for (index, (doc, span)) in docs.into_iter().zip(spans).enumerate() {
234 if span.is_empty() {
237 return Err(crate::embeddings::EmbeddingError::ResponseError(format!(
238 "document {index} produced no text to embed, so it has no \
239 embeddings to return; an empty collection in an `#[embed]` \
240 field embeds nothing"
241 )));
242 }
243
244 let embeddings = slots
249 .by_ref()
250 .take(span.len())
251 .collect::<Option<Vec<Embedding>>>()
252 .ok_or_else(|| {
253 crate::embeddings::EmbeddingError::ResponseError(format!(
254 "provider returned fewer embeddings than texts sent: \
255 document {index} is missing at least one of its {} texts \
256 (slots {}..{} of {total_texts})",
257 span.len(),
258 span.start,
259 span.end
260 ))
261 })?;
262
263 result.push((doc, embeddings));
264 }
265
266 Ok((result, usage))
267 }
268}
269
270#[cfg(test)]
271mod tests {
272 use crate::embeddings::embed::{EmbedError, TextEmbedder};
273 use crate::embeddings::{Embed, Embedding, EmbeddingError, EmbeddingModel};
274 use crate::test_utils::{MockEmbeddingModel, MockMultiTextDocument, MockTextDocument};
275
276 use super::EmbeddingsBuilder;
277
278 fn definitions_multiple_text() -> Vec<MockMultiTextDocument> {
279 vec![
280 MockMultiTextDocument::new(
281 "doc0",
282 [
283 "A green alien that lives on cold planets.",
284 "A fictional digital currency that originated in the animated series Rick and Morty.",
285 ],
286 ),
287 MockMultiTextDocument::new(
288 "doc1",
289 [
290 "An ancient tool used by the ancestors of the inhabitants of planet Jiro to farm the land.",
291 "A fictional creature found in the distant, swampy marshlands of the planet Glibbo in the Andromeda galaxy.",
292 ],
293 ),
294 ]
295 }
296
297 fn definitions_multiple_text_2() -> Vec<MockMultiTextDocument> {
298 vec![
299 MockMultiTextDocument::new("doc2", ["Another fake definitions"]),
300 MockMultiTextDocument::new("doc3", ["Some fake definition"]),
301 ]
302 }
303
304 fn definitions_single_text() -> Vec<MockTextDocument> {
305 vec![
306 MockTextDocument::new("doc0", "A green alien that lives on cold planets."),
307 MockTextDocument::new(
308 "doc1",
309 "An ancient tool used by the ancestors of the inhabitants of planet Jiro to farm the land.",
310 ),
311 ]
312 }
313
314 #[tokio::test]
315 async fn test_build_multiple_text() {
316 let fake_definitions = definitions_multiple_text();
317
318 let fake_model = MockEmbeddingModel;
319 let result = EmbeddingsBuilder::new(fake_model)
320 .documents(fake_definitions)
321 .unwrap()
322 .build()
323 .await
324 .unwrap();
325
326 assert_eq!(result.len(), 2);
327
328 let first_definition = &result[0];
329 assert_eq!(first_definition.0.id, "doc0");
330 assert_eq!(first_definition.1.len(), 2);
331 assert_eq!(
332 first_definition.1.first().map(|e| e.document.as_str()),
333 Some("A green alien that lives on cold planets.")
334 );
335
336 let second_definition = &result[1];
337 assert_eq!(second_definition.0.id, "doc1");
338 assert_eq!(second_definition.1.len(), 2);
339 assert_eq!(
340 second_definition.1.get(1).map(|e| e.document.as_str()),
341 Some(
342 "A fictional creature found in the distant, swampy marshlands of the planet Glibbo in the Andromeda galaxy."
343 )
344 )
345 }
346
347 #[tokio::test]
348 async fn test_build_single_text() {
349 let fake_definitions = definitions_single_text();
350
351 let fake_model = MockEmbeddingModel;
352 let result = EmbeddingsBuilder::new(fake_model)
353 .documents(fake_definitions)
354 .unwrap()
355 .build()
356 .await
357 .unwrap();
358
359 assert_eq!(result.len(), 2);
360
361 let first_definition = &result[0];
362 assert_eq!(first_definition.0.id, "doc0");
363 assert_eq!(first_definition.1.len(), 1);
364 assert_eq!(
365 first_definition.1.first().map(|e| e.document.as_str()),
366 Some("A green alien that lives on cold planets.")
367 );
368
369 let second_definition = &result[1];
370 assert_eq!(second_definition.0.id, "doc1");
371 assert_eq!(second_definition.1.len(), 1);
372 assert_eq!(
373 second_definition.1.first().map(|e| e.document.as_str()),
374 Some(
375 "An ancient tool used by the ancestors of the inhabitants of planet Jiro to farm the land."
376 )
377 )
378 }
379
380 #[tokio::test]
381 async fn test_build_multiple_and_single_text() {
382 let fake_definitions = definitions_multiple_text();
383 let fake_definitions_single = definitions_multiple_text_2();
384
385 let fake_model = MockEmbeddingModel;
386 let result = EmbeddingsBuilder::new(fake_model)
387 .documents(fake_definitions)
388 .unwrap()
389 .documents(fake_definitions_single)
390 .unwrap()
391 .build()
392 .await
393 .unwrap();
394
395 assert_eq!(result.len(), 4);
396
397 let second_definition = &result[1];
398 assert_eq!(second_definition.0.id, "doc1");
399 assert_eq!(second_definition.1.len(), 2);
400 assert_eq!(
401 second_definition.1.first().map(|e| e.document.as_str()),
402 Some(
403 "An ancient tool used by the ancestors of the inhabitants of planet Jiro to farm the land."
404 )
405 );
406
407 let third_definition = &result[2];
408 assert_eq!(third_definition.0.id, "doc2");
409 assert_eq!(third_definition.1.len(), 1);
410 assert_eq!(
411 third_definition.1.first().map(|e| e.document.as_str()),
412 Some("Another fake definitions")
413 )
414 }
415
416 #[tokio::test]
417 async fn test_build_string() {
418 let bindings = definitions_multiple_text();
419 let fake_definitions = bindings.iter().map(|def| def.texts.clone());
420
421 let fake_model = MockEmbeddingModel;
422 let result = EmbeddingsBuilder::new(fake_model)
423 .documents(fake_definitions)
424 .unwrap()
425 .build()
426 .await
427 .unwrap();
428
429 assert_eq!(result.len(), 2);
430
431 let first_definition = &result[0];
432 assert_eq!(first_definition.1.len(), 2);
433 assert_eq!(
434 first_definition.1.first().map(|e| e.document.as_str()),
435 Some("A green alien that lives on cold planets.")
436 );
437
438 let second_definition = &result[1];
439 assert_eq!(second_definition.1.len(), 2);
440 assert_eq!(
441 second_definition.1.get(1).map(|e| e.document.as_str()),
442 Some(
443 "A fictional creature found in the distant, swampy marshlands of the planet Glibbo in the Andromeda galaxy."
444 )
445 )
446 }
447
448 #[tokio::test]
449 async fn test_build_preserves_input_order_across_batches() {
450 let texts: Vec<String> = (0..12).map(|i| format!("text-{i:02}")).collect();
454
455 let fake_model = MockEmbeddingModel;
456 let result = EmbeddingsBuilder::new(fake_model)
457 .documents(texts.clone())
458 .unwrap()
459 .build()
460 .await
461 .unwrap();
462
463 assert_eq!(result.len(), texts.len());
464 for (i, (doc, embeddings)) in result.into_iter().enumerate() {
465 assert_eq!(doc, texts[i]);
466 assert_eq!(embeddings.len(), 1);
467 assert_eq!(embeddings[0].document, texts[i]);
468 }
469 }
470
471 #[derive(Clone)]
477 struct SlowFirstBatchModel {
478 calls: std::sync::Arc<std::sync::atomic::AtomicUsize>,
479 }
480
481 impl SlowFirstBatchModel {
482 fn new() -> Self {
483 Self {
484 calls: std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0)),
485 }
486 }
487 }
488
489 impl EmbeddingModel for SlowFirstBatchModel {
490 const MAX_DOCUMENTS: usize = 5;
491
492 type Client = crate::client::Nothing;
493
494 fn make(_: &Self::Client, _: impl Into<String>, _: Option<usize>) -> Self {
495 Self::new()
496 }
497
498 fn ndims(&self) -> usize {
499 10
500 }
501
502 async fn embed_texts(
503 &self,
504 documents: impl IntoIterator<Item = String> + crate::wasm_compat::WasmCompatSend,
505 ) -> Result<Vec<Embedding>, EmbeddingError> {
506 let nth = self.calls.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
507 if nth == 0 {
508 tokio::time::sleep(std::time::Duration::from_millis(150)).await;
509 }
510 Ok(documents
511 .into_iter()
512 .map(|document| Embedding {
513 document,
514 vec: vec![0.0; 10],
515 })
516 .collect())
517 }
518 }
519
520 #[derive(Clone)]
527 struct DescendingLatencyModel {
528 batches: std::sync::Arc<std::sync::atomic::AtomicUsize>,
529 }
530
531 impl DescendingLatencyModel {
532 fn new() -> Self {
533 Self {
534 batches: std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0)),
535 }
536 }
537 }
538
539 impl EmbeddingModel for DescendingLatencyModel {
540 const MAX_DOCUMENTS: usize = 5;
541
542 type Client = crate::client::Nothing;
543
544 fn make(_: &Self::Client, _: impl Into<String>, _: Option<usize>) -> Self {
545 Self::new()
546 }
547
548 fn ndims(&self) -> usize {
549 10
550 }
551
552 async fn embed_texts(
553 &self,
554 documents: impl IntoIterator<Item = String> + crate::wasm_compat::WasmCompatSend,
555 ) -> Result<Vec<Embedding>, EmbeddingError> {
556 let nth = self
557 .batches
558 .fetch_add(1, std::sync::atomic::Ordering::SeqCst) as u64;
559 tokio::time::sleep(std::time::Duration::from_millis(
560 120u64.saturating_sub(nth * 40),
561 ))
562 .await;
563 Ok(documents
564 .into_iter()
565 .map(|document| Embedding {
566 document,
567 vec: vec![0.0; 10],
568 })
569 .collect())
570 }
571 }
572
573 #[derive(Debug)]
582 struct NTexts {
583 doc: usize,
584 n: usize,
585 }
586
587 impl NTexts {
588 fn new(doc: usize, n: usize) -> Self {
589 Self { doc, n }
590 }
591
592 fn expected(&self) -> Vec<String> {
594 (0..self.n).map(|i| format!("d{}t{i}", self.doc)).collect()
595 }
596 }
597
598 impl Embed for NTexts {
599 fn embed(&self, embedder: &mut TextEmbedder) -> Result<(), EmbedError> {
600 for i in 0..self.n {
601 embedder.embed(format!("d{}t{i}", self.doc));
602 }
603 Ok(())
604 }
605 }
606
607 fn returned(embeddings: &[Embedding]) -> Vec<String> {
609 embeddings
610 .iter()
611 .map(|embedding| embedding.document.clone())
612 .collect()
613 }
614
615 #[tokio::test]
622 async fn test_build_preserves_text_order_within_a_straddling_document() {
623 let doc = NTexts::new(0, 6);
624 let expected = doc.expected();
625
626 let result = EmbeddingsBuilder::new(SlowFirstBatchModel::new())
627 .document(doc)
628 .unwrap()
629 .build()
630 .await
631 .unwrap();
632
633 assert_eq!(result.len(), 1);
634 assert_eq!(returned(&result[0].1), expected);
635 }
636
637 #[tokio::test]
648 async fn test_build_preserves_text_order_across_many_straddling_documents() {
649 let docs: Vec<NTexts> = (0..4).map(|doc| NTexts::new(doc, 3)).collect();
650 let expected: Vec<Vec<String>> = docs.iter().map(NTexts::expected).collect();
651
652 let result = EmbeddingsBuilder::new(DescendingLatencyModel::new())
653 .documents(docs)
654 .unwrap()
655 .build()
656 .await
657 .unwrap();
658
659 assert_eq!(result.len(), 4);
660 for (index, (_, embeddings)) in result.iter().enumerate() {
661 assert_eq!(
662 returned(embeddings),
663 expected[index],
664 "document {index} did not get its own texts, in order"
665 );
666 }
667 }
668
669 #[tokio::test]
679 async fn test_build_rejects_a_document_that_embeds_no_text() {
680 let error = EmbeddingsBuilder::new(MockEmbeddingModel)
681 .document(NTexts::new(0, 0))
682 .unwrap()
683 .build()
684 .await
685 .expect_err("a document with no texts has no embeddings");
686
687 assert!(
688 matches!(error, EmbeddingError::ResponseError(_)),
689 "unexpected error variant: {error:?}"
690 );
691 assert!(
692 error.to_string().contains("document 0 produced no text"),
693 "error should name the offending document: {error}"
694 );
695 }
696
697 #[tokio::test]
700 async fn test_build_names_the_document_that_embeds_no_text() {
701 let error = EmbeddingsBuilder::new(MockEmbeddingModel)
702 .documents(vec![
703 NTexts::new(0, 2),
704 NTexts::new(1, 2),
705 NTexts::new(2, 0),
706 ])
707 .unwrap()
708 .build()
709 .await
710 .expect_err("a document with no texts has no embeddings");
711
712 assert!(
713 error.to_string().contains("document 2 produced no text"),
714 "error should name document 2: {error}"
715 );
716 }
717
718 #[derive(Clone, Default)]
721 struct OneAtATimeReversedLatency;
722
723 impl EmbeddingModel for OneAtATimeReversedLatency {
724 const MAX_DOCUMENTS: usize = 1;
725
726 type Client = crate::client::Nothing;
727
728 fn make(_: &Self::Client, _: impl Into<String>, _: Option<usize>) -> Self {
729 Self
730 }
731
732 fn ndims(&self) -> usize {
733 10
734 }
735
736 async fn embed_texts(
737 &self,
738 documents: impl IntoIterator<Item = String> + crate::wasm_compat::WasmCompatSend,
739 ) -> Result<Vec<Embedding>, EmbeddingError> {
740 let documents: Vec<String> = documents.into_iter().collect();
741 let position = documents
748 .first()
749 .and_then(|text| text.rsplit_once('t'))
750 .and_then(|(_, n)| n.parse::<u64>().ok());
751 assert!(
752 position.is_some(),
753 "could not read a text position out of {documents:?}; \
754 this mock cannot invert completion order without it"
755 );
756 let delay = position.map_or(0, |n| 60u64.saturating_sub(n * 10));
757 tokio::time::sleep(std::time::Duration::from_millis(delay)).await;
758 Ok(documents
759 .into_iter()
760 .map(|document| Embedding {
761 document,
762 vec: vec![0.0; 10],
763 })
764 .collect())
765 }
766 }
767
768 #[tokio::test]
772 async fn test_build_order_survives_one_text_per_batch_finishing_backwards() {
773 let doc = NTexts::new(0, 6);
774 let expected = doc.expected();
775
776 let result = EmbeddingsBuilder::new(OneAtATimeReversedLatency)
777 .document(doc)
778 .unwrap()
779 .build()
780 .await
781 .unwrap();
782
783 assert_eq!(result.len(), 1);
784 assert_eq!(returned(&result[0].1), expected);
785 }
786
787 #[tokio::test]
791 async fn test_build_order_when_documents_tile_the_batch_size_exactly() {
792 let docs: Vec<NTexts> = (0..3).map(|doc| NTexts::new(doc, 5)).collect();
795 let expected: Vec<Vec<String>> = docs.iter().map(NTexts::expected).collect();
796
797 let result = EmbeddingsBuilder::new(SlowFirstBatchModel::new())
798 .documents(docs)
799 .unwrap()
800 .build()
801 .await
802 .unwrap();
803
804 assert_eq!(result.len(), 3);
805 for (index, (_, embeddings)) in result.iter().enumerate() {
806 assert_eq!(
807 returned(embeddings),
808 expected[index],
809 "document {index} did not get its own run"
810 );
811 }
812 }
813}