This commit is contained in:
Keith Martin 2026-01-21 20:21:39 +00:00 committed by GitHub
commit 98a7965aee
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 656 additions and 262 deletions

View file

@ -0,0 +1,38 @@
package search
import (
"strings"
"github.com/photoprism/photoprism/pkg/geo"
"github.com/photoprism/photoprism/pkg/geo/s2"
)
// nearSQLCreator uses the near from frm.Near and dist from frm.Dist to generate the qs (Query String) and interface of values to allow Gorm to generate the required clause
func nearSQLCreator(near string, dist float64) (qs string, values []interface{}, err error) {
photos := []Photo{}
if err := Db().Model(&Photo{}).Where("photo_uid IN (?)", SplitOr(near)).Select("photo_uid, cell_id").Find(&photos).Error; err != nil {
log.Debugf("search: %s (find nearby)", err)
return qs, values, ErrNotFound
}
if len(photos) == 0 {
return qs, values, ErrNotFound
}
wheres := make([]string, len(photos))
for item, photo := range photos {
// Set the S2 Cell ID to search for.
s2Cell := photo.CellID
// Set the search distance if unspecified.
if dist <= 0 {
dist = geo.DefaultDist
}
wheres[item] = "photos.cell_id BETWEEN ? AND ?"
s2Min, s2Max := s2.PrefixedRange(s2Cell, s2.Level(dist))
values = append(values, s2Min)
values = append(values, s2Max)
}
return strings.Join(wheres, " OR "), values, nil
}

View file

@ -0,0 +1,67 @@
package search
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestNearSQLCreator(t *testing.T) {
t.Run("Empty", func(t *testing.T) {
if _, _, err := nearSQLCreator("", float64(0)); err != nil {
assert.Equal(t, ErrNotFound, err)
} else {
t.Fail()
}
})
t.Run("ps6sg6be2lvl0y24", func(t *testing.T) {
if qs, values, err := nearSQLCreator("ps6sg6be2lvl0y24", float64(0)); err != nil {
assert.Nil(t, err)
} else {
assert.Equal(t, 2, len(values))
assert.Equal(t, "photos.cell_id BETWEEN ? AND ?", qs)
assert.Equal(t, "s2:85d1ea400004", values[0])
assert.Equal(t, "s2:85d1ea800004", values[1])
}
})
t.Run("ps6sg6byk7wrbk30", func(t *testing.T) {
if qs, values, err := nearSQLCreator("ps6sg6byk7wrbk30", float64(0)); err != nil {
assert.Nil(t, err)
} else {
assert.Equal(t, 2, len(values))
assert.Equal(t, "photos.cell_id BETWEEN ? AND ?", qs)
assert.Equal(t, "s2:1ef75a400004", values[0])
assert.Equal(t, "s2:1ef75a800004", values[1])
}
})
t.Run("ps6sg6be2lvl0y24 pipe ps6sg6byk7wrbk30", func(t *testing.T) {
if qs, values, err := nearSQLCreator("ps6sg6be2lvl0y24|ps6sg6byk7wrbk30", float64(0)); err != nil {
assert.Nil(t, err)
} else {
assert.Equal(t, 4, len(values))
assert.Equal(t, "photos.cell_id BETWEEN ? AND ? OR photos.cell_id BETWEEN ? AND ?", qs)
assert.Equal(t, "s2:85d1ea400004", values[0])
assert.Equal(t, "s2:85d1ea800004", values[1])
assert.Equal(t, "s2:1ef75a400004", values[2])
assert.Equal(t, "s2:1ef75a800004", values[3])
}
})
t.Run("ps6sg6be2lvl0y24 pipe ps6sg6byk7wrtest", func(t *testing.T) {
if qs, values, err := nearSQLCreator("ps6sg6be2lvl0y24|ps6sg6byk7wrtest", float64(0)); err != nil {
assert.Nil(t, err)
} else {
assert.Equal(t, 2, len(values))
assert.Equal(t, "photos.cell_id BETWEEN ? AND ?", qs)
assert.Equal(t, "s2:85d1ea400004", values[0])
assert.Equal(t, "s2:85d1ea800004", values[1])
}
})
t.Run("ps6sg6be2lvltest pipe ps6sg6byk7wrtest", func(t *testing.T) {
if _, _, err := nearSQLCreator("ps6sg6be2lvltest|ps6sg6byk7wrtest", float64(0)); err != nil {
assert.Equal(t, ErrNotFound, err)
} else {
t.Fail()
}
})
}

View file

