use crate::error::{RragError, RragResult};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Embedding {
pub vector: Vec<f32>,
pub dimensions: usize,
pub model: String,
}
impl Embedding {
pub fn new(vector: Vec<f32>, model: impl Into<String>) -> Self {
let dimensions = vector.len();
Self {
vector,
dimensions,
model: model.into(),
}
}
pub fn cosine_similarity(&self, other: &Embedding) -> RragResult<f32> {
if self.dimensions != other.dimensions {
return Err(RragError::validation(
"embedding_dimensions",
"dimensions must match",
format!("{} vs {}", self.dimensions, other.dimensions),
));
}
let dot_product: f32 = self
.vector
.iter()
.zip(other.vector.iter())
.map(|(a, b)| a * b)
.sum();
let norm_a: f32 = self.vector.iter().map(|x| x * x).sum::<f32>().sqrt();
let norm_b: f32 = other.vector.iter().map(|x| x * x).sum::<f32>().sqrt();
if norm_a == 0.0 || norm_b == 0.0 {
return Ok(0.0);
}
Ok(dot_product / (norm_a * norm_b))
}
pub fn euclidean_distance(&self, other: &Embedding) -> RragResult<f32> {
if self.dimensions != other.dimensions {
return Err(RragError::validation(
"embedding_dimensions",
"dimensions must match",
format!("{} vs {}", self.dimensions, other.dimensions),
));
}
let sum_of_squares: f32 = self
.vector
.iter()
.zip(other.vector.iter())
.map(|(a, b)| (a - b).powi(2))
.sum();
Ok(sum_of_squares.sqrt())
}
}
#[async_trait::async_trait]
pub trait EmbeddingProvider: Send + Sync {
async fn embed(&self, text: &str) -> RragResult<Embedding>;
fn model_name(&self) -> &str;
fn dimensions(&self) -> usize;
}
pub struct HashEmbeddingProvider {
dimensions: usize,
}
impl HashEmbeddingProvider {
pub fn new(dimensions: usize) -> Self {
Self { dimensions }
}
fn hash_embed(&self, text: &str) -> Vec<f32> {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let mut vector = vec![0.0; self.dimensions];
for i in 0..self.dimensions {
let mut hasher = DefaultHasher::new();
text.hash(&mut hasher);
i.hash(&mut hasher);
let hash = hasher.finish();
vector[i] = ((hash as f32) / (u64::MAX as f32)) * 2.0 - 1.0;
}
vector
}
}
#[async_trait::async_trait]
impl EmbeddingProvider for HashEmbeddingProvider {
async fn embed(&self, text: &str) -> RragResult<Embedding> {
let vector = self.hash_embed(text);
Ok(Embedding::new(vector, "hash"))
}
fn model_name(&self) -> &str {
"hash-embedding"
}
fn dimensions(&self) -> usize {
self.dimensions
}
}
#[derive(Debug, Clone)]
pub struct SearchResult<T> {
pub item: T,
pub score: f32,
pub distance: Option<f32>,
}
impl<T> SearchResult<T> {
pub fn new(item: T, score: f32) -> Self {
Self {
item,
score,
distance: None,
}
}
pub fn with_distance(mut self, distance: f32) -> Self {
self.distance = Some(distance);
self
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_embedding_cosine_similarity() {
let emb1 = Embedding::new(vec![1.0, 0.0, 0.0], "test");
let emb2 = Embedding::new(vec![1.0, 0.0, 0.0], "test");
let emb3 = Embedding::new(vec![0.0, 1.0, 0.0], "test");
let sim = emb1.cosine_similarity(&emb2).unwrap();
assert!((sim - 1.0).abs() < 1e-6);
let sim = emb1.cosine_similarity(&emb3).unwrap();
assert!(sim.abs() < 1e-6);
}
#[test]
fn test_embedding_euclidean_distance() {
let emb1 = Embedding::new(vec![1.0, 0.0, 0.0], "test");
let emb2 = Embedding::new(vec![1.0, 0.0, 0.0], "test");
let emb3 = Embedding::new(vec![0.0, 1.0, 0.0], "test");
let dist = emb1.euclidean_distance(&emb2).unwrap();
assert!(dist.abs() < 1e-6);
let dist = emb1.euclidean_distance(&emb3).unwrap();
assert!((dist - 1.41421356).abs() < 1e-5);
}
#[tokio::test]
async fn test_hash_embedding_provider() {
let provider = HashEmbeddingProvider::new(128);
let emb1 = provider.embed("Hello world").await.unwrap();
let emb2 = provider.embed("Hello world").await.unwrap();
let emb3 = provider.embed("Different text").await.unwrap();
assert_eq!(emb1.dimensions, 128);
let sim = emb1.cosine_similarity(&emb2).unwrap();
assert!((sim - 1.0).abs() < 1e-6);
let sim = emb1.cosine_similarity(&emb3).unwrap();
assert!(sim < 1.0);
}
}