parent
a0f49f2d56
commit
11d1034752
21 changed files with 525 additions and 222 deletions
|
@ -10,6 +10,8 @@ import (
|
|||
"github.com/photoprism/photoprism/internal/query"
|
||||
)
|
||||
|
||||
// GetPhotos searches the pictures index and returns the result as JSON.
|
||||
//
|
||||
// GET /api/v1/photos
|
||||
//
|
||||
// Query:
|
||||
|
|
|
@ -34,7 +34,7 @@ type Album struct {
|
|||
AlbumSlug string `gorm:"type:VARBINARY(255);index;" json:"Slug" yaml:"Slug"`
|
||||
AlbumPath string `gorm:"type:VARBINARY(500);index;" json:"Path" yaml:"-"`
|
||||
AlbumType string `gorm:"type:VARBINARY(8);default:'album';" json:"Type" yaml:"Type,omitempty"`
|
||||
AlbumTitle string `gorm:"type:VARCHAR(255);" json:"Title" yaml:"Title"`
|
||||
AlbumTitle string `gorm:"type:VARCHAR(255);index;" json:"Title" yaml:"Title"`
|
||||
AlbumLocation string `gorm:"type:VARCHAR(255);" json:"Location" yaml:"Location,omitempty"`
|
||||
AlbumCategory string `gorm:"type:VARCHAR(255);index;" json:"Category" yaml:"Category,omitempty"`
|
||||
AlbumCaption string `gorm:"type:TEXT;" json:"Caption" yaml:"Caption,omitempty"`
|
||||
|
|
|
@ -22,7 +22,7 @@ var MarkerFixtures = MarkerMap{
|
|||
"1000003-1": Marker{
|
||||
ID: 1,
|
||||
FileID: 1000003,
|
||||
SubjectUID: "lt9k3pw1wowuy3c3",
|
||||
SubjectUID: "jqu0xs11qekk9jx8",
|
||||
MarkerSrc: SrcImage,
|
||||
MarkerType: MarkerLabel,
|
||||
X: 0.308333,
|
||||
|
@ -35,7 +35,7 @@ var MarkerFixtures = MarkerMap{
|
|||
"1000003-2": Marker{
|
||||
ID: 2,
|
||||
FileID: 1000003,
|
||||
SubjectUID: "",
|
||||
SubjectUID: "lt9k3pw1wowuy3c3",
|
||||
FaceID: "LRG2HJBDZE66LYG7Q5SRFXO2MDTOES52",
|
||||
MarkerName: "Unknown",
|
||||
MarkerSrc: SrcImage,
|
||||
|
|
|
@ -37,7 +37,7 @@ func TestAccountSearch_ParseQueryString(t *testing.T) {
|
|||
t.FailNow()
|
||||
}
|
||||
|
||||
log.Debugf("%+v\n", form)
|
||||
// log.Debugf("%+v\n", form)
|
||||
|
||||
assert.Equal(t, "webdäv", form.Query)
|
||||
assert.Equal(t, true, form.Share)
|
||||
|
@ -54,7 +54,7 @@ func TestAccountSearch_ParseQueryString(t *testing.T) {
|
|||
t.FailNow()
|
||||
}
|
||||
|
||||
log.Debugf("%+v\n", form)
|
||||
// log.Debugf("%+v\n", form)
|
||||
|
||||
assert.Equal(t, "unknown filter: Xxx", err.Error())
|
||||
})
|
||||
|
@ -78,7 +78,7 @@ func TestAccountSearch_ParseQueryString(t *testing.T) {
|
|||
t.FailNow()
|
||||
}
|
||||
|
||||
log.Debugf("%+v\n", form)
|
||||
// log.Debugf("%+v\n", form)
|
||||
|
||||
assert.Equal(t, "strconv.Atoi: parsing \"cat\": invalid syntax", err.Error())
|
||||
})
|
||||
|
|
|
@ -13,13 +13,12 @@ func TestAlbumSearchForm(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestParseQueryStringAlbum(t *testing.T) {
|
||||
|
||||
t.Run("valid query", func(t *testing.T) {
|
||||
form := &AlbumSearch{Query: "slug:album1 favorite:true count:10"}
|
||||
|
||||
err := form.ParseQueryString()
|
||||
|
||||
log.Debugf("%+v\n", form)
|
||||
// log.Debugf("%+v\n", form)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal("err should be nil")
|
||||
|
@ -34,7 +33,7 @@ func TestParseQueryStringAlbum(t *testing.T) {
|
|||
|
||||
err := form.ParseQueryString()
|
||||
|
||||
log.Debugf("%+v\n", form)
|
||||
// log.Debugf("%+v\n", form)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal("err should be nil")
|
||||
|
@ -51,7 +50,7 @@ func TestParseQueryStringAlbum(t *testing.T) {
|
|||
|
||||
err := form.ParseQueryString()
|
||||
|
||||
log.Debugf("%+v\n", form)
|
||||
// log.Debugf("%+v\n", form)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal("err should be nil")
|
||||
|
@ -68,7 +67,7 @@ func TestParseQueryStringAlbum(t *testing.T) {
|
|||
t.FailNow()
|
||||
}
|
||||
|
||||
log.Debugf("%+v\n", form)
|
||||
// log.Debugf("%+v\n", form)
|
||||
|
||||
assert.Equal(t, "unknown filter: Xxx", err.Error())
|
||||
})
|
||||
|
@ -92,7 +91,7 @@ func TestParseQueryStringAlbum(t *testing.T) {
|
|||
t.FailNow()
|
||||
}
|
||||
|
||||
log.Debugf("%+v\n", form)
|
||||
// log.Debugf("%+v\n", form)
|
||||
|
||||
assert.Equal(t, "strconv.Atoi: parsing \"cat\": invalid syntax", err.Error())
|
||||
})
|
||||
|
|
|
@ -44,7 +44,7 @@ func TestParseQueryStringFolder(t *testing.T) {
|
|||
|
||||
err := form.ParseQueryString()
|
||||
|
||||
log.Debugf("%+v\n", form)
|
||||
// log.Debugf("%+v\n", form)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal("err should be nil")
|
||||
|
@ -62,7 +62,7 @@ func TestParseQueryStringFolder(t *testing.T) {
|
|||
|
||||
err := form.ParseQueryString()
|
||||
|
||||
log.Debugf("%+v\n", form)
|
||||
// log.Debugf("%+v\n", form)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal("err should be nil")
|
||||
|
@ -79,7 +79,7 @@ func TestParseQueryStringFolder(t *testing.T) {
|
|||
t.FailNow()
|
||||
}
|
||||
|
||||
log.Debugf("%+v\n", form)
|
||||
// log.Debugf("%+v\n", form)
|
||||
|
||||
assert.Equal(t, "unknown filter: Xxx", err.Error())
|
||||
})
|
||||
|
@ -103,7 +103,7 @@ func TestParseQueryStringFolder(t *testing.T) {
|
|||
t.FailNow()
|
||||
}
|
||||
|
||||
log.Debugf("%+v\n", form)
|
||||
// log.Debugf("%+v\n", form)
|
||||
|
||||
assert.Equal(t, "strconv.Atoi: parsing \"cat\": invalid syntax", err.Error())
|
||||
})
|
||||
|
|
|
@ -25,7 +25,12 @@ type GeoSearch struct {
|
|||
S2 string `form:"s2"`
|
||||
Olc string `form:"olc"`
|
||||
Dist uint `form:"dist"`
|
||||
Subject string `form:"subject"` // UIDs
|
||||
Subjects string `form:"subjects"` // Text
|
||||
People string `form:"people"` // Alias for Subjects
|
||||
Keywords string `form:"keywords"`
|
||||
Album string `form:"album"`
|
||||
Albums string `form:"albums"`
|
||||
Country string `form:"country"`
|
||||
Year int `form:"year"` // Moments
|
||||
Month int `form:"month"` // Moments
|
||||
|
@ -53,6 +58,10 @@ func (f *GeoSearch) ParseQueryString() error {
|
|||
f.Path = f.Folder
|
||||
}
|
||||
|
||||
if f.Subjects == "" {
|
||||
f.Subjects = f.People
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
|
@ -8,6 +8,28 @@ import (
|
|||
)
|
||||
|
||||
func TestGeoSearch(t *testing.T) {
|
||||
t.Run("subjects", func(t *testing.T) {
|
||||
form := &GeoSearch{Query: "subjects:\"Jens Mander\""}
|
||||
|
||||
err := form.ParseQueryString()
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.Equal(t, "Jens Mander", form.Subjects)
|
||||
})
|
||||
t.Run("keywords", func(t *testing.T) {
|
||||
form := &GeoSearch{Query: "keywords:\"Foo Bar\""}
|
||||
|
||||
err := form.ParseQueryString()
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.Equal(t, "Foo Bar", form.Keywords)
|
||||
})
|
||||
t.Run("valid query", func(t *testing.T) {
|
||||
form := &GeoSearch{Query: "query:\"fooBar baz\" before:2019-01-15 dist:25000 lat:33.45343166666667"}
|
||||
|
||||
|
@ -17,7 +39,7 @@ func TestGeoSearch(t *testing.T) {
|
|||
t.Fatal("err should be nil")
|
||||
}
|
||||
|
||||
log.Debugf("%+v\n", form)
|
||||
// log.Debugf("%+v\n", form)
|
||||
|
||||
assert.Equal(t, "fooBar baz", form.Query)
|
||||
assert.Equal(t, time.Date(2019, 01, 15, 0, 0, 0, 0, time.UTC), form.Before)
|
||||
|
@ -33,7 +55,7 @@ func TestGeoSearch(t *testing.T) {
|
|||
t.Fatal("err should be nil")
|
||||
}
|
||||
|
||||
log.Debugf("%+v\n", form)
|
||||
// log.Debugf("%+v\n", form)
|
||||
|
||||
assert.Equal(t, "fooBar baz", form.Query)
|
||||
assert.Equal(t, "test", form.Path)
|
||||
|
|
|
@ -19,7 +19,7 @@ func TestParseQueryStringLabel(t *testing.T) {
|
|||
|
||||
err := form.ParseQueryString()
|
||||
|
||||
log.Debugf("%+v\n", form)
|
||||
// log.Debugf("%+v\n", form)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal("err should be nil")
|
||||
|
@ -35,7 +35,7 @@ func TestParseQueryStringLabel(t *testing.T) {
|
|||
|
||||
err := form.ParseQueryString()
|
||||
|
||||
log.Debugf("%+v\n", form)
|
||||
// log.Debugf("%+v\n", form)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal("err should be nil")
|
||||
|
@ -50,7 +50,7 @@ func TestParseQueryStringLabel(t *testing.T) {
|
|||
|
||||
err := form.ParseQueryString()
|
||||
|
||||
log.Debugf("%+v\n", form)
|
||||
// log.Debugf("%+v\n", form)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal("err should be nil")
|
||||
|
@ -67,7 +67,7 @@ func TestParseQueryStringLabel(t *testing.T) {
|
|||
t.Fatal("err should NOT be nil")
|
||||
}
|
||||
|
||||
log.Debugf("%+v\n", form)
|
||||
// log.Debugf("%+v\n", form)
|
||||
|
||||
assert.Equal(t, "unknown filter: Xxx", err.Error())
|
||||
})
|
||||
|
@ -91,7 +91,7 @@ func TestParseQueryStringLabel(t *testing.T) {
|
|||
t.Fatal("err should NOT be nil")
|
||||
}
|
||||
|
||||
log.Debugf("%+v\n", form)
|
||||
// log.Debugf("%+v\n", form)
|
||||
|
||||
assert.Equal(t, "strconv.Atoi: parsing \"2019-01-15\": invalid syntax", err.Error())
|
||||
})
|
||||
|
|
|
@ -42,7 +42,12 @@ type PhotoSearch struct {
|
|||
Mono bool `form:"mono"`
|
||||
Portrait bool `form:"portrait"`
|
||||
Geo bool `form:"geo"`
|
||||
Album string `form:"album"`
|
||||
Subject string `form:"subject"` // UIDs
|
||||
Subjects string `form:"subjects"` // Text
|
||||
People string `form:"people"` // Alias for Subjects
|
||||
Keywords string `form:"keywords"`
|
||||
Album string `form:"album"` // UIDs
|
||||
Albums string `form:"albums"` // Text
|
||||
Label string `form:"label"`
|
||||
Category string `form:"category"` // Moments
|
||||
Country string `form:"country"` // Moments
|
||||
|
@ -81,6 +86,10 @@ func (f *PhotoSearch) ParseQueryString() error {
|
|||
f.Path = f.Folder
|
||||
}
|
||||
|
||||
if f.Subjects == "" {
|
||||
f.Subjects = f.People
|
||||
}
|
||||
|
||||
if f.Filter != "" {
|
||||
if err := Unserialize(f, f.Filter); err != nil {
|
||||
return err
|
||||
|
|
|
@ -14,12 +14,47 @@ func TestPhotoSearchForm(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestParseQueryString(t *testing.T) {
|
||||
t.Run("subjects", func(t *testing.T) {
|
||||
form := &PhotoSearch{Query: "subjects:\"Jens & Mander\""}
|
||||
|
||||
err := form.ParseQueryString()
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.Equal(t, "Jens & Mander", form.Subjects)
|
||||
})
|
||||
t.Run("keywords", func(t *testing.T) {
|
||||
form := &PhotoSearch{Query: "keywords:\"Foo Bar\""}
|
||||
|
||||
err := form.ParseQueryString()
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.Equal(t, "Foo Bar", form.Keywords)
|
||||
})
|
||||
t.Run("and query", func(t *testing.T) {
|
||||
form := &PhotoSearch{Query: "\"Jens & Mander\" title:\"Tübingen\""}
|
||||
|
||||
err := form.ParseQueryString()
|
||||
|
||||
// log.Debugf("%+v\n", form)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.Equal(t, "jens & mander", form.GetQuery())
|
||||
assert.Equal(t, "Tübingen", form.Title)
|
||||
})
|
||||
t.Run("path", func(t *testing.T) {
|
||||
form := &PhotoSearch{Query: "path:123abc/,EFG"}
|
||||
|
||||
err := form.ParseQueryString()
|
||||
|
||||
log.Debugf("%+v\n", form)
|
||||
// log.Debugf("%+v\n", form)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -33,7 +68,7 @@ func TestParseQueryString(t *testing.T) {
|
|||
|
||||
err := form.ParseQueryString()
|
||||
|
||||
log.Debugf("%+v\n", form)
|
||||
// log.Debugf("%+v\n", form)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -46,7 +81,7 @@ func TestParseQueryString(t *testing.T) {
|
|||
|
||||
err := form.ParseQueryString()
|
||||
|
||||
log.Debugf("%+v\n", form)
|
||||
// log.Debugf("%+v\n", form)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -65,7 +100,7 @@ func TestParseQueryString(t *testing.T) {
|
|||
|
||||
err := form.ParseQueryString()
|
||||
|
||||
log.Debugf("%+v\n", form)
|
||||
// log.Debugf("%+v\n", form)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal("err should be nil")
|
||||
|
@ -81,7 +116,7 @@ func TestParseQueryString(t *testing.T) {
|
|||
|
||||
err := form.ParseQueryString()
|
||||
|
||||
log.Debugf("%+v\n", form)
|
||||
// log.Debugf("%+v\n", form)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -96,7 +131,7 @@ func TestParseQueryString(t *testing.T) {
|
|||
|
||||
err := form.ParseQueryString()
|
||||
|
||||
log.Debugf("%+v\n", form)
|
||||
// log.Debugf("%+v\n", form)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -113,7 +148,7 @@ func TestParseQueryString(t *testing.T) {
|
|||
t.Fatal(err)
|
||||
}
|
||||
|
||||
log.Debugf("%+v\n", form)
|
||||
// log.Debugf("%+v\n", form)
|
||||
|
||||
assert.Equal(t, "unknown filter: Xxx", err.Error())
|
||||
})
|
||||
|
@ -137,7 +172,7 @@ func TestParseQueryString(t *testing.T) {
|
|||
t.Fatal(err)
|
||||
}
|
||||
|
||||
log.Debugf("%+v\n", form)
|
||||
// log.Debugf("%+v\n", form)
|
||||
|
||||
assert.Equal(t, "strconv.ParseFloat: parsing \"cat\": invalid syntax", err.Error())
|
||||
})
|
||||
|
@ -150,7 +185,7 @@ func TestParseQueryString(t *testing.T) {
|
|||
t.Fatal(err)
|
||||
}
|
||||
|
||||
log.Debugf("%+v\n", form)
|
||||
// log.Debugf("%+v\n", form)
|
||||
|
||||
assert.Equal(t, "strconv.Atoi: parsing \"cat\": invalid syntax", err.Error())
|
||||
})
|
||||
|
@ -163,7 +198,7 @@ func TestParseQueryString(t *testing.T) {
|
|||
t.Fatal(err)
|
||||
}
|
||||
|
||||
log.Debugf("%+v\n", form)
|
||||
// log.Debugf("%+v\n", form)
|
||||
|
||||
assert.Equal(t, "strconv.Atoi: parsing \"cat\": invalid syntax", err.Error())
|
||||
})
|
||||
|
@ -176,7 +211,7 @@ func TestParseQueryString(t *testing.T) {
|
|||
t.Fatal(err)
|
||||
}
|
||||
|
||||
log.Debugf("%+v\n", form)
|
||||
// log.Debugf("%+v\n", form)
|
||||
|
||||
assert.Equal(t, "Could not find format for \"cat\"", err.Error())
|
||||
})
|
||||
|
|
|
@ -54,8 +54,8 @@ func Geo(f form.GeoSearch) (results GeoResults, err error) {
|
|||
if err := Db().Where(AnySlug("custom_slug", f.Query, " ")).Find(&labels).Error; len(labels) == 0 || err != nil {
|
||||
log.Infof("search: label %s not found, using fuzzy search", txt.Quote(f.Query))
|
||||
|
||||
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))
|
||||
for _, where := range LikeAny("k.keyword", f.Query) {
|
||||
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))
|
||||
}
|
||||
} else {
|
||||
for _, l := range labels {
|
||||
|
@ -70,35 +70,66 @@ func Geo(f form.GeoSearch) (results GeoResults, err error) {
|
|||
}
|
||||
}
|
||||
|
||||
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)
|
||||
if wheres := LikeAny("k.keyword", f.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)
|
||||
}
|
||||
} 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if f.Album != "" {
|
||||
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)
|
||||
// Search for one or more keywords?
|
||||
if f.Keywords != "" {
|
||||
for _, where := range LikeAll("k.keyword", f.Keywords) {
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
// Filter for one or more subjects?
|
||||
if f.Subject != "" {
|
||||
s = s.Where(fmt.Sprintf("photos.id IN (SELECT photo_id FROM files f JOIN %s m ON f.id = m.file_id AND m.marker_invalid = 0 WHERE subject_uid IN (?))",
|
||||
entity.Marker{}.TableName()), strings.Split(strings.ToLower(f.Subject), Or))
|
||||
} else if f.Subjects != "" {
|
||||
for _, where := range LikeAny("s.subject_name", f.Subjects) {
|
||||
s = s.Where(fmt.Sprintf("photos.id IN (SELECT photo_id FROM files f JOIN %s m ON f.id = m.file_id AND m.marker_invalid = 0 JOIN %s s ON s.subject_uid = m.subject_uid WHERE (?))",
|
||||
entity.Marker{}.TableName(), entity.Subject{}.TableName()), gorm.Expr(where))
|
||||
}
|
||||
}
|
||||
|
||||
// Filter by album?
|
||||
if f.Album != "" {
|
||||
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)
|
||||
} else if f.Albums != "" {
|
||||
for _, where := range LikeAny("a.album_title", f.Albums) {
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
// Filter by camera?
|
||||
if f.Camera > 0 {
|
||||
s = s.Where("photos.camera_id = ?", f.Camera)
|
||||
}
|
||||
|
||||
// Filter by camera lens?
|
||||
if f.Lens > 0 {
|
||||
s = s.Where("photos.lens_id = ?", f.Lens)
|
||||
}
|
||||
|
||||
// Filter by year?
|
||||
if (f.Year > 0 && f.Year <= txt.YearMax) || f.Year == entity.UnknownYear {
|
||||
s = s.Where("photos.photo_year = ?", f.Year)
|
||||
}
|
||||
|
||||
// Filter by month?
|
||||
if (f.Month >= txt.MonthMin && f.Month <= txt.MonthMax) || f.Month == entity.UnknownMonth {
|
||||
s = s.Where("photos.photo_month = ?", f.Month)
|
||||
}
|
||||
|
||||
// Filter by day?
|
||||
if (f.Day >= txt.DayMin && f.Month <= txt.DayMax) || f.Day == entity.UnknownDay {
|
||||
s = s.Where("photos.photo_day = ?", f.Day)
|
||||
}
|
||||
|
|
|
@ -11,14 +11,32 @@ import (
|
|||
)
|
||||
|
||||
func TestGeo(t *testing.T) {
|
||||
t.Run("search all photos", func(t *testing.T) {
|
||||
query := form.NewGeoSearch("")
|
||||
result, err := Geo(query)
|
||||
t.Run("form.keywords", func(t *testing.T) {
|
||||
query := form.NewGeoSearch("keywords:bridge")
|
||||
|
||||
if err != nil {
|
||||
if result, err := Geo(query); err != nil {
|
||||
t.Fatal(err)
|
||||
} else {
|
||||
assert.GreaterOrEqual(t, len(result), 1)
|
||||
}
|
||||
})
|
||||
t.Run("form.subjects", func(t *testing.T) {
|
||||
query := form.NewGeoSearch("subjects:John")
|
||||
|
||||
if result, err := Geo(query); err != nil {
|
||||
t.Fatal(err)
|
||||
} else {
|
||||
assert.GreaterOrEqual(t, len(result), 0)
|
||||
}
|
||||
})
|
||||
t.Run("find_all", func(t *testing.T) {
|
||||
query := form.NewGeoSearch("")
|
||||
|
||||
if result, err := Geo(query); err != nil {
|
||||
t.Fatal(err)
|
||||
} else {
|
||||
assert.LessOrEqual(t, 4, len(result))
|
||||
}
|
||||
assert.LessOrEqual(t, 4, len(result))
|
||||
})
|
||||
|
||||
t.Run("search for bridge", func(t *testing.T) {
|
||||
|
|
111
internal/query/like.go
Normal file
111
internal/query/like.go
Normal file
|
@ -0,0 +1,111 @@
|
|||
package query
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/gosimple/slug"
|
||||
"github.com/photoprism/photoprism/pkg/txt"
|
||||
|
||||
"github.com/jinzhu/inflection"
|
||||
)
|
||||
|
||||
// LikeAny returns a single where condition matching the search keywords.
|
||||
func LikeAny(col, keywords string) (wheres []string) {
|
||||
keywords = strings.ReplaceAll(keywords, Or, " ")
|
||||
keywords = strings.ReplaceAll(keywords, OrEn, " ")
|
||||
keywords = strings.ReplaceAll(keywords, AndEn, And)
|
||||
|
||||
for _, k := range strings.Split(keywords, And) {
|
||||
var orWheres []string
|
||||
|
||||
words := txt.UniqueKeywords(k)
|
||||
|
||||
if len(words) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, w := range words {
|
||||
if len(w) > 3 {
|
||||
orWheres = append(orWheres, fmt.Sprintf("%s LIKE '%s%%'", col, w))
|
||||
} else {
|
||||
orWheres = append(orWheres, fmt.Sprintf("%s = '%s'", col, w))
|
||||
}
|
||||
|
||||
if !txt.ContainsASCIILetters(w) {
|
||||
continue
|
||||
}
|
||||
|
||||
singular := inflection.Singular(w)
|
||||
|
||||
if singular != w {
|
||||
orWheres = append(orWheres, fmt.Sprintf("%s = '%s'", col, singular))
|
||||
}
|
||||
}
|
||||
|
||||
if len(orWheres) > 0 {
|
||||
wheres = append(wheres, strings.Join(orWheres, " OR "))
|
||||
}
|
||||
}
|
||||
|
||||
return wheres
|
||||
}
|
||||
|
||||
// LikeAll returns a list of where conditions matching all search keywords.
|
||||
func LikeAll(col, keywords string) (wheres []string) {
|
||||
words := txt.UniqueKeywords(keywords)
|
||||
|
||||
if len(words) == 0 {
|
||||
return wheres
|
||||
}
|
||||
|
||||
for _, w := range words {
|
||||
if len(w) > 3 {
|
||||
wheres = append(wheres, fmt.Sprintf("%s LIKE '%s%%'", col, w))
|
||||
} else {
|
||||
wheres = append(wheres, fmt.Sprintf("%s = '%s'", col, w))
|
||||
}
|
||||
}
|
||||
|
||||
return wheres
|
||||
}
|
||||
|
||||
// AnySlug returns a where condition that matches any slug in search.
|
||||
func AnySlug(col, search, sep string) (where string) {
|
||||
if search == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
if sep == "" {
|
||||
sep = " "
|
||||
}
|
||||
|
||||
var wheres []string
|
||||
var words []string
|
||||
|
||||
for _, w := range strings.Split(search, sep) {
|
||||
w = strings.TrimSpace(w)
|
||||
|
||||
words = append(words, slug.Make(w))
|
||||
|
||||
if !txt.ContainsASCIILetters(w) {
|
||||
continue
|
||||
}
|
||||
|
||||
singular := inflection.Singular(w)
|
||||
|
||||
if singular != w {
|
||||
words = append(words, slug.Make(singular))
|
||||
}
|
||||
}
|
||||
|
||||
if len(words) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
for _, w := range words {
|
||||
wheres = append(wheres, fmt.Sprintf("%s = '%s'", col, w))
|
||||
}
|
||||
|
||||
return strings.Join(wheres, " OR ")
|
||||
}
|
123
internal/query/like_test.go
Normal file
123
internal/query/like_test.go
Normal file
|
@ -0,0 +1,123 @@
|
|||
package query
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestLikeAny(t *testing.T) {
|
||||
t.Run("and_or_search", func(t *testing.T) {
|
||||
if w := LikeAny("k.keyword", "table spoon & usa | img json"); len(w) != 2 {
|
||||
t.Fatal("two where conditions expected")
|
||||
} else {
|
||||
assert.Equal(t, "k.keyword LIKE 'spoon%' OR k.keyword LIKE 'table%'", w[0])
|
||||
assert.Equal(t, "k.keyword LIKE 'json%' OR k.keyword = 'usa'", w[1])
|
||||
}
|
||||
})
|
||||
t.Run("and_or_search_en", func(t *testing.T) {
|
||||
if w := LikeAny("k.keyword", "table spoon and usa or img json"); len(w) != 2 {
|
||||
t.Fatal("two where conditions expected")
|
||||
} else {
|
||||
assert.Equal(t, "k.keyword LIKE 'spoon%' OR k.keyword LIKE 'table%'", w[0])
|
||||
assert.Equal(t, "k.keyword LIKE 'json%' OR k.keyword = 'usa'", w[1])
|
||||
}
|
||||
})
|
||||
t.Run("table spoon usa img json", func(t *testing.T) {
|
||||
if w := LikeAny("k.keyword", "table spoon usa img json"); len(w) != 1 {
|
||||
t.Fatal("one where condition expected")
|
||||
} else {
|
||||
assert.Equal(t, "k.keyword LIKE 'json%' OR k.keyword LIKE 'spoon%' OR k.keyword LIKE 'table%' OR k.keyword = 'usa'", w[0])
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("cat dog", func(t *testing.T) {
|
||||
if w := LikeAny("k.keyword", "cat dog"); len(w) != 1 {
|
||||
t.Fatal("one where condition expected")
|
||||
} else {
|
||||
assert.Equal(t, "k.keyword = 'cat' OR k.keyword = 'dog'", w[0])
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("cats dogs", func(t *testing.T) {
|
||||
if w := LikeAny("k.keyword", "cats dogs"); len(w) != 1 {
|
||||
t.Fatal("one where condition expected")
|
||||
} else {
|
||||
assert.Equal(t, "k.keyword LIKE 'cats%' OR k.keyword = 'cat' OR k.keyword LIKE 'dogs%' OR k.keyword = 'dog'", w[0])
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("spoon", func(t *testing.T) {
|
||||
if w := LikeAny("k.keyword", "spoon"); len(w) != 1 {
|
||||
t.Fatal("one where condition expected")
|
||||
} else {
|
||||
assert.Equal(t, "k.keyword LIKE 'spoon%'", w[0])
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("img", func(t *testing.T) {
|
||||
if w := LikeAny("k.keyword", "img"); len(w) > 0 {
|
||||
t.Fatal("no where condition expected")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("empty", func(t *testing.T) {
|
||||
if w := LikeAny("k.keyword", ""); len(w) > 0 {
|
||||
t.Fatal("no where condition expected")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestLikeAll(t *testing.T) {
|
||||
t.Run("keywords", func(t *testing.T) {
|
||||
if w := LikeAll("k.keyword", "Jo Mander 李"); len(w) == 2 {
|
||||
assert.Equal(t, "k.keyword LIKE 'mander%'", w[0])
|
||||
assert.Equal(t, "k.keyword = '李'", w[1])
|
||||
} else {
|
||||
t.Logf("wheres: %#v", w)
|
||||
t.Fatal("two where conditions expected")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestAnySlug(t *testing.T) {
|
||||
t.Run("table spoon usa img json", func(t *testing.T) {
|
||||
where := AnySlug("custom_slug", "table spoon usa img json", " ")
|
||||
assert.Equal(t, "custom_slug = 'table' OR custom_slug = 'spoon' OR custom_slug = 'usa' OR custom_slug = 'img' OR custom_slug = 'json'", where)
|
||||
})
|
||||
|
||||
t.Run("cat dog", func(t *testing.T) {
|
||||
where := AnySlug("custom_slug", "cat dog", " ")
|
||||
assert.Equal(t, "custom_slug = 'cat' OR custom_slug = 'dog'", where)
|
||||
})
|
||||
|
||||
t.Run("cats dogs", func(t *testing.T) {
|
||||
where := AnySlug("custom_slug", "cats dogs", " ")
|
||||
assert.Equal(t, "custom_slug = 'cats' OR custom_slug = 'cat' OR custom_slug = 'dogs' OR custom_slug = 'dog'", where)
|
||||
})
|
||||
|
||||
t.Run("spoon", func(t *testing.T) {
|
||||
where := AnySlug("custom_slug", "spoon", " ")
|
||||
assert.Equal(t, "custom_slug = 'spoon'", where)
|
||||
})
|
||||
|
||||
t.Run("img", func(t *testing.T) {
|
||||
where := AnySlug("custom_slug", "img", " ")
|
||||
assert.Equal(t, "custom_slug = 'img'", where)
|
||||
})
|
||||
|
||||
t.Run("empty", func(t *testing.T) {
|
||||
where := AnySlug("custom_slug", "", " ")
|
||||
assert.Equal(t, "", where)
|
||||
})
|
||||
|
||||
t.Run("comma separated", func(t *testing.T) {
|
||||
where := AnySlug("custom_slug", "botanical-garden|landscape|bay", Or)
|
||||
assert.Equal(t, "custom_slug = 'botanical-garden' OR custom_slug = 'landscape' OR custom_slug = 'bay'", where)
|
||||
})
|
||||
|
||||
t.Run("len = 0", func(t *testing.T) {
|
||||
where := AnySlug("custom_slug", " ", "")
|
||||
assert.Equal(t, "custom_slug = '' OR custom_slug = ''", where)
|
||||
})
|
||||
}
|
|
@ -136,15 +136,15 @@ func PhotoSearch(f form.PhotoSearch) (results PhotoResults, count int, err error
|
|||
if f.Geo == true {
|
||||
s = s.Where("photos.cell_id <> 'zz'")
|
||||
|
||||
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))
|
||||
for _, where := range LikeAny("k.keyword", f.Query) {
|
||||
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))
|
||||
}
|
||||
} else if f.Query != "" {
|
||||
if err := Db().Where(AnySlug("custom_slug", f.Query, " ")).Find(&labels).Error; len(labels) == 0 || err != nil {
|
||||
log.Infof("search: label %s not found, using fuzzy search", txt.Quote(f.Query))
|
||||
|
||||
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))
|
||||
for _, where := range LikeAny("k.keyword", f.Query) {
|
||||
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))
|
||||
}
|
||||
} else {
|
||||
for _, l := range labels {
|
||||
|
@ -159,16 +159,36 @@ func PhotoSearch(f form.PhotoSearch) (results PhotoResults, count int, err error
|
|||
}
|
||||
}
|
||||
|
||||
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)
|
||||
if wheres := LikeAny("k.keyword", f.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)
|
||||
}
|
||||
} 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Filter by status.
|
||||
// Search for one or more keywords?
|
||||
if f.Keywords != "" {
|
||||
for _, where := range LikeAll("k.keyword", f.Keywords) {
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
// Filter for one or more subjects?
|
||||
if f.Subject != "" {
|
||||
s = s.Where(fmt.Sprintf("photos.id IN (SELECT photo_id FROM files f JOIN %s m ON f.id = m.file_id AND m.marker_invalid = 0 WHERE subject_uid IN (?))",
|
||||
entity.Marker{}.TableName()), strings.Split(strings.ToLower(f.Subject), Or))
|
||||
} else if f.Subjects != "" {
|
||||
for _, where := range LikeAny("s.subject_name", f.Subjects) {
|
||||
s = s.Where(fmt.Sprintf("photos.id IN (SELECT photo_id FROM files f JOIN %s m ON f.id = m.file_id AND m.marker_invalid = 0 JOIN %s s ON s.subject_uid = m.subject_uid WHERE (?))",
|
||||
entity.Marker{}.TableName(), entity.Subject{}.TableName()), gorm.Expr(where))
|
||||
}
|
||||
}
|
||||
|
||||
// Filter by status?
|
||||
if f.Hidden {
|
||||
s = s.Where("photos.photo_quality = -1")
|
||||
s = s.Where("photos.deleted_at IS NULL")
|
||||
|
@ -191,23 +211,27 @@ func PhotoSearch(f form.PhotoSearch) (results PhotoResults, count int, err error
|
|||
}
|
||||
}
|
||||
|
||||
// Filter by additional flags and metadata.
|
||||
// Filter by camera?
|
||||
if f.Camera > 0 {
|
||||
s = s.Where("photos.camera_id = ?", f.Camera)
|
||||
}
|
||||
|
||||
// Filter by camera lens?
|
||||
if f.Lens > 0 {
|
||||
s = s.Where("photos.lens_id = ?", f.Lens)
|
||||
}
|
||||
|
||||
// Filter by year?
|
||||
if (f.Year > 0 && f.Year <= txt.YearMax) || f.Year == entity.UnknownYear {
|
||||
s = s.Where("photos.photo_year = ?", f.Year)
|
||||
}
|
||||
|
||||
// Filter by month?
|
||||
if (f.Month >= txt.MonthMin && f.Month <= txt.MonthMax) || f.Month == entity.UnknownMonth {
|
||||
s = s.Where("photos.photo_month = ?", f.Month)
|
||||
}
|
||||
|
||||
// Filter by day?
|
||||
if (f.Day >= txt.DayMin && f.Month <= txt.DayMax) || f.Day == entity.UnknownDay {
|
||||
s = s.Where("photos.photo_day = ?", f.Day)
|
||||
}
|
||||
|
@ -363,10 +387,12 @@ func PhotoSearch(f form.PhotoSearch) (results PhotoResults, count int, err error
|
|||
s = s.Where("photos.taken_at >= ?", f.After.Format("2006-01-02"))
|
||||
}
|
||||
|
||||
// Find stacks only?
|
||||
if f.Stack {
|
||||
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')")
|
||||
}
|
||||
|
||||
// Filter by album?
|
||||
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)
|
||||
|
@ -375,6 +401,10 @@ func PhotoSearch(f form.PhotoSearch) (results PhotoResults, count int, err error
|
|||
}
|
||||
} 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)")
|
||||
} else if f.Albums != "" {
|
||||
for _, where := range LikeAny("a.album_title", f.Albums) {
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
if err := s.Scan(&results).Error; err != nil {
|
||||
|
|
|
@ -339,9 +339,69 @@ func TestPhotoSearch(t *testing.T) {
|
|||
}
|
||||
|
||||
//t.Logf("results: %+v", photos)
|
||||
assert.Equal(t, 1, len(photos))
|
||||
assert.GreaterOrEqual(t, len(photos), 1)
|
||||
|
||||
})
|
||||
t.Run("form.keywords", func(t *testing.T) {
|
||||
var f form.PhotoSearch
|
||||
f.Query = "keywords:bridge"
|
||||
f.Count = 10
|
||||
f.Offset = 0
|
||||
|
||||
photos, _, err := PhotoSearch(f)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
//t.Logf("results: %+v", photos)
|
||||
assert.GreaterOrEqual(t, len(photos), 4)
|
||||
})
|
||||
t.Run("form.subject", func(t *testing.T) {
|
||||
var f form.PhotoSearch
|
||||
f.Query = "subject:jqu0xs11qekk9jx8"
|
||||
f.Count = 10
|
||||
f.Offset = 0
|
||||
|
||||
photos, _, err := PhotoSearch(f)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
//t.Logf("results: %+v", photos)
|
||||
assert.Equal(t, 1, len(photos))
|
||||
})
|
||||
t.Run("form.subjects", func(t *testing.T) {
|
||||
var f form.PhotoSearch
|
||||
f.Query = "subjects:John"
|
||||
f.Count = 10
|
||||
f.Offset = 0
|
||||
|
||||
photos, _, err := PhotoSearch(f)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
//t.Logf("results: %+v", photos)
|
||||
assert.Equal(t, 1, len(photos))
|
||||
})
|
||||
t.Run("form.people", func(t *testing.T) {
|
||||
var f form.PhotoSearch
|
||||
f.Query = "people:John"
|
||||
f.Count = 10
|
||||
f.Offset = 0
|
||||
|
||||
photos, _, err := PhotoSearch(f)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
//t.Logf("results: %+v", photos)
|
||||
assert.Equal(t, 1, len(photos))
|
||||
})
|
||||
t.Run("form.hash", func(t *testing.T) {
|
||||
var f form.PhotoSearch
|
||||
f.Query = "hash:2cad9168fa6acc5c5c2965ddf6ec465ca42fd818"
|
||||
|
@ -522,6 +582,18 @@ func TestPhotoSearch(t *testing.T) {
|
|||
}
|
||||
assert.LessOrEqual(t, 1, len(photos))
|
||||
})
|
||||
t.Run("albums", func(t *testing.T) {
|
||||
var f form.PhotoSearch
|
||||
f.Query = ""
|
||||
f.Albums = "Berlin"
|
||||
|
||||
photos, _, err := PhotoSearch(f)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.LessOrEqual(t, 1, len(photos))
|
||||
})
|
||||
t.Run("search for state", func(t *testing.T) {
|
||||
var f form.PhotoSearch
|
||||
f.State = "KwaZulu-Natal"
|
||||
|
@ -728,6 +800,6 @@ func TestPhotoSearch(t *testing.T) {
|
|||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.GreaterOrEqual(t, 3, len(photos))
|
||||
assert.GreaterOrEqual(t, len(photos), 3)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -32,16 +32,9 @@ https://docs.photoprism.org/developer-guide/
|
|||
package query
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/gosimple/slug"
|
||||
"github.com/jinzhu/gorm"
|
||||
"github.com/photoprism/photoprism/internal/entity"
|
||||
"github.com/photoprism/photoprism/internal/event"
|
||||
"github.com/photoprism/photoprism/pkg/txt"
|
||||
|
||||
"github.com/jinzhu/gorm"
|
||||
"github.com/jinzhu/inflection"
|
||||
)
|
||||
|
||||
var log = event.Log
|
||||
|
@ -50,12 +43,15 @@ const (
|
|||
MySQL = "mysql"
|
||||
SQLite = "sqlite3"
|
||||
Or = "|"
|
||||
And = "&"
|
||||
OrEn = " or "
|
||||
AndEn = " and "
|
||||
)
|
||||
|
||||
// Max result limit for queries.
|
||||
// MaxResults is max result limit for queries.
|
||||
const MaxResults = 10000
|
||||
|
||||
// About 1km ('good enough' for now)
|
||||
// SearchRadius is about 1 km.
|
||||
const SearchRadius = 0.009
|
||||
|
||||
// Query searches given an originals path and a db instance.
|
||||
|
@ -91,74 +87,3 @@ func UnscopedDb() *gorm.DB {
|
|||
func DbDialect() string {
|
||||
return Db().Dialect().GetName()
|
||||
}
|
||||
|
||||
// LikeAny returns a where condition that matches any keyword in search.
|
||||
func LikeAny(col, search string) (where string) {
|
||||
var wheres []string
|
||||
|
||||
words := txt.UniqueKeywords(search)
|
||||
|
||||
if len(words) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
for _, w := range words {
|
||||
if len(w) > 3 {
|
||||
wheres = append(wheres, fmt.Sprintf("%s LIKE '%s%%'", col, w))
|
||||
} else {
|
||||
wheres = append(wheres, fmt.Sprintf("%s = '%s'", col, w))
|
||||
}
|
||||
|
||||
if !txt.ContainsASCIILetters(w) {
|
||||
continue
|
||||
}
|
||||
|
||||
singular := inflection.Singular(w)
|
||||
|
||||
if singular != w {
|
||||
wheres = append(wheres, fmt.Sprintf("%s = '%s'", col, singular))
|
||||
}
|
||||
}
|
||||
|
||||
return strings.Join(wheres, " OR ")
|
||||
}
|
||||
|
||||
// AnySlug returns a where condition that matches any slug in search.
|
||||
func AnySlug(col, search, sep string) (where string) {
|
||||
if search == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
if sep == "" {
|
||||
sep = " "
|
||||
}
|
||||
|
||||
var wheres []string
|
||||
var words []string
|
||||
|
||||
for _, w := range strings.Split(search, sep) {
|
||||
w = strings.TrimSpace(w)
|
||||
|
||||
words = append(words, slug.Make(w))
|
||||
|
||||
if !txt.ContainsASCIILetters(w) {
|
||||
continue
|
||||
}
|
||||
|
||||
singular := inflection.Singular(w)
|
||||
|
||||
if singular != w {
|
||||
words = append(words, slug.Make(singular))
|
||||
}
|
||||
}
|
||||
|
||||
if len(words) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
for _, w := range words {
|
||||
wheres = append(wheres, fmt.Sprintf("%s = '%s'", col, w))
|
||||
}
|
||||
|
||||
return strings.Join(wheres, " OR ")
|
||||
}
|
||||
|
|
|
@ -6,7 +6,6 @@ import (
|
|||
|
||||
"github.com/photoprism/photoprism/internal/entity"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
|
@ -24,77 +23,3 @@ func TestMain(m *testing.M) {
|
|||
|
||||
os.Exit(code)
|
||||
}
|
||||
|
||||
func TestLikeAny(t *testing.T) {
|
||||
t.Run("table spoon usa img json", func(t *testing.T) {
|
||||
where := LikeAny("k.keyword", "table spoon usa img json")
|
||||
assert.Equal(t, "k.keyword LIKE 'json%' OR k.keyword LIKE 'spoon%' OR k.keyword LIKE 'table%' OR k.keyword = 'usa'", where)
|
||||
})
|
||||
|
||||
t.Run("cat dog", func(t *testing.T) {
|
||||
where := LikeAny("k.keyword", "cat dog")
|
||||
assert.Equal(t, "k.keyword = 'cat' OR k.keyword = 'dog'", where)
|
||||
})
|
||||
|
||||
t.Run("cats dogs", func(t *testing.T) {
|
||||
where := LikeAny("k.keyword", "cats dogs")
|
||||
assert.Equal(t, "k.keyword LIKE 'cats%' OR k.keyword = 'cat' OR k.keyword LIKE 'dogs%' OR k.keyword = 'dog'", where)
|
||||
})
|
||||
|
||||
t.Run("spoon", func(t *testing.T) {
|
||||
where := LikeAny("k.keyword", "spoon")
|
||||
assert.Equal(t, "k.keyword LIKE 'spoon%'", where)
|
||||
})
|
||||
|
||||
t.Run("img", func(t *testing.T) {
|
||||
where := LikeAny("k.keyword", "img")
|
||||
assert.Equal(t, "", where)
|
||||
})
|
||||
|
||||
t.Run("empty", func(t *testing.T) {
|
||||
where := LikeAny("k.keyword", "")
|
||||
assert.Equal(t, "", where)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAnySlug(t *testing.T) {
|
||||
t.Run("table spoon usa img json", func(t *testing.T) {
|
||||
where := AnySlug("custom_slug", "table spoon usa img json", " ")
|
||||
assert.Equal(t, "custom_slug = 'table' OR custom_slug = 'spoon' OR custom_slug = 'usa' OR custom_slug = 'img' OR custom_slug = 'json'", where)
|
||||
})
|
||||
|
||||
t.Run("cat dog", func(t *testing.T) {
|
||||
where := AnySlug("custom_slug", "cat dog", " ")
|
||||
assert.Equal(t, "custom_slug = 'cat' OR custom_slug = 'dog'", where)
|
||||
})
|
||||
|
||||
t.Run("cats dogs", func(t *testing.T) {
|
||||
where := AnySlug("custom_slug", "cats dogs", " ")
|
||||
assert.Equal(t, "custom_slug = 'cats' OR custom_slug = 'cat' OR custom_slug = 'dogs' OR custom_slug = 'dog'", where)
|
||||
})
|
||||
|
||||
t.Run("spoon", func(t *testing.T) {
|
||||
where := AnySlug("custom_slug", "spoon", " ")
|
||||
assert.Equal(t, "custom_slug = 'spoon'", where)
|
||||
})
|
||||
|
||||
t.Run("img", func(t *testing.T) {
|
||||
where := AnySlug("custom_slug", "img", " ")
|
||||
assert.Equal(t, "custom_slug = 'img'", where)
|
||||
})
|
||||
|
||||
t.Run("empty", func(t *testing.T) {
|
||||
where := AnySlug("custom_slug", "", " ")
|
||||
assert.Equal(t, "", where)
|
||||
})
|
||||
|
||||
t.Run("comma separated", func(t *testing.T) {
|
||||
where := AnySlug("custom_slug", "botanical-garden|landscape|bay", Or)
|
||||
assert.Equal(t, "custom_slug = 'botanical-garden' OR custom_slug = 'landscape' OR custom_slug = 'bay'", where)
|
||||
})
|
||||
|
||||
t.Run("len = 0", func(t *testing.T) {
|
||||
where := AnySlug("custom_slug", " ", "")
|
||||
assert.Equal(t, "custom_slug = '' OR custom_slug = ''", where)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1428,7 +1428,6 @@ manchem
|
|||
manchen
|
||||
mancher
|
||||
manches
|
||||
mann
|
||||
mehr
|
||||
mein
|
||||
meine
|
||||
|
@ -1436,9 +1435,6 @@ meinem
|
|||
meinen
|
||||
meiner
|
||||
meines
|
||||
mensch
|
||||
menschen
|
||||
mich
|
||||
mir
|
||||
mit
|
||||
mittel
|
||||
|
|
|
@ -1433,7 +1433,6 @@ var StopWords = map[string]bool{
|
|||
"manchen": true,
|
||||
"mancher": true,
|
||||
"manches": true,
|
||||
"mann": true,
|
||||
"mehr": true,
|
||||
"mein": true,
|
||||
"meine": true,
|
||||
|
@ -1441,9 +1440,6 @@ var StopWords = map[string]bool{
|
|||
"meinen": true,
|
||||
"meiner": true,
|
||||
"meines": true,
|
||||
"mensch": true,
|
||||
"menschen": true,
|
||||
"mich": true,
|
||||
"mir": true,
|
||||
"mit": true,
|
||||
"mittel": true,
|
||||
|
|
Loading…
Reference in a new issue