From 87093a63d7dea54d66c1376fafe1a2862e95e592 Mon Sep 17 00:00:00 2001 From: Cameron Cordes Date: Sat, 16 May 2026 09:30:46 -0400 Subject: [PATCH] clip-search: accept library_ids (multi-select whitelist) on /photos/search MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously the endpoint only accepted `library=` (single id) — multi- select scopes had to be filtered upstream by Apollo, which kept the filter logic out of FileViewer-React's reach (it calls ImageApi directly and got no scoping for 2+ active libraries). Adds `library_ids` (comma-separated id list, e.g. `?library_ids=1,3`). Parsed inside the existing scope decision: `library_ids` wins when both are supplied; either / both empty falls back to "every enabled library" (historical default). Malformed entries return 400. Dedupes ids while preserving order so a stray `library_ids=1,1,3` doesn't double-pass to the DAO. The single-id path still works unchanged for older clients. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/clip_search.rs | 45 ++++++++++++++++++++++++++++++++++++++------- 1 file changed, 38 insertions(+), 7 deletions(-) diff --git a/src/clip_search.rs b/src/clip_search.rs index 5e8d5b9..d91e490 100644 --- a/src/clip_search.rs +++ b/src/clip_search.rs @@ -48,10 +48,17 @@ pub struct SearchQuery { /// CLIP; tunable per call when sweeping. Defaults to 0.20. #[serde(default = "default_threshold")] pub threshold: f32, - /// Optional single-library scope. When omitted, every enabled - /// library is searched. Multi-select isn't supported yet — the - /// frontend wires through one or all. + /// Optional single-library scope. Legacy param — new clients pass + /// `library_ids` instead so multi-select scopes (Apollo's HUD library + /// chips, FileViewer-React's library picker) actually filter. Kept + /// for back-compat; `library_ids` wins when both are supplied. pub library: Option, + /// Optional multi-library scope, comma-separated id list + /// (`?library_ids=1,3`). Empty / omitted = every enabled library + /// (the historical default). Apollo and FileViewer-React both send + /// this when 2+ libraries are selected; the single-library case + /// works through either param interchangeably. + pub library_ids: Option, /// Optional model-version filter. Defaults to the live engine's /// version (queried lazily). Forces a strict join so mid-flight /// model swaps can't mix geometries in a single response. @@ -172,10 +179,34 @@ pub async fn search_photos( } }; - // 2. Decide which library scope to search. - let library_ids: Vec = match query.library { - Some(id) => vec![id], - None => Vec::new(), // empty = all libraries + // 2. Decide which library scope to search. `library_ids` (multi) + // wins over the legacy `library` (single) when both are present; + // either / both empty falls back to "every enabled library". + let library_ids: Vec = if let Some(raw) = query.library_ids.as_deref() { + let mut out: Vec = Vec::new(); + for piece in raw.split(',') { + let trimmed = piece.trim(); + if trimmed.is_empty() { + continue; + } + match trimmed.parse::() { + Ok(id) => { + if !out.contains(&id) { + out.push(id); + } + } + Err(_) => { + return Ok(HttpResponse::BadRequest().json(SearchError { + error: format!("invalid library_ids entry: {trimmed:?}"), + })); + } + } + } + out + } else if let Some(id) = query.library { + vec![id] + } else { + Vec::new() }; // 3. Pull the (hash, embedding) matrix. Lock contention here is