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:
parent
0f1106eb3b
commit
a865300666
6 changed files with 74 additions and 29 deletions
|
@ -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"`
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"`
|
||||
|
|
|
@ -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"}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)))
|
||||
|
|
Loading…
Reference in a new issue