Browse Source

Output script stats for indexed fields (#71219)

We have recently introduced the ability to associate an indexed field with a script. This commit updates the existing mappings stats to output stats about the script, similar to what we already do for runtime fields.
Luca Cavanna 4 years ago
parent
commit
6422fd5df2

+ 40 - 0
docs/reference/cluster/stats.asciidoc

@@ -502,6 +502,46 @@ Number of fields mapped to the field data type in selected nodes.
 `index_count`::
 (integer)
 Number of indices containing a mapping of the field data type in selected nodes.
+
+`script_count`::
+(integer)
+Number of fields that declare a script.
+
+`lang`::
+(array of strings)
+Script languages used for the optional scripts
+
+`lines_max`::
+(integer)
+Maximum number of lines for a single field script
+
+`lines_total`::
+(integer)
+Total number of lines for the scripts
+
+`chars_max`::
+(integer)
+Maximum number of characters for a single field script
+
+`chars_total`::
+(integer)
+Total number of characters for the scripts
+
+`source_max`::
+(integer)
+Maximum number of accesses to _source for a single field script
+
+`source_total`::
+(integer)
+Total number of accesses to _source for the scripts
+
+`doc_max`::
+(integer)
+Maximum number of accesses to doc_values for a single field script
+
+`doc_total`::
+(integer)
+Total number of accesses to doc_values for the scripts
 ======
 
 `runtime_field_types`::

+ 2 - 2
server/src/internalClusterTest/java/org/elasticsearch/action/admin/cluster/stats/ClusterStatsIT.java

@@ -259,8 +259,8 @@ public class ClusterStatsIT extends ESIntegTestCase {
                 "\"eggplant\":{\"type\":\"integer\"}}}}}").get();
         response = client().admin().cluster().prepareClusterStats().get();
         assertThat(response.getIndicesStats().getMappings().getFieldTypeStats().size(), equalTo(3));
-        Set<IndexFeatureStats> stats = response.getIndicesStats().getMappings().getFieldTypeStats();
-        for (IndexFeatureStats stat : stats) {
+        Set<FieldStats> stats = response.getIndicesStats().getMappings().getFieldTypeStats();
+        for (FieldStats stat : stats) {
             if (stat.getName().equals("integer")) {
                 assertThat(stat.getCount(), greaterThanOrEqualTo(1));
             } else if (stat.getName().equals("keyword")) {

+ 106 - 0
server/src/main/java/org/elasticsearch/action/admin/cluster/stats/FieldScriptStats.java

@@ -0,0 +1,106 @@
+/*
+ * 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.common.xcontent.ToXContentFragment;
+import org.elasticsearch.common.xcontent.XContentBuilder;
+
+import java.io.IOException;
+import java.util.Objects;
+
+/**
+ * Holds stats about the content of a script
+ */
+public final class FieldScriptStats implements Writeable, ToXContentFragment {
+    private long maxLines = 0;
+    private long totalLines = 0;
+    private long maxChars = 0;
+    private long totalChars = 0;
+    private long maxSourceUsages = 0;
+    private long totalSourceUsages = 0;
+    private long maxDocUsages = 0;
+    private long totalDocUsages = 0;
+
+    FieldScriptStats() {
+    }
+
+    FieldScriptStats(StreamInput in) throws IOException {
+        this.maxLines = in.readLong();
+        this.totalLines = in.readLong();
+        this.maxChars = in.readLong();
+        this.totalChars = in.readLong();
+        this.maxSourceUsages = in.readLong();
+        this.totalSourceUsages = in.readLong();
+        this.maxDocUsages = in.readLong();
+        this.totalDocUsages = in.readLong();
+    }
+
+    @Override
+    public void writeTo(StreamOutput out) throws IOException {
+        out.writeLong(maxLines);
+        out.writeLong(totalLines);
+        out.writeLong(maxChars);
+        out.writeLong(totalChars);
+        out.writeLong(maxSourceUsages);
+        out.writeLong(totalSourceUsages);
+        out.writeLong(maxDocUsages);
+        out.writeLong(totalDocUsages);
+    }
+
+    @Override
+    public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
+        builder.field("lines_max", maxLines);
+        builder.field("lines_total", totalLines);
+        builder.field("chars_max", maxChars);
+        builder.field("chars_total", totalChars);
+        builder.field("source_max", maxSourceUsages);
+        builder.field("source_total", totalSourceUsages);
+        builder.field("doc_max", maxDocUsages);
+        builder.field("doc_total", totalDocUsages);
+        return builder;
+    }
+
+    void update(int chars, long lines, int sourceUsages, int docUsages) {
+        this.maxChars = Math.max(this.maxChars, chars);
+        this.totalChars += chars;
+        this.maxLines = Math.max(this.maxLines, lines);
+        this.totalLines += lines;
+        this.totalSourceUsages += sourceUsages;
+        this.maxSourceUsages = Math.max(this.maxSourceUsages, sourceUsages);
+        this.totalDocUsages += docUsages;
+        this.maxDocUsages = Math.max(this.maxDocUsages, docUsages);
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+        FieldScriptStats that = (FieldScriptStats) o;
+        return maxLines == that.maxLines &&
+            totalLines == that.totalLines &&
+            maxChars == that.maxChars &&
+            totalChars == that.totalChars &&
+            maxSourceUsages == that.maxSourceUsages &&
+            totalSourceUsages == that.totalSourceUsages &&
+            maxDocUsages == that.maxDocUsages &&
+            totalDocUsages == that.totalDocUsages;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(maxLines, totalLines, maxChars, totalChars, maxSourceUsages, totalSourceUsages, maxDocUsages, totalDocUsages);
+    }
+}

+ 85 - 0
server/src/main/java/org/elasticsearch/action/admin/cluster/stats/FieldStats.java

@@ -0,0 +1,85 @@
+/*
+ * 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.Version;
+import org.elasticsearch.common.io.stream.StreamInput;
+import org.elasticsearch.common.io.stream.StreamOutput;
+import org.elasticsearch.common.xcontent.XContentBuilder;
+
+import java.io.IOException;
+import java.util.HashSet;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * Holds stats about a mapped field.
+ */
+public final class FieldStats extends IndexFeatureStats {
+    int scriptCount = 0;
+    final Set<String> scriptLangs;
+    final FieldScriptStats fieldScriptStats;
+
+    FieldStats(String name) {
+        super(name);
+        scriptLangs = new HashSet<>();
+        fieldScriptStats = new FieldScriptStats();
+    }
+
+    FieldStats(StreamInput in) throws IOException {
+        super(in);
+        if (in.getVersion().onOrAfter(Version.V_8_0_0)) {
+            scriptCount = in.readVInt();
+            scriptLangs = in.readSet(StreamInput::readString);
+            fieldScriptStats = new FieldScriptStats(in);
+        } else {
+            scriptLangs = new HashSet<>();
+            fieldScriptStats = new FieldScriptStats();
+        }
+    }
+
+    @Override
+    public void writeTo(StreamOutput out) throws IOException {
+        super.writeTo(out);
+        if (out.getVersion().onOrAfter(Version.V_8_0_0)) {
+            out.writeVInt(scriptCount);
+            out.writeCollection(scriptLangs, StreamOutput::writeString);
+            fieldScriptStats.writeTo(out);
+        }
+    }
+
+    @Override
+    protected void doXContent(XContentBuilder builder, Params params) throws IOException {
+        builder.field("script_count", scriptCount);
+        if (scriptCount > 0) {
+            builder.array("lang", scriptLangs.toArray(new String[0]));
+            fieldScriptStats.toXContent(builder, params);
+        }
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+        if (super.equals(o) == false) {
+            return false;
+        }
+        FieldStats that = (FieldStats) o;
+        return scriptCount == that.scriptCount && scriptLangs.equals(that.scriptLangs) && fieldScriptStats.equals(that.fieldScriptStats);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(super.hashCode(), scriptCount, scriptLangs, fieldScriptStats);
+    }
+}

+ 6 - 2
server/src/main/java/org/elasticsearch/action/admin/cluster/stats/IndexFeatureStats.java

@@ -20,7 +20,7 @@ import java.util.Objects;
 /**
  * Statistics about an index feature.
  */
-public final class IndexFeatureStats implements ToXContent, Writeable {
+public class IndexFeatureStats implements ToXContent, Writeable {
 
     final String name;
     int count;
@@ -79,13 +79,17 @@ public final class IndexFeatureStats implements ToXContent, Writeable {
     }
 
     @Override
-    public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
+    public final XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
         builder.startObject();
         builder.field("name", name);
         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 {
+
+    }
 }

+ 35 - 19
server/src/main/java/org/elasticsearch/action/admin/cluster/stats/MappingStats.java

@@ -38,11 +38,14 @@ import java.util.regex.Pattern;
  */
 public final class MappingStats implements ToXContentFragment, Writeable {
 
+    private static final Pattern DOC_PATTERN = Pattern.compile("doc[\\[.]");
+    private static final Pattern SOURCE_PATTERN = Pattern.compile("params\\._source");
+
     /**
      * Create {@link MappingStats} from the given cluster state.
      */
     public static MappingStats of(Metadata metadata, Runnable ensureNotCancelled) {
-        Map<String, IndexFeatureStats> fieldTypes = new HashMap<>();
+        Map<String, FieldStats> fieldTypes = new HashMap<>();
         Set<String> concreteFieldNames = new HashSet<>();
         Map<String, RuntimeFieldStats> runtimeFieldTypes = new HashMap<>();
         for (IndexMetadata indexMetadata : metadata) {
@@ -65,13 +68,23 @@ public final class MappingStats implements ToXContentFragment, Writeable {
                     } else if (fieldMapping.containsKey("properties")) {
                         type = "object";
                     }
-
                     if (type != null) {
-                        IndexFeatureStats stats = fieldTypes.computeIfAbsent(type, IndexFeatureStats::new);
+                        FieldStats stats = fieldTypes.computeIfAbsent(type, FieldStats::new);
                         stats.count++;
                         if (indexFieldTypes.add(type)) {
                             stats.indexCount++;
                         }
+                        Object scriptObject = fieldMapping.get("script");
+                        if (scriptObject instanceof Map) {
+                            Map<?, ?> script = (Map<?, ?>) scriptObject;
+                            Object sourceObject = script.get("source");
+                            stats.scriptCount++;
+                            updateScriptParams(sourceObject, stats.fieldScriptStats);
+                            Object langObject = script.get("lang");
+                            if (langObject != null) {
+                                stats.scriptLangs.add(langObject.toString());
+                            }
+                        }
                     }
                 });
 
@@ -95,14 +108,7 @@ public final class MappingStats implements ToXContentFragment, Writeable {
                     } else if (scriptObject instanceof Map) {
                         Map<?, ?> script = (Map<?, ?>) scriptObject;
                         Object sourceObject = script.get("source");
-                        if (sourceObject != null) {
-                            String scriptSource = sourceObject.toString();
-                            int chars = scriptSource.length();
-                            long lines = scriptSource.lines().count();
-                            int docUsages = countOccurrences(scriptSource, "doc[\\[\\.]");
-                            int sourceUsages = countOccurrences(scriptSource, "params\\._source");
-                            stats.update(chars, lines, sourceUsages, docUsages);
-                        }
+                        updateScriptParams(sourceObject, stats.fieldScriptStats);
                         Object langObject = script.get("lang");
                         if (langObject != null) {
                             stats.scriptLangs.add(langObject.toString());
@@ -114,9 +120,19 @@ public final class MappingStats implements ToXContentFragment, Writeable {
         return new MappingStats(fieldTypes.values(), runtimeFieldTypes.values());
     }
 
-    private static int countOccurrences(String script, String keyword) {
+    private static void updateScriptParams(Object scriptSourceObject, FieldScriptStats scriptStats) {
+        if (scriptSourceObject != null) {
+            String scriptSource = scriptSourceObject.toString();
+            int chars = scriptSource.length();
+            long lines = scriptSource.lines().count();
+            int docUsages = countOccurrences(scriptSource, DOC_PATTERN);
+            int sourceUsages = countOccurrences(scriptSource, SOURCE_PATTERN);
+            scriptStats.update(chars, lines, sourceUsages, docUsages);
+        }
+    }
+
+    private static int countOccurrences(String script, Pattern pattern) {
         int occurrences = 0;
-        Pattern pattern = Pattern.compile(keyword);
         Matcher matcher = pattern.matcher(script);
         while (matcher.find()) {
             occurrences++;
@@ -124,20 +140,20 @@ public final class MappingStats implements ToXContentFragment, Writeable {
         return occurrences;
     }
 
-    private final Set<IndexFeatureStats> fieldTypeStats;
+    private final Set<FieldStats> fieldTypeStats;
     private final Set<RuntimeFieldStats> runtimeFieldStats;
 
-    MappingStats(Collection<IndexFeatureStats> fieldTypeStats, Collection<RuntimeFieldStats> runtimeFieldStats) {
-        List<IndexFeatureStats> stats = new ArrayList<>(fieldTypeStats);
+    MappingStats(Collection<FieldStats> fieldTypeStats, Collection<RuntimeFieldStats> runtimeFieldStats) {
+        List<FieldStats> stats = new ArrayList<>(fieldTypeStats);
         stats.sort(Comparator.comparing(IndexFeatureStats::getName));
-        this.fieldTypeStats = Collections.unmodifiableSet(new LinkedHashSet<IndexFeatureStats>(stats));
+        this.fieldTypeStats = Collections.unmodifiableSet(new LinkedHashSet<>(stats));
         List<RuntimeFieldStats> runtimeStats = new ArrayList<>(runtimeFieldStats);
         runtimeStats.sort(Comparator.comparing(RuntimeFieldStats::type));
         this.runtimeFieldStats = Collections.unmodifiableSet(new LinkedHashSet<>(runtimeStats));
     }
 
     MappingStats(StreamInput in) throws IOException {
-        fieldTypeStats = Collections.unmodifiableSet(new LinkedHashSet<>(in.readList(IndexFeatureStats::new)));
+        fieldTypeStats = Collections.unmodifiableSet(new LinkedHashSet<>(in.readList(FieldStats::new)));
         runtimeFieldStats = Collections.unmodifiableSet(new LinkedHashSet<>(in.readList(RuntimeFieldStats::new)));
     }
 
@@ -150,7 +166,7 @@ public final class MappingStats implements ToXContentFragment, Writeable {
     /**
      * Return stats about field types.
      */
-    public Set<IndexFeatureStats> getFieldTypeStats() {
+    public Set<FieldStats> getFieldTypeStats() {
         return fieldTypeStats;
     }
 

+ 7 - 54
server/src/main/java/org/elasticsearch/action/admin/cluster/stats/RuntimeFieldStats.java

@@ -21,23 +21,17 @@ import java.util.Set;
 
 public final class RuntimeFieldStats implements Writeable, ToXContentObject {
     private final String type;
+    final FieldScriptStats fieldScriptStats;
     int count = 0;
     int indexCount = 0;
     final Set<String> scriptLangs;
     long scriptLessCount = 0;
     long shadowedCount = 0;
-    private long maxLines = 0;
-    private long totalLines = 0;
-    private long maxChars = 0;
-    private long totalChars = 0;
-    private long maxSourceUsages = 0;
-    private long totalSourceUsages = 0;
-    private long maxDocUsages = 0;
-    private long totalDocUsages = 0;
 
     RuntimeFieldStats(String type) {
         this.type = Objects.requireNonNull(type);
         this.scriptLangs = new HashSet<>();
+        this.fieldScriptStats = new FieldScriptStats();
     }
 
     public RuntimeFieldStats(StreamInput in) throws IOException {
@@ -47,14 +41,7 @@ public final class RuntimeFieldStats implements Writeable, ToXContentObject {
         this.scriptLangs = in.readSet(StreamInput::readString);
         this.scriptLessCount = in.readLong();
         this.shadowedCount = in.readLong();
-        this.maxLines = in.readLong();
-        this.totalLines = in.readLong();
-        this.maxChars = in.readLong();
-        this.totalChars = in.readLong();
-        this.maxSourceUsages = in.readLong();
-        this.totalSourceUsages = in.readLong();
-        this.maxDocUsages = in.readLong();
-        this.totalDocUsages = in.readLong();
+        this.fieldScriptStats = new FieldScriptStats(in);
     }
 
     String type() {
@@ -69,14 +56,7 @@ public final class RuntimeFieldStats implements Writeable, ToXContentObject {
         out.writeCollection(scriptLangs, StreamOutput::writeString);
         out.writeLong(scriptLessCount);
         out.writeLong(shadowedCount);
-        out.writeLong(maxLines);
-        out.writeLong(totalLines);
-        out.writeLong(maxChars);
-        out.writeLong(totalChars);
-        out.writeLong(maxSourceUsages);
-        out.writeLong(totalSourceUsages);
-        out.writeLong(maxDocUsages);
-        out.writeLong(totalDocUsages);
+        fieldScriptStats.writeTo(out);
     }
 
     @Override
@@ -88,29 +68,11 @@ public final class RuntimeFieldStats implements Writeable, ToXContentObject {
         builder.field("scriptless_count", scriptLessCount);
         builder.field("shadowed_count", shadowedCount);
         builder.array("lang", scriptLangs.toArray(new String[0]));
-        builder.field("lines_max", maxLines);
-        builder.field("lines_total", totalLines);
-        builder.field("chars_max", maxChars);
-        builder.field("chars_total", totalChars);
-        builder.field("source_max", maxSourceUsages);
-        builder.field("source_total", totalSourceUsages);
-        builder.field("doc_max", maxDocUsages);
-        builder.field("doc_total", totalDocUsages);
+        fieldScriptStats.toXContent(builder, params);
         builder.endObject();
         return builder;
     }
 
-    void update(int chars, long lines, int sourceUsages, int docUsages) {
-        this.maxChars = Math.max(this.maxChars, chars);
-        this.totalChars += chars;
-        this.maxLines = Math.max(this.maxLines, lines);
-        this.totalLines += lines;
-        this.totalSourceUsages += sourceUsages;
-        this.maxSourceUsages = Math.max(this.maxSourceUsages, sourceUsages);
-        this.totalDocUsages += docUsages;
-        this.maxDocUsages = Math.max(this.maxDocUsages, docUsages);
-    }
-
     @Override
     public boolean equals(Object o) {
         if (this == o) {
@@ -124,22 +86,13 @@ public final class RuntimeFieldStats implements Writeable, ToXContentObject {
             indexCount == that.indexCount &&
             scriptLessCount == that.scriptLessCount &&
             shadowedCount == that.shadowedCount &&
-            maxLines == that.maxLines &&
-            totalLines == that.totalLines &&
-            maxChars == that.maxChars &&
-            totalChars == that.totalChars &&
-            maxSourceUsages == that.maxSourceUsages &&
-            totalSourceUsages == that.totalSourceUsages &&
-            maxDocUsages == that.maxDocUsages &&
-            totalDocUsages == that.totalDocUsages &&
+            fieldScriptStats.equals(that.fieldScriptStats) &&
             type.equals(that.type) &&
             scriptLangs.equals(that.scriptLangs);
     }
 
     @Override
     public int hashCode() {
-        return Objects.hash(type, count, indexCount, scriptLangs, scriptLessCount, shadowedCount,
-            maxLines, totalLines, maxChars, totalChars,
-            maxSourceUsages, totalSourceUsages, maxDocUsages, totalDocUsages);
+        return Objects.hash(type, count, indexCount, scriptLangs, scriptLessCount, shadowedCount, fieldScriptStats);
     }
 }

+ 103 - 16
server/src/test/java/org/elasticsearch/action/admin/cluster/stats/MappingStatsTests.java

@@ -12,14 +12,18 @@ import org.elasticsearch.Version;
 import org.elasticsearch.cluster.metadata.IndexMetadata;
 import org.elasticsearch.cluster.metadata.Metadata;
 import org.elasticsearch.common.Strings;
+import org.elasticsearch.common.io.stream.BytesStreamOutput;
+import org.elasticsearch.common.io.stream.StreamInput;
 import org.elasticsearch.common.io.stream.Writeable.Reader;
 import org.elasticsearch.common.settings.Settings;
 import org.elasticsearch.script.Script;
 import org.elasticsearch.tasks.TaskCancelledException;
 import org.elasticsearch.test.AbstractWireSerializingTestCase;
+import org.elasticsearch.test.VersionUtils;
 
 import java.io.IOException;
 import java.util.ArrayList;
+import java.util.Base64;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
@@ -66,6 +70,18 @@ public class MappingStatsTests extends AbstractWireSerializingTestCase<MappingSt
             "           \"type\": \"keyword\"" +
             "         }" +
             "      }" +
+            "    }," +
+            "    \"long3\": {" +
+            "      \"type\":\"long\"," +
+            "      \"script\": " + Strings.toString(script3) +
+            "    }," +
+            "    \"long4\": {" +
+            "      \"type\":\"long\"," +
+            "      \"script\": " + Strings.toString(script4) +
+            "    }," +
+            "    \"keyword3\": {" +
+            "      \"type\": \"keyword\"," +
+            "      \"script\": " + Strings.toString(script1) +
             "    }" +
             "  }" +
             "}";
@@ -78,13 +94,43 @@ public class MappingStatsTests extends AbstractWireSerializingTestCase<MappingSt
             "    \"field_types\" : [\n" +
             "      {\n" +
             "        \"name\" : \"keyword\",\n" +
-            "        \"count\" : 2,\n" +
-            "        \"index_count\" : 2\n" +
+            "        \"count\" : 4,\n" +
+            "        \"index_count\" : 2,\n" +
+            "        \"script_count\" : 2,\n" +
+            "        \"lang\" : [\n" +
+            "          \"painless\"\n" +
+            "        ],\n" +
+            "        \"lines_max\" : 1,\n" +
+            "        \"lines_total\" : 2,\n" +
+            "        \"chars_max\" : 47,\n" +
+            "        \"chars_total\" : 94,\n" +
+            "        \"source_max\" : 1,\n" +
+            "        \"source_total\" : 2,\n" +
+            "        \"doc_max\" : 2,\n" +
+            "        \"doc_total\" : 4\n" +
+            "      },\n" +
+            "      {\n" +
+            "        \"name\" : \"long\",\n" +
+            "        \"count\" : 4,\n" +
+            "        \"index_count\" : 2,\n" +
+            "        \"script_count\" : 4,\n" +
+            "        \"lang\" : [\n" +
+            "          \"painless\"\n" +
+            "        ],\n" +
+            "        \"lines_max\" : 2,\n" +
+            "        \"lines_total\" : 6,\n" +
+            "        \"chars_max\" : 68,\n" +
+            "        \"chars_total\" : 176,\n" +
+            "        \"source_max\" : 3,\n" +
+            "        \"source_total\" : 8,\n" +
+            "        \"doc_max\" : 0,\n" +
+            "        \"doc_total\" : 0\n" +
             "      },\n" +
             "      {\n" +
             "        \"name\" : \"object\",\n" +
             "        \"count\" : 2,\n" +
-            "        \"index_count\" : 2\n" +
+            "        \"index_count\" : 2,\n" +
+            "        \"script_count\" : 0\n" +
             "      }\n" +
             "    ],\n" +
             "    \"runtime_field_types\" : [\n" +
@@ -136,19 +182,13 @@ public class MappingStatsTests extends AbstractWireSerializingTestCase<MappingSt
 
     @Override
     protected MappingStats createTestInstance() {
-        Collection<IndexFeatureStats> stats = new ArrayList<>();
+        Collection<FieldStats> stats = new ArrayList<>();
         Collection<RuntimeFieldStats> runtimeFieldStats = new ArrayList<>();
         if (randomBoolean()) {
-            IndexFeatureStats s = new IndexFeatureStats("keyword");
-            s.count = 10;
-            s.indexCount = 7;
-            stats.add(s);
+            stats.add(randomFieldStats("keyword"));
         }
         if (randomBoolean()) {
-            IndexFeatureStats s = new IndexFeatureStats("integer");
-            s.count = 3;
-            s.indexCount = 3;
-            stats.add(s);
+            stats.add(randomFieldStats("double"));
         }
         if (randomBoolean()) {
             runtimeFieldStats.add(randomRuntimeFieldStats("keyword"));
@@ -159,17 +199,36 @@ public class MappingStatsTests extends AbstractWireSerializingTestCase<MappingSt
         return new MappingStats(stats, runtimeFieldStats);
     }
 
+    private static FieldStats randomFieldStats(String type) {
+        FieldStats stats = new FieldStats(type);
+        stats.count = randomIntBetween(0, Integer.MAX_VALUE);
+        stats.indexCount = randomIntBetween(0, Integer.MAX_VALUE);
+        if (randomBoolean()) {
+            stats.scriptCount = randomIntBetween(0, Integer.MAX_VALUE);
+            stats.scriptLangs.add(randomAlphaOfLengthBetween(3, 10));
+            stats.fieldScriptStats.update(
+                randomIntBetween(1, 100), randomLongBetween(100, 1000), randomIntBetween(1, 10), randomIntBetween(1, 10));
+        }
+        return stats;
+    }
+
     private static RuntimeFieldStats randomRuntimeFieldStats(String type) {
         RuntimeFieldStats stats = new RuntimeFieldStats(type);
+        stats.count = randomIntBetween(0, Integer.MAX_VALUE);
+        stats.indexCount = randomIntBetween(0, Integer.MAX_VALUE);
+        stats.scriptLessCount = randomIntBetween(0, Integer.MAX_VALUE);
+        stats.shadowedCount = randomIntBetween(0, Integer.MAX_VALUE);
+        stats.scriptLangs.add(randomAlphaOfLengthBetween(3, 10));
         if (randomBoolean()) {
-            stats.update(randomIntBetween(1, 100), randomLongBetween(100, 1000), randomIntBetween(1, 10), randomIntBetween(1, 10));
+            stats.fieldScriptStats.update(
+                randomIntBetween(1, 100), randomLongBetween(100, 1000), randomIntBetween(1, 10), randomIntBetween(1, 10));
         }
         return stats;
     }
 
     @Override
     protected MappingStats mutateInstance(MappingStats instance) throws IOException {
-        List<IndexFeatureStats> fieldTypes = new ArrayList<>(instance.getFieldTypeStats());
+        List<FieldStats> fieldTypes = new ArrayList<>(instance.getFieldTypeStats());
         List<RuntimeFieldStats> runtimeFieldTypes = new ArrayList<>(instance.getRuntimeFieldStats());
         if (randomBoolean()) {
             boolean remove = fieldTypes.size() > 0 && randomBoolean();
@@ -177,7 +236,7 @@ public class MappingStatsTests extends AbstractWireSerializingTestCase<MappingSt
                 fieldTypes.remove(randomInt(fieldTypes.size() - 1));
             }
             if (remove == false || randomBoolean()) {
-                IndexFeatureStats s = new IndexFeatureStats("float");
+                FieldStats s = new FieldStats("float");
                 s.count = 13;
                 s.indexCount = 2;
                 fieldTypes.add(s);
@@ -209,7 +268,7 @@ public class MappingStatsTests extends AbstractWireSerializingTestCase<MappingSt
                 .put(indexMetadata)
                 .build();
         MappingStats mappingStats = MappingStats.of(metadata, () -> {});
-        IndexFeatureStats expectedStats = new IndexFeatureStats("long");
+        FieldStats expectedStats = new FieldStats("long");
         expectedStats.count = 1;
         expectedStats.indexCount = 1;
         assertEquals(
@@ -250,4 +309,32 @@ public class MappingStatsTests extends AbstractWireSerializingTestCase<MappingSt
             throw new TaskCancelledException("task cancelled");
         }));
     }
+
+    public void testWriteToPre8_0() throws IOException {
+        FieldStats fieldStats = randomFieldStats("test");
+        MappingStats mappingStats = new MappingStats(Collections.singleton(fieldStats), Collections.emptyList());
+        Version version = VersionUtils.randomPreviousCompatibleVersion(random(), Version.V_8_0_0);
+        BytesStreamOutput out = new BytesStreamOutput();
+        out.setVersion(version);
+        mappingStats.writeTo(out);
+        StreamInput in = StreamInput.wrap(out.bytes().toBytesRef().bytes);
+        in.setVersion(version);
+        MappingStats deserialized = new MappingStats(in);
+        assertEquals("{\"mappings\":{\"field_types\":[" +
+            "{\"name\":\"test\",\"count\":" + fieldStats.count+ ",\"index_count\":" + fieldStats.indexCount +
+                ",\"script_count\":0}],\"runtime_field_types\":[]}}",
+            Strings.toString(deserialized));
+    }
+
+    public void testReadFromPre8_0() throws IOException {
+        String base64EncodedFromPre8_0 = "AQR0ZXN0qebzzQGSg/HlBgAAAAAAAAAA";
+        byte[] bytes = Base64.getDecoder().decode(base64EncodedFromPre8_0);
+        Version version = VersionUtils.randomPreviousCompatibleVersion(random(), Version.V_8_0_0);
+        StreamInput in = StreamInput.wrap(bytes);
+        in.setVersion(version);
+        MappingStats deserialized = new MappingStats(in);
+        assertEquals("{\"mappings\":{\"field_types\":" +
+            "[{\"name\":\"test\",\"count\":431813417,\"index_count\":1824276882,\"script_count\":0}],\"runtime_field_types\":[]}}",
+            Strings.toString(deserialized));
+    }
 }