diff --git a/src/unified_search.rs b/src/unified_search.rs index bb6344c..9eea405 100644 --- a/src/unified_search.rs +++ b/src/unified_search.rs @@ -364,13 +364,27 @@ pub async fn unified_search( // ── 4. Rank ── match semantic { Some(ref sem) => { - // Semantic term present: CLIP-rank, then keep only hits that pass - // the structured filters (by content_hash). - let scored = - match score_photos(&state, &exif_dao, sem, &library_ids, threshold, None).await { - Ok(s) => s, - Err(e) => return score_error_response(e), - }; + // When structured filters are present they ARE the constraint — + // CLIP only ranks within the candidate set. So drop the global + // similarity threshold (it's tuned for whole-library search and + // would pre-discard filter-matching photos that scored just under + // it — e.g. a 2022 beach photo at 0.18 — before the intersection + // ever runs). With no filters, keep the user's threshold for the + // plain semantic case. + let clip_threshold = if has_struct { -1.0 } else { threshold }; + let scored = match score_photos( + &state, + &exif_dao, + sem, + &library_ids, + clip_threshold, + None, + ) + .await + { + 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 {