diff --git a/src-tauri/src/db.rs b/src-tauri/src/db.rs index 83d9ac2..572212d 100644 --- a/src-tauri/src/db.rs +++ b/src-tauri/src/db.rs @@ -263,6 +263,13 @@ pub fn migrate(conn: &Connection) -> Result<()> { groups_json TEXT NOT NULL ); + CREATE TABLE IF NOT EXISTS app_kv ( + key TEXT PRIMARY KEY, + value INTEGER NOT NULL DEFAULT 0 + ); + + INSERT OR IGNORE INTO app_kv (key, value) VALUES ('embedding_revision', 0); + CREATE INDEX IF NOT EXISTS idx_images_folder_id ON images(folder_id); CREATE INDEX IF NOT EXISTS idx_images_modified_at ON images(modified_at); CREATE INDEX IF NOT EXISTS idx_embedding_jobs_status ON embedding_jobs(status); @@ -915,6 +922,13 @@ pub fn mark_embedding_ready(conn: &Connection, image_id: i64, model: &str) -> Re params![image_id, model], )?; conn.execute("DELETE FROM embedding_jobs WHERE image_id = ?1", [image_id])?; + // Advance the monotonic revision so the HNSW cache is always rebuilt after + // any embedding change, regardless of clock resolution. + conn.execute( + "INSERT INTO app_kv (key, value) VALUES ('embedding_revision', 1) + ON CONFLICT(key) DO UPDATE SET value = value + 1", + [], + )?; Ok(()) } diff --git a/src-tauri/src/indexer.rs b/src-tauri/src/indexer.rs index a5d062d..25b4c94 100644 --- a/src-tauri/src/indexer.rs +++ b/src-tauri/src/indexer.rs @@ -373,6 +373,11 @@ fn do_index(app: AppHandle, pool: &DbPool, folder_id: i64, folder_path: PathBuf) let _ = db::backfill_embedding_jobs(&conn)?; db::update_folder_count(&conn, folder_id)?; let _ = db::clear_folder_scan_error(&conn, folder_id); + // Invalidate duplicate scan cache — any reindex can change file contents + // or the set of files, making cached duplicate groups stale. + let folder_scope = format!("folder:{}", folder_id); + let _ = db::clear_duplicate_scan_cache(&conn, &folder_scope); + let _ = db::clear_duplicate_scan_cache(&conn, "all"); } emit_progress( diff --git a/src-tauri/src/vector.rs b/src-tauri/src/vector.rs index 644ece8..25a3220 100644 --- a/src-tauri/src/vector.rs +++ b/src-tauri/src/vector.rs @@ -215,13 +215,17 @@ pub fn get_image_embedding(conn: &Connection, image_id: i64) -> Result Result { - let count: i64 = conn.query_row("SELECT COUNT(*) FROM image_vec", [], |row| row.get(0))?; - let max_updated_at: Option = conn.query_row( - "SELECT MAX(embedding_updated_at) FROM images WHERE embedding_status = 'ready'", - [], - |row| row.get(0), - )?; - Ok(format!("{}:{}", count, max_updated_at.unwrap_or_default())) + // Use the monotonically incremented app_kv counter so that two embeddings + // saved within the same clock second still advance the revision, preventing + // the HNSW cache from serving stale vectors. + let revision: i64 = conn + .query_row( + "SELECT COALESCE((SELECT value FROM app_kv WHERE key = 'embedding_revision'), 0)", + [], + |row| row.get(0), + ) + .unwrap_or(0); + Ok(revision.to_string()) } // fn image_ids_for_folder(