use crate::utils::simd::SimdVectorOps;
use ndarray::{Array1, Array2};
use rand::Rng;
use rayon::prelude::*;
use std::collections::HashMap;
#[derive(Clone)]
pub struct LSH {
num_tables: usize,
hash_size: usize,
#[allow(dead_code)]
vector_dim: usize,
hyperplanes: Vec<Array2<f32>>,
hash_tables: Vec<HashMap<Vec<bool>, Vec<usize>>>,
stored_vectors: Vec<Array1<f32>>,
stored_data: Vec<f32>,
}
impl LSH {
pub fn new(vector_dim: usize, num_tables: usize, hash_size: usize) -> Self {
Self::with_expected_size(vector_dim, num_tables, hash_size, 1000)
}
pub fn with_expected_size(
vector_dim: usize,
num_tables: usize,
hash_size: usize,
expected_size: usize,
) -> Self {
let mut rng = rand::thread_rng();
let mut hyperplanes = Vec::new();
for _ in 0..num_tables {
let mut table_hyperplanes = Array2::zeros((hash_size, vector_dim));
for i in 0..hash_size {
for j in 0..vector_dim {
table_hyperplanes[[i, j]] = rng.gen_range(-1.0..1.0);
}
}
hyperplanes.push(table_hyperplanes);
}
let bucket_capacity = (expected_size as f32 / 5.0).ceil() as usize;
let optimal_capacity = bucket_capacity.max(100);
let hash_tables = vec![HashMap::with_capacity(optimal_capacity); num_tables];
Self {
num_tables,
hash_size,
vector_dim,
hyperplanes,
hash_tables,
stored_vectors: Vec::with_capacity(expected_size),
stored_data: Vec::with_capacity(expected_size),
}
}
pub fn add_vector(&mut self, vector: Array1<f32>, data: f32) {
let index = self.stored_vectors.len();
let current_load =
self.stored_vectors.len() as f32 / (self.hash_tables[0].capacity() as f32 * 0.2);
if current_load > 0.75 {
self.resize_hash_tables();
}
let mut hashes = Vec::with_capacity(self.num_tables);
for table_idx in 0..self.num_tables {
hashes.push(self.hash_vector(&vector, table_idx));
}
self.stored_vectors.push(vector);
self.stored_data.push(data);
for (table_idx, hash) in hashes.into_iter().enumerate() {
self.hash_tables[table_idx]
.entry(hash)
.or_insert_with(|| Vec::with_capacity(8)) .push(index);
}
}
fn resize_hash_tables(&mut self) {
let new_capacity = (self.hash_tables[0].capacity() * 2).max(self.stored_vectors.len());
for table in &mut self.hash_tables {
table.reserve(new_capacity - table.capacity());
}
}
pub fn query(&self, query_vector: &Array1<f32>, k: usize) -> Vec<(Array1<f32>, f32, f32)> {
let mut candidates = std::collections::HashSet::new();
let max_candidates = (self.stored_vectors.len() / 4)
.max(k * 10)
.min(self.stored_vectors.len());
if self.num_tables > 4 {
let candidate_sets: Vec<Vec<usize>> = (0..self.num_tables)
.into_par_iter()
.map(|table_idx| {
let hash = self.hash_vector(query_vector, table_idx);
if let Some(bucket) = self.hash_tables[table_idx].get(&hash) {
bucket.clone()
} else {
Vec::new()
}
})
.collect();
for candidate_set in candidate_sets {
for idx in candidate_set {
candidates.insert(idx);
if candidates.len() >= max_candidates {
break;
}
}
if candidates.len() >= max_candidates {
break;
}
}
} else {
for table_idx in 0..self.num_tables {
if candidates.len() >= max_candidates {
break;
}
let hash = self.hash_vector(query_vector, table_idx);
if let Some(bucket) = self.hash_tables[table_idx].get(&hash) {
for &idx in bucket {
candidates.insert(idx);
if candidates.len() >= max_candidates {
break;
}
}
}
}
}
if candidates.len() < k * 3 && self.stored_vectors.len() > k * 3 {
let needed = k * 5;
for idx in 0..needed.min(self.stored_vectors.len()) {
candidates.insert(idx);
if candidates.len() >= needed {
break;
}
}
}
let mut results = if candidates.len() > 50 {
candidates
.par_iter()
.map(|&idx| {
let stored_vector = &self.stored_vectors[idx];
let similarity = cosine_similarity(query_vector, stored_vector);
(stored_vector.clone(), self.stored_data[idx], similarity)
})
.collect()
} else {
let mut results = Vec::with_capacity(candidates.len());
for &idx in &candidates {
let stored_vector = &self.stored_vectors[idx];
let similarity = cosine_similarity(query_vector, stored_vector);
results.push((stored_vector.clone(), self.stored_data[idx], similarity));
}
results
};
if results.len() > 100 {
results.par_sort_unstable_by(|a, b| {
b.2.partial_cmp(&a.2).unwrap_or(std::cmp::Ordering::Equal)
});
} else {
results.sort_unstable_by(|a, b| {
b.2.partial_cmp(&a.2).unwrap_or(std::cmp::Ordering::Equal)
});
}
results.truncate(k);
results
}
fn hash_vector(&self, vector: &Array1<f32>, table_idx: usize) -> Vec<bool> {
let hyperplanes = &self.hyperplanes[table_idx];
let dot_products = hyperplanes.dot(vector);
dot_products.iter().map(|&x| x >= 0.0).collect()
}
pub fn stats(&self) -> LSHStats {
let mut bucket_sizes = Vec::new();
let mut total_buckets = 0;
let mut non_empty_buckets = 0;
for table in &self.hash_tables {
let buckets_per_table = 1_usize
.checked_shl(self.hash_size as u32)
.unwrap_or(usize::MAX);
total_buckets += buckets_per_table;
non_empty_buckets += table.len();
for bucket in table.values() {
bucket_sizes.push(bucket.len());
}
}
bucket_sizes.sort();
let median_bucket_size = if bucket_sizes.is_empty() {
0.0
} else {
bucket_sizes[bucket_sizes.len() / 2] as f32
};
let avg_bucket_size = if bucket_sizes.is_empty() {
0.0
} else {
bucket_sizes.iter().sum::<usize>() as f32 / bucket_sizes.len() as f32
};
LSHStats {
num_vectors: self.stored_vectors.len(),
num_tables: self.num_tables,
hash_size: self.hash_size,
total_buckets,
non_empty_buckets,
avg_bucket_size,
median_bucket_size,
max_bucket_size: bucket_sizes.last().copied().unwrap_or(0),
}
}
pub fn save_to_database(
&self,
db: &crate::persistence::Database,
) -> Result<(), Box<dyn std::error::Error>> {
use crate::persistence::{LSHHashFunction, LSHTableData};
let mut hash_functions = Vec::new();
for hyperplane_matrix in &self.hyperplanes {
for row in hyperplane_matrix.rows() {
hash_functions.push(LSHHashFunction {
random_vector: row.to_vec().iter().map(|&x| x as f64).collect(),
threshold: 0.0, });
}
}
let config = LSHTableData {
num_tables: self.num_tables,
num_hash_functions: self.hash_size,
vector_dim: self.vector_dim,
hash_functions,
};
db.save_lsh_config(&config)?;
db.clear_lsh_buckets()?;
for (table_idx, table) in self.hash_tables.iter().enumerate() {
for (hash_bits, indices) in table {
let hash_string = hash_bits
.iter()
.map(|&b| if b { '1' } else { '0' })
.collect::<String>();
for &position_idx in indices {
db.save_lsh_bucket(table_idx, &hash_string, position_idx as i64)?;
}
}
}
Ok(())
}
pub fn load_from_database(
db: &crate::persistence::Database,
positions: &[(Array1<f32>, f32)],
) -> Result<Option<Self>, Box<dyn std::error::Error>> {
let config = match db.load_lsh_config()? {
Some(config) => config,
None => return Ok(None),
};
let mut hyperplanes = Vec::new();
let functions_per_table = config.num_hash_functions;
for table_idx in 0..config.num_tables {
let start_idx = table_idx * functions_per_table;
let end_idx = start_idx + functions_per_table;
if end_idx <= config.hash_functions.len() {
let mut table_hyperplanes = Array2::zeros((functions_per_table, config.vector_dim));
for (func_idx, hash_func) in
config.hash_functions[start_idx..end_idx].iter().enumerate()
{
for (dim_idx, &value) in hash_func.random_vector.iter().enumerate() {
if dim_idx < config.vector_dim {
table_hyperplanes[[func_idx, dim_idx]] = value as f32;
}
}
}
hyperplanes.push(table_hyperplanes);
}
}
let mut lsh = Self {
num_tables: config.num_tables,
hash_size: config.num_hash_functions,
vector_dim: config.vector_dim,
hyperplanes,
hash_tables: vec![HashMap::with_capacity(positions.len().max(100)); config.num_tables],
stored_vectors: Vec::new(),
stored_data: Vec::new(),
};
for (vector, evaluation) in positions {
lsh.add_vector(vector.clone(), *evaluation);
}
Ok(Some(lsh))
}
pub fn from_database_or_new(
db: &crate::persistence::Database,
positions: &[(Array1<f32>, f32)],
vector_dim: usize,
num_tables: usize,
hash_size: usize,
) -> Result<Self, Box<dyn std::error::Error>> {
match Self::load_from_database(db, positions)? {
Some(lsh) => {
println!(
"Loaded LSH configuration from database with {} vectors",
lsh.stored_vectors.len()
);
Ok(lsh)
}
None => {
println!("No saved LSH configuration found, creating new LSH index");
let mut lsh =
Self::with_expected_size(vector_dim, num_tables, hash_size, positions.len());
for (vector, evaluation) in positions {
lsh.add_vector(vector.clone(), *evaluation);
}
Ok(lsh)
}
}
}
}
#[derive(Debug)]
pub struct LSHStats {
pub num_vectors: usize,
pub num_tables: usize,
pub hash_size: usize,
pub total_buckets: usize,
pub non_empty_buckets: usize,
pub avg_bucket_size: f32,
pub median_bucket_size: f32,
pub max_bucket_size: usize,
}
fn cosine_similarity(a: &Array1<f32>, b: &Array1<f32>) -> f32 {
SimdVectorOps::cosine_similarity(a, b)
}
#[cfg(test)]
mod tests {
use super::*;
use ndarray::Array1;
#[test]
fn test_lsh_creation() {
let lsh = LSH::new(128, 4, 8);
assert_eq!(lsh.num_tables, 4);
assert_eq!(lsh.hash_size, 8);
assert_eq!(lsh.vector_dim, 128);
}
#[test]
fn test_lsh_add_and_query() {
let mut lsh = LSH::new(4, 2, 4);
let vec1 = Array1::from(vec![1.0, 0.0, 0.0, 0.0]);
let vec2 = Array1::from(vec![0.0, 1.0, 0.0, 0.0]);
let vec3 = Array1::from(vec![1.0, 0.1, 0.0, 0.0]);
lsh.add_vector(vec1.clone(), 1.0);
lsh.add_vector(vec2, 2.0);
lsh.add_vector(vec3, 1.1);
let results = lsh.query(&vec1, 2);
assert!(!results.is_empty());
assert!(results[0].2 > 0.8); }
#[test]
fn test_cosine_similarity() {
let a = Array1::from(vec![1.0, 0.0, 0.0]);
let b = Array1::from(vec![1.0, 0.0, 0.0]);
let c = Array1::from(vec![0.0, 1.0, 0.0]);
assert!((cosine_similarity(&a, &b) - 1.0).abs() < 1e-6);
assert!((cosine_similarity(&a, &c) - 0.0).abs() < 1e-6);
}
}