diff --git a/docs/changelog/131032.yaml b/docs/changelog/131032.yaml new file mode 100644 index 0000000000000..c7cbc3af0f9c7 --- /dev/null +++ b/docs/changelog/131032.yaml @@ -0,0 +1,5 @@ +pr: 131032 +summary: "Fix: `GET _synonyms` returns synonyms with empty rules" +area: Relevance +type: bug +issues: [] diff --git a/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/synonyms/40_synonyms_sets_get.yml b/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/synonyms/40_synonyms_sets_get.yml index e68c93077bdec..e104c66075576 100644 --- a/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/synonyms/40_synonyms_sets_get.yml +++ b/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/synonyms/40_synonyms_sets_get.yml @@ -163,3 +163,33 @@ setup: - match: count: 12 + +--- +"Return empty rule set": + - requires: + cluster_features: [ synonyms_set.get.return_empty_synonym_sets ] + reason: "synonyms_set get api return empty synonym sets" + + - do: + synonyms.put_synonym: + id: empty-synonyms + body: + synonyms_set: [] + + - do: + synonyms.get_synonyms_sets: {} + + - match: + count: 4 + + - match: + results: + - synonyms_set: "empty-synonyms" + count: 0 + - synonyms_set: "test-synonyms-1" + count: 3 + - synonyms_set: "test-synonyms-2" + count: 1 + - synonyms_set: "test-synonyms-3" + count: 2 + diff --git a/server/src/main/java/module-info.java b/server/src/main/java/module-info.java index c904de605e347..c17148b03d11b 100644 --- a/server/src/main/java/module-info.java +++ b/server/src/main/java/module-info.java @@ -430,6 +430,7 @@ org.elasticsearch.index.mapper.MapperFeatures, org.elasticsearch.index.IndexFeatures, org.elasticsearch.search.SearchFeatures, + org.elasticsearch.synonyms.SynonymFeatures, org.elasticsearch.script.ScriptFeatures, org.elasticsearch.search.retriever.RetrieversFeatures, org.elasticsearch.action.admin.cluster.stats.ClusterStatsFeatures; diff --git a/server/src/main/java/org/elasticsearch/synonyms/SynonymFeatures.java b/server/src/main/java/org/elasticsearch/synonyms/SynonymFeatures.java new file mode 100644 index 0000000000000..b42143ed899a3 --- /dev/null +++ b/server/src/main/java/org/elasticsearch/synonyms/SynonymFeatures.java @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +package org.elasticsearch.synonyms; + +import org.elasticsearch.features.FeatureSpecification; +import org.elasticsearch.features.NodeFeature; + +import java.util.Set; + +public class SynonymFeatures implements FeatureSpecification { + private static final NodeFeature RETURN_EMPTY_SYNONYM_SETS = new NodeFeature("synonyms_set.get.return_empty_synonym_sets"); + + @Override + public Set getTestFeatures() { + return Set.of(RETURN_EMPTY_SYNONYM_SETS); + } +} diff --git a/server/src/main/java/org/elasticsearch/synonyms/SynonymsManagementAPIService.java b/server/src/main/java/org/elasticsearch/synonyms/SynonymsManagementAPIService.java index a3d3af70de28a..e6b6f295874e2 100644 --- a/server/src/main/java/org/elasticsearch/synonyms/SynonymsManagementAPIService.java +++ b/server/src/main/java/org/elasticsearch/synonyms/SynonymsManagementAPIService.java @@ -45,6 +45,9 @@ import org.elasticsearch.indices.SystemIndexDescriptor; import org.elasticsearch.rest.RestStatus; import org.elasticsearch.search.aggregations.BucketOrder; +import org.elasticsearch.search.aggregations.bucket.filter.Filters; +import org.elasticsearch.search.aggregations.bucket.filter.FiltersAggregationBuilder; +import org.elasticsearch.search.aggregations.bucket.filter.FiltersAggregator; import org.elasticsearch.search.aggregations.bucket.terms.Terms; import org.elasticsearch.search.aggregations.bucket.terms.TermsAggregationBuilder; import org.elasticsearch.search.builder.SearchSourceBuilder; @@ -89,6 +92,8 @@ public class SynonymsManagementAPIService { private static final int MAX_SYNONYMS_SETS = 10_000; private static final String SYNONYM_RULE_ID_FIELD = SynonymRule.ID_FIELD.getPreferredName(); private static final String SYNONYM_SETS_AGG_NAME = "synonym_sets_aggr"; + private static final String RULE_COUNT_AGG_NAME = "rule_count"; + private static final String RULE_COUNT_FILTER_KEY = "synonym_rules"; private static final int SYNONYMS_INDEX_MAPPINGS_VERSION = 1; private final int maxSynonymsSets; @@ -179,15 +184,33 @@ private static XContentBuilder mappings() { } } + /** + * Returns all synonym sets with their rule counts, including empty synonym sets. + * @param from The index of the first synonym set to return + * @param size The number of synonym sets to return + * @param listener The listener to return the synonym sets to + */ public void getSynonymsSets(int from, int size, ActionListener> listener) { + BoolQueryBuilder synonymSetQuery = QueryBuilders.boolQuery() + .should(QueryBuilders.termQuery(OBJECT_TYPE_FIELD, SYNONYM_SET_OBJECT_TYPE)) + .should(QueryBuilders.termQuery(OBJECT_TYPE_FIELD, SYNONYM_RULE_OBJECT_TYPE)) + .minimumShouldMatch(1); + + // Aggregation query to count only synonym rules (excluding synonym set objects) + FiltersAggregationBuilder ruleCountAggregation = new FiltersAggregationBuilder( + RULE_COUNT_AGG_NAME, + new FiltersAggregator.KeyedFilter(RULE_COUNT_FILTER_KEY, QueryBuilders.termQuery(OBJECT_TYPE_FIELD, SYNONYM_RULE_OBJECT_TYPE)) + ); + client.prepareSearch(SYNONYMS_ALIAS_NAME) .setSize(0) // Retrieves aggregated synonym rules for each synonym set, excluding the synonym set object type - .setQuery(QueryBuilders.termQuery(OBJECT_TYPE_FIELD, SYNONYM_RULE_OBJECT_TYPE)) + .setQuery(synonymSetQuery) .addAggregation( new TermsAggregationBuilder(SYNONYM_SETS_AGG_NAME).field(SYNONYMS_SET_FIELD) .order(BucketOrder.key(true)) .size(maxSynonymsSets) + .subAggregation(ruleCountAggregation) ) .setPreference(Preference.LOCAL.type()) .execute(new ActionListener<>() { @@ -195,11 +218,11 @@ public void getSynonymsSets(int from, int size, ActionListener buckets = termsAggregation.getBuckets(); - SynonymSetSummary[] synonymSetSummaries = buckets.stream() - .skip(from) - .limit(size) - .map(bucket -> new SynonymSetSummary(bucket.getDocCount(), bucket.getKeyAsString())) - .toArray(SynonymSetSummary[]::new); + SynonymSetSummary[] synonymSetSummaries = buckets.stream().skip(from).limit(size).map(bucket -> { + Filters ruleCountFilters = bucket.getAggregations().get(RULE_COUNT_AGG_NAME); + Filters.Bucket ruleCountBucket = ruleCountFilters.getBucketByKey(RULE_COUNT_FILTER_KEY); + return new SynonymSetSummary(ruleCountBucket.getDocCount(), bucket.getKeyAsString()); + }).toArray(SynonymSetSummary[]::new); listener.onResponse(new PagedResult<>(buckets.size(), synonymSetSummaries)); } diff --git a/server/src/main/resources/META-INF/services/org.elasticsearch.features.FeatureSpecification b/server/src/main/resources/META-INF/services/org.elasticsearch.features.FeatureSpecification index 82c971f9144d8..eaf2f3c2f25c3 100644 --- a/server/src/main/resources/META-INF/services/org.elasticsearch.features.FeatureSpecification +++ b/server/src/main/resources/META-INF/services/org.elasticsearch.features.FeatureSpecification @@ -13,6 +13,7 @@ org.elasticsearch.rest.action.admin.cluster.ClusterRerouteFeatures org.elasticsearch.index.IndexFeatures org.elasticsearch.index.mapper.MapperFeatures org.elasticsearch.search.SearchFeatures +org.elasticsearch.synonyms.SynonymFeatures org.elasticsearch.search.retriever.RetrieversFeatures org.elasticsearch.script.ScriptFeatures org.elasticsearch.cluster.routing.RoutingFeatures