use async_trait::async_trait;
use super::{Embeddings, EmbeddingError};
pub struct LocalEmbeddings {
dim: usize,
}
impl LocalEmbeddings {
pub fn new(dim: usize) -> Self {
Self {
dim: dim.max(1),
}
}
pub fn default_dim() -> Self {
Self::new(256)
}
fn tokenize(text: &str) -> Vec<String> {
let mut tokens = Vec::new();
let mut current = String::new();
for c in text.chars() {
if c.is_alphanumeric() {
if c.is_ascii() {
current.push(c.to_ascii_lowercase());
} else {
if !current.is_empty() {
tokens.push(std::mem::take(&mut current));
}
tokens.push(c.to_string());
}
} else if !current.is_empty() {
tokens.push(std::mem::take(&mut current));
}
}
if !current.is_empty() {
tokens.push(current);
}
tokens
}
fn hash(s: &str) -> u64 {
let mut h: u64 = 0xcbf29ce484222325;
for b in s.bytes() {
h ^= b as u64;
h = h.wrapping_mul(0x100000001b3);
}
h
}
fn embed(&self, text: &str) -> Vec<f32> {
let mut v = vec![0.0f32; self.dim];
for token in Self::tokenize(text) {
let idx = (Self::hash(&token) as usize) % self.dim;
v[idx] += 1.0;
}
let norm: f32 = v.iter().map(|x| x * x).sum::<f32>().sqrt();
if norm > 0.0 {
for x in &mut v {
*x /= norm;
}
}
v
}
}
impl Default for LocalEmbeddings {
fn default() -> Self {
Self::default_dim()
}
}
#[async_trait]
impl Embeddings for LocalEmbeddings {
async fn embed_query(&self, text: &str) -> Result<Vec<f32>, EmbeddingError> {
Ok(self.embed(text))
}
fn dimension(&self) -> usize {
self.dim
}
fn model_name(&self) -> &str {
"local-bow"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::embeddings::cosine_similarity;
#[tokio::test]
async fn test_dimension() {
let e = LocalEmbeddings::new(128);
let v = e.embed_query("hello world").await.unwrap();
assert_eq!(v.len(), 128);
assert_eq!(e.dimension(), 128);
}
#[tokio::test]
async fn test_same_text_same_vector() {
let e = LocalEmbeddings::new(64);
let a = e.embed_query("rust programming").await.unwrap();
let b = e.embed_query("rust programming").await.unwrap();
assert_eq!(a, b);
}
#[tokio::test]
async fn test_different_text_different_vector() {
let e = LocalEmbeddings::new(64);
let a = e.embed_query("rust programming").await.unwrap();
let b = e.embed_query("cooking recipe pasta").await.unwrap();
assert_ne!(a, b);
}
#[tokio::test]
async fn test_shared_words_more_similar() {
let e = LocalEmbeddings::new(256);
let base = e.embed_query("rust programming language").await.unwrap();
let similar = e.embed_query("rust programming tutorial").await.unwrap();
let different = e.embed_query("cooking pasta recipe").await.unwrap();
let sim_similar = cosine_similarity(&base, &similar);
let sim_different = cosine_similarity(&base, &different);
assert!(
sim_similar > sim_different,
"共享词应更相似: {} vs {}",
sim_similar,
sim_different
);
}
#[tokio::test]
async fn test_normalized() {
let e = LocalEmbeddings::new(64);
let v = e.embed_query("some text here").await.unwrap();
let norm: f32 = v.iter().map(|x| x * x).sum::<f32>().sqrt();
assert!((norm - 1.0).abs() < 1e-5, "norm = {}", norm);
}
#[tokio::test]
async fn test_empty_text_zero_vector() {
let e = LocalEmbeddings::new(64);
let v = e.embed_query("").await.unwrap();
assert!(v.iter().all(|x| *x == 0.0));
}
#[tokio::test]
async fn test_chinese_tokenize() {
let e = LocalEmbeddings::new(128);
let a = e.embed_query("机器学习").await.unwrap();
let b = e.embed_query("机器学习").await.unwrap();
assert_eq!(a, b);
let c = e.embed_query("深度学习").await.unwrap();
let sim = cosine_similarity(&a, &c);
assert!(sim > 0.0, "共享\"学习\"应有正相似度: {}", sim);
}
#[test]
fn test_tokenize_english() {
let t = LocalEmbeddings::tokenize("Hello, World! 123");
assert!(t.contains(&"hello".to_string()));
assert!(t.contains(&"world".to_string()));
assert!(t.contains(&"123".to_string()));
}
#[test]
fn test_tokenize_chinese() {
let t = LocalEmbeddings::tokenize("机器学习");
assert!(t.contains(&"机".to_string()));
assert!(t.contains(&"学".to_string()));
assert_eq!(t.len(), 4);
}
#[test]
fn test_model_name() {
let e = LocalEmbeddings::default_dim();
assert_eq!(e.model_name(), "local-bow");
}
}