use std::collections::{HashMap, HashSet};
use embeddenator_vsa::{SparseVec, DIM};
pub trait CandidateGenerator<V> {
type Candidate;
fn candidates(&self, query: &V, k: usize) -> Vec<Self::Candidate>;
}
pub const DEFAULT_SIGNATURE_PROBES: usize = 24;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct SignatureQueryOptions {
pub max_candidates: usize,
pub probe_radius: u8,
pub max_probes: usize,
}
impl Default for SignatureQueryOptions {
fn default() -> Self {
Self {
max_candidates: 1_000,
probe_radius: 1,
max_probes: 1 + (2 * DEFAULT_SIGNATURE_PROBES),
}
}
}
#[derive(Clone, Debug)]
pub struct TernarySignatureIndex {
probe_dims: Vec<usize>,
buckets: HashMap<u64, Vec<usize>>, }
impl TernarySignatureIndex {
pub fn build_from_map(map: &HashMap<usize, SparseVec>) -> Self {
let probe_dims = default_probe_dims(DEFAULT_SIGNATURE_PROBES);
Self::build_from_map_with_probes(map, probe_dims)
}
pub fn build_from_map_with_probes(
map: &HashMap<usize, SparseVec>,
probe_dims: Vec<usize>,
) -> Self {
let mut buckets: HashMap<u64, Vec<usize>> = HashMap::new();
let mut ids: Vec<usize> = map.keys().copied().collect();
ids.sort_unstable();
for id in ids {
let Some(vec) = map.get(&id) else { continue };
let sig = signature_for(vec, &probe_dims);
buckets.entry(sig).or_default().push(id);
}
for ids in buckets.values_mut() {
ids.sort_unstable();
ids.dedup();
}
Self {
probe_dims,
buckets,
}
}
pub fn probe_dims(&self) -> &[usize] {
&self.probe_dims
}
pub fn candidates_with_options(
&self,
query: &SparseVec,
opts: SignatureQueryOptions,
) -> Vec<usize> {
if opts.max_candidates == 0 {
return Vec::new();
}
let sig = signature_for(query, &self.probe_dims);
let probe_radius = opts.probe_radius.min(1);
let probe_sigs =
probe_signatures(sig, self.probe_dims.len(), probe_radius, opts.max_probes);
let mut seen: HashSet<usize> = HashSet::new();
let mut out: Vec<usize> = Vec::new();
for ps in probe_sigs {
let Some(ids) = self.buckets.get(&ps) else {
continue;
};
for &id in ids {
if seen.insert(id) {
out.push(id);
if out.len() >= opts.max_candidates {
break;
}
}
}
if out.len() >= opts.max_candidates {
break;
}
}
out.sort_unstable();
out
}
}
impl CandidateGenerator<SparseVec> for TernarySignatureIndex {
type Candidate = usize;
fn candidates(&self, query: &SparseVec, k: usize) -> Vec<Self::Candidate> {
self.candidates_with_options(
query,
SignatureQueryOptions {
max_candidates: k,
..SignatureQueryOptions::default()
},
)
}
}
fn default_probe_dims(count: usize) -> Vec<usize> {
let mut out = Vec::with_capacity(count);
let mut seen = HashSet::with_capacity(count * 2);
let mut state: u64 = 0xED00_0000_0000_0001u64;
while out.len() < count {
state = splitmix64(state);
let d = (state as usize) % DIM;
if seen.insert(d) {
out.push(d);
}
}
out
}
fn splitmix64(mut x: u64) -> u64 {
x = x.wrapping_add(0x9E37_79B9_7F4A_7C15);
let mut z = x;
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
z ^ (z >> 31)
}
fn signature_for(vec: &SparseVec, probe_dims: &[usize]) -> u64 {
let mut sig: u64 = 0;
for (i, &d) in probe_dims.iter().enumerate() {
let lane = match sign_at(vec, d) {
0 => 0b00u64,
1 => 0b01u64,
-1 => 0b10u64,
_ => 0b00u64,
};
sig |= lane << (2 * i);
}
sig
}
fn sign_at(vec: &SparseVec, dim: usize) -> i8 {
if vec.pos.contains(&dim) {
1
} else if vec.neg.contains(&dim) {
-1
} else {
0
}
}
fn probe_signatures(base: u64, probes: usize, radius: u8, max_probes: usize) -> Vec<u64> {
if max_probes == 0 {
return Vec::new();
}
let mut out = Vec::new();
out.push(base);
if radius == 0 {
return out;
}
for i in 0..probes {
if out.len() >= max_probes {
break;
}
let shift = 2 * i;
let mask = 0b11u64 << shift;
let cur = (base & mask) >> shift;
for &alt in &[0b00u64, 0b01u64, 0b10u64] {
if alt == cur {
continue;
}
let next = (base & !mask) | (alt << shift);
out.push(next);
if out.len() >= max_probes {
break;
}
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use embeddenator_vsa::ReversibleVSAConfig;
#[test]
fn default_probe_dims_are_stable_and_in_range() {
let a = default_probe_dims(DEFAULT_SIGNATURE_PROBES);
let b = default_probe_dims(DEFAULT_SIGNATURE_PROBES);
assert_eq!(a, b);
assert_eq!(a.len(), DEFAULT_SIGNATURE_PROBES);
for &d in &a {
assert!(d < DIM);
}
let mut uniq = a.clone();
uniq.sort_unstable();
uniq.dedup();
assert_eq!(uniq.len(), DEFAULT_SIGNATURE_PROBES);
}
#[test]
fn candidates_are_deterministic_and_include_self_when_exact_bucket_hits() {
let cfg = ReversibleVSAConfig::default();
let v0 = SparseVec::encode_data(b"alpha", &cfg, None);
let v1 = SparseVec::encode_data(b"beta", &cfg, None);
let mut map = HashMap::new();
map.insert(0, v0.clone());
map.insert(1, v1);
let idx = TernarySignatureIndex::build_from_map(&map);
let opts = SignatureQueryOptions {
max_candidates: 10,
probe_radius: 0,
max_probes: 1,
};
let c1 = idx.candidates_with_options(&v0, opts);
let c2 = idx.candidates_with_options(&v0, opts);
assert_eq!(c1, c2);
assert!(c1.contains(&0));
}
#[test]
fn probe_signatures_radius_one_includes_base_and_is_bounded() {
let base = 0u64;
let sigs = probe_signatures(base, 4, 1, 5);
assert_eq!(sigs.len(), 5);
assert_eq!(sigs[0], base);
}
}