From 86e38e152d4cd32fe43761a55a7ea2e877af4471 Mon Sep 17 00:00:00 2001 From: dtoro Date: Sun, 7 Jun 2026 00:33:23 +0200 Subject: [PATCH] perf: batch label stats query + fix scoped thumb --- sidecar/handlers_labels.go | 57 +++++++++++++++++++++--------- web/src/lib/services/photoprism.ts | 4 ++- 2 files changed, 43 insertions(+), 18 deletions(-) diff --git a/sidecar/handlers_labels.go b/sidecar/handlers_labels.go index f01b1cd..a3f22d6 100644 --- a/sidecar/handlers_labels.go +++ b/sidecar/handlers_labels.go @@ -57,29 +57,52 @@ func handleLabels(pp *ppClient, ppDb *gorm.DB) gin.HandlerFunc { return } - // For each label, count how many photos under this user's BasePath - // have that label. Remove labels with zero count for this user. - filtered := make([]PpLabel, 0, len(labels)) + // One query: count + a representative scoped thumb for every label + // the user can see. Replaces N per-label queries with a single JOIN. 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 { - var cnt int64 - if err := ppDb.Raw( - `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 { + cnt, ok := cntMap[l.UID] + if !ok || cnt == 0 { continue } l.PhotoCount = int(cnt) + if th := thumbMap[l.UID]; th != "" { + l.Thumb = th + } filtered = append(filtered, l) } diff --git a/web/src/lib/services/photoprism.ts b/web/src/lib/services/photoprism.ts index dca1d84..664b4f4 100644 --- a/web/src/lib/services/photoprism.ts +++ b/web/src/lib/services/photoprism.ts @@ -713,7 +713,9 @@ export async function listLabels(): Promise { const { data } = await sidecar.get('/api/sidecar/labels', { 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) ────────────────────────────────────