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]
172 pub fn segments_for_tags(&self, filters: &[(&str, &str)]) -> Vec<SegmentId> {
173 if filters.is_empty() {
174 return Vec::new();
175 }
176
177 let s = self.state.read();
178 let mut result: Option<HashSet<SegmentId>> = None;
179
180 for &(key, value) in filters {
181 let entry_key = make_tag_key(key, value);
182 let matching = s.index.get(&entry_key).cloned().unwrap_or_default();
183
184 result = Some(match result {
185 Some(existing) => existing.intersection(&matching).copied().collect(),
186 None => matching,
187 });
188 }
189
190 let mut out: Vec<SegmentId> = result.map_or_else(Vec::new, |s| s.into_iter().collect());
191 out.sort();
192 out
193 }
194
195 pub fn replace_segments(
201 &self,
202 old_segment_ids: &[SegmentId],
203 new_segment_id: SegmentId,
204 new_tag_values: &[(&str, &str)],
205 ) {
206 let mut s = self.state.write();
207
208 for &old_id in old_segment_ids {
210 if let Some(keys) = s.reverse.remove(&old_id) {
211 for key in &keys {
212 if let Some(segments) = s.index.get_mut(key) {
213 segments.remove(&old_id);
214 if segments.is_empty() {
215 s.index.remove(key);
216 if let Some(pos) = key.find(chronix_core::KV_SEPARATOR) {
217 let tag_key = &key[..pos];
218 let tag_val = &key[pos + 1..];
219 if let Some(vals) = s.key_values.get_mut(tag_key) {
220 vals.remove(tag_val);
221 if vals.is_empty() {
222 s.key_values.remove(tag_key);
223 }
224 }
225 }
226 }
227 }
228 }
229 }
230 }
231
232 let mut rev_keys = Vec::with_capacity(new_tag_values.len());
234 for &(key, value) in new_tag_values {
235 let entry_key = make_tag_key(key, value);
236 s.index
237 .entry(entry_key.clone())
238 .or_default()
239 .insert(new_segment_id);
240 rev_keys.push(entry_key);
241 s.key_values
242 .entry(key.to_string())
243 .or_default()
244 .insert(value.to_string());
245 }
246 s.reverse
247 .entry(new_segment_id)
248 .or_default()
249 .extend(rev_keys);
250 }
251
252 pub fn clear(&self) {
254 let mut s = self.state.write();
255 s.index.clear();
256 s.reverse.clear();
257 s.key_values.clear();
258 }
259
260 #[must_use]
262 pub fn entry_count(&self) -> usize {
263 self.state.read().index.len()
264 }
265
266 #[must_use]
271 pub fn values_for_key(&self, key: &str) -> Vec<String> {
272 let s = self.state.read();
273 let mut values: Vec<String> = s
274 .key_values
275 .get(key)
276 .map(|s| s.iter().cloned().collect())
277 .unwrap_or_default();
278 values.sort();
279 values
280 }
281
282 #[must_use]
287 pub fn all_keys(&self) -> Vec<String> {
288 let s = self.state.read();
289 let mut keys: Vec<String> = s.key_values.keys().cloned().collect();
290 keys.sort();
291 keys
292 }
293}
294
295impl Default for TagInvertedIndex {
296 fn default() -> Self {
297 Self::new()
298 }
299}
300
301#[cfg(test)]
302mod tests {
303 use super::*;
304
305 #[test]
306 fn add_and_query_tag() {
307 let idx = TagInvertedIndex::new();
308 idx.add_segment(SegmentId(1), &[("host", "srv1"), ("region", "us-east")]);
309 idx.add_segment(SegmentId(2), &[("host", "srv2"), ("region", "us-east")]);
310 idx.add_segment(SegmentId(3), &[("host", "srv1"), ("region", "eu-west")]);
311
312 let segs = idx.segments_for_tag("host", "srv1");
313 assert_eq!(segs.len(), 2);
314 assert!(segs.contains(&SegmentId(1)));
315 assert!(segs.contains(&SegmentId(3)));
316 }
317
318 #[test]
319 fn intersection_multi_tag() {
320 let idx = TagInvertedIndex::new();
321 idx.add_segment(SegmentId(1), &[("host", "srv1"), ("region", "us-east")]);
322 idx.add_segment(SegmentId(2), &[("host", "srv1"), ("region", "eu-west")]);
323 idx.add_segment(SegmentId(3), &[("host", "srv2"), ("region", "us-east")]);
324
325 let segs = idx.segments_for_tags(&[("host", "srv1"), ("region", "us-east")]);
326 assert_eq!(segs.len(), 1);
327 assert!(segs.contains(&SegmentId(1)));
328 }
329
330 #[test]
331 fn remove_segment() {
332 let idx = TagInvertedIndex::new();
333 idx.add_segment(SegmentId(1), &[("host", "srv1")]);
334 idx.add_segment(SegmentId(2), &[("host", "srv1")]);
335
336 idx.remove_segment(SegmentId(1));
337 let segs = idx.segments_for_tag("host", "srv1");
338 assert_eq!(segs.len(), 1);
339 assert!(segs.contains(&SegmentId(2)));
340 }
341
342 #[test]
343 fn missing_tag_returns_empty() {
344 let idx = TagInvertedIndex::new();
345 idx.add_segment(SegmentId(1), &[("host", "srv1")]);
346
347 assert!(idx.segments_for_tag("datacenter", "dc1").is_empty());
348 }
349
350 #[test]
351 fn clear_empties_index() {
352 let idx = TagInvertedIndex::new();
353 idx.add_segment(SegmentId(1), &[("host", "srv1")]);
354 idx.clear();
355 assert_eq!(idx.entry_count(), 0);
356 }
357
358 #[test]
359 fn empty_filters_returns_empty() {
360 let idx = TagInvertedIndex::new();
361 idx.add_segment(SegmentId(1), &[("host", "srv1")]);
362 assert!(idx.segments_for_tags(&[]).is_empty());
363 }
364
365 #[test]
366 fn values_for_key_returns_sorted_distinct() {
367 let idx = TagInvertedIndex::new();
368 idx.add_segment(SegmentId(1), &[("host", "srv1"), ("region", "us-east")]);
369 idx.add_segment(SegmentId(2), &[("host", "srv2"), ("region", "us-east")]);
370 idx.add_segment(SegmentId(3), &[("host", "srv1"), ("region", "eu-west")]);
371
372 let hosts = idx.values_for_key("host");
373 assert_eq!(hosts, vec!["srv1", "srv2"]);
374
375 let regions = idx.values_for_key("region");
376 assert_eq!(regions, vec!["eu-west", "us-east"]);
377
378 assert!(idx.values_for_key("datacenter").is_empty());
380 }
381
382 #[test]
383 fn all_keys_returns_sorted_distinct() {
384 let idx = TagInvertedIndex::new();
385 idx.add_segment(SegmentId(1), &[("host", "srv1"), ("region", "us-east")]);
386 idx.add_segment(SegmentId(2), &[("env", "prod"), ("host", "srv2")]);
387
388 let keys = idx.all_keys();
389 assert_eq!(keys, vec!["env", "host", "region"]);
390 }
391
392 #[test]
393 fn all_keys_empty_index() {
394 let idx = TagInvertedIndex::new();
395 assert!(idx.all_keys().is_empty());
396 assert!(idx.values_for_key("any").is_empty());
397 }
398
399 #[test]
400 fn remove_segment_cleans_up_reverse_index() {
401 let idx = TagInvertedIndex::new();
402 idx.add_segment(SegmentId(1), &[("host", "srv1"), ("region", "us-east")]);
403 idx.add_segment(SegmentId(2), &[("host", "srv2"), ("region", "eu-west")]);
404
405 assert_eq!(idx.state.read().reverse.len(), 2);
407
408 idx.remove_segment(SegmentId(1));
410 assert_eq!(idx.state.read().reverse.len(), 1);
411 assert!(idx.segments_for_tag("host", "srv1").is_empty());
412 assert!(idx.segments_for_tag("region", "us-east").is_empty());
413 assert_eq!(idx.segments_for_tag("host", "srv2").len(), 1);
414 assert_eq!(idx.segments_for_tag("region", "eu-west").len(), 1);
415 }
416
417 #[test]
418 fn remove_nonexistent_segment_is_noop() {
419 let idx = TagInvertedIndex::new();
420 idx.add_segment(SegmentId(1), &[("host", "srv1")]);
421 idx.remove_segment(SegmentId(999)); assert_eq!(idx.segments_for_tag("host", "srv1").len(), 1);
423 }
424
425 #[test]
429 fn equals_in_tag_key_or_value_is_unambiguous() {
430 let idx = TagInvertedIndex::new();
431 idx.add_segment(SegmentId(1), &[("a=b", "c")]);
432 idx.add_segment(SegmentId(2), &[("a", "b=c")]);
433
434 assert_eq!(idx.segments_for_tag("a=b", "c"), vec![SegmentId(1)]);
435 assert_eq!(idx.segments_for_tag("a", "b=c"), vec![SegmentId(2)]);
436
437 idx.remove_segment(SegmentId(1));
439 assert!(idx.segments_for_tag("a=b", "c").is_empty());
440 assert_eq!(
441 idx.segments_for_tag("a", "b=c"),
442 vec![SegmentId(2)],
443 "removing one segment corrupted the other tag's entry"
444 );
445 }
446
447 #[test]
448 fn key_values_index_cleanup_on_remove() {
449 let idx = TagInvertedIndex::new();
450 idx.add_segment(SegmentId(1), &[("host", "srv1"), ("region", "us-east")]);
451 idx.add_segment(SegmentId(2), &[("host", "srv2"), ("region", "us-east")]);
452
453 assert_eq!(idx.values_for_key("host"), vec!["srv1", "srv2"]);
455
456 idx.remove_segment(SegmentId(1));
458 assert_eq!(idx.values_for_key("host"), vec!["srv2"]);
459 assert_eq!(idx.values_for_key("region"), vec!["us-east"]);
461
462 idx.remove_segment(SegmentId(2));
464 assert!(idx.values_for_key("host").is_empty());
465 assert!(idx.values_for_key("region").is_empty());
466 assert!(idx.all_keys().is_empty());
467 }
468
469 #[test]
470 fn replace_segments_atomic_swap() {
471 let idx = TagInvertedIndex::new();
472 idx.add_segment(SegmentId(1), &[("host", "srv1"), ("region", "us-east")]);
473 idx.add_segment(SegmentId(2), &[("host", "srv2"), ("region", "us-east")]);
474 idx.add_segment(SegmentId(3), &[("host", "srv3"), ("region", "eu-west")]);
475
476 idx.replace_segments(
478 &[SegmentId(1), SegmentId(2)],
479 SegmentId(10),
480 &[("host", "srv1"), ("host", "srv2"), ("region", "us-east")],
481 );
482
483 assert!(idx
485 .segments_for_tag("host", "srv1")
486 .contains(&SegmentId(10)));
487 assert!(!idx.segments_for_tag("host", "srv1").contains(&SegmentId(1)));
488 assert!(idx
489 .segments_for_tag("host", "srv2")
490 .contains(&SegmentId(10)));
491 assert!(!idx.segments_for_tag("host", "srv2").contains(&SegmentId(2)));
492
493 assert_eq!(idx.segments_for_tag("host", "srv3"), vec![SegmentId(3)]);
495 assert!(idx
496 .segments_for_tag("region", "eu-west")
497 .contains(&SegmentId(3)));
498
499 assert!(idx
501 .segments_for_tag("region", "us-east")
502 .contains(&SegmentId(10)));
503
504 let mut hosts = idx.values_for_key("host");
506 hosts.sort();
507 assert_eq!(hosts, vec!["srv1", "srv2", "srv3"]);
508 }
509
510 #[test]
511 fn replace_segments_empty_old_list() {
512 let idx = TagInvertedIndex::new();
513 idx.add_segment(SegmentId(1), &[("host", "srv1")]);
514
515 idx.replace_segments(&[], SegmentId(2), &[("host", "srv2")]);
517
518 assert_eq!(idx.segments_for_tag("host", "srv1"), vec![SegmentId(1)]);
519 assert_eq!(idx.segments_for_tag("host", "srv2"), vec![SegmentId(2)]);
520 }
521}