Unified search: stage-by-stage logging to debug empty results
Log the translated query (semantic/tags/place/date/media + has_struct), the tag-filter file count, candidate-row + allowed-hash counts, and the CLIP considered/hits/after-filter counts. Pinpoints which stage drops results to zero (over-extracted filter, tag path mismatch, Any/All over-constraint, or CLIP threshold). info-level for now while debugging. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -231,6 +231,22 @@ pub async fn unified_search<TagD: TagDao>(
|
||||
let has_struct =
|
||||
has_exif_filter || gps.is_some() || !sq.tag_ids.is_empty() || sq.media_type.is_some();
|
||||
|
||||
// Stage trace: what the model extracted + whether a structured filter is
|
||||
// active. The chips show this to the user too, but logging it makes the
|
||||
// "why no results" path debuggable from the server side.
|
||||
log::info!(
|
||||
"unified_search: q={nl:?} semantic={:?} tag_ids={:?} exclude={:?} place={:?} gps={:?} date=({:?},{:?}) media={:?} unmatched={:?} has_struct={has_struct}",
|
||||
sq.semantic,
|
||||
sq.tag_ids,
|
||||
sq.exclude_tag_ids,
|
||||
resolved_place.as_ref().map(|p| p.display_name.as_str()),
|
||||
gps,
|
||||
sq.date_from,
|
||||
sq.date_to,
|
||||
sq.media_type,
|
||||
sq.unmatched_tags,
|
||||
);
|
||||
|
||||
// ── 3. Build the structured candidate set (EXIF rows passing every
|
||||
// filter). Skipped entirely for a pure-semantic query. ──
|
||||
let mut candidate: Vec<crate::database::models::ImageExif> = Vec::new();
|
||||
@@ -255,6 +271,11 @@ pub async fn unified_search<TagD: TagDao>(
|
||||
}
|
||||
}
|
||||
};
|
||||
log::info!(
|
||||
"unified_search: tag_ids={:?} -> tag_set_files={:?}",
|
||||
sq.tag_ids,
|
||||
tag_set.as_ref().map(|s| s.len())
|
||||
);
|
||||
|
||||
// EXIF query handles camera/lens/gps-box/date. With no EXIF filters
|
||||
// it returns the whole table, which we then narrow by the predicates
|
||||
@@ -322,6 +343,11 @@ pub async fn unified_search<TagD: TagDao>(
|
||||
.iter()
|
||||
.filter_map(|r| r.content_hash.clone())
|
||||
.collect();
|
||||
log::info!(
|
||||
"unified_search: candidate_rows={} allowed_hashes={}",
|
||||
candidate.len(),
|
||||
allowed_hashes.len()
|
||||
);
|
||||
}
|
||||
|
||||
// ── 4. Rank ──
|
||||
@@ -334,6 +360,8 @@ pub async fn unified_search<TagD: TagDao>(
|
||||
Ok(s) => s,
|
||||
Err(e) => return score_error_response(e),
|
||||
};
|
||||
let considered = scored.considered;
|
||||
let clip_hits = scored.hits.len();
|
||||
let hits: Vec<(f32, String)> = if has_struct {
|
||||
scored
|
||||
.hits
|
||||
@@ -343,6 +371,10 @@ pub async fn unified_search<TagD: TagDao>(
|
||||
} else {
|
||||
scored.hits
|
||||
};
|
||||
log::info!(
|
||||
"unified_search: clip considered={considered} hits={clip_hits} after_struct_filter={}",
|
||||
hits.len()
|
||||
);
|
||||
let total_matching = hits.len();
|
||||
let page = paginate(&hits, offset, limit);
|
||||
let results = resolve_hits(&exif_dao, &page);
|
||||
@@ -364,6 +396,7 @@ pub async fn unified_search<TagD: TagDao>(
|
||||
}
|
||||
candidate.sort_by(|a, b| b.date_taken.cmp(&a.date_taken));
|
||||
let total_matching = candidate.len();
|
||||
log::info!("unified_search: filters-only matches={total_matching}");
|
||||
let end = (offset + limit).min(total_matching);
|
||||
let results: Vec<SearchHit> = if offset >= total_matching {
|
||||
Vec::new()
|
||||
|
||||
Reference in New Issue
Block a user