pub trait Embedder: Send + Sync {
fn embed(&self, text: &str) -> anyhow::Result<Vec<f32>>;
fn name(&self) -> &str;
}
pub fn cosine(a: &[f32], b: &[f32]) -> f32 {
if a.is_empty() || b.is_empty() || a.len() != b.len() {
return 0.0;
}
let (mut dot, mut na, mut nb) = (0.0f32, 0.0f32, 0.0f32);
for i in 0..a.len() {
dot += a[i] * b[i];
na += a[i] * a[i];
nb += b[i] * b[i];
}
if na == 0.0 || nb == 0.0 {
0.0
} else {
(dot / (na.sqrt() * nb.sqrt())).clamp(0.0, 1.0)
}
}
pub fn vec_to_bytes(v: &[f32]) -> Vec<u8> {
let mut out = Vec::with_capacity(v.len() * 4);
for f in v {
out.extend_from_slice(&f.to_le_bytes());
}
out
}
pub fn bytes_to_vec(b: &[u8]) -> Vec<f32> {
if b.len() % 4 != 0 {
return Vec::new();
}
b.chunks_exact(4)
.map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]))
.collect()
}
pub struct OllamaEmbedder {
endpoint: String,
model: String,
}
impl OllamaEmbedder {
pub fn new(model: impl Into<String>) -> Self {
Self {
endpoint: "http://localhost:11434".into(),
model: model.into(),
}
}
pub fn with_endpoint(mut self, endpoint: impl Into<String>) -> Self {
self.endpoint = endpoint.into();
self
}
}
impl Default for OllamaEmbedder {
fn default() -> Self {
Self::new("nomic-embed-text")
}
}
impl Embedder for OllamaEmbedder {
fn name(&self) -> &str {
"ollama"
}
fn embed(&self, text: &str) -> anyhow::Result<Vec<f32>> {
let url = format!("{}/api/embeddings", self.endpoint);
let resp = ureq::post(&url)
.send_json(ureq::json!({ "model": self.model, "prompt": text }))
.map_err(|e| anyhow::anyhow!("ollama request failed: {e}"))?;
let body: serde_json::Value = resp
.into_json()
.map_err(|e| anyhow::anyhow!("ollama response not JSON: {e}"))?;
let arr = body
.get("embedding")
.and_then(|v| v.as_array())
.ok_or_else(|| anyhow::anyhow!("ollama response missing 'embedding'"))?;
let vec: Vec<f32> = arr
.iter()
.filter_map(|x| x.as_f64().map(|f| f as f32))
.collect();
if vec.is_empty() {
anyhow::bail!("ollama returned an empty embedding");
}
Ok(vec)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cosine_basics() {
assert!((cosine(&[1.0, 0.0], &[1.0, 0.0]) - 1.0).abs() < 1e-6);
assert!(cosine(&[1.0, 0.0], &[0.0, 1.0]).abs() < 1e-6);
assert_eq!(cosine(&[], &[1.0]), 0.0);
assert_eq!(cosine(&[1.0, 2.0], &[1.0]), 0.0); }
#[test]
fn vec_bytes_roundtrip() {
let v = vec![0.0f32, 1.5, -2.25, 3.125e9, f32::MIN];
assert_eq!(bytes_to_vec(&vec_to_bytes(&v)), v);
assert_eq!(vec_to_bytes(&v).len(), v.len() * 4);
assert!(bytes_to_vec(&[1, 2, 3]).is_empty());
assert!(bytes_to_vec(&[]).is_empty());
}
}