use crate::Result;
use std::str::FromStr;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EmbeddingModel {
Ollama,
}
impl FromStr for EmbeddingModel {
type Err = crate::Error;
fn from_str(s: &str) -> Result<Self> {
match s.to_lowercase().as_str() {
"ollama" => Ok(Self::Ollama),
other => Err(crate::Error::Other(format!(
"Unknown embedding model: {other}. Only 'ollama' is supported."
))),
}
}
}
impl EmbeddingModel {
pub fn dimension(&self) -> usize {
match self {
Self::Ollama => 768, }
}
}
pub fn embed(text: &str, model: EmbeddingModel) -> Result<Vec<f64>> {
match model {
EmbeddingModel::Ollama => ollama_embed(text),
}
}
pub fn cosine_similarity(a: &[f64], b: &[f64]) -> Result<f64> {
if a.len() != b.len() {
return Err(crate::Error::Other(format!(
"Vector dimension mismatch: {} vs {}",
a.len(),
b.len()
)));
}
if a.is_empty() {
return Ok(0.0);
}
let dot_product: f64 = a.iter().zip(b.iter()).map(|(x, y)| x * y).sum();
let magnitude_a: f64 = a.iter().map(|x| x * x).sum::<f64>().sqrt();
let magnitude_b: f64 = b.iter().map(|x| x * x).sum::<f64>().sqrt();
if magnitude_a == 0.0 || magnitude_b == 0.0 {
return Ok(0.0);
}
Ok(dot_product / (magnitude_a * magnitude_b))
}
fn ollama_embed(text: &str) -> Result<Vec<f64>> {
use std::process::Command;
let output = Command::new("curl")
.arg("-s")
.arg("http://localhost:11434/api/embeddings")
.arg("-d")
.arg(format!(
r#"{{"model":"nomic-embed-text","prompt":"{}"}}"#,
text.replace('"', r#"\""#)
))
.output()
.map_err(|e| crate::Error::Other(format!("Failed to call Ollama: {}", e)))?;
if !output.status.success() {
return Err(crate::Error::Other(format!(
"Ollama command failed: {}",
String::from_utf8_lossy(&output.stderr)
)));
}
let response_text = String::from_utf8(output.stdout)
.map_err(|e| crate::Error::Other(format!("Invalid UTF-8 from Ollama: {}", e)))?;
let json: serde_json::Value = serde_json::from_str(&response_text)
.map_err(|e| crate::Error::Other(format!("Failed to parse Ollama response: {}", e)))?;
let embedding_array = json
.get("embedding")
.and_then(|v| v.as_array())
.ok_or_else(|| crate::Error::Other("No embedding in Ollama response".to_string()))?;
let embedding: Vec<f64> = embedding_array.iter().filter_map(|v| v.as_f64()).collect();
if embedding.len() != 768 {
return Err(crate::Error::Other(format!(
"Expected 768-dimensional embedding, got {}",
embedding.len()
)));
}
Ok(embedding)
}
pub fn ollama_embed_sync(text: &str) -> Result<Vec<f64>> {
ollama_embed(text)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cosine_similarity() {
let a = vec![1.0, 0.0, 0.0];
let b = vec![1.0, 0.0, 0.0];
let similarity = cosine_similarity(&a, &b).unwrap();
assert!((similarity - 1.0).abs() < 1e-10);
let c = vec![1.0, 0.0, 0.0];
let d = vec![0.0, 1.0, 0.0];
let similarity = cosine_similarity(&c, &d).unwrap();
assert!((similarity - 0.0).abs() < 1e-10);
}
#[test]
fn test_embedding_model_from_str() {
assert_eq!(
"ollama".parse::<EmbeddingModel>().unwrap(),
EmbeddingModel::Ollama
);
assert!("invalid".parse::<EmbeddingModel>().is_err());
}
}