@@ -350,28 +350,68 @@ pub async fn check_optimizer_status(
350350 Ok ( ( ) )
351351}
352352
353+ /// Sanity-check a batched query response.
354+ ///
355+ /// Per scored point: score must be finite, returned vectors must not be all-zero.
356+ /// Per per-query result: scores must be monotonic (either non-increasing or non-decreasing) —
357+ /// the direction is not asserted because the workload mixes Cosine/Dot (higher = better)
358+ /// and Euclid/Manhattan (lower = better) named vectors. Both directions are valid; what's
359+ /// invalid is a zig-zag, which would indicate the result list isn't sorted at all.
353360pub fn check_search_result ( results : & QueryBatchResponse ) -> Result < ( ) , CrasherError > {
354- // assert no vector is only containing zeros
355- for point in results. result . iter ( ) . flat_map ( |result| & result. result ) {
356- if let Some ( vectors) = point
357- . vectors
358- . as_ref ( )
359- . and_then ( |v| v. vectors_options . as_ref ( ) )
360- {
361- let zeroed_vector = match vectors {
362- VectorsOptions :: Vector ( v) => check_zeroed_vector ( v) . then_some ( ( String :: new ( ) , v) ) ,
363- VectorsOptions :: Vectors ( vectors) => vectors
364- . vectors
365- . iter ( )
366- . find_map ( |( name, v) | check_zeroed_vector ( v) . then_some ( ( name. clone ( ) , v) ) ) ,
367- } ;
368- if let Some ( ( name, vector) ) = zeroed_vector {
369- return Err ( Invariant ( format ! (
370- "Query result contains zeroed vector: \n point id: {:?}\n zeroed vector name: {}\n zeroed vector: {:?}\n \n point: {:?}" ,
371- point. id, name, vector, point
372- ) ) ) ;
361+ let mut errors: Vec < String > = Vec :: new ( ) ;
362+
363+ for ( query_idx, batch_result) in results. result . iter ( ) . enumerate ( ) {
364+ for ( rank, point) in batch_result. result . iter ( ) . enumerate ( ) {
365+ // finite score
366+ if !point. score . is_finite ( ) {
367+ errors. push ( format ! (
368+ "query #{query_idx} rank #{rank}: non-finite score {} for point {:?}" ,
369+ point. score, point. id,
370+ ) ) ;
371+ }
372+ // zeroed vectors
373+ if let Some ( vectors) = point
374+ . vectors
375+ . as_ref ( )
376+ . and_then ( |v| v. vectors_options . as_ref ( ) )
377+ {
378+ let zeroed_vector = match vectors {
379+ VectorsOptions :: Vector ( v) => {
380+ check_zeroed_vector ( v) . then_some ( ( String :: new ( ) , v) )
381+ }
382+ VectorsOptions :: Vectors ( vectors) => vectors
383+ . vectors
384+ . iter ( )
385+ . find_map ( |( name, v) | check_zeroed_vector ( v) . then_some ( ( name. clone ( ) , v) ) ) ,
386+ } ;
387+ if let Some ( ( name, vector) ) = zeroed_vector {
388+ errors. push ( format ! (
389+ "query #{query_idx} rank #{rank}: zeroed vector '{name}' on point {:?}: {vector:?}" ,
390+ point. id,
391+ ) ) ;
392+ }
393+ }
394+ }
395+
396+ // monotonicity (only when every score is finite — otherwise comparison is meaningless)
397+ let scores: Vec < f32 > = batch_result. result . iter ( ) . map ( |p| p. score ) . collect ( ) ;
398+ if scores. len ( ) >= 2 && scores. iter ( ) . all ( |s| s. is_finite ( ) ) {
399+ let non_increasing = scores. windows ( 2 ) . all ( |w| w[ 0 ] >= w[ 1 ] ) ;
400+ let non_decreasing = scores. windows ( 2 ) . all ( |w| w[ 0 ] <= w[ 1 ] ) ;
401+ if !non_increasing && !non_decreasing {
402+ errors. push ( format ! (
403+ "query #{query_idx}: scores not monotonic: {scores:?}"
404+ ) ) ;
373405 }
374406 }
375407 }
376- Ok ( ( ) )
408+
409+ if errors. is_empty ( ) {
410+ Ok ( ( ) )
411+ } else {
412+ Err ( Invariant ( format ! (
413+ "Search result violations:\n {}" ,
414+ errors. join( "\n " ) ,
415+ ) ) )
416+ }
377417}
0 commit comments