use crate::lloyd;
use crate::rhdh::{padded_dim, Rhdh};
use std::collections::HashMap;
pub struct VecqIndex {
pub(crate) dim: usize,
padded: usize,
pub(crate) seed: u64,
transform: Rhdh,
pub(crate) codes: Vec<u8>, pub(crate) scales: Vec<f32>, pub(crate) n: usize, keys: Vec<Option<u64>>, key_to_slot: HashMap<u64, usize>,
alive: Vec<bool>, live: usize, }
impl VecqIndex {
pub fn new(dim: usize, seed: u64) -> Self {
let padded = padded_dim(dim);
Self {
dim,
padded,
seed,
transform: Rhdh::new(dim, seed),
codes: Vec::new(),
scales: Vec::new(),
n: 0,
keys: Vec::new(),
key_to_slot: HashMap::new(),
alive: Vec::new(),
live: 0,
}
}
pub fn len(&self) -> usize {
self.live
}
pub fn is_empty(&self) -> bool {
self.live == 0
}
pub fn slots(&self) -> usize {
self.n
}
pub fn tombstones(&self) -> usize {
self.n - self.live
}
pub fn dim(&self) -> usize {
self.dim
}
pub fn seed(&self) -> u64 {
self.seed
}
#[cfg(test)]
pub(crate) fn padded(&self) -> usize {
self.padded
}
pub(crate) fn padded_dim(&self) -> usize {
self.padded
}
pub(crate) fn live_slots(&self) -> usize {
self.live
}
pub(crate) fn slot_alive(&self, slot: usize) -> bool {
self.alive[slot]
}
pub(crate) fn slot_scale(&self, slot: usize) -> f32 {
self.scales[slot]
}
pub(crate) fn slot_codes(&self, slot: usize, bpv: usize) -> &[u8] {
&self.codes[slot * bpv..(slot + 1) * bpv]
}
pub(crate) fn init_dense(&mut self, count: usize) {
self.keys = vec![None; count];
self.alive = vec![true; count];
self.key_to_slot.clear();
self.live = count;
}
pub fn add(&mut self, v: &[f32]) -> usize {
self.append_slot(v, None)
}
pub fn add_keyed(&mut self, key: u64, v: &[f32]) -> usize {
if let Some(&slot) = self.key_to_slot.get(&key) {
let scale = self.encode_into(slot * (self.padded / 2), v);
self.scales[slot] = scale;
return slot;
}
self.append_slot(v, Some(key))
}
pub fn remove_keyed(&mut self, key: u64) -> bool {
match self.key_to_slot.remove(&key) {
Some(slot) => {
self.alive[slot] = false;
self.keys[slot] = None;
self.live -= 1;
true
}
None => false,
}
}
pub fn key_of(&self, slot: usize) -> Option<u64> {
self.keys.get(slot).copied().flatten()
}
pub fn contains_key(&self, key: u64) -> bool {
self.key_to_slot.contains_key(&key)
}
pub fn compact(&mut self) {
if self.live == self.n {
return;
}
let bpv = self.padded / 2;
let mut codes = Vec::with_capacity(self.live * bpv);
let mut scales = Vec::with_capacity(self.live);
let mut keys = Vec::with_capacity(self.live);
let mut alive = Vec::with_capacity(self.live);
self.key_to_slot.clear();
for slot in 0..self.n {
if self.alive[slot] {
codes.extend_from_slice(&self.codes[slot * bpv..(slot + 1) * bpv]);
scales.push(self.scales[slot]);
if let Some(key) = self.keys[slot] {
self.key_to_slot.insert(key, keys.len());
}
keys.push(self.keys[slot]);
alive.push(true);
}
}
self.codes = codes;
self.scales = scales;
self.keys = keys;
self.alive = alive;
self.n = self.live;
}
fn append_slot(&mut self, v: &[f32], key: Option<u64>) -> usize {
let slot = self.n;
let scale = self.encode_into(slot * (self.padded / 2), v);
self.scales.push(scale);
self.keys.push(key);
if let Some(key) = key {
self.key_to_slot.insert(key, slot);
}
self.alive.push(true);
self.n += 1;
self.live += 1;
slot
}
fn encode_into(&mut self, base: usize, v: &[f32]) -> f32 {
assert_eq!(v.len(), self.dim, "vector dim mismatch");
let norm: f32 = v.iter().map(|x| x * x).sum::<f32>().sqrt();
assert!(norm > 0.0, "zero vector");
let unit: Vec<f32> = v.iter().map(|x| x / norm).collect();
let mut rotated = Vec::with_capacity(self.padded);
self.transform.apply(&unit, &mut rotated);
let bytes_per_vec = self.padded / 2;
if self.codes.len() < base + bytes_per_vec {
self.codes.resize(base + bytes_per_vec, 0);
}
let mut sum_sq = 0f32;
for (i, &x) in rotated.iter().enumerate() {
let code = lloyd::quantize_4bit(x);
let b = base + i / 2;
let byte = if i % 2 == 0 {
(self.codes[b] & 0xF0) | code
} else {
(self.codes[b] & 0x0F) | (code << 4)
};
self.codes[b] = byte;
sum_sq += lloyd::dequantize_4bit(code).powi(2);
}
1.0 / sum_sq.sqrt()
}
pub fn prepare_query(&self, q: &[f32]) -> PreparedQuery {
assert_eq!(q.len(), self.dim);
let norm: f32 = q.iter().map(|x| x * x).sum::<f32>().sqrt();
let unit: Vec<f32> = q.iter().map(|x| x / norm).collect();
let mut rotated = Vec::with_capacity(self.padded);
self.transform.apply(&unit, &mut rotated);
let rnorm: f32 = rotated.iter().map(|x| x * x).sum::<f32>().sqrt();
for x in rotated.iter_mut() {
*x /= rnorm;
}
let mut lut = [0f32; 16];
for (c, slot) in lut.iter_mut().enumerate() {
*slot = lloyd::dequantize_4bit(c as u8);
}
PreparedQuery { rotated, lut, norm }
}
#[inline]
pub fn score(&self, pq: &PreparedQuery, idx: usize) -> f32 {
let base = idx * (self.padded / 2);
let codes = &self.codes[base..base + self.padded / 2];
let q = &pq.rotated[..self.padded];
#[cfg(target_arch = "aarch64")]
{
let raw = unsafe { neon::score_neon(codes, q, &pq.lut) };
raw * self.scales[idx]
}
#[cfg(target_arch = "x86_64")]
{
if avx2::available() {
let raw = unsafe { avx2::score_avx2(codes, q, &pq.lut) };
raw * self.scales[idx]
} else {
score_scalar(codes, q, &pq.lut) * self.scales[idx]
}
}
#[cfg(not(any(target_arch = "aarch64", target_arch = "x86_64")))]
{
score_scalar(codes, q, &pq.lut) * self.scales[idx]
}
}
pub fn search(&self, q: &[f32], k: usize) -> Vec<(usize, f32)> {
self.search_slots(q, k)
}
pub fn search_keyed(&self, q: &[f32], k: usize) -> Vec<(u64, f32)> {
self.search_slots(q, k)
.into_iter()
.filter_map(|(slot, s)| self.key_of(slot).map(|key| (key, s)))
.collect()
}
fn search_slots(&self, q: &[f32], k: usize) -> Vec<(usize, f32)> {
use std::cmp::Reverse;
use std::collections::BinaryHeap;
let pq = self.prepare_query(q);
let k = k.min(self.live).max(1);
let bpv = self.padded / 2;
let key = |s: f32| -> u32 {
let b = s.to_bits();
if b & 0x8000_0000 != 0 {
!b
} else {
b ^ 0x8000_0000
}
};
let mut heap: BinaryHeap<Reverse<(u32, usize)>> = BinaryHeap::with_capacity(k + 1);
let consider = |s: f32, idx: usize, heap: &mut BinaryHeap<Reverse<(u32, usize)>>| {
let ks = key(s);
if heap.len() < k {
heap.push(Reverse((ks, idx)));
} else if ks > heap.peek().map(|r| r.0 .0).unwrap_or(0) {
heap.push(Reverse((ks, idx)));
heap.pop();
}
};
#[cfg(target_arch = "aarch64")]
let q_rot = &pq.rotated[..self.padded];
#[cfg(target_arch = "x86_64")]
let use_avx2 = avx2::available();
let mut idx = 0;
#[cfg(target_arch = "aarch64")]
{
while idx + 4 <= self.n {
let codes4 = &self.codes[idx * bpv..(idx + 4) * bpv];
let raw = unsafe { neon::score_neon4(codes4, q_rot, &pq.lut) };
for (v, &r) in raw.iter().enumerate() {
let si = idx + v;
if self.alive[si] {
consider(r * self.scales[si], si, &mut heap);
}
}
idx += 4;
}
}
#[cfg(target_arch = "x86_64")]
{
if use_avx2 {
while idx + 4 <= self.n {
let codes4 = &self.codes[idx * bpv..(idx + 4) * bpv];
let raw =
unsafe { avx2::score_avx24(codes4, &pq.rotated[..self.padded], &pq.lut) };
for (v, &r) in raw.iter().enumerate() {
let si = idx + v;
if self.alive[si] {
consider(r * self.scales[si], si, &mut heap);
}
}
idx += 4;
}
}
}
while idx < self.n {
if self.alive[idx] {
consider(self.score(&pq, idx), idx, &mut heap);
}
idx += 1;
}
let key_undo = |k: u32| -> u32 {
if k & 0x8000_0000 != 0 {
k ^ 0x8000_0000 } else {
!k }
};
let mut out: Vec<(usize, f32)> = heap
.into_iter()
.map(|r| (r.0 .1, f32::from_bits(key_undo(r.0 .0))))
.collect();
out.sort_by(|a, b| b.1.partial_cmp(&a.1).expect("no NaN scores"));
out
}
}
#[cfg_attr(target_arch = "aarch64", cfg(test))]
pub(crate) fn score_scalar(codes: &[u8], q: &[f32], lut: &[f32; 16]) -> f32 {
let nb = codes.len();
let mut acc = [0f32; 8];
let mut i = 0;
while i + 8 <= nb {
for j in 0..8 {
let b = codes[i + j];
let c = (i + j) * 2;
acc[j] += q[c] * lut[(b & 0x0F) as usize] + q[c + 1] * lut[(b >> 4) as usize];
}
i += 8;
}
let mut tail = 0f32;
while i < nb {
let b = codes[i];
let c = i * 2;
tail += q[c] * lut[(b & 0x0F) as usize] + q[c + 1] * lut[(b >> 4) as usize];
i += 1;
}
let s01 = acc[0] + acc[1];
let s23 = acc[2] + acc[3];
let s45 = acc[4] + acc[5];
let s67 = acc[6] + acc[7];
(s01 + s23) + (s45 + s67) + tail
}
#[cfg(target_arch = "aarch64")]
mod neon {
use std::arch::aarch64::*;
#[inline]
unsafe fn gather16(tbl: uint8x16x4_t, nibbles: uint8x16_t) -> [float32x4_t; 4] {
let idx = vmulq_u8(nibbles, vdupq_n_u8(4)); let one = vdupq_n_u8(1);
let two = vdupq_n_u8(2);
let b0 = vqtbl4q_u8(tbl, idx);
let b1 = vqtbl4q_u8(tbl, vaddq_u8(idx, one));
let b2 = vqtbl4q_u8(tbl, vaddq_u8(idx, two));
let b3 = vqtbl4q_u8(tbl, vaddq_u8(idx, vdupq_n_u8(3)));
let z01 = vzip1q_u8(b0, b1); let z23 = vzip1q_u8(b2, b3); let z01b = vzip2q_u8(b0, b1);
let z23b = vzip2q_u8(b2, b3);
let lo16 = vreinterpretq_u16_u8(z01);
let hi16 = vreinterpretq_u16_u8(z23);
let lo16b = vreinterpretq_u16_u8(z01b);
let hi16b = vreinterpretq_u16_u8(z23b);
[
vreinterpretq_f32_u8(vreinterpretq_u8_u16(vzip1q_u16(lo16, hi16))),
vreinterpretq_f32_u8(vreinterpretq_u8_u16(vzip2q_u16(lo16, hi16))),
vreinterpretq_f32_u8(vreinterpretq_u8_u16(vzip1q_u16(lo16b, hi16b))),
vreinterpretq_f32_u8(vreinterpretq_u8_u16(vzip2q_u16(lo16b, hi16b))),
]
}
#[inline]
pub unsafe fn score_neon(codes: &[u8], q: &[f32], lut: &[f32; 16]) -> f32 {
let mut bytes = [0u8; 64];
for (c, &v) in lut.iter().enumerate() {
bytes[c * 4..c * 4 + 4].copy_from_slice(&v.to_le_bytes());
}
let tbl = uint8x16x4_t(
vld1q_u8(bytes[0..16].as_ptr()),
vld1q_u8(bytes[16..32].as_ptr()),
vld1q_u8(bytes[32..48].as_ptr()),
vld1q_u8(bytes[48..64].as_ptr()),
);
let mut acc_lo = vdupq_n_f32(0.0);
let mut acc_hi = vdupq_n_f32(0.0);
let nb = codes.len();
let mut i = 0;
while i + 8 <= nb {
let b8 = vld1_u8(codes.as_ptr().add(i)); let lo = vand_u8(b8, vdup_n_u8(0x0F));
let hi = vshr_n_u8(b8, 4);
let nibbles = vcombine_u8(lo, hi); let g = gather16(tbl, nibbles);
let q0 = vld1q_f32(q.as_ptr().add(i * 2));
let q1 = vld1q_f32(q.as_ptr().add(i * 2 + 4));
let q2 = vld1q_f32(q.as_ptr().add(i * 2 + 8));
let q3 = vld1q_f32(q.as_ptr().add(i * 2 + 12));
let q_even_lo = vuzp1q_f32(q0, q1); let q_even_hi = vuzp1q_f32(q2, q3);
let q_odd_lo = vuzp2q_f32(q0, q1);
let q_odd_hi = vuzp2q_f32(q2, q3);
let t_lo = vaddq_f32(vmulq_f32(q_even_lo, g[0]), vmulq_f32(q_odd_lo, g[2]));
let t_hi = vaddq_f32(vmulq_f32(q_even_hi, g[1]), vmulq_f32(q_odd_hi, g[3]));
acc_lo = vaddq_f32(acc_lo, t_lo);
acc_hi = vaddq_f32(acc_hi, t_hi);
i += 8;
}
let mut acc = [0f32; 8];
acc[0] = vgetq_lane_f32(acc_lo, 0);
acc[1] = vgetq_lane_f32(acc_lo, 1);
acc[2] = vgetq_lane_f32(acc_lo, 2);
acc[3] = vgetq_lane_f32(acc_lo, 3);
acc[4] = vgetq_lane_f32(acc_hi, 0);
acc[5] = vgetq_lane_f32(acc_hi, 1);
acc[6] = vgetq_lane_f32(acc_hi, 2);
acc[7] = vgetq_lane_f32(acc_hi, 3);
let mut tail = 0f32;
while i < nb {
let b = codes[i];
let c = i * 2;
tail += q[c] * lut[(b & 0x0F) as usize] + q[c + 1] * lut[(b >> 4) as usize];
i += 1;
}
let s01 = acc[0] + acc[1];
let s23 = acc[2] + acc[3];
let s45 = acc[4] + acc[5];
let s67 = acc[6] + acc[7];
(s01 + s23) + (s45 + s67) + tail
}
#[inline]
pub unsafe fn score_neon4(codes4: &[u8], q: &[f32], lut: &[f32; 16]) -> [f32; 4] {
let mut bytes = [0u8; 64];
for (c, &v) in lut.iter().enumerate() {
bytes[c * 4..c * 4 + 4].copy_from_slice(&v.to_le_bytes());
}
let tbl = uint8x16x4_t(
vld1q_u8(bytes[0..16].as_ptr()),
vld1q_u8(bytes[16..32].as_ptr()),
vld1q_u8(bytes[32..48].as_ptr()),
vld1q_u8(bytes[48..64].as_ptr()),
);
let nb = codes4.len() / 4; let mut acc_lo = [vdupq_n_f32(0.0); 4];
let mut acc_hi = [vdupq_n_f32(0.0); 4];
let mut i = 0;
while i + 8 <= nb {
let q0 = vld1q_f32(q.as_ptr().add(i * 2));
let q1 = vld1q_f32(q.as_ptr().add(i * 2 + 4));
let q2 = vld1q_f32(q.as_ptr().add(i * 2 + 8));
let q3 = vld1q_f32(q.as_ptr().add(i * 2 + 12));
let q_even_lo = vuzp1q_f32(q0, q1);
let q_even_hi = vuzp1q_f32(q2, q3);
let q_odd_lo = vuzp2q_f32(q0, q1);
let q_odd_hi = vuzp2q_f32(q2, q3);
for v in 0..4 {
let b8 = vld1_u8(codes4.as_ptr().add(v * nb + i));
let lo = vand_u8(b8, vdup_n_u8(0x0F));
let hi = vshr_n_u8(b8, 4);
let nibbles = vcombine_u8(lo, hi);
let g = gather16(tbl, nibbles);
let t_lo = vaddq_f32(vmulq_f32(q_even_lo, g[0]), vmulq_f32(q_odd_lo, g[2]));
let t_hi = vaddq_f32(vmulq_f32(q_even_hi, g[1]), vmulq_f32(q_odd_hi, g[3]));
acc_lo[v] = vaddq_f32(acc_lo[v], t_lo);
acc_hi[v] = vaddq_f32(acc_hi[v], t_hi);
}
i += 8;
}
let mut out = [0f32; 4];
for v in 0..4 {
let mut a = [0f32; 8];
a[0] = vgetq_lane_f32(acc_lo[v], 0);
a[1] = vgetq_lane_f32(acc_lo[v], 1);
a[2] = vgetq_lane_f32(acc_lo[v], 2);
a[3] = vgetq_lane_f32(acc_lo[v], 3);
a[4] = vgetq_lane_f32(acc_hi[v], 0);
a[5] = vgetq_lane_f32(acc_hi[v], 1);
a[6] = vgetq_lane_f32(acc_hi[v], 2);
a[7] = vgetq_lane_f32(acc_hi[v], 3);
let mut tail = 0f32;
let mut j = i;
while j < nb {
let b = codes4[v * nb + j];
let c = j * 2;
tail += q[c] * lut[(b & 0x0F) as usize] + q[c + 1] * lut[(b >> 4) as usize];
j += 1;
}
let s01 = a[0] + a[1];
let s23 = a[2] + a[3];
let s45 = a[4] + a[5];
let s67 = a[6] + a[7];
out[v] = (s01 + s23) + (s45 + s67) + tail;
}
out
}
}
#[cfg(target_arch = "x86_64")]
mod avx2 {
use std::arch::x86_64::*;
pub fn available() -> bool {
std::is_x86_feature_detected!("avx2")
}
#[inline]
unsafe fn gather8(lut: &[f32; 16], nibbles: __m128i) -> __m256 {
let idx = _mm256_cvtepu8_epi32(nibbles);
_mm256_i32gather_ps(lut.as_ptr(), idx, 4)
}
#[inline]
unsafe fn deinterleave16(q: *const f32) -> (__m256, __m256) {
let qa = _mm256_loadu_ps(q);
let qb = _mm256_loadu_ps(q.add(8));
let fixup = _mm256_setr_epi32(0, 1, 4, 5, 2, 3, 6, 7);
let even = _mm256_permutevar8x32_ps(_mm256_shuffle_ps(qa, qb, 0x88), fixup);
let odd = _mm256_permutevar8x32_ps(_mm256_shuffle_ps(qa, qb, 0xDD), fixup);
(even, odd)
}
#[inline]
#[target_feature(enable = "avx2")]
pub unsafe fn score_avx2(codes: &[u8], q: &[f32], lut: &[f32; 16]) -> f32 {
let mut acc = _mm256_setzero_ps();
let nb = codes.len();
let mut i = 0;
while i + 8 <= nb {
let b8 = _mm_loadl_epi64(codes.as_ptr().add(i) as *const __m128i);
let g_lo = gather8(lut, _mm_and_si128(b8, _mm_set1_epi8(0x0F)));
let g_hi = gather8(
lut,
_mm_and_si128(_mm_srli_epi16(b8, 4), _mm_set1_epi8(0x0F)),
);
let (even, odd) = deinterleave16(q.as_ptr().add(i * 2));
let term = _mm256_add_ps(_mm256_mul_ps(even, g_lo), _mm256_mul_ps(odd, g_hi));
acc = _mm256_add_ps(acc, term);
i += 8;
}
let mut a = [0f32; 8];
_mm256_storeu_ps(a.as_mut_ptr(), acc);
let mut tail = 0f32;
while i < nb {
let b = codes[i];
let c = i * 2;
tail += q[c] * lut[(b & 0x0F) as usize] + q[c + 1] * lut[(b >> 4) as usize];
i += 1;
}
let s01 = a[0] + a[1];
let s23 = a[2] + a[3];
let s45 = a[4] + a[5];
let s67 = a[6] + a[7];
(s01 + s23) + (s45 + s67) + tail
}
#[inline]
#[target_feature(enable = "avx2")]
pub unsafe fn score_avx24(codes4: &[u8], q: &[f32], lut: &[f32; 16]) -> [f32; 4] {
let nb = codes4.len() / 4; let mut acc = [_mm256_setzero_ps(); 4];
let mut i = 0;
while i + 8 <= nb {
let (even, odd) = deinterleave16(q.as_ptr().add(i * 2));
for (v, acc_v) in acc.iter_mut().enumerate() {
let b8 = _mm_loadl_epi64(codes4.as_ptr().add(v * nb + i) as *const __m128i);
let g_lo = gather8(lut, _mm_and_si128(b8, _mm_set1_epi8(0x0F)));
let g_hi = gather8(
lut,
_mm_and_si128(_mm_srli_epi16(b8, 4), _mm_set1_epi8(0x0F)),
);
let term = _mm256_add_ps(_mm256_mul_ps(even, g_lo), _mm256_mul_ps(odd, g_hi));
*acc_v = _mm256_add_ps(*acc_v, term);
}
i += 8;
}
let mut out = [0f32; 4];
for v in 0..4 {
let mut a = [0f32; 8];
_mm256_storeu_ps(a.as_mut_ptr(), acc[v]);
let mut tail = 0f32;
let mut j = i;
while j < nb {
let b = codes4[v * nb + j];
let c = j * 2;
tail += q[c] * lut[(b & 0x0F) as usize] + q[c + 1] * lut[(b >> 4) as usize];
j += 1;
}
let s01 = a[0] + a[1];
let s23 = a[2] + a[3];
let s45 = a[4] + a[5];
let s67 = a[6] + a[7];
out[v] = (s01 + s23) + (s45 + s67) + tail;
}
out
}
}
pub struct PreparedQuery {
rotated: Vec<f32>,
lut: [f32; 16],
#[allow(dead_code)]
norm: f32,
}
pub fn cosine_f32(a: &[f32], b: &[f32]) -> f32 {
let mut dot = 0f32;
let mut na = 0f32;
let mut nb = 0f32;
for i in 0..a.len() {
dot += a[i] * b[i];
na += a[i] * a[i];
nb += b[i] * b[i];
}
dot / (na.sqrt() * nb.sqrt())
}
#[cfg(test)]
mod tests {
use super::*;
fn rand_unit(dim: usize, seed: u64) -> Vec<f32> {
let mut x = seed | 1;
let mut v = Vec::with_capacity(dim);
for _ in 0..dim {
x ^= x << 13;
x ^= x >> 7;
x ^= x << 17;
let u1 = ((x >> 11) as f64 / (1u64 << 53) as f64).max(1e-12);
x ^= x << 16;
let u2 = (x >> 11) as f64 / (1u64 << 53) as f64;
v.push(((-2.0 * u1.ln()).sqrt() * (std::f64::consts::TAU * u2).cos()) as f32);
}
let norm: f32 = v.iter().map(|a| a * a).sum::<f32>().sqrt();
v.into_iter().map(|a| a / norm).collect()
}
#[test]
fn score_correlates_with_exact_cosine() {
let dim = 128;
let mut idx = VecqIndex::new(dim, 7);
let base: Vec<Vec<f32>> = (0..200).map(|i| rand_unit(dim, i + 1)).collect();
for v in &base {
idx.add(v);
}
let q = rand_unit(dim, 999);
let pq = idx.prepare_query(&q);
let exact: Vec<f32> = base.iter().map(|v| cosine_f32(&q, v)).collect();
let mut max_err = 0f32;
for (i, &e) in exact.iter().enumerate().take(200) {
let est = idx.score(&pq, i);
max_err = max_err.max((est - e).abs());
}
assert!(max_err < 0.2, "max score error {max_err}");
}
#[test]
fn score_reproducible_and_close_to_naive() {
let dim = 128;
let mut idx = VecqIndex::new(dim, 13);
for i in 0..50 {
idx.add(&rand_unit(dim, i + 21));
}
let q = rand_unit(dim, 321);
let pq = idx.prepare_query(&q);
for vi in 0..50 {
let base = vi * (idx.padded() / 2);
let mut naive = 0f32;
for i in 0..idx.padded() {
let b = idx.codes[base + i / 2];
let code = if i % 2 == 0 { b & 0x0F } else { b >> 4 };
naive += pq.rotated[i] * pq.lut[code as usize];
}
let s = idx.score(&pq, vi);
assert_eq!(s.to_bits(), idx.score(&pq, vi).to_bits());
assert!((s - naive * idx.scales[vi]).abs() < 1e-5, "vector {vi}");
}
}
#[test]
fn neon_matches_scalar_bitwise() {
let dim = 128;
let mut idx = VecqIndex::new(dim, 42);
for i in 0..30 {
idx.add(&rand_unit(dim, i + 500));
}
let q = rand_unit(dim, 777);
let pq = idx.prepare_query(&q);
for vi in 0..30 {
let base = vi * (idx.padded() / 2);
let codes = &idx.codes[base..base + idx.padded() / 2];
let qslice = &pq.rotated[..idx.padded()];
#[cfg(target_arch = "aarch64")]
{
let neon = unsafe { neon::score_neon(codes, qslice, &pq.lut) };
let scalar = score_scalar(codes, qslice, &pq.lut);
assert_eq!(
neon.to_bits(),
scalar.to_bits(),
"vector {vi}: NEON and scalar diverged"
);
}
#[cfg(not(target_arch = "aarch64"))]
{
let _ = (base, codes, qslice);
}
}
}
#[cfg(target_arch = "aarch64")]
#[test]
fn neon4_matches_neon_bitwise() {
let dim = 128;
let mut idx = VecqIndex::new(dim, 91);
for i in 0..12 {
idx.add(&rand_unit(dim, i + 90));
}
let q = rand_unit(dim, 1234);
let pq = idx.prepare_query(&q);
let bpv = idx.padded() / 2;
for chunk_start in (0..12).step_by(4) {
let codes4 = &idx.codes[chunk_start * bpv..(chunk_start + 4) * bpv];
let qslice = &pq.rotated[..idx.padded()];
{
let batched = unsafe { neon::score_neon4(codes4, qslice, &pq.lut) };
for v in 0..4 {
let single = unsafe {
neon::score_neon(&codes4[v * bpv..(v + 1) * bpv], qslice, &pq.lut)
};
assert_eq!(
batched[v].to_bits(),
single.to_bits(),
"chunk {chunk_start} vec {v}: neon4 diverged from neon"
);
}
}
}
}
#[cfg(target_arch = "x86_64")]
#[test]
fn avx2_matches_scalar_bitwise() {
if !avx2::available() {
return; }
for (dim, seed) in [(128, 42), (8, 43)] {
let mut idx = VecqIndex::new(dim, seed);
for i in 0..30 {
idx.add(&rand_unit(dim, i + 500));
}
let q = rand_unit(dim, 777);
let pq = idx.prepare_query(&q);
for vi in 0..30 {
let base = vi * (idx.padded() / 2);
let codes = &idx.codes[base..base + idx.padded() / 2];
let qslice = &pq.rotated[..idx.padded()];
let avx2raw = unsafe { avx2::score_avx2(codes, qslice, &pq.lut) };
let scalar = score_scalar(codes, qslice, &pq.lut);
assert_eq!(
avx2raw.to_bits(),
scalar.to_bits(),
"dim {dim} vector {vi}: AVX2 and scalar diverged"
);
}
}
}
#[cfg(target_arch = "x86_64")]
#[test]
fn avx24_matches_avx2_bitwise() {
if !avx2::available() {
return;
}
let dim = 128;
let mut idx = VecqIndex::new(dim, 91);
for i in 0..12 {
idx.add(&rand_unit(dim, i + 90));
}
let q = rand_unit(dim, 1234);
let pq = idx.prepare_query(&q);
let bpv = idx.padded() / 2;
for chunk_start in (0..12).step_by(4) {
let codes4 = &idx.codes[chunk_start * bpv..(chunk_start + 4) * bpv];
let qslice = &pq.rotated[..idx.padded()];
let batched = unsafe { avx2::score_avx24(codes4, qslice, &pq.lut) };
for v in 0..4 {
let single =
unsafe { avx2::score_avx2(&codes4[v * bpv..(v + 1) * bpv], qslice, &pq.lut) };
assert_eq!(
batched[v].to_bits(),
single.to_bits(),
"chunk {chunk_start} vec {v}: avx24 diverged from avx2"
);
}
}
}
#[cfg(target_arch = "x86_64")]
#[test]
fn search_dispatch_matches_scalar_on_avx2_hosts() {
let dim = 128;
let mut idx = VecqIndex::new(dim, 55);
for i in 0..30 {
idx.add(&rand_unit(dim, i + 800));
}
let q = rand_unit(dim, 888);
let pq = idx.prepare_query(&q);
for vi in 0..30 {
let base = vi * (idx.padded() / 2);
let codes = &idx.codes[base..base + idx.padded() / 2];
let qslice = &pq.rotated[..idx.padded()];
let scalar = score_scalar(codes, qslice, &pq.lut) * idx.scales[vi];
assert_eq!(idx.score(&pq, vi).to_bits(), scalar.to_bits());
}
}
#[test]
fn search_returns_sorted_results() {
let dim = 64;
let mut idx = VecqIndex::new(dim, 3);
for i in 0..50 {
idx.add(&rand_unit(dim, i * 31 + 5));
}
let q = rand_unit(dim, 77);
let res = idx.search(&q, 5);
assert_eq!(res.len(), 5);
for w in res.windows(2) {
assert!(w[0].1 >= w[1].1);
}
}
#[test]
fn quantized_size_is_one_eighth() {
let dim = 384;
let mut idx = VecqIndex::new(dim, 1);
idx.add(&rand_unit(dim, 11));
assert_eq!(idx.codes.len(), 512 / 2);
}
#[test]
fn keyed_add_search_remove() {
let dim = 64;
let mut idx = VecqIndex::new(dim, 5);
for i in 0..50u64 {
idx.add_keyed(1000 + i, &rand_unit(dim, i * 17 + 3));
}
assert_eq!(idx.len(), 50);
assert!(idx.contains_key(1000));
assert!(!idx.contains_key(999));
let q = rand_unit(dim, 77);
let keyed = idx.search_keyed(&q, 5);
assert_eq!(keyed.len(), 5);
for w in keyed.windows(2) {
assert!(w[0].1 >= w[1].1);
}
let positional = idx.search(&q, 5);
for ((key, ks), (slot, ps)) in keyed.iter().zip(positional.iter()) {
assert_eq!(key, &idx.key_of(*slot).unwrap());
assert_eq!(ks.to_bits(), ps.to_bits());
}
let top_key = keyed[0].0;
assert!(idx.remove_keyed(top_key));
assert!(!idx.remove_keyed(top_key), "second remove is a no-op");
assert!(!idx.remove_keyed(12345), "unknown key returns false");
assert_eq!(idx.len(), 49);
assert_eq!(idx.tombstones(), 1);
let keyed2 = idx.search_keyed(&q, 5);
assert!(!keyed2.iter().any(|(k, _)| *k == top_key));
for (k, s) in keyed2.iter() {
let old = keyed.iter().find(|(ok, _)| ok == k).map(|(_, os)| *os);
if let Some(os) = old {
assert_eq!(s.to_bits(), os.to_bits(), "key {k} score changed");
}
}
}
#[test]
fn keyed_add_same_key_replaces() {
let dim = 32;
let mut idx = VecqIndex::new(dim, 9);
idx.add_keyed(7, &rand_unit(dim, 101));
idx.add_keyed(7, &rand_unit(dim, 202));
assert_eq!(idx.len(), 1, "replace must not grow the index");
assert_eq!(idx.tombstones(), 0);
let q = rand_unit(dim, 202);
let res = idx.search_keyed(&q, 1);
assert_eq!(res[0].0, 7);
}
#[test]
fn keyed_slot_indices_stay_stable_across_remove_and_serialize() {
let dim = 64;
let mut idx = VecqIndex::new(dim, 15);
for i in 0..20u64 {
idx.add_keyed(i, &rand_unit(dim, i + 300));
}
let q = rand_unit(dim, 404);
let before = idx.search(&q, 20);
idx.remove_keyed(idx.key_of(before[0].0).unwrap());
idx.remove_keyed(idx.key_of(before[5].0).unwrap());
let after = idx.search(&q, 20);
assert_eq!(after.len(), 18);
for (slot, s) in &after {
let old = before.iter().find(|(os, _)| os == slot);
assert!(old.is_some(), "slot {slot} moved after remove");
assert_eq!(old.unwrap().1.to_bits(), s.to_bits());
}
let bytes = idx.to_bytes();
let disk = VecqIndex::from_bytes(&bytes).unwrap();
assert_eq!(disk.len(), 18);
assert_eq!(idx.search(&q, 20), after, "in-memory results unchanged");
}
#[test]
fn compact_drops_tombstones_and_preserves_results() {
let dim = 64;
let mut idx = VecqIndex::new(dim, 21);
for i in 0..40u64 {
idx.add_keyed(10 * i, &rand_unit(dim, i + 61));
}
for i in 0..20u64 {
assert!(idx.remove_keyed(10 * i));
}
let q = rand_unit(dim, 123);
let expected = idx.search_keyed(&q, 20);
idx.compact();
assert_eq!(idx.tombstones(), 0);
assert_eq!(idx.len(), 20);
assert_eq!(idx.search_keyed(&q, 20), expected);
let bytes = idx.to_bytes();
let back = VecqIndex::from_bytes(&bytes).unwrap();
let reloaded = back.search(&q, 20);
assert_eq!(reloaded.len(), 20);
for ((slot, s), (key, ks)) in reloaded.iter().zip(expected.iter()) {
assert_eq!(idx.key_of(*slot), Some(*key));
assert!(
(s - ks).abs() < 1e-3,
"key {key} score drifted: {s} vs {ks}"
);
}
}
#[test]
fn keyed_search_on_empty_and_drained_index() {
let dim = 32;
let mut idx = VecqIndex::new(dim, 31);
assert!(idx.search_keyed(&rand_unit(dim, 1), 3).is_empty());
idx.add_keyed(1, &rand_unit(dim, 2));
idx.add_keyed(2, &rand_unit(dim, 3));
assert!(idx.remove_keyed(1));
assert!(idx.remove_keyed(2));
assert!(idx.is_empty(), "drained index reports empty");
assert_eq!(idx.tombstones(), 2);
assert!(idx.search_keyed(&rand_unit(dim, 4), 3).is_empty());
}
#[test]
fn keyed_index_from_file_supports_keyed_adds() {
let dim = 64;
let mut idx = VecqIndex::new(dim, 41);
for i in 0..10u64 {
idx.add(&rand_unit(dim, i + 700));
}
let bytes = idx.to_bytes();
let mut back = VecqIndex::from_bytes(&bytes).unwrap();
back.add_keyed(555, &rand_unit(dim, 999));
assert!(back.contains_key(555));
assert_eq!(back.len(), 11);
let q = rand_unit(dim, 999);
assert_eq!(back.search_keyed(&q, 1)[0].0, 555);
}
#[test]
#[should_panic(expected = "vector dim mismatch")]
fn keyed_add_dim_mismatch_panics() {
let mut idx = VecqIndex::new(32, 3);
idx.add_keyed(1, &[0.5; 64]);
}
}