1use std::collections::HashMap;
9use std::hash::BuildHasher;
10use std::path::PathBuf;
11
12use chronix_engine::index::{SegmentCatalog, SegmentCatalogEntry, SeriesBloomFilter, TimeIndex};
13
14#[derive(Debug, Clone, Default)]
16pub struct PruningStats {
17 pub segments_total: usize,
19 pub pruned_by_time: usize,
21 pub pruned_by_bloom: usize,
23 pub pruned_by_stats: usize,
25 pub segments_remaining: usize,
27}
28
29impl PruningStats {
30 #[must_use]
32 pub fn total_pruned(&self) -> usize {
33 self.pruned_by_time + self.pruned_by_bloom + self.pruned_by_stats
34 }
35}
36
37#[derive(Debug)]
39pub struct PruningResult {
40 pub segments: Vec<PrunedSegment>,
42 pub stats: PruningStats,
44}
45
46#[derive(Debug, Clone)]
48pub struct PrunedSegment {
49 pub entry: SegmentCatalogEntry,
51 pub path: PathBuf,
53}
54
55#[must_use]
74pub fn prune_segments<S: BuildHasher>(
75 time_index: &TimeIndex,
76 catalog: &SegmentCatalog,
77 bloom_filters: &HashMap<u64, SeriesBloomFilter, S>,
78 start_ts: i64,
79 end_ts: i64,
80 series_key: Option<&chronix_core::SeriesKey>,
81 tag_filter_keys: &[&str],
82) -> PruningResult {
83 let segments_total = catalog.segment_count();
84
85 let time_matching_ids = time_index.segments_for_range(start_ts, end_ts);
87 let pruned_by_time = segments_total - time_matching_ids.len();
88
89 let entries: Vec<&SegmentCatalogEntry> = {
91 let all = catalog.all_segments();
92 let id_map: std::collections::HashMap<u64, &SegmentCatalogEntry> =
93 all.iter().map(|e| (e.segment_id.0, *e)).collect();
94 time_matching_ids
95 .iter()
96 .filter_map(|seg_id| id_map.get(&seg_id.0).copied())
97 .collect()
98 };
99
100 let bloom_lookup = |seg_id: u64| bloom_filters.get(&seg_id);
101
102 let mut result = prune_entries(entries, bloom_lookup, series_key, tag_filter_keys);
103 result.stats.segments_total = segments_total;
104 result.stats.pruned_by_time = pruned_by_time;
105 result
106}
107
108#[must_use]
121pub fn prune_entries<'a, F>(
122 entries: Vec<&'a SegmentCatalogEntry>,
123 bloom_lookup: F,
124 series_key: Option<&chronix_core::SeriesKey>,
125 tag_filter_keys: &[&str],
126) -> PruningResult
127where
128 F: Fn(u64) -> Option<&'a SeriesBloomFilter>,
129{
130 let input_count = entries.len();
131 let mut stats = PruningStats::default();
132
133 let mut surviving: Vec<PrunedSegment> = Vec::with_capacity(input_count);
135
136 for entry in entries {
137 if let Some(series_key) = series_key {
138 if let Some(bloom) = bloom_lookup(entry.segment_id.0) {
139 if !bloom.may_contain(series_key) {
140 stats.pruned_by_bloom += 1;
141 continue;
142 }
143 }
144 }
145
146 surviving.push(PrunedSegment {
147 path: entry.path.clone(),
148 entry: entry.clone(),
149 });
150 }
151
152 if !tag_filter_keys.is_empty() {
156 surviving.retain(|ps| {
157 for tag_key in tag_filter_keys {
158 if let Some(cs) = ps.entry.column_stats.iter().find(|c| c.name == *tag_key) {
159 if cs.stats.distinct_count == 0 && cs.stats.value_count == 0 {
160 stats.pruned_by_stats += 1;
161 return false;
162 }
163 }
164 }
165 true
166 });
167 }
168
169 stats.segments_remaining = surviving.len();
170
171 PruningResult {
172 segments: surviving,
173 stats,
174 }
175}
176
177#[cfg(test)]
178mod tests {
179 use super::*;
180 use chronix_core::{SegmentId, SegmentState, ShardId};
181 use chronix_engine::index::time_index::TimeIndexEntry;
182 use std::collections::BTreeMap;
183
184 fn make_entry(id: u64, shard: i64, min_ts: i64, max_ts: i64) -> SegmentCatalogEntry {
185 SegmentCatalogEntry {
186 segment_id: SegmentId(id),
187 shard_id: ShardId(shard),
188 measurement: "cpu".to_string(),
189 path: PathBuf::from(format!("shard_{shard}/seg_{id}.csx")),
190 min_timestamp: min_ts,
191 max_timestamp: max_ts,
192 row_count: 1000,
193 series_count: 10,
194 byte_size: 4096,
195 row_group_count: 1,
196 column_count: 5,
197 column_stats: Vec::new(),
198 state: SegmentState::default(),
199 }
200 }
201
202 fn setup() -> (TimeIndex, SegmentCatalog) {
203 let dir = tempfile::tempdir().unwrap();
204 let time_index = TimeIndex::new();
205 let mut catalog = SegmentCatalog::new(dir.path()).unwrap();
206
207 let entries = vec![
209 make_entry(1, 0, 100, 200),
210 make_entry(2, 0, 300, 400),
211 make_entry(3, 0, 500, 600),
212 make_entry(4, 0, 700, 800),
213 ];
214
215 for entry in &entries {
216 time_index.add_segment(TimeIndexEntry {
217 segment_id: entry.segment_id,
218 min_ts: entry.min_timestamp,
219 max_ts: entry.max_timestamp,
220 });
221 catalog.add_segment(entry.clone()).unwrap();
222 }
223
224 (time_index, catalog)
225 }
226
227 #[test]
228 fn time_pruning() {
229 let (time_index, catalog) = setup();
230 let blooms = HashMap::new();
231
232 let result = prune_segments(
233 &time_index,
234 &catalog,
235 &blooms,
236 250,
237 550, None,
239 &[],
240 );
241
242 assert_eq!(result.stats.segments_total, 4);
243 assert_eq!(result.stats.pruned_by_time, 2);
244 assert_eq!(result.segments.len(), 2);
245 assert_eq!(result.segments[0].entry.segment_id, SegmentId(2));
246 assert_eq!(result.segments[1].entry.segment_id, SegmentId(3));
247 }
248
249 #[test]
250 fn bloom_pruning() {
251 let (time_index, catalog) = setup();
252
253 let key1 = chronix_core::SeriesKey::new(
254 "cpu",
255 BTreeMap::from([("host".to_string(), "srv1".to_string())]),
256 )
257 .unwrap();
258
259 let key2 = chronix_core::SeriesKey::new(
260 "cpu",
261 BTreeMap::from([("host".to_string(), "srv2".to_string())]),
262 )
263 .unwrap();
264
265 let mut bloom2 = SeriesBloomFilter::new(100, 0.01);
267 bloom2.insert(&key1);
268
269 let mut bloom3 = SeriesBloomFilter::new(100, 0.01);
270 bloom3.insert(&key2); let mut bloom_map = HashMap::new();
273 bloom_map.insert(2, bloom2);
274 bloom_map.insert(3, bloom3);
275
276 let result = prune_segments(
277 &time_index,
278 &catalog,
279 &bloom_map,
280 250,
281 550,
282 Some(&key1),
283 &["host"],
284 );
285
286 assert_eq!(result.stats.pruned_by_bloom, 1);
288 assert_eq!(result.segments.len(), 1);
289 assert_eq!(result.segments[0].entry.segment_id, SegmentId(2));
290 }
291
292 #[test]
293 fn no_pruning_when_all_match() {
294 let (time_index, catalog) = setup();
295 let blooms = HashMap::new();
296
297 let result = prune_segments(&time_index, &catalog, &blooms, 0, 1000, None, &[]);
298
299 assert_eq!(result.stats.pruned_by_time, 0);
300 assert_eq!(result.stats.pruned_by_bloom, 0);
301 assert_eq!(result.segments.len(), 4);
302 }
303
304 #[test]
305 fn all_pruned() {
306 let (time_index, catalog) = setup();
307 let blooms = HashMap::new();
308
309 let result = prune_segments(
310 &time_index,
311 &catalog,
312 &blooms,
313 900,
314 1000, None,
316 &[],
317 );
318
319 assert_eq!(result.segments.len(), 0);
320 assert_eq!(result.stats.pruned_by_time, 4);
321 }
322
323 #[test]
324 fn stats_pruning_by_all_null_tag() {
325 use chronix_engine::index::CatalogColumnStats;
326 use chronix_engine::segment::stats::ColumnStats;
327
328 let dir = tempfile::tempdir().unwrap();
329 let time_index = TimeIndex::new();
330 let mut catalog = SegmentCatalog::new(dir.path()).unwrap();
331
332 let mut entry1 = make_entry(1, 0, 100, 200);
334 entry1.column_stats = vec![CatalogColumnStats {
335 name: "host".to_string(),
336 data_type: 1, role: 1, decimal_scale: None,
339 stats: ColumnStats {
340 min_value: 0,
341 max_value: 0,
342 min_value_u64: 0,
343 max_value_u64: 0,
344 null_count: 0,
345 value_count: 10,
346 sum: 0.0,
347 sum_i128: 0,
348 distinct_count: 2, },
350 }];
351
352 let mut entry2 = make_entry(2, 0, 300, 400);
354 entry2.column_stats = vec![CatalogColumnStats {
355 name: "host".to_string(),
356 data_type: 1,
357 role: 1,
358 decimal_scale: None,
359 stats: ColumnStats {
360 min_value: 0,
361 max_value: 0,
362 min_value_u64: 0,
363 max_value_u64: 0,
364 null_count: 10,
365 value_count: 0,
366 sum: 0.0,
367 sum_i128: 0,
368 distinct_count: 0, },
370 }];
371
372 for entry in [&entry1, &entry2] {
373 time_index.add_segment(TimeIndexEntry {
374 segment_id: entry.segment_id,
375 min_ts: entry.min_timestamp,
376 max_ts: entry.max_timestamp,
377 });
378 catalog.add_segment(entry.clone()).unwrap();
379 }
380
381 let blooms = HashMap::new();
382 let result = prune_segments(
383 &time_index,
384 &catalog,
385 &blooms,
386 0,
387 500,
388 None,
389 &["host"], );
391
392 assert_eq!(result.stats.pruned_by_stats, 1);
394 assert_eq!(result.segments.len(), 1);
395 assert_eq!(result.segments[0].entry.segment_id, SegmentId(1));
396 }
397}