diff --git a/src/unified_search.rs b/src/unified_search.rs index 7bb78dd..d80feec 100644 --- a/src/unified_search.rs +++ b/src/unified_search.rs @@ -231,6 +231,22 @@ pub async fn unified_search( 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 = Vec::new(); @@ -255,6 +271,11 @@ pub async fn unified_search( } } }; + 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( .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( 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( } 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( } 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 = if offset >= total_matching { Vec::new()