@@ -25,6 +25,11 @@ use weaver_semconv::stability::Stability;
2525//TODO: Consider using a fuzzy matching crate for improved search capabilities.
2626// e.g. Tantivy - https://github.com/open-telemetry/weaver/pull/1076#discussion_r2640681775
2727
28+ /// Maximum number of results a single `search` call returns. Requested limits
29+ /// above this are clamped. Kept in sync with the documented maximum of the
30+ /// `/api/v1/registry/search` endpoint (`SearchParams::limit`).
31+ pub const MAX_SEARCH_LIMIT : usize = 1000 ;
32+
2833/// Search context for performing fuzzy searches and O(1) lookups across the registry.
2934pub struct SearchContext {
3035 /// All searchable items for fuzzy search.
@@ -150,6 +155,7 @@ impl SearchContext {
150155 /// * `query` - Optional search query string (None = browse mode).
151156 /// * `search_type` - Filter by item type.
152157 /// * `stability` - Optional stability filter.
158+ /// * `hide_deprecated` - When true, excludes deprecated items regardless of stability.
153159 /// * `limit` - Maximum number of results.
154160 /// * `offset` - Pagination offset.
155161 ///
@@ -162,10 +168,11 @@ impl SearchContext {
162168 query : Option < & str > ,
163169 search_type : SearchType ,
164170 stability : Option < Stability > ,
171+ hide_deprecated : bool ,
165172 limit : usize ,
166173 offset : usize ,
167174 ) -> ( Vec < SearchResult > , usize ) {
168- let limit = limit. min ( 200 ) ; // Cap at 200
175+ let limit = limit. min ( MAX_SEARCH_LIMIT ) ;
169176
170177 // Filter by type
171178 let mut items: Vec < & SearchableItem > = self
@@ -179,6 +186,12 @@ impl SearchContext {
179186 items. retain ( |item| item. stability ( ) == & stability_filter) ;
180187 }
181188
189+ // `deprecated` is independent of `stability` - a deprecated item can carry any
190+ // stability level - so this is a separate retain rather than folded into the above.
191+ if hide_deprecated {
192+ items. retain ( |item| !item. is_deprecated ( ) ) ;
193+ }
194+
182195 // Branch based on whether we have a search query
183196 let ( results, total) = if let Some ( q) = query {
184197 if q. is_empty ( ) {
@@ -188,7 +201,7 @@ impl SearchContext {
188201 ( results, total)
189202 } else {
190203 // Non-empty query - search mode with scoring
191- search_mode_with_total ( items, q, limit, & self . separator )
204+ search_mode_with_total ( items, q, limit, offset , & self . separator )
192205 }
193206 } else {
194207 // No query - browse mode
@@ -331,6 +344,7 @@ fn search_mode_with_total(
331344 items : Vec < & SearchableItem > ,
332345 query : & str ,
333346 limit : usize ,
347+ offset : usize ,
334348 separator : & str ,
335349) -> ( Vec < SearchResult > , usize ) {
336350 let mut scored_items: Vec < ( u32 , & SearchableItem ) > = items
@@ -348,12 +362,13 @@ fn search_mode_with_total(
348362 // Sort by score descending
349363 scored_items. sort_by_key ( |b| std:: cmp:: Reverse ( b. 0 ) ) ;
350364
351- // Calculate total before taking limit
365+ // Calculate total before paginating
352366 let total = scored_items. len ( ) ;
353367
354- // Take top N and convert to results
368+ // Page through the ranked matches and convert to results
355369 let results = scored_items
356370 . into_iter ( )
371+ . skip ( offset)
357372 . take ( limit)
358373 . map ( |( score, item) | item. to_search_result ( score) )
359374 . collect ( ) ;
@@ -823,7 +838,7 @@ mod tests {
823838 let registry = make_test_registry ( ) ;
824839 let ctx = SearchContext :: from_registry ( & registry) ;
825840
826- let ( results, total) = ctx. search ( Some ( "http" ) , SearchType :: All , None , 10 , 0 ) ;
841+ let ( results, total) = ctx. search ( Some ( "http" ) , SearchType :: All , None , false , 10 , 0 ) ;
827842
828843 // Should find http.request.method, http.response.status_code,
829844 // http.server.request.duration, http.client
@@ -837,7 +852,7 @@ mod tests {
837852 let ctx = SearchContext :: from_registry ( & registry) ;
838853
839854 // None query = browse mode
840- let ( results, total) = ctx. search ( None , SearchType :: All , None , 100 , 0 ) ;
855+ let ( results, total) = ctx. search ( None , SearchType :: All , None , false , 100 , 0 ) ;
841856
842857 // Should return all items: 5 attributes + 1 metric + 1 span + 1 event + 1 entity = 9
843858 assert_eq ! ( total, 9 ) ;
@@ -850,25 +865,25 @@ mod tests {
850865 let ctx = SearchContext :: from_registry ( & registry) ;
851866
852867 // Filter by Attribute only
853- let ( results, total) = ctx. search ( None , SearchType :: Attribute , None , 100 , 0 ) ;
868+ let ( results, total) = ctx. search ( None , SearchType :: Attribute , None , false , 100 , 0 ) ;
854869 assert_eq ! ( total, 5 ) ; // 5 attributes (3 regular + 1 template + 1 development)
855870 assert_eq ! ( results. len( ) , 5 ) ;
856871
857872 // Filter by Metric only
858- let ( results, total) = ctx. search ( None , SearchType :: Metric , None , 100 , 0 ) ;
873+ let ( results, total) = ctx. search ( None , SearchType :: Metric , None , false , 100 , 0 ) ;
859874 assert_eq ! ( total, 1 ) ;
860875 assert_eq ! ( results. len( ) , 1 ) ;
861876
862877 // Filter by Span only
863- let ( _, total) = ctx. search ( None , SearchType :: Span , None , 100 , 0 ) ;
878+ let ( _, total) = ctx. search ( None , SearchType :: Span , None , false , 100 , 0 ) ;
864879 assert_eq ! ( total, 1 ) ;
865880
866881 // Filter by Event only
867- let ( _, total) = ctx. search ( None , SearchType :: Event , None , 100 , 0 ) ;
882+ let ( _, total) = ctx. search ( None , SearchType :: Event , None , false , 100 , 0 ) ;
868883 assert_eq ! ( total, 1 ) ;
869884
870885 // Filter by Entity only
871- let ( _, total) = ctx. search ( None , SearchType :: Entity , None , 100 , 0 ) ;
886+ let ( _, total) = ctx. search ( None , SearchType :: Entity , None , false , 100 , 0 ) ;
872887 assert_eq ! ( total, 1 ) ;
873888 }
874889
@@ -878,39 +893,70 @@ mod tests {
878893 let ctx = SearchContext :: from_registry ( & registry) ;
879894
880895 // Get first 2 items
881- let ( results1, total1) = ctx. search ( None , SearchType :: All , None , 2 , 0 ) ;
896+ let ( results1, total1) = ctx. search ( None , SearchType :: All , None , false , 2 , 0 ) ;
882897 assert_eq ! ( total1, 9 ) ;
883898 assert_eq ! ( results1. len( ) , 2 ) ;
884899
885900 // Get next 2 items with offset
886- let ( results2, total2) = ctx. search ( None , SearchType :: All , None , 2 , 2 ) ;
901+ let ( results2, total2) = ctx. search ( None , SearchType :: All , None , false , 2 , 2 ) ;
887902 assert_eq ! ( total2, 9 ) ;
888903 assert_eq ! ( results2. len( ) , 2 ) ;
889904
890905 // Get remaining items
891- let ( results3, _) = ctx. search ( None , SearchType :: All , None , 100 , 4 ) ;
906+ let ( results3, _) = ctx. search ( None , SearchType :: All , None , false , 100 , 4 ) ;
892907 assert_eq ! ( results3. len( ) , 5 ) ;
893908 }
894909
895910 #[ test]
896- fn test_search_limit_capped_at_200 ( ) {
911+ fn test_search_limit_capped_at_max ( ) {
897912 let registry = make_test_registry ( ) ;
898913 let ctx = SearchContext :: from_registry ( & registry) ;
899914
900- // Request limit > 200 should be capped
901- let ( results, _) = ctx. search ( None , SearchType :: All , None , 500 , 0 ) ;
915+ // Request limit > MAX_SEARCH_LIMIT should be capped
916+ let ( results, _) = ctx. search ( None , SearchType :: All , None , false , MAX_SEARCH_LIMIT + 1 , 0 ) ;
902917
903918 // We only have 9 items, so we get 9 (not testing the cap directly,
904919 // but ensuring it doesn't crash with large limit)
905920 assert_eq ! ( results. len( ) , 9 ) ;
906921 }
907922
923+ #[ test]
924+ fn test_search_query_mode_pagination ( ) {
925+ let registry = make_test_registry ( ) ;
926+ let ctx = SearchContext :: from_registry ( & registry) ;
927+
928+ // Collect all matches in one call, then re-fetch them one page at a time.
929+ let ( all_results, total) = ctx. search ( Some ( "http" ) , SearchType :: All , None , false , 100 , 0 ) ;
930+ assert_eq ! ( all_results. len( ) , total) ;
931+ assert ! ( total >= 4 ) ;
932+
933+ let ( page1, _) = ctx. search ( Some ( "http" ) , SearchType :: All , None , false , 2 , 0 ) ;
934+ let ( page2, _) = ctx. search ( Some ( "http" ) , SearchType :: All , None , false , 2 , 2 ) ;
935+ assert_eq ! ( page1. len( ) , 2 ) ;
936+
937+ // Pages must continue the ranked list, not repeat the top results.
938+ let paged: Vec < _ > = page1. iter ( ) . chain ( page2. iter ( ) ) . collect ( ) ;
939+ for ( paged_item, full_item) in paged. iter ( ) . zip ( all_results. iter ( ) ) {
940+ assert_eq ! (
941+ serde_json:: to_value( paged_item) . unwrap( ) ,
942+ serde_json:: to_value( full_item) . unwrap( )
943+ ) ;
944+ }
945+
946+ // Offset past the end returns an empty page but the same total.
947+ let ( past_end, past_end_total) =
948+ ctx. search ( Some ( "http" ) , SearchType :: All , None , false , 10 , total) ;
949+ assert ! ( past_end. is_empty( ) ) ;
950+ assert_eq ! ( past_end_total, total) ;
951+ }
952+
908953 #[ test]
909954 fn test_search_no_results ( ) {
910955 let registry = make_test_registry ( ) ;
911956 let ctx = SearchContext :: from_registry ( & registry) ;
912957
913- let ( results, total) = ctx. search ( Some ( "zzzznonexistent" ) , SearchType :: All , None , 10 , 0 ) ;
958+ let ( results, total) =
959+ ctx. search ( Some ( "zzzznonexistent" ) , SearchType :: All , None , false , 10 , 0 ) ;
914960
915961 assert_eq ! ( total, 0 ) ;
916962 assert ! ( results. is_empty( ) ) ;
@@ -980,8 +1026,14 @@ mod tests {
9801026 let ctx = SearchContext :: from_registry ( & registry) ;
9811027
9821028 // Filter by Stable only
983- let ( results, total) =
984- ctx. search ( None , SearchType :: Attribute , Some ( Stability :: Stable ) , 100 , 0 ) ;
1029+ let ( results, total) = ctx. search (
1030+ None ,
1031+ SearchType :: Attribute ,
1032+ Some ( Stability :: Stable ) ,
1033+ false ,
1034+ 100 ,
1035+ 0 ,
1036+ ) ;
9851037
9861038 // Should return only stable attributes (4: http.request.method, http.response.status_code, db.system, test.template)
9871039 assert_eq ! ( total, 4 ) ;
@@ -998,6 +1050,7 @@ mod tests {
9981050 None ,
9991051 SearchType :: Attribute ,
10001052 Some ( Stability :: Development ) ,
1053+ false ,
10011054 100 ,
10021055 0 ,
10031056 ) ;
@@ -1020,6 +1073,64 @@ mod tests {
10201073 assert_eq ! ( dev_item. stability( ) , & Stability :: Development ) ;
10211074 }
10221075
1076+ // =========================================================================
1077+ // Deprecated Filtering Tests
1078+ // =========================================================================
1079+
1080+ fn make_registry_with_attributes ( attributes : Vec < Attribute > ) -> ForgeResolvedRegistry {
1081+ ForgeResolvedRegistry {
1082+ schema_url : "https://example.com/schemas/1.2.3" . try_into ( ) . unwrap ( ) ,
1083+ registry : Registry {
1084+ attributes,
1085+ attribute_groups : vec ! [ ] ,
1086+ metrics : vec ! [ ] ,
1087+ spans : vec ! [ ] ,
1088+ events : vec ! [ ] ,
1089+ entities : vec ! [ ] ,
1090+ } ,
1091+ refinements : Refinements {
1092+ metrics : vec ! [ ] ,
1093+ spans : vec ! [ ] ,
1094+ events : vec ! [ ] ,
1095+ } ,
1096+ }
1097+ }
1098+
1099+ #[ test]
1100+ fn test_search_hide_deprecated ( ) {
1101+ let registry = make_registry_with_attributes ( vec ! [
1102+ make_attribute( "service.name" , "Service name" , "" , false ) ,
1103+ make_attribute( "service.namespace" , "Deprecated namespace" , "" , true ) ,
1104+ ] ) ;
1105+ let ctx = SearchContext :: from_registry ( & registry) ;
1106+
1107+ let ( results, total) = ctx. search ( None , SearchType :: All , None , false , 100 , 0 ) ;
1108+ assert_eq ! ( total, 2 ) ;
1109+ assert_eq ! ( results. len( ) , 2 ) ;
1110+
1111+ let ( results, total) = ctx. search ( None , SearchType :: All , None , true , 100 , 0 ) ;
1112+ assert_eq ! ( total, 1 ) ;
1113+ assert_eq ! ( results. len( ) , 1 ) ;
1114+ }
1115+
1116+ #[ test]
1117+ fn test_search_hide_deprecated_independent_of_stability ( ) {
1118+ // `make_attribute` always sets stability: Stable regardless of the
1119+ // deprecated flag - a deprecated item can carry any stability level,
1120+ // so `hide_deprecated` must filter independently of the stability
1121+ // filter rather than only affecting items with `stability: deprecated`.
1122+ let registry = make_registry_with_attributes ( vec ! [ make_attribute(
1123+ "service.name" ,
1124+ "Deprecated but stable" ,
1125+ "" ,
1126+ true ,
1127+ ) ] ) ;
1128+ let ctx = SearchContext :: from_registry ( & registry) ;
1129+
1130+ let ( _, total) = ctx. search ( None , SearchType :: All , Some ( Stability :: Stable ) , true , 100 , 0 ) ;
1131+ assert_eq ! ( total, 0 ) ;
1132+ }
1133+
10231134 // =========================================================================
10241135 // Namespace Browsing Tests
10251136 // =========================================================================
0 commit comments