@@ -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}
0 commit comments