1use chronix_engine::index::{SegmentCatalogEntry, SeriesBloomFilter};
18
19#[derive(Debug, Clone, Default)]
21pub struct PruningStats {
22 pub segments_total: usize,
24 pub pruned_by_time: usize,
26 pub pruned_by_bloom: usize,
28 pub pruned_by_stats: usize,
30 pub segments_remaining: usize,
32}
33
34impl PruningStats {
35 #[must_use]
37 pub fn total_pruned(&self) -> usize {
38 self.pruned_by_time + self.pruned_by_bloom + self.pruned_by_stats
39 }
40}
41
42#[derive(Debug)]
44pub struct PruningResult {
45 pub segments: Vec<PrunedSegment>,
47 pub stats: PruningStats,
49}
50
51#[derive(Debug, Clone)]
57pub struct PrunedSegment {
58 pub entry: SegmentCatalogEntry,
60}
61
62#[must_use]
75pub fn prune_entries<'a, F>(
76 entries: Vec<&'a SegmentCatalogEntry>,
77 bloom_lookup: F,
78 series_key: Option<&chronix_core::SeriesKey>,
79 tag_filter_keys: &[&str],
80) -> PruningResult
81where
82 F: Fn(u64) -> Option<&'a SeriesBloomFilter>,
83{
84 let input_count = entries.len();
85 let mut stats = PruningStats::default();
86
87 let mut surviving: Vec<PrunedSegment> = Vec::with_capacity(input_count);
89
90 for entry in entries {
91 if let Some(series_key) = series_key {
92 if let Some(bloom) = bloom_lookup(entry.segment_id.0) {
93 if !bloom.may_contain(series_key) {
94 stats.pruned_by_bloom += 1;
95 continue;
96 }
97 }
98 }
99
100 surviving.push(PrunedSegment {
101 entry: entry.clone(),
102 });
103 }
104
105 if !tag_filter_keys.is_empty() {
109 surviving.retain(|ps| {
110 for tag_key in tag_filter_keys {
111 if let Some(cs) = ps.entry.column_stats.iter().find(|c| c.name == *tag_key) {
112 if cs.stats.distinct_count == 0 && cs.stats.value_count == 0 {
113 stats.pruned_by_stats += 1;
114 return false;
115 }
116 }
117 }
118 true
119 });
120 }
121
122 stats.segments_remaining = surviving.len();
123
124 PruningResult {
125 segments: surviving,
126 stats,
127 }
128}
129
130#[cfg(test)]
131mod tests {
132 use super::*;
133 use chronix_core::{SegmentFile, SegmentId, SegmentState, ShardId};
134
135 fn make_entry(id: u64, shard: i64, min_ts: i64, max_ts: i64) -> SegmentCatalogEntry {
136 SegmentCatalogEntry {
137 segment_id: SegmentId(id),
138 shard_id: ShardId(shard),
139 measurement: "cpu".to_string(),
140 file: SegmentFile::new(ShardId(shard), &format!("seg_{id}.csx")).unwrap(),
141 min_timestamp: min_ts,
142 max_timestamp: max_ts,
143 row_count: 1000,
144 series_count: 10,
145 byte_size: 4096,
146 row_group_count: 1,
147 column_count: 5,
148 column_stats: Vec::new(),
149 state: SegmentState::default(),
150 }
151 }
152
153 fn tag_stats(
154 name: &str,
155 distinct: u32,
156 values: u64,
157 ) -> chronix_engine::index::CatalogColumnStats {
158 chronix_engine::index::CatalogColumnStats {
159 name: name.to_string(),
160 data_type: 1, role: 1, decimal_scale: None,
163 stats: chronix_engine::segment::stats::ColumnStats {
164 min_value: 0,
165 max_value: 0,
166 min_value_u64: 0,
167 max_value_u64: 0,
168 null_count: 0,
169 value_count: values,
170 sum: 0.0,
171 sum_i128: 0,
172 distinct_count: distinct,
173 },
174 }
175 }
176
177 #[test]
178 fn nothing_is_pruned_without_a_series_key_or_a_tag_filter() {
179 let entries = [make_entry(1, 0, 100, 200), make_entry(2, 0, 300, 400)];
180 let refs: Vec<&SegmentCatalogEntry> = entries.iter().collect();
181 let result = prune_entries(refs, |_| None, None, &[]);
182 assert_eq!(result.segments.len(), 2);
183 assert_eq!(result.stats.total_pruned(), 0);
184 }
185
186 #[test]
192 fn a_segment_without_a_bloom_is_kept() {
193 use chronix_engine::index::SeriesBloomFilter;
194 let entries = [make_entry(1, 0, 100, 200), make_entry(2, 0, 300, 400)];
195 let refs: Vec<&SegmentCatalogEntry> = entries.iter().collect();
196
197 let key = chronix_core::SeriesKey::new(
198 "cpu",
199 [("host".to_string(), "a".to_string())]
200 .into_iter()
201 .collect::<std::collections::BTreeMap<_, _>>(),
202 )
203 .unwrap();
204
205 let mut bloom = SeriesBloomFilter::new(8, 0.01);
207 bloom.insert(
208 &chronix_core::SeriesKey::new(
209 "cpu",
210 [("host".to_string(), "z".to_string())]
211 .into_iter()
212 .collect::<std::collections::BTreeMap<_, _>>(),
213 )
214 .unwrap(),
215 );
216
217 let result = prune_entries(refs, |id| (id == 1).then_some(&bloom), Some(&key), &[]);
218 assert_eq!(result.stats.pruned_by_bloom, 1);
219 assert_eq!(result.segments.len(), 1);
220 assert_eq!(result.segments[0].entry.segment_id, SegmentId(2));
221 }
222
223 #[test]
226 fn an_all_null_tag_column_is_pruned_by_its_statistics() {
227 let mut with_values = make_entry(1, 0, 100, 200);
228 with_values.column_stats = vec![tag_stats("host", 2, 10)];
229 let mut all_null = make_entry(2, 0, 300, 400);
230 all_null.column_stats = vec![tag_stats("host", 0, 0)];
231
232 let entries = [with_values, all_null];
233 let refs: Vec<&SegmentCatalogEntry> = entries.iter().collect();
234 let result = prune_entries(refs, |_| None, None, &["host"]);
235
236 assert_eq!(result.stats.pruned_by_stats, 1);
237 assert_eq!(result.segments.len(), 1);
238 assert_eq!(result.segments[0].entry.segment_id, SegmentId(1));
239 }
240
241 #[test]
243 fn a_segment_with_no_statistics_for_the_tag_is_kept() {
244 let entries = [make_entry(1, 0, 100, 200)];
245 let refs: Vec<&SegmentCatalogEntry> = entries.iter().collect();
246 let result = prune_entries(refs, |_| None, None, &["host"]);
247 assert_eq!(result.segments.len(), 1);
248 assert_eq!(result.stats.pruned_by_stats, 0);
249 }
250}