2021-09-18 15:32:39 +02:00
package search
2020-01-15 04:04:33 +01:00
import (
"fmt"
2021-10-01 17:26:29 +02:00
"path"
2020-01-15 04:04:33 +01:00
"strings"
"time"
2021-10-01 00:05:49 +02:00
"github.com/dustin/go-humanize/english"
2020-05-23 20:58:58 +02:00
"github.com/jinzhu/gorm"
2021-09-29 22:57:26 +02:00
2020-05-23 20:58:58 +02:00
"github.com/photoprism/photoprism/internal/entity"
2020-01-15 04:04:33 +01:00
"github.com/photoprism/photoprism/internal/form"
2021-10-01 17:26:29 +02:00
"github.com/photoprism/photoprism/pkg/fs"
2020-01-15 04:04:33 +01:00
"github.com/photoprism/photoprism/pkg/pluscode"
2021-09-29 22:57:26 +02:00
"github.com/photoprism/photoprism/pkg/rnd"
2020-01-15 04:04:33 +01:00
"github.com/photoprism/photoprism/pkg/s2"
2020-04-26 14:31:33 +02:00
"github.com/photoprism/photoprism/pkg/txt"
2020-01-15 04:04:33 +01:00
)
2021-09-18 15:32:39 +02:00
// PhotosGeo searches for photos based on Form values and returns GeoResults ([]GeoResult).
2021-09-19 13:35:44 +02:00
func PhotosGeo ( f form . PhotoSearchGeo ) ( results GeoResults , err error ) {
2020-05-23 20:58:58 +02:00
start := time . Now ( )
2020-01-15 04:04:33 +01:00
if err := f . ParseQueryString ( ) ; err != nil {
2021-09-23 23:46:17 +02:00
return GeoResults { } , err
2020-01-15 04:04:33 +01:00
}
2020-05-08 15:41:01 +02:00
s := UnscopedDb ( )
2020-01-15 04:04:33 +01:00
2020-06-05 16:49:32 +02:00
// s.LogMode(true)
2020-03-28 17:17:41 +01:00
s = s . Table ( "photos" ) .
2020-05-23 20:58:58 +02:00
Select ( ` photos . id , photos . photo_uid , photos . photo_type , photos . photo_lat , photos . photo_lng ,
photos . photo_title , photos . photo_description , photos . photo_favorite , photos . taken_at , files . file_hash , files . file_width ,
2020-05-21 10:03:56 +02:00
files . file_height ` ) .
Joins ( ` JOIN files ON files . photo_id = photos . id AND
files . file_missing = 0 AND files . file_primary AND files . deleted_at IS NULL ` ) .
2020-01-21 13:23:24 +01:00
Where ( "photos.deleted_at IS NULL" ) .
2020-05-29 18:04:30 +02:00
Where ( "photos.photo_lat <> 0" )
2020-01-15 04:04:33 +01:00
2021-09-06 14:16:46 +02:00
// Clip to reasonable size and normalize operators.
2021-09-18 15:32:39 +02:00
f . Query = txt . NormalizeQuery ( f . Query )
2021-08-30 11:26:57 +02:00
2021-09-03 20:14:11 +02:00
// Set search filters based on search terms.
2021-09-17 15:52:25 +02:00
if terms := txt . SearchTerms ( f . Query ) ; f . Query != "" && len ( terms ) == 0 {
2021-09-29 22:57:26 +02:00
if f . Title == "" {
f . Title = fmt . Sprintf ( "%s*" , strings . Trim ( f . Query , "%*" ) )
2021-09-29 20:09:34 +02:00
f . Query = ""
}
2021-09-17 15:52:25 +02:00
} else if len ( terms ) > 0 {
2021-09-03 20:14:11 +02:00
switch {
case terms [ "faces" ] :
f . Query = strings . ReplaceAll ( f . Query , "faces" , "" )
f . Faces = "true"
2021-09-06 15:42:30 +02:00
case terms [ "people" ] :
f . Query = strings . ReplaceAll ( f . Query , "people" , "" )
f . Faces = "true"
2021-09-03 20:14:11 +02:00
case terms [ "videos" ] :
f . Query = strings . ReplaceAll ( f . Query , "videos" , "" )
f . Video = true
case terms [ "favorites" ] :
f . Query = strings . ReplaceAll ( f . Query , "favorites" , "" )
f . Favorite = true
}
}
// Filter by label, label category and keywords.
2020-01-15 04:04:33 +01:00
if f . Query != "" {
2020-05-23 20:58:58 +02:00
var categories [ ] entity . Category
var labels [ ] entity . Label
var labelIds [ ] uint
2020-05-29 18:04:30 +02:00
if err := Db ( ) . Where ( AnySlug ( "custom_slug" , f . Query , " " ) ) . Find ( & labels ) . Error ; len ( labels ) == 0 || err != nil {
2021-09-23 23:46:17 +02:00
log . Debugf ( "search: label %s not found, using fuzzy search" , txt . QuoteLower ( f . Query ) )
2020-05-23 20:58:58 +02:00
2021-08-29 19:19:54 +02:00
for _ , where := range LikeAnyKeyword ( "k.keyword" , f . Query ) {
2021-08-29 16:16:49 +02:00
s = s . Where ( "photos.id IN (SELECT pk.photo_id FROM keywords k JOIN photos_keywords pk ON k.id = pk.keyword_id WHERE (?))" , gorm . Expr ( where ) )
2020-05-23 20:58:58 +02:00
}
} else {
for _ , l := range labels {
labelIds = append ( labelIds , l . ID )
Db ( ) . Where ( "category_id = ?" , l . ID ) . Find ( & categories )
2021-09-23 23:46:17 +02:00
log . Debugf ( "search: label %s includes %d categories" , txt . QuoteLower ( l . LabelName ) , len ( categories ) )
2020-05-23 20:58:58 +02:00
for _ , category := range categories {
labelIds = append ( labelIds , category . LabelID )
}
}
2021-08-29 19:19:54 +02:00
if wheres := LikeAnyKeyword ( "k.keyword" , f . Query ) ; len ( wheres ) > 0 {
2021-08-29 16:16:49 +02:00
for _ , where := range wheres {
s = s . Where ( "photos.id IN (SELECT pk.photo_id FROM keywords k JOIN photos_keywords pk ON k.id = pk.keyword_id WHERE (?)) OR " +
"photos.id IN (SELECT pl.photo_id FROM photos_labels pl WHERE pl.uncertainty < 100 AND pl.label_id IN (?))" , gorm . Expr ( where ) , labelIds )
}
2020-05-23 20:58:58 +02:00
} else {
s = s . Where ( "photos.id IN (SELECT pl.photo_id FROM photos_labels pl WHERE pl.uncertainty < 100 AND pl.label_id IN (?))" , labelIds )
}
}
}
2021-08-29 16:16:49 +02:00
// Search for one or more keywords?
if f . Keywords != "" {
2021-09-20 22:39:29 +02:00
for _ , where := range LikeAnyKeyword ( "k.keyword" , f . Keywords ) {
2021-08-29 16:16:49 +02:00
s = s . Where ( "photos.id IN (SELECT pk.photo_id FROM keywords k JOIN photos_keywords pk ON k.id = pk.keyword_id WHERE (?))" , gorm . Expr ( where ) )
}
}
2021-09-23 14:23:00 +02:00
// Filter for specific face clusters? Example: PLJ7A3G4MBGZJRMVDIUCBLC46IAP4N7O
if len ( f . Face ) >= 32 {
2021-09-18 15:32:39 +02:00
for _ , f := range strings . Split ( strings . ToUpper ( f . Face ) , txt . And ) {
s = s . Where ( fmt . Sprintf ( "photos.id IN (SELECT photo_id FROM files f JOIN %s m ON f.file_uid = m.file_uid AND m.marker_invalid = 0 WHERE face_id IN (?))" ,
entity . Marker { } . TableName ( ) ) , strings . Split ( f , txt . Or ) )
}
2021-09-23 14:23:00 +02:00
} else if txt . No ( f . Face ) {
s = s . Where ( fmt . Sprintf ( "photos.id IN (SELECT photo_id FROM files f JOIN %s m ON f.file_uid = m.file_uid AND m.marker_invalid = 0 AND m.marker_type = ? WHERE face_id IS NULL OR face_id = '')" ,
entity . Marker { } . TableName ( ) ) , entity . MarkerFace )
} else if txt . Yes ( f . Face ) {
s = s . Where ( fmt . Sprintf ( "photos.id IN (SELECT photo_id FROM files f JOIN %s m ON f.file_uid = m.file_uid AND m.marker_invalid = 0 AND m.marker_type = ? WHERE face_id IS NOT NULL AND face_id <> '')" ,
entity . Marker { } . TableName ( ) ) , entity . MarkerFace )
2021-09-18 15:32:39 +02:00
}
2021-08-29 16:16:49 +02:00
// Filter for one or more subjects?
if f . Subject != "" {
2021-09-18 15:32:39 +02:00
for _ , subj := range strings . Split ( strings . ToLower ( f . Subject ) , txt . And ) {
2021-09-20 12:36:59 +02:00
if subjects := strings . Split ( subj , txt . Or ) ; rnd . ContainsUIDs ( subjects , 'j' ) {
s = s . Where ( fmt . Sprintf ( "photos.id IN (SELECT photo_id FROM files f JOIN %s m ON f.file_uid = m.file_uid AND m.marker_invalid = 0 WHERE subj_uid IN (?))" ,
entity . Marker { } . TableName ( ) ) , subjects )
} else {
s = s . Where ( fmt . Sprintf ( "photos.id IN (SELECT photo_id FROM files f JOIN %s m ON f.file_uid = m.file_uid AND m.marker_invalid = 0 JOIN %s s ON s.subj_uid = m.subj_uid WHERE (?))" ,
entity . Marker { } . TableName ( ) , entity . Subject { } . TableName ( ) ) , gorm . Expr ( AnySlug ( "s.subj_slug" , subj , txt . Or ) ) )
}
2021-08-30 11:56:34 +02:00
}
2021-08-29 16:16:49 +02:00
} else if f . Subjects != "" {
2021-09-20 12:36:59 +02:00
for _ , where := range LikeAllNames ( Cols { "subj_name" , "subj_alias" } , f . Subjects ) {
2021-09-17 14:26:12 +02:00
s = s . Where ( fmt . Sprintf ( "photos.id IN (SELECT photo_id FROM files f JOIN %s m ON f.file_uid = m.file_uid AND m.marker_invalid = 0 JOIN %s s ON s.subj_uid = m.subj_uid WHERE (?))" ,
2021-08-29 16:16:49 +02:00
entity . Marker { } . TableName ( ) , entity . Subject { } . TableName ( ) ) , gorm . Expr ( where ) )
}
}
// Filter by album?
2021-09-20 22:58:11 +02:00
if rnd . IsPPID ( f . Album , 'a' ) {
2021-08-29 16:16:49 +02:00
s = s . Joins ( "JOIN photos_albums ON photos_albums.photo_uid = photos.photo_uid" ) .
Where ( "photos_albums.hidden = 0 AND photos_albums.album_uid = ?" , f . Album )
2021-09-20 22:58:11 +02:00
} else if f . Albums != "" || f . Album != "" {
if f . Albums == "" {
f . Albums = f . Album
}
2021-08-29 19:19:54 +02:00
for _ , where := range LikeAnyWord ( "a.album_title" , f . Albums ) {
2021-08-29 16:16:49 +02:00
s = s . Where ( "photos.photo_uid IN (SELECT pa.photo_uid FROM photos_albums pa JOIN albums a ON a.album_uid = pa.album_uid WHERE (?))" , gorm . Expr ( where ) )
}
2020-05-23 20:58:58 +02:00
}
2021-08-29 16:16:49 +02:00
// Filter by camera?
2020-05-23 20:58:58 +02:00
if f . Camera > 0 {
s = s . Where ( "photos.camera_id = ?" , f . Camera )
}
2021-08-29 16:16:49 +02:00
// Filter by camera lens?
2020-05-23 20:58:58 +02:00
if f . Lens > 0 {
s = s . Where ( "photos.lens_id = ?" , f . Lens )
}
2021-08-29 16:16:49 +02:00
// Filter by year?
2021-09-20 23:32:35 +02:00
if f . Year != "" {
s = s . Where ( AnyInt ( "photos.photo_year" , f . Year , txt . Or , entity . UnknownYear , txt . YearMax ) )
2020-05-23 20:58:58 +02:00
}
2021-08-29 16:16:49 +02:00
// Filter by month?
2021-09-20 23:32:35 +02:00
if f . Month != "" {
s = s . Where ( AnyInt ( "photos.photo_month" , f . Month , txt . Or , entity . UnknownMonth , txt . MonthMax ) )
2020-05-23 20:58:58 +02:00
}
2021-08-29 16:16:49 +02:00
// Filter by day?
2021-09-20 23:32:35 +02:00
if f . Day != "" {
s = s . Where ( AnyInt ( "photos.photo_day" , f . Day , txt . Or , entity . UnknownDay , txt . DayMax ) )
2021-05-26 09:51:00 +02:00
}
// Find or exclude people if detected.
2021-05-26 10:46:32 +02:00
if txt . IsUInt ( f . Faces ) {
s = s . Where ( "photos.photo_faces >= ?" , txt . Int ( f . Faces ) )
} else if txt . Yes ( f . Faces ) {
2021-05-26 09:51:00 +02:00
s = s . Where ( "photos.photo_faces > 0" )
2021-05-26 10:46:32 +02:00
} else if txt . No ( f . Faces ) {
2021-05-26 09:51:00 +02:00
s = s . Where ( "photos.photo_faces = 0" )
}
2020-05-23 20:58:58 +02:00
if f . Color != "" {
2021-09-18 15:32:39 +02:00
s = s . Where ( "files.file_main_color IN (?)" , strings . Split ( strings . ToLower ( f . Color ) , txt . Or ) )
2020-05-23 20:58:58 +02:00
}
if f . Favorite {
s = s . Where ( "photos.photo_favorite = 1" )
}
if f . Country != "" {
2021-09-18 15:32:39 +02:00
s = s . Where ( "photos.photo_country IN (?)" , strings . Split ( strings . ToLower ( f . Country ) , txt . Or ) )
2020-05-23 20:58:58 +02:00
}
// Filter by media type.
if f . Type != "" {
2021-09-18 15:32:39 +02:00
s = s . Where ( "photos.photo_type IN (?)" , strings . Split ( strings . ToLower ( f . Type ) , txt . Or ) )
2020-05-23 20:58:58 +02:00
}
if f . Video {
s = s . Where ( "photos.photo_type = 'video'" )
} else if f . Photo {
s = s . Where ( "photos.photo_type IN ('image','raw','live')" )
}
if f . Path != "" {
p := f . Path
if strings . HasPrefix ( p , "/" ) {
p = p [ 1 : ]
}
if strings . HasSuffix ( p , "/" ) {
s = s . Where ( "photos.photo_path = ?" , p [ : len ( p ) - 1 ] )
} else {
2021-09-29 20:09:34 +02:00
where , values := OrLike ( "photos.photo_path" , p )
s = s . Where ( where , values ... )
2020-05-23 20:58:58 +02:00
}
}
2021-10-01 17:26:29 +02:00
// Filter by primary file name without path and extension.
2021-09-29 20:09:34 +02:00
if f . Name != "" {
2021-10-01 17:26:29 +02:00
where , names := OrLike ( "photos.photo_name" , f . Name )
// Omit file path and known extensions.
for i := range names {
names [ i ] = fs . StripKnownExt ( path . Base ( names [ i ] . ( string ) ) )
}
s = s . Where ( where , names ... )
2020-01-15 04:04:33 +01:00
}
2021-09-29 22:57:26 +02:00
// Filter by photo title.
if f . Title != "" {
where , values := OrLike ( "photos.photo_title" , f . Title )
s = s . Where ( where , values ... )
}
2020-06-04 14:56:27 +02:00
// Filter by status.
if f . Archived {
2020-12-15 20:14:06 +01:00
s = s . Where ( "photos.photo_quality > -1" )
2020-06-04 14:56:27 +02:00
s = s . Where ( "photos.deleted_at IS NOT NULL" )
} else {
s = s . Where ( "photos.deleted_at IS NULL" )
if f . Private {
s = s . Where ( "photos.photo_private = 1" )
} else if f . Public {
s = s . Where ( "photos.photo_private = 0" )
}
if f . Review {
s = s . Where ( "photos.photo_quality < 3" )
} else if f . Quality != 0 && f . Private == false {
s = s . Where ( "photos.photo_quality >= ?" , f . Quality )
}
2020-04-24 13:25:04 +02:00
}
2020-04-24 10:07:13 +02:00
if f . Favorite {
s = s . Where ( "photos.photo_favorite = 1" )
}
2020-01-15 04:04:33 +01:00
if f . S2 != "" {
2020-06-05 16:49:32 +02:00
s2Min , s2Max := s2 . PrefixedRange ( f . S2 , 7 )
2020-07-12 08:27:05 +02:00
s = s . Where ( "photos.cell_id BETWEEN ? AND ?" , s2Min , s2Max )
2020-01-15 04:04:33 +01:00
} else if f . Olc != "" {
2020-06-05 16:49:32 +02:00
s2Min , s2Max := s2 . PrefixedRange ( pluscode . S2 ( f . Olc ) , 7 )
2020-07-12 08:27:05 +02:00
s = s . Where ( "photos.cell_id BETWEEN ? AND ?" , s2Min , s2Max )
2020-01-15 04:04:33 +01:00
} else {
2021-02-11 20:06:23 +01:00
// Filter by approx distance to coordinates:
if f . Lat != 0 {
2021-09-18 15:32:39 +02:00
latMin := f . Lat - Radius * float32 ( f . Dist )
latMax := f . Lat + Radius * float32 ( f . Dist )
2020-03-28 17:17:41 +01:00
s = s . Where ( "photos.photo_lat BETWEEN ? AND ?" , latMin , latMax )
2020-01-15 04:04:33 +01:00
}
2021-02-11 20:06:23 +01:00
if f . Lng != 0 {
2021-09-18 15:32:39 +02:00
lngMin := f . Lng - Radius * float32 ( f . Dist )
lngMax := f . Lng + Radius * float32 ( f . Dist )
2020-03-28 17:17:41 +01:00
s = s . Where ( "photos.photo_lng BETWEEN ? AND ?" , lngMin , lngMax )
2020-01-15 04:04:33 +01:00
}
}
if ! f . Before . IsZero ( ) {
2020-03-28 17:17:41 +01:00
s = s . Where ( "photos.taken_at <= ?" , f . Before . Format ( "2006-01-02" ) )
2020-01-15 04:04:33 +01:00
}
if ! f . After . IsZero ( ) {
2020-03-28 17:17:41 +01:00
s = s . Where ( "photos.taken_at >= ?" , f . After . Format ( "2006-01-02" ) )
2020-01-15 04:04:33 +01:00
}
2020-05-23 20:58:58 +02:00
s = s . Order ( "taken_at, photos.photo_uid" )
2020-01-15 04:04:33 +01:00
2020-03-28 17:17:41 +01:00
if result := s . Scan ( & results ) ; result . Error != nil {
2020-01-15 04:04:33 +01:00
return results , result . Error
}
2021-10-01 00:05:49 +02:00
log . Infof ( "geo: found %s for %s [%s]" , english . Plural ( len ( results ) , "result" , "results" ) , f . SerializeAll ( ) , time . Since ( start ) )
2020-05-23 20:58:58 +02:00
2020-01-15 04:04:33 +01:00
return results , nil
}