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 mut ids: Vec<u64> = allowed.to_vec();
121 ids.sort_unstable();
122 ids.dedup();
123 let postings: usize = lanes
127 .iter()
128 .filter_map(|&(dim, _)| cursors(dim))
129 .map(|c| c.remaining())
130 .sum();
131 let seek_work = ids.len().saturating_mul(lanes.len()).saturating_mul(8);
132 SCRATCH.with(|cell| {
133 let mut scratch = cell.borrow_mut();
134 if seek_work < postings {
135 wand::search_ids(&lanes, &ids, cursors, TopKSink::new(limit), &mut scratch)
136 } else {
137 let set: std::collections::HashSet<u64> = ids.into_iter().collect();
138 wand::search_with(
139 &lanes,
140 |id| set.contains(&id),
141 cursors,
142 TopKSink::new(limit),
143 SearchOptions::default(),
144 &mut scratch,
145 )
146 }
147 })
148}
149
150#[derive(Debug, Serialize, Deserialize)]
152pub struct SparseIndex {
153 dim_map: HashMap<u32, usize>,
155 dim_reverse: Vec<u32>,
157 #[serde(with = "postings_serde")]
159 postings: Vec<Postings>,
160 vectors: HashMap<u64, SparseVector>,
162}
163
164mod postings_serde {
167 use serde::{Deserialize, Deserializer, Serializer};
168
169 use crate::wand::Postings;
170
171 pub fn serialize<S: Serializer>(postings: &[Postings], s: S) -> Result<S::Ok, S::Error> {
172 s.collect_seq(postings.iter().map(|p| {
173 p.as_slice()
174 .iter()
175 .map(|x| (x.id, x.weight))
176 .collect::<Vec<(u64, f32)>>()
177 }))
178 }
179
180 pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<Vec<Postings>, D::Error> {
181 let lists: Vec<Vec<(u64, f32)>> = Deserialize::deserialize(d)?;
182 Ok(lists.into_iter().map(Postings::from_pairs).collect())
183 }
184}
185
186impl Default for SparseIndex {
187 fn default() -> Self {
188 Self::new()
189 }
190}
191
192impl SparseIndex {
193 pub fn new() -> Self {
194 Self {
195 dim_map: HashMap::new(),
196 dim_reverse: Vec::new(),
197 postings: Vec::new(),
198 vectors: HashMap::new(),
199 }
200 }
201
202 pub fn from_parts(
204 dim_map: HashMap<u32, usize>,
205 dim_reverse: Vec<u32>,
206 postings: Vec<Postings>,
207 vectors: HashMap<u64, SparseVector>,
208 ) -> Self {
209 Self {
210 dim_map,
211 dim_reverse,
212 postings,
213 vectors,
214 }
215 }
216
217 pub fn dim_map(&self) -> &HashMap<u32, usize> {
220 &self.dim_map
221 }
222
223 pub fn dim_reverse(&self) -> &[u32] {
224 &self.dim_reverse
225 }
226
227 pub fn postings(&self) -> &[Postings] {
228 &self.postings
229 }
230
231 pub fn postings_mut(&mut self) -> &mut Vec<Postings> {
232 &mut self.postings
233 }
234
235 pub fn vectors(&self) -> &HashMap<u64, SparseVector> {
236 &self.vectors
237 }
238
239 pub fn set_vectors(&mut self, vectors: HashMap<u64, SparseVector>) {
240 self.vectors = vectors;
241 }
242
243 pub fn len(&self) -> usize {
244 self.vectors.len()
245 }
246
247 pub fn is_empty(&self) -> bool {
248 self.vectors.is_empty()
249 }
250
251 fn get_or_create_dim(&mut self, token_id: u32) -> usize {
253 if let Some(&idx) = self.dim_map.get(&token_id) {
254 return idx;
255 }
256 let idx = self.postings.len();
257 self.dim_map.insert(token_id, idx);
258 self.dim_reverse.push(token_id);
259 self.postings.push(Postings::new());
260 idx
261 }
262
263 fn get_dim(&self, token_id: u32) -> Option<usize> {
265 self.dim_map.get(&token_id).copied()
266 }
267
268 pub fn insert(&mut self, node_id: u64, vector: &SparseVector) {
271 if self.vectors.contains_key(&node_id) {
272 self.remove(node_id);
273 }
274
275 for (&token_id, &weight) in vector.indices.iter().zip(&vector.values) {
276 if weight == 0.0 {
277 continue;
278 }
279 let dim_idx = self.get_or_create_dim(token_id);
280 self.postings[dim_idx].upsert(node_id, weight);
281 }
282
283 self.vectors.insert(node_id, vector.clone());
284 }
285
286 pub fn remove(&mut self, node_id: u64) -> bool {
288 let Some(vector) = self.vectors.remove(&node_id) else {
289 return false;
290 };
291 for &token_id in &vector.indices {
292 if let Some(dim_idx) = self.get_dim(token_id) {
293 self.postings[dim_idx].delete(node_id);
294 }
295 }
296 true
297 }
298
299 pub fn search(&self, query: &SparseVector, limit: usize) -> Vec<(u64, f32)> {
302 self.search_with_filter(query, limit, &|_| true)
303 }
304
305 pub fn search_filtered(
307 &self,
308 query: &SparseVector,
309 limit: usize,
310 allowed_ids: &[u64],
311 ) -> Vec<(u64, f32)> {
312 if allowed_ids.is_empty() || query.is_empty() || self.is_empty() {
313 return Vec::new();
314 }
315 run_search_allowed(query, &self.dim_map, limit, allowed_ids, |dim| {
316 self.postings
317 .get(dim as usize)
318 .filter(|p| !p.is_empty())
319 .map(Postings::cursor)
320 })
321 }
322
323 fn search_with_filter<F: Fn(u64) -> bool>(
324 &self,
325 query: &SparseVector,
326 limit: usize,
327 filter: &F,
328 ) -> Vec<(u64, f32)> {
329 if query.is_empty() || self.is_empty() {
330 return Vec::new();
331 }
332 run_search(query, &self.dim_map, limit, filter, |dim| {
333 self.postings
334 .get(dim as usize)
335 .filter(|p| !p.is_empty())
336 .map(Postings::cursor)
337 })
338 }
339
340 pub fn clear(&mut self) {
342 self.dim_map.clear();
343 self.dim_reverse.clear();
344 self.postings.clear();
345 self.vectors.clear();
346 }
347}
348
349#[cfg(test)]
350mod tests {
351 use super::*;
352
353 #[test]
354 fn sparse_vector_basics() {
355 let v = SparseVector::new(vec![1, 3, 5], vec![0.5, 0.3, 0.2]);
356 assert_eq!(v.nnz(), 3);
357 assert!(!v.is_empty());
358
359 let empty = SparseVector::new(vec![], vec![]);
360 assert!(empty.is_empty());
361 }
362
363 #[test]
364 #[should_panic(expected = "indices and values must have same length")]
365 fn sparse_vector_mismatched_lengths() {
366 SparseVector::new(vec![1, 2], vec![0.5]);
367 }
368
369 #[test]
370 fn index_insert_and_search() {
371 let mut index = SparseIndex::new();
372 index.insert(1, &SparseVector::new(vec![1, 2, 3], vec![0.5, 0.3, 0.2]));
373 index.insert(2, &SparseVector::new(vec![2, 3, 4], vec![0.4, 0.6, 0.1]));
374 index.insert(3, &SparseVector::new(vec![1, 4, 5], vec![0.9, 0.1, 0.1]));
375 assert_eq!(index.len(), 3);
376
377 let query = SparseVector::new(vec![1, 2], vec![1.0, 1.0]);
378 let results = index.search(&query, 10);
379
380 assert_eq!(results.len(), 3);
384 assert_eq!(results[0].0, 3);
385 assert!((results[0].1 - 0.9).abs() < 1e-6);
386 assert_eq!(results[1].0, 1);
387 assert!((results[1].1 - 0.8).abs() < 1e-6);
388 assert_eq!(results[2].0, 2);
389 assert!((results[2].1 - 0.4).abs() < 1e-6);
390 }
391
392 #[test]
393 fn index_remove() {
394 let mut index = SparseIndex::new();
395 index.insert(1, &SparseVector::new(vec![1, 2], vec![0.5, 0.3]));
396 index.insert(2, &SparseVector::new(vec![1, 3], vec![0.9, 0.1]));
397 assert_eq!(index.len(), 2);
398
399 assert!(index.remove(1));
400 assert_eq!(index.len(), 1);
401 assert!(!index.remove(1));
402
403 let query = SparseVector::new(vec![1], vec![1.0]);
404 let results = index.search(&query, 10);
405 assert_eq!(results.len(), 1);
406 assert_eq!(results[0].0, 2);
407 }
408
409 #[test]
410 fn index_insert_replaces() {
411 let mut index = SparseIndex::new();
412 index.insert(1, &SparseVector::new(vec![1], vec![0.5]));
413 index.insert(1, &SparseVector::new(vec![2], vec![0.9]));
414 assert_eq!(index.len(), 1);
415
416 let query = SparseVector::new(vec![1, 2], vec![1.0, 1.0]);
417 let results = index.search(&query, 10);
418 assert_eq!(results.len(), 1);
419 assert!((results[0].1 - 0.9).abs() < 1e-6);
420 }
421
422 #[test]
423 fn index_search_limit() {
424 let mut index = SparseIndex::new();
425 for i in 0..100u64 {
426 index.insert(i, &SparseVector::new(vec![1], vec![i as f32]));
427 }
428 let query = SparseVector::new(vec![1], vec![1.0]);
429 let results = index.search(&query, 5);
430 assert_eq!(results.len(), 5);
431 assert_eq!(results[0].0, 99);
432 for p in index.postings() {
433 p.check_invariants().unwrap();
434 }
435 }
436
437 #[test]
438 fn index_search_disjoint() {
439 let mut index = SparseIndex::new();
440 index.insert(1, &SparseVector::new(vec![1, 2], vec![1.0, 1.0]));
441 let query = SparseVector::new(vec![3, 4], vec![1.0, 1.0]);
442 let results = index.search(&query, 10);
443 assert!(results.is_empty());
444 }
445
446 #[test]
447 fn index_empty_search() {
448 let index = SparseIndex::new();
449 let query = SparseVector::new(vec![1], vec![1.0]);
450 assert!(index.search(&query, 10).is_empty());
451 }
452
453 #[test]
454 fn index_clear() {
455 let mut index = SparseIndex::new();
456 index.insert(1, &SparseVector::new(vec![1], vec![0.5]));
457 index.insert(2, &SparseVector::new(vec![2], vec![0.3]));
458 assert_eq!(index.len(), 2);
459
460 index.clear();
461 assert!(index.is_empty());
462 assert!(index
463 .search(&SparseVector::new(vec![1], vec![1.0]), 10)
464 .is_empty());
465 }
466
467 #[test]
468 fn index_remove_cleans_postings() {
469 let mut index = SparseIndex::new();
470 index.insert(1, &SparseVector::new(vec![42], vec![1.0]));
471 index.remove(1);
472 let dim_idx = index.get_dim(42).unwrap();
474 assert!(index.postings[dim_idx].is_empty());
475 }
476
477 #[test]
478 fn zero_weights_are_not_indexed() {
479 let mut index = SparseIndex::new();
480 index.insert(1, &SparseVector::new(vec![1, 2], vec![0.0, 0.5]));
481 index.insert(2, &SparseVector::new(vec![1], vec![0.0]));
482 assert_eq!(index.len(), 2, "the records themselves are kept");
483 assert!(index
485 .search(&SparseVector::new(vec![1], vec![1.0]), 10)
486 .is_empty());
487 let hits = index.search(&SparseVector::new(vec![1, 2], vec![1.0, 1.0]), 10);
488 assert_eq!(hits, vec![(1, 0.5)]);
489 index.insert(2, &SparseVector::new(vec![1], vec![0.7]));
491 let hits = index.search(&SparseVector::new(vec![1], vec![1.0]), 10);
492 assert_eq!(hits, vec![(2, 0.7)]);
493 assert!(index.remove(1));
494 assert!(index.remove(2));
495 assert!(index.is_empty());
496 }
497
498 #[test]
499 fn duplicate_query_dimensions_are_summed() {
500 let mut index = SparseIndex::new();
501 index.insert(1, &SparseVector::new(vec![7], vec![0.5]));
502 let once = index.search(&SparseVector::new(vec![7], vec![1.5]), 10);
503 let twice = index.search(&SparseVector::new(vec![7, 7], vec![1.0, 0.5]), 10);
504 assert_eq!(once, twice);
505 assert_eq!(twice, vec![(1, 0.75)]);
506 }
507
508 #[test]
509 fn search_filtered_basic() {
510 let mut index = SparseIndex::new();
511 index.insert(1, &SparseVector::new(vec![1, 2], vec![0.5, 0.3]));
512 index.insert(2, &SparseVector::new(vec![1, 3], vec![0.9, 0.1]));
513 index.insert(3, &SparseVector::new(vec![1], vec![0.7]));
514
515 let query = SparseVector::new(vec![1], vec![1.0]);
516
517 let results = index.search_filtered(&query, 10, &[1, 3]);
518 assert_eq!(results.len(), 2);
519 assert_eq!(results[0].0, 3); assert_eq!(results[1].0, 1); let results = index.search_filtered(&query, 10, &[2]);
523 assert_eq!(results.len(), 1);
524 assert_eq!(results[0].0, 2);
525 }
526
527 #[test]
528 fn persistence_compat() {
529 let mut index = SparseIndex::new();
531 index.insert(42, &SparseVector::new(vec![1, 2], vec![0.5, 0.3]));
532 index.insert(99, &SparseVector::new(vec![2, 3], vec![0.8, 0.2]));
533
534 let data = bincode::serialize(&index).unwrap();
535 let index2: SparseIndex = bincode::deserialize(&data).unwrap();
536
537 assert_eq!(index2.len(), 2);
538 assert_eq!(index2.postings(), index.postings());
539 let results = index2.search(&SparseVector::new(vec![2], vec![1.0]), 10);
540 assert_eq!(results.len(), 2);
541 assert_eq!(results[0].0, 99);
542 assert!((results[0].1 - 0.8).abs() < 1e-6);
543 }
544
545 #[test]
546 fn dimension_remapping() {
547 let mut index = SparseIndex::new();
548 index.insert(1, &SparseVector::new(vec![100000, 200000], vec![0.5, 0.3]));
550 assert_eq!(index.postings.len(), 2);
551 assert!(index.get_dim(100000).is_some());
552 assert!(index.get_dim(200000).is_some());
553 assert!(index.get_dim(300000).is_none());
554 }
555
556 #[test]
557 fn many_documents_search() {
558 let mut index = SparseIndex::new();
559 for i in 0..1000u64 {
561 let token = (i % 50) as u32; let weight = (i as f32) / 1000.0;
563 index.insert(i, &SparseVector::new(vec![token, token + 50], vec![weight, weight * 0.5]));
564 }
565
566 let query = SparseVector::new(vec![0, 50], vec![1.0, 1.0]);
567 let results = index.search(&query, 5);
568 assert_eq!(results.len(), 5);
569 assert_eq!(results[0].0, 950);
572 }
573}