Optimize sorting gallery by Likes/Comments via caching

This commit is contained in:
Noah Petherbridge 2024-09-21 17:25:36 -07:00
parent 0cd72a96ed
commit 955ace1e91
8 changed files with 92 additions and 55 deletions

View File

@ -261,6 +261,21 @@ func main() {
return err return err
} }
return nil
},
},
{
Name: "photo-counts",
Usage: "repopulate cached Likes and Comment counts on photos",
Action: func(c *cli.Context) error {
initdb(c)
log.Info("Running BackfillPhotoCounts()")
err := backfill.BackfillPhotoCounts()
if err != nil {
return err
}
return nil return nil
}, },
}, },

View File

@ -204,6 +204,13 @@ func Likes() http.HandlerFunc {
} }
} }
// Refresh cached like counts.
if req.TableName == "photos" {
if err := models.UpdatePhotoCachedCounts(tableID); err != nil {
log.Error("UpdatePhotoCachedCount(%d): %s", tableID, err)
}
}
// Send success response. // Send success response.
SendJSON(w, http.StatusOK, Response{ SendJSON(w, http.StatusOK, Response{
OK: true, OK: true,

View File

@ -121,6 +121,14 @@ func PostComment() http.HandlerFunc {
// Log the change. // Log the change.
models.LogDeleted(&models.User{ID: comment.UserID}, currentUser, "comments", comment.ID, "Deleted a comment.", comment) models.LogDeleted(&models.User{ID: comment.UserID}, currentUser, "comments", comment.ID, "Deleted a comment.", comment)
} }
// Refresh cached like counts.
if tableName == "photos" {
if err := models.UpdatePhotoCachedCounts(tableID); err != nil {
log.Error("UpdatePhotoCachedCount(%d): %s", tableID, err)
}
}
templates.Redirect(w, fromURL) templates.Redirect(w, fromURL)
return return
} }
@ -174,6 +182,13 @@ func PostComment() http.HandlerFunc {
session.Flash(w, r, "Comment added!") session.Flash(w, r, "Comment added!")
templates.Redirect(w, fromURL) templates.Redirect(w, fromURL)
// Refresh cached comment counts.
if tableName == "photos" {
if err := models.UpdatePhotoCachedCounts(tableID); err != nil {
log.Error("UpdatePhotoCachedCount(%d): %s", tableID, err)
}
}
// Log the change. // Log the change.
models.LogCreated(currentUser, "comments", comment.ID, "Posted a new comment.\n\n---\n\n"+message) models.LogCreated(currentUser, "comments", comment.ID, "Posted a new comment.\n\n---\n\n"+message)

View File

@ -18,10 +18,8 @@ func SiteGallery() http.HandlerFunc {
var sortWhitelist = []string{ var sortWhitelist = []string{
"created_at desc", "created_at desc",
"created_at asc", "created_at asc",
"like_count desc",
// Custom (advanced) sort options. "comment_count desc",
"by_likes",
"by_comments",
} }
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {

View File

@ -19,10 +19,8 @@ func UserPhotos() http.HandlerFunc {
"pinned desc nulls last, updated_at desc", "pinned desc nulls last, updated_at desc",
"created_at desc", "created_at desc",
"created_at asc", "created_at asc",
"like_count desc",
// Custom (advanced) sort options. "comment_count desc",
"by_likes",
"by_comments",
} }
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {

View File

@ -0,0 +1,25 @@
package backfill
import (
"code.nonshy.com/nonshy/website/pkg/models"
)
// BackfillPhotoCounts recomputes the cached Likes and Comment counts on photos.
func BackfillPhotoCounts() error {
res := models.DB.Exec(`
UPDATE photos
SET like_count = (
SELECT count(id)
FROM likes
WHERE table_name='photos'
AND table_id=photos.id
),
comment_count = (
SELECT count(id)
FROM comments
WHERE table_name='photos'
AND table_id=photos.id
);
`)
return res.Error
}

View File

@ -25,6 +25,8 @@ type Photo struct {
Gallery bool `gorm:"index"` // photo appears in the public gallery (if public) Gallery bool `gorm:"index"` // photo appears in the public gallery (if public)
Explicit bool `gorm:"index"` // is an explicit photo Explicit bool `gorm:"index"` // is an explicit photo
Pinned bool `gorm:"index"` // user pins it to the front of their gallery Pinned bool `gorm:"index"` // user pins it to the front of their gallery
LikeCount int64 `gorm:"index"` // cache of 'likes' count
CommentCount int64 `gorm:"index"` // cache of comments count
CreatedAt time.Time `gorm:"index"` CreatedAt time.Time `gorm:"index"`
UpdatedAt time.Time UpdatedAt time.Time
} }
@ -135,24 +137,6 @@ func PaginateUserPhotos(userID uint64, conf UserGallery, pager *Pagination) ([]*
placeholders = append(placeholders, explicit[0]) placeholders = append(placeholders, explicit[0])
} }
// Custom SORT parameters.
switch pager.Sort {
case "by_likes":
pager.Sort = `(
SELECT count(likes.id)
FROM likes
WHERE likes.table_name = 'photos'
AND likes.table_id = photos.id
) DESC`
case "by_comments":
pager.Sort = `(
SELECT count(comments.id)
FROM comments
WHERE comments.table_name = 'photos'
AND comments.table_id = photos.id
) DESC NULLS LAST`
}
query := DB.Where( query := DB.Where(
strings.Join(wheres, " AND "), strings.Join(wheres, " AND "),
placeholders..., placeholders...,
@ -690,38 +674,33 @@ func PaginateGalleryPhotos(user *User, conf Gallery, pager *Pagination) ([]*Phot
) )
} }
// Get count pre-sorting.
query.Model(&Photo{}).Count(&pager.Total)
// Custom SORT parameters.
switch pager.Sort {
case "by_likes":
query = query.Select(`
photos.*,
COUNT(likes.id) AS like_count
`).Joins(
"LEFT OUTER JOIN likes ON (likes.table_name='photos' AND likes.table_id=photos.id)",
).Where(
"likes.table_name = 'photos' AND likes.table_id = photos.id",
).Group("photos.id")
pager.Sort = `like_count DESC`
case "by_comments":
query = query.Select(`
photos.*,
COUNT(comments.id) AS comment_count
`).Joins(
"LEFT OUTER JOIN comments ON (comments.table_name='photos' AND comments.table_id=photos.id)",
).Where(
"comments.table_name = 'photos' AND comments.table_id = photos.id",
).Group("photos.id")
pager.Sort = `comment_count DESC`
}
query = query.Order(pager.Sort) query = query.Order(pager.Sort)
query.Model(&Photo{}).Count(&pager.Total)
result := query.Offset(pager.GetOffset()).Limit(pager.PerPage).Find(&p) result := query.Offset(pager.GetOffset()).Limit(pager.PerPage).Find(&p)
return p, result.Error return p, result.Error
} }
// UpdatePhotoCachedCounts will refresh the cached like/comment count on the photos table.
func UpdatePhotoCachedCounts(photoID uint64) error {
res := DB.Exec(`
UPDATE photos
SET like_count = (
SELECT count(id)
FROM likes
WHERE table_name='photos'
AND table_id=photos.id
),
comment_count = (
SELECT count(id)
FROM comments
WHERE table_name='photos'
AND table_id=photos.id
)
WHERE photos.id = ?;
`, photoID)
return res.Error
}
// Save photo. // Save photo.
func (p *Photo) Save() error { func (p *Photo) Save() error {
result := DB.Save(p) result := DB.Save(p)

View File

@ -414,8 +414,8 @@
{{end}} {{end}}
<option value="created_at desc"{{if eq .Sort "created_at desc"}} selected{{end}}>Most recent</option> <option value="created_at desc"{{if eq .Sort "created_at desc"}} selected{{end}}>Most recent</option>
<option value="created_at asc"{{if eq .Sort "created_at asc"}} selected{{end}}>Oldest first</option> <option value="created_at asc"{{if eq .Sort "created_at asc"}} selected{{end}}>Oldest first</option>
<option value="by_likes"{{if eq .Sort "by_likes"}} selected{{end}}>Most likes</option> <option value="like_count desc"{{if eq .Sort "like_count desc"}} selected{{end}}>Most likes</option>
<option value="by_comments"{{if eq .Sort "by_comments"}} selected{{end}}>Most comments</option> <option value="comment_count desc"{{if eq .Sort "comment_count desc"}} selected{{end}}>Most comments</option>
</select> </select>
</div> </div>
</div> </div>