@ -59,20 +59,6 @@ func searchPhotos(frm form.SearchPhotos, sess *entity.Session, resultCols string
return PhotoResults{}, 0, ErrBadRequest
}
// Find photos near another?
if txt.NotEmpty(frm.Near) {
photo := Photo{}
// Find a nearby picture using the UID or return an empty result otherwise.
if err = Db().First(&photo, "photo_uid = ?", frm.Near).Error; err != nil {
log.Debugf("search: %s (find nearby)", err)
return PhotoResults{}, 0, ErrNotFound
}
// Set the S2 Cell ID to search for.
frm.S2 = photo.CellID
}
// Set default search distance.
if frm.Dist <= 0 {
frm.Dist = geo.DefaultDist
@ -288,26 +274,26 @@ func searchPhotos(frm form.SearchPhotos, sess *entity.Session, resultCols string
if txt.NotEmpty(frm.Label) {
var categories []entity.Category
var labels []entity.Label
var labelIds []uint
var labelIDs []uint
if labelErr := Db().Where(AnySlug("label_slug", frm.Label, txt.Or)).Or(AnySlug("custom_slug", frm.Label, txt.Or)).Find(&labels).Error; len(labels) == 0 || labelErr != nil {
log.Debugf("search: label %s not found", txt.LogParamLower(frm.Label))
return PhotoResults{}, 0, 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")
}
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.
@ -398,7 +384,7 @@ func searchPhotos(frm form.SearchPhotos, sess *entity.Session, resultCols string
if frm.Query != "" {
var categories []entity.Category
var labels []entity.Label
var labelIds []uint
var labelIDs []uint
if labelsErr := Db().Where(AnySlug("custom_slug", frm.Query, " ")).Find(&labels).Error; len(labels) == 0 || labelsErr != nil {
log.Tracef("search: label %s not found, using fuzzy search", txt.LogParamLower(frm.Query))
@ -408,24 +394,24 @@ func searchPhotos(frm form.SearchPhotos, sess *entity.Session, resultCols string
}
} else {
for _, l := range labels {
labelIds = append(labelIds, l.ID)
labelIDs = append(labelIDs, l.ID)
Db().Where("category_id = ?", l.ID).Find(&categories)
log.Tracef("search: label %s includes %d categories", txt.LogParamLower(l.LabelName), len(categories))
for _, category := range categories {
labelIds = append(labelIds, category.LabelID)
labelIDs = append(labelIDs, category.LabelID)
}
}
if wheres := LikeAnyKeyword("k.keyword", frm.Query); len(wheres) > 0 {
for _, where := range wheres {
s = s.Where("files.photo_id IN (SELECT pk.photo_id FROM keywords k JOIN photos_keywords pk ON k.id = pk.keyword_id WHERE (?)) OR "+
"files.photo_id IN (SELECT pl.photo_id FROM photos_labels pl WHERE pl.uncertainty < 100 AND pl.label_id IN (?))", gorm.Expr(where), labelIds)
"files.photo_id IN (SELECT pl.photo_id FROM photos_labels pl WHERE pl.uncertainty < 100 AND pl.label_id IN (?))", gorm.Expr(where), labelIDs)
}
} else {
s = s.Where("files.photo_id IN (SELECT pl.photo_id FROM photos_labels pl WHERE pl.uncertainty < 100 AND pl.label_id IN (?))", labelIds)
s = s.Where("files.photo_id IN (SELECT pl.photo_id FROM photos_labels pl WHERE pl.uncertainty < 100 AND pl.label_id IN (?))", labelIDs)
}
}
}
@ -751,8 +737,14 @@ func searchPhotos(frm form.SearchPhotos, sess *entity.Session, resultCols string
s = s.Where("files.file_hash IN (?)", SplitOr(strings.ToLower(frm.Hash)))
}
// Filter by location code.
if txt.NotEmpty(frm.S2) {
// Find photos near another?
if txt.NotEmpty(frm.Near) {
if qs, values, err := nearSQLCreator(frm.Near, frm.Dist); err != nil {
return PhotoResults{}, 0, ErrNotFound
} else {
s = s.Where(qs, values...)
}
} else if txt.NotEmpty(frm.S2) { // Filter by location code.
// S2 Cell ID.
s2Min, s2Max := s2.PrefixedRange(frm.S2, s2.Level(frm.Dist))
s = s.Where("photos.cell_id BETWEEN ? AND ?", s2Min, s2Max)

View file

@ -0,0 +1,398 @@
package search
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/photoprism/photoprism/internal/form"
)
func TestPhotosFilterNear(t *testing.T) {
t.Run("ps6sg6be2lvl0y24", func(t *testing.T) {
var f form.SearchPhotos
f.Near = "ps6sg6be2lvl0y24"
photos, _, err := Photos(f)
if err != nil {
t.Fatal(err)
}
assert.Equal(t, 13, len(photos))
})
t.Run("ps6sg6byk7wrbk30", func(t *testing.T) {
var f form.SearchPhotos
f.Near = "ps6sg6byk7wrbk30"
photos, _, err := Photos(f)
if err != nil {
t.Fatal(err)
}
assert.Equal(t, 29, len(photos))
})
t.Run("StartsWithPercent", func(t *testing.T) {
var f form.SearchPhotos
f.Near = "%gold"
_, _, err := Photos(f)
assert.Equal(t, err.Error(), "Not found")
})
t.Run("CenterPercent", func(t *testing.T) {
var f form.SearchPhotos
f.Near = "I love % dog"
_, _, err := Photos(f)
assert.Equal(t, err.Error(), "Not found")
})
t.Run("EndsWithPercent", func(t *testing.T) {
var f form.SearchPhotos
f.Near = "sale%"
_, _, err := Photos(f)
assert.Equal(t, err.Error(), "Not found")
})
t.Run("StartsWithAmpersand", func(t *testing.T) {
var f form.SearchPhotos
f.Near = "&IlikeFood"
_, _, err := Photos(f)
assert.Equal(t, err.Error(), "Not found")
})
t.Run("CenterAmpersand", func(t *testing.T) {
var f form.SearchPhotos
f.Near = "Pets & Dogs"
_, _, err := Photos(f)
assert.Equal(t, err.Error(), "Not found")
})
t.Run("EndsWithAmpersand", func(t *testing.T) {
var f form.SearchPhotos
f.Near = "Light&"
_, _, err := Photos(f)
assert.Equal(t, err.Error(), "Not found")
})
t.Run("StartsWithSingleQuote", func(t *testing.T) {
var f form.SearchPhotos
f.Near = "'Family"
_, _, err := Photos(f)
assert.Equal(t, err.Error(), "Not found")
})
t.Run("CenterSingleQuote", func(t *testing.T) {
var f form.SearchPhotos
f.Near = "Father's type"
_, _, err := Photos(f)
assert.Equal(t, err.Error(), "Not found")
})
t.Run("EndsWithSingleQuote", func(t *testing.T) {
var f form.SearchPhotos
f.Near = "Ice Cream'"
_, _, err := Photos(f)
assert.Equal(t, err.Error(), "Not found")
})
t.Run("StartsWithAsterisk", func(t *testing.T) {
var f form.SearchPhotos
f.Near = "*Forrest"
_, _, err := Photos(f)
assert.Equal(t, err.Error(), "Not found")
})
t.Run("CenterAsterisk", func(t *testing.T) {
var f form.SearchPhotos
f.Near = "My*Kids"
_, _, err := Photos(f)
assert.Equal(t, err.Error(), "Not found")
})
t.Run("EndsWithAsterisk", func(t *testing.T) {
var f form.SearchPhotos
f.Near = "Yoga***"
_, _, err := Photos(f)
assert.Equal(t, err.Error(), "Not found")
})
t.Run("StartsWithPipe", func(t *testing.T) {
var f form.SearchPhotos
f.Near = "|Banana"
_, _, err := Photos(f)
assert.Equal(t, err.Error(), "Not found")
})
t.Run("CenterPipe", func(t *testing.T) {
var f form.SearchPhotos
f.Near = "Red|Green"
_, _, err := Photos(f)
assert.Equal(t, err.Error(), "Not found")
})
t.Run("EndsWithPipe", func(t *testing.T) {
var f form.SearchPhotos
f.Near = "Blue|"
_, _, err := Photos(f)
assert.Equal(t, err.Error(), "Not found")
})
t.Run("StartsWithNumber", func(t *testing.T) {
var f form.SearchPhotos
f.Near = "345 Shirt"
_, _, err := Photos(f)
assert.Equal(t, err.Error(), "Not found")
})
t.Run("CenterNumber", func(t *testing.T) {
var f form.SearchPhotos
f.Near = "type555 Blue"
_, _, err := Photos(f)
assert.Equal(t, err.Error(), "Not found")
})
t.Run("EndsWithNumber", func(t *testing.T) {
var f form.SearchPhotos
f.Near = "Route 66"
_, _, err := Photos(f)
assert.Equal(t, err.Error(), "Not found")
})
}
func TestPhotosQueryNear(t *testing.T) {
t.Run("ps6sg6be2lvl0y24", func(t *testing.T) {
var f form.SearchPhotos
f.Query = "near:ps6sg6be2lvl0y24"
photos, _, err := Photos(f)
if err != nil {
t.Fatal(err)
}
assert.Equal(t, 13, len(photos))
})
t.Run("ps6sg6byk7wrbk30", func(t *testing.T) {
var f form.SearchPhotos
f.Query = "near:ps6sg6byk7wrbk30"
photos, _, err := Photos(f)
if err != nil {
t.Fatal(err)
}
assert.Equal(t, 29, len(photos))
})
t.Run("ps6sg6be2lvl0y24 pipe ps6sg6byk7wrbk30", func(t *testing.T) {
var f form.SearchPhotos
f.Query = "near:ps6sg6be2lvl0y24|ps6sg6byk7wrbk30"
photos, _, err := Photos(f)
if err != nil {
t.Fatal(err)
}
assert.Equal(t, 42, len(photos))
})
t.Run("StartsWithPercent", func(t *testing.T) {
var f form.SearchPhotos
f.Query = "near:\"%gold\""
_, _, err := Photos(f)
assert.Equal(t, err.Error(), "Not found")
})
t.Run("CenterPercent", func(t *testing.T) {
var f form.SearchPhotos
f.Query = "near:\"I love % dog\""
_, _, err := Photos(f)
assert.Equal(t, err.Error(), "Not found")
})
t.Run("EndsWithPercent", func(t *testing.T) {
var f form.SearchPhotos
f.Query = "near:\"sale%\""
_, _, err := Photos(f)
assert.Equal(t, err.Error(), "Not found")
})
t.Run("StartsWithAmpersand", func(t *testing.T) {
var f form.SearchPhotos
f.Query = "near:\"&IlikeFood\""
_, _, err := Photos(f)
assert.Equal(t, err.Error(), "Not found")
})
t.Run("CenterAmpersand", func(t *testing.T) {
var f form.SearchPhotos
f.Query = "near:\"Pets & Dogs\""
_, _, err := Photos(f)
assert.Equal(t, err.Error(), "Not found")
})
t.Run("EndsWithAmpersand", func(t *testing.T) {
var f form.SearchPhotos
f.Query = "near:\"Light&\""
_, _, err := Photos(f)
assert.Equal(t, err.Error(), "Not found")
})
t.Run("StartsWithSingleQuote", func(t *testing.T) {
var f form.SearchPhotos
f.Query = "near:\"'Family\""
_, _, err := Photos(f)
assert.Equal(t, err.Error(), "Not found")
})
t.Run("CenterSingleQuote", func(t *testing.T) {
var f form.SearchPhotos
f.Query = "near:\"Father's type\""
_, _, err := Photos(f)
assert.Equal(t, err.Error(), "Not found")
})
t.Run("EndsWithSingleQuote", func(t *testing.T) {
var f form.SearchPhotos
f.Query = "near:\"Ice Cream'\""
_, _, err := Photos(f)
assert.Equal(t, err.Error(), "Not found")
})
t.Run("StartsWithAsterisk", func(t *testing.T) {
var f form.SearchPhotos
f.Query = "near:\"*Forrest\""
_, _, err := Photos(f)
assert.Equal(t, err.Error(), "Not found")
})
t.Run("CenterAsterisk", func(t *testing.T) {
var f form.SearchPhotos
f.Query = "near:\"My*Kids\""
_, _, err := Photos(f)
assert.Equal(t, err.Error(), "Not found")
})
t.Run("EndsWithAsterisk", func(t *testing.T) {
var f form.SearchPhotos
f.Query = "near:\"Yoga***\""
_, _, err := Photos(f)
assert.Equal(t, err.Error(), "Not found")
})
t.Run("StartsWithPipe", func(t *testing.T) {
var f form.SearchPhotos
f.Query = "near:\"|Banana\""
_, _, err := Photos(f)
assert.Equal(t, err.Error(), "Not found")
})
t.Run("CenterPipe", func(t *testing.T) {
var f form.SearchPhotos
f.Query = "near:\"Red|Green\""
_, _, err := Photos(f)
assert.Equal(t, err.Error(), "Not found")
})
t.Run("EndsWithPipe", func(t *testing.T) {
var f form.SearchPhotos
f.Query = "near:\"Blue|\""
_, _, err := Photos(f)
assert.Equal(t, err.Error(), "Not found")
})
t.Run("StartsWithNumber", func(t *testing.T) {
var f form.SearchPhotos
f.Query = "near:\"345 Shirt\""
_, _, err := Photos(f)
assert.Equal(t, err.Error(), "Not found")
})
t.Run("CenterNumber", func(t *testing.T) {
var f form.SearchPhotos
f.Query = "near:\"type555 Blue\""
_, _, err := Photos(f)
assert.Equal(t, err.Error(), "Not found")
})
t.Run("EndsWithNumber", func(t *testing.T) {
var f form.SearchPhotos
f.Query = "near:\"Route 66\""
_, _, err := Photos(f)
assert.Equal(t, err.Error(), "Not found")
})
}

View file

@ -43,25 +43,6 @@ func UserPhotosGeo(frm form.SearchPhotosGeo, sess *entity.Session) (results GeoR
return GeoResults{}, ErrBadRequest
}
// Find photos near another?
if txt.NotEmpty(frm.Near) {
photo := Photo{}
// Find a nearby picture using the UID or return an empty result otherwise.
if err = Db().First(&photo, "photo_uid = ?", frm.Near).Error; err != nil {
log.Debugf("search: %s (find nearby)", err)
return GeoResults{}, ErrNotFound
}
// Set the S2 Cell ID to search for.
frm.S2 = photo.CellID
// Set the search distance if unspecified.
if frm.Dist <= 0 {
frm.Dist = geo.DefaultDist
}
}
// Set default search distance.
if frm.Dist <= 0 {
frm.Dist = geo.DefaultDist
@ -161,7 +142,20 @@ func UserPhotosGeo(frm form.SearchPhotosGeo, sess *entity.Session) (results GeoR
s = s.Order("taken_at, photos.photo_uid")
} else {
// Sort by distance to UID.
s = s.Order(gorm.Expr("(photos.photo_uid = ?) DESC, ABS(? - photos.photo_lat)+ABS(? - photos.photo_lng)", frm.Near, frm.Lat, frm.Lng))
sq := ""
var values []interface{}
for item, value := range SplitOr(frm.Near) {
if item == 0 {
sq = "(photos.photo_uid IN (?"
} else {
sq = sq + ", ?"
}
values = append(values, value)
}
sq = sq + ")) DESC, ABS(? - photos.photo_lat)+ABS(? - photos.photo_lng)"
values = append(values, frm.Lat)
values = append(values, frm.Lng)
s = s.Order(gorm.Expr(sq, values...))
}
// Find specific UIDs only.
@ -210,26 +204,27 @@ func UserPhotosGeo(frm form.SearchPhotosGeo, sess *entity.Session) (results GeoR
if txt.NotEmpty(frm.Label) {
var categories []entity.Category
var labels []entity.Label
var labelIds []uint
var labelIDs []uint
if labelErr := Db().Where(AnySlug("label_slug", frm.Label, txt.Or)).Or(AnySlug("custom_slug", frm.Label, txt.Or)).Find(&labels).Error; len(labels) == 0 || labelErr != nil {
log.Debugf("search: label %s not found", txt.LogParamLower(frm.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")
}
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.
@ -310,7 +305,7 @@ func UserPhotosGeo(frm form.SearchPhotosGeo, sess *entity.Session) (results GeoR
if frm.Query != "" {
var categories []entity.Category
var labels []entity.Label
var labelIds []uint
var labelIDs []uint
if labelsErr := Db().Where(AnySlug("custom_slug", frm.Query, " ")).Find(&labels).Error; len(labels) == 0 || labelsErr != nil {
log.Tracef("search: label %s not found, using fuzzy search", txt.LogParamLower(frm.Query))
@ -320,23 +315,23 @@ func UserPhotosGeo(frm form.SearchPhotosGeo, sess *entity.Session) (results GeoR
}
} else {
for _, l := range labels {
labelIds = append(labelIds, l.ID)
labelIDs = append(labelIDs, l.ID)
Log("find categories", Db().Where("category_id = ?", l.ID).Find(&categories).Error)
log.Tracef("search: label %s includes %d categories", txt.LogParamLower(l.LabelName), len(categories))
for _, category := range categories {
labelIds = append(labelIds, category.LabelID)
labelIDs = append(labelIDs, category.LabelID)
}
}
if wheres := LikeAnyKeyword("k.keyword", frm.Query); len(wheres) > 0 {
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)
"photos.id IN (SELECT pl.photo_id FROM photos_labels pl WHERE pl.uncertainty < 100 AND pl.label_id IN (?))", gorm.Expr(where), 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)
s = s.Where("photos.id IN (SELECT pl.photo_id FROM photos_labels pl WHERE pl.uncertainty < 100 AND pl.label_id IN (?))", labelIDs)
}
}
}
@ -639,8 +634,21 @@ func UserPhotosGeo(frm form.SearchPhotosGeo, sess *entity.Session) (results GeoR
s = s.Where("photos.photo_private = 1")
}
// Filter by location code.
if txt.NotEmpty(frm.S2) {
// Filter private pictures.
if frm.Public {
s = s.Where("photos.photo_private = 0")
} else if frm.Private {
s = s.Where("photos.photo_private = 1")
}
// Find photos near another?
if txt.NotEmpty(frm.Near) {
if qs, values, err := nearSQLCreator(frm.Near, frm.Dist); err != nil {
return GeoResults{}, ErrNotFound
} else {
s = s.Where(qs, values...)
}
} else if txt.NotEmpty(frm.S2) { // Filter by location code.
// S2 Cell ID.
s2Min, s2Max := s2.PrefixedRange(frm.S2, s2.Level(frm.Dist))
s = s.Where("photos.cell_id BETWEEN ? AND ?", s2Min, s2Max)

View file

@ -49,202 +49,150 @@ func TestPhotosGeoFilterNear(t *testing.T) {
assert.Equal(t, err.Error(), "Not found")
})
//TODO error
/*t.Run("EndsWithPercent", func(t *testing.T) {
t.Run("EndsWithPercent", func(t *testing.T) {
var f form.SearchPhotosGeo
f.Near = "sale%"
photos, err := PhotosGeo(f)
_, err := PhotosGeo(f)
if err != nil {
t.Fatal(err)
}
assert.Len(t, photos, 0)
assert.Equal(t, err.Error(), "Not found")
})
t.Run("StartsWithAmpersand", func(t *testing.T) {
var f form.SearchPhotosGeo
f.Near = "&IlikeFood"
photos, err := PhotosGeo(f)
_, err := PhotosGeo(f)
if err != nil {
t.Fatal(err)
}
assert.Len(t, photos, 0)
assert.Equal(t, err.Error(), "Not found")
})
t.Run("CenterAmpersand", func(t *testing.T) {
var f form.SearchPhotosGeo
f.Near = "Pets & Dogs"
photos, err := PhotosGeo(f)
_, err := PhotosGeo(f)
if err != nil {
t.Fatal(err)
}
assert.Len(t, photos, 0)
assert.Equal(t, err.Error(), "Not found")
})
t.Run("EndsWithAmpersand", func(t *testing.T) {
var f form.SearchPhotosGeo
f.Near = "Light&"
photos, err := PhotosGeo(f)
_, err := PhotosGeo(f)
if err != nil {
t.Fatal(err)
}
assert.Len(t, photos, 0)
assert.Equal(t, err.Error(), "Not found")
})
t.Run("StartsWithSingleQuote", func(t *testing.T) {
var f form.SearchPhotosGeo
f.Near = "'Family"
photos, err := PhotosGeo(f)
_, err := PhotosGeo(f)
if err != nil {
t.Fatal(err)
}
assert.Len(t, photos, 0)
assert.Equal(t, err.Error(), "Not found")
})
t.Run("CenterSingleQuote", func(t *testing.T) {
var f form.SearchPhotosGeo
f.Near = "Father's type"
photos, err := PhotosGeo(f)
_, err := PhotosGeo(f)
if err != nil {
t.Fatal(err)
}
assert.Len(t, photos, 0)
assert.Equal(t, err.Error(), "Not found")
})
t.Run("EndsWithSingleQuote", func(t *testing.T) {
var f form.SearchPhotosGeo
f.Near = "Ice Cream'"
photos, err := PhotosGeo(f)
_, err := PhotosGeo(f)
if err != nil {
t.Fatal(err)
}
assert.Len(t, photos, 0)
assert.Equal(t, err.Error(), "Not found")
})
t.Run("StartsWithAsterisk", func(t *testing.T) {
var f form.SearchPhotosGeo
f.Near = "*Forrest"
photos, err := PhotosGeo(f)
_, err := PhotosGeo(f)
if err != nil {
t.Fatal(err)
}
assert.Len(t, photos, 0)
assert.Equal(t, err.Error(), "Not found")
})
t.Run("CenterAsterisk", func(t *testing.T) {
var f form.SearchPhotosGeo
f.Near = "My*Kids"
photos, err := PhotosGeo(f)
_, err := PhotosGeo(f)
if err != nil {
t.Fatal(err)
}
assert.Len(t, photos, 0)
assert.Equal(t, err.Error(), "Not found")
})
t.Run("EndsWithAsterisk", func(t *testing.T) {
var f form.SearchPhotosGeo
f.Near = "Yoga***"
photos, err := PhotosGeo(f)
_, err := PhotosGeo(f)
if err != nil {
t.Fatal(err)
}
assert.Len(t, photos, 0)
assert.Equal(t, err.Error(), "Not found")
})
t.Run("StartsWithPipe", func(t *testing.T) {
var f form.SearchPhotosGeo
f.Near = "|Banana"
photos, err := PhotosGeo(f)
_, err := PhotosGeo(f)
if err != nil {
t.Fatal(err)
}
assert.Len(t, photos, 0)
assert.Equal(t, err.Error(), "Not found")
})
t.Run("CenterPipe", func(t *testing.T) {
var f form.SearchPhotosGeo
f.Near = "Red|Green"
photos, err := PhotosGeo(f)
_, err := PhotosGeo(f)
if err != nil {
t.Fatal(err)
}
assert.Len(t, photos, 0)
assert.Equal(t, err.Error(), "Not found")
})
t.Run("EndsWithPipe", func(t *testing.T) {
var f form.SearchPhotosGeo
f.Near = "Blue|"
photos, err := PhotosGeo(f)
_, err := PhotosGeo(f)
if err != nil {
t.Fatal(err)
}
assert.Len(t, photos, 0)
assert.Equal(t, err.Error(), "Not found")
})
t.Run("StartsWithNumber", func(t *testing.T) {
var f form.SearchPhotosGeo
f.Near = "345 Shirt"
photos, err := PhotosGeo(f)
_, err := PhotosGeo(f)
if err != nil {
t.Fatal(err)
}
assert.Len(t, photos, 0)
assert.Equal(t, err.Error(), "Not found")
})
t.Run("CenterNumber", func(t *testing.T) {
var f form.SearchPhotosGeo
f.Near = "type555 Blue"
photos, err := PhotosGeo(f)
_, err := PhotosGeo(f)
if err != nil {
t.Fatal(err)
}
assert.Len(t, photos, 0)
assert.Equal(t, err.Error(), "Not found")
})
t.Run("EndsWithNumber", func(t *testing.T) {
var f form.SearchPhotosGeo
f.Near = "Route 66"
photos, err := PhotosGeo(f)
_, err := PhotosGeo(f)
if err != nil {
t.Fatal(err)
}
assert.Len(t, photos, 0)
})*/
assert.Equal(t, err.Error(), "Not found")
})
}
func TestPhotosGeoQueryNear(t *testing.T) {
@ -273,8 +221,7 @@ func TestPhotosGeoQueryNear(t *testing.T) {
}
assert.Len(t, photos, 26)
})
//TODO error
/*t.Run("Ps6sg6be2lvl0y24PipePs6sg6byk7wrbk30", func(t *testing.T) {
t.Run("Ps6sg6be2lvl0y24PipePs6sg6byk7wrbk30", func(t *testing.T) {
var f form.SearchPhotosGeo
f.Query = "near:ps6sg6be2lvl0y24|ps6sg6byk7wrbk30"
@ -284,224 +231,168 @@ func TestPhotosGeoQueryNear(t *testing.T) {
if err != nil {
t.Fatal(err)
}
assert.Len(t, photos, 0)
assert.Len(t, photos, 35)
})
t.Run("StartsWithPercent", func(t *testing.T) {
var f form.SearchPhotosGeo
f.Query = "near:\"%gold\""
photos, err := PhotosGeo(f)
_, err := PhotosGeo(f)
if err != nil {
t.Fatal(err)
}
assert.Len(t, photos, 0)
assert.Equal(t, err.Error(), "Not found")
})
t.Run("CenterPercent", func(t *testing.T) {
var f form.SearchPhotosGeo
f.Query = "near:\"I love % dog\""
photos, err := PhotosGeo(f)
_, err := PhotosGeo(f)
if err != nil {
t.Fatal(err)
}
assert.Len(t, photos, 0)
assert.Equal(t, err.Error(), "Not found")
})
t.Run("EndsWithPercent", func(t *testing.T) {
var f form.SearchPhotosGeo
f.Query = "near:\"sale%\""
photos, err := PhotosGeo(f)
_, err := PhotosGeo(f)
if err != nil {
t.Fatal(err)
}
assert.Len(t, photos, 0)
assert.Equal(t, err.Error(), "Not found")
})
t.Run("StartsWithAmpersand", func(t *testing.T) {
var f form.SearchPhotosGeo
f.Query = "near:\"&IlikeFood\""
photos, err := PhotosGeo(f)
_, err := PhotosGeo(f)
if err != nil {
t.Fatal(err)
}
assert.Len(t, photos, 0)
assert.Equal(t, err.Error(), "Not found")
})
t.Run("CenterAmpersand", func(t *testing.T) {
var f form.SearchPhotosGeo
f.Query = "near:\"Pets & Dogs\""
photos, err := PhotosGeo(f)
_, err := PhotosGeo(f)
if err != nil {
t.Fatal(err)
}
assert.Len(t, photos, 0)
assert.Equal(t, err.Error(), "Not found")
})
t.Run("EndsWithAmpersand", func(t *testing.T) {
var f form.SearchPhotosGeo
f.Query = "near:\"Light&\""
photos, err := PhotosGeo(f)
_, err := PhotosGeo(f)
if err != nil {
t.Fatal(err)
}
assert.Len(t, photos, 0)
assert.Equal(t, err.Error(), "Not found")
})
t.Run("StartsWithSingleQuote", func(t *testing.T) {
var f form.SearchPhotosGeo
f.Query = "near:\"'Family\""
photos, err := PhotosGeo(f)
_, err := PhotosGeo(f)
if err != nil {
t.Fatal(err)
}
assert.Len(t, photos, 0)
assert.Equal(t, err.Error(), "Not found")
})
t.Run("CenterSingleQuote", func(t *testing.T) {
var f form.SearchPhotosGeo
f.Query = "near:\"Father's type\""
photos, err := PhotosGeo(f)
_, err := PhotosGeo(f)
if err != nil {
t.Fatal(err)
}
assert.Len(t, photos, 0)
assert.Equal(t, err.Error(), "Not found")
})
t.Run("EndsWithSingleQuote", func(t *testing.T) {
var f form.SearchPhotosGeo
f.Query = "near:\"Ice Cream'\""
photos, err := PhotosGeo(f)
_, err := PhotosGeo(f)
if err != nil {
t.Fatal(err)
}
assert.Len(t, photos, 0)
assert.Equal(t, err.Error(), "Not found")
})
t.Run("StartsWithAsterisk", func(t *testing.T) {
var f form.SearchPhotosGeo
f.Query = "near:\"*Forrest\""
photos, err := PhotosGeo(f)
_, err := PhotosGeo(f)
if err != nil {
t.Fatal(err)
}
assert.Len(t, photos, 0)
assert.Equal(t, err.Error(), "Not found")
})
t.Run("CenterAsterisk", func(t *testing.T) {
var f form.SearchPhotosGeo
f.Query = "near:\"My*Kids\""
photos, err := PhotosGeo(f)
_, err := PhotosGeo(f)
if err != nil {
t.Fatal(err)
}
assert.Len(t, photos, 0)
assert.Equal(t, err.Error(), "Not found")
})
t.Run("EndsWithAsterisk", func(t *testing.T) {
var f form.SearchPhotosGeo
f.Query = "near:\"Yoga***\""
photos, err := PhotosGeo(f)
_, err := PhotosGeo(f)
if err != nil {
t.Fatal(err)
}
assert.Len(t, photos, 0)
assert.Equal(t, err.Error(), "Not found")
})
t.Run("StartsWithPipe", func(t *testing.T) {
var f form.SearchPhotosGeo
f.Query = "near:\"|Banana\""
photos, err := PhotosGeo(f)
_, err := PhotosGeo(f)
if err != nil {
t.Fatal(err)
}
assert.Len(t, photos, 0)
assert.Equal(t, err.Error(), "Not found")
})
t.Run("CenterPipe", func(t *testing.T) {
var f form.SearchPhotosGeo
f.Query = "near:\"Red|Green\""
photos, err := PhotosGeo(f)
_, err := PhotosGeo(f)
if err != nil {
t.Fatal(err)
}
assert.Len(t, photos, 0)
assert.Equal(t, err.Error(), "Not found")
})
t.Run("EndsWithPipe", func(t *testing.T) {
var f form.SearchPhotosGeo
f.Query = "near:\"Blue|\""
photos, err := PhotosGeo(f)
_, err := PhotosGeo(f)
if err != nil {
t.Fatal(err)
}
assert.Len(t, photos, 0)
assert.Equal(t, err.Error(), "Not found")
})
t.Run("StartsWithNumber", func(t *testing.T) {
var f form.SearchPhotosGeo
f.Query = "near:\"345 Shirt\""
photos, err := PhotosGeo(f)
_, err := PhotosGeo(f)
if err != nil {
t.Fatal(err)
}
assert.Len(t, photos, 0)
assert.Equal(t, err.Error(), "Not found")
})
t.Run("CenterNumber", func(t *testing.T) {
var f form.SearchPhotosGeo
f.Query = "near:\"type555 Blue\""
photos, err := PhotosGeo(f)
_, err := PhotosGeo(f)
if err != nil {
t.Fatal(err)
}
assert.Len(t, photos, 0)
assert.Equal(t, err.Error(), "Not found")
})
t.Run("EndsWithNumber", func(t *testing.T) {
var f form.SearchPhotosGeo
f.Query = "near:\"Route 66\""
photos, err := PhotosGeo(f)
_, err := PhotosGeo(f)
if err != nil {
t.Fatal(err)
}
assert.Len(t, photos, 0)
})*/
assert.Equal(t, err.Error(), "Not found")
})
}

View file

@ -52,7 +52,7 @@ type SearchPhotos struct {
Hidden bool `form:"hidden" notes:"Finds hidden content (broken or unsupported)"`
Favorite string `form:"favorite" example:"favorite:true favorite:false" notes:"Finds favorite content"`
Unsorted bool `form:"unsorted" notes:"Finds content that is not in an album"`
Near string `form:"near" example:"near:pqbcf5j446s0futy" notes:"Finds nearby pictures (UID)"`
Near string `form:"near" example:"near:pqbcf5j446s0futy" notes:"Finds nearby pictures (UID), separated by |"`
S2 string `form:"s2" example:"s2:4799e370ca54c8b9" notes:"Position, specified as S2 Cell ID"`
Olc string `form:"olc" example:"olc:8FWCHX7W+" notes:"Open Location Code (OLC)"`
Lat float64 `form:"lat" example:"lat:41.894043" notes:"Position latitude (-90.0 to 90.0 deg)"`

View file

@ -52,7 +52,7 @@ type SearchPhotosGeo struct {
Quality int `form:"quality" notes:"Minimum quality score (1-7)"`
Face string `form:"face" notes:"Find pictures with a specific face ID, you can also specify yes, no, new, or a face type"`
Faces string `form:"faces" example:"faces:yes faces:3" notes:"Minimum number of detected faces (yes means 1)"` // Find or exclude faces if detected.
Near string `form:"near" example:"near:pqbcf5j446s0futy" notes:"Finds nearby pictures (UID)"`
Near string `form:"near" example:"near:pqbcf5j446s0futy" notes:"Finds nearby pictures (UID), separated by |"`
S2 string `form:"s2" example:"s2:4799e370ca54c8b9" notes:"Position, specified as S2 Cell ID"`
Olc string `form:"olc" example:"olc:8FWCHX7W+" notes:"Open Location Code (OLC)"`
Lat float64 `form:"lat" example:"lat:41.894043" notes:"Position latitude (-90.0 to 90.0 deg)"`