diff --git a/services/search/internal/opensearchtest/os.go b/services/search/internal/opensearchtest/os.go index 16c3c10025..e8308dedf4 100644 --- a/services/search/internal/opensearchtest/os.go +++ b/services/search/internal/opensearchtest/os.go @@ -137,6 +137,19 @@ func (tc *TestClient) IndicesCreate(ctx context.Context, index string, body io.R } } +// IndicesCount returns the number of documents in the given indices. +func (tc *TestClient) IndicesCount(ctx context.Context, indices []string, body io.Reader) (int, error) { + resp, err := tc.c.Indices.Count(ctx, &opensearchgoAPI.IndicesCountReq{ + Indices: indices, + Body: body, + }) + if err != nil { + return 0, fmt.Errorf("failed to count documents in %v: %w", indices, err) + } + + return resp.Count, nil +} + type testRequireClient struct { tc *TestClient t testing.TB @@ -157,3 +170,9 @@ func (trc *testRequireClient) IndicesCreate(index string, body io.Reader) { func (trc *testRequireClient) IndicesDelete(indices []string) { require.NoError(trc.t, trc.tc.IndicesDelete(trc.t.Context(), indices)) } + +func (trc *testRequireClient) IndicesCount(indices []string, body io.Reader, want int) { + got, err := trc.tc.IndicesCount(trc.t.Context(), indices, body) + require.NoError(trc.t, err) + require.Equal(trc.t, want, got) +} diff --git a/services/search/pkg/bleve/backend_test.go b/services/search/pkg/bleve/backend_test.go index 21603ee343..a0ff3092b5 100644 --- a/services/search/pkg/bleve/backend_test.go +++ b/services/search/pkg/bleve/backend_test.go @@ -1,75 +1,28 @@ package bleve_test import ( - "context" "fmt" bleveSearch "github.com/blevesearch/bleve/v2" sprovider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - libregraph "github.com/opencloud-eu/libre-graph-api-go" - "github.com/opencloud-eu/reva/v2/pkg/storagespace" "github.com/opencloud-eu/opencloud/pkg/log" - searchmsg "github.com/opencloud-eu/opencloud/protogen/gen/opencloud/messages/search/v0" - searchsvc "github.com/opencloud-eu/opencloud/protogen/gen/opencloud/services/search/v0" "github.com/opencloud-eu/opencloud/services/search/pkg/bleve" "github.com/opencloud-eu/opencloud/services/search/pkg/content" bleveQuery "github.com/opencloud-eu/opencloud/services/search/pkg/query/bleve" "github.com/opencloud-eu/opencloud/services/search/pkg/search" ) -func hiddenByID(idx bleveSearch.Index, id string) bool { - GinkgoHelper() - - req := bleveSearch.NewSearchRequest(bleveSearch.NewDocIDQuery([]string{id})) - req.Fields = []string{"Hidden"} - - res, err := idx.Search(req) - Expect(err).ToNot(HaveOccurred()) - Expect(res.Hits).To(HaveLen(1), "no record for %s", id) - - hidden, _ := res.Hits[0].Fields["Hidden"].(bool) - return hidden -} - var _ = Describe("Bleve", func() { var ( eng *bleve.Backend idx bleveSearch.Index - doSearch = func(id string, query, path string) (*searchsvc.SearchIndexResponse, error) { - rID, err := storagespace.ParseID(id) - if err != nil { - return nil, err - } - - return eng.Search(context.Background(), &searchsvc.SearchIndexRequest{ - Query: query, - Ref: &searchmsg.Reference{ - ResourceId: &searchmsg.ResourceID{ - StorageId: rID.StorageId, - SpaceId: rID.SpaceId, - OpaqueId: rID.OpaqueId, - }, - Path: path, - }, - }) - } - - assertDocCount = func(id string, query string, expectedCount int) []*searchmsg.Match { - res, err := doSearch(id, query, "") - - ExpectWithOffset(1, err).ToNot(HaveOccurred()) - ExpectWithOffset(1, len(res.Matches)).To(Equal(expectedCount), "query returned unexpected number of results: "+query) - return res.Matches - } - rootResource search.Resource parentResource search.Resource childResource search.Resource - childResource2 search.Resource ) BeforeEach(func() { @@ -107,14 +60,6 @@ var _ = Describe("Bleve", func() { Document: content.Document{Name: "child.pdf"}, } - childResource2 = search.Resource{ - ID: "1$2!5", - ParentID: parentResource.ID, - RootID: rootResource.ID, - Path: "./parent d!r/child2.pdf", - Type: uint64(sprovider.ResourceType_RESOURCE_TYPE_FILE), - Document: content.Document{Name: "child2.pdf"}, - } }) Describe("PurgeSpace", func() { @@ -170,809 +115,4 @@ var _ = Describe("Bleve", func() { }) }) - Describe("Search", func() { - Context("by other fields than filename", func() { - It("finds files by tags", func() { - parentResource.Document.Tags = []string{"foo", "bar"} - err := eng.Upsert(parentResource.ID, parentResource) - Expect(err).ToNot(HaveOccurred()) - - assertDocCount(rootResource.ID, "Tags:foo", 1) - assertDocCount(rootResource.ID, "Tags:bar", 1) - assertDocCount(rootResource.ID, "Tags:foo Tags:bar", 1) - assertDocCount(rootResource.ID, "Tags:foo Tags:bar Tags:baz", 1) - assertDocCount(rootResource.ID, "Tags:foo Tags:bar Tags:baz", 1) - assertDocCount(rootResource.ID, "Tags:baz", 0) - }) - - It("finds files by tags case-insensitively", func() { - // exercises the []string/[]any sibling-lowercasing branch end-to-end. - parentResource.Document.Tags = []string{"Work", "Urgent"} - Expect(eng.Upsert(parentResource.ID, parentResource)).To(Succeed()) - - assertDocCount(rootResource.ID, "tag:work", 1) // stored "Work", queried lower - assertDocCount(rootResource.ID, "tag:WORK", 1) // queried upper - assertDocCount(rootResource.ID, "Tags:Urgent", 1) - assertDocCount(rootResource.ID, "tag:missing", 0) - }) - - It("binds a leading NOT to the term right after it, combined with AND", func() { - // regression: a leading NOT next to AND dropped the AND'd term, so - // `NOT tag:x AND name:y` matched nothing (a self-contradicting clause). - parentResource.Document.Tags = []string{"physik"} - childResource.Document.Tags = []string{"mathe"} - Expect(eng.Upsert(parentResource.ID, parentResource)).To(Succeed()) - Expect(eng.Upsert(childResource.ID, childResource)).To(Succeed()) - - assertDocCount(rootResource.ID, "NOT tag:physik AND name:child.pdf", 1) // the mathe child - assertDocCount(rootResource.ID, "NOT tag:mathe AND name:parent*", 1) // the physik parent - }) - - It("finds files by size", func() { - parentResource.Document.Size = 12345 - err := eng.Upsert(parentResource.ID, parentResource) - Expect(err).ToNot(HaveOccurred()) - - assertDocCount(rootResource.ID, "Size:12345", 1) - assertDocCount(rootResource.ID, "Size:>1000", 1) - assertDocCount(rootResource.ID, "Size:<100000", 1) - assertDocCount(rootResource.ID, "Size:12344", 0) - assertDocCount(rootResource.ID, "Size:<1000", 0) - assertDocCount(rootResource.ID, "Size:>100000", 0) - }) - - It("matches facet values case-insensitively", func() { - parentResource.Document.Audio = &libregraph.Audio{ - Artist: libregraph.PtrString("Some Artist"), - } - err := eng.Upsert(parentResource.ID, parentResource) - Expect(err).ToNot(HaveOccurred()) - - assertDocCount(rootResource.ID, `audio.artist:"Some Artist"`, 1) - assertDocCount(rootResource.ID, `audio.artist:"some artist"`, 1) - }) - }) - - Context("by filename", func() { - It("finds files with spaces in the filename", func() { - parentResource.Document.Name = "Foo oo.pdf" - err := eng.Upsert(parentResource.ID, parentResource) - Expect(err).ToNot(HaveOccurred()) - - assertDocCount(rootResource.ID, `name:"foo o*"`, 1) - }) - - It("finds files by digits in the filename", func() { - parentResource.Document.Name = "12345.pdf" - err := eng.Upsert(parentResource.ID, parentResource) - Expect(err).ToNot(HaveOccurred()) - - assertDocCount(rootResource.ID, "Name:1234*", 1) - }) - - It("filters hidden files", func() { - childResource.Hidden = true - err := eng.Upsert(childResource.ID, childResource) - Expect(err).ToNot(HaveOccurred()) - - assertDocCount(rootResource.ID, "Hidden:T", 1) - assertDocCount(rootResource.ID, "Hidden:F", 0) - }) - - Context("with a file in the root of the space", func() { - It("scopes the search to the specified space", func() { - parentResource.Document.Name = "foo.pdf" - err := eng.Upsert(parentResource.ID, parentResource) - Expect(err).ToNot(HaveOccurred()) - - assertDocCount(rootResource.ID, "Name:foo.pdf", 1) - assertDocCount("9$8!7", "Name:foo.pdf", 0) - }) - }) - - It("limits the search to the specified fields", func() { - parentResource.Document.Name = "bar.pdf" - err := eng.Upsert(parentResource.ID, parentResource) - Expect(err).ToNot(HaveOccurred()) - - assertDocCount(rootResource.ID, "Name:bar.pdf", 1) - assertDocCount(rootResource.ID, "Unknown:field", 0) - }) - - It("returns the total number of hits", func() { - parentResource.Document.Name = "bar.pdf" - err := eng.Upsert(parentResource.ID, parentResource) - Expect(err).ToNot(HaveOccurred()) - - res, err := doSearch(rootResource.ID, "Name:bar*", "") - Expect(err).ToNot(HaveOccurred()) - Expect(res.TotalMatches).To(Equal(int32(1))) - }) - - It("returns all desired fields", func() { - parentResource.Document.Name = "bar.pdf" - parentResource.Type = 3 - parentResource.MimeType = "application/pdf" - - err := eng.Upsert(parentResource.ID, parentResource) - Expect(err).ToNot(HaveOccurred()) - - matches := assertDocCount(rootResource.ID, fmt.Sprintf("Name:%s", parentResource.Name), 1) - match := matches[0] - Expect(match.Entity.Ref.Path).To(Equal(parentResource.Path)) - Expect(match.Entity.Name).To(Equal(parentResource.Name)) - Expect(match.Entity.Size).To(Equal(parentResource.Size)) - Expect(match.Entity.Type).To(Equal(parentResource.Type)) - Expect(match.Entity.MimeType).To(Equal(parentResource.MimeType)) - Expect(match.Entity.Deleted).To(BeFalse()) - Expect(match.Score > 0).To(BeTrue()) - }) - - It("finds files by name, prefix or substring match", func() { - parentResource.Document.Name = "foo.pdf" - - err := eng.Upsert(parentResource.ID, parentResource) - Expect(err).ToNot(HaveOccurred()) - - queries := []string{"foo.pdf", "foo*", "*oo.p*"} - for _, query := range queries { - err := eng.Upsert(parentResource.ID, parentResource) - Expect(err).ToNot(HaveOccurred()) - - assertDocCount(rootResource.ID, query, 1) - } - }) - - It("does a case-insensitive search", func() { - parentResource.Document.Name = "foo.pdf" - - err := eng.Upsert(parentResource.ID, parentResource) - Expect(err).ToNot(HaveOccurred()) - - assertDocCount(rootResource.ID, "Name:foo*", 1) - assertDocCount(rootResource.ID, "Name:Foo*", 1) - }) - - Context("and an additional file in a subdirectory", func() { - BeforeEach(func() { - err := eng.Upsert(parentResource.ID, parentResource) - Expect(err).ToNot(HaveOccurred()) - - err = eng.Upsert(childResource.ID, childResource) - Expect(err).ToNot(HaveOccurred()) - }) - - It("finds files living deeper in the tree by filename, prefix or substring match", func() { - queries := []string{"child.pdf", "child*", "*ld.*"} - for _, query := range queries { - assertDocCount(rootResource.ID, query, 1) - } - }) - }) - }) - - Context("by path", func() { - BeforeEach(func() { - for _, r := range []search.Resource{parentResource, childResource, childResource2} { - Expect(eng.Upsert(r.ID, r)).To(Succeed()) - } - }) - - It("matches a folder and its descendants", func() { - assertDocCount(rootResource.ID, `path:"./parent d!r"`, 3) - }) - - It("matches a descendant path only itself", func() { - assertDocCount(rootResource.ID, `path:"./parent d!r/child.pdf"`, 1) - }) - - It("matches case-sensitively", func() { - // paths act as references: /Foo and /foo are distinct siblings, - // so a wrong-cased path must not match - assertDocCount(rootResource.ID, `path:"./PARENT D!R"`, 0) - }) - - It("applies an AND filter to the folder itself, not only descendants", func() { - // regression: the folder-itself clause used to match unconditionally - // under an AND, so the parent leaked in despite the name filter. - matches := assertDocCount(rootResource.ID, `path:"./parent d!r" AND name:child.pdf`, 1) - Expect(matches[0].Entity.Name).To(Equal("child.pdf")) - }) - }) - - Context("by content", func() { - It("matches full-text case-insensitively, without stemming", func() { - parentResource.Document.Content = "Running Foxes" - Expect(eng.Upsert(parentResource.ID, parentResource)).To(Succeed()) - - assertDocCount(rootResource.ID, "content:running", 1) - assertDocCount(rootResource.ID, "content:RUNNING", 1) // case-insensitive - assertDocCount(rootResource.ID, "content:run", 0) // no stemming - assertDocCount(rootResource.ID, "content:run*", 1) // wildcard over the word - assertDocCount(rootResource.ID, "content:cat", 0) - }) - }) - - Context("by mediatype", func() { - It("matches categories and literal MIME types (incl. + and /)", func() { - childResource.Document.MimeType = "image/svg+xml" - childResource2.Document.MimeType = "image/png" - for _, r := range []search.Resource{childResource, childResource2} { - Expect(eng.Upsert(r.ID, r)).To(Succeed()) - } - - assertDocCount(rootResource.ID, "mediatype:image", 2) // image/* wildcard -> both - assertDocCount(rootResource.ID, "mediatype:IMAGE", 2) // categories are case-insensitive - assertDocCount(rootResource.ID, "mediatype:pdf", 0) - // literal MIME with + and /, must hit only the svg doc, not the png - assertDocCount(rootResource.ID, "mediatype:image/svg+xml", 1) - assertDocCount(rootResource.ID, "mediatype:image/png", 1) - // the same literal via the raw field name (no mediatype alias) - assertDocCount(rootResource.ID, "MimeType:image/svg+xml", 1) - assertDocCount(rootResource.ID, "MimeType:image/png", 1) - }) - - It("combines mediatype:file with another term", func() { - // regression: mediatype:file (a NOT) next to an operator dropped the - // other operand, so mediatype:file AND name:x matched nothing. - parentResource.Document.MimeType = "httpd/unix-directory" // a folder - childResource.Document.MimeType = "image/png" // a file - for _, r := range []search.Resource{parentResource, childResource} { - Expect(eng.Upsert(r.ID, r)).To(Succeed()) - } - assertDocCount(rootResource.ID, "mediatype:file", 1) // only the file - assertDocCount(rootResource.ID, "mediatype:file AND name:child.pdf", 1) // file AND its name - assertDocCount(rootResource.ID, "mediatype:file AND name:nope", 0) - }) - }) - - Context("Highlights", func() { - - It("highlights only for content searches", func() { - parentResource.Document.Name = "baz.pdf" - parentResource.Document.Content = "foo bar baz" - err := eng.Upsert(parentResource.ID, parentResource) - Expect(err).ToNot(HaveOccurred()) - - res, err := doSearch(rootResource.ID, "Name:baz*", "") - Expect(err).ToNot(HaveOccurred()) - Expect(res.TotalMatches).To(Equal(int32(1))) - Expect(res.Matches[0].Entity.Highlights).To(Equal("")) - }) - - It("highlights search terms", func() { - parentResource.Document.Name = "baz.pdf" - parentResource.Document.Content = "foo bar baz" - err := eng.Upsert(parentResource.ID, parentResource) - Expect(err).ToNot(HaveOccurred()) - - res, err := doSearch(rootResource.ID, "Content:bar", "") - Expect(err).ToNot(HaveOccurred()) - Expect(res.TotalMatches).To(Equal(int32(1))) - Expect(res.Matches[0].Entity.Highlights).To(Equal("foo bar baz")) - }) - - }) - - Context("with a file in the root of the space and folder with a file. all of them have the same name", func() { - BeforeEach(func() { - parentResource := search.Resource{ - ID: "1$2!3", - ParentID: rootResource.ID, - RootID: rootResource.ID, - Path: "./doc", - Type: uint64(sprovider.ResourceType_RESOURCE_TYPE_CONTAINER), - Document: content.Document{Name: "doc"}, - } - - childResource := search.Resource{ - ID: "1$2!4", - ParentID: parentResource.ID, - RootID: rootResource.ID, - Path: "./doc/doc.pdf", - Type: uint64(sprovider.ResourceType_RESOURCE_TYPE_FILE), - Document: content.Document{Name: "doc.pdf"}, - } - - childResource2 := search.Resource{ - ID: "1$2!7", - ParentID: parentResource.ID, - RootID: rootResource.ID, - Path: "./doc/file.pdf", - Type: uint64(sprovider.ResourceType_RESOURCE_TYPE_FILE), - Document: content.Document{Name: "file.pdf"}, - } - - rootChildResource := search.Resource{ - ID: "1$2!5", - ParentID: rootResource.ID, - RootID: rootResource.ID, - Path: "./doc.pdf", - Type: uint64(sprovider.ResourceType_RESOURCE_TYPE_FILE), - Document: content.Document{Name: "doc.pdf"}, - } - - rootChildResource2 := search.Resource{ - ID: "1$2!6", - ParentID: rootResource.ID, - RootID: rootResource.ID, - Path: "./file.pdf", - Type: uint64(sprovider.ResourceType_RESOURCE_TYPE_FILE), - Document: content.Document{Name: "file.pdf"}, - } - err := eng.Upsert(parentResource.ID, parentResource) - Expect(err).ToNot(HaveOccurred()) - - err = eng.Upsert(rootChildResource.ID, rootChildResource) - Expect(err).ToNot(HaveOccurred()) - err = eng.Upsert(rootChildResource2.ID, rootChildResource2) - Expect(err).ToNot(HaveOccurred()) - - err = eng.Upsert(childResource.ID, childResource) - Expect(err).ToNot(HaveOccurred()) - err = eng.Upsert(childResource2.ID, childResource2) - Expect(err).ToNot(HaveOccurred()) - }) - It("search *doc* in a root", func() { - res, err := doSearch(rootResource.ID, "Name:*doc*", "") - Expect(err).ToNot(HaveOccurred()) - Expect(res.TotalMatches).To(Equal(int32(3))) - }) - It("search *doc* in a subfolder", func() { - res, err := doSearch(rootResource.ID, "Name:*doc*", "./doc") - Expect(err).ToNot(HaveOccurred()) - Expect(res.TotalMatches).To(Equal(int32(2))) - }) - It("search *file* in a root", func() { - res, err := doSearch(rootResource.ID, "Name:*file*", "") - Expect(err).ToNot(HaveOccurred()) - Expect(res.TotalMatches).To(Equal(int32(2))) - }) - It("search *file* in a subfolder", func() { - res, err := doSearch(rootResource.ID, "Name:*file*", "./doc") - Expect(err).ToNot(HaveOccurred()) - Expect(res.TotalMatches).To(Equal(int32(1))) - }) - }) - - }) - - Describe("path scoped searches", func() { - BeforeEach(func() { - Expect(eng.Upsert(parentResource.ID, parentResource)).To(Succeed()) - Expect(eng.Upsert(childResource.ID, childResource)).To(Succeed()) - Expect(eng.Upsert(childResource2.ID, childResource2)).To(Succeed()) - outside := search.Resource{ - ID: "1$2!6", - ParentID: rootResource.ID, - RootID: rootResource.ID, - Path: "./other/child3.pdf", - Type: uint64(sprovider.ResourceType_RESOURCE_TYPE_FILE), - Document: content.Document{Name: "child3.pdf"}, - } - Expect(eng.Upsert(outside.ID, outside)).To(Succeed()) - }) - - It("restricts hits and totals to the scope at query level", func() { - // without the scope all three children match - res, err := doSearch(rootResource.ID, "name:child*", "") - Expect(err).ToNot(HaveOccurred()) - Expect(res.TotalMatches).To(Equal(int32(3))) - - res, err = doSearch(rootResource.ID, "name:child*", "./parent d!r") - Expect(err).ToNot(HaveOccurred()) - Expect(res.TotalMatches).To(Equal(int32(2))) - Expect(len(res.Matches)).To(Equal(2)) - }) - - It("keeps totals right on a small page", func() { - // the scope is part of the query, so totals cover the full scope - // even when the page holds a single hit - rID, err := storagespace.ParseID(rootResource.ID) - Expect(err).ToNot(HaveOccurred()) - res, err := eng.Search(context.Background(), &searchsvc.SearchIndexRequest{ - Query: "name:child*", - PageSize: 1, - Ref: &searchmsg.Reference{ - ResourceId: &searchmsg.ResourceID{ - StorageId: rID.StorageId, SpaceId: rID.SpaceId, OpaqueId: rID.OpaqueId, - }, - Path: "./parent d!r", - }, - }) - Expect(err).ToNot(HaveOccurred()) - Expect(res.TotalMatches).To(Equal(int32(2))) - Expect(len(res.Matches)).To(Equal(1)) - }) - - It("matches the scope case-sensitively", func() { - res, err := doSearch(rootResource.ID, "name:child*", "./PARENT D!R") - Expect(err).ToNot(HaveOccurred()) - Expect(res.TotalMatches).To(Equal(int32(0))) - }) - }) - - Describe("Upsert", func() { - It("adds a resourceInfo to the index", func() { - err := eng.Upsert(childResource.ID, childResource) - Expect(err).ToNot(HaveOccurred()) - - count, err := idx.DocCount() - Expect(err).ToNot(HaveOccurred()) - Expect(count).To(Equal(uint64(1))) - - query := bleveSearch.NewMatchQuery("child.pdf") - query.SetField("Name") - res, err := idx.Search(bleveSearch.NewSearchRequest(query)) - Expect(err).ToNot(HaveOccurred()) - Expect(res.Hits.Len()).To(Equal(1)) - }) - - It("updates an existing resource in the index", func() { - - err := eng.Upsert(childResource.ID, childResource) - Expect(err).ToNot(HaveOccurred()) - - countA, err := idx.DocCount() - Expect(err).ToNot(HaveOccurred()) - Expect(countA).To(Equal(uint64(1))) - - err = eng.Upsert(childResource.ID, childResource) - Expect(err).ToNot(HaveOccurred()) - - countB, err := idx.DocCount() - Expect(err).ToNot(HaveOccurred()) - Expect(countB).To(Equal(uint64(1))) - }) - }) - - Describe("Delete", func() { - It("marks a resource as deleted", func() { - err := eng.Upsert(childResource.ID, childResource) - Expect(err).ToNot(HaveOccurred()) - - assertDocCount(rootResource.ID, "Name:*child*", 1) - - err = eng.Delete(childResource.ID) - Expect(err).ToNot(HaveOccurred()) - - assertDocCount(rootResource.ID, "Name:*child*", 0) - }) - - It("marks a child resources as deleted", func() { - err := eng.Upsert(parentResource.ID, parentResource) - Expect(err).ToNot(HaveOccurred()) - - err = eng.Upsert(childResource.ID, childResource) - Expect(err).ToNot(HaveOccurred()) - - assertDocCount(rootResource.ID, `"`+parentResource.Document.Name+`"`, 1) - assertDocCount(rootResource.ID, `"`+childResource.Document.Name+`"`, 1) - - err = eng.Delete(parentResource.ID) - Expect(err).ToNot(HaveOccurred()) - - assertDocCount(rootResource.ID, `"`+parentResource.Document.Name+`"`, 0) - assertDocCount(rootResource.ID, `"`+childResource.Document.Name+`"`, 0) - }) - }) - - Describe("Restore", func() { - It("also marks child resources as restored", func() { - err := eng.Upsert(parentResource.ID, parentResource) - Expect(err).ToNot(HaveOccurred()) - - err = eng.Upsert(childResource.ID, childResource) - Expect(err).ToNot(HaveOccurred()) - - err = eng.Delete(parentResource.ID) - Expect(err).ToNot(HaveOccurred()) - - assertDocCount(rootResource.ID, `"`+parentResource.Name+`"`, 0) - assertDocCount(rootResource.ID, `"`+childResource.Name+`"`, 0) - - err = eng.Restore(parentResource.ID) - Expect(err).ToNot(HaveOccurred()) - - assertDocCount(rootResource.ID, `"`+parentResource.Name+`"`, 1) - assertDocCount(rootResource.ID, `"`+childResource.Name+`"`, 1) - }) - }) - - Describe("Purge", func() { - It("removes a resource from the index", func() { - err := eng.Upsert(childResource.ID, childResource) - Expect(err).ToNot(HaveOccurred()) - assertDocCount(rootResource.ID, "Name:child.pdf", 1) - - err = eng.Purge(childResource.ID, false) - - Expect(err).ToNot(HaveOccurred()) - assertDocCount(rootResource.ID, "Name:child.pdf", 0) - }) - It("removes a resource and its children from the index", func() { - err := eng.Upsert(parentResource.ID, parentResource) - Expect(err).ToNot(HaveOccurred()) - err = eng.Upsert(childResource.ID, childResource) - Expect(err).ToNot(HaveOccurred()) - - assertDocCount(rootResource.ID, `"`+parentResource.Document.Name+`"`, 1) - assertDocCount(rootResource.ID, `"`+childResource.Document.Name+`"`, 1) - - err = eng.Purge(parentResource.ID, false) - Expect(err).ToNot(HaveOccurred()) - - assertDocCount(rootResource.ID, `"`+parentResource.Document.Name+`"`, 0) - assertDocCount(rootResource.ID, `"`+childResource.Document.Name+`"`, 0) - }) - It("removes a resource and ignores its children from the index", func() { - err := eng.Upsert(parentResource.ID, parentResource) - Expect(err).ToNot(HaveOccurred()) - - assertDocCount(rootResource.ID, `"`+parentResource.Document.Name+`"`, 1) - - err = eng.Delete(parentResource.ID) - Expect(err).ToNot(HaveOccurred()) - - err = eng.Upsert(childResource.ID, childResource) - Expect(err).ToNot(HaveOccurred()) - - assertDocCount(rootResource.ID, `"`+childResource.Document.Name+`"`, 1) - - err = eng.Purge(parentResource.ID, true) - Expect(err).ToNot(HaveOccurred()) - - assertDocCount(rootResource.ID, `"`+parentResource.Document.Name+`"`, 0) - assertDocCount(rootResource.ID, `"`+childResource.Document.Name+`"`, 1) - }) - }) - - Describe("Move", func() { - It("renames the parent and its child resources", func() { - err := eng.Upsert(parentResource.ID, parentResource) - Expect(err).ToNot(HaveOccurred()) - - err = eng.Upsert(childResource.ID, childResource) - Expect(err).ToNot(HaveOccurred()) - - parentResource.Path = "newname" - err = eng.Move(parentResource.ID, parentResource.ParentID, "./my/newname") - Expect(err).ToNot(HaveOccurred()) - - assertDocCount(rootResource.ID, parentResource.Name, 0) - - matches := assertDocCount(rootResource.ID, "Name:child.pdf", 1) - Expect(matches[0].Entity.ParentId.OpaqueId).To(Equal("3")) - Expect(matches[0].Entity.Ref.Path).To(Equal("./my/newname/child.pdf")) - }) - - DescribeTable("keeps the flag in step with the path", - func(from, target string, hidden bool) { - parentResource.Path = from - parentResource.Hidden = search.IsHidden(from) - childResource.Path = from + "/child.pdf" - childResource.Hidden = parentResource.Hidden - - Expect(eng.Upsert(parentResource.ID, parentResource)).To(Succeed()) - Expect(eng.Upsert(childResource.ID, childResource)).To(Succeed()) - - Expect(eng.Move(parentResource.ID, parentResource.ParentID, target)).To(Succeed()) - - for _, id := range []string{parentResource.ID, childResource.ID} { - Expect(hiddenByID(idx, id)). - To(Equal(hidden), "%s after moving from %s to %s", id, from, target) - } - }, - Entry("into a dot folder", "./parent", "./.trash/parent", true), - Entry("into a plain folder", "./parent", "./archive/parent", false), - Entry("renamed with a leading dot", "./parent", "./.parent", true), - Entry("out of a dot folder", "./.trash/parent", "./archive/parent", false), - Entry("renamed without the leading dot", "./.parent", "./parent", false), - Entry("within the same dot folder", "./.trash/parent", "./.trash/moved", true), - ) - - // the trash leaves the path alone, so the flag has to come through untouched - It("carries the flag through the trash and back", func() { - childResource.Path = "./.secret/file.txt" - childResource.Hidden = true - Expect(eng.Upsert(childResource.ID, childResource)).To(Succeed()) - - Expect(eng.Delete(childResource.ID)).To(Succeed()) - Expect(hiddenByID(idx, childResource.ID)).To(BeTrue(), "after trashing") - - Expect(eng.Restore(childResource.ID)).To(Succeed()) - Expect(hiddenByID(idx, childResource.ID)).To(BeTrue(), "after restoring") - }) - - It("moves the parent and its child resources", func() { - err := eng.Upsert(parentResource.ID, parentResource) - Expect(err).ToNot(HaveOccurred()) - - err = eng.Upsert(childResource.ID, childResource) - Expect(err).ToNot(HaveOccurred()) - - parentResource.Path = " " - parentResource.ParentID = "1$2!somewhereopaqueid" - - err = eng.Move(parentResource.ID, parentResource.ParentID, "./somewhere/else/newname") - Expect(err).ToNot(HaveOccurred()) - assertDocCount(rootResource.ID, `parent d!r`, 0) - - matches := assertDocCount(rootResource.ID, "Name:child.pdf", 1) - Expect(matches[0].Entity.ParentId.OpaqueId).To(Equal("3")) - Expect(matches[0].Entity.Ref.Path).To(Equal("./somewhere/else/newname/child.pdf")) - - matches = assertDocCount(rootResource.ID, `newname`, 1) - Expect(matches[0].Entity.ParentId.OpaqueId).To(Equal("somewhereopaqueid")) - Expect(matches[0].Entity.Ref.Path).To(Equal("./somewhere/else/newname")) - - }) - - It("keeps case-insensitive search working after a move", func() { - Expect(eng.Upsert(parentResource.ID, parentResource)).To(Succeed()) - Expect(eng.Upsert(childResource.ID, childResource)).To(Succeed()) - - Expect(eng.Move(parentResource.ID, parentResource.ParentID, "./my/NewName")).To(Succeed()) - - // the lowercased name sibling is rebuilt, so a case-insensitive - // name query still works; the path is case-sensitive by design, so - // only the exact new path matches (and the old one no longer does). - assertDocCount(rootResource.ID, "name:NEWNAME", 1) - assertDocCount(rootResource.ID, `path:"./my/NewName"`, 2) - assertDocCount(rootResource.ID, `path:"./MY/NEWNAME"`, 0) - assertDocCount(rootResource.ID, `path:"./parent d!r"`, 0) - }) - }) - - Describe("StartBatch", func() { - It("starts a new batch", func() { - b, err := eng.NewBatch(100) - Expect(err).ToNot(HaveOccurred()) - - err = b.Upsert(childResource.ID, childResource) - Expect(err).ToNot(HaveOccurred()) - - count, err := idx.DocCount() - Expect(err).ToNot(HaveOccurred()) - Expect(count).To(Equal(uint64(0))) - - err = b.Push() - Expect(err).ToNot(HaveOccurred()) - - count, err = idx.DocCount() - Expect(err).ToNot(HaveOccurred()) - Expect(count).To(Equal(uint64(1))) - - query := bleveSearch.NewMatchQuery("child.pdf") - query.SetField("Name") - res, err := idx.Search(bleveSearch.NewSearchRequest(query)) - Expect(err).ToNot(HaveOccurred()) - Expect(res.Hits.Len()).To(Equal(1)) - }) - - It("doesn't intertwine different batches", func() { - b, err := eng.NewBatch(100) - Expect(err).ToNot(HaveOccurred()) - - err = b.Upsert(childResource.ID, childResource) - Expect(err).ToNot(HaveOccurred()) - - count, err := idx.DocCount() - Expect(err).ToNot(HaveOccurred()) - Expect(count).To(Equal(uint64(0))) - - b2, err := eng.NewBatch(100) - Expect(err).ToNot(HaveOccurred()) - - err = b2.Upsert(childResource2.ID, childResource2) - Expect(err).ToNot(HaveOccurred()) - - Expect(b.Push()).To(Succeed()) - count, err = idx.DocCount() - Expect(err).ToNot(HaveOccurred()) - Expect(count).To(Equal(uint64(1))) - - Expect(b2.Push()).To(Succeed()) - count, err = idx.DocCount() - Expect(err).ToNot(HaveOccurred()) - Expect(count).To(Equal(uint64(2))) - }) - }) - - Describe("File type specific metadata", func() { - - Context("with audio metadata", func() { - BeforeEach(func() { - resource := search.Resource{ - ID: "1$2!7", - ParentID: rootResource.ID, - RootID: rootResource.ID, - Path: "./some_song.mp3", - Type: uint64(sprovider.ResourceType_RESOURCE_TYPE_FILE), - Document: content.Document{ - Name: "some_song.mp3", - MimeType: "audio/mpeg", - Audio: &libregraph.Audio{ - Album: libregraph.PtrString("Some Album"), - AlbumArtist: libregraph.PtrString("Some AlbumArtist"), - Artist: libregraph.PtrString("Some Artist"), - Bitrate: libregraph.PtrInt64(192), - Composers: libregraph.PtrString("Some Composers"), - Copyright: libregraph.PtrString(""), - Disc: libregraph.PtrInt32(2), - DiscCount: libregraph.PtrInt32(5), - Duration: libregraph.PtrInt64(225000), - Genre: libregraph.PtrString("Some Genre"), - HasDrm: libregraph.PtrBool(false), - IsVariableBitrate: libregraph.PtrBool(true), - Title: libregraph.PtrString("Some Title"), - Track: libregraph.PtrInt32(34), - TrackCount: libregraph.PtrInt32(99), - Year: libregraph.PtrInt32(2004), - }, - }, - } - err := eng.Upsert(resource.ID, resource) - Expect(err).ToNot(HaveOccurred()) - }) - - It("returns audio metadata for search", func() { - matches := assertDocCount(rootResource.ID, `*song*`, 1) - audio := matches[0].Entity.Audio - - Expect(audio).ToNot(BeNil()) - - Expect(audio.Album).To(Equal(libregraph.PtrString("Some Album"))) - Expect(audio.AlbumArtist).To(Equal(libregraph.PtrString("Some AlbumArtist"))) - Expect(audio.Artist).To(Equal(libregraph.PtrString("Some Artist"))) - Expect(audio.Bitrate).To(Equal(libregraph.PtrInt64(192))) - Expect(audio.Composers).To(Equal(libregraph.PtrString("Some Composers"))) - Expect(audio.Copyright).To(Equal(libregraph.PtrString(""))) - Expect(audio.Disc).To(Equal(libregraph.PtrInt32(2))) - Expect(audio.DiscCount).To(Equal(libregraph.PtrInt32(5))) - Expect(audio.Duration).To(Equal(libregraph.PtrInt64(225000))) - Expect(audio.Genre).To(Equal(libregraph.PtrString("Some Genre"))) - Expect(audio.HasDrm).To(Equal(libregraph.PtrBool(false))) - Expect(audio.IsVariableBitrate).To(Equal(libregraph.PtrBool(true))) - Expect(audio.Title).To(Equal(libregraph.PtrString("Some Title"))) - Expect(audio.Track).To(Equal(libregraph.PtrInt32(34))) - Expect(audio.TrackCount).To(Equal(libregraph.PtrInt32(99))) - Expect(audio.Year).To(Equal(libregraph.PtrInt32(2004))) - }) - }) - - Context("with location metadata", func() { - BeforeEach(func() { - resource := search.Resource{ - ID: "1$2!7", - ParentID: rootResource.ID, - RootID: rootResource.ID, - Path: "./team.jpg", - Type: uint64(sprovider.ResourceType_RESOURCE_TYPE_FILE), - Document: content.Document{ - Name: "team.jpg", - MimeType: "image/jpeg", - Location: &libregraph.GeoCoordinates{ - Altitude: libregraph.PtrFloat64(1047.7), - Latitude: libregraph.PtrFloat64(49.48675890884328), - Longitude: libregraph.PtrFloat64(11.103870357204285), - }, - }, - } - err := eng.Upsert(resource.ID, resource) - Expect(err).ToNot(HaveOccurred()) - }) - - It("returns audio metadata for search", func() { - matches := assertDocCount(rootResource.ID, `*team*`, 1) - location := matches[0].Entity.Location - - Expect(location).ToNot(BeNil()) - - Expect(location.Altitude).To(Equal(libregraph.PtrFloat64(1047.7))) - Expect(location.Latitude).To(Equal(libregraph.PtrFloat64(49.48675890884328))) - Expect(location.Longitude).To(Equal(libregraph.PtrFloat64(11.103870357204285))) - }) - }) - }) }) diff --git a/services/search/pkg/opensearch/backend_test.go b/services/search/pkg/opensearch/backend_test.go index 6fad3a5eee..dc83e057b8 100644 --- a/services/search/pkg/opensearch/backend_test.go +++ b/services/search/pkg/opensearch/backend_test.go @@ -1,6 +1,7 @@ package opensearch_test import ( + "context" "testing" . "github.com/onsi/ginkgo/v2" @@ -8,10 +9,7 @@ import ( opensearchgo "github.com/opensearch-project/opensearch-go/v4" opensearchgoAPI "github.com/opensearch-project/opensearch-go/v4/opensearchapi" - "github.com/opencloud-eu/reva/v2/pkg/errtypes" - - searchMessage "github.com/opencloud-eu/opencloud/protogen/gen/opencloud/messages/search/v0" - searchService "github.com/opencloud-eu/opencloud/protogen/gen/opencloud/services/search/v0" + "github.com/opencloud-eu/opencloud/services/search/internal/opensearchtest" "github.com/opencloud-eu/opencloud/services/search/pkg/opensearch" ) @@ -20,6 +18,12 @@ func TestOpenSearchBackend(t *testing.T) { RunSpecs(t, "OpenSearch Backend Suite") } +func deleteIndexOnCleanup(tc *opensearchtest.TestClient, indexName string) { + DeferCleanup(func() { + Expect(tc.IndicesDelete(context.Background(), []string{indexName})).To(Succeed()) + }) +} + var _ = Describe("Backend", func() { Describe("NewBackend", func() { It("fails to create if the cluster is not healthy", func() { @@ -36,256 +40,6 @@ var _ = Describe("Backend", func() { }) }) - Describe("Search", func() { - const indexName = "opencloud-test-engine-search" - - var ( - tc *opensearchtest.TestClient - backend *opensearch.Backend - document search.Resource - ) - - BeforeEach(func() { - tc = opensearchtest.NewDefaultTestClient(GinkgoTB(), defaultConfig.Engine.OpenSearch.Client) - tc.Require.IndicesReset([]string{indexName}) - tc.Require.IndicesCount([]string{indexName}, nil, 0) - deleteIndexOnCleanup(tc, indexName) - - var err error - backend, err = opensearch.NewBackend(indexName, tc.Client()) - Expect(err).ToNot(HaveOccurred()) - - document = opensearchtest.Testdata.Resources.File - Expect(backend.Upsert(document.ID, document)).To(Succeed()) - tc.Require.IndicesRefresh([]string{indexName}, nil) - tc.Require.IndicesCount([]string{indexName}, nil, 1) - }) - - It("performs the most simple search", func() { - resp, err := backend.Search(context.Background(), &searchService.SearchIndexRequest{ - Query: fmt.Sprintf(`"%s"`, document.Name), - }) - Expect(err).ToNot(HaveOccurred()) - Expect(resp.Matches).To(HaveLen(1)) - Expect(resp.TotalMatches).To(Equal(int32(1))) - Expect(fmt.Sprintf("%s$%s!%s", resp.Matches[0].Entity.Id.StorageId, resp.Matches[0].Entity.Id.SpaceId, resp.Matches[0].Entity.Id.OpaqueId)).To(Equal(document.ID)) - }) - - It("ignores files that are marked as deleted", func() { - deletedDocument := opensearchtest.Testdata.Resources.File - deletedDocument.ID = "1$2!4" - deletedDocument.Deleted = true - - Expect(backend.Upsert(deletedDocument.ID, deletedDocument)).To(Succeed()) - tc.Require.IndicesRefresh([]string{indexName}, nil) - tc.Require.IndicesCount([]string{indexName}, nil, 2) - - resp, err := backend.Search(context.Background(), &searchService.SearchIndexRequest{ - Query: fmt.Sprintf(`"%s"`, document.Name), - }) - Expect(err).ToNot(HaveOccurred()) - Expect(resp.Matches).To(HaveLen(1)) - Expect(resp.TotalMatches).To(Equal(int32(1))) - Expect(fmt.Sprintf("%s$%s!%s", resp.Matches[0].Entity.Id.StorageId, resp.Matches[0].Entity.Id.SpaceId, resp.Matches[0].Entity.Id.OpaqueId)).To(Equal(document.ID)) - }) - - It("restricts hits and totals to the path scope", func() { - outside := opensearchtest.Testdata.Resources.File - outside.ID = "1$1!5" - outside.Path = "./other folder/else.jpg" - Expect(backend.Upsert(outside.ID, outside)).To(Succeed()) - tc.Require.IndicesRefresh([]string{indexName}, nil) - - scoped := &searchMessage.Reference{ - ResourceId: &searchMessage.ResourceID{StorageId: "1", SpaceId: "1", OpaqueId: "1"}, - Path: "./parent d!r", - } - resp, err := backend.Search(context.Background(), &searchService.SearchIndexRequest{ - Query: fmt.Sprintf(`"%s"`, document.Name), - Ref: scoped, - }) - Expect(err).ToNot(HaveOccurred()) - Expect(resp.Matches).To(HaveLen(1)) - Expect(resp.TotalMatches).To(Equal(int32(1))) - Expect(resp.Matches[0].Entity.Ref.Path).To(Equal("./parent d!r/child.jpg")) - - // the scope is a reference and matches case-sensitively - wrongCase := &searchMessage.Reference{ - ResourceId: &searchMessage.ResourceID{StorageId: "1", SpaceId: "1", OpaqueId: "1"}, - Path: "./PARENT D!R", - } - respWrongCase, err := backend.Search(context.Background(), &searchService.SearchIndexRequest{ - Query: fmt.Sprintf(`"%s"`, document.Name), - Ref: wrongCase, - }) - Expect(err).ToNot(HaveOccurred()) - Expect(respWrongCase.Matches).To(HaveLen(0)) - Expect(respWrongCase.TotalMatches).To(Equal(int32(0))) - }) - }) - - Describe("FullTextSearch", func() { - const indexName = "opencloud-test-engine-fulltext" - - var ( - tc *opensearchtest.TestClient - backend *opensearch.Backend - ) - - BeforeEach(func() { - tc = opensearchtest.NewDefaultTestClient(GinkgoTB(), defaultConfig.Engine.OpenSearch.Client) - tc.Require.IndicesReset([]string{indexName}) - tc.Require.IndicesCount([]string{indexName}, nil, 0) - deleteIndexOnCleanup(tc, indexName) - - var err error - backend, err = opensearch.NewBackend(indexName, tc.Client()) - Expect(err).ToNot(HaveOccurred()) - - document := opensearchtest.Testdata.Resources.File - document.Content = "Running Foxes" - Expect(backend.Upsert(document.ID, document)).To(Succeed()) - tc.Require.IndicesRefresh([]string{indexName}, nil) - }) - - It("searches content case-insensitively and stemmed, like bleve", func() { - // case-folded and porter-stemmed by the fulltext analyzer; the match - // query analyzes the query value the same way. "content:run*" is an - // unanalyzed wildcard over the stemmed term "run", so it must still - // route to a wildcard query (not degrade to a phrase match). - for _, q := range []string{"content:running", "content:RUNNING", "content:run", "content:run*"} { - resp, err := backend.Search(context.Background(), &searchService.SearchIndexRequest{Query: q}) - Expect(err).ToNot(HaveOccurred(), q) - Expect(resp.Matches).To(HaveLen(1), q) - } - - resp, err := backend.Search(context.Background(), &searchService.SearchIndexRequest{Query: "content:cat"}) - Expect(err).ToNot(HaveOccurred()) - Expect(resp.Matches).To(HaveLen(0)) - }) - }) - - Describe("CaseInsensitiveSearch", func() { - const indexName = "opencloud-test-engine-ci" - - var ( - tc *opensearchtest.TestClient - backend *opensearch.Backend - ) - - BeforeEach(func() { - tc = opensearchtest.NewDefaultTestClient(GinkgoTB(), defaultConfig.Engine.OpenSearch.Client) - tc.Require.IndicesReset([]string{indexName}) - tc.Require.IndicesCount([]string{indexName}, nil, 0) - deleteIndexOnCleanup(tc, indexName) - - var err error - backend, err = opensearch.NewBackend(indexName, tc.Client()) - Expect(err).ToNot(HaveOccurred()) - - folder := opensearchtest.Testdata.Resources.Folder - folder.ID = "1$2!cifolder" - folder.Path = "./My Dir" - folder.Tags = []string{"Work", "Urgent"} - Expect(backend.Upsert(folder.ID, folder)).To(Succeed()) - - child := opensearchtest.Testdata.Resources.File - child.ID = "1$2!cichild" - child.ParentID = folder.ID - child.Path = "./My Dir/report.pdf" - child.Tags = nil - Expect(backend.Upsert(child.ID, child)).To(Succeed()) - - // a doc outside the folder, so the path assertions below discriminate: - // a phrase-matched path query would analyze into the "." prefix and - // match this one too - outside := opensearchtest.Testdata.Resources.File - outside.ID = "1$2!cioutside" - outside.Path = "./other.pdf" - outside.Tags = nil - Expect(backend.Upsert(outside.ID, outside)).To(Succeed()) - tc.Require.IndicesRefresh([]string{indexName}, nil) - }) - - It("matches tags case-insensitively (array sibling)", func() { - for _, q := range []string{"tag:work", "tag:WORK", "Tags:Urgent"} { - resp, err := backend.Search(context.Background(), &searchService.SearchIndexRequest{Query: q}) - Expect(err).ToNot(HaveOccurred(), q) - Expect(resp.Matches).To(HaveLen(1), q) - } - }) - - It("matches a spaced path on the folder and its descendants case-sensitively", func() { - resp, err := backend.Search(context.Background(), &searchService.SearchIndexRequest{Query: `path:"./My Dir"`}) - Expect(err).ToNot(HaveOccurred()) - Expect(resp.Matches).To(HaveLen(2)) // folder itself + the descendant, not the outside doc - - // paths act as references, a wrong-cased path must not match - respWrongCase, err := backend.Search(context.Background(), &searchService.SearchIndexRequest{Query: `path:"./MY DIR"`}) - Expect(err).ToNot(HaveOccurred()) - Expect(respWrongCase.Matches).To(HaveLen(0)) - }) - - It("matches a spaced descendant path only itself", func() { - resp, err := backend.Search(context.Background(), &searchService.SearchIndexRequest{Query: `path:"./My Dir/report.pdf"`}) - Expect(err).ToNot(HaveOccurred()) - Expect(resp.Matches).To(HaveLen(1)) - }) - }) - - Describe("MediaTypeSearch", func() { - const indexName = "opencloud-test-engine-mediatype" - - var ( - tc *opensearchtest.TestClient - backend *opensearch.Backend - ) - - BeforeEach(func() { - tc = opensearchtest.NewDefaultTestClient(GinkgoTB(), defaultConfig.Engine.OpenSearch.Client) - tc.Require.IndicesReset([]string{indexName}) - tc.Require.IndicesCount([]string{indexName}, nil, 0) - deleteIndexOnCleanup(tc, indexName) - - var err error - backend, err = opensearch.NewBackend(indexName, tc.Client()) - Expect(err).ToNot(HaveOccurred()) - - svg := opensearchtest.Testdata.Resources.File - svg.ID = "1$2!svg" - svg.MimeType = "image/svg+xml" - Expect(backend.Upsert(svg.ID, svg)).To(Succeed()) - - png := opensearchtest.Testdata.Resources.File - png.ID = "1$2!png" - png.MimeType = "image/png" - Expect(backend.Upsert(png.ID, png)).To(Succeed()) - - folder := opensearchtest.Testdata.Resources.Folder - folder.ID = "1$2!dir" - folder.MimeType = "httpd/unix-directory" - Expect(backend.Upsert(folder.ID, folder)).To(Succeed()) - tc.Require.IndicesRefresh([]string{indexName}, nil) - }) - - DescribeTable("resolves the media type query", - func(query string, want int) { - resp, err := backend.Search(context.Background(), &searchService.SearchIndexRequest{Query: query}) - Expect(err).ToNot(HaveOccurred(), query) - Expect(resp.Matches).To(HaveLen(want), query) - }, - Entry("image/* wildcard matches both files", "mediatype:image", 2), - Entry("categories are case-insensitive", "mediatype:IMAGE", 2), - Entry("literal MIME (+ and /) via mediatype", "mediatype:image/svg+xml", 1), - Entry("same literal via the raw field name", "MimeType:image/svg+xml", 1), - Entry("literal png MIME", "mediatype:image/png", 1), - Entry("no pdf documents", "mediatype:pdf", 0), - Entry("folder category matches the directory only", "mediatype:folder", 1), - Entry("file category matches both files, not the directory", "mediatype:file", 2), - Entry("file category combined with a term", "mediatype:file AND MimeType:image/png", 1), - ) - }) - Describe("Upsert", func() { const indexName = "opencloud-test-engine-upsert" @@ -295,10 +49,12 @@ var _ = Describe("Backend", func() { ) BeforeEach(func() { + // the backend versions the physical index by schema generation + physical := opensearch.VersionedIndexName(indexName) + tc = opensearchtest.NewDefaultTestClient(GinkgoTB(), defaultConfig.Engine.OpenSearch.Client) - tc.Require.IndicesReset([]string{indexName}) - tc.Require.IndicesCount([]string{indexName}, nil, 0) - deleteIndexOnCleanup(tc, indexName) + tc.Require.IndicesReset([]string{physical}) + deleteIndexOnCleanup(tc, physical) var err error backend, err = opensearch.NewBackend(indexName, tc.Client()) @@ -309,7 +65,7 @@ var _ = Describe("Backend", func() { document := opensearchtest.Testdata.Resources.File Expect(backend.Upsert(document.ID, document)).To(Succeed()) - tc.Require.IndicesCount([]string{indexName}, nil, 1) + tc.Require.IndicesCount([]string{opensearch.VersionedIndexName(indexName)}, nil, 1) }) It("upserts a document without an mtime", func() { @@ -319,566 +75,8 @@ var _ = Describe("Backend", func() { document.Mtime = nil Expect(backend.Upsert(document.ID, document)).To(Succeed()) - tc.Require.IndicesCount([]string{indexName}, nil, 1) + tc.Require.IndicesCount([]string{opensearch.VersionedIndexName(indexName)}, nil, 1) }) }) - Describe("Move", func() { - const indexName = "opencloud-test-engine-move" - - var ( - tc *opensearchtest.TestClient - backend *opensearch.Backend - ) - - BeforeEach(func() { - tc = opensearchtest.NewDefaultTestClient(GinkgoTB(), defaultConfig.Engine.OpenSearch.Client) - tc.Require.IndicesReset([]string{indexName}) - tc.Require.IndicesCount([]string{indexName}, nil, 0) - deleteIndexOnCleanup(tc, indexName) - - var err error - backend, err = opensearch.NewBackend(indexName, tc.Client()) - Expect(err).ToNot(HaveOccurred()) - }) - - It("moves the document to a new path", func() { - document := opensearchtest.Testdata.Resources.File - tc.Require.DocumentCreate(indexName, document.ID, strings.NewReader(opensearchtest.JSONMustMarshal(GinkgoTB(), document))) - tc.Require.IndicesCount([]string{indexName}, nil, 1) - - body := opensearchtest.JSONMustMarshal(GinkgoTB(), map[string]any{ - "query": map[string]any{ - "ids": map[string]any{ - "values": []string{document.ID}, - }, - }, - }) - - resources := opensearchtest.SearchHitsMustBeConverted[search.Resource](GinkgoTB(), tc.Require.Search(indexName, strings.NewReader(body)).Hits) - Expect(resources).To(HaveLen(1)) - Expect(resources[0].Path).To(Equal(document.Path)) - - document.Path = "./new/path/to/resource" - Expect(backend.Move(document.ID, document.ParentID, document.Path)).To(Succeed()) - - resources = opensearchtest.SearchHitsMustBeConverted[search.Resource](GinkgoTB(), tc.Require.Search(indexName, strings.NewReader(body)).Hits) - Expect(resources).To(HaveLen(1)) - Expect(resources[0].Path).To(Equal(document.Path)) - }) - - It("keeps case-sensitive path search working after a move", func() { - // Spaced paths so the queries only stay exact as term queries; a phrase - // match would analyze into the "." prefix and match regardless. - document := opensearchtest.Testdata.Resources.File - document.ID = "1$2!cimove" - document.Path = "./Foo Dir/Bar" - Expect(backend.Upsert(document.ID, document)).To(Succeed()) - tc.Require.IndicesRefresh([]string{indexName}, nil) - - document.Path = "./Moved Dir/Bar" - Expect(backend.Move(document.ID, document.ParentID, document.Path)).To(Succeed()) - tc.Require.IndicesRefresh([]string{indexName}, nil) - - // Path is case-sensitive by design: the exact new path matches, a - // wrong-cased query does not, and the old path no longer matches. - respNew, err := backend.Search(context.Background(), &searchService.SearchIndexRequest{Query: `path:"./Moved Dir/Bar"`}) - Expect(err).ToNot(HaveOccurred()) - Expect(respNew.Matches).To(HaveLen(1)) - - respWrongCase, err := backend.Search(context.Background(), &searchService.SearchIndexRequest{Query: `path:"./MOVED DIR/BAR"`}) - Expect(err).ToNot(HaveOccurred()) - Expect(respWrongCase.Matches).To(HaveLen(0)) - - respOld, err := backend.Search(context.Background(), &searchService.SearchIndexRequest{Query: `path:"./Foo Dir/Bar"`}) - Expect(err).ToNot(HaveOccurred()) - Expect(respOld.Matches).To(HaveLen(0)) - }) - }) - - Describe("WriteVisibility", func() { - const indexName = "opencloud-test-engine-write-visibility" - - It("deletes a record that was just written", func() { - document := opensearchtest.Testdata.Resources.File - document.ID = "1$1!95" - document.Name = "textfile.txt" - document.Path = "./textfile.txt" - - backend, tc := newBackend(indexName) - deleteIndexOnCleanup(tc, indexName) - - Expect(backend.Upsert(document.ID, document)).To(Succeed()) - Expect(backend.Delete(document.ID)).To(Succeed()) - - resp, err := backend.Search(context.Background(), &searchService.SearchIndexRequest{ - Query: fmt.Sprintf(`name:"%s"`, document.Name), - }) - Expect(err).ToNot(HaveOccurred()) - Expect(resp.Matches).To(BeEmpty()) - }) - }) - - Describe("Delete", func() { - const indexName = "opencloud-test-engine-delete" - - var ( - tc *opensearchtest.TestClient - backend *opensearch.Backend - ) - - BeforeEach(func() { - tc = opensearchtest.NewDefaultTestClient(GinkgoTB(), defaultConfig.Engine.OpenSearch.Client) - tc.Require.IndicesReset([]string{indexName}) - tc.Require.IndicesCount([]string{indexName}, nil, 0) - deleteIndexOnCleanup(tc, indexName) - - var err error - backend, err = opensearch.NewBackend(indexName, tc.Client()) - Expect(err).ToNot(HaveOccurred()) - }) - - It("marks the document as deleted", func() { - document := opensearchtest.Testdata.Resources.File - tc.Require.DocumentCreate(indexName, document.ID, strings.NewReader(opensearchtest.JSONMustMarshal(GinkgoTB(), document))) - tc.Require.IndicesCount([]string{indexName}, nil, 1) - - body := opensearchtest.JSONMustMarshal(GinkgoTB(), map[string]any{ - "query": map[string]any{ - "term": map[string]any{ - "Deleted": map[string]any{ - "value": true, - }, - }, - }, - }) - - tc.Require.IndicesCount([]string{indexName}, strings.NewReader(body), 0) - - Expect(backend.Delete(document.ID)).To(Succeed()) - tc.Require.IndicesCount([]string{indexName}, strings.NewReader(body), 1) - }) - }) - - Describe("Restore", func() { - const indexName = "opencloud-test-engine-restore" - - var ( - tc *opensearchtest.TestClient - backend *opensearch.Backend - ) - - BeforeEach(func() { - tc = opensearchtest.NewDefaultTestClient(GinkgoTB(), defaultConfig.Engine.OpenSearch.Client) - tc.Require.IndicesReset([]string{indexName}) - tc.Require.IndicesCount([]string{indexName}, nil, 0) - deleteIndexOnCleanup(tc, indexName) - - var err error - backend, err = opensearch.NewBackend(indexName, tc.Client()) - Expect(err).ToNot(HaveOccurred()) - }) - - It("marks the document as not deleted", func() { - document := opensearchtest.Testdata.Resources.File - document.Deleted = true - tc.Require.DocumentCreate(indexName, document.ID, strings.NewReader(opensearchtest.JSONMustMarshal(GinkgoTB(), document))) - tc.Require.IndicesCount([]string{indexName}, nil, 1) - - body := opensearchtest.JSONMustMarshal(GinkgoTB(), map[string]any{ - "query": map[string]any{ - "term": map[string]any{ - "Deleted": map[string]any{ - "value": true, - }, - }, - }, - }) - - tc.Require.IndicesCount([]string{indexName}, strings.NewReader(body), 1) - - Expect(backend.Restore(document.ID)).To(Succeed()) - tc.Require.IndicesCount([]string{indexName}, strings.NewReader(body), 0) - }) - }) - - Describe("Purge", func() { - const indexName = "opencloud-test-engine-purge" - - var ( - tc *opensearchtest.TestClient - backend *opensearch.Backend - ) - - BeforeEach(func() { - tc = opensearchtest.NewDefaultTestClient(GinkgoTB(), defaultConfig.Engine.OpenSearch.Client) - tc.Require.IndicesReset([]string{indexName}) - tc.Require.IndicesCount([]string{indexName}, nil, 0) - deleteIndexOnCleanup(tc, indexName) - - var err error - backend, err = opensearch.NewBackend(indexName, tc.Client()) - Expect(err).ToNot(HaveOccurred()) - }) - - It("purges a full document", func() { - document := opensearchtest.Testdata.Resources.File - tc.Require.DocumentCreate(indexName, document.ID, strings.NewReader(opensearchtest.JSONMustMarshal(GinkgoTB(), document))) - tc.Require.IndicesCount([]string{indexName}, nil, 1) - - Expect(backend.Purge(document.ID, false)).To(Succeed()) - - tc.Require.IndicesCount([]string{indexName}, nil, 0) - }) - - It("purges resource trees", func() { - resourceFolder := opensearchtest.Testdata.Resources.Folder - tc.Require.DocumentCreate(indexName, resourceFolder.ID, strings.NewReader(opensearchtest.JSONMustMarshal(GinkgoTB(), resourceFolder))) - - resourceFile := opensearchtest.Testdata.Resources.File - tc.Require.DocumentCreate(indexName, resourceFile.ID, strings.NewReader(opensearchtest.JSONMustMarshal(GinkgoTB(), resourceFile))) - - tc.Require.IndicesCount([]string{indexName}, nil, 2) - - Expect(backend.Purge(resourceFolder.ID, false)).To(Succeed()) - - tc.Require.IndicesCount([]string{indexName}, nil, 0) - }) - - It("purges resource trees and ignores undeleted resources", func() { - resourceFolder := opensearchtest.Testdata.Resources.Folder - tc.Require.DocumentCreate(indexName, resourceFolder.ID, strings.NewReader(opensearchtest.JSONMustMarshal(GinkgoTB(), resourceFolder))) - - resourceFile := opensearchtest.Testdata.Resources.File - tc.Require.DocumentCreate(indexName, resourceFile.ID, strings.NewReader(opensearchtest.JSONMustMarshal(GinkgoTB(), resourceFile))) - - tc.Require.IndicesCount([]string{indexName}, nil, 2) - - Expect(backend.Delete(resourceFile.ID)).To(Succeed()) - tc.Require.IndicesRefresh([]string{indexName}, nil) - Expect(backend.Purge(resourceFolder.ID, true)).To(Succeed()) - - tc.Require.IndicesCount([]string{indexName}, nil, 1) - }) - }) - - Describe("PurgeSpace", func() { - const indexName = "opencloud-test-engine-purge-space" - - var ( - tc *opensearchtest.TestClient - backend *opensearch.Backend - ) - - BeforeEach(func() { - tc = opensearchtest.NewDefaultTestClient(GinkgoTB(), defaultConfig.Engine.OpenSearch.Client) - tc.Require.IndicesReset([]string{indexName}) - tc.Require.IndicesCount([]string{indexName}, nil, 0) - deleteIndexOnCleanup(tc, indexName) - - var err error - backend, err = opensearch.NewBackend(indexName, tc.Client()) - Expect(err).ToNot(HaveOccurred()) - }) - - It("takes every record of that space out of the index", func() { - gone := opensearchtest.Testdata.Resources.File - tc.Require.DocumentCreate(indexName, gone.ID, strings.NewReader(opensearchtest.JSONMustMarshal(GinkgoTB(), gone))) - - stays := opensearchtest.Testdata.Resources.File - stays.ID = "1$2!3" - stays.RootID = "1$2!2" - tc.Require.DocumentCreate(indexName, stays.ID, strings.NewReader(opensearchtest.JSONMustMarshal(GinkgoTB(), stays))) - - tc.Require.IndicesCount([]string{indexName}, nil, 2) - - Expect(backend.PurgeSpace(gone.RootID)).To(Succeed()) - - tc.Require.IndicesRefresh([]string{indexName}, nil) - left := opensearchtest.SearchHitsMustBeConverted[search.Resource]( - GinkgoTB(), - tc.Require.Search(indexName, strings.NewReader(`{"query":{"match_all":{}}}`)).Hits, - ) - Expect(left).To(HaveLen(1), "only the records of that space are gone") - Expect(left[0].ID).To(Equal(stays.ID)) - }) - }) - - Describe("Hidden", func() { - const indexName = "opencloud-test-engine-hidden" - - DescribeTable("keeps the flag in step with the path", - func(from, target string, hidden bool) { - folder := opensearchtest.Testdata.Resources.Folder - folder.ID = "1$1!30" - folder.Name = "parent" - folder.Path = from - folder.Hidden = search.IsHidden(from) - - child := opensearchtest.Testdata.Resources.File - child.ID = "1$1!31" - child.Name = "child.txt" - child.Path = from + "/child.txt" - child.ParentID = folder.ID - child.Hidden = folder.Hidden - - backend, tc := newBackend(indexName, folder, child) - deleteIndexOnCleanup(tc, indexName) - tc.Require.IndicesRefresh([]string{indexName}, nil) - - Expect(backend.Move(folder.ID, folder.ParentID, target)).To(Succeed()) - tc.Require.IndicesRefresh([]string{indexName}, nil) - - for _, id := range []string{folder.ID, child.ID} { - Expect(resourceByID(tc, indexName, id).Hidden). - To(Equal(hidden), "%s after moving from %s to %s", id, from, target) - } - }, - Entry("into a dot folder", "./parent", "./.trash/parent", true), - Entry("into a plain folder", "./parent", "./archive/parent", false), - Entry("renamed with a leading dot", "./parent", "./.parent", true), - Entry("out of a dot folder", "./.trash/parent", "./archive/parent", false), - Entry("renamed without the leading dot", "./.parent", "./parent", false), - Entry("within the same dot folder", "./.trash/parent", "./.trash/moved", true), - ) - - It("carries the flag through the trash and back", func() { - hidden := opensearchtest.Testdata.Resources.File - hidden.ID = "1$1!32" - hidden.Path = "./.secret/file.txt" - hidden.Hidden = true - - backend, tc := newBackend(indexName, hidden) - deleteIndexOnCleanup(tc, indexName) - tc.Require.IndicesRefresh([]string{indexName}, nil) - - Expect(backend.Delete(hidden.ID)).To(Succeed()) - tc.Require.IndicesRefresh([]string{indexName}, nil) - Expect(resourceByID(tc, indexName, hidden.ID).Hidden).To(BeTrue(), "after trashing") - - Expect(backend.Restore(hidden.ID)).To(Succeed()) - tc.Require.IndicesRefresh([]string{indexName}, nil) - Expect(resourceByID(tc, indexName, hidden.ID).Hidden).To(BeTrue(), "after restoring") - }) - }) - - Describe("DocCount", func() { - const indexName = "opencloud-test-engine-doc-count" - - var ( - tc *opensearchtest.TestClient - backend *opensearch.Backend - ) - - BeforeEach(func() { - tc = opensearchtest.NewDefaultTestClient(GinkgoTB(), defaultConfig.Engine.OpenSearch.Client) - tc.Require.IndicesReset([]string{indexName}) - tc.Require.IndicesCount([]string{indexName}, nil, 0) - deleteIndexOnCleanup(tc, indexName) - - var err error - backend, err = opensearch.NewBackend(indexName, tc.Client()) - Expect(err).ToNot(HaveOccurred()) - }) - - It("ignores deleted documents", func() { - document := opensearchtest.Testdata.Resources.File - tc.Require.DocumentCreate(indexName, document.ID, strings.NewReader(opensearchtest.JSONMustMarshal(GinkgoTB(), document))) - tc.Require.IndicesCount([]string{indexName}, nil, 1) - - count, err := backend.DocCount() - Expect(err).ToNot(HaveOccurred()) - Expect(count).To(Equal(uint64(1))) - - tc.Require.Update(indexName, document.ID, strings.NewReader(opensearchtest.JSONMustMarshal(GinkgoTB(), map[string]any{ - "doc": map[string]any{ - "Deleted": true, - }, - }))) - - tc.Require.IndicesCount([]string{indexName}, nil, 1) - - count, err = backend.DocCount() - Expect(err).ToNot(HaveOccurred()) - Expect(count).To(Equal(uint64(0))) - }) - }) - - // The following specs ensure that updates which affect a resource and its descendants - // (Delete, Restore, Move) are scoped to the root (space) of the target resource. Two - // resources living in different roots may share the exact same path, so matching by - // path alone would incorrectly update the wrong resource. - Describe("updateSelfAndDescendants root scope", func() { - It("deletes only the resource in the target root", func() { - const indexName = "opencloud-test-engine-root-scope-delete" - - target := opensearchtest.Testdata.Resources.File - other := otherRoot(target) - - backend, tc := newBackend(indexName, target, other) - deleteIndexOnCleanup(tc, indexName) - - Expect(backend.Delete(target.ID)).To(Succeed()) - - Expect(resourceByID(tc, indexName, target.ID).Deleted).To(BeTrue(), "target resource should be marked as deleted") - Expect(resourceByID(tc, indexName, other.ID).Deleted).To(BeFalse(), "resource in a different root must not be affected") - }) - - It("restores only the resource in the target root", func() { - const indexName = "opencloud-test-engine-root-scope-restore" - - target := opensearchtest.Testdata.Resources.File - target.Deleted = true - other := otherRoot(target) - - backend, tc := newBackend(indexName, target, other) - deleteIndexOnCleanup(tc, indexName) - - Expect(backend.Restore(target.ID)).To(Succeed()) - - Expect(resourceByID(tc, indexName, target.ID).Deleted).To(BeFalse(), "target resource should be restored") - Expect(resourceByID(tc, indexName, other.ID).Deleted).To(BeTrue(), "resource in a different root must not be affected") - }) - - It("moves only the resource in the target root", func() { - const indexName = "opencloud-test-engine-root-scope-move" - - target := opensearchtest.Testdata.Resources.File - other := otherRoot(target) - - backend, tc := newBackend(indexName, target, other) - deleteIndexOnCleanup(tc, indexName) - - Expect(backend.Move(target.ID, target.ParentID, "./new/path/to/resource")).To(Succeed()) - - Expect(resourceByID(tc, indexName, target.ID).Path).To(Equal("./new/path/to/resource"), "target resource should be moved") - Expect(resourceByID(tc, indexName, other.ID).Path).To(Equal(other.Path), "resource in a different root must not be moved") - }) - }) - - Describe("SearchInAnalyzedFields", func() { - const indexName = "opencloud-test-engine-search-analyzed-fields" - - var ( - tc *opensearchtest.TestClient - backend *opensearch.Backend - ) - - BeforeEach(func() { - dashed := opensearchtest.Testdata.Resources.Folder - dashed.ID = "1$1!10" - dashed.Name = "new-folder" - dashed.Path = "./new-folder" - dashed.Title = "quarterly report" - - plain := opensearchtest.Testdata.Resources.Folder - plain.ID = "1$1!11" - plain.Name = "documents" - plain.Path = "./documents" - plain.Title = "notes" - - spaced := opensearchtest.Testdata.Resources.Folder - spaced.ID = "1$1!12" - spaced.Name = "foo bar" - spaced.Path = "./foo bar" - spaced.Title = "spaced out" - - backend, tc = newBackend(indexName) - deleteIndexOnCleanup(tc, indexName) - for _, r := range []search.Resource{dashed, plain, spaced} { - Expect(backend.Upsert(r.ID, r)).To(Succeed()) - } - tc.Require.IndicesRefresh([]string{indexName}, nil) - tc.Require.IndicesCount([]string{indexName}, nil, 3) - }) - - DescribeTable("finds what the analyzer made of the value", - func(query string, want []string) { - resp, err := backend.Search(context.Background(), &searchService.SearchIndexRequest{Query: query}) - Expect(err).ToNot(HaveOccurred()) - - names := make([]string, 0, len(resp.Matches)) - for _, match := range resp.Matches { - names = append(names, match.Entity.Name) - } - Expect(names).To(ConsistOf(want)) - }, - Entry("the full name with the dash", "new-folder", []string{"new-folder"}), - Entry("one token of it", "new", []string{"new-folder"}), - Entry("a name without a dash", "documents", []string{"documents"}), - Entry("a wildcard", "*folder*", []string{"new-folder"}), - // the shape the web client sends for every name search - Entry("a wildcard around the whole dashed name", `name:"*new-folder*"`, []string{"new-folder"}), - Entry("a wildcard spanning the dash", `name:"*w-fol*"`, []string{"new-folder"}), - Entry("a wildcard in a different case", `name:"*NEW-FOLDER*"`, []string{"new-folder"}), - Entry("a wildcard spanning a space", `name:"*oo ba*"`, []string{"foo bar"}), - Entry("a wildcard around a name with a space", `name:"*foo bar*"`, []string{"foo bar"}), - Entry("a name with a space", `name:"foo bar"`, []string{"foo bar"}), - Entry("a title of two words", `Title:"quarterly report"`, []string{"new-folder"}), - Entry("one token of a title", "Title:quarterly", []string{"new-folder"}), - ) - }) - - Describe("SearchByTag", func() { - const indexName = "opencloud-test-engine-search-by-tag" - - var ( - tc *opensearchtest.TestClient - backend *opensearch.Backend - ) - - BeforeEach(func() { - tagged := opensearchtest.Testdata.Resources.Folder - tagged.ID = "1$1!20" - tagged.Name = "tagged" - tagged.Path = "./tagged" - tagged.Tags = []string{"foo-bar"} - - other := opensearchtest.Testdata.Resources.Folder - other.ID = "1$1!21" - other.Name = "other" - other.Path = "./other" - other.Tags = []string{"foo"} - - backend, tc = newBackend(indexName) - deleteIndexOnCleanup(tc, indexName) - for _, r := range []search.Resource{tagged, other} { - Expect(backend.Upsert(r.ID, r)).To(Succeed()) - } - tc.Require.IndicesRefresh([]string{indexName}, nil) - tc.Require.IndicesCount([]string{indexName}, nil, 2) - }) - - // a tag is one label, not prose, so it matches as a whole or not at all - DescribeTable("matches a tag as a whole", - func(query string, want []string) { - resp, err := backend.Search(context.Background(), &searchService.SearchIndexRequest{Query: query}) - Expect(err).ToNot(HaveOccurred()) - - names := make([]string, 0, len(resp.Matches)) - for _, match := range resp.Matches { - names = append(names, match.Entity.Name) - } - Expect(names).To(ConsistOf(want)) - }, - Entry("the whole tag", `tag:("foo-bar")`, []string{"tagged"}), - Entry("a token of a tag does not match it", `tag:("foo")`, []string{"other"}), - Entry("a tag in a different case", `tag:("FOO-BAR")`, []string{"tagged"}), - Entry("a wildcard reaches both", `tag:("*foo*")`, []string{"tagged", "other"}), - ) - }) - - Describe("SearchWithAnInvalidQuery", func() { - const indexName = "opencloud-test-engine-search-invalid-query" - - It("answers with a bad request", func() { - backend, tc := newBackend(indexName) - deleteIndexOnCleanup(tc, indexName) - - _, err := backend.Search(context.Background(), &searchService.SearchIndexRequest{Query: "AND mediatype:document"}) - Expect(err).To(HaveOccurred()) - Expect(err).To(BeAssignableToTypeOf(errtypes.BadRequest(""))) - Expect(err.Error()).To(Equal(`error: bad request: the expression can't begin from a binary operator: 'AND'`)) - }) - }) }) diff --git a/services/search/pkg/parity/README.md b/services/search/pkg/parity/README.md index 194397e9ff..51cb469234 100644 --- a/services/search/pkg/parity/README.md +++ b/services/search/pkg/parity/README.md @@ -233,6 +233,7 @@ Fixtures: | FIELDS-12 | `id:"1$1!ab-23"` | no match | no match | no match | ✅ | | FIELDS-13 | `audio.artist:"Some Artist"` | song.mp3 | song.mp3 | song.mp3 | ✅ | | FIELDS-14 | `audio.artist:"some artist"` | song.mp3 | song.mp3 | song.mp3 | ✅ | +| FIELDS-15 | `audio.duration>100` | no match | no match | no match | ✅ | ### deleted diff --git a/services/search/pkg/parity/query_fields_test.go b/services/search/pkg/parity/query_fields_test.go index 2d80fb4f8f..5e3120b688 100644 --- a/services/search/pkg/parity/query_fields_test.go +++ b/services/search/pkg/parity/query_fields_test.go @@ -18,7 +18,7 @@ func fieldsGroup() queryGroup { fixtureDoc("plain.txt"), fixtureFolder("box"), fixtureDoc("boxed.txt", withParent("1$1!box"), withPath("./box/boxed.txt")), - fixtureDoc("song.mp3", withMime("audio/mpeg"), withAudio(&libregraph.Audio{Artist: libregraph.PtrString("Some Artist")})), + fixtureDoc("song.mp3", withMime("audio/mpeg"), withAudio(&libregraph.Audio{Artist: libregraph.PtrString("Some Artist"), Duration: libregraph.PtrInt64(200)})), }, cases: []queryCase{ {id: 1, query: `size:42`, want: []string{"small.txt"}}, @@ -36,6 +36,7 @@ func fieldsGroup() queryGroup { // a facet value keeps its case, the field is not marked lowercase {id: 13, query: `audio.artist:"Some Artist"`, want: []string{"song.mp3"}}, {id: 14, query: `audio.artist:"some artist"`, want: []string{"song.mp3"}}, // facets search case-insensitively + {id: 15, query: `audio.duration>100`}, // number queries are gated to Size and Type on both engines }, } }