Match synced faces from an index of their boxes, not from their rows
`merge::match_faces` reads every local face's box and model to pair the other device's faces with ours. It took 54 ms of a steady-state merge on the reference library (19,000 faces). A `faces` row is eight kilobytes -- the embedding, the crop, the dense landmarks -- and `model_id` sits past the embedding, so reading it opened each row's overflow pages: SCAN f SEARCH r USING INTEGER PRIMARY KEY (rowid=?) `faces_box (image_id, model_id, x, y, w, h)` holds every column the scan asks for: SCAN f USING COVERING INDEX faces_box SEARCH r USING INTEGER PRIMARY KEY (rowid=?) The local scan went from 38 ms to 8 ms (sqlite3 on a copy, aggregated so output formatting is not timed), and `match_faces` from 54 ms to 30-37 ms; what remains is the other device's half. That is read from its snapshot, which has whatever indexes its build made -- this one will carry `faces_box` in its uploads -- and whose rows have had their crops stripped. The bench merges a full copy with crops, so it overstates that half. Created on first use in `match_faces`, with CREATE INDEX IF NOT EXISTS, rather than by a migration, for the reason `keywords::ensure_term_index` gives: a schema version bump makes older builds refuse the snapshot, and an extra index is invisible to them. The first merge after the upgrade builds it (about a second, once). Its prefix duplicates `faces_image_model`, which is left alone; the planner takes either for an (image_id, model_id) probe. Tables checksum the same after the bench run as after the old build's.
This commit is contained in:
@@ -1081,6 +1081,8 @@ fn match_faces(tx: &Connection) -> Result<std::collections::HashMap<i64, i64>, C
|
||||
|
||||
type Boxed = (i64, f32, f32, f32, f32);
|
||||
|
||||
ensure_face_box_index(tx);
|
||||
|
||||
// Local faces, grouped by the photograph's cross-device id.
|
||||
let mut local: std::collections::HashMap<(i64, String), Vec<Boxed>> =
|
||||
std::collections::HashMap::new();
|
||||
@@ -1153,6 +1155,30 @@ fn match_faces(tx: &Connection) -> Result<std::collections::HashMap<i64, i64>, C
|
||||
Ok(map)
|
||||
}
|
||||
|
||||
/// The index the local half of [`match_faces`] is read from: every column
|
||||
/// it asks of a face, so the walk touches no `faces` row.
|
||||
///
|
||||
/// A face row is eight kilobytes and `model_id` sits past the embedding, so
|
||||
/// reading it opened the row's overflow pages: 38 ms of every sync pass on
|
||||
/// the reference library, to read 19,000 boxes. From this index, 8 ms. The
|
||||
/// other device's half has no such index to use -- it is a snapshot made by
|
||||
/// whatever build that device runs -- but its rows have had their crops
|
||||
/// stripped, which is most of their width.
|
||||
///
|
||||
/// Created on first use rather than by a migration, like
|
||||
/// `keywords::ensure_term_index` and for the reason given there: a new
|
||||
/// schema version makes older builds refuse this catalog's snapshot, and an
|
||||
/// extra index is invisible to them. The first merge after an upgrade pays
|
||||
/// for building it, once. A failure is logged and the merge goes on reading
|
||||
/// rows, as it did before.
|
||||
fn ensure_face_box_index(tx: &Connection) {
|
||||
if let Err(e) = tx.execute_batch(
|
||||
"CREATE INDEX IF NOT EXISTS main.faces_box ON faces(image_id, model_id, x, y, w, h);",
|
||||
) {
|
||||
log::warn!("merge: could not create faces_box: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Intersection over union of two `(x, y, w, h)` boxes.
|
||||
fn iou(a: (f32, f32, f32, f32), b: (f32, f32, f32, f32)) -> f32 {
|
||||
let x0 = a.0.max(b.0);
|
||||
|
||||
Reference in New Issue
Block a user