feat: add region-based similarity search

Adds a "Search within image" button to the lightbox that lets the user
draw a crop region on an image and find visually similar results using
that crop's embedding. The crop is embedded in-memory without a temp
file via a new embed_image_crop method on ClipImageEmbedder.

Introduces a folder-scoped cosine search (search_image_ids_by_embedding_in_folder)
to support the current-folder scope option, and wires up the new
find_similar_by_region Tauri command end-to-end from Rust through to
the Zustand store and Lightbox UI.
This commit is contained in:
2026-06-02 19:39:31 +01:00
parent ff4a568b57
commit df17497808
6 changed files with 477 additions and 24 deletions
+73
View File
@@ -59,6 +59,19 @@ pub struct FindSimilarImagesParams {
pub threshold: Option<f32>,
}
#[derive(Deserialize)]
pub struct FindSimilarByRegionParams {
pub image_id: i64,
/// Normalized crop rect (0.01.0).
pub crop_x: f32,
pub crop_y: f32,
pub crop_w: f32,
pub crop_h: f32,
pub folder_id: Option<i64>,
pub offset: Option<usize>,
pub limit: Option<usize>,
}
#[derive(Deserialize)]
pub struct DebugSimilarImagesParams {
pub image_id: i64,
@@ -354,6 +367,66 @@ pub async fn find_similar_images(
})
}
#[tauri::command]
pub async fn find_similar_by_region(
db: State<'_, DbState>,
params: FindSimilarByRegionParams,
) -> Result<SimilarImagesPage, String> {
let conn = db.get().map_err(|e| e.to_string())?;
let limit = params.limit.unwrap_or(32);
let offset = params.offset.unwrap_or(0);
// Look up the source image path
let image = db::get_image_by_id(&conn, params.image_id).map_err(|e| e.to_string())?;
let image_path = std::path::Path::new(&image.path);
// Embed the cropped region in-memory (no temp file needed)
let embedder = embedder::ClipImageEmbedder::new().map_err(|e| e.to_string())?;
let embedding = embedder
.embed_image_crop(
image_path,
params.crop_x,
params.crop_y,
params.crop_w,
params.crop_h,
)
.map_err(|e| e.to_string())?;
// Search for similar images using the crop embedding
let image_ids = match params.folder_id {
Some(folder_id) => vector::search_image_ids_by_embedding_in_folder(
&conn,
&embedding,
folder_id,
Some(params.image_id),
offset + limit + 1,
)
.map_err(|e| e.to_string())?,
None => {
let mut ids = vector::search_image_ids_by_embedding(&conn, &embedding, offset + limit + 1)
.map_err(|e| e.to_string())?;
// Exclude the source image from global results
ids.retain(|&id| id != params.image_id);
ids
}
};
let has_more = image_ids.len() > offset + limit;
let page_ids = image_ids
.into_iter()
.skip(offset)
.take(limit)
.collect::<Vec<_>>();
let images = db::get_images_by_ids(&conn, &page_ids).map_err(|e| e.to_string())?;
Ok(SimilarImagesPage {
images,
offset,
limit,
has_more,
})
}
#[derive(Serialize)]
pub struct SimilarImagesDebug {
pub image_id: i64,