use std::collections::{HashMap, HashSet};
const SEED: u64 = 0x5eed_fade_cafe_beef;
#[derive(Clone, Debug, Default)]
pub struct LSH {
hyperplanes: Vec<f32>,
dim: usize,
num_hyperplanes: usize,
}
impl LSH {
pub fn new(dim: usize, num_tables: usize, num_hyperplanes: usize) -> Self {
let num_hyperplanes = num_hyperplanes.min(64);
let mut rng = fastrand::Rng::with_seed(SEED);
let mut hyperplanes = Vec::new();
for _ in 0..num_tables.saturating_mul(num_hyperplanes) {
let plane: Vec<f32> = (0..dim).map(|_| rng.f32() * 2.0 - 1.0).collect();
let norm = plane.iter().map(|x| x * x).sum::<f32>().sqrt();
let norm = if norm > 0.0 { norm } else { 1.0 };
hyperplanes.extend(plane.iter().map(|x| x / norm));
}
Self {
hyperplanes,
dim,
num_hyperplanes,
}
}
pub fn hash(&self, vector: &[f64], table_idx: usize) -> u64 {
let table = self.dim.saturating_mul(self.num_hyperplanes);
let planes = table_idx
.checked_mul(table)
.and_then(|start| self.hyperplanes.get(start..start.checked_add(table)?))
.unwrap_or_default();
planes
.chunks(self.dim.max(1))
.enumerate()
.fold(0, |hash, (bit, plane)| {
let dot: f32 = vector.iter().zip(plane).map(|(v, h)| *v as f32 * h).sum();
if dot >= 0.0 { hash | 1 << bit } else { hash }
})
}
}
#[derive(Clone, Debug, Default)]
pub struct LSHIndex {
lsh: LSH,
tables: Vec<HashMap<u64, Vec<String>>>,
}
impl LSHIndex {
pub fn new(dim: usize, num_tables: usize, num_hyperplanes: usize) -> Self {
let (num_tables, num_hyperplanes) = match num_tables {
0 => (1, 0),
_ => (num_tables, num_hyperplanes),
};
Self {
lsh: LSH::new(dim, num_tables, num_hyperplanes),
tables: vec![HashMap::new(); num_tables],
}
}
pub fn insert(&mut self, id: &str, embedding: &[f64]) {
for (table_idx, table) in self.tables.iter_mut().enumerate() {
let hash = self.lsh.hash(embedding, table_idx);
table.entry(hash).or_default().push(id.to_owned());
}
}
pub fn remove(&mut self, id: &str, embedding: &[f64]) {
for (table_idx, table) in self.tables.iter_mut().enumerate() {
let hash = self.lsh.hash(embedding, table_idx);
if let Some(ids) = table.get_mut(&hash) {
if let Some(at) = ids.iter().position(|stored| stored == id) {
ids.swap_remove(at);
}
if ids.is_empty() {
table.remove(&hash);
}
}
}
}
pub fn query(&self, embedding: &[f64]) -> Vec<&str> {
let mut candidates = HashSet::new();
for (table_idx, table) in self.tables.iter().enumerate() {
if let Some(ids) = table.get(&self.lsh.hash(embedding, table_idx)) {
candidates.extend(ids.iter().map(String::as_str));
}
}
candidates.into_iter().collect()
}
pub fn clear(&mut self) {
self.tables.iter_mut().for_each(HashMap::clear);
}
}
#[cfg(test)]
mod tests;