2020-05-08 15:41:01 +02:00
package query
import (
"fmt"
"strings"
"time"
2020-05-11 14:49:00 +02:00
"github.com/jinzhu/gorm"
2020-05-08 15:41:01 +02:00
"github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/form"
"github.com/photoprism/photoprism/pkg/txt"
)
2020-05-25 19:10:44 +02:00
// PhotoSearch searches for photos based on a Form and returns PhotoResults ([]PhotoResult).
func PhotoSearch ( f form . PhotoSearch ) ( results PhotoResults , count int , err error ) {
2020-05-23 20:58:58 +02:00
start := time . Now ( )
2020-05-08 15:41:01 +02:00
if err := f . ParseQueryString ( ) ; err != nil {
return results , 0 , err
}
s := UnscopedDb ( )
// s.LogMode(true)
2020-05-14 19:03:12 +02:00
// Main search query, avoids (slow) left joins.
2020-05-08 15:41:01 +02:00
s = s . Table ( "photos" ) .
Select ( ` photos . * ,
2020-08-18 00:45:53 +02:00
files . id AS file_id , files . file_uid , files . instance_id , files . file_primary , files . file_missing , files . file_name ,
2020-05-24 22:16:06 +02:00
files . file_root , files . file_hash , files . file_codec , files . file_type , files . file_mime , files . file_width ,
files . file_height , files . file_aspect_ratio , files . file_orientation , files . file_main_color ,
2020-07-16 13:02:48 +02:00
files . file_colors , files . file_luminance , files . file_chroma , files . file_projection ,
2020-05-15 09:39:32 +02:00
files . file_diff , files . file_video , files . file_duration , files . file_size ,
2020-05-08 15:41:01 +02:00
cameras . camera_make , cameras . camera_model ,
lenses . lens_make , lenses . lens_model ,
2020-07-12 08:27:05 +02:00
places . place_label , places . place_city , places . place_state , places . place_country ` ) .
2020-05-14 19:03:12 +02:00
Joins ( "JOIN files ON photos.id = files.photo_id AND files.file_missing = 0 AND files.deleted_at IS NULL" ) .
2020-05-14 15:05:59 +02:00
Joins ( "JOIN cameras ON photos.camera_id = cameras.id" ) .
Joins ( "JOIN lenses ON photos.lens_id = lenses.id" ) .
2020-06-04 17:06:42 +02:00
Joins ( "JOIN places ON photos.place_id = places.id" )
if ! f . Hidden {
s = s . Where ( "files.file_type = 'jpg' OR files.file_video = 1" )
if f . Error {
s = s . Where ( "files.file_error <> ''" )
} else {
s = s . Where ( "files.file_error = ''" )
}
}
2020-05-08 15:41:01 +02:00
2020-06-28 15:23:15 +02:00
// Return primary files only.
if f . Primary {
s = s . Where ( "files.file_primary = 1" )
}
2020-05-14 19:03:12 +02:00
// Shortcut for known photo ids.
2020-05-08 15:41:01 +02:00
if f . ID != "" {
2020-05-23 20:58:58 +02:00
s = s . Where ( "photos.photo_uid IN (?)" , strings . Split ( f . ID , "," ) )
2020-05-08 15:41:01 +02:00
s = s . Order ( "files.file_primary DESC" )
if result := s . Scan ( & results ) ; result . Error != nil {
return results , 0 , result . Error
}
2020-05-23 20:58:58 +02:00
log . Infof ( "photos: found %d results for %s [%s]" , len ( results ) , f . SerializeAll ( ) , time . Since ( start ) )
2020-05-08 15:41:01 +02:00
if f . Merged {
return results . Merged ( )
}
return results , len ( results ) , nil
}
2020-05-14 19:03:12 +02:00
// Filter by label, label category and keywords.
2020-05-08 15:41:01 +02:00
var categories [ ] entity . Category
2020-05-11 14:49:00 +02:00
var labels [ ] entity . Label
2020-05-08 15:41:01 +02:00
var labelIds [ ] uint
if f . Label != "" {
2020-05-29 18:04:30 +02:00
if err := Db ( ) . Where ( AnySlug ( "label_slug" , f . Label , "," ) ) . Or ( AnySlug ( "custom_slug" , f . Label , "," ) ) . Find ( & labels ) . Error ; len ( labels ) == 0 || err != nil {
log . Errorf ( "search: labels %s not found" , txt . Quote ( f . Label ) )
return results , 0 , fmt . Errorf ( "%s not found" , txt . Quote ( f . Label ) )
2020-05-08 15:41:01 +02:00
} else {
2020-05-29 18:04:30 +02:00
for _ , l := range labels {
labelIds = append ( labelIds , l . ID )
Db ( ) . Where ( "category_id = ?" , l . ID ) . Find ( & categories )
2020-05-08 15:41:01 +02:00
2020-05-29 18:04:30 +02:00
log . Infof ( "search: label %s includes %d categories" , txt . Quote ( l . LabelName ) , len ( categories ) )
2020-05-08 15:41:01 +02:00
2020-05-29 18:04:30 +02:00
for _ , category := range categories {
labelIds = append ( labelIds , category . LabelID )
}
2020-05-08 15:41:01 +02:00
}
2020-05-29 18:04:30 +02:00
s = s . Joins ( "JOIN photos_labels ON photos_labels.photo_id = photos.id AND photos_labels.uncertainty < 100 AND photos_labels.label_id IN (?)" , labelIds ) .
Group ( "photos.id, files.id" )
2020-05-08 15:41:01 +02:00
}
}
2020-05-14 19:03:12 +02:00
// Filter by location.
2020-07-11 23:43:29 +02:00
if f . Geo == true {
2020-07-12 08:27:05 +02:00
s = s . Where ( "photos.cell_id <> 'zz'" )
2020-05-08 15:41:01 +02:00
2020-05-11 14:49:00 +02:00
if likeAny := LikeAny ( "k.keyword" , f . Query ) ; likeAny != "" {
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 ( likeAny ) )
2020-05-08 15:41:01 +02:00
}
} else if f . Query != "" {
if len ( f . Query ) < 2 {
return results , 0 , fmt . Errorf ( "query too short" )
}
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 {
2020-05-08 15:41:01 +02:00
log . Infof ( "search: label %s not found, using fuzzy search" , txt . Quote ( f . Query ) )
2020-05-11 14:49:00 +02:00
if likeAny := LikeAny ( "k.keyword" , f . Query ) ; likeAny != "" {
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 ( likeAny ) )
}
2020-05-08 15:41:01 +02:00
} else {
2020-05-11 14:49:00 +02:00
for _ , l := range labels {
labelIds = append ( labelIds , l . ID )
2020-05-08 15:41:01 +02:00
2020-05-11 14:49:00 +02:00
Db ( ) . Where ( "category_id = ?" , l . ID ) . Find ( & categories )
2020-05-08 15:41:01 +02:00
2020-05-11 14:49:00 +02:00
log . Infof ( "search: label %s includes %d categories" , txt . Quote ( l . LabelName ) , len ( categories ) )
2020-05-08 15:41:01 +02:00
2020-05-11 14:49:00 +02:00
for _ , category := range categories {
labelIds = append ( labelIds , category . LabelID )
}
}
2020-05-08 15:41:01 +02:00
2020-05-11 14:49:00 +02:00
if likeAny := LikeAny ( "k.keyword" , f . Query ) ; likeAny != "" {
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 ( likeAny ) , labelIds )
} 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 )
}
2020-05-08 15:41:01 +02:00
}
}
2020-05-14 19:03:12 +02:00
// Filter by status.
2020-06-04 14:56:27 +02:00
if f . Hidden {
s = s . Where ( "photos.photo_quality = -1" )
s = s . Where ( "photos.deleted_at IS NULL" )
} else if f . Archived {
2020-05-08 15:41:01 +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-05-14 19:03:12 +02:00
// Filter by additional flags and metadata.
2020-05-08 15:41:01 +02:00
if f . Camera > 0 {
s = s . Where ( "photos.camera_id = ?" , f . Camera )
}
if f . Lens > 0 {
s = s . Where ( "photos.lens_id = ?" , f . Lens )
}
2020-05-21 13:26:28 +02:00
if ( f . Year > 0 && f . Year <= txt . YearMax ) || f . Year == entity . YearUnknown {
2020-05-08 15:41:01 +02:00
s = s . Where ( "photos.photo_year = ?" , f . Year )
}
2020-05-21 13:26:28 +02:00
if ( f . Month >= txt . MonthMin && f . Month <= txt . MonthMax ) || f . Month == entity . MonthUnknown {
2020-05-08 15:41:01 +02:00
s = s . Where ( "photos.photo_month = ?" , f . Month )
}
2020-07-06 07:41:33 +02:00
if ( f . Day >= txt . DayMin && f . Month <= txt . DayMax ) || f . Day == entity . DayUnknown {
s = s . Where ( "photos.photo_day = ?" , f . Day )
}
2020-05-08 15:41:01 +02:00
if f . Color != "" {
2020-05-21 13:26:28 +02:00
s = s . Where ( "files.file_main_color IN (?)" , strings . Split ( strings . ToLower ( f . Color ) , "," ) )
2020-05-08 15:41:01 +02:00
}
2020-05-14 19:03:12 +02:00
if f . Favorite {
2020-05-08 15:41:01 +02:00
s = s . Where ( "photos.photo_favorite = 1" )
}
2020-07-06 14:35:25 +02:00
if f . Scan {
s = s . Where ( "photos.photo_scan = 1" )
2020-07-05 14:48:49 +02:00
}
2020-07-16 13:02:48 +02:00
if f . Panorama {
s = s . Where ( "photos.photo_panorama = 1" )
}
2020-05-08 15:41:01 +02:00
if f . Country != "" {
2020-05-21 13:26:28 +02:00
s = s . Where ( "photos.photo_country IN (?)" , strings . Split ( strings . ToLower ( f . Country ) , "," ) )
2020-05-08 15:41:01 +02:00
}
2020-05-29 18:04:30 +02:00
if f . State != "" {
2020-07-12 08:27:05 +02:00
s = s . Where ( "places.place_state IN (?)" , strings . Split ( f . State , "," ) )
2020-05-29 18:04:30 +02:00
}
if f . Category != "" {
2020-07-12 08:27:05 +02:00
s = s . Joins ( "JOIN cells ON photos.cell_id = cells.id" ) .
Where ( "cells.cell_category IN (?)" , strings . Split ( strings . ToLower ( f . Category ) , "," ) )
2020-05-29 18:04:30 +02:00
}
2020-05-21 13:26:28 +02:00
// Filter by media type.
2020-05-21 10:03:56 +02:00
if f . Type != "" {
2020-05-21 13:26:28 +02:00
s = s . Where ( "photos.photo_type IN (?)" , strings . Split ( strings . ToLower ( f . Type ) , "," ) )
}
if f . Video {
s = s . Where ( "photos.photo_type = 'video'" )
} else if f . Photo {
s = s . Where ( "photos.photo_type IN ('image','raw','live')" )
2020-05-21 10:03:56 +02:00
}
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 ] )
2020-05-21 13:26:28 +02:00
} else if strings . Contains ( p , "," ) {
s = s . Where ( "photos.photo_path IN (?)" , strings . Split ( p , "," ) )
2020-05-21 10:03:56 +02:00
} else {
s = s . Where ( "photos.photo_path LIKE ?" , strings . ReplaceAll ( p , "*" , "%" ) )
}
}
if f . Name != "" {
s = s . Where ( "photos.photo_name LIKE ?" , strings . ReplaceAll ( f . Name , "*" , "%" ) )
}
2020-06-03 10:33:09 +02:00
if f . Original != "" {
s = s . Where ( "photos.original_name LIKE ?" , strings . ReplaceAll ( f . Original , "*" , "%" ) )
}
2020-05-08 15:41:01 +02:00
if f . Title != "" {
2020-05-21 10:03:56 +02:00
s = s . Where ( "LOWER(photos.photo_title) LIKE ?" , strings . ReplaceAll ( strings . ToLower ( f . Title ) , "*" , "%" ) )
2020-05-08 15:41:01 +02:00
}
if f . Hash != "" {
2020-05-21 13:26:28 +02:00
s = s . Where ( "files.file_hash IN (?)" , strings . Split ( strings . ToLower ( f . Hash ) , "," ) )
2020-05-08 15:41:01 +02:00
}
if f . Portrait {
s = s . Where ( "files.file_portrait = 1" )
}
if f . Mono {
s = s . Where ( "files.file_chroma = 0" )
} else if f . Chroma > 9 {
s = s . Where ( "files.file_chroma > ?" , f . Chroma )
} else if f . Chroma > 0 {
s = s . Where ( "files.file_chroma > 0 AND files.file_chroma <= ?" , f . Chroma )
}
if f . Diff != 0 {
s = s . Where ( "files.file_diff = ?" , f . Diff )
}
if f . Fmin > 0 {
s = s . Where ( "photos.photo_f_number >= ?" , f . Fmin )
}
if f . Fmax > 0 {
s = s . Where ( "photos.photo_f_number <= ?" , f . Fmax )
}
if f . Dist == 0 {
f . Dist = 20
} else if f . Dist > 5000 {
f . Dist = 5000
}
2020-05-14 19:03:12 +02:00
// Filter by distance (approximation).
2020-05-08 15:41:01 +02:00
if f . Lat > 0 {
latMin := f . Lat - SearchRadius * float32 ( f . Dist )
latMax := f . Lat + SearchRadius * float32 ( f . Dist )
s = s . Where ( "photos.photo_lat BETWEEN ? AND ?" , latMin , latMax )
}
if f . Lng > 0 {
lngMin := f . Lng - SearchRadius * float32 ( f . Dist )
lngMax := f . Lng + SearchRadius * float32 ( f . Dist )
s = s . Where ( "photos.photo_lng BETWEEN ? AND ?" , lngMin , lngMax )
}
if ! f . Before . IsZero ( ) {
s = s . Where ( "photos.taken_at <= ?" , f . Before . Format ( "2006-01-02" ) )
}
if ! f . After . IsZero ( ) {
s = s . Where ( "photos.taken_at >= ?" , f . After . Format ( "2006-01-02" ) )
}
2020-07-13 15:59:54 +02:00
if f . Stack {
2020-07-05 17:22:26 +02:00
s = s . Where ( "photos.id IN (SELECT a.photo_id FROM files a JOIN files b ON a.id != b.id AND a.photo_id = b.photo_id AND a.file_type = b.file_type WHERE a.file_type='jpg')" )
}
2020-05-30 01:41:47 +02:00
if f . Album != "" {
if f . Filter != "" {
s = s . Where ( "photos.photo_uid NOT IN (SELECT photo_uid FROM photos_albums pa WHERE pa.hidden = 1 AND pa.album_uid = ?)" , f . Album )
} else {
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 )
}
2020-07-05 14:48:49 +02:00
} else if f . Unsorted && f . Filter == "" {
s = s . Where ( "photos.photo_uid NOT IN (SELECT photo_uid FROM photos_albums pa WHERE pa.hidden = 0)" )
2020-05-30 01:41:47 +02:00
}
2020-05-14 19:03:12 +02:00
// Set sort order for results.
2020-05-08 15:41:01 +02:00
switch f . Order {
2020-07-14 12:42:31 +02:00
case entity . SortOrderEdited :
s = s . Where ( "edited_at IS NOT NULL" ) . Order ( "edited_at DESC, photos.photo_uid, files.file_primary DESC" )
2020-05-08 15:41:01 +02:00
case entity . SortOrderRelevance :
if f . Label != "" {
s = s . Order ( "photo_quality DESC, photos_labels.uncertainty ASC, taken_at DESC, files.file_primary DESC" )
} else {
s = s . Order ( "photo_quality DESC, taken_at DESC, files.file_primary DESC" )
}
case entity . SortOrderNewest :
2020-05-23 20:58:58 +02:00
s = s . Order ( "taken_at DESC, photos.photo_uid, files.file_primary DESC" )
2020-05-08 15:41:01 +02:00
case entity . SortOrderOldest :
2020-05-23 20:58:58 +02:00
s = s . Order ( "taken_at, photos.photo_uid, files.file_primary DESC" )
2020-06-30 11:27:02 +02:00
case entity . SortOrderAdded :
2020-05-08 15:41:01 +02:00
s = s . Order ( "photos.id DESC, files.file_primary DESC" )
case entity . SortOrderSimilar :
2020-05-16 17:07:44 +02:00
s = s . Where ( "files.file_diff > 0" )
2020-07-12 08:27:05 +02:00
s = s . Order ( "files.file_main_color, photos.cell_id, files.file_diff, taken_at DESC, files.file_primary DESC" )
2020-05-22 19:05:16 +02:00
case entity . SortOrderName :
s = s . Order ( "photos.photo_path, photos.photo_name, files.file_primary DESC" )
2020-05-08 15:41:01 +02:00
default :
2020-05-23 20:58:58 +02:00
s = s . Order ( "taken_at DESC, photos.photo_uid, files.file_primary DESC" )
2020-05-08 15:41:01 +02:00
}
2020-07-01 09:49:03 +02:00
if f . Count > 0 && f . Count <= MaxResults {
2020-05-08 15:41:01 +02:00
s = s . Limit ( f . Count ) . Offset ( f . Offset )
} else {
2020-07-01 09:49:03 +02:00
s = s . Limit ( MaxResults ) . Offset ( f . Offset )
2020-05-08 15:41:01 +02:00
}
2020-05-30 14:52:47 +02:00
if err := s . Scan ( & results ) . Error ; err != nil {
return results , 0 , err
2020-05-08 15:41:01 +02:00
}
2020-05-23 20:58:58 +02:00
log . Infof ( "photos: found %d results for %s [%s]" , len ( results ) , f . SerializeAll ( ) , time . Since ( start ) )
2020-05-08 15:41:01 +02:00
if f . Merged {
return results . Merged ( )
}
return results , len ( results ) , nil
}