mod onnx;
pub use onnx::OnnxEmbedder;
use super::mrl::MRL_DIMS;
pub const EMBED_DIMS: usize = 384;
pub trait Embedder: Send + Sync {
fn embed(&self, text: &str) -> Vec<f32>;
fn kind(&self) -> &'static str;
}
pub fn default_embedder(model: Option<&str>) -> Box<dyn Embedder> {
static ANNOUNCED: std::sync::Once = std::sync::Once::new();
match OnnxEmbedder::load(model) {
Some(e) => {
ANNOUNCED.call_once(|| log::info!("using ONNX embedder ({} dims)", e.dims()));
Box::new(e)
}
None => {
ANNOUNCED.call_once(|| {
log::warn!(
"embedding model unavailable — using the built-in hash fallback \
(reduced semantic accuracy). Pass --model <dir|.onnx|org/name>, \
or run with -v to see why the model didn't load."
)
});
Box::new(HashEmbedder::new())
}
}
}
#[derive(Default, Clone, Copy, Debug)]
pub struct HashEmbedder;
impl HashEmbedder {
pub fn new() -> Self {
Self
}
}
fn fnv1a(bytes: &[u8]) -> u64 {
let mut hash: u64 = 0xcbf2_9ce4_8422_2325;
for &b in bytes {
hash ^= b as u64;
hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
}
hash
}
fn tokenize(text: &str) -> impl Iterator<Item = String> + '_ {
text.split(|c: char| !c.is_alphanumeric())
.filter(|t| !t.is_empty())
.map(|t| t.to_lowercase())
}
impl Embedder for HashEmbedder {
fn kind(&self) -> &'static str {
"hash"
}
fn embed(&self, text: &str) -> Vec<f32> {
let mut v = vec![0.0f32; EMBED_DIMS];
for token in tokenize(text) {
let h = fnv1a(token.as_bytes());
let idx = (h % MRL_DIMS as u64) as usize;
let sign = if (h >> 32) & 1 == 0 { 1.0 } else { -1.0 };
v[idx] += sign;
}
v
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn embedding_has_full_width() {
assert_eq!(HashEmbedder::new().embed("hello world").len(), EMBED_DIMS);
}
#[test]
fn embedding_is_deterministic() {
let e = HashEmbedder::new();
assert_eq!(e.embed("a user account"), e.embed("a user account"));
}
#[test]
fn different_text_gives_different_vectors() {
let e = HashEmbedder::new();
assert_ne!(e.embed("users"), e.embed("posts"));
}
}