Unified NL search Phase 2: /photos/search/unified endpoint

Composes the two existing engines (Path A orchestration):
- Translate NL -> StructuredQuery via local LLM, respecting LLM_BACKEND
  (resolve_backend(Local) -> ollama or llama-swap; no hybrid).
- Forward-geocode the place name into a gps circle.
- Structured filters (tags/EXIF/geo/date/media) build a candidate set of EXIF
  rows; CLIP ranks within it, joined by content_hash. Degenerate cases match
  existing behavior: semantic-only -> plain CLIP; filters-only -> date-sorted.
- Echoes the interpreted query (incl. resolved place) for editable client chips.

Refactor: extracted reusable cores from clip_search (score_photos, resolve_hits,
parse_library_scope, score_error_response) shared by both endpoints. Removed the
Phase 1 allow-until-wired attributes now that nl_query + geo are consumed.

fmt + clippy clean; 23 backend tests pass (7 geo, 12 nl_query, 4 unified).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Cameron CordesandClaude Opus 4.8 authored and Cameron committed 2026-06-29 12:39:16 -04:00
1 parent 09ccb347c7
commit fd00dc936a
6 files changed
+675 -198

No files matched your search

-6
View File
@@ -21,12 +21,6 @@
//! `geo::forward_geocode`), and person filtering is deferred until a
//! person→photos resolver exists.
// Phase 1: this module is fully implemented and unit-tested, but its first
// consumer (the `/photos/search/unified` endpoint) lands in Phase 2. Mirrors
// llm_client.rs's allow-until-wired pattern so the bin target stays
// clippy-clean in the interim; remove when the endpoint is added.
#![allow(dead_code)]
use crate::ai::llm_client::{ChatMessage, LlmClient, Tool, strip_think_blocks};
use anyhow::{Result, anyhow};
use serde::{Deserialize, Serialize};