Filter the people rail in the query, and count the unassigned faces
`faces::people` grouped `face_person` after a LEFT JOIN over every person and sorted the lot by name; the rail then discarded the empty, unnamed groups a regrouping pass leaves behind — 17,000 of 19,000 rows on the reference library. `people_in_use` filters them in the WHERE and joins `people` to face counts aggregated first (2,000 groups), so the sort sees only the rows that will be drawn. `count_unassigned` replaces fetching 2,400 ids to take their length. `load_people` 22 ms → 10 ms.
This commit is contained in:
@@ -838,6 +838,22 @@ pub fn unassigned(conn: &Connection, model_id: &str) -> Result<Vec<FaceId>, Cata
|
|||||||
rows.collect::<Result<_, _>>().map_err(Into::into)
|
rows.collect::<Result<_, _>>().map_err(Into::into)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// How many faces have no identity yet — [`unassigned`] counted rather than
|
||||||
|
/// listed, for a screen that only shows the number.
|
||||||
|
pub fn count_unassigned(conn: &Connection, model_id: &str) -> Result<u64, CatalogError> {
|
||||||
|
let n: i64 = conn.query_row(
|
||||||
|
&format!(
|
||||||
|
"SELECT COUNT(*) FROM faces f
|
||||||
|
LEFT JOIN face_person fp ON fp.face_id = f.id
|
||||||
|
WHERE fp.face_id IS NULL AND {} = ?1",
|
||||||
|
embedder_sql("f.model_id")
|
||||||
|
),
|
||||||
|
[embedder_of(model_id)],
|
||||||
|
|r| r.get(0),
|
||||||
|
)?;
|
||||||
|
Ok(n as u64)
|
||||||
|
}
|
||||||
|
|
||||||
/// One face's stored embedding, as the clustering pass consumes it.
|
/// One face's stored embedding, as the clustering pass consumes it.
|
||||||
///
|
///
|
||||||
/// A struct rather than a tuple because it crosses a crate boundary and "the
|
/// A struct rather than a tuple because it crosses a crate boundary and "the
|
||||||
@@ -910,17 +926,46 @@ pub fn rename_person(conn: &Connection, person: PersonId, name: &str) -> Result<
|
|||||||
/// Merged-away people are excluded: they exist as redirects so a sync does not
|
/// Merged-away people are excluded: they exist as redirects so a sync does not
|
||||||
/// resurrect them, not as entries in a list.
|
/// resurrect them, not as entries in a list.
|
||||||
pub fn people(conn: &Connection) -> Result<Vec<Person>, CatalogError> {
|
pub fn people(conn: &Connection) -> Result<Vec<Person>, CatalogError> {
|
||||||
let mut q = conn.prepare(
|
people_where(conn, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Everyone who holds a face, carries a name, or was set aside — the people a
|
||||||
|
/// screen has a row for.
|
||||||
|
///
|
||||||
|
/// The rest are the empty, unnamed groups a regrouping pass leaves behind
|
||||||
|
/// (`prune_empty_unnamed`), and on the reference library they were 17,000 of
|
||||||
|
/// 19,000 rows: read, counted, sorted by name and then thrown away by the
|
||||||
|
/// caller on every redraw. Filtered here, in the query, they are never
|
||||||
|
/// sorted. The filter is SQL's `trim`, which strips spaces and not every
|
||||||
|
/// whitespace character, so a name that is only a tab is listed rather than
|
||||||
|
/// hidden — the safe direction for a row the user typed something into.
|
||||||
|
pub fn people_in_use(conn: &Connection) -> Result<Vec<Person>, CatalogError> {
|
||||||
|
people_where(conn, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [`people`], with face counts aggregated once per person *before* the join
|
||||||
|
/// rather than grouped after it: the face table is joined to the 2,000
|
||||||
|
/// people it names, not the 19,000 rows of the people table.
|
||||||
|
fn people_where(conn: &Connection, in_use: bool) -> Result<Vec<Person>, CatalogError> {
|
||||||
|
let filter = if in_use {
|
||||||
|
"AND (c.person_id IS NOT NULL OR trim(p.name) <> '' OR p.ignored)"
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
};
|
||||||
|
let mut q = conn.prepare(&format!(
|
||||||
"SELECT p.id, p.uuid, p.name,
|
"SELECT p.id, p.uuid, p.name,
|
||||||
COALESCE(SUM(fp.confirmed = 1), 0),
|
COALESCE(c.confirmed, 0),
|
||||||
COALESCE(SUM(fp.confirmed = 0), 0),
|
COALESCE(c.suggested, 0),
|
||||||
p.ignored
|
p.ignored
|
||||||
FROM people p
|
FROM people p
|
||||||
LEFT JOIN face_person fp ON fp.person_id = p.id
|
LEFT JOIN (SELECT person_id,
|
||||||
WHERE p.merged_into IS NULL
|
SUM(confirmed = 1) AS confirmed,
|
||||||
GROUP BY p.id
|
SUM(confirmed = 0) AS suggested
|
||||||
ORDER BY 4 DESC, 5 DESC, p.name",
|
FROM face_person
|
||||||
)?;
|
GROUP BY person_id) c ON c.person_id = p.id
|
||||||
|
WHERE p.merged_into IS NULL {filter}
|
||||||
|
ORDER BY 4 DESC, 5 DESC, p.name"
|
||||||
|
))?;
|
||||||
let rows = q.query_map([], |r| {
|
let rows = q.query_map([], |r| {
|
||||||
Ok(Person {
|
Ok(Person {
|
||||||
id: PersonId(r.get::<_, i64>(0)? as u64),
|
id: PersonId(r.get::<_, i64>(0)? as u64),
|
||||||
|
|||||||
@@ -216,7 +216,9 @@ pub struct IdentityView {
|
|||||||
/// Filtered rather than deleted, because this is a screen being drawn and not a
|
/// Filtered rather than deleted, because this is a screen being drawn and not a
|
||||||
/// catalog being repaired. A row is withheld; nothing is lost, a sync cannot
|
/// catalog being repaired. A row is withheld; nothing is lost, a sync cannot
|
||||||
/// resurrect what was never removed, and the prune stays the one place that
|
/// resurrect what was never removed, and the prune stays the one place that
|
||||||
/// decides these are disposable.
|
/// decides these are disposable. The filter is the catalog's
|
||||||
|
/// (`faces::people_in_use`), in the query, so the rows withheld are never
|
||||||
|
/// read or sorted either.
|
||||||
///
|
///
|
||||||
/// An empty group with a *name* still shows. That one is not debris, it is the
|
/// An empty group with a *name* still shows. That one is not debris, it is the
|
||||||
/// symptom of a real failure — a named person whose faces were regrouped out
|
/// symptom of a real failure — a named person whose faces were regrouped out
|
||||||
@@ -228,11 +230,8 @@ pub fn load_people(
|
|||||||
model_id: &str,
|
model_id: &str,
|
||||||
) -> Result<IdentityView, dr_catalog::CatalogError> {
|
) -> Result<IdentityView, dr_catalog::CatalogError> {
|
||||||
let conn = catalog.connection();
|
let conn = catalog.connection();
|
||||||
let people = faces::people(conn)?
|
let people = faces::people_in_use(conn)?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(|p| {
|
|
||||||
p.confirmed_faces + p.suggested_faces > 0 || !p.name.trim().is_empty() || p.ignored
|
|
||||||
})
|
|
||||||
.map(|p| PersonRow {
|
.map(|p| PersonRow {
|
||||||
id: p.id,
|
id: p.id,
|
||||||
name: p.name,
|
name: p.name,
|
||||||
@@ -247,7 +246,7 @@ pub fn load_people(
|
|||||||
people,
|
people,
|
||||||
selected: None,
|
selected: None,
|
||||||
faces: Vec::new(),
|
faces: Vec::new(),
|
||||||
unassigned: faces::unassigned(conn, model_id)?.len(),
|
unassigned: faces::count_unassigned(conn, model_id)? as usize,
|
||||||
calibrated: faces::calibration(conn, model_id)?.is_some_and(|(c, _)| c.valid),
|
calibrated: faces::calibration(conn, model_id)?.is_some_and(|(c, _)| c.valid),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user