Skip to content

Commit a865300

Browse files
committed
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>
1 parent 0f1106e commit a865300

File tree

6 files changed

+74
-29
lines changed

6 files changed

+74
-29
lines changed

internal/form/search_albums.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@ type SearchAlbums struct {
1010
Slug string `form:"slug"`
1111
Title string `form:"title"`
1212
Country string `json:"country"`
13-
Year string `form:"year" example:"year:1990|2003" notes:"Year Number, OR search with |"`
14-
Month string `form:"month" example:"month:7|10" notes:"Month (1-12), OR search with |"`
15-
Day string `form:"day" example:"day:3|13" notes:"Day of Month (1-31), OR search with |"`
13+
Year string `form:"year" example:"year:1990|2003" notes:"Year (separate with |)"`
14+
Month string `form:"month" example:"month:7|10" notes:"Month (1-12, separate with |)"`
15+
Day string `form:"day" example:"day:3|13" notes:"Day of Month (1-31, separate with |)"`
1616
Favorite bool `form:"favorite"`
1717
Public bool `form:"public"`
1818
Private bool `form:"private"`

internal/form/search_photos.go

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,14 @@ type SearchPhotos struct {
1313
Filter string `form:"filter" serialize:"-" notes:"-"`
1414
ID string `form:"id" example:"id:123e4567-e89b-..." notes:"Finds pictures by Exif UID, XMP Document ID or Instance ID"`
1515
UID string `form:"uid" example:"uid:pqbcf5j446s0futy" notes:"Limits results to the specified internal unique IDs"`
16-
Type string `form:"type" example:"type:raw" notes:"Media Type (image, video, raw, live, animated); OR search with |"`
17-
Path string `form:"path" example:"path:2020/Holiday" notes:"Path Name, OR search with |, supports * wildcards"`
18-
Folder string `form:"folder" example:"folder:\"*/2020\"" notes:"Path Name, OR search with |, supports * wildcards"` // Alias for Path
19-
Name string `form:"name" example:"name:\"IMG_9831-112*\"" notes:"File Name without path and extension, OR search with |"`
20-
Filename string `form:"filename" example:"filename:\"2021/07/12345.jpg\"" notes:"File Name with path and extension, OR search with |"`
21-
Original string `form:"original" example:"original:\"IMG_9831-112*\"" notes:"Original file name of imported files, OR search with |"`
22-
Title string `form:"title" example:"title:\"Lake*\"" notes:"Title, OR search with |"`
23-
Hash string `form:"hash" example:"hash:2fd4e1c67a2d" notes:"SHA1 File Hash, OR search with |"`
16+
Type string `form:"type" example:"type:raw" notes:"Media Type (image, video, raw, live, animated); separate with |"`
17+
Path string `form:"path" example:"path:2020/Holiday" notes:"Path Name (separate with |), supports * wildcards"`
18+
Folder string `form:"folder" example:"folder:\"*/2020\"" notes:"Path Name (separate with |), supports * wildcards"` // Alias for Path
19+
Name string `form:"name" example:"name:\"IMG_9831-112*\"" notes:"File Name without path and extension (separate with |)"`
20+
Filename string `form:"filename" example:"filename:\"2021/07/12345.jpg\"" notes:"File Name with path and extension (separate with |)"`
21+
Original string `form:"original" example:"original:\"IMG_9831-112*\"" notes:"Original file name of imported files (separate with |)"`
22+
Title string `form:"title" example:"title:\"Lake*\"" notes:"Title (separate with |)"`
23+
Hash string `form:"hash" example:"hash:2fd4e1c67a2d" notes:"SHA1 File Hash (separate with |)"`
2424
Primary bool `form:"primary" notes:"Finds primary JPEG files only"`
2525
Stack bool `form:"stack" notes:"Finds pictures with more than one media file"`
2626
Unstacked bool `form:"unstacked" notes:"Finds pictures with a file that has been removed from a stack"`
@@ -56,24 +56,24 @@ type SearchPhotos struct {
5656
Diff uint32 `form:"diff" notes:"Differential Perceptual Hash (000000-FFFFFF)"`
5757
Mono bool `form:"mono" notes:"Finds pictures with few or no colors"`
5858
Geo string `form:"geo" example:"geo:yes" notes:"Finds pictures with or without coordinates"`
59-
Keywords string `form:"keywords" example:"keywords:\"buffalo&water\"" notes:"Keywords, can be combined with & and |"` // Filter by keyword(s)
60-
Label string `form:"label" example:"label:cat|dog" notes:"Label Name, OR search with |"` // Label name
61-
Category string `form:"category" notes:"Location Category Name"` // Moments
62-
Country string `form:"country" example:"country:\"de|us\"" notes:"Country Code, OR search with |"` // Moments
63-
State string `form:"state" example:"state:\"Baden-Württemberg\"" notes:"Name of State (Location), OR search with |"` // Moments
64-
City string `form:"city" example:"city:\"Berlin\"" notes:"Name of City (Location), OR search with |"` // Moments
65-
Year string `form:"year" example:"year:1990|2003" notes:"Year Number, OR search with |"` // Moments
66-
Month string `form:"month" example:"month:7|10" notes:"Month (1-12), OR search with |"` // Moments
67-
Day string `form:"day" example:"day:3|13" notes:"Day of Month (1-31), OR search with |"` // Moments
59+
Keywords string `form:"keywords" example:"keywords:\"sand&water\"" notes:"Keywords (combinable with & and |)"`
60+
Label string `form:"label" example:"label:cat|dog" notes:"Label Names (separate with |)"`
61+
Category string `form:"category" example:"category:airport" notes:"Location Category"`
62+
Country string `form:"country" example:"country:\"de|us\"" notes:"Location Country Code (separate with |)"` // Moments
63+
State string `form:"state" example:"state:\"Baden-Württemberg\"" notes:"Location State (separate with |)"` // Moments
64+
City string `form:"city" example:"city:\"Berlin\"" notes:"Location City (separate with |)"` // Moments
65+
Year string `form:"year" example:"year:1990|2003" notes:"Year (separate with |)"` // Moments
66+
Month string `form:"month" example:"month:7|10" notes:"Month (1-12, separate with |)"` // Moments
67+
Day string `form:"day" example:"day:3|13" notes:"Day of Month (1-31, separate with |)"` // Moments
6868
Face string `form:"face" example:"face:PN6QO5INYTUSAATOFL43LL2ABAV5ACZG" notes:"Face ID, yes, no, new, or kind"` // UIDs
6969
Faces string `form:"faces" example:"faces:yes faces:3" notes:"Minimum number of Faces (yes = 1)"` // Find or exclude faces if detected.
7070
Subject string `form:"subject" example:"subject:\"Jane Doe & John Doe\"" notes:"Alias for person"` // UIDs
71-
Person string `form:"person" example:"person:\"Jane Doe & John Doe\"" notes:"Subject Names, exact matches, can be combined with & and |"` // Alias for Subject
71+
Person string `form:"person" example:"person:\"Jane Doe & John Doe\"" notes:"Subject Names, exact matches (combinable with & and |)"` // Alias for Subject
7272
Subjects string `form:"subjects" example:"subjects:\"Jane & John\"" notes:"Alias for people"` // People names
73-
People string `form:"people" example:"people:\"Jane & John\"" notes:"Subject Names, can be combined with & and |"` // Alias for Subjects
73+
People string `form:"people" example:"people:\"Jane & John\"" notes:"Subject Names (combinable with & and |)"` // Alias for Subjects
7474
Album string `form:"album" example:"album:berlin" notes:"Album UID or Name, supports * wildcards"` // Album UIDs or name
75-
Albums string `form:"albums" example:"albums:\"South Africa & Birds\"" notes:"Album Names, can be combined with & and |"` // Multi search with and/or
76-
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
75+
Albums string `form:"albums" example:"albums:\"South Africa & Birds\"" notes:"Album Names (combinable with & and |)"` // Multi search with and/or
76+
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
7777
Quality int `form:"quality" notes:"Minimum quality score (1-7)"` // Photo quality score
7878
Review bool `form:"review" notes:"Finds pictures in review"` // Find photos in review
7979
Camera string `form:"camera" example:"camera:canon" notes:"Camera Make/Model Name"` // Camera UID or name

internal/form/search_photos_geo.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,11 @@ type SearchPhotosGeo struct {
5353
People string `form:"people"` // Alias for Subjects
5454
Chroma int16 `form:"chroma" example:"chroma:70" notes:"Chroma (0-100)"`
5555
Mono bool `form:"mono" notes:"Finds pictures with few or no colors"`
56-
Keywords string `form:"keywords"`
56+
Keywords string `form:"keywords" example:"keywords:\"sand&water\"" notes:"Keywords (combinable with & and |)"`
57+
Label string `form:"label" example:"label:cat|dog" notes:"Label Names (separate with |)"`
58+
Category string `form:"category" example:"category:airport" notes:"Location Category"`
5759
Album string `form:"album" example:"album:berlin" notes:"Album UID or Name, supports * wildcards"`
58-
Albums string `form:"albums" example:"albums:\"South Africa & Birds\"" notes:"Album Names, can be combined with & and |"`
60+
Albums string `form:"albums" example:"albums:\"South Africa & Birds\"" notes:"Album Names (combinable with & and |)"`
5961
Country string `form:"country"`
6062
State string `form:"state"` // Moments
6163
City string `form:"city"`

internal/form/search_photos_geo_test.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,16 @@ func TestSearchPhotosGeo_Serialize(t *testing.T) {
247247
assert.Equal(t, "q:\"q:fooBar baz\" favorite:true", form.Serialize())
248248
}
249249

250+
func TestSearchPhotosGeo_Unserialize(t *testing.T) {
251+
filter := "public:true label:bay|beach|cape|seashore"
252+
frm := SearchPhotosGeo{}
253+
err := Unserialize(&frm, filter)
254+
assert.Equal(t, true, frm.Public)
255+
assert.Equal(t, "bay|beach|cape|seashore", frm.Label)
256+
assert.NoError(t, err)
257+
}
258+
259+
// public:true label:bay|beach|cape|seashore
250260
func TestSearchPhotosGeo_SerializeAll(t *testing.T) {
251261
form := &SearchPhotosGeo{Query: "q:\"fooBar baz\"", Favorite: "true"}
252262

internal/search/photos.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,8 @@ func searchPhotos(f form.SearchPhotos, sess *entity.Session, resultCols string)
107107
} else if a.AlbumFilter == "" {
108108
s = s.Joins("JOIN photos_albums ON photos_albums.photo_uid = files.photo_uid").
109109
Where("photos_albums.hidden = 0 AND photos_albums.album_uid = ?", a.AlbumUID)
110-
} else if err = form.Unserialize(&f, a.AlbumFilter); err != nil {
110+
} else if formErr := form.Unserialize(&f, a.AlbumFilter); formErr != nil {
111+
log.Debugf("search: %s (%s)", clean.Error(formErr), clean.Log(a.AlbumFilter))
111112
return PhotoResults{}, 0, ErrBadFilter
112113
} else {
113114
f.Filter = a.AlbumFilter
@@ -266,7 +267,7 @@ func searchPhotos(f form.SearchPhotos, sess *entity.Session, resultCols string)
266267
var labels []entity.Label
267268
var labelIds []uint
268269
if txt.NotEmpty(f.Label) {
269-
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 {
270+
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 {
270271
log.Debugf("search: label %s not found", txt.LogParamLower(f.Label))
271272
return PhotoResults{}, 0, nil
272273
} else {

internal/search/photos_geo.go

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,8 @@ func UserPhotosGeo(f form.SearchPhotosGeo, sess *entity.Session) (results GeoRes
9393
} else if a.AlbumFilter == "" {
9494
s = s.Joins("JOIN photos_albums ON photos_albums.photo_uid = files.photo_uid").
9595
Where("photos_albums.hidden = 0 AND photos_albums.album_uid = ?", a.AlbumUID)
96-
} else if err = form.Unserialize(&f, a.AlbumFilter); err != nil {
96+
} else if formErr := form.Unserialize(&f, a.AlbumFilter); formErr != nil {
97+
log.Debugf("search: %s (%s)", clean.Error(formErr), clean.Log(a.AlbumFilter))
9798
return GeoResults{}, ErrBadFilter
9899
} else {
99100
f.Filter = a.AlbumFilter
@@ -199,6 +200,31 @@ func UserPhotosGeo(f form.SearchPhotosGeo, sess *entity.Session) (results GeoRes
199200
}
200201
}
201202

203+
// Filter by label, label category and keywords.
204+
var categories []entity.Category
205+
var labels []entity.Label
206+
var labelIds []uint
207+
if txt.NotEmpty(f.Label) {
208+
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 {
209+
log.Debugf("search: label %s not found", txt.LogParamLower(f.Label))
210+
return GeoResults{}, nil
211+
} else {
212+
for _, l := range labels {
213+
labelIds = append(labelIds, l.ID)
214+
215+
Log("find categories", Db().Where("category_id = ?", l.ID).Find(&categories).Error)
216+
log.Debugf("search: label %s includes %d categories", txt.LogParamLower(l.LabelName), len(categories))
217+
218+
for _, category := range categories {
219+
labelIds = append(labelIds, category.LabelID)
220+
}
221+
}
222+
223+
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).
224+
Group("photos.id, files.id")
225+
}
226+
}
227+
202228
// Set search filters based on search terms.
203229
if terms := txt.SearchTerms(f.Query); f.Query != "" && len(terms) == 0 {
204230
if f.Title == "" {
@@ -444,6 +470,12 @@ func UserPhotosGeo(f form.SearchPhotosGeo, sess *entity.Session) (results GeoRes
444470
s = s.Where("places.place_city IN (?)", SplitOr(f.City))
445471
}
446472

473+
// Filter by location category.
474+
if txt.NotEmpty(f.Category) {
475+
s = s.Joins("JOIN cells ON photos.cell_id = cells.id").
476+
Where("cells.cell_category IN (?)", SplitOr(strings.ToLower(f.Category)))
477+
}
478+
447479
// Filter by media type.
448480
if txt.NotEmpty(f.Type) {
449481
s = s.Where("photos.photo_type IN (?)", SplitOr(strings.ToLower(f.Type)))

0 commit comments

Comments
 (0)