feat(search): explicit filters on unified search; list camera bodies #171

Merged
cameron merged 1 commits from feat/search-explicit-filters into master 2026-10-16 01:13:45 +00:00
Owner

Server side of the new single search screen in FileViewer-React. The app 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 accepts them.

Changes

  • /photos/search/unified takes explicit filters, using the same encodings as /photos:

    • tag_ids with tag_filter_mode (All/Any, default All), exclude_tag_ids
    • media_type, date_from/date_to
    • camera_make/camera_model
    • gps_lat/gps_lon/gps_radius_km
    • path (folder, recursive)

    How they combine with the model's interpretation:

    • Explicit tags are their own predicate, applied on top of the model's ANY-mode tags.
    • Single-valued filters replace the model's guess. An explicit date bound replaces the model's whole range.
    • An explicit GPS circle skips geocoding the model's place.
    • 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. This 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 picker can offer e.g. "Nexus 6" directly. The field is additive; camera_makes is unchanged.

  • The translation step moved into a translate() helper. Behaviour with no explicit filters is unchanged.

  • CLAUDE.md API notes updated.

Testing

  • New unit tests cover the filter merge: overrides, the echo, tag predicates, All default, media_type=all, bad GPS/ids, folder prefix. There's also a DAO test for get_cameras.
  • cargo test: 737 lib + 830 bin + integration tests pass. clippy and fmt are clean.
  • The tests need DATABASE_URL set; there's no .env in this checkout, so I pointed it at a scratch DB. On one parallel run, 4 owner_defaults tests failed when they collided on that shared file; they pass on rerun and on master.
  • Not exercised against a live LLM/CLIP setup.

Pairs with the FileViewer-React search screen PR. Merge this first: an older server ignores the new params, so chips wouldn't apply to AI searches.

🤖 Generated with Claude Code

Server side of the new single search screen in FileViewer-React. The app 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 accepts them. ## Changes - **`/photos/search/unified` takes explicit filters**, using the same encodings as `/photos`: - `tag_ids` with `tag_filter_mode` (All/Any, default All), `exclude_tag_ids` - `media_type`, `date_from`/`date_to` - `camera_make`/`camera_model` - `gps_lat`/`gps_lon`/`gps_radius_km` - `path` (folder, recursive) How they combine with the model's interpretation: - Explicit tags are their own predicate, applied on top of the model's ANY-mode tags. - Single-valued filters replace the model's guess. An explicit date bound replaces the model's whole range. - An explicit GPS circle skips geocoding the model's place. - `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. This 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 picker can offer e.g. "Nexus 6" directly. The field is additive; `camera_makes` is unchanged. - The translation step moved into a `translate()` helper. Behaviour with no explicit filters is unchanged. - CLAUDE.md API notes updated. ## Testing - New unit tests cover the filter merge: overrides, the echo, tag predicates, All default, `media_type=all`, bad GPS/ids, folder prefix. There's also a DAO test for `get_cameras`. - `cargo test`: 737 lib + 830 bin + integration tests pass. clippy and fmt are clean. - The tests need `DATABASE_URL` set; there's no `.env` in this checkout, so I pointed it at a scratch DB. On one parallel run, 4 `owner_defaults` tests failed when they collided on that shared file; they pass on rerun and on `master`. - Not exercised against a live LLM/CLIP setup. Pairs with the FileViewer-React search screen PR. **Merge this first**: an older server ignores the new params, so chips wouldn't apply to AI searches. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
cameron added 1 commit 2026-10-16 01:11:07 +00:00
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>
cameron merged commit 50dd9aa016 into master 2026-10-16 01:13:45 +00:00
cameron deleted branch feat/search-explicit-filters 2026-10-16 01:13:45 +00:00
Sign in to join this conversation.