use std::path::{Path, PathBuf};
use std::sync::Arc;
use model2vec_rs::model::StaticModel;
use crate::error::M1ndError;
use crate::error::M1ndResult;
pub const DEFAULT_MODEL_SUBDIR: &str = "assets/potion-base-8M";
pub const DEFAULT_HF_REPO: &str = "minishlab/potion-base-8M";
pub const MODEL_DIR_ENV: &str = "M1ND_EMBED_MODEL";
pub trait Embedder: Send + Sync {
fn embed(&self, text: &str) -> Box<[f32]>;
fn dim(&self) -> usize;
}
pub struct Model2VecEmbedder {
model: Arc<StaticModel>,
dim: usize,
id: String,
}
impl Model2VecEmbedder {
pub fn default_model_dir() -> PathBuf {
if let Ok(p) = std::env::var(MODEL_DIR_ENV) {
return PathBuf::from(p);
}
Path::new(env!("CARGO_MANIFEST_DIR")).join(DEFAULT_MODEL_SUBDIR)
}
pub fn from_default() -> M1ndResult<Self> {
let dir = Self::default_model_dir();
if dir.exists() {
return Self::from_dir(dir);
}
Self::from_hf(DEFAULT_HF_REPO)
}
pub fn from_hf(repo: &str) -> M1ndResult<Self> {
let model = StaticModel::from_pretrained(repo, None, None, None).map_err(|e| {
M1ndError::EmbedError(format!(
"failed to load model {repo} from Hugging Face: {e}"
))
})?;
let dim = model.encode_single("dim probe").len();
Ok(Self {
model: Arc::new(model),
dim,
id: repo.to_string(),
})
}
pub fn from_dir<P: AsRef<Path>>(dir: P) -> M1ndResult<Self> {
let dir = dir.as_ref();
if !dir.exists() {
return Err(M1ndError::EmbedError(format!(
"embed: model directory not found at {} (set {} or vendor the model; \
the blob is gitignored / Git-LFS managed)",
dir.display(),
MODEL_DIR_ENV
)));
}
let model = StaticModel::from_pretrained(dir, None, None, None).map_err(|e| {
M1ndError::EmbedError(format!("failed to load model from {}: {e}", dir.display()))
})?;
let probe = model.encode_single("dim probe");
let dim = probe.len();
let weights_len = std::fs::metadata(dir.join("model.safetensors"))
.map(|m| m.len())
.unwrap_or(0);
Ok(Self {
model: Arc::new(model),
dim,
id: format!("{}#{weights_len}", dir.display()),
})
}
pub fn model_id(&self) -> &str {
&self.id
}
}
impl Embedder for Model2VecEmbedder {
fn embed(&self, text: &str) -> Box<[f32]> {
let mut v = self.model.encode_single(text);
l2_normalize(&mut v);
v.into_boxed_slice()
}
fn dim(&self) -> usize {
self.dim
}
}
pub fn l2_normalize(v: &mut [f32]) {
let n = v.iter().map(|x| x * x).sum::<f32>().sqrt();
if n > 0.0 {
for x in v.iter_mut() {
*x /= n;
}
}
}
pub fn cosine(a: &[f32], b: &[f32]) -> f32 {
if a.len() != b.len() || a.is_empty() {
return 0.0;
}
let mut dot = 0.0f32;
let mut na = 0.0f32;
let mut nb = 0.0f32;
for i in 0..a.len() {
dot += a[i] * b[i];
na += a[i] * a[i];
nb += b[i] * b[i];
}
let denom = na.sqrt() * nb.sqrt();
if denom > 0.0 {
dot / denom
} else {
0.0
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn embedder_cosine_smoke() {
let dir = Model2VecEmbedder::default_model_dir();
if !dir.join("model.safetensors").exists() {
eprintln!(
"SKIP embedder_cosine_smoke: model not vendored at {} (set {} or fetch via Git LFS)",
dir.display(),
MODEL_DIR_ENV
);
return;
}
let emb = Model2VecEmbedder::from_dir(&dir).expect("load model");
assert!(emb.dim() > 0, "dim must be positive");
let v_a = emb.embed("graceful shutdown when the task is cancelled");
let v_b = emb.embed("abort the running job on cancellation");
let v_c = emb.embed("strawberry cheesecake recipe with whipped cream");
let norm = v_a.iter().map(|x| x * x).sum::<f32>().sqrt();
assert!(
(norm - 1.0).abs() < 1e-3,
"vector should be L2-normalized, got {norm}"
);
let related = cosine(&v_a, &v_b);
let unrelated = cosine(&v_a, &v_c);
assert!(
related > unrelated,
"related ({related}) should exceed unrelated ({unrelated})"
);
}
}