Skip to content

Commit b2a98bc

Browse files
committed
Search result sanity
1 parent c92cc64 commit b2a98bc

1 file changed

Lines changed: 60 additions & 20 deletions

File tree

src/checker.rs

Lines changed: 60 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -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.
353360
pub 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: \npoint id: {:?}\nzeroed vector name: {}\nzeroed vector: {:?}\n\npoint: {:?}",
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

Comments
 (0)