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 9d6540c118ce5..9ba66d3100eb1 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 @@ -157,3 +157,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 e6a44a0fba7a4..66811ab3ce44b 100644 --- a/server/src/main/java/module-info.java +++ b/server/src/main/java/module-info.java @@ -429,6 +429,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 70b020eb66ab5..06dce1724d392 100644 --- a/server/src/main/java/org/elasticsearch/synonyms/SynonymsManagementAPIService.java +++ b/server/src/main/java/org/elasticsearch/synonyms/SynonymsManagementAPIService.java @@ -50,6 +50,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; @@ -94,6 +97,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; public static final int INDEX_SEARCHABLE_TIMEOUT_SECONDS = 30; private final int maxSynonymsSets; @@ -185,15 +190,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<>() { @@ -201,11 +224,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 14749330f09da..4cc2e658e798d 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 @@ -14,6 +14,7 @@ org.elasticsearch.rest.action.admin.cluster.GetSnapshotsFeatures 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