@@ -141,7 +141,7 @@ impl<T: VectorElement> VectorBackend<T> {
141141}
142142
143143/// How the probe loop stopped.
144- #[ derive( Clone , Copy , Debug , PartialEq , Eq , Default ) ]
144+ #[ derive( Clone , Copy , Debug , PartialEq , Eq , Default , serde :: Serialize ) ]
145145pub enum ProbeTermination {
146146 /// The filter-effective probe budget reached `max_probe_count` — the
147147 /// probe ceiling.
@@ -158,7 +158,7 @@ pub enum ProbeTermination {
158158/// touched. Returned by [`VectorBackend::top_n`] alongside the hits.
159159/// The flat/exact path fills only `exact_rows_read`; every other field
160160/// is IVF-probe-only.
161- #[ derive( Debug , Default ) ]
161+ #[ derive( Debug , Default , serde :: Serialize ) ]
162162pub struct ProbeStats {
163163 /// Clusters visited by the probe loop, in probe order. A cluster
164164 /// appears here once we've passed the stop-condition gate for it,
@@ -625,8 +625,9 @@ mod tests {
625625 use crate :: schema:: { IndexRecordOption , Schema , Term , STORED , STRING } ;
626626 use crate :: vector:: tests:: { exhaustive_params, TestVectorIndex } ;
627627 use crate :: vector:: {
628- IvfCentroids , IvfClusterer , IvfMatrix , IvfMergeSettings , IvfVectors , VectorClusterStats ,
629- VectorDType , VectorInfo , VectorOptions , VectorStorageFormat ,
628+ IvfCentroids , IvfClusterer , IvfMatrix , IvfMergeSettings , IvfVectors ,
629+ NeighborhoodGraphSearchMetrics , SearchTerminationReason , VectorClusterStats , VectorDType ,
630+ VectorInfo , VectorOptions , VectorStorageFormat ,
630631 } ;
631632 use crate :: { Index , IndexWriter , TantivyDocument } ;
632633
@@ -2341,6 +2342,72 @@ mod tests {
23412342 Ok ( ( ) )
23422343 }
23432344
2345+ /// `ProbeStats` (and nested routing / optional graph metrics) round-trip
2346+ /// through `serde_json` with the field names callers rely on.
2347+ #[ test]
2348+ fn probe_stats_serializes_to_json ( ) {
2349+ let stats = ProbeStats {
2350+ probed_clusters : vec ! [ 2 , 5 ] ,
2351+ candidates_scored : 10 ,
2352+ vectors_visited : 20 ,
2353+ pruned_filter : 4 ,
2354+ pruned_dead : 3 ,
2355+ pruned_seen : 3 ,
2356+ postings_row : 1 ,
2357+ postings_skipped : 1 ,
2358+ exact_rows_read : 0 ,
2359+ routing : IvfSearchMetrics {
2360+ visited_count : 7 ,
2361+ graph : Some ( NeighborhoodGraphSearchMetrics {
2362+ visited_count : 7 ,
2363+ expanded_count : 4 ,
2364+ edges_scanned : 12 ,
2365+ evictions : 1 ,
2366+ result_count : 3 ,
2367+ termination_reason : SearchTerminationReason :: SearchConverged ,
2368+ } ) ,
2369+ } ,
2370+ min_candidates : 5 ,
2371+ termination : ProbeTermination :: Gate ,
2372+ } ;
2373+
2374+ let value = serde_json:: to_value ( & stats) . expect ( "ProbeStats should serialize to JSON" ) ;
2375+ assert_eq ! (
2376+ value,
2377+ serde_json:: json!( {
2378+ "probed_clusters" : [ 2 , 5 ] ,
2379+ "candidates_scored" : 10 ,
2380+ "vectors_visited" : 20 ,
2381+ "pruned_filter" : 4 ,
2382+ "pruned_dead" : 3 ,
2383+ "pruned_seen" : 3 ,
2384+ "postings_row" : 1 ,
2385+ "postings_skipped" : 1 ,
2386+ "exact_rows_read" : 0 ,
2387+ "routing" : {
2388+ "visited_count" : 7 ,
2389+ "graph" : {
2390+ "visited_count" : 7 ,
2391+ "expanded_count" : 4 ,
2392+ "edges_scanned" : 12 ,
2393+ "evictions" : 1 ,
2394+ "result_count" : 3 ,
2395+ "termination_reason" : "SearchConverged"
2396+ }
2397+ } ,
2398+ "min_candidates" : 5 ,
2399+ "termination" : "Gate"
2400+ } )
2401+ ) ;
2402+
2403+ // Exact routing leaves `graph` unset — still must serialize as null.
2404+ let mut exact_routing = stats;
2405+ exact_routing. routing . graph = None ;
2406+ let exact_value =
2407+ serde_json:: to_value ( & exact_routing) . expect ( "ProbeStats should serialize to JSON" ) ;
2408+ assert_eq ! ( exact_value[ "routing" ] [ "graph" ] , serde_json:: Value :: Null ) ;
2409+ }
2410+
23442411 // ============================================================
23452412 // Filter-aware posting fetches.
23462413 //
0 commit comments