Skip to content

Commit 4023837

Browse files
committed
use inverted index to serve labels/label_values instead of forward
1 parent 1c79821 commit 4023837

7 files changed

Lines changed: 392 additions & 32 deletions

File tree

open-tsdb/src/index.rs

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,19 @@ pub(crate) trait InvertedIndexLookup {
3232
/// Intersect posting lists for the given terms.
3333
/// Returns series IDs that match ALL terms.
3434
fn intersect(&self, terms: Vec<Attribute>) -> RoaringBitmap;
35+
36+
/// Get all attribute keys in the inverted index.
37+
fn all_keys(&self) -> Vec<Attribute>;
3538
}
3639

3740
impl<T: InvertedIndexLookup + ?Sized> InvertedIndexLookup for Box<T> {
3841
fn intersect(&self, terms: Vec<Attribute>) -> RoaringBitmap {
3942
(**self).intersect(terms)
4043
}
44+
45+
fn all_keys(&self) -> Vec<Attribute> {
46+
(**self).all_keys()
47+
}
4148
}
4249

4350
#[derive(Debug, Clone, Default)]
@@ -100,6 +107,13 @@ impl InvertedIndexLookup for InvertedIndex {
100107

101108
result
102109
}
110+
111+
fn all_keys(&self) -> Vec<Attribute> {
112+
self.postings
113+
.iter()
114+
.map(|entry| entry.key().clone())
115+
.collect()
116+
}
103117
}
104118

105119
impl InvertedIndex {
@@ -782,4 +796,50 @@ mod tests {
782796
assert!(result_bitmap.contains(19998)); // Even
783797
assert!(result_bitmap.contains(19999)); // Odd
784798
}
799+
800+
#[test]
801+
fn should_return_empty_keys_for_empty_index() {
802+
// given
803+
let index = InvertedIndex::default();
804+
805+
// when
806+
let keys = index.all_keys();
807+
808+
// then
809+
assert!(keys.is_empty());
810+
}
811+
812+
#[test]
813+
fn should_return_all_keys_from_index() {
814+
// given
815+
let index = InvertedIndex::default();
816+
817+
let term1 = Attribute {
818+
key: "env".to_string(),
819+
value: "prod".to_string(),
820+
};
821+
let term2 = Attribute {
822+
key: "env".to_string(),
823+
value: "staging".to_string(),
824+
};
825+
let term3 = Attribute {
826+
key: "method".to_string(),
827+
value: "GET".to_string(),
828+
};
829+
830+
let mut bitmap = RoaringBitmap::new();
831+
bitmap.insert(1);
832+
index.postings.insert(term1.clone(), bitmap.clone());
833+
index.postings.insert(term2.clone(), bitmap.clone());
834+
index.postings.insert(term3.clone(), bitmap);
835+
836+
// when
837+
let keys = index.all_keys();
838+
839+
// then
840+
assert_eq!(keys.len(), 3);
841+
assert!(keys.contains(&term1));
842+
assert!(keys.contains(&term2));
843+
assert!(keys.contains(&term3));
844+
}
785845
}

open-tsdb/src/minitsdb.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,20 @@ impl<'a> QueryReader for MiniQueryReader<'a> {
5050
Ok(Box::new(inverted_index))
5151
}
5252

53+
async fn all_inverted_index(&self) -> Result<Box<dyn InvertedIndexLookup + Send + Sync + '_>> {
54+
let inverted_index = self
55+
.snapshot
56+
.get_inverted_index(self.bucket.clone())
57+
.await?;
58+
Ok(Box::new(inverted_index))
59+
}
60+
61+
async fn label_values(&self, label_name: &str) -> Result<Vec<String>> {
62+
self.snapshot
63+
.get_label_values(self.bucket, label_name)
64+
.await
65+
}
66+
5367
async fn samples(
5468
&self,
5569
series_id: SeriesId,

open-tsdb/src/promql/tsdb_router.rs

Lines changed: 122 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -358,10 +358,14 @@ impl PromqlRouter for Tsdb {
358358
}
359359
};
360360

