Browse Source

Add global ordinal info to stats APIs. (#94500)

This change adds:
* Total global ordinal build time for all fields and per field.
* Max shard value count per field. The value count is per shard and of the shard with the highest count. Reporting value on index level or across indices is too expensive to report or keep track of.

This is added to common stats, which
is exposed in several stats APIs.

The following api call:

```
GET /_nodes/stats?filter_path=nodes.*.indices.fielddata&fields=key,key2
```

Returns:

```
{
    "nodes": {
        "pcMNy4GsQ8ef6Rw-bI2EFg": {
            "indices": {
                "fielddata": {
                    "memory_size_in_bytes": 2552,
                    "evictions": 0,
                    "fields": {
                        "key2": {
                            "memory_size_in_bytes": 1320
                        },
                        "key": {
                            "memory_size_in_bytes": 1232
                        }
                    },
                    "global_ordinals": {
                        "build_time_in_millis": 8,
                        "fields": {
                            "key2": {
                                "build_time_in_millis": 4,
                                "shard_max_value_count": 4
                            },
                            "key": {
                                "build_time_in_millis": 4,
                                "shard_max_value_count": 4
                            }
                        }
                    }
                }
            }
        }
    }
}
```
Martijn van Groningen 2 years ago
parent
commit
6566bb4075
19 changed files with 706 additions and 34 deletions
  1. 5 0
      docs/changelog/94500.yaml
  2. 25 1
      docs/reference/cluster/stats.asciidoc
  3. 303 0
      rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/indices.stats/13_fields.yml
  4. 6 6
      server/src/main/java/org/elasticsearch/action/admin/cluster/stats/ClusterStatsIndices.java
  5. 11 0
      server/src/main/java/org/elasticsearch/index/IndexService.java
  6. 120 4
      server/src/main/java/org/elasticsearch/index/fielddata/FieldDataStats.java
  7. 3 0
      server/src/main/java/org/elasticsearch/index/fielddata/IndexFieldDataCache.java
  8. 47 1
      server/src/main/java/org/elasticsearch/index/fielddata/ShardFieldData.java
  9. 34 0
      server/src/main/java/org/elasticsearch/index/fielddata/ordinals/GlobalOrdinalsAccounting.java
  10. 8 8
      server/src/main/java/org/elasticsearch/index/fielddata/ordinals/GlobalOrdinalsBuilder.java
  11. 17 3
      server/src/main/java/org/elasticsearch/index/fielddata/ordinals/GlobalOrdinalsIndexFieldData.java
  12. 7 3
      server/src/main/java/org/elasticsearch/indices/fielddata/cache/IndicesFieldDataCache.java
  13. 7 1
      server/src/test/java/org/elasticsearch/action/admin/cluster/node/stats/NodeStatsTests.java
  14. 73 1
      server/src/test/java/org/elasticsearch/index/fielddata/FieldDataStatsTests.java
  15. 7 0
      x-pack/plugin/core/src/main/resources/monitoring-es-mb.json
  16. 21 0
      x-pack/plugin/core/src/main/resources/monitoring-es.json
  17. 7 4
      x-pack/plugin/monitoring/src/test/java/org/elasticsearch/xpack/monitoring/collector/cluster/ClusterStatsMonitoringDocTests.java
  18. 2 1
      x-pack/plugin/monitoring/src/test/java/org/elasticsearch/xpack/monitoring/collector/indices/IndexStatsMonitoringDocTests.java
  19. 3 1
      x-pack/plugin/monitoring/src/test/java/org/elasticsearch/xpack/monitoring/collector/node/NodeStatsMonitoringDocTests.java

+ 5 - 0
docs/changelog/94500.yaml

@@ -0,0 +1,5 @@
+pr: 94500
+summary: Add global ordinal info to stats APIs
+area: Aggregations
+type: enhancement
+issues: []

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

@@ -285,6 +285,26 @@ shards assigned to selected nodes.
 (integer)
 Total number of evictions from the field data cache across all shards assigned
 to selected nodes.
+
+`global_ordinals.build_time`::
+(<<time-units,time unit>>)
+The total time spent building global ordinals for all fields.
+
+`global_ordinals.build_time_in_millis`::
+(integer)
+The total time, in milliseconds, spent building global ordinals for all fields.
+
+`global_ordinals.fields.[field-name].build_time`::
+(<<time-units,time unit>>)
+The total time spent building global ordinals for field with specified name.
+
+`global_ordinals.fields.[field-name].build_time_in_millis`::
+(integer)
+The total time, in milliseconds, spent building global ordinals for field with specified name.
+
+`global_ordinals.fields.[field-name].shard_max_value_count`::
+(long)
+The total time spent building global ordinals for field with specified name.
 =====
 
 `query_cache`::
@@ -1514,7 +1534,11 @@ The API returns the following response:
       "fielddata": {
          "memory_size": "0b",
          "memory_size_in_bytes": 0,
-         "evictions": 0
+         "evictions": 0,
+         "global_ordinals": {
+            "build_time" : "0s",
+            "build_time_in_millis" : 0
+         }
       },
       "query_cache": {
          "memory_size": "0b",

+ 303 - 0
rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/indices.stats/13_fields.yml

@@ -45,6 +45,17 @@ setup:
   - do:
       indices.refresh: {}
 
+  # Enforce creating an extra segment in at least one shard,
+  # otherwise no global ordinals will be built.
+  - do:
+      index:
+        index: test1
+        id: "2"
+        body: { "bar": "foo", "baz": "foo" }
+
+  - do:
+      indices.refresh: {}
+
   - do:
       search:
         rest_total_hits_as_int: true
@@ -73,6 +84,18 @@ setup:
         body:
           sort: [ "bar", "baz" ]
 
+  - do:
+      search:
+        rest_total_hits_as_int: true
+        body:
+          aggs:
+            my_agg_1:
+              terms:
+                field: bar
+            my_agg_2:
+              terms:
+                field: baz
+
 ---
 "Fields - blank":
   - do:
@@ -84,6 +107,23 @@ setup:
   - gt:       { _all.total.completion.size_in_bytes: 0 }
   - is_false:   _all.total.completion.fields
 
+---
+"Fields - blank - global_ordinals":
+  - skip:
+      version: " - 8.7.99"
+      reason: "global_ordinals introduced in 8.8.0"
+
+  - do:
+      indices.stats: {}
+
+  - match: { _shards.failed: 0}
+  - gt:       { _all.total.fielddata.memory_size_in_bytes: 0 }
+  - is_false:   _all.total.fielddata.fields
+  - gte:       { _all.total.fielddata.global_ordinals.build_time_in_millis: 0 }
+  - is_false:   _all.total.fielddata.global_ordinals.fields
+  - gt:       { _all.total.completion.size_in_bytes: 0 }
+  - is_false:   _all.total.completion.fields
+
 ---
 "Fields - one":
   - do:
@@ -96,6 +136,26 @@ setup:
   - gt:       { _all.total.completion.size_in_bytes: 0 }
   - is_false:   _all.total.completion.fields.bar
 
+---
+"Fields - one global_ordinals":
+  - skip:
+      version: " - 8.7.99"
+      reason: "global_ordinals introduced in 8.8.0"
+
+  - do:
+      indices.stats: { fields: bar }
+
+  - match: { _shards.failed: 0}
+  - gt:       { _all.total.fielddata.memory_size_in_bytes: 0 }
+  - gt:       { _all.total.fielddata.fields.bar.memory_size_in_bytes: 0 }
+  - is_false:   _all.total.fielddata.fields.baz
+  - gte: { _all.total.fielddata.global_ordinals.build_time_in_millis: 0 }
+  - gte: { _all.total.fielddata.global_ordinals.fields.bar.build_time_in_millis: 0 }
+  - gte: { _all.total.fielddata.global_ordinals.fields.bar.shard_max_value_count: 1 }
+  - is_false: _all.total.fielddata.global_ordinals.fields.baz
+  - gt:       { _all.total.completion.size_in_bytes: 0 }
+  - is_false:   _all.total.completion.fields.bar
+
 ---
 "Fields - multi":
   - do:
@@ -109,6 +169,27 @@ setup:
   - is_false:   _all.total.completion.fields.bar\.completion
   - gt:       { _all.total.completion.fields.baz\.completion.size_in_bytes: 0 }
 
+---
+"Fields - multi global_ordinals":
+  - skip:
+      version: " - 8.7.99"
+      reason: "global_ordinals introduced in 8.8.0"
+
+  - do:
+      indices.stats: { fields: "bar,baz.completion" }
+
+  - match: { _shards.failed: 0}
+  - gt:       { _all.total.fielddata.memory_size_in_bytes: 0 }
+  - gt:       { _all.total.fielddata.fields.bar.memory_size_in_bytes: 0 }
+  - is_false:   _all.total.fielddata.fields.baz
+  - gte: { _all.total.fielddata.global_ordinals.build_time_in_millis: 0 }
+  - gte: { _all.total.fielddata.global_ordinals.fields.bar.build_time_in_millis: 0 }
+  - gte: { _all.total.fielddata.global_ordinals.fields.bar.shard_max_value_count: 1 }
+  - is_false: _all.total.fielddata.global_ordinals.fields.baz
+  - gt:       { _all.total.completion.size_in_bytes: 0 }
+  - is_false:   _all.total.completion.fields.bar\.completion
+  - gt:       { _all.total.completion.fields.baz\.completion.size_in_bytes: 0 }
+
 ---
 "Fields - star":
   - do:
@@ -122,6 +203,29 @@ setup:
   - gt:       { _all.total.completion.fields.bar\.completion.size_in_bytes: 0 }
   - gt:       { _all.total.completion.fields.baz\.completion.size_in_bytes: 0 }
 
+---
+"Fields - star global_ordinals":
+  - skip:
+      version: " - 8.7.99"
+      reason: "global_ordinals introduced in 8.8.0"
+
+  - do:
+      indices.stats: { fields: "*" }
+
+  - match: { _shards.failed: 0}
+  - gt:       { _all.total.fielddata.memory_size_in_bytes: 0 }
+  - gt:       { _all.total.fielddata.fields.bar.memory_size_in_bytes: 0 }
+  - gt:       { _all.total.fielddata.fields.baz.memory_size_in_bytes: 0 }
+  - gte: { _all.total.fielddata.global_ordinals.build_time_in_millis: 0 }
+  - gte: { _all.total.fielddata.global_ordinals.fields.bar.build_time_in_millis: 0 }
+  - gte: { _all.total.fielddata.global_ordinals.fields.bar.shard_max_value_count: 1 }
+  - gte: { _all.total.fielddata.global_ordinals.fields.baz.build_time_in_millis: 0 }
+  - gte: { _all.total.fielddata.global_ordinals.fields.baz.shard_max_value_count: 1 }
+  - gt:       { _all.total.completion.size_in_bytes: 0 }
+  - gt:       { _all.total.completion.fields.bar\.completion.size_in_bytes: 0 }
+  - gt:       { _all.total.completion.fields.baz\.completion.size_in_bytes: 0 }
+
+
 ---
 "Fields - pattern":
   - do:
@@ -135,6 +239,27 @@ setup:
   - gt:       { _all.total.completion.fields.bar\.completion.size_in_bytes: 0 }
   - is_false:   _all.total.completion.fields.baz\.completion
 
+---
+"Fields - pattern global_ordinals":
+  - skip:
+      version: " - 8.7.99"
+      reason: "global_ordinals introduced in 8.8.0"
+
+  - do:
+      indices.stats: { fields: "bar*" }
+
+  - match: { _shards.failed: 0}
+  - gt:       { _all.total.fielddata.memory_size_in_bytes: 0 }
+  - gt:       { _all.total.fielddata.fields.bar.memory_size_in_bytes: 0 }
+  - is_false:   _all.total.fielddata.fields.baz
+  - gte: { _all.total.fielddata.global_ordinals.fields.bar.build_time_in_millis: 0 }
+  - gte: { _all.total.fielddata.global_ordinals.fields.bar.shard_max_value_count: 1 }
+  - is_false: _all.total.fielddata.global_ordinals.fields.baz
+  - gt:       { _all.total.completion.size_in_bytes: 0 }
+  - gt:       { _all.total.completion.fields.bar\.completion.size_in_bytes: 0 }
+  - is_false:   _all.total.completion.fields.baz\.completion
+
+
 ---
 "Fields - _all metric":
   - do:
@@ -148,6 +273,26 @@ setup:
   - gt:       { _all.total.completion.fields.bar\.completion.size_in_bytes: 0 }
   - is_false:   _all.total.completion.fields.baz\.completion
 
+---
+"Fields - _all metric global_ordinals":
+  - skip:
+      version: " - 8.7.99"
+      reason: "global_ordinals introduced in 8.8.0"
+
+  - do:
+      indices.stats: { fields: "bar*", metric: _all }
+
+  - match: { _shards.failed: 0}
+  - gt:       { _all.total.fielddata.memory_size_in_bytes: 0 }
+  - gt:       { _all.total.fielddata.fields.bar.memory_size_in_bytes: 0 }
+  - is_false:   _all.total.fielddata.fields.baz
+  - gte: { _all.total.fielddata.global_ordinals.fields.bar.build_time_in_millis: 0 }
+  - gte: { _all.total.fielddata.global_ordinals.fields.bar.shard_max_value_count: 1 }
+  - is_false: _all.total.fielddata.global_ordinals.fields.baz
+  - gt:       { _all.total.completion.size_in_bytes: 0 }
+  - gt:       { _all.total.completion.fields.bar\.completion.size_in_bytes: 0 }
+  - is_false:   _all.total.completion.fields.baz\.completion
+
 ---
 "Fields - fielddata metric":
   - do:
@@ -159,6 +304,25 @@ setup:
   - is_false:   _all.total.fielddata.fields.baz
   - is_false:   _all.total.completion
 
+---
+"Fields - fielddata metric global_ordinals":
+  - skip:
+      version: " - 8.7.99"
+      reason: "global_ordinals introduced in 8.8.0"
+
+  - do:
+      indices.stats: { fields: "bar*", metric: fielddata }
+
+  - match: { _shards.failed: 0}
+  - gt:       { _all.total.fielddata.memory_size_in_bytes: 0 }
+  - gt:       { _all.total.fielddata.fields.bar.memory_size_in_bytes: 0 }
+  - is_false:   _all.total.fielddata.fields.baz
+  - gte: { _all.total.fielddata.global_ordinals.fields.bar.build_time_in_millis: 0 }
+  - gte: { _all.total.fielddata.global_ordinals.fields.bar.shard_max_value_count: 1 }
+  - is_false: _all.total.fielddata.global_ordinals.fields.baz
+  - is_false:   _all.total.completion
+
+
 ---
 "Fields - completion metric":
   - do:
@@ -183,6 +347,26 @@ setup:
   - gt:       { _all.total.completion.fields.bar\.completion.size_in_bytes: 0 }
   - is_false:   _all.total.completion.fields.baz\.completion
 
+---
+"Fields - multi metric global_ordinals":
+  - skip:
+      version: " - 8.7.99"
+      reason: "global_ordinals introduced in 8.8.0"
+
+  - do:
+      indices.stats: { fields: "bar*" , metric: [ completion, fielddata, search ]}
+
+  - match: { _shards.failed: 0}
+  - gt:       { _all.total.fielddata.memory_size_in_bytes: 0 }
+  - gt:       { _all.total.fielddata.fields.bar.memory_size_in_bytes: 0 }
+  - is_false:   _all.total.fielddata.fields.baz
+  - gte: { _all.total.fielddata.global_ordinals.fields.bar.build_time_in_millis: 0 }
+  - gte: { _all.total.fielddata.global_ordinals.fields.bar.shard_max_value_count: 1 }
+  - is_false: _all.total.fielddata.global_ordinals.fields.baz
+  - gt:       { _all.total.completion.size_in_bytes: 0 }
+  - gt:       { _all.total.completion.fields.bar\.completion.size_in_bytes: 0 }
+  - is_false:   _all.total.completion.fields.baz\.completion
+
 ---
 "Fielddata fields - one":
   - do:
@@ -193,6 +377,22 @@ setup:
   - is_false:   _all.total.fielddata.fields.baz
   - is_false:   _all.total.completion.fields
 
+---
+"Fielddata fields - one global_ordinals":
+  - skip:
+      version: " - 8.7.99"
+      reason: "global_ordinals introduced in 8.8.0"
+
+  - do:
+      indices.stats: { fielddata_fields: bar }
+
+  - match: { _shards.failed: 0}
+  - gt:       { _all.total.fielddata.fields.bar.memory_size_in_bytes: 0 }
+  - is_false:   _all.total.fielddata.fields.baz
+  - gte: { _all.total.fielddata.global_ordinals.fields.bar.build_time_in_millis: 0 }
+  - gte: { _all.total.fielddata.global_ordinals.fields.bar.shard_max_value_count: 1 }
+  - is_false: _all.total.fielddata.global_ordinals.fields.baz
+
 ---
 "Fielddata fields - multi":
   - do:
@@ -203,6 +403,25 @@ setup:
   - gt:       { _all.total.fielddata.fields.baz.memory_size_in_bytes: 0 }
   - is_false:   _all.total.completion.fields
 
+---
+"Fielddata fields - multi global_ordinals":
+  - skip:
+      version: " - 8.7.99"
+      reason: "global_ordinals introduced in 8.8.0"
+
+  - do:
+      indices.stats: { fielddata_fields: "bar,baz,baz.completion" }
+
+  - match: { _shards.failed: 0}
+  - gt:       { _all.total.fielddata.fields.bar.memory_size_in_bytes: 0 }
+  - gt:       { _all.total.fielddata.fields.baz.memory_size_in_bytes: 0 }
+  - gte: { _all.total.fielddata.global_ordinals.build_time_in_millis: 0 }
+  - gte: { _all.total.fielddata.global_ordinals.fields.bar.build_time_in_millis: 0 }
+  - gte: { _all.total.fielddata.global_ordinals.fields.bar.shard_max_value_count: 1 }
+  - gte: { _all.total.fielddata.global_ordinals.fields.baz.build_time_in_millis: 0 }
+  - gte: { _all.total.fielddata.global_ordinals.fields.baz.shard_max_value_count: 1 }
+  - is_false:   _all.total.completion.fields
+
 ---
 "Fielddata fields - star":
   - do:
@@ -213,6 +432,25 @@ setup:
   - gt:       { _all.total.fielddata.fields.baz.memory_size_in_bytes: 0 }
   - is_false:   _all.total.completion.fields
 
+---
+"Fielddata fields - star global_ordinals":
+  - skip:
+      version: " - 8.7.99"
+      reason: "global_ordinals introduced in 8.8.0"
+
+  - do:
+      indices.stats: { fielddata_fields: "*" }
+
+  - match: { _shards.failed: 0}
+  - gt:       { _all.total.fielddata.fields.bar.memory_size_in_bytes: 0 }
+  - gt:       { _all.total.fielddata.fields.baz.memory_size_in_bytes: 0 }
+  - gte: { _all.total.fielddata.global_ordinals.build_time_in_millis: 0 }
+  - gte: { _all.total.fielddata.global_ordinals.fields.bar.build_time_in_millis: 0 }
+  - gte: { _all.total.fielddata.global_ordinals.fields.bar.shard_max_value_count: 1 }
+  - gte: { _all.total.fielddata.global_ordinals.fields.baz.build_time_in_millis: 0 }
+  - gte: { _all.total.fielddata.global_ordinals.fields.baz.shard_max_value_count: 1 }
+  - is_false:   _all.total.completion.fields
+
 ---
 "Fielddata fields - pattern":
   - do:
@@ -223,6 +461,22 @@ setup:
   - is_false:   _all.total.fielddata.fields.baz
   - is_false:   _all.total.completion.fields
 
+---
+"Fielddata fields - pattern global_ordinals":
+  - skip:
+      version: " - 8.7.99"
+      reason: "global_ordinals introduced in 8.8.0"
+
+  - do:
+      indices.stats: { fielddata_fields: "*r" }
+
+  - match: { _shards.failed: 0}
+  - gt:       { _all.total.fielddata.fields.bar.memory_size_in_bytes: 0 }
+  - is_false:   _all.total.fielddata.fields.baz
+  - gte: { _all.total.fielddata.global_ordinals.fields.bar.build_time_in_millis: 0 }
+  - gte: { _all.total.fielddata.global_ordinals.fields.bar.shard_max_value_count: 1 }
+  - is_false: _all.total.fielddata.global_ordinals.fields.baz
+  - is_false:   _all.total.completion.fields
 
 ---
 "Fielddata fields - all metric":
@@ -234,6 +488,23 @@ setup:
   - is_false:   _all.total.fielddata.fields.baz
   - is_false:   _all.total.completion.fields
 
+---
+"Fielddata fields - all metric global_ordinals":
+  - skip:
+      version: " - 8.7.99"
+      reason: "global_ordinals introduced in 8.8.0"
+
+  - do:
+      indices.stats: { fielddata_fields: "*r", metric: _all }
+
+  - match: { _shards.failed: 0}
+  - gt:       { _all.total.fielddata.fields.bar.memory_size_in_bytes: 0 }
+  - is_false:   _all.total.fielddata.fields.baz
+  - gte: { _all.total.fielddata.global_ordinals.fields.bar.build_time_in_millis: 0 }
+  - gte: { _all.total.fielddata.global_ordinals.fields.bar.shard_max_value_count: 1 }
+  - is_false: _all.total.fielddata.global_ordinals.fields.baz
+  - is_false:   _all.total.completion.fields
+
 ---
 "Fielddata fields - one metric":
   - do:
@@ -244,6 +515,22 @@ setup:
   - is_false:   _all.total.fielddata.fields.baz
   - is_false:   _all.total.completion.fields
 
+---
+"Fielddata fields - one metric global_ordinals":
+  - skip:
+      version: " - 8.7.99"
+      reason: "global_ordinals introduced in 8.8.0"
+
+  - do:
+      indices.stats: { fielddata_fields: "*r", metric: fielddata }
+
+  - match: { _shards.failed: 0}
+  - gt:       { _all.total.fielddata.fields.bar.memory_size_in_bytes: 0 }
+  - is_false:   _all.total.fielddata.fields.baz
+  - gte: { _all.total.fielddata.global_ordinals.fields.bar.build_time_in_millis: 0 }
+  - gte: { _all.total.fielddata.global_ordinals.fields.bar.shard_max_value_count: 1 }
+  - is_false: _all.total.fielddata.global_ordinals.fields.baz
+  - is_false:   _all.total.completion.fields
 
 ---
 "Fielddata fields - multi metric":
@@ -255,6 +542,22 @@ setup:
   - is_false:   _all.total.fielddata.fields.baz
   - is_false:   _all.total.completion.fields
 
+---
+"Fielddata fields - multi metric global_ordinals":
+  - skip:
+      version: " - 8.7.99"
+      reason: "global_ordinals introduced in 8.8.0"
+
+  - do:
+      indices.stats: { fielddata_fields: "*r", metric: [ fielddata, search] }
+
+  - match: { _shards.failed: 0}
+  - gt:       { _all.total.fielddata.fields.bar.memory_size_in_bytes: 0 }
+  - is_false:   _all.total.fielddata.fields.baz
+  - gte: { _all.total.fielddata.global_ordinals.fields.bar.build_time_in_millis: 0 }
+  - gte: { _all.total.fielddata.global_ordinals.fields.bar.shard_max_value_count: 1 }
+  - is_false: _all.total.fielddata.global_ordinals.fields.baz
+  - is_false:   _all.total.completion.fields
 
 ---
 "Completion fields - one":

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

@@ -67,13 +67,13 @@ public class ClusterStatsIndices implements ToXContentFragment {
 
                 if (shardStats.getShardRouting().primary()) {
                     indexShardStats.primaries++;
-                    docs.add(shardCommonStats.docs);
+                    docs.add(shardCommonStats.getDocs());
                 }
-                store.add(shardCommonStats.store);
-                fieldData.add(shardCommonStats.fieldData);
-                queryCache.add(shardCommonStats.queryCache);
-                completion.add(shardCommonStats.completion);
-                segments.add(shardCommonStats.segments);
+                store.add(shardCommonStats.getStore());
+                fieldData.add(shardCommonStats.getFieldData());
+                queryCache.add(shardCommonStats.getQueryCache());
+                completion.add(shardCommonStats.getCompletion());
+                segments.add(shardCommonStats.getSegments());
             }
 
             searchUsageStats.add(r.searchUsageStats());

+ 11 - 0
server/src/main/java/org/elasticsearch/index/IndexService.java

@@ -47,6 +47,7 @@ import org.elasticsearch.index.engine.EngineFactory;
 import org.elasticsearch.index.fielddata.FieldDataContext;
 import org.elasticsearch.index.fielddata.IndexFieldDataCache;
 import org.elasticsearch.index.fielddata.IndexFieldDataService;
+import org.elasticsearch.index.fielddata.ordinals.GlobalOrdinalsAccounting;
 import org.elasticsearch.index.mapper.IdFieldMapper;
 import org.elasticsearch.index.mapper.MapperRegistry;
 import org.elasticsearch.index.mapper.MapperService;
@@ -772,6 +773,16 @@ public class IndexService extends AbstractIndexComponent implements IndicesClust
             }
         }
 
