1use crate::lloyd;
5use crate::rhdh::{padded_dim, Rhdh};
6use std::collections::HashMap;
7
8pub struct VecqIndex {
23 pub(crate) dim: usize,
24 working_dim: usize,
30 padded: usize,
31 pub(crate) seed: u64,
32 transform: Rhdh,
33 pub(crate) codes: Vec<u8>, pub(crate) scales: Vec<f32>, pub(crate) n: usize, keys: Vec<Option<u64>>, key_to_slot: HashMap<u64, usize>,
38 key_to_slots: HashMap<u64, Vec<usize>>,
42 signature: Option<Vec<u8>>,
47 alive: Vec<bool>, live: usize, pub(crate) residual: bool,
54 pub(crate) codes2: Vec<u8>, pub(crate) scales2: Vec<f32>, pub(crate) bits: u8,
59}
60
61impl VecqIndex {
62 pub fn new(dim: usize, seed: u64) -> Self {
65 Self::with_working_dim(dim, dim, seed)
66 }
67
68 pub fn with_working_dim(dim: usize, working_dim: usize, seed: u64) -> Self {
78 assert!(
79 working_dim >= 1 && working_dim <= dim,
80 "working_dim must be in 1..={dim}, got {working_dim}"
81 );
82 assert!(
87 working_dim == dim || working_dim <= u16::MAX as usize,
88 "working_dim {working_dim} exceeds the u16 file-format range and does not equal dim"
89 );
90 let padded = padded_dim(working_dim);
91 Self {
92 dim,
93 working_dim,
94 padded,
95 seed,
96 transform: Rhdh::new(working_dim, seed),
97 codes: Vec::new(),
98 scales: Vec::new(),
99 n: 0,
100 keys: Vec::new(),
101 key_to_slot: HashMap::new(),
102 key_to_slots: HashMap::new(),
103 alive: Vec::new(),
104 live: 0,
105 signature: None,
106 residual: false,
107 codes2: Vec::new(),
108 scales2: Vec::new(),
109 bits: 5,
112 }
113 }
114
115 pub fn with_residual(dim: usize, seed: u64) -> Self {
123 let mut idx = Self::with_working_dim(dim, dim, seed);
124 idx.residual = true;
125 idx.bits = 4; idx
127 }
128
129 pub fn set_bits(&mut self, bits: u8) -> &mut Self {
134 assert!(
135 matches!(bits, 4..=6),
136 "unsupported bit width {bits} (supported: 4, 5, 6)"
137 );
138 assert!(self.n == 0, "bit width is frozen once vectors are added");
139 assert!(
140 !(self.residual && bits != 4),
141 "residual mode requires the 4-bit width"
142 );
143 self.bits = bits;
144 self
145 }
146
147 pub fn bits(&self) -> u8 {
149 self.bits
150 }
151
152 pub fn bytes_per_vector(&self) -> usize {
155 self.bpv()
156 }
157
158 fn bpv(&self) -> usize {
159 (self.padded * self.bits as usize).div_ceil(8)
160 }
161
162 pub fn is_residual(&self) -> bool {
164 self.residual
165 }
166
167 pub fn len(&self) -> usize {
169 self.live
170 }
171
172 pub fn is_empty(&self) -> bool {
173 self.live == 0
174 }
175
176 pub fn slots(&self) -> usize {
179 self.n
180 }
181
182 pub fn tombstones(&self) -> usize {
184 self.n - self.live
185 }
186
187 pub fn dim(&self) -> usize {
188 self.dim
189 }
190
191 pub fn working_dim(&self) -> usize {
193 self.working_dim
194 }
195
196 pub fn seed(&self) -> u64 {
197 self.seed
198 }
199
200 #[cfg(test)]
201 pub(crate) fn padded(&self) -> usize {
202 self.padded
203 }
204
205 pub(crate) fn live_slots(&self) -> usize {
208 self.live
209 }
210
211 pub(crate) fn slot_alive(&self, slot: usize) -> bool {
212 self.alive[slot]
213 }
214
215 pub(crate) fn slot_scale(&self, slot: usize) -> f32 {
216 self.scales[slot]
217 }
218
219 pub(crate) fn slot_codes(&self, slot: usize, bpv: usize) -> &[u8] {
220 &self.codes[slot * bpv..(slot + 1) * bpv]
221 }
222
223 pub(crate) fn slot_scale2(&self, slot: usize) -> f32 {
224 self.scales2[slot]
225 }
226
227 pub(crate) fn slot_codes2(&self, slot: usize, bpv: usize) -> &[u8] {
228 &self.codes2[slot * bpv..(slot + 1) * bpv]
229 }
230
231 pub(crate) fn init_dense(&mut self, count: usize) {
234 self.keys = vec![None; count];
235 self.alive = vec![true; count];
236 self.key_to_slot.clear();
237 self.key_to_slots.clear();
238 self.live = count;
239 self.signature = None;
240 }
241
242 pub(crate) fn restore_keys(&mut self, table: Vec<Option<u64>>) {
245 let count = table.len();
246 self.keys = table;
247 self.alive = vec![true; count];
248 self.live = count;
249 self.key_to_slot.clear();
250 self.key_to_slots.clear();
251 for (slot, key) in self.keys.iter().enumerate() {
252 if let Some(key) = *key {
253 if let Some(slots) = self.key_to_slots.get_mut(&key) {
254 slots.push(slot);
255 } else if let Some(first) = self.key_to_slot.remove(&key) {
256 self.key_to_slots.insert(key, vec![first, slot]);
257 } else {
258 self.key_to_slot.insert(key, slot);
259 }
260 }
261 }
262 }
263
264 pub fn add(&mut self, v: &[f32]) -> usize {
268 self.append_slot(v, None)
269 }
270
271 pub fn add_keyed(&mut self, key: u64, v: &[f32]) -> usize {
278 let slot = self.primary_slot(key);
279 match slot {
280 Some(slot) => {
281 self.signature = None;
283 let (scale0, scale2) = self.encode_into(slot * (self.bpv()), v);
284 self.scales[slot] = scale0;
285 if let Some(s2) = scale2 {
286 self.scales2[slot] = s2;
287 }
288 slot
289 }
290 None => {
291 let slot = self.append_slot(v, Some(key));
292 self.key_to_slot.insert(key, slot);
293 slot
294 }
295 }
296 }
297
298 pub fn add_keyed_multi(&mut self, key: u64, v: &[f32]) -> usize {
306 if let Some(&first) = self.key_to_slot.get(&key) {
307 self.key_to_slot.remove(&key);
309 let slot = self.append_slot(v, Some(key));
310 self.key_to_slots.insert(key, vec![first, slot]);
311 return slot;
312 }
313 if self.key_to_slots.contains_key(&key) {
314 let slot = self.append_slot(v, Some(key));
315 self.key_to_slots
316 .get_mut(&key)
317 .expect("key checked above")
318 .push(slot);
319 return slot;
320 }
321 let slot = self.append_slot(v, Some(key));
322 self.key_to_slot.insert(key, slot);
323 slot
324 }
325
326 pub fn relabel(&mut self, old_key: u64, new_key: u64) -> bool {
331 if old_key == new_key {
332 return self.contains_key(old_key);
333 }
334 if self.contains_key(new_key) {
335 return false;
336 }
337 if let Some(slot) = self.key_to_slot.remove(&old_key) {
338 self.key_to_slot.insert(new_key, slot);
339 self.keys[slot] = Some(new_key);
340 return true;
341 }
342 if let Some(slots) = self.key_to_slots.remove(&old_key) {
343 for &slot in &slots {
344 self.keys[slot] = Some(new_key);
345 }
346 self.key_to_slots.insert(new_key, slots);
347 return true;
348 }
349 false
350 }
351
352 pub fn remove_keyed(&mut self, key: u64) -> bool {
357 if let Some(slot) = self.key_to_slot.remove(&key) {
358 self.alive[slot] = false;
359 self.keys[slot] = None;
360 self.live -= 1;
361 return true;
362 }
363 if let Some(slots) = self.key_to_slots.remove(&key) {
364 for slot in slots {
365 self.alive[slot] = false;
366 self.keys[slot] = None;
367 self.live -= 1;
368 }
369 return true;
370 }
371 false
372 }
373
374 pub fn remove_keyed_at(&mut self, key: u64, slot: usize) -> bool {
380 if let Some(&primary) = self.key_to_slot.get(&key) {
381 if primary != slot {
382 return false;
383 }
384 return self.remove_keyed(key);
385 }
386 let Some(slots) = self.key_to_slots.get_mut(&key) else {
387 return false;
388 };
389 let Some(pos) = slots.iter().position(|&s| s == slot) else {
390 return false;
391 };
392 if !self.alive[slot] {
393 return false;
394 }
395 let dead = slots.swap_remove(pos);
396 self.alive[dead] = false;
397 self.keys[dead] = None;
398 self.live -= 1;
399 if slots.len() == 1 {
400 let last = slots[0];
402 self.key_to_slots.remove(&key);
403 self.key_to_slot.insert(key, last);
404 } else if slots.is_empty() {
405 self.key_to_slots.remove(&key);
406 }
407 true
408 }
409
410 fn primary_slot(&self, key: u64) -> Option<usize> {
412 self.key_to_slot
413 .get(&key)
414 .copied()
415 .or_else(|| self.key_to_slots.get(&key).and_then(|s| s.first().copied()))
416 }
417
418 pub fn key_of(&self, slot: usize) -> Option<u64> {
421 self.keys.get(slot).copied().flatten()
422 }
423
424 pub fn contains_key(&self, key: u64) -> bool {
426 self.key_to_slot.contains_key(&key) || self.key_to_slots.contains_key(&key)
427 }
428
429 pub fn enable_cascade(&mut self) {
437 assert!(
438 self.bits == 4,
439 "cascade search requires the 4-bit width (got {}-bit)",
440 self.bits
441 );
442 self.signature = Some(self.derive_signature());
443 }
444
445 pub fn cascade_enabled(&self) -> bool {
447 self.signature.is_some()
448 }
449
450 fn sig_bytes(&self) -> usize {
451 self.padded.div_ceil(4)
452 }
453
454 fn derive_signature(&self) -> Vec<u8> {
456 let bytes_per = self.sig_bytes();
457 let mut sig = vec![0u8; self.n * bytes_per];
458 let bpv = self.bpv();
459 for slot in 0..self.n {
460 let base = slot * bpv;
461 let sig_base = slot * bytes_per;
462 for (i, &byte) in self.codes[base..base + bpv].iter().enumerate() {
463 let lo = (byte & 0x0F) >> 2;
466 let hi = (byte >> 4) >> 2;
467 sig[sig_base + i / 2] |= if i % 2 == 0 {
468 lo | (hi << 2)
469 } else {
470 (lo << 4) | (hi << 6)
471 };
472 }
473 }
474 sig
475 }
476
477 pub fn search_cascade(&self, q: &[f32], k: usize, r: usize) -> Vec<(usize, f32)> {
493 let Some(sig) = &self.signature else {
494 panic!("search_cascade requires enable_cascade() first");
495 };
496 use std::cmp::Reverse;
497 use std::collections::BinaryHeap;
498
499 let bytes_per = self.sig_bytes();
500 let pq = self.prepare_query(q);
501 let k = k.min(self.live).max(1);
502 let r = r.max(k).min(self.live);
503 let rnorm = pq.rnorm;
506 let mut qsig = vec![0u8; bytes_per];
507 for (i, &x) in pq.rotated[..self.padded].iter().enumerate() {
508 let c2 = lloyd::quantize_4bit(x * rnorm) >> 2;
509 qsig[i / 4] |= c2 << ((i % 4) * 2);
510 }
511 let pair: [u8; 16] = core::array::from_fn(|i| {
513 let (qa, da) = ((i >> 2) as u32, (i & 3) as u32);
514 qa.abs_diff(da) as u8
515 });
516
517 let mut heap: BinaryHeap<(u32, usize)> = BinaryHeap::with_capacity(r + 1);
521 for slot in 0..self.n {
522 if !self.alive[slot] {
523 continue;
524 }
525 let base = slot * bytes_per;
526 let mut d = 0u32;
527 for (j, &db) in sig[base..base + bytes_per].iter().enumerate() {
528 let qb = qsig[j];
529 d += (pair[(qb & 0x03) as usize * 4 + (db & 0x03) as usize]
530 + pair[((qb >> 2) & 0x03) as usize * 4 + ((db >> 2) & 0x03) as usize]
531 + pair[((qb >> 4) & 0x03) as usize * 4 + ((db >> 4) & 0x03) as usize]
532 + pair[((qb >> 6) & 0x03) as usize * 4 + ((db >> 6) & 0x03) as usize])
533 as u32;
534 }
535 if heap.len() < r {
536 heap.push((d, slot));
537 } else if d < heap.peek().map(|e| e.0).unwrap_or(u32::MAX) {
538 heap.push((d, slot));
539 heap.pop();
540 }
541 }
542 let mut candidates: Vec<usize> = heap.into_iter().map(|(_, s)| s).collect();
543 candidates.sort_unstable();
544
545 let key = |s: f32| -> u32 {
548 let b = s.to_bits();
549 if b & 0x8000_0000 != 0 {
550 !b
551 } else {
552 b ^ 0x8000_0000
553 }
554 };
555 let mut top: BinaryHeap<Reverse<(u32, usize)>> = BinaryHeap::with_capacity(k + 1);
556 let consider = |s: f32, slot: usize, top: &mut BinaryHeap<Reverse<(u32, usize)>>| {
557 let ks = key(s);
558 if top.len() < k {
559 top.push(Reverse((ks, slot)));
560 } else if ks > top.peek().map(|e| e.0 .0).unwrap_or(0) {
561 top.push(Reverse((ks, slot)));
562 top.pop();
563 }
564 };
565 for slot in candidates {
566 consider(self.score(&pq, slot), slot, &mut top);
567 }
568 let key_undo = |k: u32| -> u32 {
569 if k & 0x8000_0000 != 0 {
570 k ^ 0x8000_0000 } else {
572 !k }
574 };
575 let mut out: Vec<(usize, f32)> = top
576 .into_iter()
577 .map(|e| (e.0 .1, f32::from_bits(key_undo(e.0 .0))))
578 .collect();
579 out.sort_by(|a, b| b.1.partial_cmp(&a.1).expect("no NaN scores"));
580 out
581 }
582
583 pub fn compact(&mut self) {
588 if self.live == self.n {
589 return;
590 }
591 self.signature = None;
593 let bpv = self.bpv();
594 let mut codes = Vec::with_capacity(self.live * bpv);
595 let mut scales = Vec::with_capacity(self.live);
596 let mut alive = Vec::with_capacity(self.live);
597 let mut new_keys: Vec<Option<u64>> = Vec::with_capacity(self.live);
598 for slot in 0..self.n {
599 if self.alive[slot] {
600 codes.extend_from_slice(&self.codes[slot * bpv..(slot + 1) * bpv]);
601 scales.push(self.scales[slot]);
602 new_keys.push(self.keys[slot]);
603 alive.push(true);
604 }
605 }
606 self.key_to_slot.clear();
607 self.key_to_slots.clear();
608 for (new_slot, key) in new_keys.iter().enumerate() {
609 if let Some(key) = key {
610 if let Some(slots) = self.key_to_slots.get_mut(key) {
611 slots.push(new_slot);
613 } else if let Some(first) = self.key_to_slot.remove(key) {
614 self.key_to_slots.insert(*key, vec![first, new_slot]);
616 } else {
617 self.key_to_slot.insert(*key, new_slot);
618 }
619 }
620 }
621 self.codes = codes;
622 self.scales = scales;
623 self.keys = new_keys;
624 self.alive = alive;
625 self.n = self.live;
626 }
627
628 fn append_slot(&mut self, v: &[f32], key: Option<u64>) -> usize {
633 self.signature = None; let slot = self.n;
635 let (scale0, scale2) = self.encode_into(slot * (self.bpv()), v);
636 self.scales.push(scale0);
637 if let Some(s2) = scale2 {
638 self.scales2.push(s2);
639 }
640 self.keys.push(key);
641 self.alive.push(true);
642 self.n += 1;
643 self.live += 1;
644 slot
645 }
646
647 fn encode_into(&mut self, base: usize, v: &[f32]) -> (f32, Option<f32>) {
650 assert_eq!(v.len(), self.dim, "vector dim mismatch");
651 let norm: f32 = v[..self.working_dim]
654 .iter()
655 .map(|x| x * x)
656 .sum::<f32>()
657 .sqrt();
658 assert!(norm > 0.0, "zero vector");
659 let unit: Vec<f32> = v[..self.working_dim].iter().map(|x| x / norm).collect();
660
661 let mut rotated = Vec::with_capacity(self.padded);
662 self.transform.apply(&unit, &mut rotated);
663
664 let bits = self.bits;
666 let bytes_per_vec = self.bpv();
667 if self.codes.len() < base + bytes_per_vec {
668 self.codes.resize(base + bytes_per_vec, 0);
669 }
670 let mut sum_sq = 0f32;
671 let mut residual_buf = Vec::new();
672 for (i, &x) in rotated.iter().enumerate() {
673 let code = lloyd::quantize(x, bits);
674 pack_code(&mut self.codes, base, i, bits, code);
675 let d = lloyd::dequantize(code, bits);
676 sum_sq += d.powi(2);
677 if self.residual {
678 if residual_buf.is_empty() {
679 residual_buf.resize(self.padded, 0.0);
680 }
681 residual_buf[i] = x - d;
682 }
683 }
684
685 let scale0 = 1.0 / sum_sq.sqrt();
688 if !self.residual {
689 return (scale0, None);
690 }
691
692 let rms: f32 = (residual_buf.iter().map(|x| x * x).sum::<f32>() / self.padded as f32)
696 .max(1e-12)
697 .sqrt();
698 if self.codes2.len() < base + bytes_per_vec {
699 self.codes2.resize(base + bytes_per_vec, 0);
700 }
701 for (i, &r) in residual_buf.iter().enumerate() {
702 let code = lloyd::quantize(r / rms, bits);
703 pack_code(&mut self.codes2, base, i, bits, code);
704 }
705 let mut cross = 0f32; let mut norm1_sq = 0f32; for i in 0..self.padded {
724 let b = base + i / 2;
725 let (c0, c1) = if i % 2 == 0 {
726 (self.codes[b] & 0x0F, self.codes2[b] & 0x0F)
727 } else {
728 (self.codes[b] >> 4, self.codes2[b] >> 4)
729 };
730 let d0 = lloyd::dequantize_4bit(c0);
731 let d1 = lloyd::dequantize_4bit(c1);
732 cross += d0 * d1;
733 norm1_sq += d1 * d1;
734 }
735 let norm_sq = sum_sq + 2.0 * rms * cross + rms * rms * norm1_sq;
736 let denom = norm_sq.max(1e-12).sqrt();
737 ((scale0 * sum_sq.sqrt()) / denom, Some(rms / denom))
738 }
739
740 pub fn prepare_query(&self, q: &[f32]) -> PreparedQuery {
742 assert_eq!(q.len(), self.dim);
743 let norm: f32 = q[..self.working_dim]
745 .iter()
746 .map(|x| x * x)
747 .sum::<f32>()
748 .sqrt();
749 assert!(norm > 0.0, "zero vector");
750 let unit: Vec<f32> = q[..self.working_dim].iter().map(|x| x / norm).collect();
751 let mut rotated = Vec::with_capacity(self.padded);
752 self.transform.apply(&unit, &mut rotated);
753 let rnorm: f32 = rotated.iter().map(|x| x * x).sum::<f32>().sqrt();
756 for x in rotated.iter_mut() {
757 *x /= rnorm;
758 }
759 let mut lut = [0f32; 16];
762 for (c, slot) in lut.iter_mut().enumerate() {
763 *slot = lloyd::dequantize_4bit(c as u8);
764 }
765 PreparedQuery {
766 rotated,
767 lut,
768 norm,
769 rnorm,
770 }
771 }
772
773 #[inline]
784 pub fn score(&self, pq: &PreparedQuery, idx: usize) -> f32 {
785 let bits = self.bits;
786 let base = idx * self.bpv();
787 let codes = &self.codes[base..base + self.bpv()];
788 let q = &pq.rotated[..self.padded];
789 let raw0 = self.score_raw(codes, q, &pq.lut, bits);
790 if !self.residual {
791 return raw0 * self.scales[idx];
792 }
793 let codes1 = &self.codes2[base..base + self.bpv()];
796 let raw1 = self.score_raw(codes1, q, &pq.lut, bits);
797 raw0 * self.scales[idx] + raw1 * self.scales2[idx]
798 }
799
800 #[inline]
803 fn score_raw(&self, codes: &[u8], q: &[f32], lut: &[f32; 16], bits: u8) -> f32 {
804 score_raw_dispatch(codes, q, lut, bits)
805 }
806
807 pub fn search(&self, q: &[f32], k: usize) -> Vec<(usize, f32)> {
814 self.search_slots(q, k)
815 }
816
817 pub fn search_keyed(&self, q: &[f32], k: usize) -> Vec<(u64, f32)> {
822 let mut seen = std::collections::HashSet::new();
823 self.search_slots(q, k)
824 .into_iter()
825 .filter_map(|(slot, s)| self.key_of(slot).map(|key| (key, s)))
826 .filter(|(key, _)| seen.insert(*key))
827 .collect()
828 }
829
830 fn search_slots(&self, q: &[f32], k: usize) -> Vec<(usize, f32)> {
831 use std::cmp::Reverse;
832 use std::collections::BinaryHeap;
833
834 let pq = self.prepare_query(q);
835 let k = k.min(self.live).max(1);
836 let bpv = self.bpv();
837 let key = |s: f32| -> u32 {
840 let b = s.to_bits();
841 if b & 0x8000_0000 != 0 {
842 !b
843 } else {
844 b ^ 0x8000_0000
845 }
846 };
847 let mut heap: BinaryHeap<Reverse<(u32, usize)>> = BinaryHeap::with_capacity(k + 1);
848 let consider = |s: f32, idx: usize, heap: &mut BinaryHeap<Reverse<(u32, usize)>>| {
849 let ks = key(s);
850 if heap.len() < k {
851 heap.push(Reverse((ks, idx)));
852 } else if ks > heap.peek().map(|r| r.0 .0).unwrap_or(0) {
853 heap.push(Reverse((ks, idx)));
854 heap.pop();
855 }
856 };
857 #[cfg(target_arch = "aarch64")]
858 let q_rot = &pq.rotated[..self.padded];
859 #[cfg(target_arch = "x86_64")]
860 let use_avx2 = avx2::available();
861 let mut idx = 0;
862 let combine = |r0: f32, r1: Option<f32>, si: usize| -> f32 {
866 match r1 {
867 Some(r1) => r0 * self.scales[si] + r1 * self.scales2[si],
868 None => r0 * self.scales[si],
869 }
870 };
871 #[cfg(target_arch = "aarch64")]
872 {
873 while idx + 4 <= self.n {
879 let codes4 = &self.codes[idx * bpv..(idx + 4) * bpv];
880 let raw: [f32; 4] = if self.bits == 4 {
881 unsafe { neon::score_neon4(codes4, q_rot, &pq.lut) }
882 } else {
883 unsafe { neon::score_neon_wide4(codes4, q_rot, self.bits) }
884 };
885 let raw1 = if self.residual {
886 Some(unsafe {
887 neon::score_neon4(&self.codes2[idx * bpv..(idx + 4) * bpv], q_rot, &pq.lut)
888 })
889 } else {
890 None
891 };
892 for (v, &r) in raw.iter().enumerate() {
893 let si = idx + v;
894 if self.alive[si] {
895 consider(combine(r, raw1.map(|a| a[v]), si), si, &mut heap);
896 }
897 }
898 idx += 4;
899 }
900 }
901 #[cfg(target_arch = "x86_64")]
902 {
903 if use_avx2 && self.bits == 4 {
904 while idx + 4 <= self.n {
909 let codes4 = &self.codes[idx * bpv..(idx + 4) * bpv];
910 let raw =
912 unsafe { avx2::score_avx24(codes4, &pq.rotated[..self.padded], &pq.lut) };
913 let raw1 = if self.residual {
914 Some(unsafe {
915 avx2::score_avx24(
916 &self.codes2[idx * bpv..(idx + 4) * bpv],
917 &pq.rotated[..self.padded],
918 &pq.lut,
919 )
920 })
921 } else {
922 None
923 };
924 for (v, &r) in raw.iter().enumerate() {
925 let si = idx + v;
926 if self.alive[si] {
927 consider(combine(r, raw1.map(|a| a[v]), si), si, &mut heap);
928 }
929 }
930 idx += 4;
931 }
932 }
933 }
934 while idx < self.n {
935 if self.alive[idx] {
936 consider(self.score(&pq, idx), idx, &mut heap);
937 }
938 idx += 1;
939 }
940 let key_undo = |k: u32| -> u32 {
942 if k & 0x8000_0000 != 0 {
943 k ^ 0x8000_0000 } else {
945 !k }
947 };
948 let mut out: Vec<(usize, f32)> = heap
949 .into_iter()
950 .map(|r| (r.0 .1, f32::from_bits(key_undo(r.0 .0))))
951 .collect();
952 out.sort_by(|a, b| b.1.partial_cmp(&a.1).expect("no NaN scores"));
953 out
954 }
955}
956
957pub(crate) fn score_batch4(codes4: &[u8], q_rot: &[f32], lut: &[f32; 16], bits: u8) -> [f32; 4] {
962 #[cfg(target_arch = "aarch64")]
963 {
964 if bits == 4 {
965 return unsafe { neon::score_neon4(codes4, q_rot, lut) };
967 }
968 unsafe { neon::score_neon_wide4(codes4, q_rot, bits) }
970 }
971 #[cfg(target_arch = "x86_64")]
972 {
973 if avx2::available() && bits == 4 {
974 return unsafe { avx2::score_avx24(codes4, q_rot, lut) };
976 }
977 let nb = codes4.len() / 4;
978 let mut out = [0f32; 4];
979 for v in 0..4 {
980 out[v] = score_raw_dispatch(&codes4[v * nb..(v + 1) * nb], q_rot, lut, bits);
981 }
982 out
983 }
984 #[cfg(not(any(target_arch = "aarch64", target_arch = "x86_64")))]
985 {
986 let nb = codes4.len() / 4;
987 let mut out = [0f32; 4];
988 for v in 0..4 {
989 out[v] = score_raw_dispatch(&codes4[v * nb..(v + 1) * nb], q_rot, lut, bits);
990 }
991 out
992 }
993}
994
995pub(crate) fn score_raw_dispatch(codes: &[u8], q: &[f32], lut: &[f32; 16], bits: u8) -> f32 {
999 if bits != 4 {
1000 return score_wide_scalar(codes, q, bits);
1001 }
1002 #[cfg(target_arch = "aarch64")]
1003 {
1004 unsafe { neon::score_neon(codes, q, lut) }
1006 }
1007 #[cfg(target_arch = "x86_64")]
1008 {
1009 if avx2::available() {
1010 unsafe { avx2::score_avx2(codes, q, lut) }
1012 } else {
1013 score_scalar(codes, q, lut)
1014 }
1015 }
1016 #[cfg(not(any(target_arch = "aarch64", target_arch = "x86_64")))]
1017 {
1018 score_scalar(codes, q, lut)
1019 }
1020}
1021
1022#[inline]
1027fn pack_code(bytes: &mut [u8], base: usize, i: usize, bits: u8, code: u8) {
1028 let w = bits as usize;
1029 let bit_off = i * w;
1030 let b = base + bit_off / 8;
1031 let shift = (bit_off % 8) as u32;
1032 let hi = bytes.get(b + 1).copied().unwrap_or(0);
1033 let window = u16::from_le_bytes([bytes[b], hi]);
1034 let mask = ((1u16 << w) - 1) << shift;
1035 let out = (window & !mask) | ((code as u16) << shift);
1036 bytes[b] = out as u8;
1037 if let Some(next) = bytes.get_mut(b + 1) {
1038 *next = (out >> 8) as u8;
1039 }
1040}
1041
1042#[inline]
1045fn unpack_code(bytes: &[u8], i: usize, bits: u8) -> u8 {
1046 let w = bits as usize;
1047 let bit_off = i * w;
1048 let b = bit_off / 8;
1049 let shift = (bit_off % 8) as u32;
1050 let hi = bytes.get(b + 1).copied().unwrap_or(0);
1051 let window = u16::from_le_bytes([bytes[b], hi]);
1052 ((window >> shift) & ((1u16 << w) - 1)) as u8
1053}
1054
1055fn score_wide_scalar(codes: &[u8], q: &[f32], bits: u8) -> f32 {
1059 let centroids = lloyd::centroids(bits);
1060 let mut acc = [0f32; 4];
1064 for (i, &qi) in q.iter().enumerate() {
1065 let c = unpack_code(codes, i, bits) as usize;
1066 acc[i & 3] += qi * centroids[c];
1067 }
1068 let s01 = acc[0] + acc[1];
1069 let s23 = acc[2] + acc[3];
1070 s01 + s23
1071}
1072
1073#[cfg_attr(target_arch = "aarch64", cfg(test))]
1076pub(crate) fn score_scalar(codes: &[u8], q: &[f32], lut: &[f32; 16]) -> f32 {
1077 let nb = codes.len();
1078 let mut acc = [0f32; 8];
1079 let mut i = 0;
1080 while i + 8 <= nb {
1081 for j in 0..8 {
1082 let b = codes[i + j];
1083 let c = (i + j) * 2;
1084 acc[j] += q[c] * lut[(b & 0x0F) as usize] + q[c + 1] * lut[(b >> 4) as usize];
1085 }
1086 i += 8;
1087 }
1088 let mut tail = 0f32;
1089 while i < nb {
1090 let b = codes[i];
1091 let c = i * 2;
1092 tail += q[c] * lut[(b & 0x0F) as usize] + q[c + 1] * lut[(b >> 4) as usize];
1093 i += 1;
1094 }
1095 let s01 = acc[0] + acc[1];
1096 let s23 = acc[2] + acc[3];
1097 let s45 = acc[4] + acc[5];
1098 let s67 = acc[6] + acc[7];
1099 (s01 + s23) + (s45 + s67) + tail
1100}
1101
1102#[cfg(target_arch = "aarch64")]
1103mod neon {
1104 use std::arch::aarch64::*;
1117
1118 #[inline]
1124 unsafe fn gather16(tbl: uint8x16x4_t, nibbles: uint8x16_t) -> [float32x4_t; 4] {
1125 let idx = vmulq_u8(nibbles, vdupq_n_u8(4)); let one = vdupq_n_u8(1);
1127 let two = vdupq_n_u8(2);
1128 let b0 = vqtbl4q_u8(tbl, idx);
1129 let b1 = vqtbl4q_u8(tbl, vaddq_u8(idx, one));
1130 let b2 = vqtbl4q_u8(tbl, vaddq_u8(idx, two));
1131 let b3 = vqtbl4q_u8(tbl, vaddq_u8(idx, vdupq_n_u8(3)));
1132 let z01 = vzip1q_u8(b0, b1); let z23 = vzip1q_u8(b2, b3); let z01b = vzip2q_u8(b0, b1);
1136 let z23b = vzip2q_u8(b2, b3);
1137 let lo16 = vreinterpretq_u16_u8(z01);
1138 let hi16 = vreinterpretq_u16_u8(z23);
1139 let lo16b = vreinterpretq_u16_u8(z01b);
1140 let hi16b = vreinterpretq_u16_u8(z23b);
1141 [
1142 vreinterpretq_f32_u8(vreinterpretq_u8_u16(vzip1q_u16(lo16, hi16))),
1143 vreinterpretq_f32_u8(vreinterpretq_u8_u16(vzip2q_u16(lo16, hi16))),
1144 vreinterpretq_f32_u8(vreinterpretq_u8_u16(vzip1q_u16(lo16b, hi16b))),
1145 vreinterpretq_f32_u8(vreinterpretq_u8_u16(vzip2q_u16(lo16b, hi16b))),
1146 ]
1147 }
1148
1149 #[inline]
1151 pub unsafe fn score_neon(codes: &[u8], q: &[f32], lut: &[f32; 16]) -> f32 {
1152 let mut bytes = [0u8; 64];
1154 for (c, &v) in lut.iter().enumerate() {
1155 bytes[c * 4..c * 4 + 4].copy_from_slice(&v.to_le_bytes());
1156 }
1157 let tbl = uint8x16x4_t(
1158 vld1q_u8(bytes[0..16].as_ptr()),
1159 vld1q_u8(bytes[16..32].as_ptr()),
1160 vld1q_u8(bytes[32..48].as_ptr()),
1161 vld1q_u8(bytes[48..64].as_ptr()),
1162 );
1163 let mut acc_lo = vdupq_n_f32(0.0);
1165 let mut acc_hi = vdupq_n_f32(0.0);
1166 let nb = codes.len();
1167 let mut i = 0;
1168 while i + 8 <= nb {
1169 let b8 = vld1_u8(codes.as_ptr().add(i)); let lo = vand_u8(b8, vdup_n_u8(0x0F));
1173 let hi = vshr_n_u8(b8, 4);
1174 let nibbles = vcombine_u8(lo, hi); let g = gather16(tbl, nibbles);
1176 let q0 = vld1q_f32(q.as_ptr().add(i * 2));
1180 let q1 = vld1q_f32(q.as_ptr().add(i * 2 + 4));
1181 let q2 = vld1q_f32(q.as_ptr().add(i * 2 + 8));
1182 let q3 = vld1q_f32(q.as_ptr().add(i * 2 + 12));
1183 let q_even_lo = vuzp1q_f32(q0, q1); let q_even_hi = vuzp1q_f32(q2, q3);
1185 let q_odd_lo = vuzp2q_f32(q0, q1);
1186 let q_odd_hi = vuzp2q_f32(q2, q3);
1187 let t_lo = vaddq_f32(vmulq_f32(q_even_lo, g[0]), vmulq_f32(q_odd_lo, g[2]));
1189 let t_hi = vaddq_f32(vmulq_f32(q_even_hi, g[1]), vmulq_f32(q_odd_hi, g[3]));
1190 acc_lo = vaddq_f32(acc_lo, t_lo);
1191 acc_hi = vaddq_f32(acc_hi, t_hi);
1192 i += 8;
1193 }
1194 let mut acc = [0f32; 8];
1196 acc[0] = vgetq_lane_f32(acc_lo, 0);
1197 acc[1] = vgetq_lane_f32(acc_lo, 1);
1198 acc[2] = vgetq_lane_f32(acc_lo, 2);
1199 acc[3] = vgetq_lane_f32(acc_lo, 3);
1200 acc[4] = vgetq_lane_f32(acc_hi, 0);
1201 acc[5] = vgetq_lane_f32(acc_hi, 1);
1202 acc[6] = vgetq_lane_f32(acc_hi, 2);
1203 acc[7] = vgetq_lane_f32(acc_hi, 3);
1204 let mut tail = 0f32;
1207 while i < nb {
1208 let b = codes[i];
1209 let c = i * 2;
1210 tail += q[c] * lut[(b & 0x0F) as usize] + q[c + 1] * lut[(b >> 4) as usize];
1211 i += 1;
1212 }
1213 let s01 = acc[0] + acc[1];
1214 let s23 = acc[2] + acc[3];
1215 let s45 = acc[4] + acc[5];
1216 let s67 = acc[6] + acc[7];
1217 (s01 + s23) + (s45 + s67) + tail
1218 }
1219
1220 #[inline]
1226 pub unsafe fn score_neon4(codes4: &[u8], q: &[f32], lut: &[f32; 16]) -> [f32; 4] {
1227 let mut bytes = [0u8; 64];
1228 for (c, &v) in lut.iter().enumerate() {
1229 bytes[c * 4..c * 4 + 4].copy_from_slice(&v.to_le_bytes());
1230 }
1231 let tbl = uint8x16x4_t(
1232 vld1q_u8(bytes[0..16].as_ptr()),
1233 vld1q_u8(bytes[16..32].as_ptr()),
1234 vld1q_u8(bytes[32..48].as_ptr()),
1235 vld1q_u8(bytes[48..64].as_ptr()),
1236 );
1237 let nb = codes4.len() / 4; let mut acc_lo = [vdupq_n_f32(0.0); 4];
1239 let mut acc_hi = [vdupq_n_f32(0.0); 4];
1240 let mut i = 0;
1241 while i + 8 <= nb {
1242 let q0 = vld1q_f32(q.as_ptr().add(i * 2));
1244 let q1 = vld1q_f32(q.as_ptr().add(i * 2 + 4));
1245 let q2 = vld1q_f32(q.as_ptr().add(i * 2 + 8));
1246 let q3 = vld1q_f32(q.as_ptr().add(i * 2 + 12));
1247 let q_even_lo = vuzp1q_f32(q0, q1);
1248 let q_even_hi = vuzp1q_f32(q2, q3);
1249 let q_odd_lo = vuzp2q_f32(q0, q1);
1250 let q_odd_hi = vuzp2q_f32(q2, q3);
1251 for v in 0..4 {
1252 let b8 = vld1_u8(codes4.as_ptr().add(v * nb + i));
1253 let lo = vand_u8(b8, vdup_n_u8(0x0F));
1254 let hi = vshr_n_u8(b8, 4);
1255 let nibbles = vcombine_u8(lo, hi);
1256 let g = gather16(tbl, nibbles);
1257 let t_lo = vaddq_f32(vmulq_f32(q_even_lo, g[0]), vmulq_f32(q_odd_lo, g[2]));
1258 let t_hi = vaddq_f32(vmulq_f32(q_even_hi, g[1]), vmulq_f32(q_odd_hi, g[3]));
1259 acc_lo[v] = vaddq_f32(acc_lo[v], t_lo);
1260 acc_hi[v] = vaddq_f32(acc_hi[v], t_hi);
1261 }
1262 i += 8;
1263 }
1264 let mut out = [0f32; 4];
1265 for v in 0..4 {
1266 let mut a = [0f32; 8];
1268 a[0] = vgetq_lane_f32(acc_lo[v], 0);
1269 a[1] = vgetq_lane_f32(acc_lo[v], 1);
1270 a[2] = vgetq_lane_f32(acc_lo[v], 2);
1271 a[3] = vgetq_lane_f32(acc_lo[v], 3);
1272 a[4] = vgetq_lane_f32(acc_hi[v], 0);
1273 a[5] = vgetq_lane_f32(acc_hi[v], 1);
1274 a[6] = vgetq_lane_f32(acc_hi[v], 2);
1275 a[7] = vgetq_lane_f32(acc_hi[v], 3);
1276 let mut tail = 0f32;
1278 let mut j = i;
1279 while j < nb {
1280 let b = codes4[v * nb + j];
1281 let c = j * 2;
1282 tail += q[c] * lut[(b & 0x0F) as usize] + q[c + 1] * lut[(b >> 4) as usize];
1283 j += 1;
1284 }
1285 let s01 = a[0] + a[1];
1286 let s23 = a[2] + a[3];
1287 let s45 = a[4] + a[5];
1288 let s67 = a[6] + a[7];
1289 out[v] = (s01 + s23) + (s45 + s67) + tail;
1290 }
1291 out
1292 }
1293
1294 pub unsafe fn score_neon_wide4(codes4: &[u8], q: &[f32], bits: u8) -> [f32; 4] {
1309 debug_assert!(matches!(bits, 5..=6));
1310 use super::unpack_code;
1311 let centroids = crate::lloyd::centroids(bits);
1312 let nb = codes4.len() / 4;
1313 let n_dims = nb * 8 / bits as usize; let w = bits as u64;
1315 let mask = (1u64 << w) - 1;
1316 let mut acc = [vdupq_n_f32(0.0); 4];
1317 let mut i = 0;
1318 while i + 8 <= n_dims {
1319 #[allow(clippy::needless_range_loop)] for v in 0..4 {
1321 let base = v * nb;
1322 let b0 = i * bits as usize;
1323 let off = base + b0 / 8;
1324 let need = (b0 % 8 + 8 * bits as usize).div_ceil(8);
1328 let mut wb = [0u8; 8];
1329 wb[..need].copy_from_slice(&codes4[off..off + need]);
1330 let win = u64::from_le_bytes(wb);
1331 let s0 = (b0 % 8) as u64;
1332 let mut g = [0f32; 8];
1333 #[allow(clippy::needless_range_loop)] for l in 0..8usize {
1335 let c = ((win >> (s0 + l as u64 * w)) & mask) as usize;
1336 debug_assert_eq!(c, unpack_code(&codes4[base..], i + l, bits) as usize);
1337 g[l] = centroids[c];
1338 }
1339 let gv = vld1q_f32(g.as_ptr());
1340 let gv2 = vld1q_f32(g.as_ptr().add(4));
1341 let qv = vld1q_f32(q.as_ptr().add(i));
1342 let qv2 = vld1q_f32(q.as_ptr().add(i + 4));
1343 acc[v] = vaddq_f32(acc[v], vmulq_f32(qv, gv));
1344 acc[v] = vaddq_f32(acc[v], vmulq_f32(qv2, gv2));
1345 }
1346 i += 8;
1347 }
1348 let mut lanes = [[0f32; 4]; 4];
1351 for v in 0..4 {
1352 lanes[v] = [
1353 vgetq_lane_f32(acc[v], 0),
1354 vgetq_lane_f32(acc[v], 1),
1355 vgetq_lane_f32(acc[v], 2),
1356 vgetq_lane_f32(acc[v], 3),
1357 ];
1358 }
1359 while i < n_dims {
1360 for v in 0..4 {
1361 let c = unpack_code(&codes4[v * nb..], i, bits) as usize;
1362 lanes[v][i & 3] += q[i] * centroids[c];
1363 }
1364 i += 1;
1365 }
1366 let mut out = [0f32; 4];
1367 for v in 0..4 {
1368 let s01 = lanes[v][0] + lanes[v][1];
1369 let s23 = lanes[v][2] + lanes[v][3];
1370 out[v] = s01 + s23;
1371 }
1372 out
1373 }
1374}
1375
1376#[cfg(target_arch = "x86_64")]
1390mod avx2 {
1391 use std::arch::x86_64::*;
1392
1393 pub fn available() -> bool {
1395 std::is_x86_feature_detected!("avx2")
1396 }
1397
1398 #[inline]
1400 unsafe fn gather8(lut: &[f32; 16], nibbles: __m128i) -> __m256 {
1401 let idx = _mm256_cvtepu8_epi32(nibbles);
1404 _mm256_i32gather_ps(lut.as_ptr(), idx, 4)
1405 }
1406
1407 #[inline]
1410 unsafe fn deinterleave16(q: *const f32) -> (__m256, __m256) {
1411 let qa = _mm256_loadu_ps(q);
1412 let qb = _mm256_loadu_ps(q.add(8));
1413 let fixup = _mm256_setr_epi32(0, 1, 4, 5, 2, 3, 6, 7);
1417 let even = _mm256_permutevar8x32_ps(_mm256_shuffle_ps(qa, qb, 0x88), fixup);
1418 let odd = _mm256_permutevar8x32_ps(_mm256_shuffle_ps(qa, qb, 0xDD), fixup);
1419 (even, odd)
1420 }
1421
1422 #[inline]
1424 #[target_feature(enable = "avx2")]
1425 pub unsafe fn score_avx2(codes: &[u8], q: &[f32], lut: &[f32; 16]) -> f32 {
1426 let mut acc = _mm256_setzero_ps();
1427 let nb = codes.len();
1428 let mut i = 0;
1429 while i + 8 <= nb {
1430 let b8 = _mm_loadl_epi64(codes.as_ptr().add(i) as *const __m128i);
1431 let g_lo = gather8(lut, _mm_and_si128(b8, _mm_set1_epi8(0x0F)));
1432 let g_hi = gather8(
1433 lut,
1434 _mm_and_si128(_mm_srli_epi16(b8, 4), _mm_set1_epi8(0x0F)),
1435 );
1436 let (even, odd) = deinterleave16(q.as_ptr().add(i * 2));
1437 let term = _mm256_add_ps(_mm256_mul_ps(even, g_lo), _mm256_mul_ps(odd, g_hi));
1439 acc = _mm256_add_ps(acc, term);
1440 i += 8;
1441 }
1442 let mut a = [0f32; 8];
1444 _mm256_storeu_ps(a.as_mut_ptr(), acc);
1445 let mut tail = 0f32;
1448 while i < nb {
1449 let b = codes[i];
1450 let c = i * 2;
1451 tail += q[c] * lut[(b & 0x0F) as usize] + q[c + 1] * lut[(b >> 4) as usize];
1452 i += 1;
1453 }
1454 let s01 = a[0] + a[1];
1455 let s23 = a[2] + a[3];
1456 let s45 = a[4] + a[5];
1457 let s67 = a[6] + a[7];
1458 (s01 + s23) + (s45 + s67) + tail
1459 }
1460
1461 #[inline]
1466 #[target_feature(enable = "avx2")]
1467 pub unsafe fn score_avx24(codes4: &[u8], q: &[f32], lut: &[f32; 16]) -> [f32; 4] {
1468 let nb = codes4.len() / 4; let mut acc = [_mm256_setzero_ps(); 4];
1470 let mut i = 0;
1471 while i + 8 <= nb {
1472 let (even, odd) = deinterleave16(q.as_ptr().add(i * 2));
1474 for (v, acc_v) in acc.iter_mut().enumerate() {
1475 let b8 = _mm_loadl_epi64(codes4.as_ptr().add(v * nb + i) as *const __m128i);
1476 let g_lo = gather8(lut, _mm_and_si128(b8, _mm_set1_epi8(0x0F)));
1477 let g_hi = gather8(
1478 lut,
1479 _mm_and_si128(_mm_srli_epi16(b8, 4), _mm_set1_epi8(0x0F)),
1480 );
1481 let term = _mm256_add_ps(_mm256_mul_ps(even, g_lo), _mm256_mul_ps(odd, g_hi));
1482 *acc_v = _mm256_add_ps(*acc_v, term);
1483 }
1484 i += 8;
1485 }
1486 let mut out = [0f32; 4];
1487 for v in 0..4 {
1488 let mut a = [0f32; 8];
1489 _mm256_storeu_ps(a.as_mut_ptr(), acc[v]);
1490 let mut tail = 0f32;
1492 let mut j = i;
1493 while j < nb {
1494 let b = codes4[v * nb + j];
1495 let c = j * 2;
1496 tail += q[c] * lut[(b & 0x0F) as usize] + q[c + 1] * lut[(b >> 4) as usize];
1497 j += 1;
1498 }
1499 let s01 = a[0] + a[1];
1500 let s23 = a[2] + a[3];
1501 let s45 = a[4] + a[5];
1502 let s67 = a[6] + a[7];
1503 out[v] = (s01 + s23) + (s45 + s67) + tail;
1504 }
1505 out
1506 }
1507}
1508
1509pub struct PreparedQuery {
1511 rotated: Vec<f32>,
1512 lut: [f32; 16],
1513 #[allow(dead_code)]
1514 norm: f32,
1515 rnorm: f32,
1519}
1520
1521pub fn cosine_f32(a: &[f32], b: &[f32]) -> f32 {
1523 let mut dot = 0f32;
1524 let mut na = 0f32;
1525 let mut nb = 0f32;
1526 for i in 0..a.len() {
1527 dot += a[i] * b[i];
1528 na += a[i] * a[i];
1529 nb += b[i] * b[i];
1530 }
1531 dot / (na.sqrt() * nb.sqrt())
1532}
1533
1534#[cfg(test)]
1535mod tests {
1536 use super::*;
1537
1538 fn rand_unit(dim: usize, seed: u64) -> Vec<f32> {
1539 let mut x = seed | 1;
1541 let mut v = Vec::with_capacity(dim);
1542 for _ in 0..dim {
1543 x ^= x << 13;
1544 x ^= x >> 7;
1545 x ^= x << 17;
1546 let u1 = ((x >> 11) as f64 / (1u64 << 53) as f64).max(1e-12);
1547 x ^= x << 16;
1548 let u2 = (x >> 11) as f64 / (1u64 << 53) as f64;
1549 v.push(((-2.0 * u1.ln()).sqrt() * (std::f64::consts::TAU * u2).cos()) as f32);
1550 }
1551 let norm: f32 = v.iter().map(|a| a * a).sum::<f32>().sqrt();
1552 v.into_iter().map(|a| a / norm).collect()
1553 }
1554
1555 #[test]
1556 fn score_correlates_with_exact_cosine() {
1557 let dim = 128;
1558 let mut idx = VecqIndex::new(dim, 7);
1559 let base: Vec<Vec<f32>> = (0..200).map(|i| rand_unit(dim, i + 1)).collect();
1560 for v in &base {
1561 idx.add(v);
1562 }
1563 let q = rand_unit(dim, 999);
1564 let pq = idx.prepare_query(&q);
1565 let exact: Vec<f32> = base.iter().map(|v| cosine_f32(&q, v)).collect();
1566 let mut max_err = 0f32;
1567 for (i, &e) in exact.iter().enumerate().take(200) {
1568 let est = idx.score(&pq, i);
1569 max_err = max_err.max((est - e).abs());
1570 }
1571 assert!(max_err < 0.2, "max score error {max_err}");
1572 }
1573
1574 #[test]
1575 fn score_reproducible_and_close_to_naive() {
1576 let dim = 128;
1577 for bits in [4u8, 5, 6] {
1580 let mut idx = VecqIndex::new(dim, 13);
1581 idx.set_bits(bits);
1582 for i in 0..50 {
1583 idx.add(&rand_unit(dim, i + 21));
1584 }
1585 let q = rand_unit(dim, 321);
1586 let pq = idx.prepare_query(&q);
1587 for vi in 0..50 {
1588 let base = vi * idx.bytes_per_vector();
1589 let mut naive = 0f32;
1590 for i in 0..idx.padded() {
1591 let code = unpack_code(&idx.codes[base..], i, bits) as usize;
1592 naive += pq.rotated[i] * lloyd::centroids(bits)[code];
1593 }
1594 let s = idx.score(&pq, vi);
1595 assert_eq!(s.to_bits(), idx.score(&pq, vi).to_bits());
1596 assert!(
1597 (s - naive * idx.scales[vi]).abs() < 1e-5,
1598 "bits {bits} vector {vi}"
1599 );
1600 }
1601 }
1602 }
1603
1604 #[test]
1605 fn neon_matches_scalar_bitwise() {
1606 let dim = 128;
1607 let mut idx = VecqIndex::new(dim, 42);
1608 for i in 0..30 {
1609 idx.add(&rand_unit(dim, i + 500));
1610 }
1611 let q = rand_unit(dim, 777);
1612 let pq = idx.prepare_query(&q);
1613 for vi in 0..30 {
1614 let base = vi * (idx.padded() / 2);
1615 let codes = &idx.codes[base..base + idx.padded() / 2];
1616 let qslice = &pq.rotated[..idx.padded()];
1617 #[cfg(target_arch = "aarch64")]
1618 {
1619 let neon = unsafe { neon::score_neon(codes, qslice, &pq.lut) };
1620 let scalar = score_scalar(codes, qslice, &pq.lut);
1621 assert_eq!(
1622 neon.to_bits(),
1623 scalar.to_bits(),
1624 "vector {vi}: NEON and scalar diverged"
1625 );
1626 }
1627 #[cfg(not(target_arch = "aarch64"))]
1628 {
1629 let _ = (base, codes, qslice);
1630 }
1631 }
1632 }
1633
1634 #[cfg(target_arch = "aarch64")]
1635 #[test]
1636 fn neon4_matches_neon_bitwise() {
1637 let dim = 128;
1638 let mut idx = VecqIndex::new(dim, 91);
1639 idx.set_bits(4); for i in 0..12 {
1641 idx.add(&rand_unit(dim, i + 90));
1642 }
1643 let q = rand_unit(dim, 1234);
1644 let pq = idx.prepare_query(&q);
1645 let bpv = idx.padded() / 2;
1646 for chunk_start in (0..12).step_by(4) {
1647 let codes4 = &idx.codes[chunk_start * bpv..(chunk_start + 4) * bpv];
1648 let qslice = &pq.rotated[..idx.padded()];
1649 {
1650 let batched = unsafe { neon::score_neon4(codes4, qslice, &pq.lut) };
1651 for v in 0..4 {
1652 let single = unsafe {
1653 neon::score_neon(&codes4[v * bpv..(v + 1) * bpv], qslice, &pq.lut)
1654 };
1655 assert_eq!(
1656 batched[v].to_bits(),
1657 single.to_bits(),
1658 "chunk {chunk_start} vec {v}: neon4 diverged from neon"
1659 );
1660 }
1661 }
1662 }
1663 }
1664
1665 #[cfg(target_arch = "aarch64")]
1666 #[test]
1667 fn neon_wide_matches_scalar_bitwise() {
1668 for bits in [5u8, 6] {
1672 for dim in [128usize, 384] {
1673 let mut idx = VecqIndex::new(dim, 91);
1674 idx.set_bits(bits);
1675 for i in 0..12 {
1676 idx.add(&rand_unit(dim, i + 90));
1677 }
1678 let q = rand_unit(dim, 1234);
1679 let pq = idx.prepare_query(&q);
1680 let bpv = idx.bytes_per_vector();
1681 for chunk_start in (0..12).step_by(4) {
1682 let codes4 = &idx.codes[chunk_start * bpv..(chunk_start + 4) * bpv];
1683 let qslice = &pq.rotated[..idx.padded()];
1684 let batched = unsafe { neon::score_neon_wide4(codes4, qslice, bits) };
1685 for v in 0..4 {
1686 let scalar =
1687 score_wide_scalar(&codes4[v * bpv..(v + 1) * bpv], qslice, bits);
1688 assert_eq!(
1689 batched[v].to_bits(),
1690 scalar.to_bits(),
1691 "bits {bits} dim {dim} chunk {chunk_start} vec {v}"
1692 );
1693 }
1694 }
1695 }
1696 }
1697 }
1698
1699 #[cfg(target_arch = "x86_64")]
1700 #[test]
1701 fn avx2_matches_scalar_bitwise() {
1702 if !avx2::available() {
1703 return; }
1705 for (dim, seed) in [(128, 42), (8, 43)] {
1708 let mut idx = VecqIndex::new(dim, seed);
1709 for i in 0..30 {
1710 idx.add(&rand_unit(dim, i + 500));
1711 }
1712 let q = rand_unit(dim, 777);
1713 let pq = idx.prepare_query(&q);
1714 for vi in 0..30 {
1715 let base = vi * (idx.padded() / 2);
1716 let codes = &idx.codes[base..base + idx.padded() / 2];
1717 let qslice = &pq.rotated[..idx.padded()];
1718 let avx2raw = unsafe { avx2::score_avx2(codes, qslice, &pq.lut) };
1719 let scalar = score_scalar(codes, qslice, &pq.lut);
1720 assert_eq!(
1721 avx2raw.to_bits(),
1722 scalar.to_bits(),
1723 "dim {dim} vector {vi}: AVX2 and scalar diverged"
1724 );
1725 }
1726 }
1727 }
1728
1729 #[cfg(target_arch = "x86_64")]
1730 #[test]
1731 fn avx24_matches_avx2_bitwise() {
1732 if !avx2::available() {
1733 return;
1734 }
1735 let dim = 128;
1736 let mut idx = VecqIndex::new(dim, 91);
1737 for i in 0..12 {
1738 idx.add(&rand_unit(dim, i + 90));
1739 }
1740 let q = rand_unit(dim, 1234);
1741 let pq = idx.prepare_query(&q);
1742 let bpv = idx.padded() / 2;
1743 for chunk_start in (0..12).step_by(4) {
1744 let codes4 = &idx.codes[chunk_start * bpv..(chunk_start + 4) * bpv];
1745 let qslice = &pq.rotated[..idx.padded()];
1746 let batched = unsafe { avx2::score_avx24(codes4, qslice, &pq.lut) };
1747 for v in 0..4 {
1748 let single =
1749 unsafe { avx2::score_avx2(&codes4[v * bpv..(v + 1) * bpv], qslice, &pq.lut) };
1750 assert_eq!(
1751 batched[v].to_bits(),
1752 single.to_bits(),
1753 "chunk {chunk_start} vec {v}: avx24 diverged from avx2"
1754 );
1755 }
1756 }
1757 }
1758
1759 #[cfg(target_arch = "x86_64")]
1760 #[test]
1761 fn search_dispatch_matches_scalar_on_avx2_hosts() {
1762 let dim = 128;
1765 for bits in [4u8, 5, 6] {
1766 let mut idx = VecqIndex::new(dim, 55);
1767 idx.set_bits(bits);
1768 for i in 0..30 {
1769 idx.add(&rand_unit(dim, i + 800));
1770 }
1771 let q = rand_unit(dim, 888);
1772 let pq = idx.prepare_query(&q);
1773 for vi in 0..30 {
1774 let base = vi * idx.bytes_per_vector();
1775 let codes = &idx.codes[base..base + idx.bytes_per_vector()];
1776 let qslice = &pq.rotated[..idx.padded()];
1777 let scalar = if bits == 4 {
1778 score_scalar(codes, qslice, &pq.lut)
1779 } else {
1780 score_wide_scalar(codes, qslice, bits)
1781 };
1782 assert_eq!(
1783 idx.score(&pq, vi).to_bits(),
1784 (scalar * idx.scales[vi]).to_bits(),
1785 "bits {bits} vec {vi}"
1786 );
1787 }
1788 }
1789 }
1790
1791 #[test]
1792 fn search_returns_sorted_results() {
1793 let dim = 64;
1794 let mut idx = VecqIndex::new(dim, 3);
1795 for i in 0..50 {
1796 idx.add(&rand_unit(dim, i * 31 + 5));
1797 }
1798 let q = rand_unit(dim, 77);
1799 let res = idx.search(&q, 5);
1800 assert_eq!(res.len(), 5);
1801 for w in res.windows(2) {
1802 assert!(w[0].1 >= w[1].1);
1803 }
1804 }
1805
1806 #[test]
1807 fn quantized_size_is_one_eighth() {
1808 let dim = 384;
1809 let mut idx = VecqIndex::new(dim, 1);
1810 idx.add(&rand_unit(dim, 11));
1811 assert_eq!(idx.codes.len(), 320);
1813 assert_eq!(idx.bytes_per_vector(), 320);
1814 let mut idx4 = VecqIndex::new(dim, 1);
1816 idx4.set_bits(4);
1817 idx4.add(&rand_unit(dim, 11));
1818 assert_eq!(idx4.codes.len(), 256);
1819 let mut idx6 = VecqIndex::new(dim, 1);
1820 idx6.set_bits(6);
1821 idx6.add(&rand_unit(dim, 11));
1822 assert_eq!(idx6.codes.len(), 384);
1823 }
1824
1825 #[test]
1826 fn keyed_add_search_remove() {
1827 let dim = 64;
1828 let mut idx = VecqIndex::new(dim, 5);
1829 for i in 0..50u64 {
1830 idx.add_keyed(1000 + i, &rand_unit(dim, i * 17 + 3));
1831 }
1832 assert_eq!(idx.len(), 50);
1833 assert!(idx.contains_key(1000));
1834 assert!(!idx.contains_key(999));
1835
1836 let q = rand_unit(dim, 77);
1837 let keyed = idx.search_keyed(&q, 5);
1838 assert_eq!(keyed.len(), 5);
1839 for w in keyed.windows(2) {
1840 assert!(w[0].1 >= w[1].1);
1841 }
1842 let positional = idx.search(&q, 5);
1844 for ((key, ks), (slot, ps)) in keyed.iter().zip(positional.iter()) {
1845 assert_eq!(key, &idx.key_of(*slot).unwrap());
1846 assert_eq!(ks.to_bits(), ps.to_bits());
1847 }
1848
1849 let top_key = keyed[0].0;
1851 assert!(idx.remove_keyed(top_key));
1852 assert!(!idx.remove_keyed(top_key), "second remove is a no-op");
1853 assert!(!idx.remove_keyed(12345), "unknown key returns false");
1854 assert_eq!(idx.len(), 49);
1855 assert_eq!(idx.tombstones(), 1);
1856 let keyed2 = idx.search_keyed(&q, 5);
1857 assert!(!keyed2.iter().any(|(k, _)| *k == top_key));
1858 for (k, s) in keyed2.iter() {
1859 let old = keyed.iter().find(|(ok, _)| ok == k).map(|(_, os)| *os);
1860 if let Some(os) = old {
1861 assert_eq!(s.to_bits(), os.to_bits(), "key {k} score changed");
1862 }
1863 }
1864 }
1865
1866 #[test]
1867 fn keyed_add_same_key_replaces() {
1868 let dim = 32;
1869 let mut idx = VecqIndex::new(dim, 9);
1870 idx.add_keyed(7, &rand_unit(dim, 101));
1871 idx.add_keyed(7, &rand_unit(dim, 202));
1872 assert_eq!(idx.len(), 1, "replace must not grow the index");
1873 assert_eq!(idx.tombstones(), 0);
1874 let q = rand_unit(dim, 202);
1876 let res = idx.search_keyed(&q, 1);
1877 assert_eq!(res[0].0, 7);
1878 }
1879
1880 #[test]
1881 fn keyed_slot_indices_stay_stable_across_remove_and_serialize() {
1882 let dim = 64;
1883 let mut idx = VecqIndex::new(dim, 15);
1884 for i in 0..20u64 {
1885 idx.add_keyed(i, &rand_unit(dim, i + 300));
1886 }
1887 let q = rand_unit(dim, 404);
1888 let before = idx.search(&q, 20);
1889 idx.remove_keyed(idx.key_of(before[0].0).unwrap());
1891 idx.remove_keyed(idx.key_of(before[5].0).unwrap());
1892 let after = idx.search(&q, 20);
1893 assert_eq!(after.len(), 18);
1894 for (slot, s) in &after {
1895 let old = before.iter().find(|(os, _)| os == slot);
1896 assert!(old.is_some(), "slot {slot} moved after remove");
1897 assert_eq!(old.unwrap().1.to_bits(), s.to_bits());
1898 }
1899 let bytes = idx.to_bytes();
1901 let disk = VecqIndex::from_bytes(&bytes).unwrap();
1902 assert_eq!(disk.len(), 18);
1903 assert_eq!(idx.search(&q, 20), after, "in-memory results unchanged");
1904 }
1905
1906 #[test]
1907 fn compact_drops_tombstones_and_preserves_results() {
1908 let dim = 64;
1909 let mut idx = VecqIndex::new(dim, 21);
1910 for i in 0..40u64 {
1911 idx.add_keyed(10 * i, &rand_unit(dim, i + 61));
1912 }
1913 for i in 0..20u64 {
1914 assert!(idx.remove_keyed(10 * i));
1915 }
1916 let q = rand_unit(dim, 123);
1917 let expected = idx.search_keyed(&q, 20);
1918 idx.compact();
1919 assert_eq!(idx.tombstones(), 0);
1920 assert_eq!(idx.len(), 20);
1921 assert_eq!(idx.search_keyed(&q, 20), expected);
1922 let bytes = idx.to_bytes();
1926 let back = VecqIndex::from_bytes(&bytes).unwrap();
1927 let reloaded = back.search(&q, 20);
1928 assert_eq!(reloaded.len(), 20);
1929 for ((slot, s), (key, ks)) in reloaded.iter().zip(expected.iter()) {
1930 assert_eq!(idx.key_of(*slot), Some(*key));
1931 assert!(
1932 (s - ks).abs() < 1e-3,
1933 "key {key} score drifted: {s} vs {ks}"
1934 );
1935 }
1936 }
1937
1938 #[test]
1939 fn keyed_search_on_empty_and_drained_index() {
1940 let dim = 32;
1941 let mut idx = VecqIndex::new(dim, 31);
1942 assert!(idx.search_keyed(&rand_unit(dim, 1), 3).is_empty());
1943 idx.add_keyed(1, &rand_unit(dim, 2));
1944 idx.add_keyed(2, &rand_unit(dim, 3));
1945 assert!(idx.remove_keyed(1));
1946 assert!(idx.remove_keyed(2));
1947 assert!(idx.is_empty(), "drained index reports empty");
1948 assert_eq!(idx.tombstones(), 2);
1949 assert!(idx.search_keyed(&rand_unit(dim, 4), 3).is_empty());
1950 }
1951
1952 #[test]
1953 fn keyed_keys_survive_file_round_trip() {
1954 let dim = 64;
1956 let mut idx = VecqIndex::new(dim, 42);
1957 let v = rand_unit(dim, 11);
1958 idx.add_keyed(10, &v);
1959 idx.add_keyed(20, &rand_unit(dim, 22));
1960 let bytes = idx.to_bytes();
1961 let mut back = VecqIndex::from_bytes(&bytes).expect("parse");
1962 assert_eq!(back.len(), 2);
1963 assert!(back.contains_key(10));
1964 assert!(back.contains_key(20));
1965 assert_eq!(back.key_of(0), Some(10));
1966 assert_eq!(back.key_of(1), Some(20));
1967 let hits = back.search_keyed(&v, 5);
1968 assert!(!hits.is_empty(), "keys must survive reload (issue #32)");
1969 assert_eq!(hits[0].0, 10);
1970 assert!(back.remove_keyed(20));
1972 assert_eq!(back.len(), 1);
1973 let slot = back.add_keyed_multi(10, &rand_unit(dim, 33));
1974 assert_eq!(back.key_of(slot), Some(10));
1975 assert!(back.relabel(10, 30));
1976 assert_eq!(back.key_of(slot), Some(30));
1977 }
1978
1979 #[test]
1980 fn multi_key_round_trip_and_compact() {
1981 let dim = 64;
1982 let mut idx = VecqIndex::new(dim, 7);
1983 idx.add_keyed(1, &rand_unit(dim, 101));
1984 idx.add_keyed_multi(1, &rand_unit(dim, 202));
1985 idx.add_keyed_multi(1, &rand_unit(dim, 303));
1986 idx.add_keyed(2, &rand_unit(dim, 404));
1987 let bytes = idx.to_bytes();
1988 let mut back = VecqIndex::from_bytes(&bytes).expect("parse");
1989 assert_eq!(back.len(), 4);
1990 assert!(back.remove_keyed_at(1, 1));
1992 assert!(back.contains_key(1));
1993 assert_eq!(back.tombstones(), 1);
1994 back.compact();
1996 assert_eq!(back.tombstones(), 0);
1997 assert!(back.contains_key(1));
1998 assert!(back.contains_key(2));
1999 let probe = rand_unit(dim, 404);
2000 assert_eq!(back.search_keyed(&probe, 5)[0].0, 2);
2001 let bytes2 = back.to_bytes();
2003 assert_eq!(bytes2, back.to_bytes());
2004 assert_eq!(VecqIndex::from_bytes(&bytes2).unwrap().len(), 3);
2005 }
2006
2007 #[test]
2008 fn keyed_index_from_file_supports_keyed_adds() {
2009 let dim = 64;
2010 let mut idx = VecqIndex::new(dim, 41);
2011 for i in 0..10u64 {
2012 idx.add(&rand_unit(dim, i + 700));
2013 }
2014 let bytes = idx.to_bytes();
2015 let mut back = VecqIndex::from_bytes(&bytes).unwrap();
2016 back.add_keyed(555, &rand_unit(dim, 999));
2017 assert!(back.contains_key(555));
2018 assert_eq!(back.len(), 11);
2019 let q = rand_unit(dim, 999);
2020 assert_eq!(back.search_keyed(&q, 1)[0].0, 555);
2021 }
2022
2023 #[test]
2024 #[should_panic(expected = "vector dim mismatch")]
2025 fn keyed_add_dim_mismatch_panics() {
2026 let mut idx = VecqIndex::new(32, 3);
2027 idx.add_keyed(1, &[0.5; 64]);
2028 }
2029
2030 #[test]
2033 fn relabel_moves_key_and_rejects_conflicts() {
2034 let dim = 32;
2035 let mut idx = VecqIndex::new(dim, 5);
2036 idx.add_keyed(1, &rand_unit(dim, 11));
2037 assert!(idx.relabel(1, 2), "relabel to a free key succeeds");
2038 assert!(!idx.contains_key(1));
2039 assert!(idx.contains_key(2));
2040 let q = rand_unit(dim, 11);
2042 assert_eq!(idx.search_keyed(&q, 1)[0].0, 2);
2043 assert!(!idx.relabel(1, 3));
2045 idx.add_keyed(3, &rand_unit(dim, 22));
2047 assert!(!idx.relabel(3, 2));
2048 assert!(idx.relabel(2, 2));
2050 assert!(idx.contains_key(2));
2051 }
2052
2053 #[test]
2054 fn multi_key_add_search_and_dedupe() {
2055 let dim = 32;
2056 let mut idx = VecqIndex::new(dim, 9);
2057 let v1 = rand_unit(dim, 101);
2058 let v2 = rand_unit(dim, 202);
2059 idx.add_keyed(7, &v1);
2060 idx.add_keyed_multi(7, &v2);
2061 assert_eq!(idx.len(), 2, "multi add appends a slot");
2062 assert_eq!(idx.tombstones(), 0);
2063 let q_v2 = v2.clone();
2065 let hits = idx.search_keyed(&q_v2, 5);
2066 assert_eq!(hits.len(), 1, "key must be deduped across its slots");
2067 assert_eq!(hits[0].0, 7);
2068 let q_v1 = v1.clone();
2069 assert_eq!(idx.search_keyed(&q_v1, 5)[0].0, 7);
2070 idx.add_keyed(8, &rand_unit(dim, 303));
2072 let hits = idx.search_keyed(&q_v2, 5);
2073 assert_eq!(hits.len(), 2);
2074 assert_eq!(hits[0].0, 7);
2075 }
2076
2077 #[test]
2078 fn multi_key_remove_and_remove_at() {
2079 let dim = 32;
2080 let mut idx = VecqIndex::new(dim, 13);
2081 idx.add_keyed(7, &rand_unit(dim, 101));
2082 idx.add_keyed_multi(7, &rand_unit(dim, 202));
2083 assert!(idx.remove_keyed(7));
2085 assert_eq!(idx.len(), 0);
2086 assert_eq!(idx.tombstones(), 2);
2087 assert!(!idx.contains_key(7));
2088 assert!(!idx.remove_keyed(7));
2089 let mut idx2 = VecqIndex::new(dim, 17);
2091 idx2.add_keyed(9, &rand_unit(dim, 111));
2092 let slot0 = idx2.add_keyed_multi(9, &rand_unit(dim, 222));
2093 let slot1 = idx2.add_keyed_multi(9, &rand_unit(dim, 333));
2094 assert!(idx2.remove_keyed_at(9, slot0));
2095 assert!(idx2.contains_key(9));
2096 assert!(idx2.remove_keyed_at(9, slot1));
2097 assert!(idx2.contains_key(9), "primary slot keeps the key alive");
2098 assert!(!idx2.remove_keyed_at(9, slot0), "already-dead slot");
2099 assert_eq!(idx2.len(), 1);
2100 }
2101
2102 #[test]
2103 fn multi_key_replace_and_relabel_and_compact() {
2104 let dim = 32;
2105 let mut idx = VecqIndex::new(dim, 21);
2106 idx.add_keyed(7, &rand_unit(dim, 101));
2107 idx.add_keyed_multi(7, &rand_unit(dim, 202));
2108 let v3 = rand_unit(dim, 404);
2110 let replaced = idx.add_keyed(7, &v3);
2111 assert_eq!(idx.len(), 2, "replace must not grow the index");
2112 let hits = idx.search_keyed(&v3, 5);
2113 assert_eq!(hits[0].0, 7);
2114 let _ = replaced;
2115 assert!(idx.relabel(7, 8));
2117 assert_eq!(idx.search_keyed(&v3, 5)[0].0, 8);
2118 let q = v3.clone();
2120 let expected = idx.search_keyed(&q, 5);
2121 idx.compact();
2122 assert_eq!(idx.tombstones(), 0);
2123 assert_eq!(idx.search_keyed(&q, 5), expected);
2124 }
2125
2126 #[test]
2129 fn working_dim_equal_to_dim_matches_new_bitwise() {
2130 let dim = 128;
2131 let mut a = VecqIndex::new(dim, 7);
2132 let mut b = VecqIndex::with_working_dim(dim, dim, 7);
2133 for i in 0..20 {
2134 a.add(&rand_unit(dim, i + 61));
2135 b.add(&rand_unit(dim, i + 61));
2136 }
2137 let q = rand_unit(dim, 321);
2138 let ra = a.search(&q, 5);
2139 let rb = b.search(&q, 5);
2140 assert_eq!(ra.len(), rb.len());
2141 for ((sa, fa), (sb, fb)) in ra.iter().zip(rb.iter()) {
2142 assert_eq!(sa, sb);
2143 assert_eq!(fa.to_bits(), fb.to_bits());
2144 }
2145 assert_eq!(a.to_bytes(), b.to_bytes());
2146 assert_eq!(b.working_dim(), dim);
2147 }
2148
2149 #[test]
2150 fn working_dim_truncates_storage_and_keeps_leading_signal() {
2151 let (dim, working, n) = (256, 64, 40);
2156 let signal: Vec<Vec<f32>> = (0..n).map(|i| rand_unit(working, i * 13 + 5)).collect();
2157 let mut full = VecqIndex::new(dim, 11);
2158 let mut trunc = VecqIndex::with_working_dim(dim, working, 11);
2159 for (i, s) in signal.iter().enumerate() {
2160 let mut v = vec![0f32; dim];
2162 v[..working].copy_from_slice(s);
2163 let noise = rand_unit(dim - working, 9_000 + i as u64);
2164 v[working..].copy_from_slice(&noise.iter().map(|x: &f32| x * 0.05).collect::<Vec<_>>());
2165 let norm: f32 = v.iter().map(|x| x * x).sum::<f32>().sqrt();
2166 for x in v.iter_mut() {
2167 *x /= norm;
2168 }
2169 full.add(&v);
2170 trunc.add(&v);
2171 }
2172 assert_eq!(trunc.working_dim(), working);
2173 assert!(
2175 trunc.to_bytes().len() * 3 < full.to_bytes().len(),
2176 "expected ~3x smaller"
2177 );
2178 let q = &signal[0];
2180 let mut qv = vec![0f32; dim];
2181 qv[..working].copy_from_slice(q);
2182 let noise = rand_unit(dim - working, 9_000);
2183 qv[working..].copy_from_slice(&noise.iter().map(|x: &f32| x * 0.05).collect::<Vec<_>>());
2184 let norm: f32 = qv.iter().map(|x| x * x).sum::<f32>().sqrt();
2185 for x in qv.iter_mut() {
2186 *x /= norm;
2187 }
2188 let rt = trunc.search(&qv, 1);
2189 assert_eq!(rt[0].0, 0, "truncated index must rank vector 0 first");
2190 }
2191
2192 #[test]
2193 fn working_dim_round_trips_through_file() {
2194 let (dim, working) = (256, 64);
2195 let mut idx = VecqIndex::with_working_dim(dim, working, 21);
2196 for i in 0..10 {
2197 idx.add(&rand_unit(dim, i + 700));
2198 }
2199 let q = rand_unit(dim, 999);
2200 let expected = idx.search(&q, 5);
2201 let bytes = idx.to_bytes();
2202 let back = VecqIndex::from_bytes(&bytes).expect("parse v1.2");
2203 assert_eq!(back.working_dim(), working);
2204 assert_eq!(back.dim(), dim);
2205 let reloaded = back.search(&q, 5);
2208 assert_eq!(reloaded[0].0, expected[0].0);
2209 assert_eq!(reloaded.len(), expected.len());
2210 for ((s0, f0), (_, f1)) in reloaded.iter().zip(expected.iter()) {
2211 assert!((f0 - f1).abs() < 1e-3, "slot {s0}: {f0} vs {f1}");
2212 }
2213 }
2214
2215 #[test]
2216 fn legacy_v11_file_still_loads() {
2217 let mut idx = VecqIndex::new(128, 33);
2222 idx.set_bits(4); for i in 0..8 {
2224 idx.add(&rand_unit(128, i + 50));
2225 }
2226 let bytes = idx.to_bytes(); assert_eq!(u16::from_le_bytes([bytes[4], bytes[5]]), 259);
2228 let mut v11 = bytes.clone();
2229 v11[4] = 257u16.to_le_bytes()[0];
2230 v11[5] = 257u16.to_le_bytes()[1];
2231 let q = rand_unit(128, 777);
2232 assert_eq!(
2233 VecqIndex::from_bytes(&v11).unwrap().search(&q, 5),
2234 VecqIndex::from_bytes(&bytes).unwrap().search(&q, 5)
2235 );
2236 }
2237
2238 #[test]
2239 #[should_panic(expected = "working_dim")]
2240 fn working_dim_greater_than_dim_panics() {
2241 let _ = VecqIndex::with_working_dim(64, 128, 1);
2242 }
2243
2244 #[test]
2245 #[should_panic(expected = "working_dim")]
2246 fn working_dim_zero_panics() {
2247 let _ = VecqIndex::with_working_dim(64, 0, 1);
2248 }
2249
2250 #[test]
2251 #[should_panic(expected = "u16")]
2252 fn working_dim_beyond_u16_range_panics() {
2253 let _ = VecqIndex::with_working_dim(100_000, 70_000, 1);
2258 }
2259
2260 #[test]
2261 fn full_dim_index_may_exceed_u16_dim() {
2262 let mut idx = VecqIndex::with_working_dim(70_000, 70_000, 1);
2265 let mut v = vec![0f32; 70_000];
2266 v[0] = 1.0;
2267 idx.add(&v);
2268 let q = vec![0f32; 70_000];
2269 let mut q = q;
2270 q[0] = 1.0;
2271 assert_eq!(idx.search(&q, 1)[0].0, 0);
2272 }
2273
2274 fn clustered(n: usize, dim: usize, clusters: usize, seed: u64, spread: f32) -> Vec<Vec<f32>> {
2281 fn next(x: &mut u64) -> f32 {
2282 *x ^= *x << 13;
2286 *x ^= *x >> 7;
2287 *x ^= *x << 17;
2288 *x as f32 / u32::MAX as f32 - 0.5
2289 }
2290 let mut x = seed | 1;
2291 let mut centroids: Vec<Vec<f32>> = Vec::new();
2292 for _ in 0..clusters {
2293 let mut v: Vec<f32> = (0..dim).map(|_| next(&mut x)).collect();
2294 let norm: f32 = v.iter().map(|a| a * a).sum::<f32>().sqrt();
2295 v.iter_mut().for_each(|a| *a /= norm);
2296 centroids.push(v);
2297 }
2298 (0..n)
2299 .map(|i| {
2300 let c = ¢roids[i % clusters];
2301 let mut v: Vec<f32> = c.iter().map(|&a| a + spread * next(&mut x)).collect();
2302 let norm: f32 = v.iter().map(|a| a * a).sum::<f32>().sqrt();
2303 v.iter_mut().for_each(|a| *a /= norm);
2304 v
2305 })
2306 .collect()
2307 }
2308
2309 fn exact_top(base: &[Vec<f32>], q: &[f32], k: usize) -> Vec<usize> {
2310 let mut s: Vec<(usize, f32)> = base
2311 .iter()
2312 .enumerate()
2313 .map(|(i, v)| (i, cosine_f32(q, v)))
2314 .collect();
2315 s.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap());
2316 s.into_iter().take(k).map(|(i, _)| i).collect()
2317 }
2318
2319 #[test]
2320 fn cascade_with_full_r_matches_exact_search_bitwise() {
2321 let dim = 128;
2322 let base = clustered(60, dim, 10, 5, 0.5);
2323 let mut idx = VecqIndex::new(dim, 42);
2324 idx.set_bits(4); for v in &base {
2326 idx.add(v);
2327 }
2328 idx.enable_cascade();
2329 assert!(idx.cascade_enabled());
2330 let q = &base[0];
2331 let exact = idx.search(q, 10);
2332 let casc = idx.search_cascade(q, 10, 60);
2333 assert_eq!(casc.len(), exact.len());
2334 for ((sa, fa), (sb, fb)) in exact.iter().zip(casc.iter()) {
2335 assert_eq!(sa, sb, "slot order must match with r >= n");
2336 assert_eq!(fa.to_bits(), fb.to_bits(), "scores must be bit-identical");
2337 }
2338 }
2339
2340 #[test]
2341 fn cascade_recall_grows_with_r() {
2342 let recall = |idx: &VecqIndex, base: &[Vec<f32>], r: usize| -> f32 {
2349 let queries = &base[990..];
2350 let mut sum = 0f32;
2351 for q in queries {
2352 let truth = exact_top(base, q, 10);
2353 let got: Vec<usize> = idx
2354 .search_cascade(q, 10, r)
2355 .into_iter()
2356 .map(|(i, _)| i)
2357 .collect();
2358 sum += truth.iter().filter(|t| got.contains(t)).count() as f32 / 10.0;
2359 }
2360 sum / queries.len() as f32
2361 };
2362 let dim = 128;
2366 let base = clustered(1000, dim, 50, 9, 0.1);
2367 let mut idx = VecqIndex::new(dim, 42);
2368 idx.set_bits(4); for v in &base {
2370 idx.add(v);
2371 }
2372 idx.enable_cascade();
2373 let r100 = recall(&idx, &base, 100);
2374 assert!(r100 >= 0.5, "moderate set: r=100 (10% scan) recall {r100}");
2375 let base = clustered(1000, dim, 50, 9, 0.5);
2378 let mut idx = VecqIndex::new(dim, 42);
2379 idx.set_bits(4); for v in &base {
2381 idx.add(v);
2382 }
2383 idx.enable_cascade();
2384 let (r25, r50, r100, r200) = (
2385 recall(&idx, &base, 25),
2386 recall(&idx, &base, 50),
2387 recall(&idx, &base, 100),
2388 recall(&idx, &base, 200),
2389 );
2390 assert!(
2391 r25 <= r50 && r50 <= r100 && r100 <= r200,
2392 "recall must be monotone in r: {r25} {r50} {r100} {r200}"
2393 );
2394 assert!(r200 >= 0.6, "adversarial set: r=200 recall {r200}");
2395 }
2396
2397 #[test]
2398 fn cascade_skips_tombstones_and_stays_deterministic() {
2399 let dim = 64;
2400 let base = clustered(80, dim, 8, 3, 0.5);
2401 let mut idx = VecqIndex::new(dim, 42);
2402 idx.set_bits(4); for v in &base {
2404 idx.add(v);
2405 }
2406 idx.enable_cascade();
2407 idx.remove_keyed(0);
2408 idx.remove_keyed(1);
2409 let q = &base[5];
2410 let a = idx.search_cascade(q, 10, 80);
2411 let b = idx.search_cascade(q, 10, 80);
2412 assert_eq!(a, b, "deterministic across calls");
2413 assert!(
2414 !a.iter().any(|(s, _)| *s < 2),
2415 "tombstoned slots must be skipped"
2416 );
2417 let exact = idx.search(q, 10);
2419 assert_eq!(a, exact);
2420 }
2421
2422 #[test]
2423 fn cascade_works_after_reload_and_compact() {
2424 let dim = 64;
2425 let base = clustered(40, dim, 6, 11, 0.5);
2426 let mut idx = VecqIndex::new(dim, 42);
2427 idx.set_bits(4); for v in &base {
2429 idx.add_keyed(100 + 1, v);
2430 }
2431 let bytes = idx.to_bytes();
2432 let mut back = VecqIndex::from_bytes(&bytes).unwrap();
2433 assert!(!back.cascade_enabled(), "signatures are not persisted");
2434 back.enable_cascade();
2435 let q = &base[3];
2436 let casc = back.search_cascade(q, 5, 40);
2437 assert_eq!(casc, back.search(q, 5));
2438 back.remove_keyed(101);
2440 back.compact();
2441 back.enable_cascade();
2442 let casc2 = back.search_cascade(q, 5, 39);
2443 assert_eq!(casc2, back.search(q, 5));
2444 }
2445
2446 #[test]
2447 fn cascade_on_empty_index_returns_empty() {
2448 let mut idx = VecqIndex::new(64, 1);
2449 idx.set_bits(4); idx.enable_cascade();
2451 let q = rand_unit(64, 2);
2452 assert!(idx.search_cascade(&q, 5, 50).is_empty());
2453 }
2454
2455 #[test]
2456 fn cascade_r_clamps_to_live_count() {
2457 let dim = 32;
2458 let mut idx = VecqIndex::new(dim, 3);
2459 idx.set_bits(4); for i in 0..4 {
2461 idx.add(&rand_unit(dim, i + 80));
2462 }
2463 idx.enable_cascade();
2464 let q = rand_unit(dim, 90);
2465 let casc = idx.search_cascade(&q, 2, 1000);
2466 assert_eq!(casc.len(), 2);
2467 assert_eq!(casc, idx.search(&q, 2));
2468 }
2469
2470 #[test]
2471 fn cascade_works_with_working_dim_index() {
2472 let dim = 128;
2473 let working = 64;
2474 let base = clustered(50, dim, 7, 13, 0.5);
2475 let mut idx = VecqIndex::with_working_dim(dim, working, 42);
2476 idx.set_bits(4); for v in &base {
2478 idx.add(v);
2479 }
2480 idx.enable_cascade();
2481 let q = &base[2];
2482 assert_eq!(idx.search_cascade(q, 5, 50), idx.search(q, 5));
2483 }
2484
2485 #[test]
2488 fn residual_improves_recall_over_plain_on_noisy_data() {
2489 let dim = 128;
2493 let base = clustered(600, dim, 30, 19, 0.5);
2494 let mut plain = VecqIndex::new(dim, 42);
2495 plain.set_bits(4); let mut resid = VecqIndex::with_residual(dim, 42);
2497 for v in &base {
2498 plain.add(v);
2499 resid.add(v);
2500 }
2501 let queries = &base[590..];
2502 let recall = |idx: &VecqIndex| -> f32 {
2503 let mut sum = 0f32;
2504 for q in queries {
2505 let truth = exact_top(&base, q, 10);
2506 let got: Vec<usize> = idx.search(q, 10).into_iter().map(|(i, _)| i).collect();
2507 sum += truth.iter().filter(|t| got.contains(t)).count() as f32 / 10.0;
2508 }
2509 sum / queries.len() as f32
2510 };
2511 let r_plain = recall(&plain);
2512 let r_resid = recall(&resid);
2513 assert!(
2520 r_resid > r_plain,
2521 "residual must beat plain: resid {r_resid} vs plain {r_plain}"
2522 );
2523 assert!(
2524 r_resid - r_plain >= 0.05,
2525 "residual gain must be substantial: resid {r_resid} vs plain {r_plain}"
2526 );
2527 assert!(
2528 r_resid >= 0.8,
2529 "residual recall {r_resid} (plain {r_plain})"
2530 );
2531 }
2532
2533 #[test]
2534 fn residual_search_sorted_and_deterministic() {
2535 let dim = 64;
2536 let mut idx = VecqIndex::with_residual(dim, 7);
2537 for i in 0..40 {
2538 idx.add(&rand_unit(dim, i * 7 + 1));
2539 }
2540 let q = rand_unit(dim, 555);
2541 let r1 = idx.search(&q, 10);
2542 let r2 = idx.search(&q, 10);
2543 assert_eq!(r1, r2);
2544 assert_eq!(r1.len(), 10);
2545 for w in r1.windows(2) {
2546 assert!(w[0].1 >= w[1].1);
2547 }
2548 assert!(idx.is_residual());
2549 }
2550
2551 #[test]
2552 fn residual_round_trips_through_file() {
2553 let dim = 64;
2554 let mut idx = VecqIndex::with_residual(dim, 21);
2555 for i in 0..12 {
2556 idx.add(&rand_unit(dim, i + 400));
2557 }
2558 let q = rand_unit(dim, 888);
2559 let expected = idx.search(&q, 5);
2560 let bytes = idx.to_bytes();
2561 assert_eq!(u16::from_le_bytes([bytes[4], bytes[5]]), 260, "v1.4 = 260");
2562 let back = VecqIndex::from_bytes(&bytes).expect("parse v1.4");
2563 assert!(back.is_residual());
2564 let reloaded = back.search(&q, 5);
2565 assert_eq!(reloaded[0].0, expected[0].0);
2566 for ((_, f0), (_, f1)) in reloaded.iter().zip(expected.iter()) {
2567 assert!((f0 - f1).abs() < 1e-3, "{f0} vs {f1}");
2568 }
2569 assert_eq!(bytes, back.to_bytes());
2571 }
2572
2573 #[test]
2574 fn residual_keyed_and_cascade_compose() {
2575 let dim = 64;
2576 let mut idx = VecqIndex::with_residual(dim, 31);
2577 idx.add_keyed(5, &rand_unit(dim, 101));
2578 idx.add_keyed(6, &rand_unit(dim, 202));
2579 idx.enable_cascade();
2580 let q = rand_unit(dim, 101);
2581 let hits = idx.search_keyed(&q, 5);
2582 assert_eq!(hits[0].0, 5);
2583 let casc = idx.search_cascade(&q, 2, 2);
2584 assert_eq!(casc, idx.search(&q, 2));
2585 let bytes = idx.to_bytes();
2586 let back = VecqIndex::from_bytes(&bytes).unwrap();
2587 assert!(back.contains_key(5) && back.contains_key(6));
2588 assert_eq!(back.search_keyed(&q, 5)[0].0, 5);
2589 }
2590
2591 #[test]
2592 fn plain_index_stays_v13() {
2593 let mut idx = VecqIndex::new(64, 3);
2596 idx.add(&rand_unit(64, 1));
2597 assert!(!idx.is_residual());
2598 let bytes = idx.to_bytes();
2599 assert_eq!(u16::from_le_bytes([bytes[4], bytes[5]]), 261, "v1.5 = 261");
2600 assert_eq!(bytes[24], 5, "width byte");
2601 let mut idx4 = VecqIndex::new(64, 3);
2602 idx4.set_bits(4);
2603 idx4.add(&rand_unit(64, 1));
2604 let bytes4 = idx4.to_bytes();
2605 assert_eq!(
2606 u16::from_le_bytes([bytes4[4], bytes4[5]]),
2607 259,
2608 "v1.3 = 259"
2609 );
2610 let mut res = VecqIndex::with_residual(64, 3);
2612 res.add(&rand_unit(64, 1));
2613 let bytes_r = res.to_bytes();
2614 assert_eq!(
2615 u16::from_le_bytes([bytes_r[4], bytes_r[5]]),
2616 260,
2617 "v1.4 = 260"
2618 );
2619 }
2620
2621 #[test]
2622 fn wide_plain_matches_or_beats_residual_recall() {
2623 let dim = 128;
2628 let base = clustered(600, dim, 30, 19, 0.5);
2629 let mut plain6 = VecqIndex::new(dim, 42);
2630 plain6.set_bits(6);
2631 let mut resid = VecqIndex::with_residual(dim, 42);
2632 for v in &base {
2633 plain6.add(v);
2634 resid.add(v);
2635 }
2636 let queries = &base[590..];
2637 let recall = |idx: &VecqIndex| -> f32 {
2638 let mut sum = 0f32;
2639 for q in queries {
2640 let truth = exact_top(&base, q, 10);
2641 let got: Vec<usize> = idx.search(q, 10).into_iter().map(|(i, _)| i).collect();
2642 sum += truth.iter().filter(|t| got.contains(t)).count() as f32 / 10.0;
2643 }
2644 sum / queries.len() as f32
2645 };
2646 let r6 = recall(&plain6);
2647 let rr = recall(&resid);
2648 assert!(
2650 2 * resid.bytes_per_vector() > plain6.bytes_per_vector(),
2651 "6-bit plain must be smaller than residual"
2652 );
2653 assert!(
2654 r6 >= rr - 0.01,
2655 "plain 6-bit recall {r6} must match/beats residual {rr}"
2656 );
2657 }
2658}
2659
2660#[cfg(test)]
2661mod residual_tests {
2662 use super::*;
2663
2664 #[test]
2665 fn residual_reconstruction_mse_halved() {
2666 fn next(x: &mut u64) -> f32 {
2667 *x ^= *x << 13;
2668 *x ^= *x >> 7;
2669 *x ^= *x << 17;
2670 *x as f32 / u32::MAX as f32 - 0.5
2671 }
2672 let dim = 128;
2673 let mut x = 19 | 1;
2674 let mut centroids = Vec::new();
2675 for _ in 0..10 {
2676 let mut v: Vec<f32> = (0..dim).map(|_| next(&mut x)).collect();
2677 let n: f32 = v.iter().map(|a| a * a).sum::<f32>().sqrt();
2678 v.iter_mut().for_each(|a| *a /= n);
2679 centroids.push(v);
2680 }
2681 let base: Vec<Vec<f32>> = (0..50)
2682 .map(|i| {
2683 let c = ¢roids[i % 10];
2684 let mut v: Vec<f32> = c.iter().map(|&a| a + 0.5 * next(&mut x)).collect();
2685 let n: f32 = v.iter().map(|a| a * a).sum::<f32>().sqrt();
2686 v.iter_mut().for_each(|a| *a /= n);
2687 v
2688 })
2689 .collect();
2690 let mut plain = VecqIndex::new(dim, 42);
2691 plain.set_bits(4); let mut resid = VecqIndex::with_residual(dim, 42);
2693 for v in &base {
2694 plain.add(v);
2695 resid.add(v);
2696 }
2697 let bpv = plain.padded() / 2;
2700 let mut mse0 = 0f32;
2701 let mut mse1 = 0f32;
2702 for (slot, v) in base.iter().enumerate() {
2703 let unit: Vec<f32> = {
2704 let n: f32 = v.iter().map(|a| a * a).sum::<f32>().sqrt();
2705 v.iter().map(|a| a / n).collect()
2706 };
2707 let mut rot = Vec::new();
2708 resid.transform.apply(&unit, &mut rot);
2709 let deq = |codes: &[u8], scale: f32| -> Vec<f32> {
2710 let mut acc = vec![0f32; rot.len()];
2711 for (i, &b) in codes.iter().enumerate() {
2712 acc[2 * i] += lloyd::dequantize_4bit(b & 0x0F) * scale;
2713 acc[2 * i + 1] += lloyd::dequantize_4bit(b >> 4) * scale;
2714 }
2715 acc
2716 };
2717 let sp = (plain.padded() as f32).sqrt();
2720 let x0 = deq(
2721 &plain.codes[slot * bpv..(slot + 1) * bpv],
2722 plain.scales[slot] * sp,
2723 );
2724 let x1 = deq(
2725 &resid.codes[slot * bpv..(slot + 1) * bpv],
2726 resid.scales[slot] * sp,
2727 );
2728 let x2 = deq(
2729 &resid.codes2[slot * bpv..(slot + 1) * bpv],
2730 resid.scales2[slot] * sp,
2731 );
2732 let err = |xh: &[f32]| -> f32 {
2733 xh.iter()
2734 .zip(rot.iter())
2735 .map(|(a, b)| (a - b) * (a - b))
2736 .sum::<f32>()
2737 / rot.len() as f32
2738 };
2739 mse0 += err(&x0);
2740 let xtot: Vec<f32> = x1.iter().zip(x2.iter()).map(|(a, b)| a + b).collect();
2742 mse1 += err(&xtot);
2743 }
2744 let m0 = mse0 / base.len() as f32;
2745 let m1 = mse1 / base.len() as f32;
2746 println!("mse plain={:.5} residual={:.5}", m0, m1);
2747 assert!(
2749 m1 < m0 * 0.5,
2750 "residual reconstruction must at least halve plain MSE: {m0} vs {m1}"
2751 );
2752 }
2753}