1use std::cmp::Ordering;
29use std::collections::BinaryHeap;
30
31use rayon::prelude::*;
32
33pub const MAX_DIMS: usize = 1 << 16;
35
36#[derive(Debug, Clone, PartialEq)]
38pub enum SparseIndexError {
39 Malformed(String),
41 BadValue { doc: usize, value: f32 },
43 TooLarge(String),
45}
46
47impl std::fmt::Display for SparseIndexError {
48 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
49 match self {
50 Self::Malformed(m) => write!(f, "malformed sparse matrix: {m}"),
51 Self::BadValue { doc, value } => {
52 write!(
53 f,
54 "document {doc} has value {value}; values must be finite and >= 0"
55 )
56 }
57 Self::TooLarge(m) => write!(f, "sparse matrix too large: {m}"),
58 }
59 }
60}
61
62impl std::error::Error for SparseIndexError {}
63
64#[derive(Clone, Copy)]
66pub struct CsrView<'a> {
67 pub indptr: &'a [i64],
68 pub indices: &'a [i32],
69 pub data: &'a [f32],
70 pub n_dims: usize,
71}
72
73impl<'a> CsrView<'a> {
74 pub fn from_spmat(bytes: &'a [u8]) -> Result<Self, SparseIndexError> {
77 let bad = |m: String| SparseIndexError::Malformed(m);
78 let header: &[i64] = bytes
79 .get(..24)
80 .and_then(|h| bytemuck::try_cast_slice(h).ok())
81 .ok_or_else(|| bad("spmat header is truncated or misaligned".into()))?;
82 let size = |v: i64| usize::try_from(v).map_err(|_| bad(format!("negative size {v}")));
83 let (rows, cols, nnz) = (size(header[0])?, size(header[1])?, size(header[2])?);
84 let span = |n: usize, w: usize| {
85 n.checked_mul(w)
86 .ok_or_else(|| bad("spmat size overflows".into()))
87 };
88 let indptr_end = 24 + span(rows + 1, 8)?;
89 let indices_end = indptr_end + span(nnz, 4)?;
90 let data_end = indices_end + span(nnz, 4)?;
91 if bytes.len() != data_end {
92 return Err(bad(format!(
93 "file is {} bytes, header implies {data_end}",
94 bytes.len()
95 )));
96 }
97 let cast_err = |e: bytemuck::PodCastError| bad(format!("misaligned section: {e:?}"));
98 Ok(Self {
99 indptr: bytemuck::try_cast_slice(&bytes[24..indptr_end]).map_err(cast_err)?,
100 indices: bytemuck::try_cast_slice(&bytes[indptr_end..indices_end]).map_err(cast_err)?,
101 data: bytemuck::try_cast_slice(&bytes[indices_end..data_end]).map_err(cast_err)?,
102 n_dims: cols,
103 })
104 }
105
106 pub fn validate(&self) -> Result<usize, SparseIndexError> {
108 let rows = self
109 .indptr
110 .len()
111 .checked_sub(1)
112 .ok_or_else(|| SparseIndexError::Malformed("indptr is empty".into()))?;
113 if self.indices.len() != self.data.len() {
114 return Err(SparseIndexError::Malformed(format!(
115 "{} indices but {} values",
116 self.indices.len(),
117 self.data.len()
118 )));
119 }
120 if self.indptr[0] != 0 || self.indptr[rows] != self.indices.len() as i64 {
121 return Err(SparseIndexError::Malformed(
122 "indptr must start at 0 and end at nnz".into(),
123 ));
124 }
125 if self.indptr.windows(2).any(|w| w[0] > w[1]) {
126 return Err(SparseIndexError::Malformed("indptr decreases".into()));
127 }
128 if let Some(&bad) = self
129 .indices
130 .iter()
131 .find(|&&c| c < 0 || c as usize >= self.n_dims)
132 {
133 return Err(SparseIndexError::Malformed(format!(
134 "coordinate {bad} outside [0, {})",
135 self.n_dims
136 )));
137 }
138 Ok(rows)
139 }
140
141 pub fn row(&self, r: usize) -> (&'a [i32], &'a [f32]) {
143 let (a, b) = (self.indptr[r] as usize, self.indptr[r + 1] as usize);
144 (&self.indices[a..b], &self.data[a..b])
145 }
146}
147
148#[derive(Clone, Copy, Debug)]
150pub struct SparseBuildParams {
151 pub postings_per_term: usize,
153 pub block_fraction: f32,
155 pub summary_energy: f32,
157 pub transpose_budget: usize,
159 pub seed: u64,
161 pub half_values: bool,
164}
165
166impl SparseBuildParams {
167 pub fn exact() -> Self {
169 Self {
170 postings_per_term: usize::MAX,
171 summary_energy: 1.0,
172 half_values: false,
173 ..Self::default()
174 }
175 }
176}
177
178impl Default for SparseBuildParams {
179 fn default() -> Self {
180 Self {
181 postings_per_term: 4000,
182 block_fraction: 0.1,
183 summary_energy: 0.4,
184 transpose_budget: 1 << 26,
185 seed: 0x5eed,
186 half_values: true,
187 }
188 }
189}
190
191#[derive(Clone, Copy, Debug)]
193pub struct SparseSearchParams {
194 pub query_cut: usize,
196 pub heap_factor: f32,
198}
199
200impl SparseSearchParams {
201 pub fn exact() -> Self {
202 Self {
203 query_cut: usize::MAX,
204 heap_factor: 1.0,
205 }
206 }
207}
208
209pub struct SparseImpactIndex {
211 n_dims: usize,
212 n_docs: usize,
213 fwd_offsets: Vec<u64>,
215 fwd_ids: Vec<u16>,
216 fwd_vals: Vec<f32>,
217 fwd_half: Vec<u16>,
220 term_blocks: Vec<u32>,
222 block_doc_offsets: Vec<u64>,
224 block_docs: Vec<u32>,
225 block_sum_offsets: Vec<u64>,
228 sum_ids: Vec<u16>,
229 sum_codes: Vec<u8>,
230 block_scales: Vec<f32>,
231}
232
233struct TermBlocks {
235 doc_counts: Vec<u32>,
236 docs: Vec<u32>,
237 sum_counts: Vec<u32>,
238 sum_ids: Vec<u16>,
239 sum_codes: Vec<u8>,
240 scales: Vec<f32>,
241}
242
243fn splitmix(state: &mut u64) -> u64 {
244 *state = state.wrapping_add(0x9e37_79b9_7f4a_7c15);
245 let mut z = *state;
246 z = (z ^ (z >> 30)).wrapping_mul(0xbf58_476d_1ce4_e5b9);
247 z = (z ^ (z >> 27)).wrapping_mul(0x94d0_49bb_1331_11eb);
248 z ^ (z >> 31)
249}
250
251struct BuildScratch {
253 dense: Vec<f32>,
254 touched: Vec<u16>,
255 starts: Vec<u32>,
256}
257
258impl BuildScratch {
259 fn new(n_dims: usize) -> Self {
260 Self {
261 dense: vec![0.0; n_dims],
262 touched: Vec::new(),
263 starts: vec![u32::MAX; n_dims + 1],
264 }
265 }
266}
267
268impl SparseImpactIndex {
269 pub fn build(csr: CsrView<'_>, params: SparseBuildParams) -> Result<Self, SparseIndexError> {
271 let n_docs = csr.validate()?;
272 if csr.n_dims > MAX_DIMS {
273 return Err(SparseIndexError::TooLarge(format!(
274 "{} coordinates, at most {MAX_DIMS}",
275 csr.n_dims
276 )));
277 }
278 if n_docs > u32::MAX as usize {
279 return Err(SparseIndexError::TooLarge(format!("{n_docs} documents")));
280 }
281 if let Some(i) = csr.data.iter().position(|v| !(v.is_finite() && *v >= 0.0)) {
282 let doc = csr.indptr.partition_point(|&p| p <= i as i64) - 1;
283 return Err(SparseIndexError::BadValue {
284 doc,
285 value: csr.data[i],
286 });
287 }
288 let n_dims = csr.n_dims;
289
290 let mut fwd_offsets = Vec::with_capacity(n_docs + 1);
292 fwd_offsets.push(0u64);
293 let nnz = csr.data.iter().filter(|&&v| v > 0.0).count();
294 let mut fwd_ids = Vec::with_capacity(nnz);
295 let (mut fwd_vals, mut fwd_half) = if params.half_values {
297 (Vec::new(), Vec::with_capacity(nnz))
298 } else {
299 (Vec::with_capacity(nnz), Vec::new())
300 };
301 let mut df = vec![0u64; n_dims];
302 for r in 0..n_docs {
303 let (ids, vals) = csr.row(r);
304 for (&c, &v) in ids.iter().zip(vals) {
305 if v > 0.0 {
306 fwd_ids.push(c as u16);
307 if params.half_values {
308 fwd_half.push(f16_bits(v));
309 } else {
310 fwd_vals.push(v);
311 }
312 df[c as usize] += 1;
313 }
314 }
315 fwd_offsets.push(fwd_ids.len() as u64);
316 }
317
318 let mut index = Self {
319 n_dims,
320 n_docs,
321 fwd_offsets,
322 fwd_ids,
323 fwd_vals,
324 fwd_half,
325 term_blocks: Vec::with_capacity(n_dims + 1),
326 block_doc_offsets: vec![0],
327 block_docs: Vec::new(),
328 block_sum_offsets: vec![0],
329 sum_ids: Vec::new(),
330 sum_codes: Vec::new(),
331 block_scales: Vec::new(),
332 };
333 index.term_blocks.push(0);
334
335 let budget = params.transpose_budget.max(1) as u64;
337 let mut lo = 0usize;
338 while lo < n_dims {
339 let mut hi = lo;
340 let mut total = 0u64;
341 while hi < n_dims && (hi == lo || total + df[hi] <= budget) {
342 total += df[hi];
343 hi += 1;
344 }
345 let mut starts = Vec::with_capacity(hi - lo + 1);
346 starts.push(0u64);
347 for &d in &df[lo..hi] {
348 starts.push(starts.last().copied().unwrap_or(0) + d);
349 }
350 let mut cursor = starts.clone();
351 let mut postings = vec![(0u32, 0f32); total as usize];
352 for d in 0..n_docs {
353 for (c, v) in index.entries(d as u32) {
354 let c = c as usize;
355 if (lo..hi).contains(&c) {
356 let slot = &mut cursor[c - lo];
357 postings[*slot as usize] = (d as u32, v);
358 *slot += 1;
359 }
360 }
361 }
362 let lists: Vec<&mut [(u32, f32)]> = {
363 let mut rest = postings.as_mut_slice();
364 let mut out = Vec::with_capacity(hi - lo);
365 for t in 0..hi - lo {
366 let (head, tail) = rest.split_at_mut((starts[t + 1] - starts[t]) as usize);
367 out.push(head);
368 rest = tail;
369 }
370 out
371 };
372 let this = &index;
373 let built: Vec<TermBlocks> = lists
374 .into_par_iter()
375 .enumerate()
376 .map_init(
377 || BuildScratch::new(n_dims),
378 |scratch, (i, list)| this.cluster_list(lo + i, list, params, scratch),
379 )
380 .collect();
381 for tb in built {
382 index.append(tb);
383 }
384 lo = hi;
385 }
386 Ok(index)
387 }
388
389 fn entries(&self, d: u32) -> impl Iterator<Item = (u16, f32)> + '_ {
391 let (a, b) = (
392 self.fwd_offsets[d as usize] as usize,
393 self.fwd_offsets[d as usize + 1] as usize,
394 );
395 let half = !self.fwd_half.is_empty();
396 (a..b).map(move |i| {
397 let v = if half {
398 f16_value(self.fwd_half[i])
399 } else {
400 self.fwd_vals[i]
401 };
402 (self.fwd_ids[i], v)
403 })
404 }
405
406 #[inline]
408 fn score(&self, d: u32, dense: &[f32]) -> f32 {
409 let (a, b) = (
410 self.fwd_offsets[d as usize] as usize,
411 self.fwd_offsets[d as usize + 1] as usize,
412 );
413 if self.fwd_half.is_empty() {
414 dot(&self.fwd_ids[a..b], &self.fwd_vals[a..b], dense)
415 } else {
416 dot_half(&self.fwd_ids[a..b], &self.fwd_half[a..b], dense)
417 }
418 }
419
420 #[inline]
422 fn prefetch_row(&self, d: u32) {
423 let (a, b) = (
424 self.fwd_offsets[d as usize] as usize,
425 self.fwd_offsets[d as usize + 1] as usize,
426 );
427 prefetch(self.fwd_ids[a..].as_ptr().cast(), (b - a) * 2);
428 if self.fwd_half.is_empty() {
429 prefetch(self.fwd_vals[a..].as_ptr().cast(), (b - a) * 4);
430 } else {
431 prefetch(self.fwd_half[a..].as_ptr().cast(), (b - a) * 2);
432 }
433 }
434
435 fn append(&mut self, tb: TermBlocks) {
436 let mut doc_at = 0usize;
437 let mut sum_at = 0usize;
438 for ((&dc, &sc), &scale) in tb.doc_counts.iter().zip(&tb.sum_counts).zip(&tb.scales) {
439 let (dc, sc) = (dc as usize, sc as usize);
440 self.block_docs
441 .extend_from_slice(&tb.docs[doc_at..doc_at + dc]);
442 self.block_doc_offsets.push(self.block_docs.len() as u64);
443 self.sum_ids
444 .extend_from_slice(&tb.sum_ids[sum_at..sum_at + sc]);
445 self.sum_codes
446 .extend_from_slice(&tb.sum_codes[sum_at..sum_at + sc]);
447 self.block_sum_offsets.push(self.sum_ids.len() as u64);
448 self.block_scales.push(scale);
449 doc_at += dc;
450 sum_at += sc;
451 }
452 let blocks = self.block_scales.len() as u32;
453 self.term_blocks.push(blocks);
454 }
455
456 fn cluster_list(
458 &self,
459 term: usize,
460 list: &mut [(u32, f32)],
461 params: SparseBuildParams,
462 scratch: &mut BuildScratch,
463 ) -> TermBlocks {
464 let mut out = TermBlocks {
465 doc_counts: Vec::new(),
466 docs: Vec::new(),
467 sum_counts: Vec::new(),
468 sum_ids: Vec::new(),
469 sum_codes: Vec::new(),
470 scales: Vec::new(),
471 };
472 if list.is_empty() {
473 return out;
474 }
475 let by_value_desc =
476 |a: &(u32, f32), b: &(u32, f32)| b.1.total_cmp(&a.1).then(a.0.cmp(&b.0));
477 let keep = params.postings_per_term.min(list.len());
478 if keep == 0 {
479 return out;
480 }
481 if keep < list.len() {
482 list.select_nth_unstable_by(keep - 1, by_value_desc);
483 }
484 let list = &mut list[..keep];
485
486 let n_blocks = ((keep as f64 * f64::from(params.block_fraction)).ceil() as usize)
487 .max(1)
488 .min(keep);
489 let assign: Vec<u32> = if n_blocks == 1 {
490 vec![0; keep]
491 } else {
492 self.assign_to_centroids(term, list, n_blocks, params.seed, scratch)
493 };
494
495 let mut order: Vec<u32> = (0..keep as u32).collect();
497 order.sort_unstable_by_key(|&i| (assign[i as usize], list[i as usize].0));
498 let mut groups: Vec<(f32, &[u32])> = Vec::with_capacity(n_blocks);
499 let mut start = 0;
500 while start < keep {
501 let b = assign[order[start] as usize];
502 let mut end = start;
503 let mut top = 0f32;
504 while end < keep && assign[order[end] as usize] == b {
505 top = top.max(list[order[end] as usize].1);
506 end += 1;
507 }
508 groups.push((top, &order[start..end]));
509 start = end;
510 }
511 groups.sort_by(|a, b| b.0.total_cmp(&a.0));
513
514 for (_, members) in groups {
515 for &i in members {
516 out.docs.push(list[i as usize].0);
517 }
518 out.doc_counts.push(members.len() as u32);
519 self.summarize(
520 members.iter().map(|&i| list[i as usize].0),
521 params.summary_energy,
522 scratch,
523 &mut out,
524 );
525 }
526 out
527 }
528
529 fn assign_to_centroids(
532 &self,
533 term: usize,
534 list: &[(u32, f32)],
535 n_blocks: usize,
536 seed: u64,
537 scratch: &mut BuildScratch,
538 ) -> Vec<u32> {
539 let mut state = seed ^ (term as u64).wrapping_mul(0x2545_f491_4f6c_dd1d);
540 let mut pos: Vec<u32> = (0..list.len() as u32).collect();
542 for i in 0..n_blocks {
543 let j = i + (splitmix(&mut state) % (list.len() - i) as u64) as usize;
544 pos.swap(i, j);
545 }
546 let mut entries: Vec<(u16, u32, f32)> = Vec::new();
548 for (c, &p) in pos[..n_blocks].iter().enumerate() {
549 entries.extend(
550 self.entries(list[p as usize].0)
551 .map(|(t, v)| (t, c as u32, v)),
552 );
553 }
554 entries.sort_unstable_by_key(|e| (e.0, e.1));
555 let starts = &mut scratch.starts;
556 let mut touched = Vec::new();
557 for (i, e) in entries.iter().enumerate() {
558 let t = e.0 as usize;
559 if starts[t] == u32::MAX {
560 starts[t] = i as u32;
561 touched.push(t);
562 }
563 }
564 let mut scores = vec![0f32; n_blocks];
565 let mut assign = Vec::with_capacity(list.len());
566 for &(doc, _) in list {
567 scores.iter_mut().for_each(|s| *s = 0.0);
568 for (t, v) in self.entries(doc) {
569 let s = starts[t as usize];
570 if s == u32::MAX {
571 continue;
572 }
573 for e in entries[s as usize..].iter().take_while(|e| e.0 == t) {
574 scores[e.1 as usize] += v * e.2;
575 }
576 }
577 let mut best = 0usize;
578 for (c, &s) in scores.iter().enumerate() {
579 if s > scores[best] {
580 best = c;
581 }
582 }
583 assign.push(best as u32);
584 }
585 for t in touched {
586 starts[t] = u32::MAX;
587 }
588 assign
589 }
590
591 fn summarize(
594 &self,
595 docs: impl Iterator<Item = u32>,
596 energy: f32,
597 scratch: &mut BuildScratch,
598 out: &mut TermBlocks,
599 ) {
600 let BuildScratch { dense, touched, .. } = scratch;
601 for d in docs {
602 for (t, v) in self.entries(d) {
603 let slot = &mut dense[t as usize];
604 if *slot == 0.0 {
605 touched.push(t);
606 }
607 *slot = slot.max(v);
608 }
609 }
610 let mut coords: Vec<(u16, f32)> = touched.iter().map(|&t| (t, dense[t as usize])).collect();
611 for &t in touched.iter() {
612 dense[t as usize] = 0.0;
613 }
614 touched.clear();
615 if energy < 1.0 {
616 coords.sort_unstable_by(|a, b| b.1.total_cmp(&a.1).then(a.0.cmp(&b.0)));
617 let total: f64 = coords.iter().map(|c| f64::from(c.1)).sum();
618 let target = total * f64::from(energy.max(0.0));
619 let mut acc = 0f64;
620 let mut keep = 0;
621 while keep < coords.len() && (keep == 0 || acc < target) {
622 acc += f64::from(coords[keep].1);
623 keep += 1;
624 }
625 coords.truncate(keep);
626 }
627 coords.sort_unstable_by_key(|c| c.0);
628 let max = coords.iter().fold(0f32, |m, c| m.max(c.1));
629 let scale = if max > 0.0 {
631 max / 255.0 * (1.0 + 1e-4)
632 } else {
633 1.0
634 };
635 for &(t, v) in &coords {
636 let mut code = (v / scale).ceil().min(255.0) as u8;
637 while code < 255 && f32::from(code) * scale < v {
638 code += 1;
639 }
640 out.sum_ids.push(t);
641 out.sum_codes.push(code);
642 }
643 out.sum_counts.push(coords.len() as u32);
644 out.scales.push(scale);
645 }
646
647 pub fn n_docs(&self) -> usize {
648 self.n_docs
649 }
650
651 pub fn n_dims(&self) -> usize {
652 self.n_dims
653 }
654
655 pub fn memory_bytes(&self) -> usize {
657 self.fwd_offsets.len() * 8
658 + self.fwd_ids.len() * 2
659 + self.fwd_vals.len() * 4
660 + self.fwd_half.len() * 2
661 + self.term_blocks.len() * 4
662 + self.block_doc_offsets.len() * 8
663 + self.block_docs.len() * 4
664 + self.block_sum_offsets.len() * 8
665 + self.sum_ids.len() * 2
666 + self.sum_codes.len()
667 + self.block_scales.len() * 4
668 }
669
670 pub fn block_stats(&self) -> (usize, usize) {
672 (self.block_scales.len(), self.block_docs.len())
673 }
674
675 pub fn searcher(&self) -> SparseSearcher<'_> {
677 SparseSearcher {
678 index: self,
679 dense: vec![0.0; self.n_dims],
680 dense_pos: vec![0.0; self.n_dims],
681 visited: vec![0; self.n_docs.div_ceil(64)],
682 touched: Vec::new(),
683 heap: BinaryHeap::new(),
684 skipped_blocks: 0,
685 }
686 }
687
688 pub fn brute_force(&self, ids: &[u32], vals: &[f32], k: usize) -> Vec<(u32, f32)> {
690 let mut dense = vec![0f32; self.n_dims];
691 for (&c, &v) in ids.iter().zip(vals) {
692 if (c as usize) < self.n_dims {
693 dense[c as usize] += v;
694 }
695 }
696 let mut heap = BinaryHeap::new();
697 for d in 0..self.n_docs as u32 {
698 let s = self.score(d, &dense);
699 if s > 0.0 {
700 offer(&mut heap, k, Hit { score: s, doc: d });
701 }
702 }
703 into_sorted(heap)
704 }
705}
706
707#[inline]
708fn dot(ids: &[u16], vals: &[f32], dense: &[f32]) -> f32 {
709 let mut acc = [0f32; 4];
710 let (id4, id_rest) = ids.as_chunks::<4>();
711 let (v4, v_rest) = vals.as_chunks::<4>();
712 for (i, v) in id4.iter().zip(v4) {
713 for j in 0..4 {
714 acc[j] += dense[i[j] as usize] * v[j];
715 }
716 }
717 let mut tail = 0f32;
718 for (&i, &v) in id_rest.iter().zip(v_rest) {
719 tail += dense[i as usize] * v;
720 }
721 (acc[0] + acc[1]) + (acc[2] + acc[3]) + tail
722}
723
724#[inline]
726fn prefetch(ptr: *const u8, bytes: usize) {
727 let end = ptr as usize + bytes;
728 let mut line = ptr as usize & !63;
729 while line < end {
730 #[cfg(target_arch = "aarch64")]
731 unsafe {
734 std::arch::asm!(
735 "prfm pldl1keep, [{0}]",
736 in(reg) line,
737 options(nostack, preserves_flags, readonly)
738 );
739 }
740 #[cfg(target_arch = "x86_64")]
741 unsafe {
744 std::arch::x86_64::_mm_prefetch::<{ std::arch::x86_64::_MM_HINT_T0 }>(
745 line as *const i8,
746 );
747 }
748 line += 64;
749 }
750}
751
752fn f16_bits(v: f32) -> u16 {
756 const MIN_NORMAL: f32 = 6.103_515_6e-5;
757 if v <= 0.0 {
758 return 0;
759 }
760 let v = v.clamp(MIN_NORMAL, 65504.0);
761 let bits = v.to_bits() - ((127 - 15) << 23);
762 let rounded = bits + 0x0fff + ((bits >> 13) & 1);
763 ((rounded >> 13).min(0x7bff)) as u16
764}
765
766#[inline]
768fn f16_value(h: u16) -> f32 {
769 let bits = (u32::from(h) << 13) + ((127 - 15) << 23);
770 if h == 0 {
771 0.0
772 } else {
773 f32::from_bits(bits)
774 }
775}
776
777#[inline]
778fn dot_half(ids: &[u16], vals: &[u16], dense: &[f32]) -> f32 {
779 let mut acc = [0f32; 4];
780 let (id4, id_rest) = ids.as_chunks::<4>();
781 let (v4, v_rest) = vals.as_chunks::<4>();
782 for (i, v) in id4.iter().zip(v4) {
783 for j in 0..4 {
784 acc[j] += dense[i[j] as usize] * f16_value(v[j]);
785 }
786 }
787 let mut tail = 0f32;
788 for (&i, &v) in id_rest.iter().zip(v_rest) {
789 tail += dense[i as usize] * f16_value(v);
790 }
791 (acc[0] + acc[1]) + (acc[2] + acc[3]) + tail
792}
793
794#[derive(Clone, Copy, PartialEq)]
797struct Hit {
798 score: f32,
799 doc: u32,
800}
801
802impl Eq for Hit {}
803
804impl Ord for Hit {
805 fn cmp(&self, other: &Self) -> Ordering {
806 other
807 .score
808 .total_cmp(&self.score)
809 .then(self.doc.cmp(&other.doc))
810 }
811}
812
813impl PartialOrd for Hit {
814 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
815 Some(self.cmp(other))
816 }
817}
818
819fn offer(heap: &mut BinaryHeap<Hit>, k: usize, hit: Hit) {
820 if heap.len() < k {
821 heap.push(hit);
822 } else if let Some(mut worst) = heap.peek_mut() {
823 if hit < *worst {
824 *worst = hit;
825 }
826 }
827}
828
829fn into_sorted(heap: BinaryHeap<Hit>) -> Vec<(u32, f32)> {
830 heap.into_sorted_vec()
831 .into_iter()
832 .map(|h| (h.doc, h.score))
833 .collect()
834}
835
836pub struct SparseSearcher<'a> {
838 index: &'a SparseImpactIndex,
839 dense: Vec<f32>,
840 dense_pos: Vec<f32>,
841 visited: Vec<u64>,
842 touched: Vec<u32>,
843 heap: BinaryHeap<Hit>,
844 skipped_blocks: u64,
846}
847
848impl SparseSearcher<'_> {
849 pub fn search(
853 &mut self,
854 ids: &[u32],
855 vals: &[f32],
856 k: usize,
857 params: SparseSearchParams,
858 ) -> Vec<(u32, f32)> {
859 let index = self.index;
860 if k == 0 {
861 return Vec::new();
862 }
863 let mut terms: Vec<(u32, f32)> = Vec::with_capacity(ids.len());
864 for (&c, &v) in ids.iter().zip(vals) {
865 if (c as usize) < index.n_dims && v.is_finite() && v != 0.0 {
866 if self.dense[c as usize] == 0.0 && self.dense_pos[c as usize] == 0.0 {
867 terms.push((c, 0.0));
868 }
869 self.dense[c as usize] += v;
870 }
871 }
872 for t in terms.iter_mut() {
873 let w = self.dense[t.0 as usize];
874 t.1 = w;
875 self.dense_pos[t.0 as usize] = w.max(0.0);
876 }
877 terms.sort_unstable_by(|a, b| b.1.total_cmp(&a.1).then(a.0.cmp(&b.0)));
878 let walked = terms
879 .iter()
880 .take(params.query_cut)
881 .take_while(|t| t.1 > 0.0);
882
883 let heap = &mut self.heap;
884 heap.clear();
885 for &(t, _) in walked {
886 let blocks =
887 index.term_blocks[t as usize] as usize..index.term_blocks[t as usize + 1] as usize;
888 for b in blocks {
889 if heap.len() == k {
890 let (s0, s1) = (
891 index.block_sum_offsets[b] as usize,
892 index.block_sum_offsets[b + 1] as usize,
893 );
894 let mut bound = 0f32;
895 for (&c, &code) in index.sum_ids[s0..s1].iter().zip(&index.sum_codes[s0..s1]) {
896 bound += self.dense_pos[c as usize] * f32::from(code);
897 }
898 bound *= index.block_scales[b];
899 let threshold = heap.peek().map_or(f32::MIN, |h| h.score);
900 if bound * params.heap_factor < threshold {
901 self.skipped_blocks += 1;
902 continue;
903 }
904 }
905 let (d0, d1) = (
906 index.block_doc_offsets[b] as usize,
907 index.block_doc_offsets[b + 1] as usize,
908 );
909 let docs = &index.block_docs[d0..d1];
910 if let Some(&first) = docs.first() {
911 index.prefetch_row(first);
912 }
913 for (i, &d) in docs.iter().enumerate() {
914 if let Some(&next) = docs.get(i + 1) {
915 index.prefetch_row(next);
916 }
917 let (w, bit) = ((d >> 6) as usize, 1u64 << (d & 63));
918 if self.visited[w] & bit != 0 {
919 continue;
920 }
921 if self.visited[w] == 0 {
922 self.touched.push(w as u32);
923 }
924 self.visited[w] |= bit;
925 let s = index.score(d, &self.dense);
926 if s > 0.0 {
927 offer(heap, k, Hit { score: s, doc: d });
928 }
929 }
930 }
931 }
932 for &w in &self.touched {
933 self.visited[w as usize] = 0;
934 }
935 self.touched.clear();
936 for t in &terms {
937 self.dense[t.0 as usize] = 0.0;
938 self.dense_pos[t.0 as usize] = 0.0;
939 }
940 into_sorted(std::mem::take(heap))
941 }
942}
943
944impl SparseImpactIndex {
945 pub fn search_batch(
948 &self,
949 queries: CsrView<'_>,
950 k: usize,
951 params: SparseSearchParams,
952 ) -> Result<Vec<u32>, SparseIndexError> {
953 let nq = queries.validate()?;
954 let mut out = vec![u32::MAX; nq * k];
955 if k == 0 {
956 return Ok(out);
957 }
958 let min_len = nq.div_ceil(rayon::current_num_threads() * 4).max(1);
961 out.par_chunks_mut(k)
962 .enumerate()
963 .with_min_len(min_len)
964 .for_each_init(
965 || (self.searcher(), Vec::new()),
966 |(searcher, ids), (q, row)| {
967 let (qi, qv) = queries.row(q);
968 ids.clear();
969 ids.extend(qi.iter().map(|&c| c as u32));
970 for (slot, (doc, _)) in row.iter_mut().zip(searcher.search(ids, qv, k, params))
971 {
972 *slot = doc;
973 }
974 },
975 );
976 Ok(out)
977 }
978}
979
980#[cfg(test)]
981mod tests {
982 use super::*;
983
984 struct Csr {
985 indptr: Vec<i64>,
986 indices: Vec<i32>,
987 data: Vec<f32>,
988 n_dims: usize,
989 }
990
991 impl Csr {
992 fn view(&self) -> CsrView<'_> {
993 CsrView {
994 indptr: &self.indptr,
995 indices: &self.indices,
996 data: &self.data,
997 n_dims: self.n_dims,
998 }
999 }
1000 }
1001
1002 fn random_csr(rows: usize, n_dims: usize, max_nnz: usize, state: &mut u64) -> Csr {
1005 let mut indptr = vec![0i64];
1006 let mut indices = Vec::new();
1007 let mut data = Vec::new();
1008 for r in 0..rows {
1009 if r % 17 == 5 && r > 0 {
1010 let (a, b) = (indptr[r - 1] as usize, indptr[r] as usize);
1011 indices.extend_from_within(a..b);
1012 data.extend_from_within(a..b);
1013 } else {
1014 let nnz = (splitmix(state) % (max_nnz as u64 + 1)) as usize;
1015 let mut cols: Vec<i32> = (0..nnz)
1016 .map(|_| {
1017 let u = splitmix(state) % (n_dims as u64 * n_dims as u64);
1018 (u as f64).sqrt() as i32
1019 })
1020 .collect();
1021 cols.sort_unstable();
1022 cols.dedup();
1023 for c in cols {
1024 indices.push(c);
1025 data.push((1 + splitmix(state) % 8) as f32 * 0.25);
1026 }
1027 }
1028 indptr.push(indices.len() as i64);
1029 }
1030 Csr {
1031 indptr,
1032 indices,
1033 data,
1034 n_dims,
1035 }
1036 }
1037
1038 #[test]
1039 fn exact_mode_matches_brute_force() {
1040 let mut state = 7u64;
1041 for (rows, n_dims, frac) in [(600, 64, 0.1f32), (2000, 300, 0.05), (50, 16, 0.5)] {
1042 let docs = random_csr(rows, n_dims, 24, &mut state);
1043 let params = SparseBuildParams {
1044 block_fraction: frac,
1045 transpose_budget: 997,
1046 ..SparseBuildParams::exact()
1047 };
1048 let index = SparseImpactIndex::build(docs.view(), params).unwrap();
1049 let queries = random_csr(80, n_dims + 4, 12, &mut state);
1050 let mut searcher = index.searcher();
1051 for q in 0..80 {
1052 let (a, b) = (queries.indptr[q] as usize, queries.indptr[q + 1] as usize);
1053 let ids: Vec<u32> = queries.indices[a..b].iter().map(|&c| c as u32).collect();
1054 let mut vals = queries.data[a..b].to_vec();
1055 if q % 9 == 0 && !vals.is_empty() {
1056 vals[0] = -vals[0];
1057 }
1058 for k in [1, 10, 37] {
1059 let want = index.brute_force(&ids, &vals, k);
1060 let got = searcher.search(&ids, &vals, k, SparseSearchParams::exact());
1061 assert_eq!(got, want, "rows {rows} query {q} k {k}");
1062 }
1063 }
1064 assert!(
1066 searcher.skipped_blocks > 0,
1067 "rows {rows}: no block was skipped"
1068 );
1069 }
1070 }
1071
1072 #[test]
1073 fn approximate_mode_returns_true_scores() {
1074 let mut state = 11u64;
1075 let docs = random_csr(3000, 200, 30, &mut state);
1076 let index = SparseImpactIndex::build(
1077 docs.view(),
1078 SparseBuildParams {
1079 postings_per_term: 100,
1080 ..SparseBuildParams::default()
1081 },
1082 )
1083 .unwrap();
1084 let params = SparseSearchParams {
1085 query_cut: 5,
1086 heap_factor: 0.8,
1087 };
1088 let mut searcher = index.searcher();
1089 let (ids, vals) = ([3u32, 40, 41, 150], [1.0f32, 0.5, 0.75, 2.0]);
1090 let got = searcher.search(&ids, &vals, 10, params);
1091 assert!(!got.is_empty());
1092 let mut dense = vec![0f32; 200];
1093 for (&c, &v) in ids.iter().zip(&vals) {
1094 dense[c as usize] = v;
1095 }
1096 for w in got.windows(2) {
1097 assert!(w[0].1 >= w[1].1);
1098 }
1099 for &(d, s) in &got {
1100 assert_eq!(s, index.score(d, &dense));
1101 }
1102 let batch = index
1103 .search_batch(
1104 CsrView {
1105 indptr: &[0, 4],
1106 indices: &[3, 40, 41, 150],
1107 data: &vals,
1108 n_dims: 200,
1109 },
1110 10,
1111 params,
1112 )
1113 .unwrap();
1114 let ids_only: Vec<u32> = got.iter().map(|h| h.0).collect();
1115 assert_eq!(&batch[..ids_only.len()], ids_only.as_slice());
1116 }
1117
1118 #[test]
1119 fn half_values_round_to_nearest() {
1120 for v in [
1121 6.2e-5f32, 0.001, 0.1, 0.333, 1.0, 1.5, 2.75, 7.9, 1000.0, 65504.0,
1122 ] {
1123 let h = f16_value(f16_bits(v));
1124 assert!((h - v).abs() <= v * (1.0 / 2048.0), "{v} -> {h}");
1125 }
1126 assert_eq!(f16_value(f16_bits(0.0)), 0.0);
1127 assert_eq!(f16_value(f16_bits(1e9)), 65504.0);
1128 assert!(f16_value(f16_bits(1e-9)) > 0.0);
1129 }
1130
1131 #[test]
1132 fn rejects_bad_input() {
1133 let bad = |indptr: Vec<i64>, indices: Vec<i32>, data: Vec<f32>| {
1134 SparseImpactIndex::build(
1135 CsrView {
1136 indptr: &indptr,
1137 indices: &indices,
1138 data: &data,
1139 n_dims: 4,
1140 },
1141 SparseBuildParams::exact(),
1142 )
1143 .is_err()
1144 };
1145 assert!(bad(vec![0, 1], vec![4], vec![1.0]));
1146 assert!(bad(vec![0, 1], vec![0], vec![-1.0]));
1147 assert!(bad(vec![0, 1], vec![0], vec![f32::NAN]));
1148 assert!(bad(vec![0, 2], vec![0], vec![1.0]));
1149 assert!(bad(vec![], vec![], vec![]));
1150 }
1151}