Browse Source

Add synonyms sets information to cluster stats (#97900)

Carlos Delgado 2 years ago
parent
commit
c0a99baef5

+ 2 - 1
docs/reference/cluster/stats.asciidoc

@@ -1595,7 +1595,8 @@ The API returns the following response:
         "built_in_char_filters": [],
         "built_in_tokenizers": [],
         "built_in_filters": [],
-        "built_in_analyzers": []
+        "built_in_analyzers": [],
+        "synonyms": {}
       },
       "versions": [
         {

+ 106 - 0
modules/analysis-common/src/yamlRestTest/resources/rest-api-spec/test/cluster.stats/20_analysis_stats_synonyms.yml

@@ -0,0 +1,106 @@
+---
+"get cluster stats returns synonym sets stats":
+
+  - skip:
+      version: " - 8.9.99"
+      reason:  "synonym sets stats are added for v8.10.0"
+
+  - do:
+      cluster.stats: {}
+
+  - length: { indices.analysis.synonyms: 0 }
+
+  - do:
+      indices.create:
+        index: test-index1
+        body:
+          settings:
+            analysis:
+              filter:
+                bigram_max_size:
+                  type: length
+                  max: 16
+                  min: 0
+                synonyms_inline_filter:
+                  type: synonym
+                  synonyms: ["foo bar", "bar => baz"]
+
+                other_inline_filter:
+                  type: synonym
+                  synonyms: ["foo bar baz"]
+
+                synonyms_path_filter:
+                  type: synonym
+                  synonyms_path: "/a/reused/path"
+
+                other_synonyms_path_filter:
+                  type: synonym_graph
+                  synonyms_path: "/a/different/path"
+
+                another_synonyms_path_filter:
+                  type: synonym_graph
+                  synonyms_path: "/another/different/path"
+
+                synonyms_set_filter:
+                  type: synonym_graph
+                  synonyms_set: reused-synonym-set
+
+
+
+  - do:
+      indices.create:
+        index: test-index2
+        body:
+          settings:
+            analysis:
+              filter:
+                en-stem-filter:
+                  name: light_english
+                  type: stemmer
+                  language: light_english
+
+                other_synonyms_filter:
+                  type: synonym
+                  synonyms_set: another-synonym-set
+
+                a_repeated_synonyms_set_filter:
+                  type: synonym
+                  synonyms_set: reused-synonym-set
+
+                repeated_inline_filter:
+                  type: synonym
+                  synonyms: ["foo bar", "bar => baz"]
+
+
+
+  - do:
+      indices.create:
+        index: test-index3
+        body:
+          settings:
+            analysis:
+              filter:
+                other_synonyms_filter:
+                  type: synonym
+                  synonyms_set: a-different-synonym-set
+
+                a_repeated_synonyms_set_filter:
+                  type: synonym
+                  synonyms_set: reused-synonym-set
+
+                more_inline_filter:
+                  type: synonym
+                  synonyms: ["foo bar", "bar => baz"]
+
+
+
+  - do:
+      cluster.stats: {}
+
+  - length: { indices.analysis.synonyms: 3 }
+  - match: { indices.analysis.synonyms.synonyms.count: 4 }
+  - match: { indices.analysis.synonyms.synonyms.index_count: 3 }
+  - match: { indices.analysis.synonyms.synonyms_path.count: 3 }
+  - match: { indices.analysis.synonyms.synonyms_path.index_count: 1 }
+  - match: { indices.analysis.synonyms.synonyms_set.count: 3 }
+  - match: { indices.analysis.synonyms.synonyms_set.index_count: 3 }

+ 2 - 1
server/src/main/java/org/elasticsearch/TransportVersion.java

@@ -167,9 +167,10 @@ public record TransportVersion(int id) implements VersionId<TransportVersion> {
     public static final TransportVersion V_8_500_042 = registerTransportVersion(8_500_042, "763b4801-a4fc-47c4-aff5-7f5a757b8a07");
     public static final TransportVersion V_8_500_043 = registerTransportVersion(8_500_043, "50baabd14-7f5c-4f8c-9351-94e0d397aabc");
     public static final TransportVersion V_8_500_044 = registerTransportVersion(8_500_044, "96b83320-2317-4e9d-b735-356f18c1d76a");
+    public static final TransportVersion V_8_500_045 = registerTransportVersion(8_500_045, "24a596dd-c843-4c0a-90b3-759697d74026");
 
     private static class CurrentHolder {
-        private static final TransportVersion CURRENT = findCurrent(V_8_500_044);
+        private static final TransportVersion CURRENT = findCurrent(V_8_500_045);
 
         // finds the pluggable current version, or uses the given fallback
         private static TransportVersion findCurrent(TransportVersion fallback) {

+ 78 - 4
server/src/main/java/org/elasticsearch/action/admin/cluster/stats/AnalysisStats.java

@@ -8,6 +8,7 @@
 
 package org.elasticsearch.action.admin.cluster.stats;
 
+import org.elasticsearch.TransportVersion;
 import org.elasticsearch.cluster.metadata.IndexMetadata;
 import org.elasticsearch.cluster.metadata.MappingMetadata;
 import org.elasticsearch.cluster.metadata.Metadata;
@@ -33,12 +34,19 @@ import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
+import java.util.TreeMap;
+
+import static org.elasticsearch.TransportVersion.V_8_500_045;
 
 /**
  * Statistics about analysis usage.
  */
 public final class AnalysisStats implements ToXContentFragment, Writeable {
 
+    private static final TransportVersion SYNONYM_SETS_VERSION = V_8_500_045;
+
+    private static final Set<String> SYNONYM_FILTER_TYPES = Set.of("synonym", "synonym_graph");
+
     /**
      * Create {@link AnalysisStats} from the given cluster state.
      */
@@ -51,6 +59,9 @@ public final class AnalysisStats implements ToXContentFragment, Writeable {
         final Map<String, IndexFeatureStats> usedBuiltInTokenizers = new HashMap<>();
         final Map<String, IndexFeatureStats> usedBuiltInTokenFilters = new HashMap<>();
         final Map<String, IndexFeatureStats> usedBuiltInAnalyzers = new HashMap<>();
+        final Map<String, SynonymsStats> usedSynonyms = new HashMap<>();
+        final Set<String> synonymsIdsUsedInIndices = new HashSet<>();
+        final Set<String> synonymsIdsUsed = new HashSet<>();
 
         final Map<MappingMetadata, Integer> mappingCounts = new IdentityHashMap<>(metadata.getMappingsByHash().size());
         for (IndexMetadata indexMetadata : metadata) {
@@ -118,6 +129,13 @@ public final class AnalysisStats implements ToXContentFragment, Writeable {
             Map<String, Settings> tokenFilterSettings = indexSettings.getGroups("index.analysis.filter");
             usedBuiltInTokenFilters.keySet().removeAll(tokenFilterSettings.keySet());
             aggregateAnalysisTypes(tokenFilterSettings.values(), usedTokenFilterTypes, indexTokenFilterTypes);
+            aggregateSynonymsStats(
+                tokenFilterSettings.values(),
+                usedSynonyms,
+                indexMetadata.getIndex().getName(),
+                synonymsIdsUsed,
+                synonymsIdsUsedInIndices
+            );
             countMapping(mappingCounts, indexMetadata);
         }
         for (Map.Entry<MappingMetadata, Integer> mappingAndCount : mappingCounts.entrySet()) {
@@ -147,7 +165,8 @@ public final class AnalysisStats implements ToXContentFragment, Writeable {
             usedBuiltInCharFilters.values(),
             usedBuiltInTokenizers.values(),
             usedBuiltInTokenFilters.values(),
-            usedBuiltInAnalyzers.values()
+            usedBuiltInAnalyzers.values(),
+            usedSynonyms
         );
     }
 
@@ -176,6 +195,39 @@ public final class AnalysisStats implements ToXContentFragment, Writeable {
         }
     }
 
+    private static void aggregateSynonymsStats(
+        Collection<Settings> filterSettings,
+        Map<String, SynonymsStats> synonymsStats,
+        String indexName,
+        Set<String> synonymsIdsUsed,
+        Set<String> synonymIdsUsedInIndices
+    ) {
+        for (Settings filterComponentSettings : filterSettings) {
+            final String type = filterComponentSettings.get("type");
+            if (SYNONYM_FILTER_TYPES.contains(type)) {
+                boolean isInline = false;
+                String synonymRuleType = "synonyms_set";
+                // Avoid requesting settings for synonyms rule type, as it transforms to string a potentially large number of synonym rules
+                String synonymId = filterComponentSettings.get(synonymRuleType);
+                if (synonymId == null) {
+                    synonymRuleType = "synonyms_path";
+                    synonymId = filterComponentSettings.get(synonymRuleType);
+                }
+                if (synonymId == null) {
+                    synonymRuleType = "synonyms";
+                    isInline = true;
+                }
+                SynonymsStats stat = synonymsStats.computeIfAbsent(synonymRuleType, id -> new SynonymsStats());
+                if (synonymIdsUsedInIndices.add(synonymRuleType + indexName)) {
+                    stat.indexCount++;
+                }
+                if (isInline || synonymsIdsUsed.add(synonymRuleType + synonymId)) {
+                    stat.count++;
+                }
+            }
+        }
+    }
+
     private static Set<IndexFeatureStats> sort(Collection<IndexFeatureStats> set) {
         List<IndexFeatureStats> list = new ArrayList<>(set);
         list.sort(Comparator.comparing(IndexFeatureStats::getName));
@@ -185,6 +237,8 @@ public final class AnalysisStats implements ToXContentFragment, Writeable {
     private final Set<IndexFeatureStats> usedCharFilters, usedTokenizers, usedTokenFilters, usedAnalyzers;
     private final Set<IndexFeatureStats> usedBuiltInCharFilters, usedBuiltInTokenizers, usedBuiltInTokenFilters, usedBuiltInAnalyzers;
 
+    private final Map<String, SynonymsStats> usedSynonyms;
+
     AnalysisStats(
         Collection<IndexFeatureStats> usedCharFilters,
         Collection<IndexFeatureStats> usedTokenizers,
@@ -193,7 +247,8 @@ public final class AnalysisStats implements ToXContentFragment, Writeable {
         Collection<IndexFeatureStats> usedBuiltInCharFilters,
         Collection<IndexFeatureStats> usedBuiltInTokenizers,
         Collection<IndexFeatureStats> usedBuiltInTokenFilters,
-        Collection<IndexFeatureStats> usedBuiltInAnalyzers
+        Collection<IndexFeatureStats> usedBuiltInAnalyzers,
+        Map<String, SynonymsStats> usedSynonyms
     ) {
         this.usedCharFilters = sort(usedCharFilters);
         this.usedTokenizers = sort(usedTokenizers);
@@ -203,6 +258,7 @@ public final class AnalysisStats implements ToXContentFragment, Writeable {
         this.usedBuiltInTokenizers = sort(usedBuiltInTokenizers);
         this.usedBuiltInTokenFilters = sort(usedBuiltInTokenFilters);
         this.usedBuiltInAnalyzers = sort(usedBuiltInAnalyzers);
+        this.usedSynonyms = new TreeMap<>(usedSynonyms);
     }
 
     public AnalysisStats(StreamInput input) throws IOException {
@@ -214,6 +270,11 @@ public final class AnalysisStats implements ToXContentFragment, Writeable {
         usedBuiltInTokenizers = Collections.unmodifiableSet(new LinkedHashSet<>(input.readList(IndexFeatureStats::new)));
         usedBuiltInTokenFilters = Collections.unmodifiableSet(new LinkedHashSet<>(input.readList(IndexFeatureStats::new)));
         usedBuiltInAnalyzers = Collections.unmodifiableSet(new LinkedHashSet<>(input.readList(IndexFeatureStats::new)));
+        if (input.getTransportVersion().onOrAfter(SYNONYM_SETS_VERSION)) {
+            usedSynonyms = input.readImmutableMap(SynonymsStats::new);
+        } else {
+            usedSynonyms = Collections.emptyMap();
+        }
     }
 
     @Override
@@ -226,6 +287,9 @@ public final class AnalysisStats implements ToXContentFragment, Writeable {
         out.writeCollection(usedBuiltInTokenizers);
         out.writeCollection(usedBuiltInTokenFilters);
         out.writeCollection(usedBuiltInAnalyzers);
+        if (out.getTransportVersion().onOrAfter(SYNONYM_SETS_VERSION)) {
+            out.writeMap(usedSynonyms, StreamOutput::writeString, (o, v) -> v.writeTo(o));
+        }
     }
 
     /**
@@ -284,6 +348,10 @@ public final class AnalysisStats implements ToXContentFragment, Writeable {
         return usedBuiltInAnalyzers;
     }
 
+    public Map<String, SynonymsStats> getUsedSynonyms() {
+        return usedSynonyms;
+    }
+
     @Override
     public boolean equals(Object o) {
         if (this == o) return true;
@@ -296,7 +364,8 @@ public final class AnalysisStats implements ToXContentFragment, Writeable {
             && Objects.equals(usedBuiltInCharFilters, that.usedBuiltInCharFilters)
             && Objects.equals(usedBuiltInTokenizers, that.usedBuiltInTokenizers)
             && Objects.equals(usedBuiltInTokenFilters, that.usedBuiltInTokenFilters)
-            && Objects.equals(usedBuiltInAnalyzers, that.usedBuiltInAnalyzers);
+            && Objects.equals(usedBuiltInAnalyzers, that.usedBuiltInAnalyzers)
+            && Objects.equals(usedSynonyms, that.usedSynonyms);
     }
 
     @Override
@@ -309,7 +378,8 @@ public final class AnalysisStats implements ToXContentFragment, Writeable {
             usedBuiltInCharFilters,
             usedBuiltInTokenizers,
             usedBuiltInTokenFilters,
-            usedBuiltInAnalyzers
+            usedBuiltInAnalyzers,
+            usedSynonyms
         );
     }
 
@@ -333,7 +403,11 @@ public final class AnalysisStats implements ToXContentFragment, Writeable {
         toXContentCollection(builder, params, "built_in_tokenizers", usedBuiltInTokenizers);
         toXContentCollection(builder, params, "built_in_filters", usedBuiltInTokenFilters);
         toXContentCollection(builder, params, "built_in_analyzers", usedBuiltInAnalyzers);
+        builder.field("synonyms");
+        builder.map(usedSynonyms);
+
         builder.endObject();
+
         return builder;
     }
 

+ 5 - 0
server/src/main/java/org/elasticsearch/action/admin/cluster/stats/IndexFeatureStats.java

@@ -92,4 +92,9 @@ public class IndexFeatureStats implements ToXContentObject, Writeable {
     protected void doXContent(XContentBuilder builder, Params params) throws IOException {
 
     }
+
+    @Override
+    public String toString() {
+        return "IndexFeatureStats{" + "name='" + name + '\'' + ", count=" + count + ", indexCount=" + indexCount + '}';
+    }
 }

+ 87 - 0
server/src/main/java/org/elasticsearch/action/admin/cluster/stats/SynonymsStats.java

@@ -0,0 +1,87 @@
+/*
+ * 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 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 or the Server
+ * Side Public License, v 1.
+ */
+
+package org.elasticsearch.action.admin.cluster.stats;
+
+import org.elasticsearch.common.io.stream.StreamInput;
+import org.elasticsearch.common.io.stream.StreamOutput;
+import org.elasticsearch.common.io.stream.Writeable;
+import org.elasticsearch.xcontent.ToXContentObject;
+import org.elasticsearch.xcontent.XContentBuilder;
+
+import java.io.IOException;
+import java.util.Objects;
+
+/**
+ * Statistics about an index feature.
+ */
+public class SynonymsStats implements ToXContentObject, Writeable {
+
+    int count;
+    int indexCount;
+
+    SynonymsStats() {}
+
+    SynonymsStats(StreamInput in) throws IOException {
+        this.count = in.readVInt();
+        this.indexCount = in.readVInt();
+    }
+
+    @Override
+    public void writeTo(StreamOutput out) throws IOException {
+        out.writeVInt(count);
+        out.writeVInt(indexCount);
+    }
+
+    /**
+     * Return the number of times this synonym type is used across the cluster.
+     */
+    public int getCount() {
+        return count;
+    }
+
+    /**
+     * Return the number of indices that use this synonym type across the cluster.
+     */
+    public int getIndexCount() {
+        return indexCount;
+    }
+
+    @Override
+    public boolean equals(Object other) {
+        if (other instanceof SynonymsStats == false) {
+            return false;
+        }
+        SynonymsStats that = (SynonymsStats) other;
+        return count == that.count && indexCount == that.indexCount;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(count, indexCount);
+    }
+
+    @Override
+    public final XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
+        builder.startObject();
+        builder.field("count", count);
+        builder.field("index_count", indexCount);
+        doXContent(builder, params);
+        builder.endObject();
+        return builder;
+    }
+
+    protected void doXContent(XContentBuilder builder, Params params) throws IOException {
+
+    }
+
+    @Override
+    public String toString() {
+        return "SynonymsStats{count=" + count + ", indexCount=" + indexCount + '}';
+    }
+}

+ 176 - 10
server/src/test/java/org/elasticsearch/action/admin/cluster/stats/AnalysisStatsTests.java

@@ -8,6 +8,7 @@
 
 package org.elasticsearch.action.admin.cluster.stats;
 
+import org.elasticsearch.Version;
 import org.elasticsearch.cluster.metadata.IndexMetadata;
 import org.elasticsearch.cluster.metadata.Metadata;
 import org.elasticsearch.common.io.stream.Writeable.Reader;
@@ -15,13 +16,19 @@ import org.elasticsearch.common.settings.Settings;
 import org.elasticsearch.index.IndexVersion;
 import org.elasticsearch.tasks.TaskCancelledException;
 import org.elasticsearch.test.AbstractWireSerializingTestCase;
+import org.elasticsearch.xcontent.XContentType;
 
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.HashSet;
+import java.util.Map;
 import java.util.Set;
+import java.util.TreeMap;
 
 public class AnalysisStatsTests extends AbstractWireSerializingTestCase<AnalysisStats> {
 
+    private static final String[] SYNONYM_RULES_TYPES = { "synonyms", "synonyms_set", "synonyms_path" };
+
     @Override
     protected Reader<AnalysisStats> instanceReader() {
         return AnalysisStats::new;
@@ -34,6 +41,20 @@ public class AnalysisStatsTests extends AbstractWireSerializingTestCase<Analysis
         return stats;
     }
 
+    private static Map<String, SynonymsStats> randomSynonymsStats() {
+        Map<String, SynonymsStats> result = new HashMap<>();
+        for (String ruleType : SYNONYM_RULES_TYPES) {
+            if (randomBoolean()) {
+                SynonymsStats stats = new SynonymsStats();
+                stats.indexCount = randomIntBetween(1, 10);
+                stats.count = randomIntBetween(stats.indexCount, 100);
+
+                result.put(ruleType, stats);
+            }
+        }
+        return result;
+    }
+
     @Override
     protected AnalysisStats createTestInstance() {
         Set<IndexFeatureStats> charFilters = new HashSet<>();
@@ -75,6 +96,8 @@ public class AnalysisStatsTests extends AbstractWireSerializingTestCase<Analysis
         if (randomBoolean()) {
             builtInAnalyzers.add(randomStats("french"));
         }
+        Map<String, SynonymsStats> synonymsStats = randomSynonymsStats();
+
         return new AnalysisStats(
             charFilters,
             tokenizers,
@@ -83,13 +106,14 @@ public class AnalysisStatsTests extends AbstractWireSerializingTestCase<Analysis
             builtInCharFilters,
             builtInTokenizers,
             builtInTokenFilters,
-            builtInAnalyzers
+            builtInAnalyzers,
+            synonymsStats
         );
     }
 
     @Override
     protected AnalysisStats mutateInstance(AnalysisStats instance) {
-        switch (randomInt(7)) {
+        switch (randomInt(8)) {
             case 0 -> {
                 Set<IndexFeatureStats> charFilters = new HashSet<>(instance.getUsedCharFilterTypes());
                 if (charFilters.removeIf(s -> s.getName().equals("pattern_replace")) == false) {
@@ -103,7 +127,8 @@ public class AnalysisStatsTests extends AbstractWireSerializingTestCase<Analysis
                     instance.getUsedBuiltInCharFilters(),
                     instance.getUsedBuiltInTokenizers(),
                     instance.getUsedBuiltInTokenFilters(),
-                    instance.getUsedBuiltInAnalyzers()
+                    instance.getUsedBuiltInAnalyzers(),
+                    instance.getUsedSynonyms()
                 );
             }
             case 1 -> {
@@ -119,7 +144,8 @@ public class AnalysisStatsTests extends AbstractWireSerializingTestCase<Analysis
                     instance.getUsedBuiltInCharFilters(),
                     instance.getUsedBuiltInTokenizers(),
                     instance.getUsedBuiltInTokenFilters(),
-                    instance.getUsedBuiltInAnalyzers()
+                    instance.getUsedBuiltInAnalyzers(),
+                    instance.getUsedSynonyms()
                 );
             }
             case 2 -> {
@@ -135,7 +161,8 @@ public class AnalysisStatsTests extends AbstractWireSerializingTestCase<Analysis
                     instance.getUsedBuiltInCharFilters(),
                     instance.getUsedBuiltInTokenizers(),
                     instance.getUsedBuiltInTokenFilters(),
-                    instance.getUsedBuiltInAnalyzers()
+                    instance.getUsedBuiltInAnalyzers(),
+                    instance.getUsedSynonyms()
                 );
             }
             case 3 -> {
@@ -151,7 +178,8 @@ public class AnalysisStatsTests extends AbstractWireSerializingTestCase<Analysis
                     instance.getUsedBuiltInCharFilters(),
                     instance.getUsedBuiltInTokenizers(),
                     instance.getUsedBuiltInTokenFilters(),
-                    instance.getUsedBuiltInAnalyzers()
+                    instance.getUsedBuiltInAnalyzers(),
+                    instance.getUsedSynonyms()
                 );
             }
             case 4 -> {
@@ -167,7 +195,8 @@ public class AnalysisStatsTests extends AbstractWireSerializingTestCase<Analysis
                     builtInCharFilters,
                     instance.getUsedBuiltInTokenizers(),
                     instance.getUsedBuiltInTokenFilters(),
-                    instance.getUsedBuiltInAnalyzers()
+                    instance.getUsedBuiltInAnalyzers(),
+                    instance.getUsedSynonyms()
                 );
             }
             case 5 -> {
@@ -183,7 +212,8 @@ public class AnalysisStatsTests extends AbstractWireSerializingTestCase<Analysis
                     instance.getUsedBuiltInCharFilters(),
                     builtInTokenizers,
                     instance.getUsedBuiltInTokenFilters(),
-                    instance.getUsedBuiltInAnalyzers()
+                    instance.getUsedBuiltInAnalyzers(),
+                    instance.getUsedSynonyms()
                 );
             }
             case 6 -> {
@@ -199,7 +229,8 @@ public class AnalysisStatsTests extends AbstractWireSerializingTestCase<Analysis
                     instance.getUsedBuiltInCharFilters(),
                     instance.getUsedBuiltInTokenizers(),
                     builtInTokenFilters,
-                    instance.getUsedBuiltInAnalyzers()
+                    instance.getUsedBuiltInAnalyzers(),
+                    instance.getUsedSynonyms()
                 );
             }
             case 7 -> {
@@ -215,7 +246,21 @@ public class AnalysisStatsTests extends AbstractWireSerializingTestCase<Analysis
                     instance.getUsedBuiltInCharFilters(),
                     instance.getUsedBuiltInTokenizers(),
                     instance.getUsedBuiltInTokenFilters(),
-                    builtInAnalyzers
+                    builtInAnalyzers,
+                    instance.getUsedSynonyms()
+                );
+            }
+            case 8 -> {
+                return new AnalysisStats(
+                    instance.getUsedCharFilterTypes(),
+                    instance.getUsedTokenizerTypes(),
+                    instance.getUsedTokenFilterTypes(),
+                    instance.getUsedAnalyzerTypes(),
+                    instance.getUsedBuiltInCharFilters(),
+                    instance.getUsedBuiltInTokenizers(),
+                    instance.getUsedBuiltInTokenFilters(),
+                    instance.getUsedBuiltInAnalyzers(),
+                    randomValueOtherThan(instance.getUsedSynonyms(), AnalysisStatsTests::randomSynonymsStats)
                 );
             }
             default -> throw new AssertionError();
@@ -280,4 +325,125 @@ public class AnalysisStatsTests extends AbstractWireSerializingTestCase<Analysis
             () -> AnalysisStats.of(metadata, () -> { throw new TaskCancelledException("task cancelled"); })
         );
     }
+
+    public void testSynonymsStats() {
+        final String settingsSourceIndex1 = """
+            {
+              "index": {
+                "analysis": {
+                  "filter": {
+                    "bigram_max_size": {
+                      "type": "length",
+                      "max": "16",
+                      "min": "0"
+                    },
+                    "synonyms_inline_filter": {
+                      "type": "synonym",
+                      "synonyms": ["foo, bar", "bar => baz"]
+                    },
+                    "other_inline_filter": {
+                      "type": "synonym",
+                      "synonyms": ["foo, bar, baz"]
+                    },
+                    "synonyms_path_filter": {
+                      "type": "synonym",
+                      "synonyms_path": "/a/reused/path"
+                    },
+                    "other_synonyms_path_filter": {
+                      "type": "synonym_graph",
+                      "synonyms_path": "/a/different/path"
+                    },
+                    "another_synonyms_path_filter": {
+                      "type": "synonym_graph",
+                      "synonyms_path": "/another/different/path"
+                    },
+                    "synonyms_set_filter": {
+                      "type": "synonym_graph",
+                      "synonyms_set": "reused-synonym-set"
+                    }
+                  }
+                }
+              }
+            }
+            """;
+        Settings settings = indexSettings(Version.CURRENT, 4, 1).loadFromSource(settingsSourceIndex1, XContentType.JSON).build();
+        IndexMetadata indexMetadata1 = new IndexMetadata.Builder("foo").settings(settings).build();
+
+        final String settingsSourceIndex2 = """
+            {
+              "index": {
+                "analysis": {
+                  "filter": {
+                    "en-stem-filter": {
+                      "name": "light_english",
+                      "type": "stemmer",
+                      "language": "light_english"
+                    },
+                    "other_synonyms_filter": {
+                      "type": "synonym",
+                      "synonyms_set": "another-synonym-set"
+                    },
+                    "a_repeated_synonyms_set_filter": {
+                      "type": "synonym",
+                      "synonyms_set": "reused-synonym-set"
+                    },
+                    "repeated_inline_filter": {
+                      "type": "synonym",
+                      "synonyms": ["foo, bar", "bar => baz"]
+                    }
+                  }
+                }
+              }
+            }
+            """;
+        Settings settings2 = indexSettings(Version.CURRENT, 4, 1).loadFromSource(settingsSourceIndex2, XContentType.JSON).build();
+        IndexMetadata indexMetadata2 = new IndexMetadata.Builder("bar").settings(settings2).build();
+
+        final String settingsSourceIndex3 = """
+            {
+              "index": {
+                "analysis": {
+                  "filter": {
+                    "other_synonyms_filter": {
+                      "type": "synonym",
+                      "synonyms_set": "a-different-synonym-set"
+                    },
+                    "a_repeated_synonyms_set_filter": {
+                      "type": "synonym",
+                      "synonyms_set": "reused-synonym-set"
+                    },
+                    "more_inline_filter": {
+                      "type": "synonym",
+                      "synonyms": ["foo, bar", "bar => baz"]
+                    }
+                  }
+                }
+              }
+            }
+            """;
+        Settings settings3 = indexSettings(Version.CURRENT, 4, 1).loadFromSource(settingsSourceIndex3, XContentType.JSON).build();
+        IndexMetadata indexMetadata3 = new IndexMetadata.Builder("baz").settings(settings3).build();
+
+        Metadata metadata = new Metadata.Builder().build()
+            .withAddedIndex(indexMetadata1)
+            .withAddedIndex(indexMetadata2)
+            .withAddedIndex(indexMetadata3);
+        AnalysisStats analysisStats = AnalysisStats.of(metadata, () -> {});
+        SynonymsStats expectedSynonymSetStats = new SynonymsStats();
+        expectedSynonymSetStats.count = 3;
+        expectedSynonymSetStats.indexCount = 3;
+        SynonymsStats expectedSynonymPathStats = new SynonymsStats();
+        expectedSynonymPathStats.count = 3;
+        expectedSynonymPathStats.indexCount = 1;
+        SynonymsStats expectedSynonymInlineStats = new SynonymsStats();
+        expectedSynonymInlineStats.count = 4;
+        expectedSynonymInlineStats.indexCount = 3;
+
+        Map<String, SynonymsStats> expectedSynonymStats = new TreeMap<>();
+        expectedSynonymStats.put("synonyms_set", expectedSynonymSetStats);
+        expectedSynonymStats.put("synonyms_path", expectedSynonymPathStats);
+        expectedSynonymStats.put("synonyms", expectedSynonymInlineStats);
+
+        assertEquals(expectedSynonymStats, analysisStats.getUsedSynonyms());
+    }
 }

+ 2 - 1
x-pack/plugin/monitoring/src/test/java/org/elasticsearch/xpack/monitoring/collector/cluster/ClusterStatsMonitoringDocTests.java

@@ -575,7 +575,8 @@ public class ClusterStatsMonitoringDocTests extends BaseMonitoringDocTestCase<Cl
                     "built_in_char_filters": [],
                     "built_in_tokenizers": [],
                     "built_in_filters": [],
-                    "built_in_analyzers": []
+                    "built_in_analyzers": [],
+                    "synonyms": {}
                   },
                   "versions": [],
                   "search" : {