361-
// Get forward index - either filtered by matches or all series
362-
let forward_index = match &request.matches {
361+
// Collect label names using hybrid approach:
362+
// - Filtered (match[]): use forward index (targeted I/O for matching series)
363+
// - Unfiltered: use inverted index (direct access to all label keys)
364+
let mut label_names: HashSet<String> = HashSet::new();
365+
366+
match &request.matches {
363367
Some(matches) if !matches.is_empty() => {
364-
// Get matching series IDs first
368+
// Filtered: use forward index for targeted I/O
365369
let series_ids = match get_matching_series(&reader, matches).await {
366370
Ok(ids) => ids,
367371
Err(e) => {
@@ -375,7 +379,7 @@ impl PromqlRouter for Tsdb {
375379
}
376380
};
377381
let series_ids_vec: Vec<SeriesId> = series_ids.iter().copied().collect();
378-
match reader.forward_index(&series_ids_vec).await {
382+
let forward_index = match reader.forward_index(&series_ids_vec).await {
379383
Ok(index) => index,
380384
Err(e) => {
381385
let err = ErrorResponse::internal(e.to_string());
@@ -386,11 +390,16 @@ impl PromqlRouter for Tsdb {
386390
error_type: Some(err.error_type),
387391
};
388392
}
393+
};
394+
for (_id, spec) in forward_index.all_series() {
395+
for attr in &spec.attributes {
396+
label_names.insert(attr.key.clone());
397+
}
389398
}
390399
}
391400
_ => {
392-
// No match[] provided - get all series
393-
match reader.all_forward_index().await {
401+
// Unfiltered: use inverted index for direct key access
402+
let inverted_index = match reader.all_inverted_index().await {
394403
Ok(index) => index,
395404
Err(e) => {
396405
let err = ErrorResponse::internal(e.to_string());
@@ -401,18 +410,13 @@ impl PromqlRouter for Tsdb {
401410
error_type: Some(err.error_type),
402411
};
403412
}
413+
};
414+
for attr in inverted_index.all_keys() {
415+
label_names.insert(attr.key);
404416
}
405417
}
406418
};
407419

408-
// Collect all unique label names from all series
409-
let mut label_names: HashSet<String> = HashSet::new();
410-
for (_id, spec) in forward_index.all_series() {
411-
for attr in &spec.attributes {
412-
label_names.insert(attr.key.clone());
413-
}
414-
}
415-
416420
// Sort and apply limit
417421
let mut result: Vec<String> = label_names.into_iter().collect();
418422
result.sort();
@@ -447,10 +451,14 @@ impl PromqlRouter for Tsdb {
447451
}
448452
};
449453

450-
// Get forward index - either filtered by matches or all series
451-
let forward_index = match &request.matches {
454+
// Collect label values using hybrid approach:
455+
// - Filtered (match[]): use forward index (targeted I/O for matching series)
456+
// - Unfiltered: use inverted index (direct access to all label keys)
457+
let mut values: HashSet<String> = HashSet::new();
458+
459+
match &request.matches {
452460
Some(matches) if !matches.is_empty() => {
453-
// Get matching series IDs first
461+
// Filtered: use forward index for targeted I/O
454462
let series_ids = match get_matching_series(&reader, matches).await {
455463
Ok(ids) => ids,
456464
Err(e) => {
@@ -464,7 +472,7 @@ impl PromqlRouter for Tsdb {
464472
}
465473
};
466474
let series_ids_vec: Vec<SeriesId> = series_ids.iter().copied().collect();
467-
match reader.forward_index(&series_ids_vec).await {
475+
let forward_index = match reader.forward_index(&series_ids_vec).await {
468476
Ok(index) => index,
469477
Err(e) => {
470478
let err = ErrorResponse::internal(e.to_string());
@@ -475,12 +483,19 @@ impl PromqlRouter for Tsdb {
475483
error_type: Some(err.error_type),
476484
};
477485
}
486+
};
487+
for (_id, spec) in forward_index.all_series() {
488+
for attr in &spec.attributes {
489+
if attr.key == request.label_name {
490+
values.insert(attr.value.clone());
491+
}
492+
}
478493
}
479494
}
480495
_ => {
481-
// No match[] provided - get all series
482-
match reader.all_forward_index().await {
483-
Ok(index) => index,
496+
// Unfiltered: use optimized label_values that scans only keys for this label
497+
let label_values = match reader.label_values(&request.label_name).await {
498+
Ok(vals) => vals,
484499
Err(e) => {
485500
let err = ErrorResponse::internal(e.to_string());
486501
return LabelValuesResponse {
@@ -490,20 +505,11 @@ impl PromqlRouter for Tsdb {
490505
error_type: Some(err.error_type),
491506
};
492507
}
493-
}
508+
};
509+
values.extend(label_values);
494510
}
495511
};
496512

497-
// Collect all unique values for the specified label
498-
let mut values: HashSet<String> = HashSet::new();
499-
for (_id, spec) in forward_index.all_series() {
500-
for attr in &spec.attributes {
501-
if attr.key == request.label_name {
502-
values.insert(attr.value.clone());
503-
}
504-
}
505-
}
506-
507513
// Sort and apply limit
508514
let mut result: Vec<String> = values.into_iter().collect();
509515
result.sort();
@@ -894,4 +900,88 @@ mod tests {
894900
assert!(data.contains(&"prod".to_string()));
895901
assert!(data.contains(&"staging".to_string()));
896902
}
903+
904+
#[tokio::test]
905+
async fn should_filter_labels_by_match_correctly() {
906+
// given: two different metrics with different labels
907+
let storage = create_test_storage().await;
908+
let tsdb = Tsdb::new(storage);
909+
910+
let bucket = TimeBucket::hour(60);
911+
let mini = tsdb.get_or_create_for_ingest(bucket).await.unwrap();
912+
913+
// http_requests has env and method labels
914+
let samples = vec![
915+
create_sample(
916+
"http_requests",
917+
vec![("env", "prod"), ("method", "GET")],
918+
4_000_000,
919+
10.0,
920+
),
921+
// db_queries has env and table labels (different from http_requests)
922+
create_sample(
923+
"db_queries",
924+
vec![("env", "prod"), ("table", "users")],
925+
4_000_000,
926+
20.0,
927+
),
928+
];
929+
mini.ingest(samples).await.unwrap();
930+
tsdb.flush().await.unwrap();
931+
932+
// when: query labels with match[] filter for http_requests only
933+
let request = LabelsRequest {
934+
matches: Some(vec!["http_requests".to_string()]),
935+
start: Some(3600),
936+
end: Some(7200),
937+
limit: None,
938+
};
939+
let response = tsdb.labels(request).await;
940+
941+
// then: should only return labels from http_requests, not db_queries
942+
assert_eq!(response.status, "success");
943+
let data = response.data.unwrap();
944+
assert!(data.contains(&"__name__".to_string()));
945+
assert!(data.contains(&"env".to_string()));
946+
assert!(data.contains(&"method".to_string()));
947+
// table label should NOT be present since it belongs to db_queries
948+
assert!(!data.contains(&"table".to_string()));
949+
}
950+
951+
#[tokio::test]
952+
async fn should_filter_label_values_by_match_correctly() {
953+
// given: two different metrics with same label name but different values
954+
let storage = create_test_storage().await;
955+
let tsdb = Tsdb::new(storage);
956+
957+
let bucket = TimeBucket::hour(60);
958+
let mini = tsdb.get_or_create_for_ingest(bucket).await.unwrap();
959+
960+
let samples = vec![
961+
// http_requests with env=prod
962+
create_sample("http_requests", vec![("env", "prod")], 4_000_000, 10.0),
963+
// db_queries with env=staging (different metric, different env value)
964+
create_sample("db_queries", vec![("env", "staging")], 4_000_000, 20.0),
965+
];
966+
mini.ingest(samples).await.unwrap();
967+
tsdb.flush().await.unwrap();
968+
969+
// when: query label values for "env" with match[] filter for http_requests only
970+
let request = LabelValuesRequest {
971+
label_name: "env".to_string(),
972+
matches: Some(vec!["http_requests".to_string()]),
973+
start: Some(3600),
974+
end: Some(7200),
975+
limit: None,
976+
};
977+
let response = tsdb.label_values(request).await;
978+
979+
// then: should only return env values from http_requests, not db_queries
980+
assert_eq!(response.status, "success");
981+
let data = response.data.unwrap();
982+
assert_eq!(data.len(), 1);
983+
assert!(data.contains(&"prod".to_string()));
984+
// staging should NOT be present since it belongs to db_queries
985+
assert!(!data.contains(&"staging".to_string()));
986+
}
897987
}

open-tsdb/src/query.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,15 @@ pub(crate) trait QueryReader: Send + Sync {
2626
terms: &[Attribute],
2727
) -> Result<Box<dyn InvertedIndexLookup + Send + Sync + '_>>;
2828

29+
/// Get a view into all inverted index data.
30+
/// Used for labels/label_values queries to access all attribute keys.
31+
async fn all_inverted_index(&self) -> Result<Box<dyn InvertedIndexLookup + Send + Sync + '_>>;
32+
33+
/// Get all unique values for a specific label name.
34+
/// This is more efficient than loading all inverted index data when
35+
/// only values for a single label are needed.
36+
async fn label_values(&self, label_name: &str) -> Result<Vec<String>>;
37+
2938
/// Get samples for a series within a time range, merging from all layers.
3039
/// Returns samples sorted by timestamp with duplicates removed (head takes priority).
3140
async fn samples(&self, series_id: SeriesId, start_ms: u64, end_ms: u64)
@@ -70,6 +79,23 @@ pub(crate) mod test_utils {
7079
Ok(Box::new(self.inverted_index.clone()))
7180
}
7281

82+
async fn all_inverted_index(
83+
&self,
84+
) -> Result<Box<dyn InvertedIndexLookup + Send + Sync + '_>> {
85+
Ok(Box::new(self.inverted_index.clone()))
86+
}
87+
88+
async fn label_values(&self, label_name: &str) -> Result<Vec<String>> {
89+
let values: Vec<String> = self
90+
.inverted_index
91+
.postings
92+
.iter()
93+
.filter(|entry| entry.key().key == label_name)
94+
.map(|entry| entry.key().value.clone())
95+
.collect();
96+
Ok(values)
97+
}
98+
7399
async fn samples(
74100
&self,
75101
series_id: SeriesId,

0 commit comments

Comments
 (0)