Places: Add support for "label" and "category" search filters #1187

This also improves the documentation of existing search filters.

Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
Michael Mayer 2023-09-20 22:07:24 +02:00
parent 0f1106eb3b
commit a865300666
6 changed files with 74 additions and 29 deletions

View file

@ -10,9 +10,9 @@ type SearchAlbums struct {
Slug string `form:"slug"`
Title string `form:"title"`
Country string `json:"country"`
Year string `form:"year" example:"year:1990|2003" notes:"Year Number, OR search with |"`
Month string `form:"month" example:"month:7|10" notes:"Month (1-12), OR search with |"`
Day string `form:"day" example:"day:3|13" notes:"Day of Month (1-31), OR search with |"`
Year string `form:"year" example:"year:1990|2003" notes:"Year (separate with |)"`
Month string `form:"month" example:"month:7|10" notes:"Month (1-12, separate with |)"`
Day string `form:"day" example:"day:3|13" notes:"Day of Month (1-31, separate with |)"`
Favorite bool `form:"favorite"`
Public bool `form:"public"`
Private bool `form:"private"`

View file

@ -13,14 +13,14 @@ type SearchPhotos struct {
Filter string `form:"filter" serialize:"-" notes:"-"`
ID string `form:"id" example:"id:123e4567-e89b-..." notes:"Finds pictures by Exif UID, XMP Document ID or Instance ID"`
UID string `form:"uid" example:"uid:pqbcf5j446s0futy" notes:"Limits results to the specified internal unique IDs"`
Type string `form:"type" example:"type:raw" notes:"Media Type (image, video, raw, live, animated); OR search with |"`
Path string `form:"path" example:"path:2020/Holiday" notes:"Path Name, OR search with |, supports * wildcards"`
Folder string `form:"folder" example:"folder:\"*/2020\"" notes:"Path Name, OR search with |, supports * wildcards"` // Alias for Path
Name string `form:"name" example:"name:\"IMG_9831-112*\"" notes:"File Name without path and extension, OR search with |"`
Filename string `form:"filename" example:"filename:\"2021/07/12345.jpg\"" notes:"File Name with path and extension, OR search with |"`
Original string `form:"original" example:"original:\"IMG_9831-112*\"" notes:"Original file name of imported files, OR search with |"`
Title string `form:"title" example:"title:\"Lake*\"" notes:"Title, OR search with |"`
Hash string `form:"hash" example:"hash:2fd4e1c67a2d" notes:"SHA1 File Hash, OR search with |"`
Type string `form:"type" example:"type:raw" notes:"Media Type (image, video, raw, live, animated); separate with |"`
Path string `form:"path" example:"path:2020/Holiday" notes:"Path Name (separate with |), supports * wildcards"`
Folder string `form:"folder" example:"folder:\"*/2020\"" notes:"Path Name (separate with |), supports * wildcards"` // Alias for Path
Name string `form:"name" example:"name:\"IMG_9831-112*\"" notes:"File Name without path and extension (separate with |)"`
Filename string `form:"filename" example:"filename:\"2021/07/12345.jpg\"" notes:"File Name with path and extension (separate with |)"`
Original string `form:"original" example:"original:\"IMG_9831-112*\"" notes:"Original file name of imported files (separate with |)"`
Title string `form:"title" example:"title:\"Lake*\"" notes:"Title (separate with |)"`
Hash string `form:"hash" example:"hash:2fd4e1c67a2d" notes:"SHA1 File Hash (separate with |)"`
Primary bool `form:"primary" notes:"Finds primary JPEG files only"`
Stack bool `form:"stack" notes:"Finds pictures with more than one media file"`
Unstacked bool `form:"unstacked" notes:"Finds pictures with a file that has been removed from a stack"`
@ -56,24 +56,24 @@ type SearchPhotos struct {
Diff uint32 `form:"diff" notes:"Differential Perceptual Hash (000000-FFFFFF)"`
Mono bool `form:"mono" notes:"Finds pictures with few or no colors"`
Geo string `form:"geo" example:"geo:yes" notes:"Finds pictures with or without coordinates"`
Keywords string `form:"keywords" example:"keywords:\"buffalo&water\"" notes:"Keywords, can be combined with & and |"` // Filter by keyword(s)
Label string `form:"label" example:"label:cat|dog" notes:"Label Name, OR search with |"` // Label name
Category string `form:"category" notes:"Location Category Name"` // Moments
Country string `form:"country" example:"country:\"de|us\"" notes:"Country Code, OR search with |"` // Moments
State string `form:"state" example:"state:\"Baden-Württemberg\"" notes:"Name of State (Location), OR search with |"` // Moments
City string `form:"city" example:"city:\"Berlin\"" notes:"Name of City (Location), OR search with |"` // Moments
Year string `form:"year" example:"year:1990|2003" notes:"Year Number, OR search with |"` // Moments
Month string `form:"month" example:"month:7|10" notes:"Month (1-12), OR search with |"` // Moments
Day string `form:"day" example:"day:3|13" notes:"Day of Month (1-31), OR search with |"` // Moments
Keywords string `form:"keywords" example:"keywords:\"sand&water\"" notes:"Keywords (combinable with & and |)"`
Label string `form:"label" example:"label:cat|dog" notes:"Label Names (separate with |)"`
Category string `form:"category" example:"category:airport" notes:"Location Category"`
Country string `form:"country" example:"country:\"de|us\"" notes:"Location Country Code (separate with |)"` // Moments
State string `form:"state" example:"state:\"Baden-Württemberg\"" notes:"Location State (separate with |)"` // Moments
City string `form:"city" example:"city:\"Berlin\"" notes:"Location City (separate with |)"` // Moments
Year string `form:"year" example:"year:1990|2003" notes:"Year (separate with |)"` // Moments
Month string `form:"month" example:"month:7|10" notes:"Month (1-12, separate with |)"` // Moments
Day string `form:"day" example:"day:3|13" notes:"Day of Month (1-31, separate with |)"` // Moments
Face string `form:"face" example:"face:PN6QO5INYTUSAATOFL43LL2ABAV5ACZG" notes:"Face ID, yes, no, new, or kind"` // UIDs
Faces string `form:"faces" example:"faces:yes faces:3" notes:"Minimum number of Faces (yes = 1)"` // Find or exclude faces if detected.
Subject string `form:"subject" example:"subject:\"Jane Doe & John Doe\"" notes:"Alias for person"` // UIDs
Person string `form:"person" example:"person:\"Jane Doe & John Doe\"" notes:"Subject Names, exact matches, can be combined with & and |"` // Alias for Subject
Person string `form:"person" example:"person:\"Jane Doe & John Doe\"" notes:"Subject Names, exact matches (combinable with & and |)"` // Alias for Subject
Subjects string `form:"subjects" example:"subjects:\"Jane & John\"" notes:"Alias for people"` // People names
People string `form:"people" example:"people:\"Jane & John\"" notes:"Subject Names, can be combined with & and |"` // Alias for Subjects
People string `form:"people" example:"people:\"Jane & John\"" notes:"Subject Names (combinable with & and |)"` // Alias for Subjects
Album string `form:"album" example:"album:berlin" notes:"Album UID or Name, supports * wildcards"` // Album UIDs or name
Albums string `form:"albums" example:"albums:\"South Africa & Birds\"" notes:"Album Names, can be combined with & and |"` // Multi search with and/or
Color string `form:"color" example:"color:\"red|blue\"" notes:"Color Name (purple, magenta, pink, red, orange, gold, yellow, lime, green, teal, cyan, blue, brown, white, grey, black), OR search with |"` // Main color
Albums string `form:"albums" example:"albums:\"South Africa & Birds\"" notes:"Album Names (combinable with & and |)"` // Multi search with and/or
Color string `form:"color" example:"color:\"red|blue\"" notes:"Color Name (purple, magenta, pink, red, orange, gold, yellow, lime, green, teal, cyan, blue, brown, white, grey, black) (separate with |)"` // Main color
Quality int `form:"quality" notes:"Minimum quality score (1-7)"` // Photo quality score
Review bool `form:"review" notes:"Finds pictures in review"` // Find photos in review
Camera string `form:"camera" example:"camera:canon" notes:"Camera Make/Model Name"` // Camera UID or name

View file

@ -53,9 +53,11 @@ type SearchPhotosGeo struct {
People string `form:"people"` // Alias for Subjects
Chroma int16 `form:"chroma" example:"chroma:70" notes:"Chroma (0-100)"`
Mono bool `form:"mono" notes:"Finds pictures with few or no colors"`
Keywords string `form:"keywords"`
Keywords string `form:"keywords" example:"keywords:\"sand&water\"" notes:"Keywords (combinable with & and |)"`
Label string `form:"label" example:"label:cat|dog" notes:"Label Names (separate with |)"`
Category string `form:"category" example:"category:airport" notes:"Location Category"`
Album string `form:"album" example:"album:berlin" notes:"Album UID or Name, supports * wildcards"`
Albums string `form:"albums" example:"albums:\"South Africa & Birds\"" notes:"Album Names, can be combined with & and |"`
Albums string `form:"albums" example:"albums:\"South Africa & Birds\"" notes:"Album Names (combinable with & and |)"`
Country string `form:"country"`
State string `form:"state"` // Moments
City string `form:"city"`

View file

@ -247,6 +247,16 @@ func TestSearchPhotosGeo_Serialize(t *testing.T) {
assert.Equal(t, "q:\"q:fooBar baz\" favorite:true", form.Serialize())
}
func TestSearchPhotosGeo_Unserialize(t *testing.T) {
filter := "public:true label:bay|beach|cape|seashore"
frm := SearchPhotosGeo{}
err := Unserialize(&frm, filter)
assert.Equal(t, true, frm.Public)
assert.Equal(t, "bay|beach|cape|seashore", frm.Label)
assert.NoError(t, err)
}
// public:true label:bay|beach|cape|seashore
func TestSearchPhotosGeo_SerializeAll(t *testing.T) {
form := &SearchPhotosGeo{Query: "q:\"fooBar baz\"", Favorite: "true"}

View file

@ -107,7 +107,8 @@ func searchPhotos(f form.SearchPhotos, sess *entity.Session, resultCols string)
} else if a.AlbumFilter == "" {
s = s.Joins("JOIN photos_albums ON photos_albums.photo_uid = files.photo_uid").
Where("photos_albums.hidden = 0 AND photos_albums.album_uid = ?", a.AlbumUID)
} else if err = form.Unserialize(&f, a.AlbumFilter); err != nil {
} else if formErr := form.Unserialize(&f, a.AlbumFilter); formErr != nil {
log.Debugf("search: %s (%s)", clean.Error(formErr), clean.Log(a.AlbumFilter))
return PhotoResults{}, 0, ErrBadFilter
} else {
f.Filter = a.AlbumFilter
@ -266,7 +267,7 @@ func searchPhotos(f form.SearchPhotos, sess *entity.Session, resultCols string)
var labels []entity.Label
var labelIds []uint
if txt.NotEmpty(f.Label) {
if err := Db().Where(AnySlug("label_slug", f.Label, txt.Or)).Or(AnySlug("custom_slug", f.Label, txt.Or)).Find(&labels).Error; len(labels) == 0 || err != nil {
if labelErr := Db().Where(AnySlug("label_slug", f.Label, txt.Or)).Or(AnySlug("custom_slug", f.Label, txt.Or)).Find(&labels).Error; len(labels) == 0 || labelErr != nil {
log.Debugf("search: label %s not found", txt.LogParamLower(f.Label))
return PhotoResults{}, 0, nil
} else {

View file

@ -93,7 +93,8 @@ func UserPhotosGeo(f form.SearchPhotosGeo, sess *entity.Session) (results GeoRes
} else if a.AlbumFilter == "" {
s = s.Joins("JOIN photos_albums ON photos_albums.photo_uid = files.photo_uid").
Where("photos_albums.hidden = 0 AND photos_albums.album_uid = ?", a.AlbumUID)
} else if err = form.Unserialize(&f, a.AlbumFilter); err != nil {
} else if formErr := form.Unserialize(&f, a.AlbumFilter); formErr != nil {
log.Debugf("search: %s (%s)", clean.Error(formErr), clean.Log(a.AlbumFilter))
return GeoResults{}, ErrBadFilter
} else {
f.Filter = a.AlbumFilter
@ -199,6 +200,31 @@ func UserPhotosGeo(f form.SearchPhotosGeo, sess *entity.Session) (results GeoRes
}
}
// Filter by label, label category and keywords.
var categories []entity.Category
var labels []entity.Label
var labelIds []uint
if txt.NotEmpty(f.Label) {
if labelErr := Db().Where(AnySlug("label_slug", f.Label, txt.Or)).Or(AnySlug("custom_slug", f.Label, txt.Or)).Find(&labels).Error; len(labels) == 0 || labelErr != nil {
log.Debugf("search: label %s not found", txt.LogParamLower(f.Label))
return GeoResults{}, nil
} else {
for _, l := range labels {
labelIds = append(labelIds, l.ID)
Log("find categories", Db().Where("category_id = ?", l.ID).Find(&categories).Error)
log.Debugf("search: label %s includes %d categories", txt.LogParamLower(l.LabelName), len(categories))
for _, category := range categories {
labelIds = append(labelIds, category.LabelID)
}
}
s = s.Joins("JOIN photos_labels ON photos_labels.photo_id = files.photo_id AND photos_labels.uncertainty < 100 AND photos_labels.label_id IN (?)", labelIds).
Group("photos.id, files.id")
}
}
// Set search filters based on search terms.
if terms := txt.SearchTerms(f.Query); f.Query != "" && len(terms) == 0 {
if f.Title == "" {
@ -444,6 +470,12 @@ func UserPhotosGeo(f form.SearchPhotosGeo, sess *entity.Session) (results GeoRes
s = s.Where("places.place_city IN (?)", SplitOr(f.City))
}
// Filter by location category.
if txt.NotEmpty(f.Category) {
s = s.Joins("JOIN cells ON photos.cell_id = cells.id").
Where("cells.cell_category IN (?)", SplitOr(strings.ToLower(f.Category)))
}
// Filter by media type.
if txt.NotEmpty(f.Type) {
s = s.Where("photos.photo_type IN (?)", SplitOr(strings.ToLower(f.Type)))