use crate::cost::usage::Usage;
use crate::model::{ModelId, Reach};
use crate::provider::Access;
use crate::Result;
use async_trait::async_trait;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Purpose {
Document,
Query,
}
impl Purpose {
pub fn as_str(self) -> &'static str {
match self {
Purpose::Document => "document",
Purpose::Query => "query",
}
}
}
impl std::fmt::Display for Purpose {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct EmbedRequest {
pub model: ModelId,
pub inputs: Vec<String>,
pub purpose: Option<Purpose>,
pub dimensions: Option<u32>,
}
impl EmbedRequest {
pub fn new(model: ModelId, inputs: Vec<String>) -> Self {
Self {
model,
inputs,
purpose: None,
dimensions: None,
}
}
pub fn one(model: impl Into<ModelId>, input: impl Into<String>) -> Self {
Self::new(model.into(), vec![input.into()])
}
#[must_use]
pub fn for_purpose(mut self, purpose: Purpose) -> Self {
self.purpose = Some(purpose);
self
}
#[must_use]
pub fn with_dimensions(mut self, dimensions: u32) -> Self {
self.dimensions = Some(dimensions);
self
}
pub fn len(&self) -> usize {
self.inputs.len()
}
pub fn is_empty(&self) -> bool {
self.inputs.is_empty()
}
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct Embedding {
pub model: ModelId,
pub vector: Vec<f32>,
}
impl Embedding {
pub fn new(model: ModelId, vector: Vec<f32>) -> Self {
Self { model, vector }
}
pub fn dimensions(&self) -> usize {
self.vector.len()
}
pub fn similarity(&self, other: &Embedding) -> Option<f32> {
if self.model != other.model {
return None;
}
if self.vector.len() != other.vector.len() || self.vector.is_empty() {
return None;
}
let mut dot = 0.0f32;
let mut here = 0.0f32;
let mut there = 0.0f32;
for (a, b) in self.vector.iter().zip(&other.vector) {
dot += a * b;
here += a * a;
there += b * b;
}
let magnitude = here.sqrt() * there.sqrt();
if magnitude == 0.0 {
return Some(0.0);
}
Some((dot / magnitude).clamp(-1.0, 1.0))
}
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct Embeddings {
pub vectors: Vec<Embedding>,
pub model: ModelId,
pub usage: Usage,
}
impl Embeddings {
pub fn new(vectors: Vec<Embedding>, model: ModelId, usage: Usage) -> Self {
Self {
vectors,
model,
usage,
}
}
pub fn len(&self) -> usize {
self.vectors.len()
}
pub fn is_empty(&self) -> bool {
self.vectors.is_empty()
}
pub fn get(&self, index: usize) -> Option<&Embedding> {
self.vectors.get(index)
}
pub fn dimensions(&self) -> Option<usize> {
let first = self.vectors.first()?.dimensions();
self.vectors
.iter()
.all(|v| v.dimensions() == first)
.then_some(first)
}
pub fn into_vectors(self) -> Vec<Vec<f32>> {
self.vectors.into_iter().map(|e| e.vector).collect()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct EmbeddingCapabilities {
pub dimensions: u32,
pub max_input_tokens: u32,
pub max_batch: u32,
pub resizable: bool,
pub purposes: bool,
pub reach: Reach,
}
impl EmbeddingCapabilities {
pub fn none(reach: Reach) -> Self {
Self {
dimensions: 0,
max_input_tokens: 0,
max_batch: 0,
resizable: false,
purposes: false,
reach,
}
}
#[must_use]
pub fn with_dimensions(mut self, dimensions: u32) -> Self {
self.dimensions = dimensions;
self
}
#[must_use]
pub fn with_max_input_tokens(mut self, tokens: u32) -> Self {
self.max_input_tokens = tokens;
self
}
#[must_use]
pub fn with_max_batch(mut self, inputs: u32) -> Self {
self.max_batch = inputs;
self
}
#[must_use]
pub fn resizable(mut self) -> Self {
self.resizable = true;
self
}
#[must_use]
pub fn with_purposes(mut self) -> Self {
self.purposes = true;
self
}
}
#[async_trait]
pub trait Embedder: Send + Sync {
fn id(&self) -> &str;
fn capabilities(&self, model: &ModelId) -> Option<EmbeddingCapabilities>;
async fn embed(&self, request: EmbedRequest) -> Result<Embeddings>;
async fn validate(&self, _model: &ModelId) -> Access {
Access::unknown(format!("{} has no free way to be asked", self.id()))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn vector(model: &str, numbers: &[f32]) -> Embedding {
Embedding::new(model.into(), numbers.to_vec())
}
#[test]
fn two_vectors_from_different_models_cannot_be_compared() {
let one = vector("text-embedding-3-small", &[1.0, 0.0, 0.0]);
let other = vector("embed-english-v3", &[1.0, 0.0, 0.0]);
assert_eq!(one.similarity(&other), None);
assert_eq!(
one.similarity(&vector("text-embedding-3-small", &[1.0, 0.0, 0.0])),
Some(1.0),
"the same model still compares"
);
}
#[test]
fn vectors_of_different_lengths_have_no_similarity() {
let full = vector("m", &[1.0, 0.0, 0.0]);
let shortened = vector("m", &[1.0, 0.0]);
assert_eq!(full.similarity(&shortened), None);
}
#[test]
fn an_empty_vector_compares_to_nothing() {
assert_eq!(vector("m", &[]).similarity(&vector("m", &[])), None);
}
#[test]
fn a_zero_vector_points_at_nothing_rather_than_dividing_by_nothing() {
let zero = vector("m", &[0.0, 0.0]);
assert_eq!(zero.similarity(&vector("m", &[1.0, 0.0])), Some(0.0));
assert_eq!(zero.similarity(&zero), Some(0.0));
}
#[test]
fn opposite_vectors_are_minus_one_and_perpendicular_ones_are_nought() {
let east = vector("m", &[1.0, 0.0]);
let west = vector("m", &[-1.0, 0.0]);
let north = vector("m", &[0.0, 1.0]);
assert_eq!(east.similarity(&west), Some(-1.0));
assert_eq!(east.similarity(&north), Some(0.0));
}
#[test]
fn similarity_never_leaves_the_range_rounding_could_push_it_out_of() {
let long = Embedding::new("m".into(), vec![0.1; 3_000]);
let same = long.clone();
let answer = long.similarity(&same).unwrap_or(f32::NAN);
assert!((-1.0..=1.0).contains(&answer), "{answer}");
}
#[test]
fn a_request_says_nothing_about_purpose_unless_asked_to() {
let plain = EmbedRequest::new("m".into(), vec!["a".into()]);
assert_eq!(plain.purpose, None);
assert_eq!(plain.dimensions, None);
let asked = EmbedRequest::one("m", "a")
.for_purpose(Purpose::Query)
.with_dimensions(256);
assert_eq!(asked.purpose, Some(Purpose::Query));
assert_eq!(asked.dimensions, Some(256));
assert_eq!(asked.len(), 1);
}
#[test]
fn an_empty_request_can_be_seen_before_it_reaches_a_network() {
assert!(EmbedRequest::new("m".into(), vec![]).is_empty());
assert!(!EmbedRequest::one("m", "a").is_empty());
}
#[test]
fn a_reply_reports_one_dimension_count_only_when_its_vectors_agree() {
let model: ModelId = "m".into();
let agreeing = Embeddings::new(
vec![vector("m", &[1.0, 2.0]), vector("m", &[3.0, 4.0])],
model.clone(),
Usage::absent(),
);
assert_eq!(agreeing.dimensions(), Some(2));
assert_eq!(agreeing.len(), 2);
let ragged = Embeddings::new(
vec![vector("m", &[1.0, 2.0]), vector("m", &[3.0])],
model.clone(),
Usage::absent(),
);
assert_eq!(
ragged.dimensions(),
None,
"worth finding out about rather than averaging over"
);
let nothing = Embeddings::new(vec![], model, Usage::absent());
assert_eq!(nothing.dimensions(), None);
assert!(nothing.is_empty());
}
#[test]
fn capabilities_start_at_nothing_rather_than_at_something_plausible() {
let none = EmbeddingCapabilities::none(Reach::FirstPartyApi);
assert_eq!(none.dimensions, 0);
assert_eq!(none.max_batch, 0);
assert!(!none.resizable);
assert!(!none.purposes);
let filled = EmbeddingCapabilities::none(Reach::FirstPartyApi)
.with_dimensions(1_536)
.with_max_input_tokens(8_191)
.with_max_batch(2_048)
.resizable();
assert_eq!(filled.dimensions, 1_536);
assert!(filled.resizable);
assert!(!filled.purposes, "still false unless said");
}
#[test]
fn dropping_the_model_is_something_a_reader_can_see_happening() {
let reply = Embeddings::new(
vec![vector("m", &[1.0]), vector("m", &[2.0])],
"m".into(),
Usage::absent(),
);
assert_eq!(reply.into_vectors(), vec![vec![1.0], vec![2.0]]);
}
}