1pub mod builder;
16
17use std::collections::BTreeMap;
18use std::sync::Arc;
19use std::time::Instant;
20
21use arrow_schema::extension::ExtensionType;
22use common_base::range_read::RangeReader;
23use common_telemetry::warn;
24use datatypes::data_type::ConcreteDataType;
25use datatypes::extension::json::Json2ExtensionType;
26use index::inverted_index::format::reader::{InvertedIndexBlobReader, InvertedIndexReadMetrics};
27use index::inverted_index::search::index_apply::{
28 ApplyOutput, IndexApplier, IndexNotFoundStrategy, PredicatesIndexApplier, SearchContext,
29};
30use index::inverted_index::search::predicate::Predicate;
31use index::target::IndexTarget;
32use object_store::ObjectStore;
33use puffin::puffin_manager::cache::PuffinMetadataCacheRef;
34use puffin::puffin_manager::{PuffinManager, PuffinReader};
35use snafu::ResultExt;
36use store_api::metadata::{RegionMetadata, RegionMetadataRef};
37use store_api::region_request::PathType;
38
39use crate::access_layer::{RegionFilePathFactory, WriteCachePathProvider};
40use crate::cache::file_cache::{FileCacheRef, FileType, IndexKey};
41use crate::cache::index::inverted_index::{CachedInvertedIndexBlobReader, InvertedIndexCacheRef};
42use crate::cache::index::result_cache::PredicateKey;
43use crate::error::{
44 ApplyInvertedIndexSnafu, BuildIndexApplierSnafu, MetadataSnafu, PuffinBuildReaderSnafu,
45 PuffinReadBlobSnafu, Result,
46};
47use crate::metrics::{INDEX_APPLY_ELAPSED, INDEX_APPLY_MEMORY_USAGE};
48use crate::sst::file::RegionIndexId;
49use crate::sst::index::inverted_index::INDEX_BLOB_TYPE;
50use crate::sst::index::puffin_manager::{BlobReader, PuffinManagerFactory};
51use crate::sst::index::{TYPE_INVERTED_INDEX, trigger_index_background_download};
52
53#[derive(Default, Clone)]
55pub struct InvertedIndexApplyMetrics {
56 pub apply_elapsed: std::time::Duration,
58 pub blob_cache_miss: usize,
60 pub blob_read_bytes: u64,
62 pub inverted_index_read_metrics: InvertedIndexReadMetrics,
64}
65
66impl std::fmt::Debug for InvertedIndexApplyMetrics {
67 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
68 let Self {
69 apply_elapsed,
70 blob_cache_miss,
71 blob_read_bytes,
72 inverted_index_read_metrics,
73 } = self;
74
75 if self.is_empty() {
76 return write!(f, "{{}}");
77 }
78 write!(f, "{{")?;
79
80 write!(f, "\"apply_elapsed\":\"{:?}\"", apply_elapsed)?;
81
82 if *blob_cache_miss > 0 {
83 write!(f, ", \"blob_cache_miss\":{}", blob_cache_miss)?;
84 }
85 if *blob_read_bytes > 0 {
86 write!(f, ", \"blob_read_bytes\":{}", blob_read_bytes)?;
87 }
88 write!(
89 f,
90 ", \"inverted_index_read_metrics\":{:?}",
91 inverted_index_read_metrics
92 )?;
93
94 write!(f, "}}")
95 }
96}
97
98impl InvertedIndexApplyMetrics {
99 pub fn is_empty(&self) -> bool {
101 self.apply_elapsed.is_zero()
102 }
103
104 pub fn merge_from(&mut self, other: &Self) {
106 self.apply_elapsed += other.apply_elapsed;
107 self.blob_cache_miss += other.blob_cache_miss;
108 self.blob_read_bytes += other.blob_read_bytes;
109 self.inverted_index_read_metrics
110 .merge_from(&other.inverted_index_read_metrics);
111 }
112}
113
114pub(crate) struct InvertedIndexApplier {
117 table_dir: String,
119
120 path_type: PathType,
122
123 store: ObjectStore,
125
126 file_cache: Option<FileCacheRef>,
128
129 puffin_manager_factory: PuffinManagerFactory,
131
132 inverted_index_cache: Option<InvertedIndexCacheRef>,
134
135 puffin_metadata_cache: Option<PuffinMetadataCacheRef>,
137
138 predicates: BTreeMap<IndexTarget, Vec<Predicate>>,
140
141 default_plan: SstApplyPlan,
143
144 expected_predicate_target_types: BTreeMap<IndexTarget, ConcreteDataType>,
146}
147
148pub(crate) type InvertedIndexApplierRef = Arc<InvertedIndexApplier>;
149
150#[derive(Clone)]
151pub(crate) struct SstApplyPlan {
152 pub predicate_key: PredicateKey,
153 pub index_applier: Arc<PredicatesIndexApplier>,
154}
155
156impl InvertedIndexApplier {
157 pub fn new(
159 table_dir: String,
160 path_type: PathType,
161 store: ObjectStore,
162 puffin_manager_factory: PuffinManagerFactory,
163 predicates: BTreeMap<IndexTarget, Vec<Predicate>>,
164 expected_predicate_target_types: BTreeMap<IndexTarget, ConcreteDataType>,
165 ) -> Result<Self> {
166 let default_plan = Self::build_apply_plan(&predicates)?;
167 INDEX_APPLY_MEMORY_USAGE.add(default_plan.index_applier.memory_usage() as i64);
168
169 Ok(Self {
170 table_dir,
171 path_type,
172 store,
173 file_cache: None,
174 puffin_manager_factory,
175 inverted_index_cache: None,
176 puffin_metadata_cache: None,
177 predicates,
178 default_plan,
179 expected_predicate_target_types,
180 })
181 }
182
183 pub fn with_file_cache(mut self, file_cache: Option<FileCacheRef>) -> Self {
185 self.file_cache = file_cache;
186 self
187 }
188
189 pub fn with_index_cache(mut self, index_cache: Option<InvertedIndexCacheRef>) -> Self {
191 self.inverted_index_cache = index_cache;
192 self
193 }
194
195 pub fn with_puffin_metadata_cache(
197 mut self,
198 puffin_metadata_cache: Option<PuffinMetadataCacheRef>,
199 ) -> Self {
200 self.puffin_metadata_cache = puffin_metadata_cache;
201 self
202 }
203
204 #[tracing::instrument(
212 skip_all,
213 fields(file_id = %file_id)
214 )]
215 pub async fn apply(
216 &self,
217 file_id: RegionIndexId,
218 file_size_hint: Option<u64>,
219 index_applier: &PredicatesIndexApplier,
220 mut metrics: Option<&mut InvertedIndexApplyMetrics>,
221 ) -> Result<ApplyOutput> {
222 let start = Instant::now();
223
224 let context = SearchContext {
225 index_not_found_strategy: IndexNotFoundStrategy::Ignore,
228 };
229
230 let mut cache_miss = 0;
231 let blob = match self.cached_blob_reader(file_id, file_size_hint).await {
232 Ok(Some(puffin_reader)) => puffin_reader,
233 other => {
234 cache_miss += 1;
235 if let Err(err) = other {
236 warn!(err; "An unexpected error occurred while reading the cached index file. Fallback to remote index file.")
237 }
238 self.remote_blob_reader(file_id, file_size_hint).await?
239 }
240 };
241
242 let blob_size = blob.metadata().await.context(MetadataSnafu)?.content_length;
243
244 let result = if let Some(index_cache) = &self.inverted_index_cache {
245 let mut index_reader = CachedInvertedIndexBlobReader::new(
246 file_id.file_id(),
247 file_id.version,
248 blob_size,
249 InvertedIndexBlobReader::new(blob),
250 index_cache.clone(),
251 );
252 index_applier
253 .apply(
254 context,
255 &mut index_reader,
256 metrics
257 .as_deref_mut()
258 .map(|m| &mut m.inverted_index_read_metrics),
259 )
260 .await
261 .context(ApplyInvertedIndexSnafu)
262 } else {
263 let mut index_reader = InvertedIndexBlobReader::new(blob);
264 index_applier
265 .apply(
266 context,
267 &mut index_reader,
268 metrics
269 .as_deref_mut()
270 .map(|m| &mut m.inverted_index_read_metrics),
271 )
272 .await
273 .context(ApplyInvertedIndexSnafu)
274 };
275
276 let elapsed = start.elapsed();
278 INDEX_APPLY_ELAPSED
279 .with_label_values(&[TYPE_INVERTED_INDEX])
280 .observe(elapsed.as_secs_f64());
281
282 if let Some(metrics) = metrics {
283 metrics.apply_elapsed = elapsed;
284 metrics.blob_cache_miss = cache_miss;
285 metrics.blob_read_bytes = blob_size;
286 }
287
288 result
289 }
290
291 async fn cached_blob_reader(
293 &self,
294 file_id: RegionIndexId,
295 file_size_hint: Option<u64>,
296 ) -> Result<Option<BlobReader>> {
297 let Some(file_cache) = &self.file_cache else {
298 return Ok(None);
299 };
300
301 let index_key = IndexKey::new(
302 file_id.region_id(),
303 file_id.file_id(),
304 FileType::Puffin(file_id.version),
305 );
306 if file_cache.get(index_key).await.is_none() {
307 return Ok(None);
308 };
309
310 let puffin_manager = self.puffin_manager_factory.build(
311 file_cache.local_store(),
312 WriteCachePathProvider::new(file_cache.clone()),
313 );
314
315 let reader = puffin_manager
317 .reader(&file_id)
318 .await
319 .context(PuffinBuildReaderSnafu)?
320 .with_file_size_hint(file_size_hint)
321 .blob(INDEX_BLOB_TYPE)
322 .await
323 .context(PuffinReadBlobSnafu)?
324 .reader()
325 .await
326 .context(PuffinBuildReaderSnafu)?;
327 Ok(Some(reader))
328 }
329
330 async fn remote_blob_reader(
332 &self,
333 file_id: RegionIndexId,
334 file_size_hint: Option<u64>,
335 ) -> Result<BlobReader> {
336 let path_factory = RegionFilePathFactory::new(self.table_dir.clone(), self.path_type);
337
338 trigger_index_background_download(
340 self.file_cache.as_ref(),
341 &file_id,
342 file_size_hint,
343 &path_factory,
344 &self.store,
345 );
346
347 let puffin_manager = self
348 .puffin_manager_factory
349 .build(self.store.clone(), path_factory)
350 .with_puffin_metadata_cache(self.puffin_metadata_cache.clone());
351
352 puffin_manager
353 .reader(&file_id)
354 .await
355 .context(PuffinBuildReaderSnafu)?
356 .with_file_size_hint(file_size_hint)
357 .blob(INDEX_BLOB_TYPE)
358 .await
359 .context(PuffinReadBlobSnafu)?
360 .reader()
361 .await
362 .context(PuffinBuildReaderSnafu)
363 }
364
365 pub fn plan_for_sst(&self, sst_metadata: &RegionMetadataRef) -> Result<Option<SstApplyPlan>> {
369 let mut compatible_predicates = BTreeMap::new();
370 let mut has_type_mismatch = false;
371
372 for (target, predicates) in &self.predicates {
373 let Some(expected) = self.expected_predicate_target_types.get(target) else {
374 has_type_mismatch = true;
375 continue;
376 };
377 let actual = index_target_data_type(sst_metadata, target);
378 let compatible = match target {
381 IndexTarget::ColumnId(_) => actual.as_ref().is_none_or(|ty| ty == expected),
382 IndexTarget::JsonPath(_) => actual.as_ref() == Some(expected),
383 };
384 if !compatible {
385 has_type_mismatch = true;
386 continue;
387 }
388
389 compatible_predicates.insert(target.clone(), predicates.clone());
390 }
391
392 if compatible_predicates.is_empty() {
393 return Ok(None);
394 }
395
396 if !has_type_mismatch {
397 return Ok(Some(self.default_plan.clone()));
398 }
399
400 let plan = Self::build_apply_plan(&compatible_predicates)?;
401 Ok(Some(plan))
402 }
403
404 fn build_apply_plan(
405 predicates_by_target: &BTreeMap<IndexTarget, Vec<Predicate>>,
406 ) -> Result<SstApplyPlan> {
407 let mut predicates = Vec::with_capacity(predicates_by_target.len());
408 for (target, preds) in predicates_by_target {
409 let name = target.encode();
410 predicates.push((name, preds.clone()));
411 }
412 let index_applier =
413 PredicatesIndexApplier::try_from(predicates).context(BuildIndexApplierSnafu)?;
414
415 let predicate_key = PredicateKey::new_inverted(Arc::new(predicates_by_target.clone()));
416 Ok(SstApplyPlan {
417 predicate_key,
418 index_applier: Arc::new(index_applier),
419 })
420 }
421}
422
423impl Drop for InvertedIndexApplier {
424 fn drop(&mut self) {
425 INDEX_APPLY_MEMORY_USAGE.sub(self.default_plan.index_applier.memory_usage() as i64);
426 }
427}
428
429fn index_target_data_type(
432 metadata: &RegionMetadata,
433 target: &IndexTarget,
434) -> Option<ConcreteDataType> {
435 let target = match target {
436 IndexTarget::ColumnId(id) => {
437 return metadata
438 .column_by_id(*id)
439 .map(|col| col.column_schema.data_type.clone());
440 }
441 IndexTarget::JsonPath(target) => target,
442 };
443 let col = metadata.column_by_id(target.column_id())?;
444 let extension = col
445 .column_schema
446 .extension_type::<Json2ExtensionType>()
447 .ok()??;
448 extension
449 .metadata()
450 .json_settings()
451 .type_hints()
452 .iter()
453 .find(|hint| hint.path == target.path() && hint.inverted_index)
454 .map(|hint| hint.data_type.clone())
455}
456
457#[cfg(test)]
458mod tests {
459 use api::v1::SemanticType;
460 use datatypes::data_type::ConcreteDataType;
461 use datatypes::schema::ColumnSchema;
462 use futures::io::Cursor;
463 use index::inverted_index::search::predicate::RegexMatchPredicate;
464 use object_store::services::Memory;
465 use puffin::puffin_manager::PuffinWriter;
466 use store_api::metadata::{ColumnMetadata, RegionMetadataBuilder};
467 use store_api::storage::{FileId, RegionId};
468
469 use super::*;
470 use crate::sst::index::RegionFileId;
471
472 #[tokio::test]
473 async fn test_plan_for_sst() {
474 let (_d, puffin_manager_factory) =
475 PuffinManagerFactory::new_for_test_async("test_plan_for_sst_basic_").await;
476 let object_store = ObjectStore::new(Memory::default()).unwrap();
477 let table_dir = "table_dir".to_string();
478
479 let mut predicates = BTreeMap::new();
480 predicates.insert(
481 IndexTarget::ColumnId(1),
482 vec![Predicate::RegexMatch(RegexMatchPredicate {
483 pattern: "foo".to_string(),
484 })],
485 );
486 let expected_predicate_target_types = BTreeMap::from_iter([(
487 IndexTarget::ColumnId(1),
488 ConcreteDataType::string_datatype(),
489 )]);
490
491 let sst_index_applier = InvertedIndexApplier::new(
492 table_dir,
493 PathType::Bare,
494 object_store,
495 puffin_manager_factory,
496 predicates,
497 expected_predicate_target_types,
498 )
499 .unwrap();
500 let plan = sst_index_applier
501 .plan_for_sst(&mock_region_metadata())
502 .unwrap();
503 assert!(plan.is_some());
504 }
505
506 #[tokio::test]
507 async fn test_plan_for_sst_type_mismatch() {
508 let (_d, puffin_manager_factory) =
509 PuffinManagerFactory::new_for_test_async("test_plan_for_sst_type_mismatch_").await;
510 let object_store = ObjectStore::new(Memory::default()).unwrap();
511 let table_dir = "table_dir".to_string();
512
513 let mut predicates = BTreeMap::new();
514 predicates.insert(
515 IndexTarget::ColumnId(1),
516 vec![Predicate::RegexMatch(RegexMatchPredicate {
517 pattern: "foo".to_string(),
518 })],
519 );
520 let expected_predicate_target_types =
522 BTreeMap::from_iter([(IndexTarget::ColumnId(1), ConcreteDataType::int64_datatype())]);
523
524 let sst_index_applier = InvertedIndexApplier::new(
525 table_dir,
526 PathType::Bare,
527 object_store,
528 puffin_manager_factory,
529 predicates,
530 expected_predicate_target_types,
531 )
532 .unwrap();
533 let plan = sst_index_applier
534 .plan_for_sst(&mock_region_metadata())
535 .unwrap();
536 assert!(plan.is_none());
537 }
538
539 #[tokio::test]
540 async fn test_index_applier_apply_invalid_blob_type() {
541 let (_d, puffin_manager_factory) =
542 PuffinManagerFactory::new_for_test_async("test_index_applier_apply_invalid_blob_type_")
543 .await;
544 let object_store = ObjectStore::new(Memory::default()).unwrap();
545 let file_id = RegionFileId::new(0.into(), FileId::random());
546 let index_id = RegionIndexId::new(file_id, 0);
547 let table_dir = "table_dir".to_string();
548
549 let puffin_manager = puffin_manager_factory.build(
550 object_store.clone(),
551 RegionFilePathFactory::new(table_dir.clone(), PathType::Bare),
552 );
553 let mut writer = puffin_manager.writer(&index_id).await.unwrap();
554 writer
555 .put_blob(
556 "invalid_blob_type",
557 Cursor::new(vec![]),
558 Default::default(),
559 Default::default(),
560 )
561 .await
562 .unwrap();
563 writer.finish().await.unwrap();
564
565 let mut predicates = BTreeMap::new();
566 predicates.insert(
567 IndexTarget::ColumnId(1),
568 vec![Predicate::RegexMatch(RegexMatchPredicate {
569 pattern: "foo".to_string(),
570 })],
571 );
572 let expected_predicate_target_types = BTreeMap::from_iter([(
573 IndexTarget::ColumnId(1),
574 ConcreteDataType::string_datatype(),
575 )]);
576 let sst_index_applier = InvertedIndexApplier::new(
577 table_dir.clone(),
578 PathType::Bare,
579 object_store,
580 puffin_manager_factory,
581 predicates,
582 expected_predicate_target_types,
583 )
584 .unwrap();
585 let plan = sst_index_applier
586 .plan_for_sst(&mock_region_metadata())
587 .unwrap()
588 .unwrap();
589 let res = sst_index_applier
590 .apply(index_id, None, &plan.index_applier, None)
591 .await;
592 assert!(format!("{:?}", res.unwrap_err()).contains("Blob not found"));
593 }
594
595 fn mock_region_metadata() -> RegionMetadataRef {
596 let mut builder = RegionMetadataBuilder::new(RegionId::new(1024, 1));
597 builder
598 .push_column_metadata(ColumnMetadata {
599 column_schema: ColumnSchema::new("tag", ConcreteDataType::string_datatype(), false),
600 semantic_type: SemanticType::Tag,
601 column_id: 1,
602 })
603 .push_column_metadata(ColumnMetadata {
604 column_schema: ColumnSchema::new(
605 "ts",
606 ConcreteDataType::timestamp_millisecond_datatype(),
607 false,
608 ),
609 semantic_type: SemanticType::Timestamp,
610 column_id: 2,
611 })
612 .primary_key(vec![1]);
613 Arc::new(builder.build().unwrap())
614 }
615}