Skip to main content

mito2/sst/index/inverted_index/
applier.rs

1// Copyright 2023 Greptime Team
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15pub 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/// Metrics for tracking inverted index apply operations.
54#[derive(Default, Clone)]
55pub struct InvertedIndexApplyMetrics {
56    /// Total time spent applying the index.
57    pub apply_elapsed: std::time::Duration,
58    /// Number of blob cache misses (0 or 1).
59    pub blob_cache_miss: usize,
60    /// Total size of blobs read (in bytes).
61    pub blob_read_bytes: u64,
62    /// Metrics for inverted index reads.
63    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    /// Returns true if the metrics are empty (contain no meaningful data).
100    pub fn is_empty(&self) -> bool {
101        self.apply_elapsed.is_zero()
102    }
103
104    /// Merges another metrics into this one.
105    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
114/// `InvertedIndexApplier` is responsible for applying predicates to the provided SST files
115/// and returning the relevant row group ids for further scan.
116pub(crate) struct InvertedIndexApplier {
117    /// The root directory of the table.
118    table_dir: String,
119
120    /// Path type for generating file paths.
121    path_type: PathType,
122
123    /// Store responsible for accessing remote index files.
124    store: ObjectStore,
125
126    /// The cache of index files.
127    file_cache: Option<FileCacheRef>,
128
129    /// The puffin manager factory.
130    puffin_manager_factory: PuffinManagerFactory,
131
132    /// In-memory cache for inverted index.
133    inverted_index_cache: Option<InvertedIndexCacheRef>,
134
135    /// Puffin metadata cache.
136    puffin_metadata_cache: Option<PuffinMetadataCacheRef>,
137
138    /// All collected predicates.
139    predicates: BTreeMap<IndexTarget, Vec<Predicate>>,
140
141    /// Default apply plan built from all collected predicates.
142    default_plan: SstApplyPlan,
143
144    /// Expected predicate target types from the latest region metadata.
145    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    /// Creates a new `InvertedIndexApplier`.
158    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    /// Sets the file cache.
184    pub fn with_file_cache(mut self, file_cache: Option<FileCacheRef>) -> Self {
185        self.file_cache = file_cache;
186        self
187    }
188
189    /// Sets the index cache.
190    pub fn with_index_cache(mut self, index_cache: Option<InvertedIndexCacheRef>) -> Self {
191        self.inverted_index_cache = index_cache;
192        self
193    }
194
195    /// Sets the puffin metadata cache.
196    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    /// Applies predicates to one SST file with the provided index applier.
205    ///
206    /// # Arguments
207    /// * `file_id` - The region file ID to apply predicates to
208    /// * `file_size_hint` - Optional hint for file size to avoid extra metadata reads
209    /// * `index_applier` - Inverted index applier produced by `plan_for_sst`.
210    /// * `metrics` - Optional mutable reference to collect metrics on demand
211    #[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            // Older SSTs may lack an index enabled by ALTER, or a column with a default value.
226            // Missing indexes cannot rule out matching rows.
227            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        // Record elapsed time to histogram and collect metrics if requested
277        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    /// Creates a blob reader from the cached index file.
292    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        // Adds file size hint to the puffin reader to avoid extra metadata read.
316        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    /// Creates a blob reader from the remote index file.
331    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 background download if file cache and file size are available
339        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    /// Builds a per-SST apply plan.
366    ///
367    /// Returns `None` when no compatible predicate remains for this SST.
368    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            // JSON paths require a matching type hint in this SST. Ordinary missing
379            // columns retain the missing-index fallback for added columns with defaults.
380            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
429/// Resolves the value type used to encode an index predicate.
430/// JSON paths must have an enabled inverted-index type hint.
431fn 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        // Column id 1 is String in `mock_region_metadata`, set expected type to Int64.
521        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}