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 common_base::range_read::RangeReader;
22use common_telemetry::warn;
23use datatypes::data_type::ConcreteDataType;
24use index::inverted_index::format::reader::{InvertedIndexBlobReader, InvertedIndexReadMetrics};
25use index::inverted_index::search::index_apply::{
26    ApplyOutput, IndexApplier, IndexNotFoundStrategy, PredicatesIndexApplier, SearchContext,
27};
28use index::inverted_index::search::predicate::Predicate;
29use index::target::IndexTarget;
30use object_store::ObjectStore;
31use puffin::puffin_manager::cache::PuffinMetadataCacheRef;
32use puffin::puffin_manager::{PuffinManager, PuffinReader};
33use snafu::ResultExt;
34use store_api::metadata::RegionMetadataRef;
35use store_api::region_request::PathType;
36use store_api::storage::ColumnId;
37
38use crate::access_layer::{RegionFilePathFactory, WriteCachePathProvider};
39use crate::cache::file_cache::{FileCacheRef, FileType, IndexKey};
40use crate::cache::index::inverted_index::{CachedInvertedIndexBlobReader, InvertedIndexCacheRef};
41use crate::cache::index::result_cache::PredicateKey;
42use crate::error::{
43    ApplyInvertedIndexSnafu, BuildIndexApplierSnafu, MetadataSnafu, PuffinBuildReaderSnafu,
44    PuffinReadBlobSnafu, Result,
45};
46use crate::metrics::{INDEX_APPLY_ELAPSED, INDEX_APPLY_MEMORY_USAGE};
47use crate::sst::file::RegionIndexId;
48use crate::sst::index::inverted_index::INDEX_BLOB_TYPE;
49use crate::sst::index::puffin_manager::{BlobReader, PuffinManagerFactory};
50use crate::sst::index::{TYPE_INVERTED_INDEX, trigger_index_background_download};
51
52/// Metrics for tracking inverted index apply operations.
53#[derive(Default, Clone)]
54pub struct InvertedIndexApplyMetrics {
55    /// Total time spent applying the index.
56    pub apply_elapsed: std::time::Duration,
57    /// Number of blob cache misses (0 or 1).
58    pub blob_cache_miss: usize,
59    /// Total size of blobs read (in bytes).
60    pub blob_read_bytes: u64,
61    /// Metrics for inverted index reads.
62    pub inverted_index_read_metrics: InvertedIndexReadMetrics,
63}
64
65impl std::fmt::Debug for InvertedIndexApplyMetrics {
66    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
67        let Self {
68            apply_elapsed,
69            blob_cache_miss,
70            blob_read_bytes,
71            inverted_index_read_metrics,
72        } = self;
73
74        if self.is_empty() {
75            return write!(f, "{{}}");
76        }
77        write!(f, "{{")?;
78
79        write!(f, "\"apply_elapsed\":\"{:?}\"", apply_elapsed)?;
80
81        if *blob_cache_miss > 0 {
82            write!(f, ", \"blob_cache_miss\":{}", blob_cache_miss)?;
83        }
84        if *blob_read_bytes > 0 {
85            write!(f, ", \"blob_read_bytes\":{}", blob_read_bytes)?;
86        }
87        write!(
88            f,
89            ", \"inverted_index_read_metrics\":{:?}",
90            inverted_index_read_metrics
91        )?;
92
93        write!(f, "}}")
94    }
95}
96
97impl InvertedIndexApplyMetrics {
98    /// Returns true if the metrics are empty (contain no meaningful data).
99    pub fn is_empty(&self) -> bool {
100        self.apply_elapsed.is_zero()
101    }
102
103    /// Merges another metrics into this one.
104    pub fn merge_from(&mut self, other: &Self) {
105        self.apply_elapsed += other.apply_elapsed;
106        self.blob_cache_miss += other.blob_cache_miss;
107        self.blob_read_bytes += other.blob_read_bytes;
108        self.inverted_index_read_metrics
109            .merge_from(&other.inverted_index_read_metrics);
110    }
111}
112
113/// `InvertedIndexApplier` is responsible for applying predicates to the provided SST files
114/// and returning the relevant row group ids for further scan.
115pub(crate) struct InvertedIndexApplier {
116    /// The root directory of the table.
117    table_dir: String,
118
119    /// Path type for generating file paths.
120    path_type: PathType,
121
122    /// Store responsible for accessing remote index files.
123    store: ObjectStore,
124
125    /// The cache of index files.
126    file_cache: Option<FileCacheRef>,
127
128    /// The puffin manager factory.
129    puffin_manager_factory: PuffinManagerFactory,
130
131    /// In-memory cache for inverted index.
132    inverted_index_cache: Option<InvertedIndexCacheRef>,
133
134    /// Puffin metadata cache.
135    puffin_metadata_cache: Option<PuffinMetadataCacheRef>,
136
137    /// All collected predicates.
138    predicates: BTreeMap<ColumnId, Vec<Predicate>>,
139
140    /// Default apply plan built from all collected predicates.
141    default_plan: SstApplyPlan,
142
143    /// Expected predicate column types from the latest region metadata.
144    expected_predicate_col_types: BTreeMap<ColumnId, ConcreteDataType>,
145}
146
147pub(crate) type InvertedIndexApplierRef = Arc<InvertedIndexApplier>;
148
149#[derive(Clone)]
150pub(crate) struct SstApplyPlan {
151    pub predicate_key: PredicateKey,
152    pub index_applier: Arc<PredicatesIndexApplier>,
153}
154
155impl InvertedIndexApplier {
156    /// Creates a new `InvertedIndexApplier`.
157    pub fn new(
158        table_dir: String,
159        path_type: PathType,
160        store: ObjectStore,
161        puffin_manager_factory: PuffinManagerFactory,
162        predicates: BTreeMap<ColumnId, Vec<Predicate>>,
163        expected_predicate_col_types: BTreeMap<ColumnId, ConcreteDataType>,
164    ) -> Result<Self> {
165        let default_plan = Self::build_apply_plan(&predicates)?;
166        INDEX_APPLY_MEMORY_USAGE.add(default_plan.index_applier.memory_usage() as i64);
167
168        Ok(Self {
169            table_dir,
170            path_type,
171            store,
172            file_cache: None,
173            puffin_manager_factory,
174            inverted_index_cache: None,
175            puffin_metadata_cache: None,
176            predicates,
177            default_plan,
178            expected_predicate_col_types,
179        })
180    }
181
182    /// Sets the file cache.
183    pub fn with_file_cache(mut self, file_cache: Option<FileCacheRef>) -> Self {
184        self.file_cache = file_cache;
185        self
186    }
187
188    /// Sets the index cache.
189    pub fn with_index_cache(mut self, index_cache: Option<InvertedIndexCacheRef>) -> Self {
190        self.inverted_index_cache = index_cache;
191        self
192    }
193
194    /// Sets the puffin metadata cache.
195    pub fn with_puffin_metadata_cache(
196        mut self,
197        puffin_metadata_cache: Option<PuffinMetadataCacheRef>,
198    ) -> Self {
199        self.puffin_metadata_cache = puffin_metadata_cache;
200        self
201    }
202
203    /// Applies predicates to one SST file with the provided index applier.
204    ///
205    /// # Arguments
206    /// * `file_id` - The region file ID to apply predicates to
207    /// * `file_size_hint` - Optional hint for file size to avoid extra metadata reads
208    /// * `index_applier` - Inverted index applier produced by `plan_for_sst`.
209    /// * `metrics` - Optional mutable reference to collect metrics on demand
210    #[tracing::instrument(
211        skip_all,
212        fields(file_id = %file_id)
213    )]
214    pub async fn apply(
215        &self,
216        file_id: RegionIndexId,
217        file_size_hint: Option<u64>,
218        index_applier: &PredicatesIndexApplier,
219        mut metrics: Option<&mut InvertedIndexApplyMetrics>,
220    ) -> Result<ApplyOutput> {
221        let start = Instant::now();
222
223        let context = SearchContext {
224            // Older SSTs may lack an index enabled by ALTER, or a column with a default value.
225            // Missing indexes cannot rule out matching rows.
226            index_not_found_strategy: IndexNotFoundStrategy::Ignore,
227        };
228
229        let mut cache_miss = 0;
230        let blob = match self.cached_blob_reader(file_id, file_size_hint).await {
231            Ok(Some(puffin_reader)) => puffin_reader,
232            other => {
233                cache_miss += 1;
234                if let Err(err) = other {
235                    warn!(err; "An unexpected error occurred while reading the cached index file. Fallback to remote index file.")
236                }
237                self.remote_blob_reader(file_id, file_size_hint).await?
238            }
239        };
240
241        let blob_size = blob.metadata().await.context(MetadataSnafu)?.content_length;
242
243        let result = if let Some(index_cache) = &self.inverted_index_cache {
244            let mut index_reader = CachedInvertedIndexBlobReader::new(
245                file_id.file_id(),
246                file_id.version,
247                blob_size,
248                InvertedIndexBlobReader::new(blob),
249                index_cache.clone(),
250            );
251            index_applier
252                .apply(
253                    context,
254                    &mut index_reader,
255                    metrics
256                        .as_deref_mut()
257                        .map(|m| &mut m.inverted_index_read_metrics),
258                )
259                .await
260                .context(ApplyInvertedIndexSnafu)
261        } else {
262            let mut index_reader = InvertedIndexBlobReader::new(blob);
263            index_applier
264                .apply(
265                    context,
266                    &mut index_reader,
267                    metrics
268                        .as_deref_mut()
269                        .map(|m| &mut m.inverted_index_read_metrics),
270                )
271                .await
272                .context(ApplyInvertedIndexSnafu)
273        };
274
275        // Record elapsed time to histogram and collect metrics if requested
276        let elapsed = start.elapsed();
277        INDEX_APPLY_ELAPSED
278            .with_label_values(&[TYPE_INVERTED_INDEX])
279            .observe(elapsed.as_secs_f64());
280
281        if let Some(metrics) = metrics {
282            metrics.apply_elapsed = elapsed;
283            metrics.blob_cache_miss = cache_miss;
284            metrics.blob_read_bytes = blob_size;
285        }
286
287        result
288    }
289
290    /// Creates a blob reader from the cached index file.
291    async fn cached_blob_reader(
292        &self,
293        file_id: RegionIndexId,
294        file_size_hint: Option<u64>,
295    ) -> Result<Option<BlobReader>> {
296        let Some(file_cache) = &self.file_cache else {
297            return Ok(None);
298        };
299
300        let index_key = IndexKey::new(
301            file_id.region_id(),
302            file_id.file_id(),
303            FileType::Puffin(file_id.version),
304        );
305        if file_cache.get(index_key).await.is_none() {
306            return Ok(None);
307        };
308
309        let puffin_manager = self.puffin_manager_factory.build(
310            file_cache.local_store(),
311            WriteCachePathProvider::new(file_cache.clone()),
312        );
313
314        // Adds file size hint to the puffin reader to avoid extra metadata read.
315        let reader = puffin_manager
316            .reader(&file_id)
317            .await
318            .context(PuffinBuildReaderSnafu)?
319            .with_file_size_hint(file_size_hint)
320            .blob(INDEX_BLOB_TYPE)
321            .await
322            .context(PuffinReadBlobSnafu)?
323            .reader()
324            .await
325            .context(PuffinBuildReaderSnafu)?;
326        Ok(Some(reader))
327    }
328
329    /// Creates a blob reader from the remote index file.
330    async fn remote_blob_reader(
331        &self,
332        file_id: RegionIndexId,
333        file_size_hint: Option<u64>,
334    ) -> Result<BlobReader> {
335        let path_factory = RegionFilePathFactory::new(self.table_dir.clone(), self.path_type);
336
337        // Trigger background download if file cache and file size are available
338        trigger_index_background_download(
339            self.file_cache.as_ref(),
340            &file_id,
341            file_size_hint,
342            &path_factory,
343            &self.store,
344        );
345
346        let puffin_manager = self
347            .puffin_manager_factory
348            .build(self.store.clone(), path_factory)
349            .with_puffin_metadata_cache(self.puffin_metadata_cache.clone());
350
351        puffin_manager
352            .reader(&file_id)
353            .await
354            .context(PuffinBuildReaderSnafu)?
355            .with_file_size_hint(file_size_hint)
356            .blob(INDEX_BLOB_TYPE)
357            .await
358            .context(PuffinReadBlobSnafu)?
359            .reader()
360            .await
361            .context(PuffinBuildReaderSnafu)
362    }
363
364    /// Builds a per-SST apply plan.
365    ///
366    /// Returns `None` when no compatible predicate remains for this SST.
367    pub fn plan_for_sst(&self, sst_metadata: &RegionMetadataRef) -> Result<Option<SstApplyPlan>> {
368        let mut compatible_predicates = BTreeMap::new();
369        let mut has_type_mismatch = false;
370
371        for (col_id, expected) in &self.expected_predicate_col_types {
372            if let Some(sst_col) = sst_metadata.column_by_id(*col_id)
373                && sst_col.column_schema.data_type != *expected
374            {
375                has_type_mismatch = true;
376                continue;
377            }
378
379            if let Some(predicates) = self.predicates.get(col_id) {
380                compatible_predicates.insert(*col_id, predicates.clone());
381            }
382        }
383
384        if compatible_predicates.is_empty() {
385            return Ok(None);
386        }
387
388        if !has_type_mismatch {
389            return Ok(Some(self.default_plan.clone()));
390        }
391
392        let plan = Self::build_apply_plan(&compatible_predicates)?;
393        Ok(Some(plan))
394    }
395
396    fn build_apply_plan(
397        predicates_by_col: &BTreeMap<ColumnId, Vec<Predicate>>,
398    ) -> Result<SstApplyPlan> {
399        let predicates = predicates_by_col
400            .iter()
401            .map(|(col_id, preds)| (format!("{}", IndexTarget::ColumnId(*col_id)), preds.clone()))
402            .collect();
403
404        let index_applier =
405            PredicatesIndexApplier::try_from(predicates).context(BuildIndexApplierSnafu)?;
406
407        let predicate_key = PredicateKey::new_inverted(Arc::new(predicates_by_col.clone()));
408        Ok(SstApplyPlan {
409            predicate_key,
410            index_applier: Arc::new(index_applier),
411        })
412    }
413}
414
415impl Drop for InvertedIndexApplier {
416    fn drop(&mut self) {
417        INDEX_APPLY_MEMORY_USAGE.sub(self.default_plan.index_applier.memory_usage() as i64);
418    }
419}
420
421#[cfg(test)]
422mod tests {
423    use api::v1::SemanticType;
424    use datatypes::data_type::ConcreteDataType;
425    use datatypes::schema::ColumnSchema;
426    use futures::io::Cursor;
427    use index::inverted_index::search::predicate::RegexMatchPredicate;
428    use object_store::services::Memory;
429    use puffin::puffin_manager::PuffinWriter;
430    use store_api::metadata::{ColumnMetadata, RegionMetadataBuilder};
431    use store_api::storage::{FileId, RegionId};
432
433    use super::*;
434    use crate::sst::index::RegionFileId;
435
436    #[tokio::test]
437    async fn test_plan_for_sst() {
438        let (_d, puffin_manager_factory) =
439            PuffinManagerFactory::new_for_test_async("test_plan_for_sst_basic_").await;
440        let object_store = ObjectStore::new(Memory::default()).unwrap();
441        let table_dir = "table_dir".to_string();
442
443        let mut predicates = BTreeMap::new();
444        predicates.insert(
445            1,
446            vec![Predicate::RegexMatch(RegexMatchPredicate {
447                pattern: "foo".to_string(),
448            })],
449        );
450        let expected_predicate_col_types =
451            BTreeMap::from_iter([(1, ConcreteDataType::string_datatype())]);
452
453        let sst_index_applier = InvertedIndexApplier::new(
454            table_dir,
455            PathType::Bare,
456            object_store,
457            puffin_manager_factory,
458            predicates,
459            expected_predicate_col_types,
460        )
461        .unwrap();
462        let plan = sst_index_applier
463            .plan_for_sst(&mock_region_metadata())
464            .unwrap();
465        assert!(plan.is_some());
466    }
467
468    #[tokio::test]
469    async fn test_plan_for_sst_type_mismatch() {
470        let (_d, puffin_manager_factory) =
471            PuffinManagerFactory::new_for_test_async("test_plan_for_sst_type_mismatch_").await;
472        let object_store = ObjectStore::new(Memory::default()).unwrap();
473        let table_dir = "table_dir".to_string();
474
475        let mut predicates = BTreeMap::new();
476        predicates.insert(
477            1,
478            vec![Predicate::RegexMatch(RegexMatchPredicate {
479                pattern: "foo".to_string(),
480            })],
481        );
482        // Column id 1 is String in `mock_region_metadata`, set expected type to Int64.
483        let expected_predicate_col_types =
484            BTreeMap::from_iter([(1, ConcreteDataType::int64_datatype())]);
485
486        let sst_index_applier = InvertedIndexApplier::new(
487            table_dir,
488            PathType::Bare,
489            object_store,
490            puffin_manager_factory,
491            predicates,
492            expected_predicate_col_types,
493        )
494        .unwrap();
495        let plan = sst_index_applier
496            .plan_for_sst(&mock_region_metadata())
497            .unwrap();
498        assert!(plan.is_none());
499    }
500
501    #[tokio::test]
502    async fn test_index_applier_apply_invalid_blob_type() {
503        let (_d, puffin_manager_factory) =
504            PuffinManagerFactory::new_for_test_async("test_index_applier_apply_invalid_blob_type_")
505                .await;
506        let object_store = ObjectStore::new(Memory::default()).unwrap();
507        let file_id = RegionFileId::new(0.into(), FileId::random());
508        let index_id = RegionIndexId::new(file_id, 0);
509        let table_dir = "table_dir".to_string();
510
511        let puffin_manager = puffin_manager_factory.build(
512            object_store.clone(),
513            RegionFilePathFactory::new(table_dir.clone(), PathType::Bare),
514        );
515        let mut writer = puffin_manager.writer(&index_id).await.unwrap();
516        writer
517            .put_blob(
518                "invalid_blob_type",
519                Cursor::new(vec![]),
520                Default::default(),
521                Default::default(),
522            )
523            .await
524            .unwrap();
525        writer.finish().await.unwrap();
526
527        let mut predicates = BTreeMap::new();
528        predicates.insert(
529            1,
530            vec![Predicate::RegexMatch(RegexMatchPredicate {
531                pattern: "foo".to_string(),
532            })],
533        );
534        let expected_predicate_col_types =
535            BTreeMap::from_iter([(1, ConcreteDataType::string_datatype())]);
536        let sst_index_applier = InvertedIndexApplier::new(
537            table_dir.clone(),
538            PathType::Bare,
539            object_store,
540            puffin_manager_factory,
541            predicates,
542            expected_predicate_col_types,
543        )
544        .unwrap();
545        let plan = sst_index_applier
546            .plan_for_sst(&mock_region_metadata())
547            .unwrap()
548            .unwrap();
549        let res = sst_index_applier
550            .apply(index_id, None, &plan.index_applier, None)
551            .await;
552        assert!(format!("{:?}", res.unwrap_err()).contains("Blob not found"));
553    }
554
555    fn mock_region_metadata() -> RegionMetadataRef {
556        let mut builder = RegionMetadataBuilder::new(RegionId::new(1024, 1));
557        builder
558            .push_column_metadata(ColumnMetadata {
559                column_schema: ColumnSchema::new("tag", ConcreteDataType::string_datatype(), false),
560                semantic_type: SemanticType::Tag,
561                column_id: 1,
562            })
563            .push_column_metadata(ColumnMetadata {
564                column_schema: ColumnSchema::new(
565                    "ts",
566                    ConcreteDataType::timestamp_millisecond_datatype(),
567                    false,
568                ),
569                semantic_type: SemanticType::Timestamp,
570                column_id: 2,
571            })
572            .primary_key(vec![1]);
573        Arc::new(builder.build().unwrap())
574    }
575}