201 lines
7.1 KiB
Rust
201 lines
7.1 KiB
Rust
//! This module provides conversion of a Point structure to a FlatPoint containing just the Id of a point
|
|
//! and those of its neighbours.
|
|
//! The whole Hnsw structure is then flattened into a Hashtable associating the data ID of a point to
|
|
//! its corresponding FlatPoint.
|
|
//! It can be used, for example, when reloading only the graph part of the data to have knowledge
|
|
//! of relative proximity of points as described just by their DataId
|
|
//!
|
|
|
|
use hashbrown::HashMap;
|
|
use std::cmp::Ordering;
|
|
|
|
use crate::hnsw;
|
|
use anndists::dist::distances::Distance;
|
|
use hnsw::*;
|
|
use log::error;
|
|
|
|
// an ordering of Neighbour of a Point
|
|
|
|
impl PartialEq for Neighbour {
|
|
fn eq(&self, other: &Neighbour) -> bool {
|
|
self.distance == other.distance
|
|
} // end eq
|
|
}
|
|
|
|
impl Eq for Neighbour {}
|
|
|
|
// order points by distance to self.
|
|
#[allow(clippy::non_canonical_partial_ord_impl)]
|
|
impl PartialOrd for Neighbour {
|
|
fn partial_cmp(&self, other: &Neighbour) -> Option<Ordering> {
|
|
self.distance.partial_cmp(&other.distance)
|
|
} // end cmp
|
|
} // end impl PartialOrd
|
|
|
|
impl Ord for Neighbour {
|
|
fn cmp(&self, other: &Neighbour) -> Ordering {
|
|
if !self.distance.is_nan() && !other.distance.is_nan() {
|
|
self.distance.partial_cmp(&other.distance).unwrap()
|
|
} else {
|
|
panic!("got a NaN in a distance");
|
|
}
|
|
} // end cmp
|
|
}
|
|
|
|
/// a reduced version of point inserted in the Hnsw structure.
|
|
/// It contains original id of point as submitted to the struct Hnsw
|
|
/// an ordered (by distance) list of neighbours to the point
|
|
/// and it position in layers.
|
|
#[derive(Clone)]
|
|
pub struct FlatPoint {
|
|
/// an id coming from client using hnsw, should identify point uniquely
|
|
origin_id: DataId,
|
|
/// a point id identifying point as stored in our structure
|
|
p_id: PointId,
|
|
/// neighbours info
|
|
neighbours: Vec<Neighbour>,
|
|
}
|
|
|
|
impl FlatPoint {
|
|
/// returns the neighbours orderded by distance.
|
|
pub fn get_neighbours(&self) -> &Vec<Neighbour> {
|
|
&self.neighbours
|
|
}
|
|
/// returns the origin id of the point
|
|
pub fn get_id(&self) -> DataId {
|
|
self.origin_id
|
|
}
|
|
//
|
|
pub fn get_p_id(&self) -> PointId {
|
|
self.p_id
|
|
}
|
|
} // end impl block for FlatPoint
|
|
|
|
fn flatten_point<T: Clone + Send + Sync>(point: &Point<T>) -> FlatPoint {
|
|
let neighbours = point.get_neighborhood_id();
|
|
// now we flatten neighbours
|
|
let mut flat_neighbours = Vec::<Neighbour>::new();
|
|
for layer in neighbours {
|
|
for neighbour in layer {
|
|
flat_neighbours.push(neighbour);
|
|
}
|
|
}
|
|
flat_neighbours.sort_unstable();
|
|
FlatPoint {
|
|
origin_id: point.get_origin_id(),
|
|
p_id: point.get_point_id(),
|
|
neighbours: flat_neighbours,
|
|
}
|
|
} // end of flatten_point
|
|
|
|
/// A structure providing neighbourhood information of a point stored in the Hnsw structure given its DataId.
|
|
/// The structure uses the [FlatPoint] structure.
|
|
/// This structure can be obtained by FlatNeighborhood::from<&Hnsw<T,D>>
|
|
pub struct FlatNeighborhood {
|
|
hash_t: HashMap<DataId, FlatPoint>,
|
|
}
|
|
|
|
impl FlatNeighborhood {
|
|
/// get neighbour of a point given its id.
|
|
/// The neighbours are sorted in increasing distance from data_id.
|
|
pub fn get_neighbours(&self, p_id: DataId) -> Option<Vec<Neighbour>> {
|
|
self.hash_t
|
|
.get(&p_id)
|
|
.map(|point| point.get_neighbours().clone())
|
|
}
|
|
} // end impl block for FlatNeighborhood
|
|
|
|
impl<T: Clone + Send + Sync, D: Distance<T> + Send + Sync> From<&Hnsw<'_, T, D>>
|
|
for FlatNeighborhood
|
|
{
|
|
/// extract from the Hnsw strucure a hashtable mapping original DataId into a FlatPoint structure gathering its neighbourhood information.
|
|
/// Useful after reloading from a dump with T=NoData and D = NoDist as points are then reloaded with neighbourhood information only.
|
|
fn from(hnsw: &Hnsw<T, D>) -> Self {
|
|
let mut hash_t = HashMap::new();
|
|
let pt_iter = hnsw.get_point_indexation().into_iter();
|
|
//
|
|
for point in pt_iter {
|
|
// println!("point : {:?}", _point.p_id);
|
|
let res_insert = hash_t.insert(point.get_origin_id(), flatten_point(&point));
|
|
if let Some(old_point) = res_insert {
|
|
error!("2 points with same origin id {:?}", old_point.origin_id);
|
|
}
|
|
}
|
|
FlatNeighborhood { hash_t }
|
|
}
|
|
} // e,d of Fom implementation
|
|
|
|
#[cfg(test)]
|
|
|
|
mod tests {
|
|
|
|
use super::*;
|
|
use anndists::dist::distances::*;
|
|
use log::debug;
|
|
|
|
use crate::api::AnnT;
|
|
use crate::hnswio::*;
|
|
|
|
use rand::distr::{Distribution, Uniform};
|
|
|
|
fn log_init_test() {
|
|
let _ = env_logger::builder().is_test(true).try_init();
|
|
}
|
|
|
|
#[test]
|
|
fn test_dump_reload_graph_flatten() {
|
|
println!("\n\n test_dump_reload_graph_flatten");
|
|
log_init_test();
|
|
// generate a random test
|
|
let mut rng = rand::rng();
|
|
let unif = Uniform::<f32>::new(0., 1.).unwrap();
|
|
// 1000 vectors of size 10 f32
|
|
let nbcolumn = 1000;
|
|
let nbrow = 10;
|
|
let mut xsi;
|
|
let mut data = Vec::with_capacity(nbcolumn);
|
|
for j in 0..nbcolumn {
|
|
data.push(Vec::with_capacity(nbrow));
|
|
for _ in 0..nbrow {
|
|
xsi = unif.sample(&mut rng);
|
|
data[j].push(xsi);
|
|
}
|
|
}
|
|
// define hnsw
|
|
let ef_construct = 25;
|
|
let nb_connection = 10;
|
|
let hnsw = Hnsw::<f32, DistL1>::new(nb_connection, nbcolumn, 16, ef_construct, DistL1 {});
|
|
for (i, d) in data.iter().enumerate() {
|
|
hnsw.insert((d, i));
|
|
}
|
|
// some loggin info
|
|
hnsw.dump_layer_info();
|
|
// get flat neighbours of point 3
|
|
let neighborhood_before_dump = FlatNeighborhood::from(&hnsw);
|
|
let nbg_2_before = neighborhood_before_dump.get_neighbours(2).unwrap();
|
|
println!("voisins du point 2 {:?}", nbg_2_before);
|
|
// dump in a file. Must take care of name as tests runs in // !!!
|
|
let fname = "dumpreloadtestflat";
|
|
let directory = tempfile::tempdir().unwrap();
|
|
let _res = hnsw.file_dump(directory.path(), fname);
|
|
// This will dump in 2 files named dumpreloadtest.hnsw.graph and dumpreloadtest.hnsw.data
|
|
//
|
|
// reload
|
|
debug!("HNSW reload");
|
|
// we will need a procedural macro to get from distance name to its instantiation.
|
|
// from now on we test with DistL1
|
|
let mut reloader = HnswIo::new(directory.path(), fname);
|
|
let hnsw_loaded: Hnsw<NoData, NoDist> = reloader.load_hnsw().unwrap();
|
|
let neighborhood_after_dump = FlatNeighborhood::from(&hnsw_loaded);
|
|
let nbg_2_after = neighborhood_after_dump.get_neighbours(2).unwrap();
|
|
println!("Neighbors of point 2 {:?}", nbg_2_after);
|
|
// test equality of neighborhood
|
|
assert_eq!(nbg_2_after.len(), nbg_2_before.len());
|
|
for i in 0..nbg_2_before.len() {
|
|
assert_eq!(nbg_2_before[i].p_id, nbg_2_after[i].p_id);
|
|
assert_eq!(nbg_2_before[i].distance, nbg_2_after[i].distance);
|
|
}
|
|
check_graph_equality(&hnsw_loaded, &hnsw);
|
|
} // end of test_dump_reload
|
|
} // end module test
|