use std::collections::{HashMap, HashSet};
use crate::embeddings::{DistanceMetric, Embedding};
fn xorshift64(mut state: u64) -> impl FnMut() -> f32 {
move || {
state ^= state << 13;
state ^= state >> 7;
state ^= state << 17;
(state as i64 as f32) / (i64::MAX as f32)
}
}
struct Hyperplanes {
planes: Vec<Vec<f32>>,
num_hyperplanes: usize,
}
impl Hyperplanes {
fn new(dim: usize, num_tables: usize, num_hyperplanes: usize, seed: u64) -> Self {
let mut rand = xorshift64(seed | 1); let total = num_tables * num_hyperplanes;
let mut planes = Vec::with_capacity(total);
for _ in 0..total {
let mut plane: Vec<f32> = (0..dim).map(|_| rand()).collect();
let norm: f32 = plane.iter().map(|x| x * x).sum::<f32>().sqrt();
if norm > 0.0 {
plane.iter_mut().for_each(|v| *v /= norm);
}
planes.push(plane);
}
Self { planes, num_hyperplanes }
}
fn hash(&self, vector: &[f64], table_idx: usize) -> u64 {
let start = table_idx * self.num_hyperplanes;
let mut hash = 0u64;
for (bit, plane) in self.planes[start..start + self.num_hyperplanes]
.iter()
.enumerate()
{
let dot: f32 = vector
.iter()
.zip(plane.iter())
.map(|(&v, &h)| v as f32 * h)
.sum();
if dot >= 0.0 {
hash |= 1 << bit;
}
}
hash
}
}
pub struct LshIndex {
planes: Hyperplanes,
tables: Vec<HashMap<u64, Vec<String>>>,
num_tables: usize,
}
impl LshIndex {
pub fn new(dim: usize, num_hyperplanes: usize, num_tables: usize, seed: u64) -> Self {
Self {
planes: Hyperplanes::new(dim, num_tables, num_hyperplanes, seed),
tables: vec![HashMap::new(); num_tables],
num_tables,
}
}
pub fn insert(&mut self, id: String, embedding: &[f64]) {
for table_idx in 0..self.num_tables {
let hash = self.planes.hash(embedding, table_idx);
self.tables[table_idx]
.entry(hash)
.or_default()
.push(id.clone());
}
}
pub fn query(&self, embedding: &[f64]) -> Vec<String> {
let mut candidates = HashSet::new();
for table_idx in 0..self.num_tables {
let hash = self.planes.hash(embedding, table_idx);
if let Some(ids) = self.tables[table_idx].get(&hash) {
candidates.extend(ids.iter().cloned());
}
}
candidates.into_iter().collect()
}
pub fn len(&self) -> usize {
self.tables.first().map_or(0, |t| t.values().map(|v| v.len()).sum())
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub fn clear(&mut self) {
self.tables.iter_mut().for_each(|t| t.clear());
}
pub fn search(
&self,
query: &Embedding,
store: &HashMap<String, Embedding>,
metric: Option<DistanceMetric>,
) -> Vec<(String, f64)> {
let metric = metric.unwrap_or(DistanceMetric::Cosine { normalized: false });
let mut ranked: Vec<(String, f64)> = self
.query(&query.vec)
.into_iter()
.filter_map(|id| store.get(&id).map(|emb| (id, metric.score(query, emb))))
.collect();
if metric.higher_is_better() {
ranked.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
} else {
ranked.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
}
ranked
}
}