feat(search): explicit filters on unified search; list camera bodies
The app's new search screen keeps tags, type, date, camera and place as chips and sends free text to the unified endpoint. For the AI to add to those chips instead of replacing them, the endpoint now takes them: - /photos/search/unified accepts the /photos filters explicitly (tag_ids with All/Any, exclude_tag_ids, media_type, dates, camera make/model, gps circle) plus a recursive `path`. Explicit tags are their own predicate on top of the model's ANY-mode tags; single-valued filters replace the model's guess. `interpreted` echoes only what the model added, so the client doesn't show the user's own chips twice. - interpret=false skips the LLM and uses q verbatim as the CLIP term, which replaces the app's separate "Semantic" mode with a visual match inside the user's filters. - /image/tags/all also returns `cameras` (make + model with counts) so the camera picker can offer e.g. "Nexus 6" directly. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
ed52a07d9d
commit
015ddd6c77
4 files changed
+511
-69
No files matched your search
@@ -388,6 +388,12 @@ pub trait ExifDao: Sync + Send {
|
||||
/// Get distinct camera makes with counts
|
||||
fn get_camera_makes(&mut self) -> Result<Vec<(String, i64)>, DbError>;
|
||||
|
||||
/// Distinct (make, model) pairs with counts, most-used first. Rows
|
||||
/// without a model are skipped — `get_camera_makes` covers those.
|
||||
fn get_cameras(&mut self) -> Result<Vec<(String, String, i64)>, DbError> {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
|
||||
/// Update file path in EXIF database
|
||||
fn update_file_path(&mut self, old_path: &str, new_path: &str) -> Result<(), DbError>;
|
||||
|
||||
@@ -1147,6 +1153,31 @@ impl ExifDao for SqliteExifDao {
|
||||
.map_err(|e| DbError::log(DbErrorKind::QueryError, e))
|
||||
}
|
||||
|
||||
fn get_cameras(&mut self) -> Result<Vec<(String, String, i64)>, DbError> {
|
||||
trace_db_call("query", "get_cameras", |_span| {
|
||||
use diesel::dsl::count;
|
||||
use schema::image_exif::dsl::*;
|
||||
|
||||
let mut connection = self.connection.lock().expect("Unable to get ExifDao");
|
||||
|
||||
image_exif
|
||||
.filter(camera_make.is_not_null())
|
||||
.filter(camera_model.is_not_null())
|
||||
.group_by((camera_make, camera_model))
|
||||
.select((camera_make, camera_model, count(id)))
|
||||
.order(count(id).desc())
|
||||
.load::<(Option<String>, Option<String>, i64)>(connection.deref_mut())
|
||||
.map(|records| {
|
||||
records
|
||||
.into_iter()
|
||||
.filter_map(|(make, model, cnt)| Some((make?, model?, cnt)))
|
||||
.collect()
|
||||
})
|
||||
.map_err(|e| anyhow::anyhow!("Query error: {}", e))
|
||||
})
|
||||
.map_err(|e| DbError::log(DbErrorKind::QueryError, e))
|
||||
}
|
||||
|
||||
fn update_file_path(&mut self, old_path: &str, new_path: &str) -> Result<(), DbError> {
|
||||
trace_db_call("update", "update_file_path", |_span| {
|
||||
use schema::image_exif::dsl::*;
|
||||
@@ -2584,6 +2615,56 @@ mod exif_dao_tests {
|
||||
assert_eq!(lib2[0].library_id, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_cameras_groups_make_and_model_most_used_first() {
|
||||
let mut dao = setup_two_libraries();
|
||||
let mut camera = |rel: &str, make: Option<&str>, model: Option<&str>| {
|
||||
dao.store_exif(InsertImageExif {
|
||||
library_id: 1,
|
||||
file_path: rel.to_string(),
|
||||
camera_make: make.map(str::to_string),
|
||||
camera_model: model.map(str::to_string),
|
||||
lens_model: None,
|
||||
width: None,
|
||||
height: None,
|
||||
orientation: None,
|
||||
gps_latitude: None,
|
||||
gps_longitude: None,
|
||||
gps_altitude: None,
|
||||
focal_length: None,
|
||||
aperture: None,
|
||||
shutter_speed: None,
|
||||
iso: None,
|
||||
date_taken: None,
|
||||
created_time: 0,
|
||||
last_modified: 0,
|
||||
content_hash: None,
|
||||
size_bytes: None,
|
||||
phash_64: None,
|
||||
dhash_64: None,
|
||||
date_taken_source: None,
|
||||
date_utc: None,
|
||||
camera_offset_minutes: None,
|
||||
date_taken_tz_source: None,
|
||||
})
|
||||
.expect("insert exif row");
|
||||
};
|
||||
camera("a.jpg", Some("motorola"), Some("Nexus 6"));
|
||||
camera("b.jpg", Some("Google"), Some("Pixel 7"));
|
||||
camera("c.jpg", Some("Google"), Some("Pixel 7"));
|
||||
// No model: covered by get_camera_makes, skipped here.
|
||||
camera("d.jpg", Some("Canon"), None);
|
||||
camera("e.jpg", None, None);
|
||||
|
||||
assert_eq!(
|
||||
dao.get_cameras().unwrap(),
|
||||
vec![
|
||||
("Google".to_string(), "Pixel 7".to_string(), 2),
|
||||
("motorola".to_string(), "Nexus 6".to_string(), 1),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/// Row with the full date triple, for the true-instant filter tests.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn insert_dated_row(
|
||||
|
||||
Reference in new issue
Block a user