1use std::cell::RefCell;
17use std::collections::HashMap;
18
19use serde::{Deserialize, Serialize};
20
21use crate::wand::{self, DimId, PostingCursor, Postings, Scratch, SearchOptions, TopKSink};
22
23#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
25pub struct SparseVector {
26 pub indices: Vec<u32>,
27 pub values: Vec<f32>,
28}
29
30impl SparseVector {
31 pub fn new(indices: Vec<u32>, values: Vec<f32>) -> Self {
32 assert_eq!(
33 indices.len(),
34 values.len(),
35 "indices and values must have same length"
36 );
37 Self { indices, values }
38 }
39
40 pub fn nnz(&self) -> usize {
41 self.indices.len()
42 }
43
44 pub fn is_empty(&self) -> bool {
45 self.indices.is_empty()
46 }
47}
48
49thread_local! {
50 static SCRATCH: RefCell<Scratch> = RefCell::new(Scratch::new());
52}
53
54pub(crate) fn run_search<C, F, R>(
60 query: &SparseVector,
61 dim_map: &HashMap<u32, usize>,
62 limit: usize,
63 filter: F,
64 cursors: R,
65) -> Vec<(u64, f32)>
66where
67 C: PostingCursor,
68 F: Fn(u64) -> bool,
69 R: FnMut(DimId) -> Option<C>,
70{
71 let lanes: Vec<(DimId, f32)> = query
72 .indices
73 .iter()
74 .zip(&query.values)
75 .filter_map(|(token, &w)| dim_map.get(token).map(|&dim| (dim as DimId, w)))
76 .collect();
77 if lanes.is_empty() {
78 return Vec::new();
79 }
80 SCRATCH.with(|cell| {
81 let mut scratch = cell.borrow_mut();
82 wand::search_with(
83 &lanes,
84 filter,
85 cursors,
86 TopKSink::new(limit),
87 SearchOptions::default(),
88 &mut scratch,
89 )
90 })
91}
92
93pub(crate) fn run_search_allowed<C, R>(
98 query: &SparseVector,
99 dim_map: &HashMap<u32, usize>,
100 limit: usize,
101 allowed: &[u64],
102 mut cursors: R,
103) -> Vec<(u64, f32)>
104where
105 C: PostingCursor,
106 R: FnMut(DimId) -> Option<C>,
107{
108 if allowed.is_empty() {
109 return Vec::new();
110 }
111 let lanes: Vec<(DimId, f32)> = query
112 .indices
113 .iter()
114 .zip(&query.values)
115 .filter_map(|(token, &w)| dim_map.get(token).map(|&dim| (dim as DimId, w)))
116 .collect();
117 if lanes.is_empty() {
118 return Vec::new();
119 }
120 let sorted_unique = allowed.windows(2).all(|w| w[0] < w[1]);
127 let owned: Vec<u64>;
128 let ids: &[u64] = if sorted_unique {
129 allowed
130 } else {
131 let mut v = allowed.to_vec();
132 v.sort_unstable();
133 v.dedup();
134 owned = v;
135 &owned
136 };
137 let postings: usize = lanes
141 .iter()
142 .filter_map(|&(dim, _)| cursors(dim))
143 .map(|c| c.remaining())
144 .sum();
145 let seek_work = ids.len().saturating_mul(lanes.len()).saturating_mul(8);
146 SCRATCH.with(|cell| {
147 let mut scratch = cell.borrow_mut();
148 if seek_work < postings {
149 wand::search_ids(&lanes, ids, cursors, TopKSink::new(limit), &mut scratch)
150 } else if sorted_unique {
151 wand::search_with(
155 &lanes,
156 |id| ids.binary_search(&id).is_ok(),
157 cursors,
158 TopKSink::new(limit),
159 SearchOptions::default(),
160 &mut scratch,
161 )
162 } else {
163 let set: std::collections::HashSet<u64> = ids.iter().copied().collect();
164 wand::search_with(
165 &lanes,
166 |id| set.contains(&id),
167 cursors,
168 TopKSink::new(limit),
169 SearchOptions::default(),
170 &mut scratch,
171 )
172 }
173 })
174}
175
176#[derive(Debug, Serialize, Deserialize)]
178pub struct SparseIndex {
179 dim_map: HashMap<u32, usize>,
181 dim_reverse: Vec<u32>,
183 #[serde(with = "postings_serde")]
185 postings: Vec<Postings>,
186 vectors: HashMap<u64, SparseVector>,
188}
189
190mod postings_serde {
193 use serde::{Deserialize, Deserializer, Serializer};
194
195 use crate::wand::Postings;
196
197 pub fn serialize<S: Serializer>(postings: &[Postings], s: S) -> Result<S::Ok, S::Error> {
198 s.collect_seq(postings.iter().map(|p| {
199 p.as_slice()
200 .iter()
201 .map(|x| (x.id, x.weight))
202 .collect::<Vec<(u64, f32)>>()
203 }))
204 }
205
206 pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<Vec<Postings>, D::Error> {
207 let lists: Vec<Vec<(u64, f32)>> = Deserialize::deserialize(d)?;
208 Ok(lists.into_iter().map(Postings::from_pairs).collect())
209 }
210}
211
212impl Default for SparseIndex {
213 fn default() -> Self {
214 Self::new()
215 }
216}
217
218impl SparseIndex {
219 pub fn new() -> Self {
220 Self {
221 dim_map: HashMap::new(),
222 dim_reverse: Vec::new(),
223 postings: Vec::new(),
224 vectors: HashMap::new(),
225 }
226 }
227
228 pub fn from_parts(
230 dim_map: HashMap<u32, usize>,
231 dim_reverse: Vec<u32>,
232 postings: Vec<Postings>,
233 vectors: HashMap<u64, SparseVector>,
234 ) -> Self {
235 Self {
236 dim_map,
237 dim_reverse,
238 postings,
239 vectors,
240 }
241 }
242
243 pub fn dim_map(&self) -> &HashMap<u32, usize> {
246 &self.dim_map
247 }
248
249 pub fn dim_reverse(&self) -> &[u32] {
250 &self.dim_reverse
251 }
252
253 pub fn postings(&self) -> &[Postings] {
254 &self.postings
255 }
256
257 pub fn postings_mut(&mut self) -> &mut Vec<Postings> {
258 &mut self.postings
259 }
260
261 pub fn vectors(&self) -> &HashMap<u64, SparseVector> {
262 &self.vectors
263 }
264
265 pub fn set_vectors(&mut self, vectors: HashMap<u64, SparseVector>) {
266 self.vectors = vectors;
267 }
268
269 pub fn len(&self) -> usize {
270 self.vectors.len()
271 }
272
273 pub fn is_empty(&self) -> bool {
274 self.vectors.is_empty()
275 }
276
277 fn get_or_create_dim(&mut self, token_id: u32) -> usize {
279 if let Some(&idx) = self.dim_map.get(&token_id) {
280 return idx;
281 }
282 let idx = self.postings.len();
283 self.dim_map.insert(token_id, idx);
284 self.dim_reverse.push(token_id);
285 self.postings.push(Postings::new());
286 idx
287 }
288
289 fn get_dim(&self, token_id: u32) -> Option<usize> {
291 self.dim_map.get(&token_id).copied()
292 }
293
294 pub fn insert(&mut self, node_id: u64, vector: &SparseVector) {
297 if self.vectors.contains_key(&node_id) {
298 self.remove(node_id);
299 }
300
301 for (&token_id, &weight) in vector.indices.iter().zip(&vector.values) {
302 if weight == 0.0 {
303 continue;
304 }
305 let dim_idx = self.get_or_create_dim(token_id);
306 self.postings[dim_idx].upsert(node_id, weight);
307 }
308
309 self.vectors.insert(node_id, vector.clone());
310 }
311
312 pub fn remove(&mut self, node_id: u64) -> bool {
314 let Some(vector) = self.vectors.remove(&node_id) else {
315 return false;
316 };
317 for &token_id in &vector.indices {
318 if let Some(dim_idx) = self.get_dim(token_id) {
319 self.postings[dim_idx].delete(node_id);
320 }
321 }
322 true
323 }
324
325 pub fn search(&self, query: &SparseVector, limit: usize) -> Vec<(u64, f32)> {
328 self.search_with_filter(query, limit, &|_| true)
329 }
330
331 pub fn search_filtered(
333 &self,
334 query: &SparseVector,
335 limit: usize,
336 allowed_ids: &[u64],
337 ) -> Vec<(u64, f32)> {
338 if allowed_ids.is_empty() || query.is_empty() || self.is_empty() {
339 return Vec::new();
340 }
341 run_search_allowed(query, &self.dim_map, limit, allowed_ids, |dim| {
342 self.postings
343 .get(dim as usize)
344 .filter(|p| !p.is_empty())
345 .map(Postings::cursor)
346 })
347 }
348
349 fn search_with_filter<F: Fn(u64) -> bool>(
350 &self,
351 query: &SparseVector,
352 limit: usize,
353 filter: &F,
354 ) -> Vec<(u64, f32)> {
355 if query.is_empty() || self.is_empty() {
356 return Vec::new();
357 }
358 run_search(query, &self.dim_map, limit, filter, |dim| {
359 self.postings
360 .get(dim as usize)
361 .filter(|p| !p.is_empty())
362 .map(Postings::cursor)
363 })
364 }
365
366 pub fn clear(&mut self) {
368 self.dim_map.clear();
369 self.dim_reverse.clear();
370 self.postings.clear();
371 self.vectors.clear();
372 }
373}
374
375#[cfg(test)]
376mod tests {
377 use super::*;
378
379 #[test]
380 fn sparse_vector_basics() {
381 let v = SparseVector::new(vec![1, 3, 5], vec![0.5, 0.3, 0.2]);
382 assert_eq!(v.nnz(), 3);
383 assert!(!v.is_empty());
384
385 let empty = SparseVector::new(vec![], vec![]);
386 assert!(empty.is_empty());
387 }
388
389 #[test]
390 #[should_panic(expected = "indices and values must have same length")]
391 fn sparse_vector_mismatched_lengths() {
392 SparseVector::new(vec![1, 2], vec![0.5]);
393 }
394
395 #[test]
396 fn index_insert_and_search() {
397 let mut index = SparseIndex::new();
398 index.insert(1, &SparseVector::new(vec![1, 2, 3], vec![0.5, 0.3, 0.2]));
399 index.insert(2, &SparseVector::new(vec![2, 3, 4], vec![0.4, 0.6, 0.1]));
400 index.insert(3, &SparseVector::new(vec![1, 4, 5], vec![0.9, 0.1, 0.1]));
401 assert_eq!(index.len(), 3);
402
403 let query = SparseVector::new(vec![1, 2], vec![1.0, 1.0]);
404 let results = index.search(&query, 10);
405
406 assert_eq!(results.len(), 3);
410 assert_eq!(results[0].0, 3);
411 assert!((results[0].1 - 0.9).abs() < 1e-6);
412 assert_eq!(results[1].0, 1);
413 assert!((results[1].1 - 0.8).abs() < 1e-6);
414 assert_eq!(results[2].0, 2);
415 assert!((results[2].1 - 0.4).abs() < 1e-6);
416 }
417
418 #[test]
419 fn index_remove() {
420 let mut index = SparseIndex::new();
421 index.insert(1, &SparseVector::new(vec![1, 2], vec![0.5, 0.3]));
422 index.insert(2, &SparseVector::new(vec![1, 3], vec![0.9, 0.1]));
423 assert_eq!(index.len(), 2);
424
425 assert!(index.remove(1));
426 assert_eq!(index.len(), 1);
427 assert!(!index.remove(1));
428
429 let query = SparseVector::new(vec![1], vec![1.0]);
430 let results = index.search(&query, 10);
431 assert_eq!(results.len(), 1);
432 assert_eq!(results[0].0, 2);
433 }
434
435 #[test]
436 fn index_insert_replaces() {
437 let mut index = SparseIndex::new();
438 index.insert(1, &SparseVector::new(vec![1], vec![0.5]));
439 index.insert(1, &SparseVector::new(vec![2], vec![0.9]));
440 assert_eq!(index.len(), 1);
441
442 let query = SparseVector::new(vec![1, 2], vec![1.0, 1.0]);
443 let results = index.search(&query, 10);
444 assert_eq!(results.len(), 1);
445 assert!((results[0].1 - 0.9).abs() < 1e-6);
446 }
447
448 #[test]
449 fn index_search_limit() {
450 let mut index = SparseIndex::new();
451 for i in 0..100u64 {
452 index.insert(i, &SparseVector::new(vec![1], vec![i as f32]));
453 }
454 let query = SparseVector::new(vec![1], vec![1.0]);
455 let results = index.search(&query, 5);
456 assert_eq!(results.len(), 5);
457 assert_eq!(results[0].0, 99);
458 for p in index.postings() {
459 p.check_invariants().unwrap();
460 }
461 }
462
463 #[test]
464 fn index_search_disjoint() {
465 let mut index = SparseIndex::new();
466 index.insert(1, &SparseVector::new(vec![1, 2], vec![1.0, 1.0]));
467 let query = SparseVector::new(vec![3, 4], vec![1.0, 1.0]);
468 let results = index.search(&query, 10);
469 assert!(results.is_empty());
470 }
471
472 #[test]
473 fn index_empty_search() {
474 let index = SparseIndex::new();
475 let query = SparseVector::new(vec![1], vec![1.0]);
476 assert!(index.search(&query, 10).is_empty());
477 }
478
479 #[test]
480 fn index_clear() {
481 let mut index = SparseIndex::new();
482 index.insert(1, &SparseVector::new(vec![1], vec![0.5]));
483 index.insert(2, &SparseVector::new(vec![2], vec![0.3]));
484 assert_eq!(index.len(), 2);
485
486 index.clear();
487 assert!(index.is_empty());
488 assert!(index
489 .search(&SparseVector::new(vec![1], vec![1.0]), 10)
490 .is_empty());
491 }
492
493 #[test]
494 fn index_remove_cleans_postings() {
495 let mut index = SparseIndex::new();
496 index.insert(1, &SparseVector::new(vec![42], vec![1.0]));
497 index.remove(1);
498 let dim_idx = index.get_dim(42).unwrap();
500 assert!(index.postings[dim_idx].is_empty());
501 }
502
503 #[test]
504 fn zero_weights_are_not_indexed() {
505 let mut index = SparseIndex::new();
506 index.insert(1, &SparseVector::new(vec![1, 2], vec![0.0, 0.5]));
507 index.insert(2, &SparseVector::new(vec![1], vec![0.0]));
508 assert_eq!(index.len(), 2, "the records themselves are kept");
509 assert!(index
511 .search(&SparseVector::new(vec![1], vec![1.0]), 10)
512 .is_empty());
513 let hits = index.search(&SparseVector::new(vec![1, 2], vec![1.0, 1.0]), 10);
514 assert_eq!(hits, vec![(1, 0.5)]);
515 index.insert(2, &SparseVector::new(vec![1], vec![0.7]));
517 let hits = index.search(&SparseVector::new(vec![1], vec![1.0]), 10);
518 assert_eq!(hits, vec![(2, 0.7)]);
519 assert!(index.remove(1));
520 assert!(index.remove(2));
521 assert!(index.is_empty());
522 }
523
524 #[test]
525 fn duplicate_query_dimensions_are_summed() {
526 let mut index = SparseIndex::new();
527 index.insert(1, &SparseVector::new(vec![7], vec![0.5]));
528 let once = index.search(&SparseVector::new(vec![7], vec![1.5]), 10);
529 let twice = index.search(&SparseVector::new(vec![7, 7], vec![1.0, 0.5]), 10);
530 assert_eq!(once, twice);
531 assert_eq!(twice, vec![(1, 0.75)]);
532 }
533
534 #[test]
535 fn search_filtered_basic() {
536 let mut index = SparseIndex::new();
537 index.insert(1, &SparseVector::new(vec![1, 2], vec![0.5, 0.3]));
538 index.insert(2, &SparseVector::new(vec![1, 3], vec![0.9, 0.1]));
539 index.insert(3, &SparseVector::new(vec![1], vec![0.7]));
540
541 let query = SparseVector::new(vec![1], vec![1.0]);
542
543 let results = index.search_filtered(&query, 10, &[1, 3]);
544 assert_eq!(results.len(), 2);
545 assert_eq!(results[0].0, 3); assert_eq!(results[1].0, 1); let results = index.search_filtered(&query, 10, &[2]);
549 assert_eq!(results.len(), 1);
550 assert_eq!(results[0].0, 2);
551 }
552
553 #[test]
554 fn persistence_compat() {
555 let mut index = SparseIndex::new();
557 index.insert(42, &SparseVector::new(vec![1, 2], vec![0.5, 0.3]));
558 index.insert(99, &SparseVector::new(vec![2, 3], vec![0.8, 0.2]));
559
560 let data = bincode::serialize(&index).unwrap();
561 let index2: SparseIndex = bincode::deserialize(&data).unwrap();
562
563 assert_eq!(index2.len(), 2);
564 assert_eq!(index2.postings(), index.postings());
565 let results = index2.search(&SparseVector::new(vec![2], vec![1.0]), 10);
566 assert_eq!(results.len(), 2);
567 assert_eq!(results[0].0, 99);
568 assert!((results[0].1 - 0.8).abs() < 1e-6);
569 }
570
571 #[test]
572 fn dimension_remapping() {
573 let mut index = SparseIndex::new();
574 index.insert(1, &SparseVector::new(vec![100000, 200000], vec![0.5, 0.3]));
576 assert_eq!(index.postings.len(), 2);
577 assert!(index.get_dim(100000).is_some());
578 assert!(index.get_dim(200000).is_some());
579 assert!(index.get_dim(300000).is_none());
580 }
581
582 #[test]
583 fn many_documents_search() {
584 let mut index = SparseIndex::new();
585 for i in 0..1000u64 {
587 let token = (i % 50) as u32; let weight = (i as f32) / 1000.0;
589 index.insert(i, &SparseVector::new(vec![token, token + 50], vec![weight, weight * 0.5]));
590 }
591
592 let query = SparseVector::new(vec![0, 50], vec![1.0, 1.0]);
593 let results = index.search(&query, 5);
594 assert_eq!(results.len(), 5);
595 assert_eq!(results[0].0, 950);
598 }
599}