mirror of
https://github.com/photoprism/photoprism.git
synced 2025-12-12 00:34:13 +01:00
Signed-off-by: Michael Mayer <michael@photoprism.app> Co-authored-by: Michael Mayer <michael@photoprism.app> Co-authored-by: graciousgrey <theresagresch@gmail.com>
180 lines
5.0 KiB
Go
180 lines
5.0 KiB
Go
package entity
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/dustin/go-humanize/english"
|
|
gc "github.com/patrickmn/go-cache"
|
|
|
|
"github.com/photoprism/photoprism/pkg/clean"
|
|
)
|
|
|
|
// Keyword and PhotoKeyword cache expiration times and cleanup interval.
|
|
const (
|
|
keywordCacheDefaultExpiration = 15 * time.Minute
|
|
keywordCacheErrorExpiration = 5 * time.Minute
|
|
keywordCacheCleanupInterval = 10 * time.Minute
|
|
photoKeywordCacheExpiration = 24 * time.Hour
|
|
)
|
|
|
|
// Cache Keyword and PhotoKeyword entities for faster indexing.
|
|
var (
|
|
keywordCache = gc.New(keywordCacheDefaultExpiration, keywordCacheCleanupInterval)
|
|
photoKeywordCache = gc.New(photoKeywordCacheExpiration, keywordCacheCleanupInterval)
|
|
photoKeywordCacheMutex = sync.Mutex{}
|
|
)
|
|
|
|
// photoKeywordCacheKey returns a string key for the photoKeywordCache.
|
|
func photoKeywordCacheKey(photoId, keywordId uint) string {
|
|
return fmt.Sprintf("%d-%d", photoId, keywordId)
|
|
}
|
|
|
|
// FlushKeywordCache removes all cached Keyword entities from the cache.
|
|
func FlushKeywordCache() {
|
|
keywordCache.Flush()
|
|
}
|
|
|
|
// FlushCachedKeyword deletes a cached Keyword entity from the cache.
|
|
func FlushCachedKeyword(m *Keyword) {
|
|
if m == nil {
|
|
return
|
|
} else if m.HasID() {
|
|
keywordCache.Delete(m.Keyword)
|
|
}
|
|
}
|
|
|
|
// FlushPhotoKeywordCache removes all cached PhotoKeyword entities from the cache.
|
|
func FlushPhotoKeywordCache() {
|
|
photoKeywordCacheMutex.Lock()
|
|
defer photoKeywordCacheMutex.Unlock()
|
|
|
|
start := time.Now()
|
|
|
|
photoKeywordCache.Flush()
|
|
|
|
log.Debugf("index: flushed photo keywords cache [%s]", time.Since(start))
|
|
}
|
|
|
|
// FlushCachedPhotoKeyword deletes a cached PhotoKeyword entity from the cache.
|
|
func FlushCachedPhotoKeyword(m *PhotoKeyword) {
|
|
if m == nil {
|
|
return
|
|
} else if m.HasID() {
|
|
photoKeywordCache.Delete(photoKeywordCacheKey(m.PhotoID, m.KeywordID))
|
|
}
|
|
}
|
|
|
|
// CachePhotoKeywords preloads the photo-keyword cache from the database to speed up lookups.
|
|
func CachePhotoKeywords() (err error) {
|
|
photoKeywordCacheMutex.Lock()
|
|
defer photoKeywordCacheMutex.Unlock()
|
|
|
|
start := time.Now()
|
|
|
|
var photoKeywords []PhotoKeyword
|
|
|
|
// Find photo keyword assignments.
|
|
if err = UnscopedDb().
|
|
Raw("SELECT * FROM photos_keywords").
|
|
Scan(&photoKeywords).Error; err != nil {
|
|
return err
|
|
}
|
|
|
|
// Cache existing keyword assignments.
|
|
for _, m := range photoKeywords {
|
|
photoKeywordCache.SetDefault(m.CacheKey(), m)
|
|
}
|
|
|
|
log.Debugf("index: cached %s [%s]", english.Plural(len(photoKeywords), "photo keyword", "photo keywords"), time.Since(start))
|
|
|
|
return nil
|
|
}
|
|
|
|
// FindKeyword resolves a keyword by its normalized name, optionally consulting
|
|
// the in-memory cache before hitting the database.
|
|
func FindKeyword(keyword string, cached bool) (*Keyword, error) {
|
|
if keyword == "" {
|
|
return &Keyword{}, errors.New("missing keyword name")
|
|
}
|
|
|
|
// Return cached keyword, if found.
|
|
if cached {
|
|
if cacheData, ok := keywordCache.Get(keyword); ok {
|
|
log.Tracef("keyword: cache hit for %s", keyword)
|
|
|
|
// Get cached data.
|
|
if result := cacheData.(*Keyword); result.HasID() {
|
|
// Return cached entity.
|
|
return result, nil
|
|
} else {
|
|
// Return cached "not found" error.
|
|
return &Keyword{}, fmt.Errorf("keyword not found")
|
|
}
|
|
}
|
|
}
|
|
|
|
// Fetch and cache keyword.
|
|
result := &Keyword{}
|
|
|
|
if find := Db().First(result, "keyword = ?", keyword); find.RecordNotFound() {
|
|
keywordCache.Set(keyword, result, keywordCacheErrorExpiration)
|
|
return result, fmt.Errorf("keyword not found")
|
|
} else if find.Error != nil {
|
|
keywordCache.Set(keyword, result, keywordCacheErrorExpiration)
|
|
return result, find.Error
|
|
} else {
|
|
keywordCache.SetDefault(result.Keyword, result)
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// FindPhotoKeyword loads the photo-keyword join row for the given IDs, using the cache when enabled.
|
|
func FindPhotoKeyword(photoId, keywordId uint, cached bool) (*PhotoKeyword, error) {
|
|
if photoId == 0 {
|
|
return &PhotoKeyword{}, errors.New("invalid photo id")
|
|
} else if keywordId == 0 {
|
|
return &PhotoKeyword{}, errors.New("invalid keyword id")
|
|
}
|
|
|
|
cacheKey := photoKeywordCacheKey(photoId, keywordId)
|
|
|
|
if cacheKey == "" {
|
|
return &PhotoKeyword{}, fmt.Errorf("invalid cache key %s", clean.LogQuote(cacheKey))
|
|
}
|
|
|
|
// Return cached keyword, if found.
|
|
if cached {
|
|
if cacheData, ok := photoKeywordCache.Get(cacheKey); ok {
|
|
log.Tracef("photo-keyword: cache hit for %s", cacheKey)
|
|
|
|
// Get cached data.
|
|
if result := cacheData.(PhotoKeyword); result.HasID() {
|
|
// Return cached entity.
|
|
return &result, nil
|
|
} else {
|
|
// Return cached "not found" error.
|
|
return &PhotoKeyword{}, fmt.Errorf("photo-keyword not found")
|
|
}
|
|
}
|
|
}
|
|
|
|
// Fetch and cache photo-keyword.
|
|
result := &PhotoKeyword{}
|
|
|
|
if find := Db().First(result, "photo_id = ? AND keyword_id = ?", photoId, keywordId); find.RecordNotFound() {
|
|
photoKeywordCache.Set(cacheKey, *result, keywordCacheErrorExpiration)
|
|
return result, fmt.Errorf("photo-keyword not found")
|
|
} else if find.Error != nil {
|
|
photoKeywordCache.Set(cacheKey, *result, keywordCacheErrorExpiration)
|
|
return result, find.Error
|
|
} else {
|
|
photoKeywordCache.SetDefault(cacheKey, *result)
|
|
}
|
|
|
|
return result, nil
|
|
}
|