1use crate::rowid::RowId;
14use crate::Result;
15use std::collections::HashMap;
16use std::sync::Arc;
17
18type Postings = HashMap<u32, Vec<(RowId, f32)>>;
19
20#[derive(Clone)]
22pub struct SparseIndex {
23 frozen: Arc<Vec<Arc<Postings>>>,
24 active: Postings,
25}
26
27impl SparseIndex {
28 pub fn new() -> Self {
29 Self {
30 frozen: Arc::new(Vec::new()),
31 active: HashMap::new(),
32 }
33 }
34
35 pub fn insert(&mut self, terms: &[(u32, f32)], row_id: RowId) {
38 for &(token, weight) in terms {
39 self.active.entry(token).or_default().push((row_id, weight));
40 }
41 }
42
43 pub fn search(&self, query: &[(u32, f32)], k: usize) -> Vec<(RowId, f64)> {
45 self.search_filtered(query, k, |_| true)
46 }
47
48 pub fn search_filtered(
49 &self,
50 query: &[(u32, f32)],
51 k: usize,
52 allowed: impl Fn(RowId) -> bool,
53 ) -> Vec<(RowId, f64)> {
54 let mut scores: HashMap<u64, f64> = HashMap::new();
55 for &(token, q_weight) in query {
56 for postings in self.layers() {
57 if let Some(list) = postings.get(&token) {
58 for &(rid, d_weight) in list {
59 if allowed(rid) {
60 *scores.entry(rid.0).or_insert(0.0) +=
61 f64::from(q_weight) * f64::from(d_weight);
62 }
63 }
64 }
65 }
66 }
67 let mut ranked: Vec<(RowId, f64)> = scores
68 .into_iter()
69 .map(|(rid, score)| (RowId(rid), score))
70 .collect();
71 ranked.sort_by(|a, b| b.1.total_cmp(&a.1).then_with(|| a.0.cmp(&b.0)));
72 ranked.truncate(k);
73 ranked
74 }
75
76 pub fn search_with_context(
77 &self,
78 query: &[(u32, f32)],
79 k: usize,
80 context: Option<&crate::query::AiExecutionContext>,
81 ) -> Result<Vec<(RowId, f64)>> {
82 let mut scores: HashMap<u64, f64> = HashMap::new();
83 for &(token, q_weight) in query {
84 if let Some(context) = context {
85 context.checkpoint()?;
86 }
87 for postings in self.layers() {
88 if let Some(list) = postings.get(&token) {
89 for chunk in list.chunks(256) {
90 if let Some(context) = context {
91 context.consume(chunk.len())?;
92 }
93 for &(rid, d_weight) in chunk {
94 if !scores.contains_key(&rid.0)
95 && scores.len() >= crate::query::MAX_RAW_INDEX_CANDIDATES
96 {
97 return Err(crate::MongrelError::WorkBudgetExceeded);
98 }
99 *scores.entry(rid.0).or_insert(0.0) +=
100 f64::from(q_weight) * f64::from(d_weight);
101 }
102 }
103 }
104 }
105 }
106 let mut ranked: Vec<_> = scores
107 .into_iter()
108 .map(|(rid, score)| (RowId(rid), score))
109 .collect();
110 if let Some(context) = context {
111 context.consume(ranked.len())?;
112 }
113 let order = |left: &(RowId, f64), right: &(RowId, f64)| {
114 right
115 .1
116 .total_cmp(&left.1)
117 .then_with(|| left.0.cmp(&right.0))
118 };
119 if ranked.len() > k {
120 ranked.select_nth_unstable_by(k, order);
121 ranked.truncate(k);
122 }
123 ranked.sort_by(order);
124 Ok(ranked)
125 }
126
127 pub fn candidate_row_ids(&self, query: &[(u32, f32)]) -> Vec<RowId> {
128 let mut row_ids = std::collections::HashSet::new();
129 for (token, _) in query {
130 for postings in self.layers() {
131 if let Some(list) = postings.get(token) {
132 row_ids.extend(list.iter().map(|(row_id, _)| *row_id));
133 }
134 }
135 }
136 row_ids.into_iter().collect()
137 }
138
139 pub fn is_empty(&self) -> bool {
140 self.active.is_empty() && self.frozen.is_empty()
141 }
142
143 pub fn entries(&self) -> Vec<(u32, Vec<(RowId, f32)>)> {
145 let mut entries = HashMap::<u32, Vec<(RowId, f32)>>::new();
146 for postings in self.layers() {
147 for (token, list) in postings {
148 entries.entry(*token).or_default().extend(list);
149 }
150 }
151 entries.into_iter().collect()
152 }
153
154 pub fn from_entries(entries: Vec<(u32, Vec<(RowId, f32)>)>) -> Self {
156 let mut active = HashMap::new();
157 for (t, list) in entries {
158 active.insert(t, list);
159 }
160 Self {
161 frozen: Arc::new(Vec::new()),
162 active,
163 }
164 }
165
166 fn layers(&self) -> impl Iterator<Item = &Postings> {
167 self.frozen
168 .iter()
169 .map(Arc::as_ref)
170 .chain(std::iter::once(&self.active))
171 }
172
173 pub(crate) fn seal(&mut self) {
174 if self.active.is_empty() {
175 return;
176 }
177 Arc::make_mut(&mut self.frozen).push(Arc::new(std::mem::take(&mut self.active)));
178 if self.frozen.len() >= crate::MAX_READ_GENERATION_LAYERS {
179 self.consolidate();
180 }
181 }
182
183 fn consolidate(&mut self) {
184 let entries = self.entries();
185 let mut postings = HashMap::new();
186 for (token, list) in entries {
187 postings.insert(token, list);
188 }
189 self.frozen = Arc::new(vec![Arc::new(postings)]);
190 }
191
192 #[cfg(test)]
193 pub(crate) fn frozen_layer_count(&self) -> usize {
194 self.frozen.len()
195 }
196}
197
198impl Default for SparseIndex {
199 fn default() -> Self {
200 Self::new()
201 }
202}
203
204#[cfg(test)]
205mod tests {
206 use super::*;
207
208 #[test]
209 fn ranks_by_sparse_overlap() {
210 let mut idx = SparseIndex::new();
211 idx.insert(&[(1, 2.0), (2, 1.0)], RowId(0));
213 idx.insert(&[(1, 1.0), (3, 3.0)], RowId(1));
214 idx.insert(&[(2, 5.0)], RowId(2));
215 let top = idx.search(&[(1, 1.0), (2, 1.0)], 3);
217 assert_eq!(top[0], (RowId(2), 5.0));
218 assert_eq!(top[1], (RowId(0), 3.0));
219 assert_eq!(top[2], (RowId(1), 1.0));
220 }
221
222 #[test]
223 fn unique_candidates_stop_at_raw_ceiling() {
224 let mut idx = SparseIndex::new();
225 for row_id in 0..=crate::query::MAX_RAW_INDEX_CANDIDATES {
226 idx.insert(&[(1, 1.0)], RowId(row_id as u64));
227 }
228 let context = crate::query::AiExecutionContext::new(None, usize::MAX);
229 assert!(matches!(
230 idx.search_with_context(&[(1, 1.0)], 1, Some(&context)),
231 Err(crate::MongrelError::WorkBudgetExceeded)
232 ));
233 }
234
235 #[test]
236 fn sealed_generations_merge_postings_and_consolidate() {
237 let mut writer = SparseIndex::new();
238 for id in 0..crate::MAX_READ_GENERATION_LAYERS as u64 + 2 {
239 writer.insert(&[(1, 1.0)], RowId(id));
240 writer.seal();
241 }
242 assert!(writer.frozen_layer_count() < crate::MAX_READ_GENERATION_LAYERS);
243 let generation = writer.clone();
244 writer.insert(&[(1, 10.0)], RowId(99));
245 assert!(!generation
246 .candidate_row_ids(&[(1, 1.0)])
247 .contains(&RowId(99)));
248 assert!(writer.candidate_row_ids(&[(1, 1.0)]).contains(&RowId(99)));
249 }
250}