Skip to main content

mito2/series_index/
searcher.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
15use api::v1::SemanticType;
16use async_stream::try_stream;
17use common_recordbatch::filter::SimpleFilterEvaluator;
18use common_time::range::TimestampRange;
19use datafusion_expr::{Expr, col, lit};
20use datatypes::arrow::array::{ArrayRef, UInt32Array, UInt64Array};
21use datatypes::arrow::buffer::BooleanBuffer;
22use datatypes::arrow::datatypes::{DataType, SchemaRef};
23use futures::TryStreamExt;
24use object_store::ObjectStore;
25use snafu::{OptionExt, ResultExt, ensure};
26use store_api::metadata::RegionMetadataRef;
27use table::predicate::Predicate;
28
29use crate::error::{
30    InvalidMetaSnafu, InvalidRecordBatchSnafu, RecordBatchSnafu, Result, UnexpectedSnafu,
31};
32use crate::series_index::{
33    MAX_TS_COLUMN, METRIC_SERIES_ID_BATCH_SIZE, MIN_TS_COLUMN, MetricSeriesId,
34    MetricSeriesIdStream, ROW_COUNT_COLUMN, TABLE_ID_COLUMN, TSID_COLUMN, series_index_schema,
35};
36use crate::sst::parquet::index_reader::ParquetIndexReader;
37use crate::sst::parquet::prefilter::simple_tag_filters;
38
39/// Searches a series-index file for metric series matching query predicates.
40pub struct SeriesIndexSearcher {
41    object_store: ObjectStore,
42    filters: Vec<(Expr, SimpleFilterEvaluator)>,
43    empty_time_range: bool,
44}
45
46impl SeriesIndexSearcher {
47    /// Creates a searcher reusable across series-index files of `metadata`.
48    pub fn try_new(
49        metadata: RegionMetadataRef,
50        object_store: ObjectStore,
51        predicate: Option<&Predicate>,
52        time_range: Option<TimestampRange>,
53    ) -> Result<Self> {
54        // Keep search-time metadata validation identical to the writer.
55        series_index_schema(&metadata)?;
56
57        let mut filters = simple_tag_filters(&metadata, None, predicate);
58        let (empty_time_range, time_exprs) = time_range_filters(&metadata, time_range)?;
59        for expr in time_exprs {
60            let filter = SimpleFilterEvaluator::try_new(&expr).context(UnexpectedSnafu {
61                reason: "failed to build an internal series-index time filter",
62            })?;
63            filters.push((expr, filter));
64        }
65
66        Ok(Self {
67            object_store,
68            filters,
69            empty_time_range,
70        })
71    }
72
73    /// Searches `path` and returns sorted batches of matching metric-series IDs.
74    pub async fn search(&self, path: &str) -> Result<MetricSeriesIdStream> {
75        if self.empty_time_range {
76            return Ok(Box::pin(futures::stream::empty()));
77        }
78
79        let reader = ParquetIndexReader::open(self.object_store.clone(), path).await?;
80        validate_index_schema(reader.schema())?;
81
82        // An older index file may not contain tags added by schema evolution.
83        // Ignore filters on those tags to preserve a conservative candidate set.
84        let (pruning_predicate, filters) = self.filters_for_schema(reader.schema());
85        let mut projection_columns = Vec::with_capacity(filters.len() + 2);
86        projection_columns.extend([TABLE_ID_COLUMN, TSID_COLUMN]);
87        projection_columns.extend(filters.iter().map(SimpleFilterEvaluator::column_name));
88        let mut batches = reader.read(&pruning_predicate, &projection_columns)?;
89
90        Ok(Box::pin(try_stream! {
91            let mut last_series = None;
92            let mut output = Vec::with_capacity(METRIC_SERIES_ID_BATCH_SIZE);
93            while let Some(batch) = batches.try_next().await? {
94                let mut mask = BooleanBuffer::new_set(batch.num_rows());
95                for filter in &filters {
96                    let column = column(&batch, filter.column_name())?;
97                    let evaluated = filter.evaluate_array(column).context(RecordBatchSnafu)?;
98                    mask = &mask & &evaluated;
99                }
100
101                let table_ids = column(&batch, TABLE_ID_COLUMN)?
102                    .as_any()
103                    .downcast_ref::<UInt32Array>()
104                    .context(InvalidRecordBatchSnafu {
105                        reason: "series index __table_id is not UInt32",
106                    })?;
107                let tsids = column(&batch, TSID_COLUMN)?
108                    .as_any()
109                    .downcast_ref::<UInt64Array>()
110                    .context(InvalidRecordBatchSnafu {
111                        reason: "series index __tsid is not UInt64",
112                    })?;
113
114                for (row, matched) in mask.iter().enumerate() {
115                    if !matched {
116                        continue;
117                    }
118                    let series = MetricSeriesId {
119                        table_id: table_ids.value(row),
120                        tsid: tsids.value(row),
121                    };
122                    if last_series == Some(series) {
123                        continue;
124                    }
125                    last_series = Some(series);
126                    output.push(series);
127                    if output.len() == METRIC_SERIES_ID_BATCH_SIZE {
128                        yield std::mem::replace(
129                            &mut output,
130                            Vec::with_capacity(METRIC_SERIES_ID_BATCH_SIZE),
131                        );
132                    }
133                }
134            }
135            if !output.is_empty() {
136                yield output;
137            }
138        }))
139    }
140
141    fn filters_for_schema(&self, schema: &SchemaRef) -> (Predicate, Vec<SimpleFilterEvaluator>) {
142        let (exprs, filters): (Vec<_>, Vec<_>) = self
143            .filters
144            .iter()
145            .filter(|(_, filter)| schema.field_with_name(filter.column_name()).is_ok())
146            .cloned()
147            .unzip();
148        (Predicate::new(exprs), filters)
149    }
150}
151
152// Builds `__series_min_ts`/`__series_max_ts` predicates in the unit of the
153// given (region) metadata. NOTE: the searcher is constructed once per region
154// while the raw i64 bounds were written in each file's unit; files written
155// before/after a time index unit widening would need a per-file unit before
156// this comparison is safe. See the note on `timestamp_values` in the writer.
157fn time_range_filters(
158    metadata: &RegionMetadataRef,
159    time_range: Option<TimestampRange>,
160) -> Result<(bool, Vec<Expr>)> {
161    let Some(time_range) = time_range else {
162        return Ok((false, Vec::new()));
163    };
164    if time_range.is_empty() {
165        return Ok((true, Vec::new()));
166    }
167
168    let time_index = metadata.time_index_column();
169    ensure!(
170        time_index.semantic_type == SemanticType::Timestamp,
171        InvalidMetaSnafu {
172            reason: "series index metadata has no timestamp time index",
173        }
174    );
175    let timestamp_type =
176        time_index
177            .column_schema
178            .data_type
179            .as_timestamp()
180            .context(InvalidMetaSnafu {
181                reason: "series index time index is not a timestamp",
182            })?;
183    let unit = timestamp_type.unit();
184    let mut exprs = Vec::with_capacity(2);
185    // A series overlaps [start, end) only if its maximum is at least start.
186    // Round start up so a series ending before an unaligned start is pruned.
187    if let Some(start) = time_range
188        .start()
189        .and_then(|start| start.convert_to_ceil(unit))
190    {
191        exprs.push(col(MAX_TS_COLUMN).gt_eq(lit(start.value())));
192    }
193    // A series overlaps [start, end) only if its minimum is less than end.
194    // Round the exclusive end up to avoid pruning the containing unit interval.
195    if let Some(end) = time_range.end().and_then(|end| end.convert_to_ceil(unit)) {
196        exprs.push(col(MIN_TS_COLUMN).lt(lit(end.value())));
197    }
198    Ok((false, exprs))
199}
200
201fn validate_index_schema(schema: &SchemaRef) -> Result<()> {
202    for (name, data_type) in [
203        (MIN_TS_COLUMN, DataType::Int64),
204        (MAX_TS_COLUMN, DataType::Int64),
205        (ROW_COUNT_COLUMN, DataType::UInt64),
206        (TABLE_ID_COLUMN, DataType::UInt32),
207        (TSID_COLUMN, DataType::UInt64),
208    ] {
209        let field = schema
210            .field_with_name(name)
211            .ok()
212            .with_context(|| InvalidRecordBatchSnafu {
213                reason: format!("series index is missing internal column {name}"),
214            })?;
215        ensure!(
216            field.data_type() == &data_type && !field.is_nullable(),
217            InvalidRecordBatchSnafu {
218                reason: format!(
219                    "series index internal column {name} must be non-nullable {data_type:?}, got {:?}",
220                    field.data_type()
221                ),
222            }
223        );
224    }
225    Ok(())
226}
227
228fn column<'a>(
229    batch: &'a datatypes::arrow::record_batch::RecordBatch,
230    name: &str,
231) -> Result<&'a ArrayRef> {
232    let index = batch
233        .schema()
234        .index_of(name)
235        .ok()
236        .with_context(|| InvalidRecordBatchSnafu {
237            reason: format!("series index batch is missing column {name}"),
238        })?;
239    Ok(batch.column(index))
240}
241
242#[cfg(test)]
243mod tests {
244    use std::sync::Arc;
245
246    use datafusion_expr::{col, lit};
247    use datatypes::arrow::array::{BinaryArray, TimestampMillisecondArray, UInt8Array};
248    use datatypes::arrow::datatypes::{Field, Schema};
249    use datatypes::arrow::record_batch::RecordBatch;
250    use datatypes::prelude::ConcreteDataType;
251    use datatypes::schema::ColumnSchema;
252    use futures::TryStreamExt;
253    use object_store::services::Memory;
254    use store_api::codec::PrimaryKeyEncoding;
255    use store_api::metadata::{ColumnMetadata, RegionMetadataBuilder};
256
257    use super::*;
258    use crate::series_index::{SeriesIndexWriter, SeriesIndexWriterOptions};
259    use crate::test_util::sst_util::{new_sparse_primary_key, sst_region_metadata_with_encoding};
260
261    fn object_store() -> ObjectStore {
262        ObjectStore::new(Memory::default()).unwrap()
263    }
264
265    fn flat_batch(primary_keys: &[Vec<u8>], timestamps: &[i64]) -> RecordBatch {
266        let schema = Arc::new(Schema::new(vec![
267            Field::new(
268                "ts",
269                DataType::Timestamp(datatypes::arrow::datatypes::TimeUnit::Millisecond, None),
270                false,
271            ),
272            Field::new("__primary_key", DataType::Binary, false),
273            Field::new("__sequence", DataType::UInt64, false),
274            Field::new("__op_type", DataType::UInt8, false),
275        ]));
276        RecordBatch::try_new(
277            schema,
278            vec![
279                Arc::new(TimestampMillisecondArray::from(timestamps.to_vec())),
280                Arc::new(BinaryArray::from_iter_values(
281                    primary_keys.iter().map(Vec::as_slice),
282                )),
283                Arc::new(UInt64Array::from(vec![1; timestamps.len()])),
284                Arc::new(UInt8Array::from(vec![0; timestamps.len()])),
285            ],
286        )
287        .unwrap()
288    }
289
290    async fn write_index(
291        metadata: RegionMetadataRef,
292        object_store: ObjectStore,
293        path: &str,
294        rows: &[(u32, u64, &str, &str, i64)],
295        row_group_size: usize,
296    ) {
297        let primary_keys = rows
298            .iter()
299            .map(|(table_id, tsid, tag_0, tag_1, _)| {
300                new_sparse_primary_key(&[*tag_0, *tag_1], &metadata, *table_id, *tsid)
301            })
302            .collect::<Vec<_>>();
303        let timestamps = rows.iter().map(|row| row.4).collect::<Vec<_>>();
304        let mut writer = SeriesIndexWriter::try_new(
305            metadata,
306            object_store,
307            path,
308            SeriesIndexWriterOptions { row_group_size },
309            None,
310        )
311        .await
312        .unwrap();
313        writer
314            .write(&flat_batch(&primary_keys, &timestamps))
315            .await
316            .unwrap();
317        writer.finish().await.unwrap();
318    }
319
320    async fn collect_ids(stream: MetricSeriesIdStream) -> Vec<MetricSeriesId> {
321        stream
322            .try_collect::<Vec<_>>()
323            .await
324            .unwrap()
325            .into_iter()
326            .flatten()
327            .collect()
328    }
329
330    #[tokio::test]
331    async fn search_applies_candidate_tag_filters_and_time_overlap() {
332        let metadata = Arc::new(sst_region_metadata_with_encoding(
333            PrimaryKeyEncoding::Sparse,
334        ));
335        let object_store = object_store();
336        let path = "search.parquet";
337        write_index(
338            metadata.clone(),
339            object_store.clone(),
340            path,
341            &[
342                (1, 10, "a", "x", 10),
343                (1, 20, "b", "x", 20),
344                (1, 30, "a", "y", 30),
345            ],
346            2,
347        )
348        .await;
349
350        // The field filter is not available in the series index and is ignored,
351        // matching candidate-primary-key filter behavior.
352        let predicate = Predicate::new(vec![
353            col("tag_0").eq(lit("a")),
354            col("field_0").gt(lit(0_u64)),
355        ]);
356        let time_range = TimestampRange::new(
357            common_time::Timestamp::new_millisecond(20),
358            common_time::Timestamp::new_millisecond(31),
359        )
360        .unwrap();
361        let searcher = SeriesIndexSearcher::try_new(
362            metadata.clone(),
363            object_store.clone(),
364            Some(&predicate),
365            Some(time_range),
366        )
367        .unwrap();
368        let ids = collect_ids(searcher.search(path).await.unwrap()).await;
369        assert_eq!(
370            ids,
371            vec![MetricSeriesId {
372                table_id: 1,
373                tsid: 30
374            }]
375        );
376
377        // Both bounds fall between millisecond ticks. Rounding the inclusive
378        // start and exclusive end upward leaves only the 30 ms series.
379        let time_range = TimestampRange::new(
380            common_time::Timestamp::new_microsecond(20_001),
381            common_time::Timestamp::new_microsecond(30_001),
382        )
383        .unwrap();
384        let searcher = SeriesIndexSearcher::try_new(
385            metadata.clone(),
386            object_store.clone(),
387            None,
388            Some(time_range),
389        )
390        .unwrap();
391        let ids = collect_ids(searcher.search(path).await.unwrap()).await;
392        assert_eq!(
393            ids,
394            vec![MetricSeriesId {
395                table_id: 1,
396                tsid: 30
397            }]
398        );
399
400        // The stored maximum equal to the query start intersects, while the
401        // stored minimum equal to the exclusive query end does not.
402        let time_range = TimestampRange::new(
403            common_time::Timestamp::new_millisecond(20),
404            common_time::Timestamp::new_millisecond(30),
405        )
406        .unwrap();
407        let searcher =
408            SeriesIndexSearcher::try_new(metadata, object_store, None, Some(time_range)).unwrap();
409        let ids = collect_ids(searcher.search(path).await.unwrap()).await;
410        assert_eq!(
411            ids,
412            vec![MetricSeriesId {
413                table_id: 1,
414                tsid: 20
415            }]
416        );
417    }
418
419    #[tokio::test]
420    async fn search_skips_filters_for_columns_missing_from_older_index() {
421        let old_metadata = Arc::new(sst_region_metadata_with_encoding(
422            PrimaryKeyEncoding::Sparse,
423        ));
424        let object_store = object_store();
425        let path = "schema-evolution.parquet";
426        write_index(
427            old_metadata.clone(),
428            object_store.clone(),
429            path,
430            &[
431                (1, 10, "a", "x", 10),
432                (1, 20, "b", "x", 20),
433                (1, 30, "a", "y", 30),
434            ],
435            2,
436        )
437        .await;
438
439        let mut builder = RegionMetadataBuilder::from_existing(old_metadata.as_ref().clone());
440        builder.push_column_metadata(ColumnMetadata {
441            column_schema: ColumnSchema::new("tag_2", ConcreteDataType::string_datatype(), true),
442            semantic_type: SemanticType::Tag,
443            column_id: 4,
444        });
445        let mut primary_key = old_metadata.primary_key.clone();
446        primary_key.push(4);
447        builder.primary_key(primary_key);
448        let current_metadata = Arc::new(builder.build().unwrap());
449
450        let predicate =
451            Predicate::new(vec![col("tag_0").eq(lit("a")), col("tag_2").eq(lit("new"))]);
452        let searcher =
453            SeriesIndexSearcher::try_new(current_metadata, object_store, Some(&predicate), None)
454                .unwrap();
455        let ids = collect_ids(searcher.search(path).await.unwrap()).await;
456        assert_eq!(
457            ids,
458            vec![
459                MetricSeriesId {
460                    table_id: 1,
461                    tsid: 10,
462                },
463                MetricSeriesId {
464                    table_id: 1,
465                    tsid: 30,
466                },
467            ]
468        );
469    }
470
471    #[tokio::test]
472    async fn search_streams_fixed_size_batches() {
473        let metadata = Arc::new(sst_region_metadata_with_encoding(
474            PrimaryKeyEncoding::Sparse,
475        ));
476        let object_store = object_store();
477        let rows = (0..501_u64)
478            .map(|tsid| (1, tsid, "a", "x", tsid as i64))
479            .collect::<Vec<_>>();
480        write_index(
481            metadata.clone(),
482            object_store.clone(),
483            "batching.parquet",
484            &rows,
485            100,
486        )
487        .await;
488
489        let searcher = SeriesIndexSearcher::try_new(metadata, object_store, None, None).unwrap();
490        let batches = searcher
491            .search("batching.parquet")
492            .await
493            .unwrap()
494            .try_collect::<Vec<_>>()
495            .await
496            .unwrap();
497        assert_eq!(batches.iter().map(Vec::len).collect::<Vec<_>>(), [500, 1]);
498        assert_eq!(
499            batches[0][0],
500            MetricSeriesId {
501                table_id: 1,
502                tsid: 0
503            }
504        );
505        assert_eq!(
506            batches[1][0],
507            MetricSeriesId {
508                table_id: 1,
509                tsid: 500
510            }
511        );
512    }
513
514    #[tokio::test]
515    async fn search_prunes_row_groups_and_empty_ranges() {
516        let metadata = Arc::new(sst_region_metadata_with_encoding(
517            PrimaryKeyEncoding::Sparse,
518        ));
519        let object_store = object_store();
520        let path = "pruning.parquet";
521        write_index(
522            metadata.clone(),
523            object_store.clone(),
524            path,
525            &[
526                (1, 0, "a", "x", 0),
527                (1, 1, "b", "x", 1),
528                (1, 2, "m", "x", 2),
529                (1, 3, "m", "x", 3),
530                (1, 4, "y", "x", 4),
531                (1, 5, "z", "x", 5),
532            ],
533            2,
534        )
535        .await;
536
537        let predicate = Predicate::new(vec![col("tag_0").eq(lit("m"))]);
538        let searcher = SeriesIndexSearcher::try_new(
539            metadata.clone(),
540            object_store.clone(),
541            Some(&predicate),
542            None,
543        )
544        .unwrap();
545        let reader = ParquetIndexReader::open(object_store.clone(), path)
546            .await
547            .unwrap();
548        let (pruning_predicate, _) = searcher.filters_for_schema(reader.schema());
549        assert_eq!(reader.row_groups_to_read(&pruning_predicate), vec![1]);
550
551        let empty = SeriesIndexSearcher::try_new(
552            metadata,
553            object_store,
554            None,
555            Some(TimestampRange::empty()),
556        )
557        .unwrap();
558        assert!(
559            empty
560                .search("does-not-need-to-exist.parquet")
561                .await
562                .unwrap()
563                .try_collect::<Vec<_>>()
564                .await
565                .unwrap()
566                .is_empty()
567        );
568    }
569}