1use 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
39pub struct SeriesIndexSearcher {
41 object_store: ObjectStore,
42 filters: Vec<(Expr, SimpleFilterEvaluator)>,
43 empty_time_range: bool,
44}
45
46impl SeriesIndexSearcher {
47 pub fn try_new(
49 metadata: RegionMetadataRef,
50 object_store: ObjectStore,
51 predicate: Option<&Predicate>,
52 time_range: Option<TimestampRange>,
53 ) -> Result<Self> {
54 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 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 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
152fn 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 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 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, ×tamps))
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 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 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 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}