feature/clip-semantic-search #96

Merged
cameron merged 5 commits from feature/clip-semantic-search into master 2026-05-16 00:32:33 +00:00
6 changed files with 34 additions and 26 deletions
Showing only changes of commit 66267cc345 - Show all commits

View File

@@ -181,10 +181,7 @@ impl ClipClient {
/// Encode a natural-language query to an embedding. Used by the
/// search route to rank stored image embeddings by cosine sim.
pub async fn encode_text(
&self,
text: &str,
) -> std::result::Result<EncodeResponse, ClipError> {
pub async fn encode_text(&self, text: &str) -> std::result::Result<EncodeResponse, ClipError> {
let Some(base) = self.base_url.as_deref() else {
return Err(ClipError::Disabled);
};
@@ -206,9 +203,10 @@ impl ClipClient {
};
let status = resp.status();
if status.is_success() {
let body: EncodeResponse = resp.json().await.map_err(|e| {
ClipError::Transient(anyhow::anyhow!("clip response decode: {e}"))
})?;
let body: EncodeResponse = resp
.json()
.await
.map_err(|e| ClipError::Transient(anyhow::anyhow!("clip response decode: {e}")))?;
return Ok(body);
}
let body_text = resp.text().await.unwrap_or_default();
@@ -246,9 +244,10 @@ impl ClipClient {
};
let status = resp.status();
if status.is_success() {
let body: EncodeResponse = resp.json().await.map_err(|e| {
ClipError::Transient(anyhow::anyhow!("clip response decode: {e}"))
})?;
let body: EncodeResponse = resp
.json()
.await
.map_err(|e| ClipError::Transient(anyhow::anyhow!("clip response decode: {e}")))?;
return Ok(body);
}
let body_text = resp.text().await.unwrap_or_default();

View File

@@ -273,10 +273,12 @@ pub fn process_clip_backlog(
let candidates: Vec<crate::clip_watch::ClipCandidate> = rows
.into_iter()
.map(|(rel_path, content_hash)| crate::clip_watch::ClipCandidate {
rel_path,
content_hash,
})
.map(
|(rel_path, content_hash)| crate::clip_watch::ClipCandidate {
rel_path,
content_hash,
},
)
.collect();
crate::clip_watch::run_clip_encoding_pass(

View File

@@ -122,7 +122,10 @@ async fn main() -> anyhow::Result<()> {
.into_iter()
.find(|l| l.id == args.library)
.ok_or_else(|| anyhow::anyhow!("library id {} not found", args.library))?;
info!("probing library #{} ({}) at {}", lib.id, lib.name, lib.root_path);
info!(
"probing library #{} ({}) at {}",
lib.id, lib.name, lib.root_path
);
let dao: Arc<Mutex<Box<dyn ExifDao>>> = Arc::new(Mutex::new(Box::new(SqliteExifDao::new())));
let ctx = opentelemetry::Context::new();
@@ -136,9 +139,7 @@ async fn main() -> anyhow::Result<()> {
let query_vec = decode_f32_vec(&query_resp.embedding)?;
info!(
"query encoded ({}d, {}ms): {:?}",
query_resp.embedding_dim,
query_resp.duration_ms,
args.query
query_resp.embedding_dim, query_resp.duration_ms, args.query
);
// Page through (id, rel_path), filter to images on disk, encode up
@@ -245,7 +246,11 @@ async fn main() -> anyhow::Result<()> {
scores.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(std::cmp::Ordering::Equal));
let elapsed = started.elapsed();
println!();
println!("── top {} for query: {:?} ──", args.top.min(scores.len()), args.query);
println!(
"── top {} for query: {:?} ──",
args.top.min(scores.len()),
args.query
);
for (i, (sim, path)) in scores.iter().take(args.top).enumerate() {
println!("[{:>2}] sim={:.3} {}", i + 1, sim, path);
}

View File

@@ -121,7 +121,7 @@ pub async fn search_photos(
}));
}
let limit = query.limit.min(200).max(1);
let limit = query.limit.clamp(1, 200);
let threshold = query.threshold.clamp(-1.0, 1.0);
// 1. Encode the query text. Fast — Apollo's text encoder is ~50ms
@@ -174,7 +174,10 @@ pub async fn search_photos(
match dao.list_clip_index(
&ctx,
&library_ids,
query.model_version.as_deref().or(Some(&query_resp.model_version)),
query
.model_version
.as_deref()
.or(Some(&query_resp.model_version)),
) {
Ok(r) => r,
Err(e) => {
@@ -257,7 +260,8 @@ pub async fn search_photos(
Err(e) => {
log::warn!(
"clip_search: find_by_content_hash failed for {}: {:?}",
hash, e
hash,
e
);
continue;
}

View File

@@ -142,10 +142,7 @@ async fn process_one(
let emb_bytes = match resp.decode_embedding() {
Ok(b) => b,
Err(e) => {
warn!(
"clip_watch: bad embedding for {}: {:?}",
cand.rel_path, e
);
warn!("clip_watch: bad embedding for {}: {:?}", cand.rel_path, e);
return;
}
};

View File

@@ -450,6 +450,7 @@ impl AppState {
insight_chat,
preview_dao,
FaceClient::new(None), // disabled in test
ClipClient::new(None), // disabled in test
)
}
}