Browse Source

Add query rulesets counts to enterprise search telemetry (#98071)

Kathleen DeRusso 2 years ago
parent
commit
4367c3f31c
13 changed files with 507 additions and 103 deletions
  1. 20 3
      docs/reference/query-rules/apis/list-query-rulesets.asciidoc
  2. 6 0
      docs/reference/rest-api/usage.asciidoc
  3. 2 1
      server/src/main/java/org/elasticsearch/TransportVersion.java
  4. 36 9
      x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/application/EnterpriseSearchFeatureSetUsage.java
  5. 57 0
      x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/application/EnterpriseSearchFeatureSetUsageBWCSerializingTests.java
  6. 0 47
      x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/application/EnterpriseSearchFeatureSetUsageSerializingTests.java
  7. 104 5
      x-pack/plugin/ent-search/qa/rest/src/yamlRestTest/resources/rest-api-spec/test/entsearch/100_usage.yml
  8. 95 7
      x-pack/plugin/ent-search/qa/rest/src/yamlRestTest/resources/rest-api-spec/test/entsearch/225_query_ruleset_list.yml
  9. 88 11
      x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/EnterpriseSearchUsageTransportAction.java
  10. 22 3
      x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/rules/QueryRulesIndexService.java
  11. 41 12
      x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/rules/QueryRulesetListItem.java
  12. 17 3
      x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/rules/QueryRulesIndexServiceTests.java
  13. 19 2
      x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/rules/action/ListQueryRulesetsActionResponseBWCSerializingTests.java

+ 20 - 3
docs/reference/query-rules/apis/list-query-rulesets.asciidoc

@@ -58,16 +58,33 @@ A sample response:
     "results": [
         {
             "ruleset_id": "ruleset-1",
-            "rules_count": 10
+            "rule_total_count": 10,
+            "rule_criteria_types_counts: {
+                "exact": 5,
+                "fuzzy": 5
+            }
         },
         {
             "ruleset_id": "ruleset-2",
-            "rules_count": 15
+            "rule_total_count": 15,
+            "rule_criteria_types_counts: {
+                "exact": 5,
+                "fuzzy": 10,
+                "gt": 4
+            }
         },
         {
             "ruleset_id": "ruleset-3",
-            "rules_count": 5
+            "rule_total_count": 5,
+            "rule_criteria_types_counts: {
+                "exact": 1,
+                "contains": 4
+            }
         }
     ]
 }
 ----
+// TEST[skip:TBD]
+
+[NOTE]
+The counts in `rule_criteria_types_counts` may be larger than the value of `rule_total_count`, because a rule may have multiple criteria.

+ 6 - 0
docs/reference/rest-api/usage.asciidoc

@@ -427,6 +427,12 @@ GET /_xpack/usage
     },
     "analytics_collections": {
       "count": 0
+    },
+    "query_rulesets": {
+      "total_rule_count": 0,
+      "total_count": 0,
+      "min_rule_count": 0,
+      "max_rule_count": 0
     }
   }
 }

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

