From 6e5898e7663e5810afb0c8e16ec6b975dd81adf8 Mon Sep 17 00:00:00 2001 From: Cameron Cordes Date: Sun, 14 Jun 2026 02:20:06 -0400 Subject: [PATCH] Unified search: rank within filtered set instead of pre-thresholding CLIP MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When structured filters are present they're the constraint and CLIP only ranks within the candidate set, so drop the global similarity threshold for that case. Previously the 0.2 whole-library threshold ran BEFORE intersecting with the filters, discarding filter-matching photos that scored just under it (e.g. a 2022 beach photo at 0.18) — producing after_struct_filter=0 even when matches existed. Plain semantic (no filters) keeps the user's threshold. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/unified_search.rs | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) 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 {