// 注入点:Embedder。宿主实现向量化;缺省是 HashingEmbedder。
// 缺省实现 HashingEmbedder:确定性词袋哈希嵌入,无网络、O(n)。
// Clock 注入点见 clock.mbt。
///|
/// 嵌入服务抽象。生产环境可注入 vcdb 等适配实现。
pub(open) trait Embedder {
/// 批量嵌入。返回数组与输入一一对应,每条长度必须等于 dim()。
fn embed(self : Self, texts : Array[String]) -> Result[
Array[Array[Double]],
MoomemError,
]
/// 嵌入维度。
fn dim(self : Self) -> Int32
}
///|
/// 缺省嵌入器:小写化 → 分词(与 BM25 同一分词器 tokenize)→
/// 每 token FNV-1a 哈希到 [0, dim) 桶 → 桶内 TF 累加 → L2 归一。
/// 确定性、离线,三后端行为一致。
pub struct HashingEmbedder {
dim : Int32
}
///|
/// 构造 HashingEmbedder;dim 必须 > 0,否则返回 Err。
pub fn HashingEmbedder::new(
dim : Int32,
) -> Result[HashingEmbedder, MoomemError] {
if dim <= 0 {
return Err(
MoomemError::EmbeddingFailure(
"HashingEmbedder dim must be positive, got \{dim}",
),
)
}
Ok(HashingEmbedder::{ dim, })
}
///|
pub impl Embedder for HashingEmbedder with fn embed(self, texts) {
let result : Array[Array[Double]] = []
for text in texts {
result.push(hashing_embed_one(text, self.dim))
}
Ok(result)
}
///|
pub impl Embedder for HashingEmbedder with fn dim(self) {
self.dim
}
///|
pub extend HashingEmbedder with Embedder::{embed, dim}
///|
fn hashing_embed_one(text : String, dim : Int32) -> Array[Double] {
let vector = Array::make(dim, 0.0)
let tokens = tokenize(text)
for token in tokens {
let bucket = (fnv1a(token) & 0x7FFFFFFF) % dim
vector[bucket] = vector[bucket] + 1.0
}
// L2 归一
let mut norm_sq = 0.0
for value in vector {
norm_sq = norm_sq + value * value
}
let norm = norm_sq.sqrt()
if norm > 0.0 {
for i = 0; i < dim; i = i + 1 {
vector[i] = vector[i] / norm
}
}
vector
}
///|
/// 两向量余弦相似度;长度为 0 或不相等时返回 0.0(不 panic)。
fn cosine_similarity(a : Array[Double], b : Array[Double]) -> Double {
if a.length() == 0 || a.length() != b.length() {
return 0.0
}
let mut dot = 0.0
let mut na = 0.0
let mut nb = 0.0
for i = 0; i < a.length(); i = i + 1 {
dot = dot + a[i] * b[i]
na = na + a[i] * a[i]
nb = nb + b[i] * b[i]
}
if na == 0.0 || nb == 0.0 {
return 0.0
}
dot / (na.sqrt() * nb.sqrt())
}