mongreldb_core/index/
sparse.rs1use crate::rowid::RowId;
14use crate::Result;
15use std::collections::HashMap;
16
17#[derive(Clone)]
19pub struct SparseIndex {
20 postings: HashMap<u32, Vec<(RowId, f32)>>,
21}
22
23impl SparseIndex {
24 pub fn new() -> Self {
25 Self {
26 postings: HashMap::new(),
27 }
28 }
29
30 pub fn insert(&mut self, terms: &[(u32, f32)], row_id: RowId) {
33 for &(token, weight) in terms {
34 self.postings
35 .entry(token)
36 .or_default()
37 .push((row_id, weight));
38 }
39 }
40
41 pub fn search(&self, query: &[(u32, f32)], k: usize) -> Vec<(RowId, f64)> {
43 self.search_filtered(query, k, |_| true)
44 }
45
46 pub fn search_filtered(
47 &self,
48 query: &[(u32, f32)],
49 k: usize,
50 allowed: impl Fn(RowId) -> bool,
51 ) -> Vec<(RowId, f64)> {
52 let mut scores: HashMap<u64, f64> = HashMap::new();
53 for &(token, q_weight) in query {
54 if let Some(list) = self.postings.get(&token) {
55 for &(rid, d_weight) in list {
56 if allowed(rid) {
57 *scores.entry(rid.0).or_insert(0.0) +=
58 f64::from(q_weight) * f64::from(d_weight);
59 }
60 }
61 }
62 }
63 let mut ranked: Vec<(RowId, f64)> = scores
64 .into_iter()
65 .map(|(rid, score)| (RowId(rid), score))
66 .collect();
67 ranked.sort_by(|a, b| b.1.total_cmp(&a.1).then_with(|| a.0.cmp(&b.0)));
68 ranked.truncate(k);
69 ranked
70 }
71
72 pub fn search_with_context(
73 &self,
74 query: &[(u32, f32)],
75 k: usize,
76 context: Option<&crate::query::AiExecutionContext>,
77 ) -> Result<Vec<(RowId, f64)>> {
78 let mut scores: HashMap<u64, f64> = HashMap::new();
79 for &(token, q_weight) in query {
80 if let Some(context) = context {
81 context.checkpoint()?;
82 }
83 if let Some(list) = self.postings.get(&token) {
84 for chunk in list.chunks(256) {
85 if let Some(context) = context {
86 context.consume(chunk.len())?;
87 }
88 for &(rid, d_weight) in chunk {
89 if !scores.contains_key(&rid.0)
90 && scores.len() >= crate::query::MAX_RAW_INDEX_CANDIDATES
91 {
92 return Err(crate::MongrelError::WorkBudgetExceeded);
93 }
94 *scores.entry(rid.0).or_insert(0.0) +=
95 f64::from(q_weight) * f64::from(d_weight);
96 }
97 }
98 }
99 }
100 let mut ranked: Vec<_> = scores
101 .into_iter()
102 .map(|(rid, score)| (RowId(rid), score))
103 .collect();
104 if let Some(context) = context {
105 context.consume(ranked.len())?;
106 }
107 let order = |left: &(RowId, f64), right: &(RowId, f64)| {
108 right
109 .1
110 .total_cmp(&left.1)
111 .then_with(|| left.0.cmp(&right.0))
112 };
113 if ranked.len() > k {
114 ranked.select_nth_unstable_by(k, order);
115 ranked.truncate(k);
116 }
117 ranked.sort_by(order);
118 Ok(ranked)
119 }
120
121 pub fn candidate_row_ids(&self, query: &[(u32, f32)]) -> Vec<RowId> {
122 let mut row_ids = std::collections::HashSet::new();
123 for (token, _) in query {
124 if let Some(postings) = self.postings.get(token) {
125 row_ids.extend(postings.iter().map(|(row_id, _)| *row_id));
126 }
127 }
128 row_ids.into_iter().collect()
129 }
130
131 pub fn is_empty(&self) -> bool {
132 self.postings.is_empty()
133 }
134
135 pub fn entries(&self) -> Vec<(u32, Vec<(RowId, f32)>)> {
137 self.postings
138 .iter()
139 .map(|(t, list)| (*t, list.clone()))
140 .collect()
141 }
142
143 pub fn from_entries(entries: Vec<(u32, Vec<(RowId, f32)>)>) -> Self {
145 let mut postings = HashMap::new();
146 for (t, list) in entries {
147 postings.insert(t, list);
148 }
149 Self { postings }
150 }
151}
152
153impl Default for SparseIndex {
154 fn default() -> Self {
155 Self::new()
156 }
157}
158
159#[cfg(test)]
160mod tests {
161 use super::*;
162
163 #[test]
164 fn ranks_by_sparse_overlap() {
165 let mut idx = SparseIndex::new();
166 idx.insert(&[(1, 2.0), (2, 1.0)], RowId(0));
168 idx.insert(&[(1, 1.0), (3, 3.0)], RowId(1));
169 idx.insert(&[(2, 5.0)], RowId(2));
170 let top = idx.search(&[(1, 1.0), (2, 1.0)], 3);
172 assert_eq!(top[0], (RowId(2), 5.0));
173 assert_eq!(top[1], (RowId(0), 3.0));
174 assert_eq!(top[2], (RowId(1), 1.0));
175 }
176
177 #[test]
178 fn unique_candidates_stop_at_raw_ceiling() {
179 let mut idx = SparseIndex::new();
180 for row_id in 0..=crate::query::MAX_RAW_INDEX_CANDIDATES {
181 idx.insert(&[(1, 1.0)], RowId(row_id as u64));
182 }
183 let context = crate::query::AiExecutionContext::new(None, usize::MAX);
184 assert!(matches!(
185 idx.search_with_context(&[(1, 1.0)], 1, Some(&context)),
186 Err(crate::MongrelError::WorkBudgetExceeded)
187 ));
188 }
189}