@@ -174,9 +174,10 @@ public record TransportVersion(int id) implements VersionId<TransportVersion> {
     public static final TransportVersion V_8_500_049 = registerTransportVersion(8_500_049, "828bb6ce-2fbb-11ee-be56-0242ac120002");
     public static final TransportVersion V_8_500_050 = registerTransportVersion(8_500_050, "69722fa2-7c0a-4227-86fb-6d6a9a0a0321");
     public static final TransportVersion V_8_500_051 = registerTransportVersion(8_500_051, "a28b43bc-bb5f-4406-afcf-26900aa98a71");
+    public static final TransportVersion V_8_500_052 = registerTransportVersion(8_500_052, "2d382b3d-9838-4cce-84c8-4142113e5c2b");
 
     private static class CurrentHolder {
-        private static final TransportVersion CURRENT = findCurrent(V_8_500_051);
+        private static final TransportVersion CURRENT = findCurrent(V_8_500_052);
 
         // finds the pluggable current version, or uses the given fallback
         private static TransportVersion findCurrent(TransportVersion fallback) {

+ 36 - 9
x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/application/EnterpriseSearchFeatureSetUsage.java

@@ -15,46 +15,67 @@ import org.elasticsearch.xpack.core.XPackFeatureSet;
 import org.elasticsearch.xpack.core.XPackField;
 
 import java.io.IOException;
-import java.util.Collections;
+import java.util.HashMap;
 import java.util.Map;
 import java.util.Objects;
 
 public class EnterpriseSearchFeatureSetUsage extends XPackFeatureSet.Usage {
 
+    static final TransportVersion BEHAVIORAL_ANALYTICS_TRANSPORT_VERSION = TransportVersion.V_8_8_1;
+    static final TransportVersion QUERY_RULES_TRANSPORT_VERSION = TransportVersion.V_8_500_046;
+
     public static final String SEARCH_APPLICATIONS = "search_applications";
     public static final String ANALYTICS_COLLECTIONS = "analytics_collections";
+    public static final String QUERY_RULESETS = "query_rulesets";
     public static final String COUNT = "count";
+    public static final String TOTAL_COUNT = "total_count";
+    public static final String TOTAL_RULE_COUNT = "total_rule_count";
+    public static final String MIN_RULE_COUNT = "min_rule_count";
+    public static final String MAX_RULE_COUNT = "max_rule_count";
+    public static final String RULE_CRITERIA_TOTAL_COUNTS = "rule_criteria_total_counts";
+
     private final Map<String, Object> searchApplicationsUsage;
     private final Map<String, Object> analyticsCollectionsUsage;
+    private final Map<String, Object> queryRulesUsage;
 
     public EnterpriseSearchFeatureSetUsage(
         boolean available,
         boolean enabled,
         Map<String, Object> searchApplicationsUsage,
-        Map<String, Object> analyticsCollectionsUsage
+        Map<String, Object> analyticsCollectionsUsage,
+        Map<String, Object> queryRulesUsage
     ) {
         super(XPackField.ENTERPRISE_SEARCH, available, enabled);
         this.searchApplicationsUsage = Objects.requireNonNull(searchApplicationsUsage);
         this.analyticsCollectionsUsage = Objects.requireNonNull(analyticsCollectionsUsage);
+        this.queryRulesUsage = Objects.requireNonNull(queryRulesUsage);
     }
 
     public EnterpriseSearchFeatureSetUsage(StreamInput in) throws IOException {
         super(in);
         this.searchApplicationsUsage = in.readMap();
-        if (in.getTransportVersion().onOrAfter(TransportVersion.V_8_8_1)) {
-            this.analyticsCollectionsUsage = in.readMap();
-        } else {
-            this.analyticsCollectionsUsage = Collections.emptyMap();
+        Map<String, Object> analyticsCollectionsUsage = new HashMap<>();
+        Map<String, Object> queryRulesUsage = new HashMap<>();
+        if (in.getTransportVersion().onOrAfter(QUERY_RULES_TRANSPORT_VERSION)) {
+            analyticsCollectionsUsage = in.readMap();
+            queryRulesUsage = in.readMap();
+        } else if (in.getTransportVersion().onOrAfter(BEHAVIORAL_ANALYTICS_TRANSPORT_VERSION)) {
+            analyticsCollectionsUsage = in.readMap();
         }
+        this.analyticsCollectionsUsage = analyticsCollectionsUsage;
+        this.queryRulesUsage = queryRulesUsage;
     }
 
     @Override
     public void writeTo(StreamOutput out) throws IOException {
         super.writeTo(out);
         out.writeGenericMap(searchApplicationsUsage);
-        if (out.getTransportVersion().onOrAfter(TransportVersion.V_8_8_1)) {
+        if (out.getTransportVersion().onOrAfter(BEHAVIORAL_ANALYTICS_TRANSPORT_VERSION)) {
             out.writeGenericMap(analyticsCollectionsUsage);
         }
+        if (out.getTransportVersion().onOrAfter(QUERY_RULES_TRANSPORT_VERSION)) {
+            out.writeGenericMap(queryRulesUsage);
+        }
     }
 
     @Override
@@ -67,6 +88,7 @@ public class EnterpriseSearchFeatureSetUsage extends XPackFeatureSet.Usage {
         super.innerXContent(builder, params);
         builder.field(SEARCH_APPLICATIONS, searchApplicationsUsage);
         builder.field(ANALYTICS_COLLECTIONS, analyticsCollectionsUsage);
+        builder.field(QUERY_RULESETS, queryRulesUsage);
     }
 
     @Override
@@ -75,12 +97,13 @@ public class EnterpriseSearchFeatureSetUsage extends XPackFeatureSet.Usage {
         if (o == null || getClass() != o.getClass()) return false;
         EnterpriseSearchFeatureSetUsage that = (EnterpriseSearchFeatureSetUsage) o;
         return Objects.equals(searchApplicationsUsage, that.searchApplicationsUsage)
-            && Objects.equals(analyticsCollectionsUsage, that.analyticsCollectionsUsage);
+            && Objects.equals(analyticsCollectionsUsage, that.analyticsCollectionsUsage)
+            && Objects.equals(queryRulesUsage, that.queryRulesUsage);
     }
 
     @Override
     public int hashCode() {
-        return Objects.hash(searchApplicationsUsage, analyticsCollectionsUsage);
+        return Objects.hash(searchApplicationsUsage, analyticsCollectionsUsage, queryRulesUsage);
     }
 
     public Map<String, Object> getSearchApplicationsUsage() {
@@ -90,4 +113,8 @@ public class EnterpriseSearchFeatureSetUsage extends XPackFeatureSet.Usage {
     public Map<String, Object> getAnalyticsCollectionsUsage() {
         return analyticsCollectionsUsage;
     }
+
+    public Map<String, Object> getQueryRulesUsage() {
+        return queryRulesUsage;
+    }
 }

+ 57 - 0
x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/application/EnterpriseSearchFeatureSetUsageBWCSerializingTests.java

@@ -0,0 +1,57 @@
+/*
+ * 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; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+package org.elasticsearch.xpack.core.application;
+
+import org.elasticsearch.TransportVersion;
+import org.elasticsearch.common.io.stream.Writeable;
+import org.elasticsearch.xpack.core.ml.AbstractBWCWireSerializationTestCase;
+
+import java.io.IOException;
+import java.util.Map;
+
+import static org.elasticsearch.xpack.core.application.EnterpriseSearchFeatureSetUsage.BEHAVIORAL_ANALYTICS_TRANSPORT_VERSION;
+import static org.elasticsearch.xpack.core.application.EnterpriseSearchFeatureSetUsage.QUERY_RULES_TRANSPORT_VERSION;
+
+public class EnterpriseSearchFeatureSetUsageBWCSerializingTests extends AbstractBWCWireSerializationTestCase<
+    EnterpriseSearchFeatureSetUsage> {
+
+    @Override
+    protected EnterpriseSearchFeatureSetUsage createTestInstance() {
+        Map<String, Object> searchApplicationsStats = Map.of(EnterpriseSearchFeatureSetUsage.COUNT, randomLongBetween(0, 100000));
+        Map<String, Object> analyticsCollectionsStats = Map.of(EnterpriseSearchFeatureSetUsage.COUNT, randomLongBetween(0, 100000));
+        Map<String, Object> queryRulesStats = Map.of(EnterpriseSearchFeatureSetUsage.COUNT, randomLongBetween(0, 100000));
+        return new EnterpriseSearchFeatureSetUsage(true, true, searchApplicationsStats, analyticsCollectionsStats, queryRulesStats);
+    }
+
+    @Override
+    protected EnterpriseSearchFeatureSetUsage mutateInstance(EnterpriseSearchFeatureSetUsage instance) throws IOException {
+        return randomValueOtherThan(instance, this::createTestInstance);
+    }
+
+    @Override
+    protected Writeable.Reader<EnterpriseSearchFeatureSetUsage> instanceReader() {
+        return EnterpriseSearchFeatureSetUsage::new;
+    }
+
+    @Override
+    protected EnterpriseSearchFeatureSetUsage mutateInstanceForVersion(EnterpriseSearchFeatureSetUsage instance, TransportVersion version) {
+        if (version.onOrAfter(QUERY_RULES_TRANSPORT_VERSION)) {
+            return instance;
+        } else if (version.onOrAfter(BEHAVIORAL_ANALYTICS_TRANSPORT_VERSION)) {
+            return new EnterpriseSearchFeatureSetUsage(
+                true,
+                true,
+                instance.getSearchApplicationsUsage(),
+                instance.getAnalyticsCollectionsUsage(),
+                Map.of()
+            );
+        } else {
+            return new EnterpriseSearchFeatureSetUsage(true, true, instance.getSearchApplicationsUsage(), Map.of(), Map.of());
+        }
+    }
+}

+ 0 - 47
x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/application/EnterpriseSearchFeatureSetUsageSerializingTests.java

@@ -1,47 +0,0 @@
-/*
- * 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; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-package org.elasticsearch.xpack.core.application;
-
-import org.elasticsearch.common.io.stream.Writeable;
-import org.elasticsearch.test.AbstractWireSerializingTestCase;
-
-import java.io.IOException;
-import java.util.HashMap;
-import java.util.Map;
-
-public class EnterpriseSearchFeatureSetUsageSerializingTests extends AbstractWireSerializingTestCase<EnterpriseSearchFeatureSetUsage> {
-
-    @Override
-    protected EnterpriseSearchFeatureSetUsage createTestInstance() {
-        Map<String, Object> searchApplicationsStats = new HashMap<>();
-        Map<String, Object> analyticsCollectionsStats = new HashMap<>();
-        searchApplicationsStats.put(EnterpriseSearchFeatureSetUsage.COUNT, randomLongBetween(0, 100000));
-        analyticsCollectionsStats.put(EnterpriseSearchFeatureSetUsage.COUNT, randomLongBetween(0, 100000));
-        return new EnterpriseSearchFeatureSetUsage(true, true, searchApplicationsStats, analyticsCollectionsStats);
-    }
-
-    @Override
-    protected EnterpriseSearchFeatureSetUsage mutateInstance(EnterpriseSearchFeatureSetUsage instance) throws IOException {
-        long searchApplicationsCount = (long) instance.getSearchApplicationsUsage().get(EnterpriseSearchFeatureSetUsage.COUNT);
-        searchApplicationsCount = randomValueOtherThan(searchApplicationsCount, () -> randomLongBetween(0, 100000));
-        long analyticsCollectionsCount = (long) instance.getAnalyticsCollectionsUsage().get(EnterpriseSearchFeatureSetUsage.COUNT);
-        analyticsCollectionsCount = randomValueOtherThan(analyticsCollectionsCount, () -> randomLongBetween(0, 100000));
-
-        Map<String, Object> searchApplicationsStats = new HashMap<>();
-        Map<String, Object> analyticsCollectionsStats = new HashMap<>();
-        searchApplicationsStats.put("count", searchApplicationsCount);
-        analyticsCollectionsStats.put("count", analyticsCollectionsCount);
-
-        return new EnterpriseSearchFeatureSetUsage(true, true, searchApplicationsStats, analyticsCollectionsStats);
-    }
-
-    @Override
-    protected Writeable.Reader<EnterpriseSearchFeatureSetUsage> instanceReader() {
-        return EnterpriseSearchFeatureSetUsage::new;
-    }
-}

+ 104 - 5
x-pack/plugin/ent-search/qa/rest/src/yamlRestTest/resources/rest-api-spec/test/entsearch/100_usage.yml

@@ -35,7 +35,8 @@ teardown:
       enabled: true,
       available: true,
       search_applications: { count: 0 },
-      analytics_collections: { count: 0 }
+      analytics_collections: { count: 0 },
+      query_rulesets: {  total_count: 0, total_rule_count: 0, min_rule_count: 0, max_rule_count: 0 }
     }
   }
 
@@ -59,7 +60,8 @@ teardown:
       enabled: true,
       available: true,
       search_applications: { count: 1 },
-      analytics_collections: { count: 0 }
+      analytics_collections: { count: 0 },
+      query_rulesets: {  total_count: 0, total_rule_count: 0, min_rule_count: 0, max_rule_count: 0 }
     }
   }
 
@@ -87,7 +89,8 @@ teardown:
       enabled: true,
       available: true,
       search_applications: { count: 2 },
-      analytics_collections: { count: 1 }
+      analytics_collections: { count: 1 },
+      query_rulesets: {  total_count: 0, total_rule_count: 0, min_rule_count: 0, max_rule_count: 0 }
     }
   }
 
@@ -103,7 +106,8 @@ teardown:
       enabled: true,
       available: true,
       search_applications: { count: 1 },
-      analytics_collections: { count: 1 }
+      analytics_collections: { count: 1 },
+      query_rulesets: {  total_count: 0, total_rule_count: 0, min_rule_count: 0, max_rule_count: 0 }
     }
   }
 
@@ -119,6 +123,101 @@ teardown:
       enabled: true,
       available: true,
       search_applications: { count: 1 },
-      analytics_collections: { count: 0 }
+      analytics_collections: { count: 0 },
+      query_rulesets: {  total_count: 0, total_rule_count: 0, min_rule_count: 0, max_rule_count: 0 }
     }
   }
+
+  - do:
+      query_ruleset.put:
+        ruleset_id: test-query-ruleset
+        body:
+          rules:
+            - rule_id: query-rule-id1
+              type: pinned
+              criteria:
+                - type: exact
+                  metadata: query_string
+                  values: [ puggles ]
+              actions:
+                ids:
+                  - 'id1'
+                  - 'id2'
+            - rule_id: query-rule-id2
+              type: pinned
+              criteria:
+                - type: exact
+                  metadata: query_string
+                  values: [ pugs ]
+              actions:
+                ids:
+                  - 'id3'
+                  - 'id4'
+
+  - do:
+      query_ruleset.put:
+        ruleset_id: test-query-ruleset2
+        body:
+          rules:
+            - rule_id: query-rule-id1
+              type: pinned
+              criteria:
+                - type: exact
+                  metadata: query_string
+                  values: [ beagles ]
+              actions:
+                ids:
+                  - 'id1'
+                  - 'id2'
+            - rule_id: query-rule-id2
+              type: pinned
+              criteria:
+                - type: exact
+                  metadata: query_string
+                  values: [ pugs ]
+              actions:
+                ids:
+                  - 'id3'
+                  - 'id4'
+            - rule_id: query-rule-id3
+              type: pinned
+              criteria:
+                - type: exact
+                  metadata: query_string
+                  values: [ puggles ]
+              actions:
+                ids:
+                  - 'id4'
+                  - 'id5'
+
+  - do:
+      xpack.usage: { }
+
+  - match: {
+    enterprise_search: {
+      enabled: true,
+      available: true,
+      search_applications: { count: 1 },
+      analytics_collections: { count: 0 },
+      query_rulesets: {  total_count: 2, total_rule_count: 5, min_rule_count: 2, max_rule_count: 3, rule_criteria_total_counts: { exact: 5 } }
+    }
+  }
+
+  - do:
+      query_ruleset.delete:
+        ruleset_id: test-query-ruleset2
+
+  - do:
+      xpack.usage: { }
+
+  - match: {
+    enterprise_search: {
+      enabled: true,
+      available: true,
+      search_applications: { count: 1 },
+      analytics_collections: { count: 0 },
+      query_rulesets: {  total_count: 1, total_rule_count: 2, min_rule_count: 2, max_rule_count: 2, rule_criteria_total_counts: { exact: 2 } }
+    }
+  }
+
+

+ 95 - 7
x-pack/plugin/ent-search/qa/rest/src/yamlRestTest/resources/rest-api-spec/test/entsearch/225_query_ruleset_list.yml

@@ -119,13 +119,16 @@ setup:
 
   # Alphabetical order by ruleset_id for results
   - match: { results.0.ruleset_id: "test-query-ruleset-1" }
-  - match: { results.0.rules_count: 3 }
+  - match: { results.0.rule_total_count: 3 }
+  - match: { results.0.rule_criteria_types_counts: { exact: 3 } }
 
   - match: { results.1.ruleset_id: "test-query-ruleset-2" }
-  - match: { results.1.rules_count: 4 }
+  - match: { results.1.rule_total_count: 4 }
+  - match: { results.1.rule_criteria_types_counts: { exact: 4 } }
 
   - match: { results.2.ruleset_id: "test-query-ruleset-3" }
-  - match: { results.2.rules_count: 2 }
+  - match: { results.2.rule_total_count: 2 }
+  - match: { results.2.rule_criteria_types_counts: { exact: 2 } }
 
 ---
 "List Query Rulesets - with from":
@@ -137,10 +140,12 @@ setup:
 
   # Alphabetical order by ruleset_id for results
   - match: { results.0.ruleset_id: "test-query-ruleset-2" }
-  - match: { results.0.rules_count: 4 }
+  - match: { results.0.rule_total_count: 4 }
+  - match: { results.0.rule_criteria_types_counts: { exact: 4 } }
 
   - match: { results.1.ruleset_id: "test-query-ruleset-3" }
-  - match: { results.1.rules_count: 2 }
+  - match: { results.1.rule_total_count: 2 }
+  - match: { results.1.rule_criteria_types_counts: { exact: 2 } }
 
 ---
 "List Query Rulesets - with size":
@@ -152,10 +157,12 @@ setup:
 
   # Alphabetical order by ruleset_id for results
   - match: { results.0.ruleset_id: "test-query-ruleset-1" }
-  - match: { results.0.rules_count: 3 }
+  - match: { results.0.rule_total_count: 3 }
+  - match: { results.0.rule_criteria_types_counts: { exact: 3 } }
 
   - match: { results.1.ruleset_id: "test-query-ruleset-2" }
-  - match: { results.1.rules_count: 4 }
+  - match: { results.1.rule_total_count: 4 }
+  - match: { results.1.rule_criteria_types_counts: { exact: 4 } }
 
 ---
 "List Query Rulesets - empty":
@@ -175,3 +182,84 @@ setup:
       query_ruleset.list: { }
 
   - match: { count: 0 }
+
+---
+"List Query Rulesets with multiple rules":
+  - do:
+      query_ruleset.put:
+        ruleset_id: a-test-query-ruleset-with-lots-of-criteria
+        body:
+          rules:
+            - rule_id: query-rule-id1
+              type: pinned
+              criteria:
+                - type: exact
+                  metadata: query_string
+                  values: [ puggles ]
+                - type: gt
+                  metadata: year
+                  values: [ 2023 ]
+              actions:
+                ids:
+                  - 'id1'
+                  - 'id2'
+            - rule_id: query-rule-id2
+              type: pinned
+              criteria:
+                - type: exact
+                  metadata: query_string
+                  values: [ pug ]
+              actions:
+                ids:
+                  - 'id3'
+                  - 'id4'
+            - rule_id: query-rule-id3
+              type: pinned
+              criteria:
+                - type: fuzzy
+                  metadata: query_string
+                  values: [ puggles ]
+              actions:
+                ids:
+                  - 'id5'
+                  - 'id6'
+            - rule_id: query-rule-id4
+              type: pinned
+              criteria:
+                - type: always
+              actions:
+                ids:
+                  - 'id7'
+                  - 'id8'
+            - rule_id: query-rule-id5
+              type: pinned
+              criteria:
+                - type: prefix
+                  metadata: query_string
+                  values: [ pug ]
+                - type: suffix
+                  metadata: query_string
+                  values: [ gle ]
+              actions:
+                ids:
+                  - 'id9'
+                  - 'id10'
+
+  - do:
+      query_ruleset.list:
+        from: 0
+        size: 1
+
+  - match: { count: 4 }
+
+  # Alphabetical order by ruleset_id for results
+  - match: { results.0.ruleset_id: "a-test-query-ruleset-with-lots-of-criteria" }
+  - match: { results.0.rule_total_count: 5 }
+  - match:
+      results.0.rule_criteria_types_counts:
+        exact: 2
+        gt: 1
+        fuzzy: 1
+        prefix: 1
+        suffix: 1
+        always: 1

+ 88 - 11
x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/EnterpriseSearchUsageTransportAction.java

@@ -8,8 +8,13 @@
 package org.elasticsearch.xpack.application;
 
 import org.elasticsearch.action.ActionListener;
+import org.elasticsearch.action.admin.indices.stats.IndexStats;
+import org.elasticsearch.action.admin.indices.stats.IndicesStatsAction;
+import org.elasticsearch.action.admin.indices.stats.IndicesStatsRequest;
+import org.elasticsearch.action.admin.indices.stats.IndicesStatsResponse;
 import org.elasticsearch.action.support.ActionFilters;
 import org.elasticsearch.client.internal.Client;
+import org.elasticsearch.client.internal.IndicesAdminClient;
 import org.elasticsearch.client.internal.OriginSettingClient;
 import org.elasticsearch.cluster.ClusterState;
 import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver;
@@ -24,6 +29,10 @@ import org.elasticsearch.tasks.Task;
 import org.elasticsearch.threadpool.ThreadPool;
 import org.elasticsearch.transport.TransportService;
 import org.elasticsearch.xpack.application.analytics.action.GetAnalyticsCollectionAction;
+import org.elasticsearch.xpack.application.rules.QueryRuleCriteriaType;
+import org.elasticsearch.xpack.application.rules.QueryRulesIndexService;
+import org.elasticsearch.xpack.application.rules.QueryRulesetListItem;
+import org.elasticsearch.xpack.application.rules.action.ListQueryRulesetsAction;
 import org.elasticsearch.xpack.application.search.action.ListSearchApplicationAction;
 import org.elasticsearch.xpack.application.utils.LicenseUtils;
 import org.elasticsearch.xpack.core.XPackSettings;
@@ -35,14 +44,23 @@ import org.elasticsearch.xpack.core.application.EnterpriseSearchFeatureSetUsage;
 
 import java.util.Collections;
 import java.util.HashMap;
+import java.util.IntSummaryStatistics;
+import java.util.List;
+import java.util.Locale;
 import java.util.Map;
 
 import static org.elasticsearch.xpack.core.ClientHelper.ENT_SEARCH_ORIGIN;
+import static org.elasticsearch.xpack.core.application.EnterpriseSearchFeatureSetUsage.MAX_RULE_COUNT;
+import static org.elasticsearch.xpack.core.application.EnterpriseSearchFeatureSetUsage.MIN_RULE_COUNT;
+import static org.elasticsearch.xpack.core.application.EnterpriseSearchFeatureSetUsage.RULE_CRITERIA_TOTAL_COUNTS;
+import static org.elasticsearch.xpack.core.application.EnterpriseSearchFeatureSetUsage.TOTAL_COUNT;
+import static org.elasticsearch.xpack.core.application.EnterpriseSearchFeatureSetUsage.TOTAL_RULE_COUNT;
 
 public class EnterpriseSearchUsageTransportAction extends XPackUsageFeatureTransportAction {
     private static final Logger logger = LogManager.getLogger(EnterpriseSearchUsageTransportAction.class);
     private final XPackLicenseState licenseState;
     private final OriginSettingClient clientWithOrigin;
+    private final IndicesAdminClient indicesAdminClient;
 
     private final boolean enabled;
 
@@ -67,6 +85,7 @@ public class EnterpriseSearchUsageTransportAction extends XPackUsageFeatureTrans
         );
         this.licenseState = licenseState;
         this.clientWithOrigin = new OriginSettingClient(client, ENT_SEARCH_ORIGIN);
+        this.indicesAdminClient = clientWithOrigin.admin().indices();
         this.enabled = XPackSettings.ENTERPRISE_SEARCH_ENABLED.get(settings);
     }
 
@@ -82,6 +101,7 @@ public class EnterpriseSearchUsageTransportAction extends XPackUsageFeatureTrans
                 LicenseUtils.LICENSED_ENT_SEARCH_FEATURE.checkWithoutTracking(licenseState),
                 enabled,
                 Collections.emptyMap(),
+                Collections.emptyMap(),
                 Collections.emptyMap()
             );
             listener.onResponse(new XPackUsageFeatureResponse(usage));
@@ -90,8 +110,9 @@ public class EnterpriseSearchUsageTransportAction extends XPackUsageFeatureTrans
 
         Map<String, Object> searchApplicationsUsage = new HashMap<>();
         Map<String, Object> analyticsCollectionsUsage = new HashMap<>();
+        Map<String, Object> queryRulesUsage = new HashMap<>();
 
-        // Step 2: Fetch search applications count and return usage
+        // Step 3: Fetch search applications count and return usage
         ListSearchApplicationAction.Request searchApplicationsCountRequest = new ListSearchApplicationAction.Request(
             "*",
             new PageParams(0, 0)
@@ -104,7 +125,8 @@ public class EnterpriseSearchUsageTransportAction extends XPackUsageFeatureTrans
                         LicenseUtils.LICENSED_ENT_SEARCH_FEATURE.checkWithoutTracking(licenseState),
                         enabled,
                         searchApplicationsUsage,
-                        analyticsCollectionsUsage
+                        analyticsCollectionsUsage,
+                        queryRulesUsage
                     )
                 )
             );
@@ -115,18 +137,17 @@ public class EnterpriseSearchUsageTransportAction extends XPackUsageFeatureTrans
                         LicenseUtils.LICENSED_ENT_SEARCH_FEATURE.checkWithoutTracking(licenseState),
                         enabled,
                         Collections.emptyMap(),
-                        analyticsCollectionsUsage
+                        analyticsCollectionsUsage,
+                        queryRulesUsage
                     )
                 )
             );
         });
 
-        // Step 1: Fetch analytics collections count
-        GetAnalyticsCollectionAction.Request analyticsCollectionsCountRequest = new GetAnalyticsCollectionAction.Request(
-            new String[] { "*" }
-        );
-        ActionListener<GetAnalyticsCollectionAction.Response> analyticsCollectionsCountListener = ActionListener.wrap(response -> {
-            addAnalyticsCollectionsUsage(response, analyticsCollectionsUsage);
+        // Step 2: Fetch query rules stats
+
+        ActionListener<ListQueryRulesetsAction.Response> listQueryRulesetsListener = ActionListener.wrap(response -> {
+            addQueryRulesetUsage(response, queryRulesUsage);
             clientWithOrigin.execute(ListSearchApplicationAction.INSTANCE, searchApplicationsCountRequest, searchApplicationsCountListener);
         },
             e -> {
@@ -138,6 +159,43 @@ public class EnterpriseSearchUsageTransportAction extends XPackUsageFeatureTrans
             }
         );
 
+        IndicesStatsRequest indicesStatsRequest = indicesAdminClient.prepareStats(QueryRulesIndexService.QUERY_RULES_ALIAS_NAME)
+            .setDocs(true)
+            .request();
+
+        // Step 1: Fetch analytics collections count
+        GetAnalyticsCollectionAction.Request analyticsCollectionsCountRequest = new GetAnalyticsCollectionAction.Request(
+            new String[] { "*" }
+        );
+
+        ActionListener<GetAnalyticsCollectionAction.Response> analyticsCollectionsCountListener = ActionListener.wrap(response -> {
+            addAnalyticsCollectionsUsage(response, analyticsCollectionsUsage);
+            indicesAdminClient.execute(IndicesStatsAction.INSTANCE, indicesStatsRequest, new ActionListener<>() {
+                @Override
+                public void onResponse(IndicesStatsResponse indicesStatsResponse) {
+                    Map<String, IndexStats> indicesStats = indicesStatsResponse.getIndices();
+                    int queryRulesetCount = indicesStats.values()
+                        .stream()
+                        .mapToInt(indexShardStats -> (int) indexShardStats.getPrimaries().getDocs().getCount())
+                        .sum();
+
+                    ListQueryRulesetsAction.Request queryRulesetsCountRequest = new ListQueryRulesetsAction.Request(
+                        new PageParams(0, queryRulesetCount)
+                    );
+                    clientWithOrigin.execute(ListQueryRulesetsAction.INSTANCE, queryRulesetsCountRequest, listQueryRulesetsListener);
+                }
+
+                @Override
+                public void onFailure(Exception e) {
+                    ListQueryRulesetsAction.Request queryRulesetsCountRequest = new ListQueryRulesetsAction.Request(new PageParams(0, 0));
+                    clientWithOrigin.execute(ListQueryRulesetsAction.INSTANCE, queryRulesetsCountRequest, listQueryRulesetsListener);
+                }
+            });
+        }, e -> {
+            ListQueryRulesetsAction.Request queryRulesetsCountRequest = new ListQueryRulesetsAction.Request(new PageParams(0, 0));
+            clientWithOrigin.execute(ListQueryRulesetsAction.INSTANCE, queryRulesetsCountRequest, listQueryRulesetsListener);
+        });
+
         // Step 0: Kick off requests
         clientWithOrigin.execute(
             GetAnalyticsCollectionAction.INSTANCE,
@@ -148,7 +206,6 @@ public class EnterpriseSearchUsageTransportAction extends XPackUsageFeatureTrans
 
     private void addSearchApplicationsUsage(ListSearchApplicationAction.Response response, Map<String, Object> searchApplicationsUsage) {
         long count = response.queryPage().count();
-
         searchApplicationsUsage.put(EnterpriseSearchFeatureSetUsage.COUNT, count);
     }
 
@@ -157,7 +214,27 @@ public class EnterpriseSearchUsageTransportAction extends XPackUsageFeatureTrans
         Map<String, Object> analyticsCollectionsUsage
     ) {
         long count = response.getAnalyticsCollections().size();
-
         analyticsCollectionsUsage.put(EnterpriseSearchFeatureSetUsage.COUNT, count);
     }
+
+    private void addQueryRulesetUsage(ListQueryRulesetsAction.Response response, Map<String, Object> queryRulesUsage) {
+        List<QueryRulesetListItem> results = response.queryPage().results();
+        IntSummaryStatistics ruleStats = results.stream().mapToInt(QueryRulesetListItem::ruleTotalCount).summaryStatistics();
+
+        Map<QueryRuleCriteriaType, Integer> criteriaTypeCountMap = new HashMap<>();
+        results.stream()
+            .flatMap(result -> result.criteriaTypeToCountMap().entrySet().stream())
+            .forEach(entry -> criteriaTypeCountMap.merge(entry.getKey(), entry.getValue(), Integer::sum));
+
+        Map<String, Object> rulesTypeCountMap = new HashMap<>();
+        criteriaTypeCountMap.forEach((criteriaType, count) -> rulesTypeCountMap.put(criteriaType.name().toLowerCase(Locale.ROOT), count));
+
+        queryRulesUsage.put(TOTAL_COUNT, response.queryPage().count());
+        queryRulesUsage.put(TOTAL_RULE_COUNT, ruleStats.getSum());
+        queryRulesUsage.put(MIN_RULE_COUNT, results.isEmpty() ? 0 : ruleStats.getMin());
+        queryRulesUsage.put(MAX_RULE_COUNT, results.isEmpty() ? 0 : ruleStats.getMax());
+        if (rulesTypeCountMap.isEmpty() == false) {
+            queryRulesUsage.put(RULE_CRITERIA_TOTAL_COUNTS, rulesTypeCountMap);
+        }
+    }
 }

+ 22 - 3
x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/rules/QueryRulesIndexService.java

@@ -44,6 +44,8 @@ import java.io.UncheckedIOException;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
@@ -278,7 +280,13 @@ public class QueryRulesIndexService {
             final SearchSourceBuilder source = new SearchSourceBuilder().from(from)
                 .size(size)
                 .query(new MatchAllQueryBuilder())
-                .fetchSource(new String[] { QueryRuleset.ID_FIELD.getPreferredName(), QueryRuleset.RULES_FIELD.getPreferredName() }, null)
+                .fetchSource(
+                    new String[] {
+                        QueryRuleset.ID_FIELD.getPreferredName(),
+                        QueryRuleset.RULES_FIELD.getPreferredName(),
+                        QueryRuleset.RULES_FIELD.getPreferredName() + "." + QueryRule.TYPE_FIELD.getPreferredName() },
+                    null
+                )
                 .sort(QueryRuleset.ID_FIELD.getPreferredName(), SortOrder.ASC);
             final SearchRequest req = new SearchRequest(QUERY_RULES_ALIAS_NAME).source(source);
             clientWithOrigin.search(req, new ActionListener<>() {
@@ -312,9 +320,20 @@ public class QueryRulesIndexService {
         final Map<String, Object> sourceMap = searchHit.getSourceAsMap();
         final String rulesetId = (String) sourceMap.get(QueryRuleset.ID_FIELD.getPreferredName());
         @SuppressWarnings("unchecked")
-        final int numRules = ((List<QueryRule>) sourceMap.get(QueryRuleset.RULES_FIELD.getPreferredName())).size();
+        final List<LinkedHashMap<?, ?>> rules = ((List<LinkedHashMap<?, ?>>) sourceMap.get(QueryRuleset.RULES_FIELD.getPreferredName()));
+        final int numRules = rules.size();
+        final Map<QueryRuleCriteriaType, Integer> queryRuleCriteriaTypeToCountMap = new HashMap<>();
+        for (LinkedHashMap<?, ?> rule : rules) {
+            @SuppressWarnings("unchecked")
+            List<LinkedHashMap<?, ?>> criteriaList = ((List<LinkedHashMap<?, ?>>) rule.get(QueryRule.CRITERIA_FIELD.getPreferredName()));
+            for (LinkedHashMap<?, ?> criteria : criteriaList) {
+                final String criteriaType = ((String) criteria.get(QueryRuleCriteria.TYPE_FIELD.getPreferredName()));
+                final QueryRuleCriteriaType queryRuleCriteriaType = QueryRuleCriteriaType.type(criteriaType);
+                queryRuleCriteriaTypeToCountMap.compute(queryRuleCriteriaType, (k, v) -> v == null ? 1 : v + 1);
+            }
+        }
 
-        return new QueryRulesetListItem(rulesetId, numRules);
+        return new QueryRulesetListItem(rulesetId, numRules, queryRuleCriteriaTypeToCountMap);
     }
 
     static class DelegatingIndexNotFoundActionListener<T, R> extends DelegatingActionListener<T, R> {

+ 41 - 12
x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/rules/QueryRulesetListItem.java

@@ -7,6 +7,7 @@
 
 package org.elasticsearch.xpack.application.rules;
 
+import org.elasticsearch.TransportVersion;
 import org.elasticsearch.common.io.stream.StreamInput;
 import org.elasticsearch.common.io.stream.StreamOutput;
 import org.elasticsearch.common.io.stream.Writeable;
@@ -15,6 +16,8 @@ import org.elasticsearch.xcontent.ToXContentObject;
 import org.elasticsearch.xcontent.XContentBuilder;
 
 import java.io.IOException;
+import java.util.Locale;
+import java.util.Map;
 import java.util.Objects;
 
 /**
@@ -23,34 +26,51 @@ import java.util.Objects;
  */
 public class QueryRulesetListItem implements Writeable, ToXContentObject {
 
+    // TODO we need to actually bump transport version, but there's no point until main is merged. Placeholder for now.
+    public static final TransportVersion EXPANDED_RULESET_COUNT_TRANSPORT_VERSION = TransportVersion.V_8_500_052;
+
     public static final ParseField RULESET_ID_FIELD = new ParseField("ruleset_id");
-    public static final ParseField NUM_RULES_FIELD = new ParseField("rules_count");
+    public static final ParseField RULE_TOTAL_COUNT_FIELD = new ParseField("rule_total_count");
+    public static final ParseField RULE_CRITERIA_TYPE_COUNTS_FIELD = new ParseField("rule_criteria_types_counts");
 
     private final String rulesetId;
-    private final int numRules;
+    private final int ruleTotalCount;
+    private final Map<QueryRuleCriteriaType, Integer> criteriaTypeToCountMap;
 
     /**
      * Constructs a QueryRulesetListItem.
      *
      * @param rulesetId The unique identifier for the ruleset
-     * @param numRules  The number of rules contained within the ruleset.
+     * @param ruleTotalCount  The number of rules contained within the ruleset.
+     * @param criteriaTypeToCountMap A map of criteria type to the number of rules of that type.
      */
-    public QueryRulesetListItem(String rulesetId, int numRules) {
+    public QueryRulesetListItem(String rulesetId, int ruleTotalCount, Map<QueryRuleCriteriaType, Integer> criteriaTypeToCountMap) {
         Objects.requireNonNull(rulesetId, "rulesetId cannot be null on a QueryRuleListItem");
         this.rulesetId = rulesetId;
-        this.numRules = numRules;
+        this.ruleTotalCount = ruleTotalCount;
+        this.criteriaTypeToCountMap = criteriaTypeToCountMap;
     }
 
     public QueryRulesetListItem(StreamInput in) throws IOException {
         this.rulesetId = in.readString();
-        this.numRules = in.readInt();
+        this.ruleTotalCount = in.readInt();
+        if (in.getTransportVersion().onOrAfter(EXPANDED_RULESET_COUNT_TRANSPORT_VERSION)) {
+            this.criteriaTypeToCountMap = in.readMap(m -> in.readEnum(QueryRuleCriteriaType.class), StreamInput::readInt);
+        } else {
+            this.criteriaTypeToCountMap = Map.of();
+        }
     }
 
     @Override
     public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
         builder.startObject();
         builder.field(RULESET_ID_FIELD.getPreferredName(), rulesetId);
-        builder.field(NUM_RULES_FIELD.getPreferredName(), numRules);
+        builder.field(RULE_TOTAL_COUNT_FIELD.getPreferredName(), ruleTotalCount);
+        builder.startObject(RULE_CRITERIA_TYPE_COUNTS_FIELD.getPreferredName());
+        for (QueryRuleCriteriaType criteriaType : criteriaTypeToCountMap.keySet()) {
+            builder.field(criteriaType.name().toLowerCase(Locale.ROOT), criteriaTypeToCountMap.get(criteriaType));
+        }
+        builder.endObject();
         builder.endObject();
         return builder;
     }
@@ -58,7 +78,10 @@ public class QueryRulesetListItem implements Writeable, ToXContentObject {
     @Override
     public void writeTo(StreamOutput out) throws IOException {
         out.writeString(rulesetId);
-        out.writeInt(numRules);
+        out.writeInt(ruleTotalCount);
+        if (out.getTransportVersion().onOrAfter(EXPANDED_RULESET_COUNT_TRANSPORT_VERSION)) {
+            out.writeMap(criteriaTypeToCountMap, StreamOutput::writeEnum, StreamOutput::writeInt);
+        }
     }
 
     /**
@@ -75,8 +98,12 @@ public class QueryRulesetListItem implements Writeable, ToXContentObject {
      *
      * @return the total number of rules.
      */
-    public int numRules() {
-        return numRules;
+    public int ruleTotalCount() {
+        return ruleTotalCount;
+    }
+
+    public Map<QueryRuleCriteriaType, Integer> criteriaTypeToCountMap() {
+        return criteriaTypeToCountMap;
     }
 
     @Override
@@ -84,11 +111,13 @@ public class QueryRulesetListItem implements Writeable, ToXContentObject {
         if (this == o) return true;
         if (o == null || getClass() != o.getClass()) return false;
         QueryRulesetListItem that = (QueryRulesetListItem) o;
-        return numRules == that.numRules && Objects.equals(rulesetId, that.rulesetId);
+        return ruleTotalCount == that.ruleTotalCount
+            && Objects.equals(rulesetId, that.rulesetId)
+            && Objects.equals(criteriaTypeToCountMap, that.criteriaTypeToCountMap);
     }
 
     @Override
     public int hashCode() {
-        return Objects.hash(rulesetId, numRules);
+        return Objects.hash(rulesetId, ruleTotalCount, criteriaTypeToCountMap);
     }
 }

+ 17 - 3
x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/rules/QueryRulesIndexServiceTests.java

@@ -33,6 +33,8 @@ import java.util.concurrent.atomic.AtomicReference;
 
 import static org.elasticsearch.xpack.application.rules.QueryRule.QueryRuleType;
 import static org.elasticsearch.xpack.application.rules.QueryRuleCriteriaType.EXACT;
+import static org.elasticsearch.xpack.application.rules.QueryRuleCriteriaType.FUZZY;
+import static org.elasticsearch.xpack.application.rules.QueryRuleCriteriaType.GTE;
 import static org.elasticsearch.xpack.application.rules.QueryRulesIndexService.QUERY_RULES_CONCRETE_INDEX_NAME;
 import static org.hamcrest.CoreMatchers.anyOf;
 import static org.hamcrest.CoreMatchers.equalTo;
@@ -108,13 +110,19 @@ public class QueryRulesIndexServiceTests extends ESSingleNodeTestCase {
                 new QueryRule(
                     "my_rule_" + i,
                     QueryRuleType.PINNED,
-                    List.of(new QueryRuleCriteria(EXACT, "query_string", List.of("foo" + i))),
+                    List.of(
+                        new QueryRuleCriteria(EXACT, "query_string", List.of("foo" + i)),
+                        new QueryRuleCriteria(GTE, "query_string", List.of(i))
+                    ),
                     Map.of("ids", List.of("id1", "id2"))
                 ),
                 new QueryRule(
                     "my_rule_" + i + "_" + (i + 1),
                     QueryRuleType.PINNED,
-                    List.of(new QueryRuleCriteria(EXACT, "query_string", List.of("bar" + i))),
+                    List.of(
+                        new QueryRuleCriteria(FUZZY, "query_string", List.of("bar" + i)),
+                        new QueryRuleCriteria(GTE, "user.age", List.of(i))
+                    ),
                     Map.of("ids", List.of("id3", "id4"))
                 )
             );
@@ -147,8 +155,14 @@ public class QueryRulesIndexServiceTests extends ESSingleNodeTestCase {
 
             for (int i = 0; i < 5; i++) {
                 int index = i + 5;
-                String rulesetId = rulesets.get(i).rulesetId();
+                QueryRulesetListItem ruleset = rulesets.get(i);
+                String rulesetId = ruleset.rulesetId();
                 assertThat(rulesetId, equalTo("my_ruleset_" + index));
+                Map<QueryRuleCriteriaType, Integer> criteriaTypeCountMap = ruleset.criteriaTypeToCountMap();
+                assertThat(criteriaTypeCountMap.size(), equalTo(3));
+                assertThat(criteriaTypeCountMap.get(EXACT), equalTo(1));
+                assertThat(criteriaTypeCountMap.get(FUZZY), equalTo(1));
+                assertThat(criteriaTypeCountMap.get(GTE), equalTo(2));
             }
         }
     }

+ 19 - 2
x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/rules/action/ListQueryRulesetsActionResponseBWCSerializingTests.java

@@ -9,11 +9,16 @@ package org.elasticsearch.xpack.application.rules.action;
 
 import org.elasticsearch.TransportVersion;
 import org.elasticsearch.common.io.stream.Writeable;
+import org.elasticsearch.xpack.application.rules.QueryRuleCriteriaType;
 import org.elasticsearch.xpack.application.rules.QueryRuleset;
 import org.elasticsearch.xpack.application.rules.QueryRulesetListItem;
 import org.elasticsearch.xpack.application.search.SearchApplicationTestUtils;
 import org.elasticsearch.xpack.core.ml.AbstractBWCWireSerializationTestCase;
 
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
 public class ListQueryRulesetsActionResponseBWCSerializingTests extends AbstractBWCWireSerializationTestCase<
     ListQueryRulesetsAction.Response> {
 
@@ -25,7 +30,11 @@ public class ListQueryRulesetsActionResponseBWCSerializingTests extends Abstract
     private static ListQueryRulesetsAction.Response randomQueryRulesetListItem() {
         return new ListQueryRulesetsAction.Response(randomList(10, () -> {
             QueryRuleset queryRuleset = SearchApplicationTestUtils.randomQueryRuleset();
-            return new QueryRulesetListItem(queryRuleset.id(), queryRuleset.rules().size());
+            Map<QueryRuleCriteriaType, Integer> criteriaTypeToCountMap = Map.of(
+                randomFrom(QueryRuleCriteriaType.values()),
+                randomIntBetween(0, 10)
+            );
+            return new QueryRulesetListItem(queryRuleset.id(), queryRuleset.rules().size(), criteriaTypeToCountMap);
         }), randomLongBetween(0, 1000));
     }
 
@@ -44,6 +53,14 @@ public class ListQueryRulesetsActionResponseBWCSerializingTests extends Abstract
         ListQueryRulesetsAction.Response instance,
         TransportVersion version
     ) {
-        return instance;
+        if (version.onOrAfter(QueryRulesetListItem.EXPANDED_RULESET_COUNT_TRANSPORT_VERSION)) {
+            return instance;
+        } else {
+            List<QueryRulesetListItem> updatedResults = new ArrayList<>();
+            for (QueryRulesetListItem listItem : instance.queryPage.results()) {
+                updatedResults.add(new QueryRulesetListItem(listItem.rulesetId(), listItem.ruleTotalCount(), Map.of()));
+            }
+            return new ListQueryRulesetsAction.Response(updatedResults, instance.queryPage.count());
+        }
     }
 }