From f271bfb7efa92e9a667b821fc227b43c95a9ecd0 Mon Sep 17 00:00:00 2001 From: Dominik Schmidt Date: Wed, 12 Aug 2026 16:45:06 +0200 Subject: [PATCH] feat(search): native order_by sorting in the opensearch backend Name is analyzed (lowercaseKeyword) and therefore a text field, which OpenSearch refuses to sort on without fielddata; enable it in the index template. The keyword tokenizer emits one term per document, so the fielddata cache stays small. The schema version has not shipped yet, so no reindex is needed. (cherry picked from commit f1d0e7b773b170b119dbd59fa176d656237bed96) --- services/search/pkg/opensearch/backend.go | 21 +++++++++++++++++++ services/search/pkg/opensearch/index.go | 1 - .../pkg/opensearch/internal/osu/request.go | 1 + 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/services/search/pkg/opensearch/backend.go b/services/search/pkg/opensearch/backend.go index c3d7a3e710..96f0d0ce18 100644 --- a/services/search/pkg/opensearch/backend.go +++ b/services/search/pkg/opensearch/backend.go @@ -127,6 +127,26 @@ func (b *Backend) Search(ctx context.Context, sir *searchService.SearchIndexRequ if err != nil { return nil, err } + // Sort natively in the index; the service layer re-establishes this order + // when merging matches across spaces. Score sorting (the default) stays in + // place when no order_by is given. Missing values sort last in both + // directions, matching the cross-space merge. + var sortClause []map[string]any + if orderBy := sir.GetOrderBy(); len(orderBy) > 0 { + sortClause = make([]map[string]any, 0, len(orderBy)+1) + for _, sp := range orderBy { + field, ok := search.SortIndexField(sp.GetName()) + if !ok { + return nil, errtypes.BadRequest(fmt.Sprintf("field %q is not sortable", sp.GetName())) + } + order := "asc" + if sp.GetIsDescending() { + order = "desc" + } + sortClause = append(sortClause, map[string]any{field: map[string]any{"order": order, "missing": "_last"}}) + } + sortClause = append(sortClause, map[string]any{"_score": map[string]any{"order": "desc"}}) + } req, err := osu.BuildSearchReq(&opensearchgoAPI.SearchReq{ Indices: []string{b.index}, @@ -147,6 +167,7 @@ func (b *Backend) Search(ctx context.Context, sir *searchService.SearchIndexRequ }, }, Aggs: builtAggs, + Sort: sortClause, }, ) if err != nil { diff --git a/services/search/pkg/opensearch/index.go b/services/search/pkg/opensearch/index.go index b68b71f302..79b8a98ec3 100644 --- a/services/search/pkg/opensearch/index.go +++ b/services/search/pkg/opensearch/index.go @@ -74,7 +74,6 @@ func buildResourceMapping() ([]byte, error) { if err != nil { return nil, err } - index := map[string]any{ "settings": map[string]any{ "number_of_shards": "1", diff --git a/services/search/pkg/opensearch/internal/osu/request.go b/services/search/pkg/opensearch/internal/osu/request.go index f6f4e238ee..608773c165 100644 --- a/services/search/pkg/opensearch/internal/osu/request.go +++ b/services/search/pkg/opensearch/internal/osu/request.go @@ -96,6 +96,7 @@ func BuildSearchReq(req *opensearchgoAPI.SearchReq, q Builder, p ...SearchBodyPa type SearchBodyParams struct { Highlight *BodyParamHighlight `json:"highlight,omitempty"` Aggs map[string]any `json:"aggs,omitempty"` + Sort []map[string]any `json:"sort,omitempty"` } //----------------------------------------------------------------------------//