From 79e616da3ae888d14bf226abec8d061073ca769e 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 e79e053aa0..b0dda15dc5 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"` } //----------------------------------------------------------------------------//