use std::time::Instant; use arroy::Distance; use super::error::CompositeEmbedderContainsHuggingFace; use super::{ hf, manual, ollama, openai, rest, DistributionShift, EmbedError, Embedding, NewEmbedderError, }; use crate::ThreadPoolNoAbort; #[derive(Debug)] pub enum SubEmbedder { /// An embedder based on running local models, fetched from the Hugging Face Hub. HuggingFace(hf::Embedder), /// An embedder based on making embedding queries against the OpenAI API. OpenAi(openai::Embedder), /// An embedder based on the user providing the embeddings in the documents and queries. UserProvided(manual::Embedder), /// An embedder based on making embedding queries against an embedding server. Ollama(ollama::Embedder), /// An embedder based on making embedding queries against a generic JSON/REST embedding server. Rest(rest::Embedder), } #[derive(Debug, Clone, Hash, PartialEq, Eq, serde::Deserialize, serde::Serialize)] pub enum SubEmbedderOptions { HuggingFace(hf::EmbedderOptions), OpenAi(openai::EmbedderOptions), Ollama(ollama::EmbedderOptions), UserProvided(manual::EmbedderOptions), Rest(rest::EmbedderOptions), } impl SubEmbedderOptions { pub fn distribution(&self) -> Option { match self { SubEmbedderOptions::HuggingFace(embedder_options) => embedder_options.distribution, SubEmbedderOptions::OpenAi(embedder_options) => embedder_options.distribution, SubEmbedderOptions::Ollama(embedder_options) => embedder_options.distribution, SubEmbedderOptions::UserProvided(embedder_options) => embedder_options.distribution, SubEmbedderOptions::Rest(embedder_options) => embedder_options.distribution, } } } #[derive(Debug)] pub struct Embedder { pub(super) search: SubEmbedder, pub(super) index: SubEmbedder, } #[derive(Debug, Clone, Hash, PartialEq, Eq, serde::Deserialize, serde::Serialize)] pub struct EmbedderOptions { pub search: SubEmbedderOptions, pub index: SubEmbedderOptions, } impl Embedder { pub fn new( EmbedderOptions { search, index }: EmbedderOptions, ) -> Result { let search = SubEmbedder::new(search)?; let index = SubEmbedder::new(index)?; // check dimensions if search.dimensions() != index.dimensions() { return Err(NewEmbedderError::composite_dimensions_mismatch( search.dimensions(), index.dimensions(), )); } // check similarity let search_embeddings = search .embed( vec![ "test".into(), "a brave dog".into(), "This is a sample text. It is meant to compare similarity.".into(), ], None, ) .map_err(|error| NewEmbedderError::composite_test_embedding_failed(error, "search"))?; let index_embeddings = index .embed( vec![ "test".into(), "a brave dog".into(), "This is a sample text. It is meant to compare similarity.".into(), ], None, ) .map_err(|error| { NewEmbedderError::composite_test_embedding_failed(error, "indexing") })?; let hint = configuration_hint(&search, &index); check_similarity(search_embeddings, index_embeddings, hint)?; Ok(Self { search, index }) } /// Indicates the dimensions of a single embedding produced by the embedder. pub fn dimensions(&self) -> usize { // can use the dimensions of any embedder since they should match self.index.dimensions() } /// An optional distribution used to apply an affine transformation to the similarity score of a document. pub fn distribution(&self) -> Option { // 3 cases here: // 1. distribution provided by user => use that one, which was stored in search // 2. no user-provided distribution, distribution in search embedder => use that one // 2. no user-provided distribution, no distribution in search embedder => use the distribution in indexing embedder self.search.distribution().or_else(|| self.index.distribution()) } } impl SubEmbedder { pub fn new(options: SubEmbedderOptions) -> std::result::Result { Ok(match options { SubEmbedderOptions::HuggingFace(options) => { Self::HuggingFace(hf::Embedder::new(options)?) } SubEmbedderOptions::OpenAi(options) => Self::OpenAi(openai::Embedder::new(options)?), SubEmbedderOptions::Ollama(options) => Self::Ollama(ollama::Embedder::new(options)?), SubEmbedderOptions::UserProvided(options) => { Self::UserProvided(manual::Embedder::new(options)) } SubEmbedderOptions::Rest(options) => { Self::Rest(rest::Embedder::new(options, rest::ConfigurationSource::User)?) } }) } pub fn embed( &self, texts: Vec, deadline: Option, ) -> std::result::Result, EmbedError> { match self { SubEmbedder::HuggingFace(embedder) => embedder.embed(texts), SubEmbedder::OpenAi(embedder) => embedder.embed(&texts, deadline), SubEmbedder::Ollama(embedder) => embedder.embed(&texts, deadline), SubEmbedder::UserProvided(embedder) => embedder.embed(&texts), SubEmbedder::Rest(embedder) => embedder.embed(texts, deadline), } } /// Embed multiple chunks of texts. /// /// Each chunk is composed of one or multiple texts. pub fn embed_index( &self, text_chunks: Vec>, threads: &ThreadPoolNoAbort, ) -> std::result::Result>, EmbedError> { match self { SubEmbedder::HuggingFace(embedder) => embedder.embed_index(text_chunks), SubEmbedder::OpenAi(embedder) => embedder.embed_index(text_chunks, threads), SubEmbedder::Ollama(embedder) => embedder.embed_index(text_chunks, threads), SubEmbedder::UserProvided(embedder) => embedder.embed_index(text_chunks), SubEmbedder::Rest(embedder) => embedder.embed_index(text_chunks, threads), } } /// Non-owning variant of [`Self::embed_index`]. pub fn embed_index_ref( &self, texts: &[&str], threads: &ThreadPoolNoAbort, ) -> std::result::Result, EmbedError> { match self { SubEmbedder::HuggingFace(embedder) => embedder.embed_index_ref(texts), SubEmbedder::OpenAi(embedder) => embedder.embed_index_ref(texts, threads), SubEmbedder::Ollama(embedder) => embedder.embed_index_ref(texts, threads), SubEmbedder::UserProvided(embedder) => embedder.embed_index_ref(texts), SubEmbedder::Rest(embedder) => embedder.embed_index_ref(texts, threads), } } /// Indicates the preferred number of chunks to pass to [`Self::embed_chunks`] pub fn chunk_count_hint(&self) -> usize { match self { SubEmbedder::HuggingFace(embedder) => embedder.chunk_count_hint(), SubEmbedder::OpenAi(embedder) => embedder.chunk_count_hint(), SubEmbedder::Ollama(embedder) => embedder.chunk_count_hint(), SubEmbedder::UserProvided(_) => 100, SubEmbedder::Rest(embedder) => embedder.chunk_count_hint(), } } /// Indicates the preferred number of texts in a single chunk passed to [`Self::embed`] pub fn prompt_count_in_chunk_hint(&self) -> usize { match self { SubEmbedder::HuggingFace(embedder) => embedder.prompt_count_in_chunk_hint(), SubEmbedder::OpenAi(embedder) => embedder.prompt_count_in_chunk_hint(), SubEmbedder::Ollama(embedder) => embedder.prompt_count_in_chunk_hint(), SubEmbedder::UserProvided(_) => 1, SubEmbedder::Rest(embedder) => embedder.prompt_count_in_chunk_hint(), } } pub fn uses_document_template(&self) -> bool { match self { SubEmbedder::HuggingFace(_) | SubEmbedder::OpenAi(_) | SubEmbedder::Ollama(_) | SubEmbedder::Rest(_) => true, SubEmbedder::UserProvided(_) => false, } } /// Indicates the dimensions of a single embedding produced by the embedder. pub fn dimensions(&self) -> usize { match self { SubEmbedder::HuggingFace(embedder) => embedder.dimensions(), SubEmbedder::OpenAi(embedder) => embedder.dimensions(), SubEmbedder::Ollama(embedder) => embedder.dimensions(), SubEmbedder::UserProvided(embedder) => embedder.dimensions(), SubEmbedder::Rest(embedder) => embedder.dimensions(), } } /// An optional distribution used to apply an affine transformation to the similarity score of a document. pub fn distribution(&self) -> Option { match self { SubEmbedder::HuggingFace(embedder) => embedder.distribution(), SubEmbedder::OpenAi(embedder) => embedder.distribution(), SubEmbedder::Ollama(embedder) => embedder.distribution(), SubEmbedder::UserProvided(embedder) => embedder.distribution(), SubEmbedder::Rest(embedder) => embedder.distribution(), } } } fn check_similarity( left: Vec, right: Vec, hint: CompositeEmbedderContainsHuggingFace, ) -> Result<(), NewEmbedderError> { if left.len() != right.len() { return Err(NewEmbedderError::composite_embedding_count_mismatch(left.len(), right.len())); } for (left, right) in left.into_iter().zip(right) { let left = arroy::internals::UnalignedVector::from_slice(&left); let right = arroy::internals::UnalignedVector::from_slice(&right); let left = arroy::internals::Leaf { header: arroy::distances::Cosine::new_header(&left), vector: left, }; let right = arroy::internals::Leaf { header: arroy::distances::Cosine::new_header(&right), vector: right, }; let distance = arroy::distances::Cosine::built_distance(&left, &right); if distance > super::MAX_COMPOSITE_DISTANCE { return Err(NewEmbedderError::composite_embedding_value_mismatch(distance, hint)); } } Ok(()) } fn configuration_hint( search: &SubEmbedder, index: &SubEmbedder, ) -> CompositeEmbedderContainsHuggingFace { match (search, index) { (SubEmbedder::HuggingFace(_), SubEmbedder::HuggingFace(_)) => { CompositeEmbedderContainsHuggingFace::Both } (SubEmbedder::HuggingFace(_), _) => CompositeEmbedderContainsHuggingFace::Search, (_, SubEmbedder::HuggingFace(_)) => CompositeEmbedderContainsHuggingFace::Indexing, _ => CompositeEmbedderContainsHuggingFace::None, } }