1use std::collections::BTreeMap;
16use std::sync::Arc;
17
18use index::bloom_filter::applier::InListPredicate;
19use index::inverted_index::search::predicate::{Predicate, RangePredicate};
20use index::target::IndexTarget;
21use moka::notification::RemovalCause;
22use moka::sync::Cache;
23use store_api::storage::FileId;
24
25use crate::metrics::{CACHE_BYTES, CACHE_EVICTION, CACHE_HIT, CACHE_MISS};
26use crate::sst::index::fulltext_index::applier::builder::{
27 FulltextLike, FulltextQuery, FulltextRequest, FulltextTerm,
28};
29use crate::sst::parquet::row_selection::RowGroupSelection;
30
31const INDEX_RESULT_TYPE: &str = "index_result";
32
33pub struct IndexResultCache {
40 cache: Cache<(PredicateKey, FileId), Arc<RowGroupSelection>>,
41}
42
43impl IndexResultCache {
44 pub fn new(capacity: u64) -> Self {
46 fn to_str(cause: RemovalCause) -> &'static str {
47 match cause {
48 RemovalCause::Expired => "expired",
49 RemovalCause::Explicit => "explicit",
50 RemovalCause::Replaced => "replaced",
51 RemovalCause::Size => "size",
52 }
53 }
54
55 let cache = Cache::builder()
56 .max_capacity(capacity)
57 .weigher(Self::index_result_cache_weight)
58 .eviction_listener(|k, v, cause| {
59 let size = Self::index_result_cache_weight(&k, &v);
60 CACHE_BYTES
61 .with_label_values(&[INDEX_RESULT_TYPE])
62 .sub(size.into());
63 CACHE_EVICTION
64 .with_label_values(&[INDEX_RESULT_TYPE, to_str(cause)])
65 .inc();
66 })
67 .support_invalidation_closures()
68 .build();
69 Self { cache }
70 }
71
72 pub fn put(&self, key: PredicateKey, file_id: FileId, result: Arc<RowGroupSelection>) {
76 let key = (key, file_id);
77 let size = Self::index_result_cache_weight(&key, &result);
78 CACHE_BYTES
79 .with_label_values(&[INDEX_RESULT_TYPE])
80 .add(size.into());
81 self.cache.insert(key, result);
82 }
83
84 pub fn get(&self, key: &PredicateKey, file_id: FileId) -> Option<Arc<RowGroupSelection>> {
89 let res = self.cache.get(&(key.clone(), file_id));
90 if res.is_some() {
91 CACHE_HIT.with_label_values(&[INDEX_RESULT_TYPE]).inc();
92 } else {
93 CACHE_MISS.with_label_values(&[INDEX_RESULT_TYPE]).inc()
94 }
95 res
96 }
97
98 fn index_result_cache_weight(k: &(PredicateKey, FileId), v: &Arc<RowGroupSelection>) -> u32 {
100 k.0.mem_usage() as u32 + v.mem_usage() as u32
101 }
102
103 pub fn invalidate_file(&self, file_id: FileId) {
105 self.cache
106 .invalidate_entries_if(move |(_, cached_file_id), _| *cached_file_id == file_id)
107 .expect("cache should support invalidation closures");
108 }
109}
110
111#[derive(Debug, Clone, PartialEq, Eq, Hash)]
113pub enum PredicateKey {
114 Fulltext(FulltextIndexKey),
116 Bloom(BloomFilterKey),
118 Inverted(InvertedIndexKey),
120 MinMax(MinMaxKey),
122}
123
124impl PredicateKey {
125 pub fn new_fulltext(predicates: Arc<BTreeMap<IndexTarget, FulltextRequest>>) -> Self {
127 Self::Fulltext(FulltextIndexKey::new(predicates))
128 }
129
130 pub fn new_bloom(predicates: Arc<BTreeMap<IndexTarget, Vec<InListPredicate>>>) -> Self {
132 Self::Bloom(BloomFilterKey::new(predicates))
133 }
134
135 pub fn new_inverted(predicates: Arc<BTreeMap<IndexTarget, Vec<Predicate>>>) -> Self {
137 Self::Inverted(InvertedIndexKey::new(predicates))
138 }
139
140 pub fn new_minmax(exprs: Arc<Vec<String>>, schema_version: u64, skip_fields: bool) -> Self {
142 Self::MinMax(MinMaxKey::new(exprs, schema_version, skip_fields))
143 }
144
145 pub fn mem_usage(&self) -> usize {
147 match self {
148 Self::Fulltext(key) => key.mem_usage,
149 Self::Bloom(key) => key.mem_usage,
150 Self::Inverted(key) => key.mem_usage,
151 Self::MinMax(key) => key.mem_usage,
152 }
153 }
154}
155
156#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
158pub struct FulltextIndexKey {
159 predicates: Arc<BTreeMap<IndexTarget, FulltextRequest>>,
160 mem_usage: usize,
161}
162
163impl FulltextIndexKey {
164 pub fn new(predicates: Arc<BTreeMap<IndexTarget, FulltextRequest>>) -> Self {
167 let mem_usage = predicates
168 .iter()
169 .map(|(target, request)| {
170 let target_size = target.approx_mem_size();
171 let query_size = request
172 .queries
173 .iter()
174 .map(|query| query.0.len() + size_of::<FulltextQuery>())
175 .sum::<usize>();
176 let term_size = request
177 .terms
178 .iter()
179 .map(|term| term.term.len() + size_of::<FulltextTerm>())
180 .sum::<usize>();
181 let like_size = request
182 .likes
183 .iter()
184 .map(|like| like.pattern.len() + size_of::<FulltextLike>())
185 .sum::<usize>();
186 target_size + query_size + term_size + like_size
187 })
188 .sum();
189 Self {
190 predicates,
191 mem_usage,
192 }
193 }
194}
195
196#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
198pub struct BloomFilterKey {
199 predicates: Arc<BTreeMap<IndexTarget, Vec<InListPredicate>>>,
200 mem_usage: usize,
201}
202
203impl BloomFilterKey {
204 pub fn new(predicates: Arc<BTreeMap<IndexTarget, Vec<InListPredicate>>>) -> Self {
207 let mem_usage = predicates
208 .iter()
209 .map(|(target, predicates)| {
210 target.approx_mem_size()
211 + predicates
212 .iter()
213 .map(|predicate| {
214 predicate.list.iter().map(|list| list.len()).sum::<usize>()
215 })
216 .sum::<usize>()
217 })
218 .sum();
219 Self {
220 predicates,
221 mem_usage,
222 }
223 }
224}
225
226#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
228pub struct InvertedIndexKey {
229 predicates: Arc<BTreeMap<IndexTarget, Vec<Predicate>>>,
230 mem_usage: usize,
231}
232
233impl InvertedIndexKey {
234 pub fn new(predicates: Arc<BTreeMap<IndexTarget, Vec<Predicate>>>) -> Self {
237 let mem_usage = predicates
238 .iter()
239 .map(|(target, predicates)| {
240 target.approx_mem_size()
241 + predicates
242 .iter()
243 .map(|predicate| match predicate {
244 Predicate::InList(predicate) => {
245 predicate.list.iter().map(|list| list.len()).sum::<usize>()
246 }
247 Predicate::Range(_) => size_of::<RangePredicate>(),
248 Predicate::RegexMatch(predicate) => predicate.pattern.len(),
249 })
250 .sum::<usize>()
251 })
252 .sum();
253
254 Self {
255 predicates,
256 mem_usage,
257 }
258 }
259}
260
261#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
263pub struct MinMaxKey {
264 exprs: Arc<Vec<String>>,
265 schema_version: u64,
266 skip_fields: bool,
267 mem_usage: usize,
268}
269
270impl MinMaxKey {
271 pub fn new(exprs: Arc<Vec<String>>, schema_version: u64, skip_fields: bool) -> Self {
272 let mem_usage = size_of::<Self>()
273 + size_of::<Vec<String>>()
274 + exprs.len() * size_of::<String>()
275 + exprs.iter().map(|s| s.len()).sum::<usize>();
276 Self {
277 exprs,
278 schema_version,
279 skip_fields,
280 mem_usage,
281 }
282 }
283}
284
285#[cfg(test)]
286#[allow(clippy::single_range_in_vec_init)]
287mod tests {
288 use std::collections::{BTreeMap, BTreeSet};
289 use std::sync::Arc;
290
291 use index::bloom_filter::applier::InListPredicate as BloomInListPredicate;
292 use index::inverted_index::search::predicate::{Predicate, Range, RangePredicate};
293
294 use super::*;
295 use crate::sst::index::fulltext_index::applier::builder::{
296 FulltextQuery, FulltextRequest, FulltextTerm,
297 };
298 use crate::sst::parquet::row_selection::RowGroupSelection;
299
300 #[test]
301 fn test_inverted_cache_separates_json_paths() {
302 let key = |path: &str| {
303 PredicateKey::new_inverted(Arc::new(BTreeMap::from([(
304 IndexTarget::new_json_path(1, vec![path.into()]).unwrap(),
305 vec![Predicate::Range(RangePredicate {
306 range: Range {
307 lower: None,
308 upper: None,
309 },
310 })],
311 )])))
312 };
313 let a = key("a");
314 let b = key("b");
315 let cache = IndexResultCache::new(10000);
316 let file = FileId::random();
317 cache.put(a.clone(), file, Arc::new(RowGroupSelection::new(10, 10)));
318 assert!(cache.get(&a, file).is_some());
319 assert!(cache.get(&b, file).is_none());
320 }
321
322 #[test]
323 fn test_cache_basic_operations() {
324 let cache = IndexResultCache::new(1000);
325 let file_id = FileId::random();
326
327 let predicates = BTreeMap::new();
329 let key = PredicateKey::new_fulltext(Arc::new(predicates));
330 let selection = Arc::new(RowGroupSelection::from_row_ids(
331 [1, 2, 3].into_iter().collect(),
332 1,
333 10,
334 ));
335
336 cache.put(key.clone(), file_id, selection.clone());
338 let retrieved = cache.get(&key, file_id);
339 assert!(retrieved.is_some());
340 assert_eq!(
341 retrieved.unwrap().as_ref().row_count(),
342 selection.as_ref().row_count()
343 );
344
345 let non_existent_file_id = FileId::random();
347 assert!(cache.get(&key, non_existent_file_id).is_none());
348 }
349
350 #[test]
351 fn test_minmax_key_should_distinguish_schema_version_and_skip_fields() {
352 let exprs = Arc::new(vec!["col > 1".to_string()]);
353
354 let key1 = PredicateKey::new_minmax(exprs.clone(), 1, false);
355 let key2 = PredicateKey::new_minmax(exprs.clone(), 2, false);
356 assert_ne!(key1, key2);
357
358 let key3 = PredicateKey::new_minmax(exprs, 1, true);
359 assert_ne!(key1, key3);
360 }
361
362 #[test]
363 fn test_cache_capacity_limit() {
364 let cache = IndexResultCache::new(100);
366 let file_id1 = FileId::random();
367 let file_id2 = FileId::random();
368
369 let mut predicates1 = BTreeMap::new();
371 let request1 = FulltextRequest {
372 queries: vec![
373 FulltextQuery(
374 "test query 1 with a very long string to ensure large weight".to_string(),
375 ),
376 FulltextQuery("another long query string".to_string()),
377 ],
378 terms: vec![],
379 likes: vec![],
380 };
381 predicates1.insert(IndexTarget::ColumnId(1), request1);
382 let key1 = PredicateKey::new_fulltext(Arc::new(predicates1));
383 let selection1 = Arc::new(RowGroupSelection::default());
384
385 let mut predicates2 = BTreeMap::new();
386 let request2 = FulltextRequest {
387 queries: vec![
388 FulltextQuery(
389 "test query 2 with a very long string to ensure large weight".to_string(),
390 ),
391 FulltextQuery("another long query string".to_string()),
392 ],
393 terms: vec![],
394 likes: vec![],
395 };
396 predicates2.insert(IndexTarget::ColumnId(1), request2);
397 let key2 = PredicateKey::new_fulltext(Arc::new(predicates2));
398 let selection2 = Arc::new(RowGroupSelection::default());
399
400 let weight1 =
402 IndexResultCache::index_result_cache_weight(&(key1.clone(), file_id1), &selection1);
403 let weight2 =
404 IndexResultCache::index_result_cache_weight(&(key2.clone(), file_id2), &selection2);
405 assert!(weight1 > 100);
406 assert!(weight2 > 100);
407
408 cache.put(key1.clone(), file_id1, selection1.clone());
410
411 let retrieved1 = cache.get(&key1, file_id1);
413 assert!(retrieved1.is_some());
414 assert_eq!(
415 retrieved1.unwrap().as_ref().row_count(),
416 selection1.as_ref().row_count()
417 );
418
419 cache.put(key2.clone(), file_id2, selection2.clone());
421
422 let retrieved2 = cache.get(&key2, file_id2);
424 assert!(retrieved2.is_some());
425 assert_eq!(
426 retrieved2.unwrap().as_ref().row_count(),
427 selection2.as_ref().row_count()
428 );
429
430 cache.cache.run_pending_tasks();
432 let retrieved1_after_eviction = cache.get(&key1, file_id1);
433 assert!(
434 retrieved1_after_eviction.is_none(),
435 "First key should have been evicted"
436 );
437 }
438
439 #[test]
440 fn test_index_result_cache_weight() {
441 let file_id = FileId::random();
442
443 let empty_predicates = BTreeMap::new();
445 let empty_key = PredicateKey::new_fulltext(Arc::new(empty_predicates));
446 let empty_selection = Arc::new(RowGroupSelection::default());
447 let empty_weight = IndexResultCache::index_result_cache_weight(
448 &(empty_key.clone(), file_id),
449 &empty_selection,
450 );
451 assert_eq!(empty_weight, 0);
452 assert_eq!(
453 empty_weight,
454 empty_key.mem_usage() as u32 + empty_selection.mem_usage() as u32
455 );
456
457 let mut predicates1 = BTreeMap::new();
459 let request1 = FulltextRequest {
460 queries: vec![FulltextQuery("test query".to_string())],
461 terms: vec![FulltextTerm {
462 col_lowered: false,
463 term: "test term".to_string(),
464 }],
465 likes: vec![],
466 };
467 predicates1.insert(IndexTarget::ColumnId(1), request1);
468 let key1 = PredicateKey::new_fulltext(Arc::new(predicates1));
469 let selection1 = Arc::new(RowGroupSelection::new(100, 250));
470 let weight1 =
471 IndexResultCache::index_result_cache_weight(&(key1.clone(), file_id), &selection1);
472 assert!(weight1 > 0);
473 assert_eq!(
474 weight1,
475 key1.mem_usage() as u32 + selection1.mem_usage() as u32
476 );
477
478 let mut predicates2 = BTreeMap::new();
480 let predicate2 = BloomInListPredicate {
481 list: BTreeSet::from([b"test1".to_vec(), b"test2".to_vec()]),
482 };
483 predicates2.insert(IndexTarget::ColumnId(1), vec![predicate2]);
484 let key2 = PredicateKey::new_bloom(Arc::new(predicates2));
485 let selection2 = Arc::new(RowGroupSelection::from_row_ids(
486 [1, 2, 3].into_iter().collect(),
487 100,
488 1,
489 ));
490 let weight2 =
491 IndexResultCache::index_result_cache_weight(&(key2.clone(), file_id), &selection2);
492 assert!(weight2 > 0);
493 assert_eq!(
494 weight2,
495 key2.mem_usage() as u32 + selection2.mem_usage() as u32
496 );
497
498 let mut predicates3 = BTreeMap::new();
500 let predicate3 = Predicate::Range(RangePredicate {
501 range: Range {
502 lower: None,
503 upper: None,
504 },
505 });
506 predicates3.insert(IndexTarget::ColumnId(1), vec![predicate3]);
507 let key3 = PredicateKey::new_inverted(Arc::new(predicates3));
508 let selection3 = Arc::new(RowGroupSelection::from_row_ranges(
509 vec![(0, vec![5..15])],
510 20,
511 ));
512 let weight3 =
513 IndexResultCache::index_result_cache_weight(&(key3.clone(), file_id), &selection3);
514 assert!(weight3 > 0);
515 assert_eq!(
516 weight3,
517 key3.mem_usage() as u32 + selection3.mem_usage() as u32
518 );
519 }
520}