1use std::collections::{HashMap, HashSet};
8
9use parking_lot::RwLock;
10
11use chronix_core::SegmentId;
12
13#[inline]
21fn make_tag_key(key: &str, value: &str) -> String {
22 let mut s = String::with_capacity(key.len() + 1 + value.len());
23 s.push_str(key);
24 s.push(chronix_core::KV_SEPARATOR);
25 s.push_str(value);
26 s
27}
28
29#[derive(Debug)]
48pub struct TagInvertedIndex {
49 state: RwLock<InvertedState>,
50 max_tag_cardinality: usize,
56}
57
58#[derive(Debug, Default)]
60struct InvertedState {
61 index: HashMap<String, HashSet<SegmentId>>,
63 reverse: HashMap<SegmentId, HashSet<String>>,
65 key_values: HashMap<String, HashSet<String>>,
67}
68
69impl TagInvertedIndex {
70 #[must_use]
72 pub fn new() -> Self {
73 Self {
74 state: RwLock::new(InvertedState::default()),
75 max_tag_cardinality: 1_000_000,
76 }
77 }
78
79 #[must_use]
83 pub fn with_max_cardinality(max_tag_cardinality: usize) -> Self {
84 Self {
85 state: RwLock::new(InvertedState::default()),
86 max_tag_cardinality,
87 }
88 }
89
90 pub fn add_segment(&self, segment_id: SegmentId, tag_values: &[(&str, &str)]) {
96 let mut s = self.state.write();
97 let mut rev_keys = Vec::with_capacity(tag_values.len());
98 for &(key, value) in tag_values {
99 let entry_key = make_tag_key(key, value);
100 s.index
101 .entry(entry_key.clone())
102 .or_default()
103 .insert(segment_id);
104 rev_keys.push(entry_key);
105 let kv_entry = s.key_values.entry(key.to_string()).or_default();
108 if kv_entry.len() < self.max_tag_cardinality {
109 kv_entry.insert(value.to_string());
110 } else if !kv_entry.contains(value) {
111 tracing::warn!(
112 key,
113 cardinality = kv_entry.len(),
114 limit = self.max_tag_cardinality,
115 "tag cardinality limit reached — new values dropped"
116 );
117 metrics::counter!("chronix_tag_cardinality_limit_reached_total", "key" => key.to_string())
118 .increment(1);
119 }
120 }
121 s.reverse.entry(segment_id).or_default().extend(rev_keys);
122 }
123
124 pub fn remove_segment(&self, segment_id: SegmentId) {
127 let mut s = self.state.write();
128
129 if let Some(keys) = s.reverse.remove(&segment_id) {
130 for key in &keys {
131 if let Some(segments) = s.index.get_mut(key) {
132 segments.remove(&segment_id);
133 if segments.is_empty() {
134 s.index.remove(key);
135 if let Some(pos) = key.find(chronix_core::KV_SEPARATOR) {
138 let tag_key = &key[..pos];
139 let tag_val = &key[pos + 1..];
140 if let Some(vals) = s.key_values.get_mut(tag_key) {
141 vals.remove(tag_val);
142 if vals.is_empty() {
143 s.key_values.remove(tag_key);
144 }
145 }
146 }
147 }
148 }
149 }
150 }
151 }
152
153 #[must_use]
155 pub fn segments_for_tag(&self, key: &str, value: &str) -> Vec<SegmentId> {
156 let entry_key = make_tag_key(key, value);
157 let s = self.state.read();
158 let mut result: Vec<SegmentId> = s
159 .index
160 .get(&entry_key)
161 .map(|s| s.iter().copied().collect())
162 .unwrap_or_default();
163 result.sort();
164 result
165 }
166
167 #[must_use]
176 pub fn pruner<'a>(&'a self, filters: &[(&str, &str)]) -> TagPruner<'a> {
177 TagPruner {
178 keys: filters.iter().map(|&(k, v)| make_tag_key(k, v)).collect(),
179 state: self.state.read(),
180 }
181 }
182
183 pub fn replace_segments(
189 &self,
190 old_segment_ids: &[SegmentId],
191 new_segment_id: SegmentId,
192 new_tag_values: &[(&str, &str)],
193 ) {
194 let mut s = self.state.write();
195
196 for &old_id in old_segment_ids {
198 if let Some(keys) = s.reverse.remove(&old_id) {
199 for key in &keys {
200 if let Some(segments) = s.index.get_mut(key) {
201 segments.remove(&old_id);
202 if segments.is_empty() {
203 s.index.remove(key);
204 if let Some(pos) = key.find(chronix_core::KV_SEPARATOR) {
205 let tag_key = &key[..pos];
206 let tag_val = &key[pos + 1..];
207 if let Some(vals) = s.key_values.get_mut(tag_key) {
208 vals.remove(tag_val);
209 if vals.is_empty() {
210 s.key_values.remove(tag_key);
211 }
212 }
213 }
214 }
215 }
216 }
217 }
218 }
219
220 let mut rev_keys = Vec::with_capacity(new_tag_values.len());
222 for &(key, value) in new_tag_values {
223 let entry_key = make_tag_key(key, value);
224 s.index
225 .entry(entry_key.clone())
226 .or_default()
227 .insert(new_segment_id);
228 rev_keys.push(entry_key);
229 s.key_values
230 .entry(key.to_string())
231 .or_default()
232 .insert(value.to_string());
233 }
234 s.reverse
235 .entry(new_segment_id)
236 .or_default()
237 .extend(rev_keys);
238 }
239
240 pub fn clear(&self) {
242 let mut s = self.state.write();
243 s.index.clear();
244 s.reverse.clear();
245 s.key_values.clear();
246 }
247
248 #[must_use]
250 pub fn entry_count(&self) -> usize {
251 self.state.read().index.len()
252 }
253
254 #[must_use]
259 pub fn values_for_key(&self, key: &str) -> Vec<String> {
260 let s = self.state.read();
261 let mut values: Vec<String> = s
262 .key_values
263 .get(key)
264 .map(|s| s.iter().cloned().collect())
265 .unwrap_or_default();
266 values.sort();
267 values
268 }
269
270 #[must_use]
275 pub fn all_keys(&self) -> Vec<String> {
276 let s = self.state.read();
277 let mut keys: Vec<String> = s.key_values.keys().cloned().collect();
278 keys.sort();
279 keys
280 }
281}
282
283impl Default for TagInvertedIndex {
284 fn default() -> Self {
285 Self::new()
286 }
287}
288
289pub struct TagPruner<'a> {
308 state: parking_lot::RwLockReadGuard<'a, InvertedState>,
309 keys: Vec<String>,
310}
311
312impl TagPruner<'_> {
313 #[must_use]
318 pub fn excludes(&self, segment_id: SegmentId) -> bool {
319 if self.keys.is_empty() {
320 return false;
321 }
322 let Some(have) = self.state.reverse.get(&segment_id) else {
323 return false;
324 };
325 !self.keys.iter().all(|k| have.contains(k))
326 }
327}
328
329#[cfg(test)]
330mod tests {
331 use super::*;
332
333 #[test]
334 fn add_and_query_tag() {
335 let idx = TagInvertedIndex::new();
336 idx.add_segment(SegmentId(1), &[("host", "srv1"), ("region", "us-east")]);
337 idx.add_segment(SegmentId(2), &[("host", "srv2"), ("region", "us-east")]);
338 idx.add_segment(SegmentId(3), &[("host", "srv1"), ("region", "eu-west")]);
339
340 let segs = idx.segments_for_tag("host", "srv1");
341 assert_eq!(segs.len(), 2);
342 assert!(segs.contains(&SegmentId(1)));
343 assert!(segs.contains(&SegmentId(3)));
344 }
345
346 #[test]
347 fn a_segment_missing_any_filtered_pair_is_excluded() {
348 let idx = TagInvertedIndex::new();
349 idx.add_segment(SegmentId(1), &[("host", "srv1"), ("region", "us-east")]);
350 idx.add_segment(SegmentId(2), &[("host", "srv1"), ("region", "eu-west")]);
351 idx.add_segment(SegmentId(3), &[("host", "srv2"), ("region", "us-east")]);
352
353 let pruner = idx.pruner(&[("host", "srv1"), ("region", "us-east")]);
354 assert!(!pruner.excludes(SegmentId(1)));
355 assert!(pruner.excludes(SegmentId(2)));
356 assert!(pruner.excludes(SegmentId(3)));
357 }
358
359 #[test]
364 fn a_segment_the_index_has_never_seen_is_never_excluded() {
365 let idx = TagInvertedIndex::new();
366 idx.add_segment(SegmentId(1), &[("host", "srv1")]);
367
368 let pruner = idx.pruner(&[("host", "srv1")]);
369 assert!(
370 !pruner.excludes(SegmentId(99)),
371 "an unindexed segment must be scanned, not pruned"
372 );
373 }
374
375 #[test]
376 fn no_filters_excludes_nothing() {
377 let idx = TagInvertedIndex::new();
378 idx.add_segment(SegmentId(1), &[("host", "srv1")]);
379 let pruner = idx.pruner(&[]);
380 assert!(!pruner.excludes(SegmentId(1)));
381 assert!(!pruner.excludes(SegmentId(2)));
382 }
383
384 #[test]
385 fn remove_segment() {
386 let idx = TagInvertedIndex::new();
387 idx.add_segment(SegmentId(1), &[("host", "srv1")]);
388 idx.add_segment(SegmentId(2), &[("host", "srv1")]);
389
390 idx.remove_segment(SegmentId(1));
391 let segs = idx.segments_for_tag("host", "srv1");
392 assert_eq!(segs.len(), 1);
393 assert!(segs.contains(&SegmentId(2)));
394 }
395
396 #[test]
397 fn missing_tag_returns_empty() {
398 let idx = TagInvertedIndex::new();
399 idx.add_segment(SegmentId(1), &[("host", "srv1")]);
400
401 assert!(idx.segments_for_tag("datacenter", "dc1").is_empty());
402 }
403
404 #[test]
405 fn clear_empties_index() {
406 let idx = TagInvertedIndex::new();
407 idx.add_segment(SegmentId(1), &[("host", "srv1")]);
408 idx.clear();
409 assert_eq!(idx.entry_count(), 0);
410 }
411
412 #[test]
413 fn values_for_key_returns_sorted_distinct() {
414 let idx = TagInvertedIndex::new();
415 idx.add_segment(SegmentId(1), &[("host", "srv1"), ("region", "us-east")]);
416 idx.add_segment(SegmentId(2), &[("host", "srv2"), ("region", "us-east")]);
417 idx.add_segment(SegmentId(3), &[("host", "srv1"), ("region", "eu-west")]);
418
419 let hosts = idx.values_for_key("host");
420 assert_eq!(hosts, vec!["srv1", "srv2"]);
421
422 let regions = idx.values_for_key("region");
423 assert_eq!(regions, vec!["eu-west", "us-east"]);
424
425 assert!(idx.values_for_key("datacenter").is_empty());
427 }
428
429 #[test]
430 fn all_keys_returns_sorted_distinct() {
431 let idx = TagInvertedIndex::new();
432 idx.add_segment(SegmentId(1), &[("host", "srv1"), ("region", "us-east")]);
433 idx.add_segment(SegmentId(2), &[("env", "prod"), ("host", "srv2")]);
434
435 let keys = idx.all_keys();
436 assert_eq!(keys, vec!["env", "host", "region"]);
437 }
438
439 #[test]
440 fn all_keys_empty_index() {
441 let idx = TagInvertedIndex::new();
442 assert!(idx.all_keys().is_empty());
443 assert!(idx.values_for_key("any").is_empty());
444 }
445
446 #[test]
447 fn remove_segment_cleans_up_reverse_index() {
448 let idx = TagInvertedIndex::new();
449 idx.add_segment(SegmentId(1), &[("host", "srv1"), ("region", "us-east")]);
450 idx.add_segment(SegmentId(2), &[("host", "srv2"), ("region", "eu-west")]);
451
452 assert_eq!(idx.state.read().reverse.len(), 2);
454
455 idx.remove_segment(SegmentId(1));
457 assert_eq!(idx.state.read().reverse.len(), 1);
458 assert!(idx.segments_for_tag("host", "srv1").is_empty());
459 assert!(idx.segments_for_tag("region", "us-east").is_empty());
460 assert_eq!(idx.segments_for_tag("host", "srv2").len(), 1);
461 assert_eq!(idx.segments_for_tag("region", "eu-west").len(), 1);
462 }
463
464 #[test]
465 fn remove_nonexistent_segment_is_noop() {
466 let idx = TagInvertedIndex::new();
467 idx.add_segment(SegmentId(1), &[("host", "srv1")]);
468 idx.remove_segment(SegmentId(999)); assert_eq!(idx.segments_for_tag("host", "srv1").len(), 1);
470 }
471
472 #[test]
476 fn equals_in_tag_key_or_value_is_unambiguous() {
477 let idx = TagInvertedIndex::new();
478 idx.add_segment(SegmentId(1), &[("a=b", "c")]);
479 idx.add_segment(SegmentId(2), &[("a", "b=c")]);
480
481 assert_eq!(idx.segments_for_tag("a=b", "c"), vec![SegmentId(1)]);
482 assert_eq!(idx.segments_for_tag("a", "b=c"), vec![SegmentId(2)]);
483
484 idx.remove_segment(SegmentId(1));
486 assert!(idx.segments_for_tag("a=b", "c").is_empty());
487 assert_eq!(
488 idx.segments_for_tag("a", "b=c"),
489 vec![SegmentId(2)],
490 "removing one segment corrupted the other tag's entry"
491 );
492 }
493
494 #[test]
495 fn key_values_index_cleanup_on_remove() {
496 let idx = TagInvertedIndex::new();
497 idx.add_segment(SegmentId(1), &[("host", "srv1"), ("region", "us-east")]);
498 idx.add_segment(SegmentId(2), &[("host", "srv2"), ("region", "us-east")]);
499
500 assert_eq!(idx.values_for_key("host"), vec!["srv1", "srv2"]);
502
503 idx.remove_segment(SegmentId(1));
505 assert_eq!(idx.values_for_key("host"), vec!["srv2"]);
506 assert_eq!(idx.values_for_key("region"), vec!["us-east"]);
508
509 idx.remove_segment(SegmentId(2));
511 assert!(idx.values_for_key("host").is_empty());
512 assert!(idx.values_for_key("region").is_empty());
513 assert!(idx.all_keys().is_empty());
514 }
515
516 #[test]
517 fn replace_segments_atomic_swap() {
518 let idx = TagInvertedIndex::new();
519 idx.add_segment(SegmentId(1), &[("host", "srv1"), ("region", "us-east")]);
520 idx.add_segment(SegmentId(2), &[("host", "srv2"), ("region", "us-east")]);
521 idx.add_segment(SegmentId(3), &[("host", "srv3"), ("region", "eu-west")]);
522
523 idx.replace_segments(
525 &[SegmentId(1), SegmentId(2)],
526 SegmentId(10),
527 &[("host", "srv1"), ("host", "srv2"), ("region", "us-east")],
528 );
529
530 assert!(
532 idx.segments_for_tag("host", "srv1")
533 .contains(&SegmentId(10))
534 );
535 assert!(!idx.segments_for_tag("host", "srv1").contains(&SegmentId(1)));
536 assert!(
537 idx.segments_for_tag("host", "srv2")
538 .contains(&SegmentId(10))
539 );
540 assert!(!idx.segments_for_tag("host", "srv2").contains(&SegmentId(2)));
541
542 assert_eq!(idx.segments_for_tag("host", "srv3"), vec![SegmentId(3)]);
544 assert!(
545 idx.segments_for_tag("region", "eu-west")
546 .contains(&SegmentId(3))
547 );
548
549 assert!(
551 idx.segments_for_tag("region", "us-east")
552 .contains(&SegmentId(10))
553 );
554
555 let mut hosts = idx.values_for_key("host");
557 hosts.sort();
558 assert_eq!(hosts, vec!["srv1", "srv2", "srv3"]);
559 }
560
561 #[test]
562 fn replace_segments_empty_old_list() {
563 let idx = TagInvertedIndex::new();
564 idx.add_segment(SegmentId(1), &[("host", "srv1")]);
565
566 idx.replace_segments(&[], SegmentId(2), &[("host", "srv2")]);
568
569 assert_eq!(idx.segments_for_tag("host", "srv1"), vec![SegmentId(1)]);
570 assert_eq!(idx.segments_for_tag("host", "srv2"), vec![SegmentId(2)]);
571 }
572}