From f6a18209ef6c70190720d12cd53dda1e33fd849b Mon Sep 17 00:00:00 2001 From: Dominik Schmidt Date: Fri, 11 Sep 2026 07:18:32 +0000 Subject: [PATCH 1/9] fix(search): normalize KQL path once The trailing slash is trimmed in the normalizer instead of in each compiler; an empty path is the space root. --- .../search/pkg/opensearch/internal/convert/kql_transpile.go | 3 --- services/search/pkg/parity/README.md | 2 ++ services/search/pkg/parity/query_path_test.go | 2 ++ services/search/pkg/query/bleve/compiler.go | 3 --- services/search/pkg/query/normalize.go | 6 ++++++ 5 files changed, 10 insertions(+), 6 deletions(-) diff --git a/services/search/pkg/opensearch/internal/convert/kql_transpile.go b/services/search/pkg/opensearch/internal/convert/kql_transpile.go index 9747c7e2b3..101677651e 100644 --- a/services/search/pkg/opensearch/internal/convert/kql_transpile.go +++ b/services/search/pkg/opensearch/internal/convert/kql_transpile.go @@ -119,9 +119,6 @@ func (t kqlOpensearchTranspiler) toBuilder(node ast.Node) (osu.Builder, error) { } field, value := node.Key, node.Value - if query.FieldIsPath(node.Key) { - value = strings.TrimSuffix(value, "/") - } if node.CaseInsensitive { field += mapping.LowercaseSuffix value = strings.ToLower(value) diff --git a/services/search/pkg/parity/README.md b/services/search/pkg/parity/README.md index 186a48badb..3c737aad07 100644 --- a/services/search/pkg/parity/README.md +++ b/services/search/pkg/parity/README.md @@ -250,6 +250,8 @@ Fixtures: | PATH-06 | `path:"./DOCUMENTS"` | docs-upper | docs-upper | docs-upper | ✅ | | PATH-07 | `path:"./Documents"` | docs-mixed | docs-mixed | docs-mixed | ✅ | | PATH-08 | `path:"./parent/"` | child.jpg, parent | child.jpg, parent | child.jpg, parent | ✅ | +| PATH-09 | `path:"/"` | child.jpg, docs-lower, docs-mixed, docs-upper, parent | child.jpg, docs-lower, docs-mixed, docs-upper, parent | child.jpg, docs-lower, docs-mixed, docs-upper, parent | ✅ | +| PATH-10 | `path:""` | child.jpg, docs-lower, docs-mixed, docs-upper, parent | child.jpg, docs-lower, docs-mixed, docs-upper, parent | child.jpg, docs-lower, docs-mixed, docs-upper, parent | ✅ | ### fields diff --git a/services/search/pkg/parity/query_path_test.go b/services/search/pkg/parity/query_path_test.go index e97e8e1596..b4d27f2482 100644 --- a/services/search/pkg/parity/query_path_test.go +++ b/services/search/pkg/parity/query_path_test.go @@ -23,6 +23,8 @@ func pathGroup() queryGroup { {id: 6, query: `path:"./DOCUMENTS"`, want: []string{"docs-upper"}}, {id: 7, query: `path:"./Documents"`, want: []string{"docs-mixed"}}, {id: 8, query: `path:"./parent/"`, want: []string{"parent", "child.jpg"}}, + {id: 9, query: `path:"/"`, want: []string{"parent", "child.jpg", "docs-lower", "docs-upper", "docs-mixed"}}, + {id: 10, query: `path:""`, want: []string{"parent", "child.jpg", "docs-lower", "docs-upper", "docs-mixed"}}, }, } } diff --git a/services/search/pkg/query/bleve/compiler.go b/services/search/pkg/query/bleve/compiler.go index a1dba9a65a..74f7d60dc6 100644 --- a/services/search/pkg/query/bleve/compiler.go +++ b/services/search/pkg/query/bleve/compiler.go @@ -95,9 +95,6 @@ func walk(offset int, nodes []ast.Node) (bleveQuery.Query, int, error) { // bleve treats `/` and `+` as literals mid-term, so a literal MIME like // image/svg+xml still matches exactly. val := n.Value - if searchQuery.FieldIsPath(n.Key) { - val = strings.TrimSuffix(val, "/") - } k := n.Key v := val if k != "ID" && k != "Size" && k != "MimeType" { diff --git a/services/search/pkg/query/normalize.go b/services/search/pkg/query/normalize.go index bce1fd1be6..0bdc5efcf1 100644 --- a/services/search/pkg/query/normalize.go +++ b/services/search/pkg/query/normalize.go @@ -36,6 +36,12 @@ func normalizeNodes(nodes []ast.Node, resolve func(string) string, defaultKey st switch node := n.(type) { case *ast.StringNode: node.Key = resolveKey(node.Key) + if FieldIsPath(node.Key) { + node.Value = strings.TrimSuffix(node.Value, "/") + if node.Value == "" { + node.Value = "." + } + } if FieldValueIsNormalized(node.Key) { node.Value = strings.ToLower(node.Value) } From 29da8274364a899dfd2f0c28bfd3bcc341fb7bef Mon Sep 17 00:00:00 2001 From: Dominik Schmidt Date: Thu, 16 Jul 2026 01:20:28 +0200 Subject: [PATCH 2/9] fix(search): report rejected documents when indexing to OpenSearch --- services/search/pkg/opensearch/batch.go | 13 ++++- services/search/pkg/opensearch/batch_test.go | 53 ++++++++++++++++++++ 2 files changed, 64 insertions(+), 2 deletions(-) create mode 100644 services/search/pkg/opensearch/batch_test.go diff --git a/services/search/pkg/opensearch/batch.go b/services/search/pkg/opensearch/batch.go index 9d53bce1bb..b3b713bd99 100644 --- a/services/search/pkg/opensearch/batch.go +++ b/services/search/pkg/opensearch/batch.go @@ -234,11 +234,20 @@ func (b *Batch) Push() error { body.WriteString("\n") } - if _, err := b.client.Bulk(context.Background(), opensearchgoAPI.BulkReq{ + resp, err := b.client.Bulk(context.Background(), opensearchgoAPI.BulkReq{ Body: strings.NewReader(body.String()), Params: opensearchgoAPI.BulkParams{Refresh: "wait_for"}, - }); err != nil { + }) + switch { + case err != nil: return fmt.Errorf("failed to execute bulk operations: %w", err) + case resp.Errors: + items, err := json.Marshal(resp.Items) + if err != nil { + return fmt.Errorf("failed to marshal bulk response: %w", err) + } + + return fmt.Errorf("failed to execute bulk operations, response: %s", items) } bulkOperations = nil diff --git a/services/search/pkg/opensearch/batch_test.go b/services/search/pkg/opensearch/batch_test.go new file mode 100644 index 0000000000..2b1f57a283 --- /dev/null +++ b/services/search/pkg/opensearch/batch_test.go @@ -0,0 +1,53 @@ +package opensearch_test + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/opencloud-eu/opencloud/services/search/internal/opensearchtest" + "github.com/opencloud-eu/opencloud/services/search/pkg/opensearch" +) + +func TestBatch_Push(t *testing.T) { + tc := opensearchtest.NewDefaultTestClient(t, defaultConfig.Engine.OpenSearch.Client) + + t.Run("reports the documents the bulk API rejected", func(t *testing.T) { + indexName := "opencloud-test-batch-push-rejected" + tc.Require.IndicesReset([]string{indexName}) + defer tc.Require.IndicesDelete([]string{indexName}) + + // Name is a string, mapping it as a long makes every document fail to parse. + tc.Require.IndicesCreate(indexName, strings.NewReader(`{"mappings":{"properties":{"Name":{"type":"long"}}}}`)) + + batch, err := opensearch.NewBatch(tc.Client(), indexName, 10) + require.NoError(t, err) + + document := opensearchtest.Testdata.Resources.File + require.NoError(t, batch.Upsert(document.ID, document)) + + err = batch.Push() + require.Error(t, err) + require.ErrorContains(t, err, document.ID) + require.ErrorContains(t, err, "mapper_parsing_exception") + tc.Require.IndicesCount([]string{indexName}, nil, 0) + }) + + t.Run("pushes the documents the bulk API accepted", func(t *testing.T) { + indexName := "opencloud-test-batch-push-accepted" + tc.Require.IndicesReset([]string{indexName}) + defer tc.Require.IndicesDelete([]string{indexName}) + + tc.Require.IndicesCreate(indexName, strings.NewReader(opensearch.IndexManagerLatest.String())) + + batch, err := opensearch.NewBatch(tc.Client(), indexName, 10) + require.NoError(t, err) + + document := opensearchtest.Testdata.Resources.File + require.NoError(t, batch.Upsert(document.ID, document)) + require.NoError(t, batch.Push()) + + tc.Require.IndicesCount([]string{indexName}, nil, 1) + }) +} From 8aef6c5c22017837b2b60e934b86721abd2b0acc Mon Sep 17 00:00:00 2001 From: Dominik Schmidt Date: Thu, 16 Jul 2026 01:20:28 +0200 Subject: [PATCH 3/9] fix(search): print delete by query failures as json --- services/search/pkg/opensearch/batch.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/services/search/pkg/opensearch/batch.go b/services/search/pkg/opensearch/batch.go index b3b713bd99..783de44762 100644 --- a/services/search/pkg/opensearch/batch.go +++ b/services/search/pkg/opensearch/batch.go @@ -197,7 +197,12 @@ func (b *Batch) Purge(id string, onlyDeleted bool) error { case err != nil: return fmt.Errorf("failed to delete by query: %w", err) case len(resp.Failures) != 0: - return fmt.Errorf("failed to delete by query, failures: %v", resp.Failures) + failures, err := json.Marshal(resp.Failures) + if err != nil { + return fmt.Errorf("failed to marshal delete by query failures: %w", err) + } + + return fmt.Errorf("failed to delete by query, failures: %s", failures) } return nil From ff6b17177ee3d834b6f4b031eb3305e5ca30daa1 Mon Sep 17 00:00:00 2001 From: Dominik Schmidt Date: Thu, 16 Jul 2026 01:28:58 +0200 Subject: [PATCH 4/9] Apply suggestion from @dschmidt --- services/search/pkg/opensearch/batch.go | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/services/search/pkg/opensearch/batch.go b/services/search/pkg/opensearch/batch.go index 783de44762..3e535e8a05 100644 --- a/services/search/pkg/opensearch/batch.go +++ b/services/search/pkg/opensearch/batch.go @@ -247,12 +247,21 @@ func (b *Batch) Push() error { case err != nil: return fmt.Errorf("failed to execute bulk operations: %w", err) case resp.Errors: - items, err := json.Marshal(resp.Items) - if err != nil { - return fmt.Errorf("failed to marshal bulk response: %w", err) + var failed []opensearchgoAPI.BulkRespItem + for _, item := range resp.Items { + for _, result := range item { + if result.Error != nil { + failed = append(failed, result) + } + } } - return fmt.Errorf("failed to execute bulk operations, response: %s", items) + failures, err := json.Marshal(failed) + if err != nil { + return fmt.Errorf("failed to marshal bulk failures: %w", err) + } + + return fmt.Errorf("failed to execute bulk operations, failures: %s", failures) } bulkOperations = nil From da8d490982ac2834480076f475a8d581fb984883 Mon Sep 17 00:00:00 2001 From: Dominik Schmidt Date: Tue, 27 Oct 2026 10:57:27 +0100 Subject: [PATCH 5/9] test(search): port the batch specs to ginkgo --- services/search/pkg/opensearch/batch_test.go | 79 +++++++++++--------- 1 file changed, 42 insertions(+), 37 deletions(-) diff --git a/services/search/pkg/opensearch/batch_test.go b/services/search/pkg/opensearch/batch_test.go index 2b1f57a283..9a78d28668 100644 --- a/services/search/pkg/opensearch/batch_test.go +++ b/services/search/pkg/opensearch/batch_test.go @@ -2,52 +2,57 @@ package opensearch_test import ( "strings" - "testing" - "github.com/stretchr/testify/require" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" "github.com/opencloud-eu/opencloud/services/search/internal/opensearchtest" "github.com/opencloud-eu/opencloud/services/search/pkg/opensearch" ) -func TestBatch_Push(t *testing.T) { - tc := opensearchtest.NewDefaultTestClient(t, defaultConfig.Engine.OpenSearch.Client) +var _ = Describe("Batch", func() { + Describe("Push", func() { + var tc *opensearchtest.TestClient - t.Run("reports the documents the bulk API rejected", func(t *testing.T) { - indexName := "opencloud-test-batch-push-rejected" - tc.Require.IndicesReset([]string{indexName}) - defer tc.Require.IndicesDelete([]string{indexName}) + BeforeEach(func() { + tc = opensearchtest.NewDefaultTestClient(GinkgoTB(), defaultConfig.Engine.OpenSearch.Client) + }) - // Name is a string, mapping it as a long makes every document fail to parse. - tc.Require.IndicesCreate(indexName, strings.NewReader(`{"mappings":{"properties":{"Name":{"type":"long"}}}}`)) + It("reports the documents the bulk API rejected", func() { + const indexName = "opencloud-test-batch-push-rejected" + tc.Require.IndicesReset([]string{indexName}) + deleteIndexOnCleanup(tc, indexName) - batch, err := opensearch.NewBatch(tc.Client(), indexName, 10) - require.NoError(t, err) + // Name is a string, mapping it as a long makes every document fail to parse. + tc.Require.IndicesCreate(indexName, strings.NewReader(`{"mappings":{"properties":{"Name":{"type":"long"}}}}`)) - document := opensearchtest.Testdata.Resources.File - require.NoError(t, batch.Upsert(document.ID, document)) + batch, err := opensearch.NewBatch(tc.Client(), indexName, 10) + Expect(err).ToNot(HaveOccurred()) - err = batch.Push() - require.Error(t, err) - require.ErrorContains(t, err, document.ID) - require.ErrorContains(t, err, "mapper_parsing_exception") - tc.Require.IndicesCount([]string{indexName}, nil, 0) + document := opensearchtest.Testdata.Resources.File + Expect(batch.Upsert(document.ID, document)).To(Succeed()) + + err = batch.Push() + Expect(err).To(MatchError(ContainSubstring(document.ID))) + Expect(err).To(MatchError(ContainSubstring("mapper_parsing_exception"))) + tc.Require.IndicesCount([]string{indexName}, nil, 0) + }) + + It("pushes the documents the bulk API accepted", func() { + const indexName = "opencloud-test-batch-push-accepted" + tc.Require.IndicesReset([]string{indexName}) + deleteIndexOnCleanup(tc, indexName) + + tc.Require.IndicesCreate(indexName, strings.NewReader(opensearch.IndexManagerLatest.String())) + + batch, err := opensearch.NewBatch(tc.Client(), indexName, 10) + Expect(err).ToNot(HaveOccurred()) + + document := opensearchtest.Testdata.Resources.File + Expect(batch.Upsert(document.ID, document)).To(Succeed()) + Expect(batch.Push()).To(Succeed()) + + tc.Require.IndicesCount([]string{indexName}, nil, 1) + }) }) - - t.Run("pushes the documents the bulk API accepted", func(t *testing.T) { - indexName := "opencloud-test-batch-push-accepted" - tc.Require.IndicesReset([]string{indexName}) - defer tc.Require.IndicesDelete([]string{indexName}) - - tc.Require.IndicesCreate(indexName, strings.NewReader(opensearch.IndexManagerLatest.String())) - - batch, err := opensearch.NewBatch(tc.Client(), indexName, 10) - require.NoError(t, err) - - document := opensearchtest.Testdata.Resources.File - require.NoError(t, batch.Upsert(document.ID, document)) - require.NoError(t, batch.Push()) - - tc.Require.IndicesCount([]string{indexName}, nil, 1) - }) -} +}) From 4a720c1a86adafbc49c4b9d6d137fcc76bf61849 Mon Sep 17 00:00:00 2001 From: Dominik Schmidt Date: Fri, 30 Oct 2026 11:23:00 +0100 Subject: [PATCH 6/9] fix(search): resolve KQL path values like the scope path --- services/search/pkg/parity/README.md | 3 +++ services/search/pkg/parity/query_path_test.go | 3 +++ .../search/pkg/query/bleve/compiler_test.go | 2 +- services/search/pkg/query/normalize.go | 6 ++---- services/search/pkg/query/normalize_test.go | 17 +++++++++++++++++ 5 files changed, 26 insertions(+), 5 deletions(-) diff --git a/services/search/pkg/parity/README.md b/services/search/pkg/parity/README.md index 3c737aad07..425762a893 100644 --- a/services/search/pkg/parity/README.md +++ b/services/search/pkg/parity/README.md @@ -252,6 +252,9 @@ Fixtures: | PATH-08 | `path:"./parent/"` | child.jpg, parent | child.jpg, parent | child.jpg, parent | ✅ | | PATH-09 | `path:"/"` | child.jpg, docs-lower, docs-mixed, docs-upper, parent | child.jpg, docs-lower, docs-mixed, docs-upper, parent | child.jpg, docs-lower, docs-mixed, docs-upper, parent | ✅ | | PATH-10 | `path:""` | child.jpg, docs-lower, docs-mixed, docs-upper, parent | child.jpg, docs-lower, docs-mixed, docs-upper, parent | child.jpg, docs-lower, docs-mixed, docs-upper, parent | ✅ | +| PATH-11 | `path:"/parent"` | child.jpg, parent | child.jpg, parent | child.jpg, parent | ✅ | +| PATH-12 | `path:"parent"` | child.jpg, parent | child.jpg, parent | child.jpg, parent | ✅ | +| PATH-13 | `path:"parent/*"` | child.jpg | child.jpg | child.jpg | ✅ | ### fields diff --git a/services/search/pkg/parity/query_path_test.go b/services/search/pkg/parity/query_path_test.go index b4d27f2482..9c20759a71 100644 --- a/services/search/pkg/parity/query_path_test.go +++ b/services/search/pkg/parity/query_path_test.go @@ -25,6 +25,9 @@ func pathGroup() queryGroup { {id: 8, query: `path:"./parent/"`, want: []string{"parent", "child.jpg"}}, {id: 9, query: `path:"/"`, want: []string{"parent", "child.jpg", "docs-lower", "docs-upper", "docs-mixed"}}, {id: 10, query: `path:""`, want: []string{"parent", "child.jpg", "docs-lower", "docs-upper", "docs-mixed"}}, + {id: 11, query: `path:"/parent"`, want: []string{"parent", "child.jpg"}}, + {id: 12, query: `path:"parent"`, want: []string{"parent", "child.jpg"}}, + {id: 13, query: `path:"parent/*"`, want: []string{"child.jpg"}}, }, } } diff --git a/services/search/pkg/query/bleve/compiler_test.go b/services/search/pkg/query/bleve/compiler_test.go index 632a29b387..606c953050 100644 --- a/services/search/pkg/query/bleve/compiler_test.go +++ b/services/search/pkg/query/bleve/compiler_test.go @@ -59,7 +59,7 @@ func Test_compile(t *testing.T) { }, }, want: func() query.Query { - tq := query.NewTermQuery("/Foo") + tq := query.NewTermQuery("./Foo") tq.SetField("Path") return query.NewConjunctionQuery([]query.Query{tq}) }(), diff --git a/services/search/pkg/query/normalize.go b/services/search/pkg/query/normalize.go index 0bdc5efcf1..05e86f1518 100644 --- a/services/search/pkg/query/normalize.go +++ b/services/search/pkg/query/normalize.go @@ -5,6 +5,7 @@ import ( "strings" provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" + "github.com/opencloud-eu/reva/v2/pkg/utils" "reflect" "github.com/opencloud-eu/opencloud/pkg/ast" @@ -37,10 +38,7 @@ func normalizeNodes(nodes []ast.Node, resolve func(string) string, defaultKey st case *ast.StringNode: node.Key = resolveKey(node.Key) if FieldIsPath(node.Key) { - node.Value = strings.TrimSuffix(node.Value, "/") - if node.Value == "" { - node.Value = "." - } + node.Value = utils.MakeRelativePath(node.Value) } if FieldValueIsNormalized(node.Key) { node.Value = strings.ToLower(node.Value) diff --git a/services/search/pkg/query/normalize_test.go b/services/search/pkg/query/normalize_test.go index 94eecfb300..611f7f6b58 100644 --- a/services/search/pkg/query/normalize_test.go +++ b/services/search/pkg/query/normalize_test.go @@ -127,6 +127,23 @@ var _ = Describe("Normalize", func() { })) }) + DescribeTable("normalizes path values to the stored relative form", + func(in, want string) { + got := norm(&ast.StringNode{Key: "path", Value: in}) + Expect(got).To(Equal([]ast.Node{&ast.StringNode{Key: "Path", Value: want}})) + }, + Entry("relative", "./parent", "./parent"), + Entry("trailing slash", "./parent/", "./parent"), + Entry("absolute", "/parent", "./parent"), + Entry("bare", "parent", "./parent"), + Entry("nested bare", "parent/child.jpg", "./parent/child.jpg"), + Entry("root", "/", "."), + Entry("empty", "", "."), + Entry("dot", ".", "."), + Entry("wildcard", "*child*", "./*child*"), + Entry("wildcard trailing slash", "./par*/", "./par*"), + ) + It("converts value nodes to pointers", func() { got := norm( ast.StringNode{Key: "name", Value: "x"}, From ff482878d38fb0973b0733806a780315c006acd0 Mon Sep 17 00:00:00 2001 From: Dominik Schmidt Date: Fri, 30 Oct 2026 11:53:45 +0100 Subject: [PATCH 7/9] fix(search): keep pushing a batch after a rejected bulk, join the errors --- services/search/pkg/opensearch/batch.go | 12 +++++--- services/search/pkg/opensearch/batch_test.go | 31 ++++++++++++++++++++ 2 files changed, 39 insertions(+), 4 deletions(-) diff --git a/services/search/pkg/opensearch/batch.go b/services/search/pkg/opensearch/batch.go index 3e535e8a05..698aba8562 100644 --- a/services/search/pkg/opensearch/batch.go +++ b/services/search/pkg/opensearch/batch.go @@ -228,6 +228,7 @@ func (b *Batch) Push() error { if len(bulkOperations) == 0 { return nil } + defer func() { bulkOperations = nil }() var body strings.Builder for _, operation := range bulkOperations { @@ -264,7 +265,6 @@ func (b *Batch) Push() error { return fmt.Errorf("failed to execute bulk operations, failures: %s", failures) } - bulkOperations = nil return nil } @@ -272,21 +272,25 @@ func (b *Batch) Push() error { // unfortunately, operations like DeleteByQuery cannot be part of the bulk API, // so we need to push the previous bulk operations before executing such operations // this might lead to smaller bulks than the configured size, but ensures correct order + var errs []error for _, operation := range b.operations { switch op := operation.(type) { case func() []map[string]any: bulkOperations = append(bulkOperations, op()...) case func() error: if err := pushBulkOperations(); err != nil { - return fmt.Errorf("failed to push operations: %w", err) + errs = append(errs, fmt.Errorf("failed to push operations: %w", err)) } if err := op(); err != nil { - return fmt.Errorf("failed to execute operation: %w", err) + errs = append(errs, fmt.Errorf("failed to execute operation: %w", err)) } } } + if err := pushBulkOperations(); err != nil { + errs = append(errs, fmt.Errorf("failed to push operations: %w", err)) + } - return pushBulkOperations() + return errors.Join(errs...) } func (b *Batch) withSizeLimit(f func() error) error { diff --git a/services/search/pkg/opensearch/batch_test.go b/services/search/pkg/opensearch/batch_test.go index 9a78d28668..22cdcbfb06 100644 --- a/services/search/pkg/opensearch/batch_test.go +++ b/services/search/pkg/opensearch/batch_test.go @@ -38,6 +38,37 @@ var _ = Describe("Batch", func() { tc.Require.IndicesCount([]string{indexName}, nil, 0) }) + It("keeps executing the operations queued after a rejected bulk", func() { + const indexName = "opencloud-test-batch-push-continue" + tc.Require.IndicesReset([]string{indexName}) + deleteIndexOnCleanup(tc, indexName) + + tc.Require.IndicesCreate(indexName, strings.NewReader(opensearch.IndexManagerLatest.String())) + + seed, err := opensearch.NewBatch(tc.Client(), indexName, 10) + Expect(err).ToNot(HaveOccurred()) + document := opensearchtest.Testdata.Resources.File + Expect(seed.Upsert(document.ID, document)).To(Succeed()) + Expect(seed.Push()).To(Succeed()) + + batch, err := opensearch.NewBatch(tc.Client(), indexName, 10) + Expect(err).ToNot(HaveOccurred()) + + // OpenSearch rejects ids longer than 512 bytes per item, the bulk itself goes through. + rejectedID := strings.Repeat("x", 513) + Expect(batch.Upsert(rejectedID, document)).To(Succeed()) + Expect(batch.Delete(document.ID)).To(Succeed()) + sibling := document + sibling.ID = "1$1!4" + Expect(batch.Upsert(sibling.ID, sibling)).To(Succeed()) + + err = batch.Push() + Expect(err).To(MatchError(ContainSubstring(rejectedID))) + Expect(err).ToNot(MatchError(ContainSubstring(sibling.ID))) + tc.Require.IndicesCount([]string{indexName}, nil, 2) + tc.Require.IndicesCount([]string{indexName}, strings.NewReader(`{"query":{"term":{"Deleted":true}}}`), 1) + }) + It("pushes the documents the bulk API accepted", func() { const indexName = "opencloud-test-batch-push-accepted" tc.Require.IndicesReset([]string{indexName}) From e1f313be9898ae98557a7c70e48276c7eff3a8d9 Mon Sep 17 00:00:00 2001 From: Dominik Schmidt Date: Fri, 30 Oct 2026 12:56:41 +0100 Subject: [PATCH 8/9] fix(search): keep a leading wildcard in place of the KQL path prefix --- services/search/pkg/parity/README.md | 4 ++++ services/search/pkg/parity/query_path_test.go | 4 ++++ services/search/pkg/query/normalize.go | 7 ++++++- services/search/pkg/query/normalize_test.go | 8 ++++++-- 4 files changed, 20 insertions(+), 3 deletions(-) diff --git a/services/search/pkg/parity/README.md b/services/search/pkg/parity/README.md index 425762a893..cb4a7470b2 100644 --- a/services/search/pkg/parity/README.md +++ b/services/search/pkg/parity/README.md @@ -255,6 +255,10 @@ Fixtures: | PATH-11 | `path:"/parent"` | child.jpg, parent | child.jpg, parent | child.jpg, parent | ✅ | | PATH-12 | `path:"parent"` | child.jpg, parent | child.jpg, parent | child.jpg, parent | ✅ | | PATH-13 | `path:"parent/*"` | child.jpg | child.jpg | child.jpg | ✅ | +| PATH-14 | `path:"*/parent"` | child.jpg, parent | child.jpg, parent | child.jpg, parent | ✅ | +| PATH-15 | `path:"?/parent"` | child.jpg, parent | child.jpg, parent | child.jpg, parent | ✅ | +| PATH-16 | `path:"*/Documents"` | docs-mixed | docs-mixed | docs-mixed | ✅ | +| PATH-17 | `path:"*/parent/"` | child.jpg, parent | child.jpg, parent | child.jpg, parent | ✅ | ### fields diff --git a/services/search/pkg/parity/query_path_test.go b/services/search/pkg/parity/query_path_test.go index 9c20759a71..0200a98f68 100644 --- a/services/search/pkg/parity/query_path_test.go +++ b/services/search/pkg/parity/query_path_test.go @@ -28,6 +28,10 @@ func pathGroup() queryGroup { {id: 11, query: `path:"/parent"`, want: []string{"parent", "child.jpg"}}, {id: 12, query: `path:"parent"`, want: []string{"parent", "child.jpg"}}, {id: 13, query: `path:"parent/*"`, want: []string{"child.jpg"}}, + {id: 14, query: `path:"*/parent"`, want: []string{"parent", "child.jpg"}}, + {id: 15, query: `path:"?/parent"`, want: []string{"parent", "child.jpg"}}, + {id: 16, query: `path:"*/Documents"`, want: []string{"docs-mixed"}}, + {id: 17, query: `path:"*/parent/"`, want: []string{"parent", "child.jpg"}}, }, } } diff --git a/services/search/pkg/query/normalize.go b/services/search/pkg/query/normalize.go index 05e86f1518..364a23f683 100644 --- a/services/search/pkg/query/normalize.go +++ b/services/search/pkg/query/normalize.go @@ -38,7 +38,12 @@ func normalizeNodes(nodes []ast.Node, resolve func(string) string, defaultKey st case *ast.StringNode: node.Key = resolveKey(node.Key) if FieldIsPath(node.Key) { - node.Value = utils.MakeRelativePath(node.Value) + if strings.IndexAny(node.Value, "*?") == 0 { + // a leading wildcard stands in for the "./" prefix, anchoring would break it + node.Value = strings.TrimSuffix(node.Value, "/") + } else { + node.Value = utils.MakeRelativePath(node.Value) + } } if FieldValueIsNormalized(node.Key) { node.Value = strings.ToLower(node.Value) diff --git a/services/search/pkg/query/normalize_test.go b/services/search/pkg/query/normalize_test.go index 611f7f6b58..afc9182c3e 100644 --- a/services/search/pkg/query/normalize_test.go +++ b/services/search/pkg/query/normalize_test.go @@ -140,8 +140,12 @@ var _ = Describe("Normalize", func() { Entry("root", "/", "."), Entry("empty", "", "."), Entry("dot", ".", "."), - Entry("wildcard", "*child*", "./*child*"), - Entry("wildcard trailing slash", "./par*/", "./par*"), + Entry("wildcard", "./par*/", "./par*"), + Entry("bare wildcard", "parent/*", "./parent/*"), + Entry("leading wildcard", "*child*", "*child*"), + Entry("leading wildcard covers the prefix", "*/parent", "*/parent"), + Entry("leading single-char wildcard covers the prefix", "?/parent", "?/parent"), + Entry("leading wildcard trailing slash", "*/parent/", "*/parent"), ) It("converts value nodes to pointers", func() { From 5ccce9c7e703e94b10a845db79e0f8561ffe6717 Mon Sep 17 00:00:00 2001 From: opencloudeu Date: Sat, 31 Oct 2026 23:17:01 +0000 Subject: [PATCH 9/9] [tx] updated from transifex --- services/graph/pkg/l10n/locale/sv/LC_MESSAGES/graph.po | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/graph/pkg/l10n/locale/sv/LC_MESSAGES/graph.po b/services/graph/pkg/l10n/locale/sv/LC_MESSAGES/graph.po index 57323d07e4..ddb83fd0a4 100644 --- a/services/graph/pkg/l10n/locale/sv/LC_MESSAGES/graph.po +++ b/services/graph/pkg/l10n/locale/sv/LC_MESSAGES/graph.po @@ -11,7 +11,7 @@ msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: EMAIL\n" -"POT-Creation-Date: 2026-10-10 23:16+0000\n" +"POT-Creation-Date: 2026-10-31 23:16+0000\n" "PO-Revision-Date: 2025-01-27 10:17+0000\n" "Last-Translator: Daniel Nylander , 2026\n" "Language-Team: Swedish (https://app.transifex.com/opencloud-eu/teams/204053/sv/)\n"