use std::cmp::Ordering;
use std::collections::{BinaryHeap, HashSet};
use super::hnsw::{HnswParams, DEFAULT_EF_SEARCH, DEFAULT_M};
use super::simd::{detect_simd, dot_i8, SimdKind};
use super::tier2::Sq8Cache;
#[derive(Debug, Clone, Copy)]
struct Cand {
idx: usize,
score: i32,
}
impl PartialEq for Cand {
fn eq(&self, other: &Self) -> bool {
self.idx == other.idx
}
}
impl Eq for Cand {}
impl PartialOrd for Cand {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Cand {
fn cmp(&self, other: &Self) -> Ordering {
self.score
.cmp(&other.score)
.then_with(|| self.idx.cmp(&other.idx))
}
}
#[derive(Debug, Clone, Copy)]
struct MinCand(Cand);
impl PartialEq for MinCand {
fn eq(&self, other: &Self) -> bool {
self.0.idx == other.0.idx
}
}
impl Eq for MinCand {}
impl PartialOrd for MinCand {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for MinCand {
fn cmp(&self, other: &Self) -> Ordering {
other.0.cmp(&self.0)
}
}
#[derive(Debug)]
pub struct Sq8Nsw {
cache: Sq8Cache,
neighbors: Vec<Vec<usize>>,
entry: usize,
params: HnswParams,
simd: SimdKind,
}
impl Sq8Nsw {
pub fn cache(&self) -> &Sq8Cache {
&self.cache
}
pub fn len(&self) -> usize {
self.cache.len()
}
pub fn is_empty(&self) -> bool {
self.cache.is_empty()
}
pub fn params(&self) -> HnswParams {
self.params
}
pub fn synth_for_bench(n: usize, dim: usize, params: HnswParams) -> Self {
let mut cache = Sq8Cache::new(dim);
for id in 0..n as u64 {
let mut row = vec![0i8; dim];
for (j, slot) in row.iter_mut().enumerate() {
let v = ((id as usize).wrapping_mul(31) + j * 17) % 254;
*slot = (v as i32 - 127) as i8;
}
cache.push_sq8(id, &row).expect("dim matches");
}
let m = params.m.clamp(super::hnsw::M_MIN, super::hnsw::M_MAX);
let mut neighbors = vec![Vec::with_capacity(m); n];
if n == 0 {
return Self {
cache,
neighbors,
entry: 0,
params,
simd: detect_simd(),
};
}
for (i, slot) in neighbors.iter_mut().enumerate() {
let mut nbrs = HashSet::new();
for d in 1..=(m / 2).max(1) {
nbrs.insert((i + d) % n);
nbrs.insert((i + n - d) % n);
}
for k in 0..(m.saturating_sub(nbrs.len())) {
let jump =
1 + ((i.wrapping_mul(2654435761) + k * 97) % (n.saturating_sub(1).max(1)));
nbrs.insert((i + jump) % n);
}
nbrs.remove(&i);
let mut list: Vec<usize> = nbrs.into_iter().take(m).collect();
list.sort_unstable();
*slot = list;
}
Self {
cache,
neighbors,
entry: 0,
params,
simd: detect_simd(),
}
}
fn score_at(&self, idx: usize, query: &[i8]) -> i32 {
let row = self.cache.row(idx).expect("idx in range");
dot_i8(row, query, self.simd)
}
pub fn search(&self, query: &[i8], k: usize, ef: usize) -> Vec<usize> {
if self.is_empty() || k == 0 {
return Vec::new();
}
let ef = ef.max(k).max(1);
let entry = self.entry.min(self.len() - 1);
let mut visited = HashSet::new();
visited.insert(entry);
let mut candidates = BinaryHeap::new(); let mut w = BinaryHeap::new(); let entry_score = self.score_at(entry, query);
candidates.push(Cand {
idx: entry,
score: entry_score,
});
w.push(MinCand(Cand {
idx: entry,
score: entry_score,
}));
while let Some(c) = candidates.pop() {
let worst = w.peek().map(|m| m.0.score).unwrap_or(i32::MIN);
if c.score < worst && w.len() >= ef {
break;
}
for &nb in &self.neighbors[c.idx] {
if !visited.insert(nb) {
continue;
}
let s = self.score_at(nb, query);
let worst = w.peek().map(|m| m.0.score).unwrap_or(i32::MIN);
if s > worst || w.len() < ef {
candidates.push(Cand { idx: nb, score: s });
w.push(MinCand(Cand { idx: nb, score: s }));
if w.len() > ef {
w.pop();
}
}
}
}
let mut out: Vec<Cand> = w.into_iter().map(|m| m.0).collect();
out.sort_by(|a, b| b.cmp(a));
out.into_iter().map(|c| c.idx).take(k).collect()
}
pub fn search_ids(&self, query: &[i8], k: usize, ef: usize) -> Vec<u64> {
self.search(query, k, ef)
.into_iter()
.filter_map(|i| self.cache.id_at(i))
.collect()
}
}
pub fn synth_query(dim: usize, seed: u64) -> Vec<i8> {
let mut q = vec![0i8; dim];
for (j, slot) in q.iter_mut().enumerate() {
let v = ((seed as usize).wrapping_mul(13) + j * 11) % 254;
*slot = (v as i32 - 127) as i8;
}
q
}
pub fn bench_params() -> HnswParams {
HnswParams {
m: DEFAULT_M,
ef_search: DEFAULT_EF_SEARCH,
..HnswParams::default()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::vector_engine::engine::DEFAULT_VECTOR_DIM;
#[test]
fn synth_nsw_search_returns_k() {
let graph = Sq8Nsw::synth_for_bench(256, DEFAULT_VECTOR_DIM, bench_params());
let q = synth_query(DEFAULT_VECTOR_DIM, 7);
let hits = graph.search(&q, 10, DEFAULT_EF_SEARCH);
assert_eq!(hits.len(), 10);
assert!(hits.iter().all(|&i| i < graph.len()));
}
#[test]
fn synth_nsw_search_ids_match_rows() {
let graph = Sq8Nsw::synth_for_bench(128, DEFAULT_VECTOR_DIM, bench_params());
let q = synth_query(DEFAULT_VECTOR_DIM, 11);
let idxs = graph.search(&q, 8, DEFAULT_EF_SEARCH);
let ids = graph.search_ids(&q, 8, DEFAULT_EF_SEARCH);
assert_eq!(ids.len(), idxs.len());
for (idx, id) in idxs.iter().zip(ids.iter()) {
assert_eq!(graph.cache().id_at(*idx), Some(*id));
}
}
#[test]
fn empty_graph_search_ok() {
let graph = Sq8Nsw::synth_for_bench(0, 8, bench_params());
assert!(graph.search(&[0i8; 8], 5, 10).is_empty());
assert!(graph.search_ids(&[0i8; 8], 5, 10).is_empty());
}
}