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
}
}
#[derive(Clone)]
pub struct FakeEmbedder {
dim: usize,
anchored: Vec<String>,
anchor_weight: f32,
}
impl FakeEmbedder {
pub fn new(dim: usize) -> Self {
Self {
dim: dim.max(1),
anchored: Vec::new(),
anchor_weight: 0.0,
}
}
pub fn with_anchor(dim: usize, anchored: &[&str], anchor_weight: f32) -> Self {
Self {
dim: dim.max(1),
anchored: anchored.iter().map(|s| s.to_string()).collect(),
anchor_weight: anchor_weight.clamp(0.0, 1.0),
}
}
fn seed_of(text: &str) -> u64 {
let mut h = 0xcbf29ce484222325u64;
for b in text.as_bytes() {
h ^= *b as u64;
h = h.wrapping_mul(0x00000100000001B3);
}
h
}
fn raw_vector(&self, seed: u64) -> Vec<f32> {
let mut state = seed | 1; let mut v = Vec::with_capacity(self.dim);
let mut sum = 0.0f32;
for _ in 0..self.dim {
state = state
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
let u = (state >> 33) as f32 / (1u64 << 31) as f32; v.push(u);
sum += u;
}
let mean = sum / self.dim as f32;
for x in v.iter_mut() {
*x -= mean;
}
v
}
}
impl Embedder for FakeEmbedder {
fn embed(&self, text: &str) -> Box<[f32]> {
let mut v = self.raw_vector(Self::seed_of(text));
if self.anchor_weight > 0.0 && self.anchored.iter().any(|a| a == text) {
let anchor = self.raw_vector(Self::seed_of("\u{0}m1nd-fake-anchor\u{0}"));
for (x, a) in v.iter_mut().zip(anchor.iter()) {
*x = (1.0 - self.anchor_weight) * *x + self.anchor_weight * *a;
}
}
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})"
);
}
}