perf: batch label stats query + fix scoped thumb
This commit is contained in:
@@ -57,29 +57,52 @@ func handleLabels(pp *ppClient, ppDb *gorm.DB) gin.HandlerFunc {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// For each label, count how many photos under this user's BasePath
|
// One query: count + a representative scoped thumb for every label
|
||||||
// have that label. Remove labels with zero count for this user.
|
// the user can see. Replaces N per-label queries with a single JOIN.
|
||||||
filtered := make([]PpLabel, 0, len(labels))
|
|
||||||
prefix := basePath + "/%"
|
prefix := basePath + "/%"
|
||||||
|
|
||||||
|
type labelStat struct {
|
||||||
|
LabelUID string `gorm:"column:label_uid"`
|
||||||
|
Cnt int64 `gorm:"column:cnt"`
|
||||||
|
ThumbHash string `gorm:"column:thumb_hash"`
|
||||||
|
}
|
||||||
|
var stats []labelStat
|
||||||
|
if err := ppDb.Raw(`
|
||||||
|
SELECT lb.label_uid AS label_uid,
|
||||||
|
COUNT(DISTINCT p.id) AS cnt,
|
||||||
|
COALESCE(MIN(f.file_hash), '') AS thumb_hash
|
||||||
|
FROM photos_labels pl
|
||||||
|
JOIN photos p ON pl.photo_id = p.id
|
||||||
|
JOIN labels lb ON pl.label_id = lb.id
|
||||||
|
LEFT JOIN files f ON f.photo_uid = p.photo_uid
|
||||||
|
AND f.file_primary = 1
|
||||||
|
AND f.file_missing = 0
|
||||||
|
WHERE (p.photo_path = ? OR p.photo_path LIKE ?)
|
||||||
|
AND p.deleted_at IS NULL
|
||||||
|
GROUP BY lb.label_uid
|
||||||
|
HAVING cnt > 0
|
||||||
|
`, basePath, prefix).Scan(&stats).Error; err != nil {
|
||||||
|
c.JSON(http.StatusBadGateway, gin.H{"error": "label stats query failed"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cntMap := make(map[string]int64, len(stats))
|
||||||
|
thumbMap := make(map[string]string, len(stats))
|
||||||
|
for _, s := range stats {
|
||||||
|
cntMap[s.LabelUID] = s.Cnt
|
||||||
|
thumbMap[s.LabelUID] = s.ThumbHash
|
||||||
|
}
|
||||||
|
|
||||||
|
filtered := make([]PpLabel, 0, len(stats))
|
||||||
for _, l := range labels {
|
for _, l := range labels {
|
||||||
var cnt int64
|
cnt, ok := cntMap[l.UID]
|
||||||
if err := ppDb.Raw(
|
if !ok || cnt == 0 {
|
||||||
`SELECT COUNT(*) FROM photos_labels pl
|
|
||||||
JOIN photos p ON pl.photo_id = p.id
|
|
||||||
JOIN labels lb ON pl.label_id = lb.id
|
|
||||||
WHERE lb.label_uid = ?
|
|
||||||
AND (p.photo_path = ? OR p.photo_path LIKE ?)
|
|
||||||
AND p.deleted_at IS NULL`,
|
|
||||||
l.UID, basePath, prefix,
|
|
||||||
).Count(&cnt).Error; err != nil {
|
|
||||||
// On DB error, skip this label rather than failing the whole response.
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if cnt == 0 {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
l.PhotoCount = int(cnt)
|
l.PhotoCount = int(cnt)
|
||||||
|
if th := thumbMap[l.UID]; th != "" {
|
||||||
|
l.Thumb = th
|
||||||
|
}
|
||||||
filtered = append(filtered, l)
|
filtered = append(filtered, l)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -713,7 +713,9 @@ export async function listLabels(): Promise<PpLabel[]> {
|
|||||||
const { data } = await sidecar.get<PpLabel[]>('/api/sidecar/labels', {
|
const { data } = await sidecar.get<PpLabel[]>('/api/sidecar/labels', {
|
||||||
params: { count: 1000, order: 'count', all: true, perPage: 1000 }
|
params: { count: 1000, order: 'count', all: true, perPage: 1000 }
|
||||||
});
|
});
|
||||||
return filterByUserPhotos(data, (l) => `label:${l.Slug}`);
|
// Sidecar already filters to the user's scope and sets correct counts +
|
||||||
|
// thumbs in one DB query — no need to probe each label individually.
|
||||||
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Subjects (people / face recognition) ────────────────────────────────────
|
// ── Subjects (people / face recognition) ────────────────────────────────────
|
||||||
|
|||||||
Reference in New Issue
Block a user