+        @Override
+        public void onCache(ShardId shardId, String fieldName, GlobalOrdinalsAccounting info) {
+            if (shardId != null) {
+                final IndexShard shard = indexService.getShardOrNull(shardId.id());
+                if (shard != null) {
+                    shard.fieldData().onCache(shardId, fieldName, info);
+                }
+            }
+        }
+
         @Override
         public void onRemoval(ShardId shardId, String fieldName, boolean wasEvicted, long sizeInBytes) {
             if (shardId != null) {

+ 120 - 4
server/src/main/java/org/elasticsearch/index/fielddata/FieldDataStats.java

@@ -8,16 +8,19 @@
 
 package org.elasticsearch.index.fielddata;
 
+import org.elasticsearch.TransportVersion;
 import org.elasticsearch.common.FieldMemoryStats;
 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.unit.ByteSizeValue;
 import org.elasticsearch.core.Nullable;
+import org.elasticsearch.core.TimeValue;
 import org.elasticsearch.xcontent.ToXContentFragment;
 import org.elasticsearch.xcontent.XContentBuilder;
 
 import java.io.IOException;
+import java.util.Map;
 import java.util.Objects;
 
 public class FieldDataStats implements Writeable, ToXContentFragment {
@@ -27,25 +30,43 @@ public class FieldDataStats implements Writeable, ToXContentFragment {
     private static final String MEMORY_SIZE_IN_BYTES = "memory_size_in_bytes";
     private static final String EVICTIONS = "evictions";
     private static final String FIELDS = "fields";
+    private static final String GLOBAL_ORDINALS = "global_ordinals";
+    private static final String SHARD_MAX_VALUE_COUNT = "shard_max_value_count";
+    private static final String BUILD_TIME = "build_time";
     private long memorySize;
     private long evictions;
     @Nullable
     private FieldMemoryStats fields;
+    private final GlobalOrdinalsStats globalOrdinalsStats;
 
     public FieldDataStats() {
-
+        this.globalOrdinalsStats = new GlobalOrdinalsStats(0, null);
     }
 
     public FieldDataStats(StreamInput in) throws IOException {
         memorySize = in.readVLong();
         evictions = in.readVLong();
         fields = in.readOptionalWriteable(FieldMemoryStats::new);
+        if (in.getTransportVersion().onOrAfter(TransportVersion.V_8_8_0)) {
+            long buildTimeMillis = in.readVLong();
+            Map<String, GlobalOrdinalsStats.GlobalOrdinalFieldStats> fieldGlobalOrdinalsStats = null;
+            if (in.readBoolean()) {
+                fieldGlobalOrdinalsStats = in.readMap(
+                    StreamInput::readString,
+                    in1 -> new GlobalOrdinalsStats.GlobalOrdinalFieldStats(in1.readVLong(), in1.readVLong())
+                );
+            }
+            globalOrdinalsStats = new GlobalOrdinalsStats(buildTimeMillis, fieldGlobalOrdinalsStats);
+        } else {
+            globalOrdinalsStats = new GlobalOrdinalsStats(0, null);
+        }
     }
 
-    public FieldDataStats(long memorySize, long evictions, @Nullable FieldMemoryStats fields) {
+    public FieldDataStats(long memorySize, long evictions, @Nullable FieldMemoryStats fields, GlobalOrdinalsStats globalOrdinalsStats) {
         this.memorySize = memorySize;
         this.evictions = evictions;
         this.fields = fields;
+        this.globalOrdinalsStats = Objects.requireNonNull(globalOrdinalsStats);
     }
 
     public void add(FieldDataStats stats) {
@@ -61,6 +82,7 @@ public class FieldDataStats implements Writeable, ToXContentFragment {
                 fields.add(stats.fields);
             }
         }
+        this.globalOrdinalsStats.add(stats.globalOrdinalsStats);
     }
 
     public long getMemorySizeInBytes() {
@@ -80,11 +102,27 @@ public class FieldDataStats implements Writeable, ToXContentFragment {
         return fields;
     }
 
+    public GlobalOrdinalsStats getGlobalOrdinalsStats() {
+        return globalOrdinalsStats;
+    }
+
     @Override
     public void writeTo(StreamOutput out) throws IOException {
         out.writeVLong(memorySize);
         out.writeVLong(evictions);
         out.writeOptionalWriteable(fields);
+        if (out.getTransportVersion().onOrAfter(TransportVersion.V_8_8_0)) {
+            out.writeVLong(globalOrdinalsStats.buildTimeMillis);
+            if (globalOrdinalsStats.fieldGlobalOrdinalsStats != null) {
+                out.writeBoolean(true);
+                out.writeMap(globalOrdinalsStats.fieldGlobalOrdinalsStats, StreamOutput::writeString, (out1, value) -> {
+                    out1.writeVLong(value.totalBuildingTime);
+                    out1.writeVLong(value.valueCount);
+                });
+            } else {
+                out.writeBoolean(false);
+            }
+        }
     }
 
     @Override
@@ -95,6 +133,19 @@ public class FieldDataStats implements Writeable, ToXContentFragment {
         if (fields != null) {
             fields.toXContent(builder, FIELDS, MEMORY_SIZE_IN_BYTES, MEMORY_SIZE);
         }
+        builder.startObject(GLOBAL_ORDINALS);
+        builder.humanReadableField(BUILD_TIME + "_in_millis", BUILD_TIME, new TimeValue(globalOrdinalsStats.buildTimeMillis));
+        if (globalOrdinalsStats.fieldGlobalOrdinalsStats != null) {
+            builder.startObject(FIELDS);
+            for (var entry : globalOrdinalsStats.fieldGlobalOrdinalsStats.entrySet()) {
+                builder.startObject(entry.getKey());
+                builder.humanReadableField(BUILD_TIME + "_in_millis", BUILD_TIME, new TimeValue(entry.getValue().totalBuildingTime));
+                builder.field(SHARD_MAX_VALUE_COUNT, entry.getValue().valueCount);
+                builder.endObject();
+            }
+            builder.endObject();
+        }
+        builder.endObject();
         builder.endObject();
         return builder;
     }
@@ -104,11 +155,76 @@ public class FieldDataStats implements Writeable, ToXContentFragment {
         if (this == o) return true;
         if (o == null || getClass() != o.getClass()) return false;
         FieldDataStats that = (FieldDataStats) o;
-        return memorySize == that.memorySize && evictions == that.evictions && Objects.equals(fields, that.fields);
+        return memorySize == that.memorySize
+            && evictions == that.evictions
+            && Objects.equals(fields, that.fields)
+            && Objects.equals(globalOrdinalsStats, that.globalOrdinalsStats);
     }
 
     @Override
     public int hashCode() {
-        return Objects.hash(memorySize, evictions, fields);
+        return Objects.hash(memorySize, evictions, fields, globalOrdinalsStats);
+    }
+
+    /**
+     * The global ordinal stats. Keeps track of total build time for all fields that support global ordinals.
+     * Also keeps track of build time per field and the maximum unique value on a shard level.
+     * <p>
+     * Global ordinals can speed up sorting and aggregations, but can be expensive to build (dependents on number of unique values).
+     * Each time a refresh happens global ordinals need to be rebuilt. These stats should give more insight on these costs.
+     */
+    public static class GlobalOrdinalsStats {
+
+        private long buildTimeMillis;
+        @Nullable
+        private Map<String, GlobalOrdinalFieldStats> fieldGlobalOrdinalsStats;
+
+        public GlobalOrdinalsStats(long buildTimeMillis, Map<String, GlobalOrdinalFieldStats> fieldGlobalOrdinalsStats) {
+            this.buildTimeMillis = buildTimeMillis;
+            this.fieldGlobalOrdinalsStats = fieldGlobalOrdinalsStats;
+        }
+
+        public long getBuildTimeMillis() {
+            return buildTimeMillis;
+        }
+
+        @Nullable
+        public Map<String, GlobalOrdinalFieldStats> getFieldGlobalOrdinalsStats() {
+            return fieldGlobalOrdinalsStats;
+        }
+
+        void add(GlobalOrdinalsStats other) {
+            buildTimeMillis += other.buildTimeMillis;
+            if (fieldGlobalOrdinalsStats != null && other.fieldGlobalOrdinalsStats != null) {
+                for (var entry : other.fieldGlobalOrdinalsStats.entrySet()) {
+                    fieldGlobalOrdinalsStats.merge(
+                        entry.getKey(),
+                        entry.getValue(),
+                        (value1, value2) -> new GlobalOrdinalFieldStats(
+                            value1.totalBuildingTime + value2.totalBuildingTime,
+                            Math.max(value1.valueCount, value2.valueCount)
+                        )
+                    );
+                }
+            } else if (other.fieldGlobalOrdinalsStats != null) {
+                fieldGlobalOrdinalsStats = other.fieldGlobalOrdinalsStats;
+            }
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) return true;
+            if (o == null || getClass() != o.getClass()) return false;
+            GlobalOrdinalsStats that = (GlobalOrdinalsStats) o;
+            return buildTimeMillis == that.buildTimeMillis && Objects.equals(fieldGlobalOrdinalsStats, that.fieldGlobalOrdinalsStats);
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(buildTimeMillis, fieldGlobalOrdinalsStats);
+        }
+
+        public record GlobalOrdinalFieldStats(long totalBuildingTime, long valueCount) {}
+
     }
 }

+ 3 - 0
server/src/main/java/org/elasticsearch/index/fielddata/IndexFieldDataCache.java

@@ -11,6 +11,7 @@ package org.elasticsearch.index.fielddata;
 import org.apache.lucene.index.DirectoryReader;
 import org.apache.lucene.index.LeafReaderContext;
 import org.apache.lucene.util.Accountable;
+import org.elasticsearch.index.fielddata.ordinals.GlobalOrdinalsAccounting;
 import org.elasticsearch.index.shard.ShardId;
 
 /**
@@ -40,6 +41,8 @@ public interface IndexFieldDataCache {
          */
         default void onCache(ShardId shardId, String fieldName, Accountable ramUsage) {}
 
+        default void onCache(ShardId shardId, String fieldName, GlobalOrdinalsAccounting info) {}
+
         /**
          * Called after the fielddata is unloaded
          */

+ 47 - 1
server/src/main/java/org/elasticsearch/index/fielddata/ShardFieldData.java

@@ -14,20 +14,25 @@ import org.elasticsearch.common.metrics.CounterMetric;
 import org.elasticsearch.common.regex.Regex;
 import org.elasticsearch.common.util.CollectionUtils;
 import org.elasticsearch.common.util.concurrent.ConcurrentCollections;
+import org.elasticsearch.index.fielddata.ordinals.GlobalOrdinalsAccounting;
 import org.elasticsearch.index.shard.ShardId;
 
 import java.util.HashMap;
 import java.util.Map;
 import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.atomic.AtomicLong;
 
 public class ShardFieldData implements IndexFieldDataCache.Listener {
 
     private final CounterMetric evictionsMetric = new CounterMetric();
     private final CounterMetric totalMetric = new CounterMetric();
     private final ConcurrentMap<String, CounterMetric> perFieldTotals = ConcurrentCollections.newConcurrentMap();
+    private final CounterMetric buildTime = new CounterMetric();
+    private final ConcurrentMap<String, GlobalOrdinalFieldStats> perFieldGlobalOrdinalStats = ConcurrentCollections.newConcurrentMap();
 
     public FieldDataStats stats(String... fields) {
         Map<String, Long> fieldTotals = null;
+        Map<String, FieldDataStats.GlobalOrdinalsStats.GlobalOrdinalFieldStats> fieldGlobalOrdinalsStats = null;
         if (CollectionUtils.isEmpty(fields) == false) {
             fieldTotals = new HashMap<>();
             for (Map.Entry<String, CounterMetric> entry : perFieldTotals.entrySet()) {
@@ -35,11 +40,26 @@ public class ShardFieldData implements IndexFieldDataCache.Listener {
                     fieldTotals.put(entry.getKey(), entry.getValue().count());
                 }
             }
+            for (var entry : perFieldGlobalOrdinalStats.entrySet()) {
+                if (Regex.simpleMatch(fields, entry.getKey())) {
+                    if (fieldGlobalOrdinalsStats == null) {
+                        fieldGlobalOrdinalsStats = new HashMap<>();
+                    }
+                    fieldGlobalOrdinalsStats.put(
+                        entry.getKey(),
+                        new FieldDataStats.GlobalOrdinalsStats.GlobalOrdinalFieldStats(
+                            entry.getValue().totalBuildTime.count(),
+                            entry.getValue().valueCount.get()
+                        )
+                    );
+                }
+            }
         }
         return new FieldDataStats(
             totalMetric.count(),
             evictionsMetric.count(),
-            fieldTotals == null ? null : new FieldMemoryStats(fieldTotals)
+            fieldTotals == null ? null : new FieldMemoryStats(fieldTotals),
+            new FieldDataStats.GlobalOrdinalsStats(buildTime.count(), fieldGlobalOrdinalsStats)
         );
     }
 
@@ -59,6 +79,21 @@ public class ShardFieldData implements IndexFieldDataCache.Listener {
         }
     }
 
+    @Override
+    public void onCache(ShardId shardId, String fieldName, GlobalOrdinalsAccounting info) {
+        buildTime.inc(info.getBuildingTime().millis());
+        perFieldGlobalOrdinalStats.compute(fieldName, (f, globalOrdinalFieldStats) -> {
+            if (globalOrdinalFieldStats == null) {
+                globalOrdinalFieldStats = new GlobalOrdinalFieldStats();
+            }
+            globalOrdinalFieldStats.totalBuildTime.inc(info.getBuildingTime().millis());
+            if (globalOrdinalFieldStats.valueCount.get() < info.getValueCount()) {
+                globalOrdinalFieldStats.valueCount.set(info.getValueCount());
+            }
+            return globalOrdinalFieldStats;
+        });
+    }
+
     @Override
     public void onRemoval(ShardId shardId, String fieldName, boolean wasEvicted, long sizeInBytes) {
         if (wasEvicted) {
@@ -73,4 +108,15 @@ public class ShardFieldData implements IndexFieldDataCache.Listener {
             }
         }
     }
+
+    static class GlobalOrdinalFieldStats {
+
+        private final CounterMetric totalBuildTime;
+        private final AtomicLong valueCount;
+
+        GlobalOrdinalFieldStats() {
+            this.totalBuildTime = new CounterMetric();
+            this.valueCount = new AtomicLong();
+        }
+    }
 }

+ 34 - 0
server/src/main/java/org/elasticsearch/index/fielddata/ordinals/GlobalOrdinalsAccounting.java

@@ -0,0 +1,34 @@
+/*
+ * 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.index.fielddata.ordinals;
+
+import org.elasticsearch.core.TimeValue;
+
+/**
+ * An interface global ordinals index field data instances can implement in order to keep track of building time costs.
+ * There is no global ordinal field data interface, so this is its own thing now.
+ * <p>
+ * This is a little bit similar to {@link org.apache.lucene.util.Accountable}.
+ * The building time is a big cost for global ordinals and one of its downsides.
+ * Each time the an {@link org.apache.lucene.index.IndexReader} gets re-opened,
+ * then global ordinals need to be rebuild. The cost depends on the cardinality of the field.
+ */
+public interface GlobalOrdinalsAccounting {
+
+    /**
+     * @return unique value count of global ordinals implementing this interface.
+     */
+    long getValueCount();
+
+    /**
+     * @return the total time spent building this global ordinal instance.
+     */
+    TimeValue getBuildingTime();
+
+}

+ 8 - 8
server/src/main/java/org/elasticsearch/index/fielddata/ordinals/GlobalOrdinalsBuilder.java

@@ -54,13 +54,9 @@ public enum GlobalOrdinalsBuilder {
         final long memorySizeInBytes = ordinalMap.ramBytesUsed();
         breakerService.getBreaker(CircuitBreaker.FIELDDATA).addWithoutBreaking(memorySizeInBytes);
 
+        TimeValue took = new TimeValue(System.nanoTime() - startTimeNS, TimeUnit.NANOSECONDS);
         if (logger.isDebugEnabled()) {
-            logger.debug(
-                "global-ordinals [{}][{}] took [{}]",
-                indexFieldData.getFieldName(),
-                ordinalMap.getValueCount(),
-                new TimeValue(System.nanoTime() - startTimeNS, TimeUnit.NANOSECONDS)
-            );
+            logger.debug("global-ordinals [{}][{}] took [{}]", indexFieldData.getFieldName(), ordinalMap.getValueCount(), took);
         }
         return new GlobalOrdinalsIndexFieldData(
             indexFieldData.getFieldName(),
@@ -68,7 +64,8 @@ public enum GlobalOrdinalsBuilder {
             atomicFD,
             ordinalMap,
             memorySizeInBytes,
-            toScriptFieldFactory
+            toScriptFieldFactory,
+            took
         );
     }
 
@@ -78,6 +75,7 @@ public enum GlobalOrdinalsBuilder {
         ToScriptFieldFactory<SortedSetDocValues> toScriptFieldFactory
     ) throws IOException {
         assert indexReader.leaves().size() > 1;
+        long startTimeNS = System.nanoTime();
 
         final LeafOrdinalsFieldData[] atomicFD = new LeafOrdinalsFieldData[indexReader.leaves().size()];
         final SortedSetDocValues[] subs = new SortedSetDocValues[indexReader.leaves().size()];
@@ -99,13 +97,15 @@ public enum GlobalOrdinalsBuilder {
             subs[i] = atomicFD[i].getOrdinalsValues();
         }
         final OrdinalMap ordinalMap = OrdinalMap.build(null, subs, PackedInts.DEFAULT);
+        TimeValue took = new TimeValue(System.nanoTime() - startTimeNS, TimeUnit.NANOSECONDS);
         return new GlobalOrdinalsIndexFieldData(
             indexFieldData.getFieldName(),
             indexFieldData.getValuesSourceType(),
             atomicFD,
             ordinalMap,
             0,
-            toScriptFieldFactory
+            toScriptFieldFactory,
+            took
         );
     }
 

+ 17 - 3
server/src/main/java/org/elasticsearch/index/fielddata/ordinals/GlobalOrdinalsIndexFieldData.java

@@ -16,6 +16,7 @@ import org.apache.lucene.search.SortField;
 import org.apache.lucene.util.Accountable;
 import org.elasticsearch.common.util.BigArrays;
 import org.elasticsearch.core.Nullable;
+import org.elasticsearch.core.TimeValue;
 import org.elasticsearch.index.fielddata.IndexFieldData.XFieldComparatorSource.Nested;
 import org.elasticsearch.index.fielddata.IndexOrdinalsFieldData;
 import org.elasticsearch.index.fielddata.LeafOrdinalsFieldData;
@@ -40,7 +41,7 @@ import java.util.Collection;
  * this is done to avoid creating all segment's {@link TermsEnum} each time we want to access the values of a single
  * segment.
  */
-public final class GlobalOrdinalsIndexFieldData implements IndexOrdinalsFieldData, Accountable {
+public final class GlobalOrdinalsIndexFieldData implements IndexOrdinalsFieldData, Accountable, GlobalOrdinalsAccounting {
 
     private final String fieldName;
     private final ValuesSourceType valuesSourceType;
@@ -49,14 +50,16 @@ public final class GlobalOrdinalsIndexFieldData implements IndexOrdinalsFieldDat
     private final OrdinalMap ordinalMap;
     private final LeafOrdinalsFieldData[] segmentAfd;
     private final ToScriptFieldFactory<SortedSetDocValues> toScriptFieldFactory;
+    private final TimeValue took;
 
-    protected GlobalOrdinalsIndexFieldData(
+    GlobalOrdinalsIndexFieldData(
         String fieldName,
         ValuesSourceType valuesSourceType,
         LeafOrdinalsFieldData[] segmentAfd,
         OrdinalMap ordinalMap,
         long memorySizeInBytes,
-        ToScriptFieldFactory<SortedSetDocValues> toScriptFieldFactory
+        ToScriptFieldFactory<SortedSetDocValues> toScriptFieldFactory,
+        TimeValue took
     ) {
         this.fieldName = fieldName;
         this.valuesSourceType = valuesSourceType;
@@ -64,6 +67,7 @@ public final class GlobalOrdinalsIndexFieldData implements IndexOrdinalsFieldDat
         this.ordinalMap = ordinalMap;
         this.segmentAfd = segmentAfd;
         this.toScriptFieldFactory = toScriptFieldFactory;
+        this.took = took;
     }
 
     public IndexOrdinalsFieldData newConsumer(DirectoryReader source) {
@@ -134,6 +138,16 @@ public final class GlobalOrdinalsIndexFieldData implements IndexOrdinalsFieldDat
         return true;
     }
 
+    @Override
+    public long getValueCount() {
+        return ordinalMap.getValueCount();
+    }
+
+    @Override
+    public TimeValue getBuildingTime() {
+        return took;
+    }
+
     /**
      * A non-thread safe {@link IndexOrdinalsFieldData} for global ordinals that creates the {@link TermsEnum} of each
      * segment once and use them to provide a single lookup per segment.

+ 7 - 3
server/src/main/java/org/elasticsearch/indices/fielddata/cache/IndicesFieldDataCache.java

@@ -30,6 +30,7 @@ import org.elasticsearch.index.Index;
 import org.elasticsearch.index.fielddata.IndexFieldData;
 import org.elasticsearch.index.fielddata.IndexFieldDataCache;
 import org.elasticsearch.index.fielddata.LeafFieldData;
+import org.elasticsearch.index.fielddata.ordinals.GlobalOrdinalsAccounting;
 import org.elasticsearch.index.shard.ShardId;
 import org.elasticsearch.index.shard.ShardUtils;
 
@@ -162,16 +163,19 @@ public class IndicesFieldDataCache implements RemovalListener<IndicesFieldDataCa
             final Accountable accountable = cache.computeIfAbsent(key, k -> {
                 ElasticsearchDirectoryReader.addReaderCloseListener(indexReader, IndexFieldCache.this);
                 Collections.addAll(k.listeners, this.listeners);
-                final Accountable ifd = (Accountable) indexFieldData.loadGlobalDirect(indexReader);
+                final IndexFieldData<?> ifd = indexFieldData.loadGlobalDirect(indexReader);
                 for (Listener listener : k.listeners) {
                     try {
-                        listener.onCache(shardId, fieldName, ifd);
+                        listener.onCache(shardId, fieldName, (Accountable) ifd);
+                        if (ifd instanceof GlobalOrdinalsAccounting) {
+                            listener.onCache(shardId, fieldName, (GlobalOrdinalsAccounting) ifd);
+                        }
                     } catch (Exception e) {
                         // load anyway since listeners should not throw exceptions
                         logger.error("Failed to call listener on global ordinals loading", e);
                     }
                 }
-                return ifd;
+                return (Accountable) ifd;
             });
             return (IFD) accountable;
         }

+ 7 - 1
server/src/test/java/org/elasticsearch/action/admin/cluster/node/stats/NodeStatsTests.java

@@ -617,7 +617,13 @@ public class NodeStatsTests extends ESTestCase {
 
         final CommonStats indicesCommonStats = new CommonStats(CommonStatsFlags.ALL);
         indicesCommonStats.getDocs().add(new DocsStats(++iota, ++iota, ++iota));
-        indicesCommonStats.getFieldData().add(new FieldDataStats(++iota, ++iota, null));
+        Map<String, FieldDataStats.GlobalOrdinalsStats.GlobalOrdinalFieldStats> fieldOrdinalStats = new HashMap<>();
+        fieldOrdinalStats.put(
+            randomAlphaOfLength(4),
+            new FieldDataStats.GlobalOrdinalsStats.GlobalOrdinalFieldStats(randomNonNegativeLong(), randomNonNegativeLong())
+        );
+        var ordinalStats = new FieldDataStats.GlobalOrdinalsStats(randomNonNegativeLong(), fieldOrdinalStats);
+        indicesCommonStats.getFieldData().add(new FieldDataStats(++iota, ++iota, null, ordinalStats));
         indicesCommonStats.getStore().add(new StoreStats(++iota, ++iota, ++iota));
 
         final IndexingStats.Stats indexingStats = new IndexingStats.Stats(

+ 73 - 1
server/src/test/java/org/elasticsearch/index/fielddata/FieldDataStatsTests.java

@@ -14,12 +14,21 @@ import org.elasticsearch.common.io.stream.StreamInput;
 import org.elasticsearch.test.ESTestCase;
 
 import java.io.IOException;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
 
 public class FieldDataStatsTests extends ESTestCase {
 
     public void testSerialize() throws IOException {
         FieldMemoryStats map = randomBoolean() ? null : FieldMemoryStatsTests.randomFieldMemoryStats();
-        FieldDataStats stats = new FieldDataStats(randomNonNegativeLong(), randomNonNegativeLong(), map);
+        Map<String, FieldDataStats.GlobalOrdinalsStats.GlobalOrdinalFieldStats> fieldOrdinalStats = new HashMap<>();
+        fieldOrdinalStats.put(
+            randomAlphaOfLength(4),
+            new FieldDataStats.GlobalOrdinalsStats.GlobalOrdinalFieldStats(randomNonNegativeLong(), randomNonNegativeLong())
+        );
+        FieldDataStats.GlobalOrdinalsStats glob = new FieldDataStats.GlobalOrdinalsStats(randomNonNegativeLong(), fieldOrdinalStats);
+        FieldDataStats stats = new FieldDataStats(randomNonNegativeLong(), randomNonNegativeLong(), map, glob);
         BytesStreamOutput out = new BytesStreamOutput();
         stats.writeTo(out);
         StreamInput input = out.bytes().streamInput();
@@ -29,4 +38,67 @@ public class FieldDataStatsTests extends ESTestCase {
         assertEquals(stats.getMemorySize(), read.getMemorySize());
         assertEquals(stats.getFields(), read.getFields());
     }
+
+    public void testAdd() {
+        FieldDataStats fieldDataStats = createInstance(1L, 1L, 1L, List.of());
+        fieldDataStats.add(createInstance(1L, 1L, 1L, List.of()));
+        assertEquals(fieldDataStats.getMemorySizeInBytes(), 2L);
+        assertEquals(fieldDataStats.getEvictions(), 2L);
+        assertEquals(fieldDataStats.getGlobalOrdinalsStats().getBuildTimeMillis(), 2L);
+        assertNull(fieldDataStats.getGlobalOrdinalsStats().getFieldGlobalOrdinalsStats());
+
+        fieldDataStats = createInstance(2L, 2L, 2L, List.of(Map.entry("field1", new long[] { 2L, 2L })));
+        fieldDataStats.add(createInstance(2L, 2L, 2L, List.of(Map.entry("field1", new long[] { 2L, 2L }))));
+        assertEquals(fieldDataStats.getMemorySizeInBytes(), 4L);
+        assertEquals(fieldDataStats.getEvictions(), 4L);
+        assertEquals(fieldDataStats.getGlobalOrdinalsStats().getBuildTimeMillis(), 4L);
+        assertEquals(fieldDataStats.getGlobalOrdinalsStats().getFieldGlobalOrdinalsStats().size(), 1);
+        assertEquals(fieldDataStats.getGlobalOrdinalsStats().getFieldGlobalOrdinalsStats().get("field1").valueCount(), 2L);
+        assertEquals(fieldDataStats.getGlobalOrdinalsStats().getFieldGlobalOrdinalsStats().get("field1").totalBuildingTime(), 4L);
+
+        fieldDataStats = createInstance(2L, 2L, 2L, List.of(Map.entry("field1", new long[] { 2L, 2L })));
+        fieldDataStats.add(
+            createInstance(2L, 2L, 4L, List.of(Map.entry("field1", new long[] { 2L, 2L }), Map.entry("field2", new long[] { 2L, 2L })))
+        );
+        assertEquals(fieldDataStats.getMemorySizeInBytes(), 4L);
+        assertEquals(fieldDataStats.getEvictions(), 4L);
+        assertEquals(fieldDataStats.getGlobalOrdinalsStats().getBuildTimeMillis(), 6L);
+        assertEquals(fieldDataStats.getGlobalOrdinalsStats().getFieldGlobalOrdinalsStats().size(), 2);
+        assertEquals(fieldDataStats.getGlobalOrdinalsStats().getFieldGlobalOrdinalsStats().get("field1").valueCount(), 2L);
+        assertEquals(fieldDataStats.getGlobalOrdinalsStats().getFieldGlobalOrdinalsStats().get("field1").totalBuildingTime(), 4L);
+        assertEquals(fieldDataStats.getGlobalOrdinalsStats().getFieldGlobalOrdinalsStats().size(), 2);
+        assertEquals(fieldDataStats.getGlobalOrdinalsStats().getFieldGlobalOrdinalsStats().get("field2").valueCount(), 2L);
+        assertEquals(fieldDataStats.getGlobalOrdinalsStats().getFieldGlobalOrdinalsStats().get("field2").totalBuildingTime(), 2L);
+
+        fieldDataStats = createInstance(0L, 0L, 0L, List.of());
+        fieldDataStats.add(createInstance(2L, 2L, 2L, List.of(Map.entry("field1", new long[] { 2L, 2L }))));
+        assertEquals(fieldDataStats.getMemorySizeInBytes(), 2L);
+        assertEquals(fieldDataStats.getEvictions(), 2L);
+        assertEquals(fieldDataStats.getGlobalOrdinalsStats().getBuildTimeMillis(), 2L);
+        assertEquals(fieldDataStats.getGlobalOrdinalsStats().getFieldGlobalOrdinalsStats().size(), 1);
+        assertEquals(fieldDataStats.getGlobalOrdinalsStats().getFieldGlobalOrdinalsStats().get("field1").valueCount(), 2L);
+        assertEquals(fieldDataStats.getGlobalOrdinalsStats().getFieldGlobalOrdinalsStats().get("field1").totalBuildingTime(), 2L);
+
+        fieldDataStats = createInstance(2L, 2L, 2L, List.of(Map.entry("field1", new long[] { 2L, 2L })));
+        fieldDataStats.add(createInstance(0L, 0L, 0L, List.of()));
+        assertEquals(fieldDataStats.getMemorySizeInBytes(), 2L);
+        assertEquals(fieldDataStats.getEvictions(), 2L);
+        assertEquals(fieldDataStats.getGlobalOrdinalsStats().getBuildTimeMillis(), 2L);
+        assertEquals(fieldDataStats.getGlobalOrdinalsStats().getFieldGlobalOrdinalsStats().size(), 1);
+        assertEquals(fieldDataStats.getGlobalOrdinalsStats().getFieldGlobalOrdinalsStats().get("field1").valueCount(), 2L);
+        assertEquals(fieldDataStats.getGlobalOrdinalsStats().getFieldGlobalOrdinalsStats().get("field1").totalBuildingTime(), 2L);
+    }
+
+    private static FieldDataStats createInstance(
+        long memoryInSize,
+        long evictions,
+        long buildTime,
+        List<Map.Entry<String, long[]>> entries
+    ) {
+        Map<String, FieldDataStats.GlobalOrdinalsStats.GlobalOrdinalFieldStats> f = entries.isEmpty() ? null : new HashMap<>();
+        for (Map.Entry<String, long[]> entry : entries) {
+            f.put(entry.getKey(), new FieldDataStats.GlobalOrdinalsStats.GlobalOrdinalFieldStats(entry.getValue()[0], entry.getValue()[1]));
+        }
+        return new FieldDataStats(memoryInSize, evictions, null, new FieldDataStats.GlobalOrdinalsStats(buildTime, f));
+    }
 }

+ 7 - 0
x-pack/plugin/core/src/main/resources/monitoring-es-mb.json

@@ -1351,6 +1351,13 @@
                         },
                         "evictions": {
                           "type": "long"
+                        },
+                        "global_ordinals": {
+                          "properties": {
+                            "build_time_in_millis": {
+                              "type": "long"
+                            }
+                          }
                         }
                       }
                     },

+ 21 - 0
x-pack/plugin/core/src/main/resources/monitoring-es.json

@@ -181,6 +181,13 @@
                     },
                     "evictions": {
                       "type": "long"
+                    },
+                    "global_ordinals": {
+                      "properties": {
+                        "build_time_in_millis": {
+                          "type": "long"
+                        }
+                      }
                     }
                   }
                 },
@@ -334,6 +341,13 @@
                     },
                     "evictions": {
                       "type": "long"
+                    },
+                    "global_ordinals": {
+                      "properties": {
+                        "build_time_in_millis": {
+                          "type": "long"
+                        }
+                      }
                     }
                   }
                 },
@@ -535,6 +549,13 @@
                     },
                     "evictions": {
                       "type": "long"
+                    },
+                    "global_ordinals": {
+                      "properties": {
+                        "build_time_in_millis": {
+                          "type": "long"
+                        }
+                      }
                     }
                   }
                 },

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

@@ -18,7 +18,6 @@ import org.elasticsearch.action.admin.cluster.stats.MappingStats;
 import org.elasticsearch.action.admin.cluster.stats.SearchUsageStats;
 import org.elasticsearch.action.admin.cluster.stats.VersionStats;
 import org.elasticsearch.action.admin.indices.stats.CommonStats;
-import org.elasticsearch.action.admin.indices.stats.CommonStatsFlags;
 import org.elasticsearch.action.admin.indices.stats.ShardStats;
 import org.elasticsearch.cluster.ClusterName;
 import org.elasticsearch.cluster.ClusterSnapshotStats;
@@ -43,6 +42,7 @@ import org.elasticsearch.common.xcontent.XContentHelper;
 import org.elasticsearch.core.Strings;
 import org.elasticsearch.core.TimeValue;
 import org.elasticsearch.discovery.DiscoveryModule;
+import org.elasticsearch.index.fielddata.FieldDataStats;
 import org.elasticsearch.index.shard.ShardId;
 import org.elasticsearch.license.License;
 import org.elasticsearch.monitor.fs.FsInfo;
@@ -408,7 +408,9 @@ public class ClusterStatsMonitoringDocTests extends BaseMonitoringDocTestCase<Cl
 
         final ShardStats mockShardStats = mock(ShardStats.class);
         when(mockShardStats.getShardRouting()).thenReturn(shardRouting);
-        when(mockShardStats.getStats()).thenReturn(new CommonStats(CommonStatsFlags.ALL));
+        CommonStats commonStats = mock(CommonStats.class);
+        when(commonStats.getFieldData()).thenReturn(new FieldDataStats(1, 0, null, new FieldDataStats.GlobalOrdinalsStats(1, null)));
+        when(mockShardStats.getStats()).thenReturn(commonStats);
 
         final ClusterStatsNodeResponse mockNodeResponse = mock(ClusterStatsNodeResponse.class);
         when(mockNodeResponse.clusterStatus()).thenReturn(ClusterHealthStatus.RED);
@@ -524,8 +526,9 @@ public class ClusterStatsMonitoringDocTests extends BaseMonitoringDocTestCase<Cl
                     "reserved_in_bytes": 0
                   },
                   "fielddata": {
-                    "memory_size_in_bytes": 0,
-                    "evictions": 0
+                    "memory_size_in_bytes": 1,
+                    "evictions": 0,
+                    "global_ordinals":{"build_time_in_millis":1}
                   },
                   "query_cache": {
                     "memory_size_in_bytes": 0,

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

@@ -44,6 +44,7 @@ import org.junit.Before;
 import java.io.IOException;
 import java.util.Date;
 import java.util.Locale;
+import java.util.Map;
 import java.util.Set;
 
 import static org.elasticsearch.common.xcontent.XContentHelper.convertToJson;
@@ -380,7 +381,7 @@ public class IndexStatsMonitoringDocTests extends BaseFilteredMonitoringDocTestC
 
         final CommonStats commonStats = new CommonStats(CommonStatsFlags.ALL);
         commonStats.getDocs().add(new DocsStats(++iota, no, randomNonNegativeLong()));
-        commonStats.getFieldData().add(new FieldDataStats(++iota, ++iota, null));
+        commonStats.getFieldData().add(new FieldDataStats(++iota, ++iota, null, new FieldDataStats.GlobalOrdinalsStats(0L, Map.of())));
         commonStats.getMerge().add(no, no, no, ++iota, no, no, no, no, no, no);
         commonStats.getQueryCache().add(new QueryCacheStats(++iota, ++iota, ++iota, ++iota, no));
         commonStats.getRequestCache().add(new RequestCacheStats(++iota, ++iota, ++iota, ++iota));

+ 3 - 1
x-pack/plugin/monitoring/src/test/java/org/elasticsearch/xpack/monitoring/collector/node/NodeStatsMonitoringDocTests.java

@@ -40,6 +40,7 @@ import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
 
 import static java.util.Collections.emptyList;
@@ -329,7 +330,8 @@ public class NodeStatsMonitoringDocTests extends BaseFilteredMonitoringDocTestCa
         // Indices
         final CommonStats indicesCommonStats = new CommonStats(CommonStatsFlags.ALL);
         indicesCommonStats.getDocs().add(new DocsStats(++iota, no, randomNonNegativeLong()));
-        indicesCommonStats.getFieldData().add(new FieldDataStats(++iota, ++iota, null));
+        indicesCommonStats.getFieldData()
+            .add(new FieldDataStats(++iota, ++iota, null, new FieldDataStats.GlobalOrdinalsStats(0L, Map.of())));
         indicesCommonStats.getStore().add(new StoreStats(++iota, no, no));
 
         final IndexingStats.Stats indexingStats = new IndexingStats.Stats(