clip-search: accept library_ids (multi-select whitelist) on /photos/search
Previously the endpoint only accepted `library=<id>` (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) <noreply@anthropic.com>
This commit is contained in:
@@ -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<i32>,
|
||||
/// 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<String>,
|
||||
/// 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<i32> = 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<i32> = if let Some(raw) = query.library_ids.as_deref() {
|
||||
let mut out: Vec<i32> = Vec::new();
|
||||
for piece in raw.split(',') {
|
||||
let trimmed = piece.trim();
|
||||
if trimmed.is_empty() {
|
||||
continue;
|
||||
}
|
||||
match trimmed.parse::<i32>() {
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user