Pārlūkot izejas kodu

[Query Rules] Extend match types (#97730)

Kathleen DeRusso 2 gadi atpakaļ
vecāks
revīzija
8537146813
22 mainītis faili ar 589 papildinājumiem un 141 dzēšanām
  1. 1 1
      docs/reference/query-rules/apis/delete-query-ruleset.asciidoc
  2. 4 4
      docs/reference/query-rules/apis/get-query-ruleset.asciidoc
  3. 2 2
      docs/reference/query-rules/apis/put-query-ruleset.asciidoc
  4. 2 1
      server/src/main/java/org/elasticsearch/TransportVersion.java
  5. 7 7
      x-pack/plugin/ent-search/qa/rest/src/yamlRestTest/resources/rest-api-spec/test/entsearch/200_query_ruleset_put.yml
  6. 9 9
      x-pack/plugin/ent-search/qa/rest/src/yamlRestTest/resources/rest-api-spec/test/entsearch/225_query_ruleset_list.yml
  7. 1 1
      x-pack/plugin/ent-search/qa/rest/src/yamlRestTest/resources/rest-api-spec/test/entsearch/250_query_ruleset_delete.yml
  8. 61 4
      x-pack/plugin/ent-search/qa/rest/src/yamlRestTest/resources/rest-api-spec/test/entsearch/260_rule_query_search.yml
  9. 1 0
      x-pack/plugin/ent-search/src/main/java/module-info.java
  10. 31 16
      x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/rules/QueryRule.java
  11. 80 60
      x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/rules/QueryRuleCriteria.java
  12. 133 0
      x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/rules/QueryRuleCriteriaType.java
  13. 10 9
      x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/rules/QueryRulesIndexService.java
  14. 5 0
      x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/rules/action/PutQueryRulesetAction.java
  15. 136 2
      x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/rules/QueryRuleCriteriaTests.java
  16. 42 5
      x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/rules/QueryRuleTests.java
  17. 8 8
      x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/rules/QueryRulesIndexServiceTests.java
  18. 4 4
      x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/rules/QueryRulesetTests.java
  19. 2 1
      x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/rules/RuleQueryBuilderTests.java
  20. 20 1
      x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/rules/action/GetQueryRulesetActionResponseBWCSerializingTests.java
  21. 23 1
      x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/rules/action/PutQueryRulesetActionRequestBWCSerializingTests.java
  22. 7 5
      x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/search/SearchApplicationTestUtils.java

+ 1 - 1
docs/reference/query-rules/apis/delete-query-ruleset.asciidoc

@@ -54,7 +54,7 @@ PUT _query_rules/my-ruleset
                 {
                     "type": "exact",
                     "metadata": "query_string",
-                    "value": "marvel"
+                    "values": ["marvel"]
                 }
             ],
             "actions": {

+ 4 - 4
docs/reference/query-rules/apis/get-query-ruleset.asciidoc

@@ -54,7 +54,7 @@ PUT _query_rules/my-ruleset
                 {
                     "type": "exact",
                     "metadata": "query_string",
-                    "value": "marvel"
+                    "values": ["marvel"]
                 }
             ],
             "actions": {
@@ -71,7 +71,7 @@ PUT _query_rules/my-ruleset
                 {
                     "type": "exact",
                     "metadata": "query_string",
-                    "value": "dc"
+                    "values": ["dc"]
                 }
             ],
             "actions": {
@@ -119,7 +119,7 @@ A sample response:
                 {
                     "type": "exact",
                     "metadata": "query_string",
-                    "value": "marvel"
+                    "values": ["marvel"]
                 }
             ],
             "actions": {
@@ -136,7 +136,7 @@ A sample response:
                 {
                     "type": "exact",
                     "metadata": "query_string",
-                    "value": "dc"
+                    "values": ["dc"]
                 }
             ],
             "actions": {

+ 2 - 2
docs/reference/query-rules/apis/put-query-ruleset.asciidoc

@@ -92,7 +92,7 @@ PUT _query_rules/my-ruleset
                 {
                     "type": "exact",
                     "metadata": "query_string",
-                    "value": "marvel"
+                    "values": ["marvel"]
                 }
             ],
             "actions": {
@@ -109,7 +109,7 @@ PUT _query_rules/my-ruleset
                 {
                     "type": "exact",
                     "metadata": "query_string",
-                    "value": "dc"
+                    "values": ["dc"]
                 }
             ],
             "actions": {

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

@@ -168,9 +168,10 @@ public record TransportVersion(int id) implements VersionId<TransportVersion> {
     public static final TransportVersion V_8_500_043 = registerTransportVersion(8_500_043, "50baabd14-7f5c-4f8c-9351-94e0d397aabc");
     public static final TransportVersion V_8_500_044 = registerTransportVersion(8_500_044, "96b83320-2317-4e9d-b735-356f18c1d76a");
     public static final TransportVersion V_8_500_045 = registerTransportVersion(8_500_045, "24a596dd-c843-4c0a-90b3-759697d74026");
+    public static final TransportVersion V_8_500_046 = registerTransportVersion(8_500_046, "61666d4c-a4f0-40db-8a3d-4806718247c5");
 
     private static class CurrentHolder {
-        private static final TransportVersion CURRENT = findCurrent(V_8_500_045);
+        private static final TransportVersion CURRENT = findCurrent(V_8_500_046);
 
         // finds the pluggable current version, or uses the given fallback
         private static TransportVersion findCurrent(TransportVersion fallback) {

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

@@ -16,7 +16,7 @@ setup:
               criteria:
                 - type: exact
                   metadata: query_string
-                  value: elastic
+                  values: [elastic]
               actions:
                 ids:
                   - 'id1'
@@ -26,7 +26,7 @@ setup:
               criteria:
                 - type: exact
                   metadata: query_string
-                  value: kibana
+                  values: [kibana]
               actions:
                 docs:
                   - '_index': 'test-index1'
@@ -47,7 +47,7 @@ setup:
           criteria:
             - type: exact
               metadata: query_string
-              value: elastic
+              values: [elastic]
           actions:
             ids:
               - 'id1'
@@ -57,7 +57,7 @@ setup:
           criteria:
             - type: exact
               metadata: query_string
-              value: kibana
+              values: [kibana]
           actions:
             docs:
               - '_index': 'test-index1'
@@ -77,7 +77,7 @@ setup:
             criteria:
               type: 'exact'
               metadata: 'query_string'
-              value: 'elastic'
+              values: ['elastic']
             actions:
               ids:
                 - 'id1'
@@ -94,7 +94,7 @@ setup:
             criteria:
               type: 'exact'
               metadata: 'query_string'
-              value: 'elastic'
+              values: ['elastic']
             actions:
               ids:
                 - 'id2'
@@ -118,7 +118,7 @@ setup:
             criteria:
               type: 'exact'
               metadata: 'query_string'
-              value: 'elastic'
+              values: ['elastic']
             actions:
               ids:
                 - 'id1'

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

@@ -12,7 +12,7 @@ setup:
               criteria:
                 - type: exact
                   metadata: query_string
-                  value: elastic
+                  values: [elastic]
               actions:
                 ids:
                   - 'id1'
@@ -22,7 +22,7 @@ setup:
               criteria:
                 - type: exact
                   metadata: query_string
-                  value: kibana
+                  values: [kibana]
               actions:
                 ids:
                   - 'id3'
@@ -38,7 +38,7 @@ setup:
               criteria:
                 - type: exact
                   metadata: query_string
-                  value: elastic
+                  values: [elastic]
               actions:
                 ids:
                   - 'id1'
@@ -48,7 +48,7 @@ setup:
               criteria:
                 - type: exact
                   metadata: query_string
-                  value: kibana
+                  values: [kibana]
               actions:
                 ids:
                   - 'id3'
@@ -58,7 +58,7 @@ setup:
               criteria:
                 - type: exact
                   metadata: query_string
-                  value: logstash
+                  values: [logstash]
               actions:
                 ids:
                   - 'id5'
@@ -74,7 +74,7 @@ setup:
               criteria:
                 - type: exact
                   metadata: query_string
-                  value: elastic
+                  values: [elastic]
               actions:
                 ids:
                   - 'id1'
@@ -84,7 +84,7 @@ setup:
               criteria:
                 - type: exact
                   metadata: query_string
-                  value: kibana
+                  values: [kibana]
               actions:
                 ids:
                   - 'id3'
@@ -94,7 +94,7 @@ setup:
               criteria:
                 - type: exact
                   metadata: query_string
-                  value: logstash
+                  values: [logstash]
               actions:
                 ids:
                   - 'id5'
@@ -104,7 +104,7 @@ setup:
               criteria:
                 - type: exact
                   metadata: query_string
-                  value: beats
+                  values: [beats]
               actions:
                 ids:
                   - 'id7'

+ 1 - 1
x-pack/plugin/ent-search/qa/rest/src/yamlRestTest/resources/rest-api-spec/test/entsearch/250_query_ruleset_delete.yml

@@ -12,7 +12,7 @@ setup:
               criteria:
                 - type: exact
                   metadata: query_string
-                  value: elastic
+                  values: [elastic]
               actions:
                 ids:
                   - 'id1'

+ 61 - 4
x-pack/plugin/ent-search/qa/rest/src/yamlRestTest/resources/rest-api-spec/test/entsearch/260_rule_query_search.yml

@@ -29,6 +29,9 @@ setup:
           - index:
               _id: doc4
           - { "text": "you know, for search" }
+          - index:
+              _id: doc5
+          - { "text": "beats" }
 
   - do:
       query_ruleset.put:
@@ -40,7 +43,7 @@ setup:
               criteria:
                 - type: exact
                   metadata: query_string
-                  value: search
+                  values: [search]
               actions:
                 ids:
                   - 'doc1'
@@ -49,11 +52,22 @@ setup:
               criteria:
                 - type: exact
                   metadata: query_string
-                  value: ui
+                  values: [ui]
               actions:
                 docs:
                   - '_index': 'test-index1'
                     '_id': 'doc2'
+            - rule_id: rule3
+              type: pinned
+              criteria:
+                - type: contains
+                  metadata: query_string
+                  values: [ kibana, logstash ]
+              actions:
+                ids:
+                  - 'doc2'
+                  - 'doc3'
+
 
 ---
 "Perform a rule query with an ID match":
@@ -123,6 +137,25 @@ setup:
 ---
 "Perform a rule query with no matching rules":
 
+  - do:
+      search:
+        body:
+          query:
+            rule_query:
+              organic:
+                query_string:
+                  default_field: text
+                  query: beats
+              match_criteria:
+                query_string: beats
+              ruleset_id: test-ruleset
+
+  - match: { hits.total.value: 1 }
+  - match: { hits.hits.0._id: 'doc5' }
+
+---
+"Perform a rule query with multiple matching rules":
+
   - do:
       search:
         body:
@@ -136,5 +169,29 @@ setup:
                 query_string: logstash
               ruleset_id: test-ruleset
 
-  - match: { hits.total.value: 1 }
-  - match: { hits.hits.0._id: 'doc3' }
+  - match: { hits.total.value: 2 }
+  - match: { hits.hits.0._id: 'doc2' }
+  - match: { hits.hits.1._id: 'doc3'}
+
+
+---
+"Perform a rule query that matches complex rules":
+
+  - do:
+      search:
+        body:
+          query:
+            rule_query:
+              organic:
+                query_string:
+                  default_field: text
+                  query: elastic and kibana are good for search
+              match_criteria:
+                query_string: elastic and kibana are good for search
+              ruleset_id: test-ruleset
+
+  - match: { hits.total.value: 4 }
+  - match: { hits.hits.0._id: 'doc2' }
+  - match: { hits.hits.1._id: 'doc3' }
+
+

+ 1 - 0
x-pack/plugin/ent-search/src/main/java/module-info.java

@@ -20,6 +20,7 @@ module org.elasticsearch.application {
     requires org.elasticsearch.xcontent;
     requires org.elasticsearch.xcore;
     requires org.elasticsearch.searchbusinessrules;
+    requires org.apache.lucene.suggest;
 
     exports org.elasticsearch.xpack.application;
     exports org.elasticsearch.xpack.application.analytics;

+ 31 - 16
x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/rules/QueryRule.java

@@ -14,6 +14,8 @@ import org.elasticsearch.common.io.stream.StreamInput;
 import org.elasticsearch.common.io.stream.StreamOutput;
 import org.elasticsearch.common.io.stream.Writeable;
 import org.elasticsearch.common.xcontent.XContentHelper;
+import org.elasticsearch.logging.LogManager;
+import org.elasticsearch.logging.Logger;
 import org.elasticsearch.xcontent.ConstructingObjectParser;
 import org.elasticsearch.xcontent.ParseField;
 import org.elasticsearch.xcontent.ToXContentObject;
@@ -32,6 +34,7 @@ import java.util.Objects;
 
 import static org.elasticsearch.xcontent.ConstructingObjectParser.constructorArg;
 import static org.elasticsearch.xcontent.ConstructingObjectParser.optionalConstructorArg;
+import static org.elasticsearch.xpack.application.rules.QueryRuleCriteriaType.ALWAYS;
 import static org.elasticsearch.xpack.searchbusinessrules.PinnedQueryBuilder.DOCS_FIELD;
 import static org.elasticsearch.xpack.searchbusinessrules.PinnedQueryBuilder.IDS_FIELD;
 import static org.elasticsearch.xpack.searchbusinessrules.PinnedQueryBuilder.Item.INDEX_FIELD;
@@ -53,6 +56,8 @@ public class QueryRule implements Writeable, ToXContentObject {
     private final List<QueryRuleCriteria> criteria;
     private final Map<String, Object> actions;
 
+    private final Logger logger = LogManager.getLogger(QueryRule.class);
+
     public enum QueryRuleType {
         PINNED;
 
@@ -283,29 +288,39 @@ public class QueryRule implements Writeable, ToXContentObject {
 
         List<String> matchingPinnedIds = new ArrayList<>();
         List<PinnedQueryBuilder.Item> matchingPinnedDocs = new ArrayList<>();
+        Boolean isRuleMatch = null;
 
+        // All specified criteria in a rule must match for the rule to be applied
         for (QueryRuleCriteria criterion : criteria) {
             for (String match : matchCriteria.keySet()) {
-                final String matchValue = matchCriteria.get(match).toString();
-                if (criterion.criteriaMetadata().equals(match) && criterion.isMatch(matchValue)) {
-                    if (actions.containsKey(IDS_FIELD.getPreferredName())) {
-                        matchingPinnedIds.addAll((List<String>) actions.get(IDS_FIELD.getPreferredName()));
-                    } else if (actions.containsKey(DOCS_FIELD.getPreferredName())) {
-                        List<Map<String, String>> docsToPin = (List<Map<String, String>>) actions.get(DOCS_FIELD.getPreferredName());
-                        List<PinnedQueryBuilder.Item> items = docsToPin.stream()
-                            .map(
-                                map -> new PinnedQueryBuilder.Item(
-                                    map.get(INDEX_FIELD.getPreferredName()),
-                                    map.get(PinnedQueryBuilder.Item.ID_FIELD.getPreferredName())
-                                )
-                            )
-                            .toList();
-                        matchingPinnedDocs.addAll(items);
-                    }
+                final Object matchValue = matchCriteria.get(match);
+                final QueryRuleCriteriaType criteriaType = criterion.criteriaType();
+                final String criteriaMetadata = criterion.criteriaMetadata();
+
+                if (criteriaType == ALWAYS || (criteriaMetadata != null && criteriaMetadata.equals(match))) {
+                    boolean singleCriterionMatches = criterion.isMatch(matchValue, criteriaType);
+                    isRuleMatch = (isRuleMatch == null) ? singleCriterionMatches : isRuleMatch && singleCriterionMatches;
                 }
             }
         }
 
+        if (isRuleMatch != null && isRuleMatch) {
+            if (actions.containsKey(IDS_FIELD.getPreferredName())) {
+                matchingPinnedIds.addAll((List<String>) actions.get(IDS_FIELD.getPreferredName()));
+            } else if (actions.containsKey(DOCS_FIELD.getPreferredName())) {
+                List<Map<String, String>> docsToPin = (List<Map<String, String>>) actions.get(DOCS_FIELD.getPreferredName());
+                List<PinnedQueryBuilder.Item> items = docsToPin.stream()
+                    .map(
+                        map -> new PinnedQueryBuilder.Item(
+                            map.get(INDEX_FIELD.getPreferredName()),
+                            map.get(PinnedQueryBuilder.Item.ID_FIELD.getPreferredName())
+                        )
+                    )
+                    .toList();
+                matchingPinnedDocs.addAll(items);
+            }
+        }
+
         List<String> pinnedIds = appliedRules.pinnedIds();
         List<PinnedQueryBuilder.Item> pinnedDocs = appliedRules.pinnedDocs();
         pinnedIds.addAll(matchingPinnedIds);

+ 80 - 60
x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/rules/QueryRuleCriteria.java

@@ -8,12 +8,16 @@
 package org.elasticsearch.xpack.application.rules;
 
 import org.elasticsearch.ElasticsearchParseException;
+import org.elasticsearch.TransportVersion;
 import org.elasticsearch.common.Strings;
 import org.elasticsearch.common.bytes.BytesReference;
 import org.elasticsearch.common.io.stream.StreamInput;
 import org.elasticsearch.common.io.stream.StreamOutput;
 import org.elasticsearch.common.io.stream.Writeable;
 import org.elasticsearch.common.xcontent.XContentHelper;
+import org.elasticsearch.core.Nullable;
+import org.elasticsearch.logging.LogManager;
+import org.elasticsearch.logging.Logger;
 import org.elasticsearch.xcontent.ConstructingObjectParser;
 import org.elasticsearch.xcontent.ParseField;
 import org.elasticsearch.xcontent.ToXContentObject;
@@ -23,84 +27,92 @@ import org.elasticsearch.xcontent.XContentParserConfiguration;
 import org.elasticsearch.xcontent.XContentType;
 
 import java.io.IOException;
-import java.util.Locale;
+import java.util.List;
 import java.util.Objects;
 
 import static org.elasticsearch.xcontent.ConstructingObjectParser.constructorArg;
+import static org.elasticsearch.xcontent.ConstructingObjectParser.optionalConstructorArg;
+import static org.elasticsearch.xpack.application.rules.QueryRuleCriteriaType.ALWAYS;
 
 public class QueryRuleCriteria implements Writeable, ToXContentObject {
-    private final CriteriaType criteriaType;
-    private final String criteriaMetadata;
-    private final Object criteriaValue;
-
-    public enum CriteriaType {
-        EXACT;
 
-        public static CriteriaType criteriaType(String criteriaType) {
-            for (CriteriaType type : values()) {
-                if (type.name().equalsIgnoreCase(criteriaType)) {
-                    return type;
-                }
-            }
-            throw new IllegalArgumentException("Unknown CriteriaType: " + criteriaType);
-        }
+    public static final TransportVersion CRITERIA_METADATA_VALUES_TRANSPORT_VERSION = TransportVersion.V_8_500_046;
+    private final QueryRuleCriteriaType criteriaType;
+    private final String criteriaMetadata;
+    private final List<Object> criteriaValues;
 
-        @Override
-        public String toString() {
-            return name().toLowerCase(Locale.ROOT);
-        }
-    }
+    private static final Logger logger = LogManager.getLogger(QueryRuleCriteria.class);
 
     /**
      *
-     * @param criteriaType The {@link CriteriaType}, indicating how the criteria is matched
-     * @param criteriaMetadata The metadata for this identifier, indicating the criteria key of what is matched against
-     * @param criteriaValue The value to match against when evaluating {@link QueryRuleCriteria} against a {@link QueryRule}
+     * @param criteriaType The {@link QueryRuleCriteriaType}, indicating how the criteria is matched
+     * @param criteriaMetadata The metadata for this identifier, indicating the criteria key of what is matched against.
+     *                         Required unless the CriteriaType is ALWAYS.
+     * @param criteriaValues The values to match against when evaluating {@link QueryRuleCriteria} against a {@link QueryRule}
+     *                      Required unless the CriteriaType is ALWAYS.
      */
-    public QueryRuleCriteria(CriteriaType criteriaType, String criteriaMetadata, Object criteriaValue) {
+    public QueryRuleCriteria(QueryRuleCriteriaType criteriaType, @Nullable String criteriaMetadata, @Nullable List<Object> criteriaValues) {
 
         Objects.requireNonNull(criteriaType);
-        Objects.requireNonNull(criteriaMetadata);
-        Objects.requireNonNull(criteriaValue);
-
-        if ((criteriaType == CriteriaType.EXACT) == false) {
-            throw new IllegalArgumentException("Invalid criteriaType " + criteriaType);
-        }
 
-        if (Strings.isNullOrEmpty(criteriaMetadata)) {
-            throw new IllegalArgumentException("criteriaMetadata cannot be blank");
+        if (criteriaType != ALWAYS) {
+            if (Strings.isNullOrEmpty(criteriaMetadata)) {
+                throw new IllegalArgumentException("criteriaMetadata cannot be blank");
+            }
+            if (criteriaValues == null || criteriaValues.isEmpty()) {
+                throw new IllegalArgumentException("criteriaValues cannot be null or empty");
+            }
         }
 
-        this.criteriaType = criteriaType;
         this.criteriaMetadata = criteriaMetadata;
-        this.criteriaValue = criteriaValue;
+        this.criteriaValues = criteriaValues;
+        this.criteriaType = criteriaType;
+
     }
 
     public QueryRuleCriteria(StreamInput in) throws IOException {
-        this.criteriaType = in.readEnum(CriteriaType.class);
-        this.criteriaMetadata = in.readString();
-        this.criteriaValue = in.readGenericValue();
+        this.criteriaType = in.readEnum(QueryRuleCriteriaType.class);
+        if (in.getTransportVersion().onOrAfter(CRITERIA_METADATA_VALUES_TRANSPORT_VERSION)) {
+            this.criteriaMetadata = in.readOptionalString();
+            this.criteriaValues = in.readOptionalList(StreamInput::readGenericValue);
+        } else {
+            this.criteriaMetadata = in.readString();
+            this.criteriaValues = List.of(in.readGenericValue());
+        }
+    }
+
+    @Override
+    public void writeTo(StreamOutput out) throws IOException {
+        out.writeEnum(criteriaType);
+        if (out.getTransportVersion().onOrAfter(CRITERIA_METADATA_VALUES_TRANSPORT_VERSION)) {
+            out.writeOptionalString(criteriaMetadata);
+            out.writeOptionalCollection(criteriaValues, StreamOutput::writeGenericValue);
+        } else {
+            out.writeString(criteriaMetadata);
+            out.writeGenericValue(criteriaValues().get(0));
+        }
     }
 
     private static final ConstructingObjectParser<QueryRuleCriteria, String> PARSER = new ConstructingObjectParser<>(
         "query_rule_criteria",
         false,
         (params, resourceName) -> {
-            final CriteriaType type = CriteriaType.criteriaType((String) params[0]);
-            final String metadata = (String) params[1];
-            final Object value = params[2];
-            return new QueryRuleCriteria(type, metadata, value);
+            final QueryRuleCriteriaType type = QueryRuleCriteriaType.type((String) params[0]);
+            final String metadata = params.length >= 3 ? (String) params[1] : null;
+            @SuppressWarnings("unchecked")
+            final List<Object> values = params.length >= 3 ? (List<Object>) params[2] : null;
+            return new QueryRuleCriteria(type, metadata, values);
         }
     );
 
     public static final ParseField TYPE_FIELD = new ParseField("type");
     public static final ParseField METADATA_FIELD = new ParseField("metadata");
-    public static final ParseField VALUE_FIELD = new ParseField("value");
+    public static final ParseField VALUES_FIELD = new ParseField("values");
 
     static {
         PARSER.declareString(constructorArg(), TYPE_FIELD);
-        PARSER.declareString(constructorArg(), METADATA_FIELD);
-        PARSER.declareString(constructorArg(), VALUE_FIELD);
+        PARSER.declareStringOrNull(optionalConstructorArg(), METADATA_FIELD);
+        PARSER.declareStringArray(optionalConstructorArg(), VALUES_FIELD);
     }
 
     /**
@@ -134,21 +146,18 @@ public class QueryRuleCriteria implements Writeable, ToXContentObject {
         builder.startObject();
         {
             builder.field(TYPE_FIELD.getPreferredName(), criteriaType);
-            builder.field(METADATA_FIELD.getPreferredName(), criteriaMetadata);
-            builder.field(VALUE_FIELD.getPreferredName(), criteriaValue);
+            if (criteriaMetadata != null) {
+                builder.field(METADATA_FIELD.getPreferredName(), criteriaMetadata);
+            }
+            if (criteriaValues != null) {
+                builder.array(VALUES_FIELD.getPreferredName(), criteriaValues.toArray());
+            }
         }
         builder.endObject();
         return builder;
     }
 
-    @Override
-    public void writeTo(StreamOutput out) throws IOException {
-        out.writeEnum(criteriaType);
-        out.writeString(criteriaMetadata);
-        out.writeGenericValue(criteriaValue);
-    }
-
-    public CriteriaType criteriaType() {
+    public QueryRuleCriteriaType criteriaType() {
         return criteriaType;
     }
 
@@ -156,8 +165,8 @@ public class QueryRuleCriteria implements Writeable, ToXContentObject {
         return criteriaMetadata;
     }
 
-    public Object criteriaValue() {
-        return criteriaValue;
+    public List<Object> criteriaValues() {
+        return criteriaValues;
     }
 
     @Override
@@ -167,12 +176,12 @@ public class QueryRuleCriteria implements Writeable, ToXContentObject {
         QueryRuleCriteria that = (QueryRuleCriteria) o;
         return criteriaType == that.criteriaType
             && Objects.equals(criteriaMetadata, that.criteriaMetadata)
-            && Objects.equals(criteriaValue, that.criteriaValue);
+            && Objects.equals(criteriaValues, that.criteriaValues);
     }
 
     @Override
     public int hashCode() {
-        return Objects.hash(criteriaType, criteriaMetadata, criteriaValue);
+        return Objects.hash(criteriaType, criteriaMetadata, criteriaValues);
     }
 
     @Override
@@ -180,7 +189,18 @@ public class QueryRuleCriteria implements Writeable, ToXContentObject {
         return Strings.toString(this);
     }
 
-    public boolean isMatch(String matchString) {
-        return criteriaType == CriteriaType.EXACT && criteriaValue.equals(matchString);
+    public boolean isMatch(Object matchValue, QueryRuleCriteriaType matchType) {
+        if (matchType == ALWAYS) {
+            return true;
+        }
+        final String matchString = matchValue.toString();
+        for (Object criteriaValue : criteriaValues) {
+            matchType.validateInput(matchValue);
+            boolean matchFound = matchType.isMatch(matchString, criteriaValue);
+            if (matchFound) {
+                return true;
+            }
+        }
+        return false;
     }
 }

+ 133 - 0
x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/rules/QueryRuleCriteriaType.java

@@ -0,0 +1,133 @@
+/*
+ * 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.application.rules;
+
+import org.apache.lucene.search.spell.LevenshteinDistance;
+
+import java.util.List;
+import java.util.Locale;
+
+/**
+ * Defines the different types of query rule criteria and their rules for matching input against the criteria.
+ */
+public enum QueryRuleCriteriaType {
+    ALWAYS {
+        @Override
+        public boolean isMatch(Object input, Object criteriaValue) {
+            return true;
+        }
+    },
+    EXACT {
+        @Override
+        public boolean isMatch(Object input, Object criteriaValue) {
+            if (input instanceof String && criteriaValue instanceof String) {
+                return input.equals(criteriaValue);
+            } else {
+                return parseDouble(input) == parseDouble(criteriaValue);
+            }
+        }
+    },
+    FUZZY {
+        @Override
+        public boolean isMatch(Object input, Object criteriaValue) {
+            final LevenshteinDistance ld = new LevenshteinDistance();
+            if (input instanceof String && criteriaValue instanceof String) {
+                return ld.getDistance((String) input, (String) criteriaValue) > 0.5f;
+            }
+            return false;
+        }
+    },
+    PREFIX {
+        @Override
+        public boolean isMatch(Object input, Object criteriaValue) {
+            return ((String) input).startsWith((String) criteriaValue);
+        }
+    },
+    SUFFIX {
+        @Override
+        public boolean isMatch(Object input, Object criteriaValue) {
+            return ((String) input).endsWith((String) criteriaValue);
+        }
+    },
+    CONTAINS {
+        @Override
+        public boolean isMatch(Object input, Object criteriaValue) {
+            return ((String) input).contains((String) criteriaValue);
+        }
+    },
+    LT {
+        @Override
+        public boolean isMatch(Object input, Object criteriaValue) {
+            return parseDouble(input) < parseDouble(criteriaValue);
+        }
+    },
+    LTE {
+        @Override
+        public boolean isMatch(Object input, Object criteriaValue) {
+            return parseDouble(input) <= parseDouble(criteriaValue);
+        }
+    },
+    GT {
+        @Override
+        public boolean isMatch(Object input, Object criteriaValue) {
+            return parseDouble(input) > parseDouble(criteriaValue);
+        }
+    },
+    GTE {
+        @Override
+        public boolean isMatch(Object input, Object criteriaValue) {
+            validateInput(input);
+            return parseDouble(input) >= parseDouble(criteriaValue);
+        }
+    };
+
+    public void validateInput(Object input) {
+        boolean isValid = isValidForInput(input);
+        if (isValid == false) {
+            throw new IllegalArgumentException("Input [" + input + "] is not valid for CriteriaType [" + this + "]");
+        }
+    }
+
+    public abstract boolean isMatch(Object input, Object criteriaValue);
+
+    public static QueryRuleCriteriaType type(String criteriaType) {
+        for (QueryRuleCriteriaType type : values()) {
+            if (type.name().equalsIgnoreCase(criteriaType)) {
+                return type;
+            }
+        }
+        throw new IllegalArgumentException("Unknown QueryRuleCriteriaType: " + criteriaType);
+    }
+
+    @Override
+    public String toString() {
+        return name().toLowerCase(Locale.ROOT);
+    }
+
+    private boolean isValidForInput(Object input) {
+        if (this == EXACT) {
+            return input instanceof String || input instanceof Number;
+        } else if (List.of(FUZZY, PREFIX, SUFFIX, CONTAINS).contains(this)) {
+            return input instanceof String;
+        } else if (List.of(LT, LTE, GT, GTE).contains(this)) {
+            try {
+                if (input instanceof Number == false) {
+                    parseDouble(input.toString());
+                }
+                return true;
+            } catch (NumberFormatException e) {
+                return false;
+            }
+        }
+        return false;
+    }
+
+    private static double parseDouble(Object input) {
+        return (input instanceof Number) ? ((Number) input).doubleValue() : Double.parseDouble(input.toString());
+    }
+}

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

@@ -137,7 +137,7 @@ public class QueryRulesIndexService {
                             builder.field("type", "keyword");
                             builder.endObject();
 
-                            builder.startObject(QueryRuleCriteria.VALUE_FIELD.getPreferredName());
+                            builder.startObject(QueryRuleCriteria.VALUES_FIELD.getPreferredName());
                             builder.field("type", "object");
                             builder.field("enabled", false);
                             builder.endObject();
@@ -180,13 +180,13 @@ public class QueryRulesIndexService {
             }
             final Map<String, Object> source = getResponse.getSource();
             @SuppressWarnings("unchecked")
-            final List<QueryRule> rules = ((List<Map<String, Object>>) source.get("rules")).stream()
+            final List<QueryRule> rules = ((List<Map<String, Object>>) source.get(QueryRuleset.RULES_FIELD.getPreferredName())).stream()
                 .map(
                     rule -> new QueryRule(
-                        (String) rule.get("rule_id"),
-                        QueryRuleType.queryRuleType((String) rule.get("type")),
-                        parseCriteria((List<Map<String, Object>>) rule.get("criteria")),
-                        (Map<String, Object>) rule.get("actions")
+                        (String) rule.get(QueryRule.ID_FIELD.getPreferredName()),
+                        QueryRuleType.queryRuleType((String) rule.get(QueryRule.TYPE_FIELD.getPreferredName())),
+                        parseCriteria((List<Map<String, Object>>) rule.get(QueryRule.CRITERIA_FIELD.getPreferredName())),
+                        (Map<String, Object>) rule.get(QueryRule.ACTIONS_FIELD.getPreferredName())
                     )
                 )
                 .collect(Collectors.toList());
@@ -195,14 +195,15 @@ public class QueryRulesIndexService {
         }));
     }
 
+    @SuppressWarnings("unchecked")
     private List<QueryRuleCriteria> parseCriteria(List<Map<String, Object>> rawCriteria) {
         List<QueryRuleCriteria> criteria = new ArrayList<>(rawCriteria.size());
         for (Map<String, Object> entry : rawCriteria) {
             criteria.add(
                 new QueryRuleCriteria(
-                    QueryRuleCriteria.CriteriaType.criteriaType((String) entry.get("type")),
-                    (String) entry.get("metadata"),
-                    entry.get("value")
+                    QueryRuleCriteriaType.type((String) entry.get(QueryRuleCriteria.TYPE_FIELD.getPreferredName())),
+                    (String) entry.get(QueryRuleCriteria.METADATA_FIELD.getPreferredName()),
+                    (List<Object>) entry.get(QueryRuleCriteria.VALUES_FIELD.getPreferredName())
                 )
             );
         }

+ 5 - 0
x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/rules/action/PutQueryRulesetAction.java

@@ -118,6 +118,11 @@ public class PutQueryRulesetAction extends ActionType<PutQueryRulesetAction.Resp
             return new PutQueryRulesetAction.Request(QueryRuleset.fromXContent(id, parser));
         }
 
+        @Override
+        public String toString() {
+            return Strings.toString(this);
+        }
+
     }
 
     public static class Response extends ActionResponse implements StatusToXContentObject {

+ 136 - 2
x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/rules/QueryRuleCriteriaTests.java

@@ -26,6 +26,16 @@ import java.util.List;
 import static java.util.Collections.emptyList;
 import static org.elasticsearch.common.xcontent.XContentHelper.toXContent;
 import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertToXContentEquivalent;
+import static org.elasticsearch.xpack.application.rules.QueryRuleCriteriaType.ALWAYS;
+import static org.elasticsearch.xpack.application.rules.QueryRuleCriteriaType.CONTAINS;
+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.GT;
+import static org.elasticsearch.xpack.application.rules.QueryRuleCriteriaType.GTE;
+import static org.elasticsearch.xpack.application.rules.QueryRuleCriteriaType.LT;
+import static org.elasticsearch.xpack.application.rules.QueryRuleCriteriaType.LTE;
+import static org.elasticsearch.xpack.application.rules.QueryRuleCriteriaType.PREFIX;
+import static org.elasticsearch.xpack.application.rules.QueryRuleCriteriaType.SUFFIX;
 import static org.hamcrest.CoreMatchers.equalTo;
 
 public class QueryRuleCriteriaTests extends ESTestCase {
@@ -47,12 +57,20 @@ public class QueryRuleCriteriaTests extends ESTestCase {
         }
     }
 
+    public final void testAlwaysSerialization() throws IOException {
+        for (int runs = 0; runs < 10; runs++) {
+            QueryRuleCriteria testInstance = new QueryRuleCriteria(ALWAYS, null, null);
+            assertTransportSerialization(testInstance);
+            assertXContent(testInstance, randomBoolean());
+        }
+    }
+
     public void testToXContent() throws IOException {
         String content = XContentHelper.stripWhitespace("""
             {
               "type": "exact",
-              "metadata": "query_string",
-              "value": "foo"
+              "metadata": "my-key",
+              "values": ["foo","bar"]
             }""");
 
         QueryRuleCriteria queryRuleCriteria = QueryRuleCriteria.fromXContentBytes(new BytesArray(content), XContentType.JSON);
@@ -65,6 +83,122 @@ public class QueryRuleCriteriaTests extends ESTestCase {
         assertToXContentEquivalent(originalBytes, toXContent(parsed, XContentType.JSON, humanReadable), XContentType.JSON);
     }
 
+    public void testAlwaysToXContent() throws IOException {
+        String content = XContentHelper.stripWhitespace("""
+            {
+              "type": "always"
+            }""");
+
+        QueryRuleCriteria queryRuleCriteria = QueryRuleCriteria.fromXContentBytes(new BytesArray(content), XContentType.JSON);
+        boolean humanReadable = true;
+        BytesReference originalBytes = toShuffledXContent(queryRuleCriteria, XContentType.JSON, ToXContent.EMPTY_PARAMS, humanReadable);
+        QueryRuleCriteria parsed;
+        try (XContentParser parser = createParser(XContentType.JSON.xContent(), originalBytes)) {
+            parsed = QueryRuleCriteria.fromXContent(parser);
+        }
+        assertToXContentEquivalent(originalBytes, toXContent(parsed, XContentType.JSON, humanReadable), XContentType.JSON);
+    }
+
+    public void testExactMatch() {
+        QueryRuleCriteriaType type = EXACT;
+        QueryRuleCriteria queryRuleCriteria = new QueryRuleCriteria(type, "query", List.of("elastic"));
+        assertTrue(queryRuleCriteria.isMatch("elastic", type));
+        assertFalse(queryRuleCriteria.isMatch("elasticc", type));
+
+        queryRuleCriteria = new QueryRuleCriteria(type, "zip_code", List.of("12345"));
+        assertTrue(queryRuleCriteria.isMatch(12345, type));
+        assertTrue(queryRuleCriteria.isMatch("12345", type));
+        assertFalse(queryRuleCriteria.isMatch("123456", type));
+    }
+
+    public void testFuzzyExactMatch() {
+        QueryRuleCriteriaType type = FUZZY;
+        QueryRuleCriteria queryRuleCriteria = new QueryRuleCriteria(type, "query", List.of("elastic"));
+        assertTrue(queryRuleCriteria.isMatch("elastic", type));
+        assertTrue(queryRuleCriteria.isMatch("elasticc", type));
+        assertFalse(queryRuleCriteria.isMatch("elastic elastic elastic elastic", type));
+    }
+
+    public void testPrefixMatch() {
+        QueryRuleCriteriaType type = PREFIX;
+        QueryRuleCriteria queryRuleCriteria = new QueryRuleCriteria(type, "query", List.of("elastic", "kibana"));
+        assertTrue(queryRuleCriteria.isMatch("elastic", type));
+        assertTrue(queryRuleCriteria.isMatch("kibana", type));
+        assertTrue(queryRuleCriteria.isMatch("elastic is a great search engine", type));
+        assertTrue(queryRuleCriteria.isMatch("kibana is a great visualization tool", type));
+        assertFalse(queryRuleCriteria.isMatch("you know, for search - elastic, kibana", type));
+    }
+
+    public void testSuffixMatch() {
+        QueryRuleCriteriaType type = SUFFIX;
+        QueryRuleCriteria queryRuleCriteria = new QueryRuleCriteria(type, "query", List.of("search", "lucene"));
+        assertTrue(queryRuleCriteria.isMatch("search", type));
+        assertTrue(queryRuleCriteria.isMatch("lucene", type));
+        assertTrue(queryRuleCriteria.isMatch("you know, for search", type));
+        assertTrue(queryRuleCriteria.isMatch("elasticsearch is built on top of lucene", type));
+        assertFalse(queryRuleCriteria.isMatch("search is a good use case for elastic", type));
+        assertFalse(queryRuleCriteria.isMatch("lucene and elastic are open source", type));
+    }
+
+    public void testContainsMatch() {
+        QueryRuleCriteriaType type = CONTAINS;
+        QueryRuleCriteria queryRuleCriteria = new QueryRuleCriteria(type, "query", List.of("elastic"));
+        assertTrue(queryRuleCriteria.isMatch("elastic", type));
+        assertTrue(queryRuleCriteria.isMatch("I use elastic for search", type));
+        assertFalse(queryRuleCriteria.isMatch("you know, for search", type));
+    }
+
+    public void testLtMatch() {
+        QueryRuleCriteriaType type = LT;
+        QueryRuleCriteria queryRuleCriteria = new QueryRuleCriteria(type, "age", List.of("10"));
+        assertTrue(queryRuleCriteria.isMatch(5, type));
+        assertFalse(queryRuleCriteria.isMatch(10, type));
+        assertFalse(queryRuleCriteria.isMatch(20, type));
+    }
+
+    public void testLteMatch() {
+        QueryRuleCriteriaType type = LTE;
+        QueryRuleCriteria queryRuleCriteria = new QueryRuleCriteria(type, "age", List.of("10"));
+        assertTrue(queryRuleCriteria.isMatch(5, type));
+        assertTrue(queryRuleCriteria.isMatch(10, type));
+        assertFalse(queryRuleCriteria.isMatch(20, type));
+    }
+
+    public void testGtMatch() {
+        QueryRuleCriteriaType type = GT;
+        QueryRuleCriteria queryRuleCriteria = new QueryRuleCriteria(type, "age", List.of("10"));
+        assertTrue(queryRuleCriteria.isMatch(20, type));
+        assertFalse(queryRuleCriteria.isMatch(10, type));
+        assertFalse(queryRuleCriteria.isMatch(5, type));
+    }
+
+    public void testGteMatch() {
+        QueryRuleCriteriaType type = GTE;
+        QueryRuleCriteria queryRuleCriteria = new QueryRuleCriteria(type, "age", List.of("10"));
+        assertTrue(queryRuleCriteria.isMatch(20, type));
+        assertTrue(queryRuleCriteria.isMatch(10, type));
+        assertFalse(queryRuleCriteria.isMatch(5, type));
+    }
+
+    public void testAlwaysMatch() {
+        QueryRuleCriteriaType type = ALWAYS;
+        QueryRuleCriteria queryRuleCriteria = new QueryRuleCriteria(type, null, null);
+        assertTrue(queryRuleCriteria.isMatch("elastic", type));
+        assertTrue(queryRuleCriteria.isMatch(42, type));
+    }
+
+    public void testInvalidCriteriaInput() {
+        for (QueryRuleCriteriaType type : List.of(FUZZY, PREFIX, SUFFIX, CONTAINS)) {
+            QueryRuleCriteria queryRuleCriteria = new QueryRuleCriteria(type, "foo", List.of("bar"));
+            expectThrows(IllegalArgumentException.class, () -> queryRuleCriteria.isMatch(42, type));
+        }
+
+        for (QueryRuleCriteriaType type : List.of(LT, LTE, GT, GTE)) {
+            QueryRuleCriteria queryRuleCriteria = new QueryRuleCriteria(type, "foo", List.of(42));
+            expectThrows(IllegalArgumentException.class, () -> queryRuleCriteria.isMatch("puggles", type));
+        }
+    }
+
     private void assertXContent(QueryRuleCriteria queryRule, boolean humanReadable) throws IOException {
         BytesReference originalBytes = toShuffledXContent(queryRule, XContentType.JSON, ToXContent.EMPTY_PARAMS, humanReadable);
         QueryRuleCriteria parsed;

+ 42 - 5
x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/rules/QueryRuleTests.java

@@ -21,11 +21,16 @@ import org.elasticsearch.xpack.application.search.SearchApplicationTestUtils;
 import org.junit.Before;
 
 import java.io.IOException;
+import java.util.Collections;
 import java.util.List;
+import java.util.Map;
 
 import static java.util.Collections.emptyList;
 import static org.elasticsearch.common.xcontent.XContentHelper.toXContent;
 import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertToXContentEquivalent;
+import static org.elasticsearch.xpack.application.rules.QueryRuleCriteriaType.EXACT;
+import static org.elasticsearch.xpack.application.rules.QueryRuleCriteriaType.PREFIX;
+import static org.elasticsearch.xpack.application.rules.QueryRuleCriteriaType.SUFFIX;
 import static org.hamcrest.CoreMatchers.equalTo;
 
 public class QueryRuleTests extends ESTestCase {
@@ -53,7 +58,7 @@ public class QueryRuleTests extends ESTestCase {
               "rule_id": "my_query_rule",
               "type": "pinned",
               "criteria": [
-                { "type": "exact", "metadata": "query_string", "value": "foo" }
+                { "type": "exact", "metadata": "query_string", "values": ["foo", "bar"] }
               ],
               "actions": {
                 "ids": ["id1", "id2"]
@@ -75,7 +80,7 @@ public class QueryRuleTests extends ESTestCase {
             {
               "type": "pinned",
               "criteria": [
-                { "type": "exact", "metadata": "query_string", "value": "foo" }
+                { "type": "exact", "metadata": "query_string", "values": ["foo", "bar"] }
               ],
               "actions": {
                   "ids": ["id1", "id2"]
@@ -101,7 +106,7 @@ public class QueryRuleTests extends ESTestCase {
               "rule_id": "my_query_rule",
               "type": "pinned",
               "criteria": [
-                { "type": "exact", "metadata": "query_string", "value": "foo" }
+                { "type": "exact", "metadata": "query_string", "values": ["foo", "bar"] }
               ],
               "actions": {
                 "ids": ["id1", "id2"]
@@ -116,7 +121,7 @@ public class QueryRuleTests extends ESTestCase {
               "rule_id": "my_query_rule",
               "type": "pinned",
               "criteria": [
-                { "type": "exact", "metadata": "query_string", "value": "foo" }
+                { "type": "exact", "metadata": "query_string", "values": ["foo", "bar"] }
               ],
               "actions": {
                 "docs": [
@@ -151,7 +156,7 @@ public class QueryRuleTests extends ESTestCase {
               "rule_id": "my_query_rule",
               "type": "pinned",
               "criteria": [
-                { "type": "exact", "metadata": "query_string", "value": "foo" }
+                { "type": "exact", "metadata": "query_string", "values": ["foo", "bar"] }
               ],
               "actions": {
                   "foo": "bar"
@@ -160,6 +165,38 @@ public class QueryRuleTests extends ESTestCase {
         expectThrows(IllegalArgumentException.class, () -> QueryRule.fromXContentBytes(new BytesArray(content), XContentType.JSON));
     }
 
+    public void testApplyRuleWithOneCriteria() {
+        QueryRule rule = new QueryRule(
+            randomAlphaOfLength(10),
+            QueryRule.QueryRuleType.PINNED,
+            List.of(new QueryRuleCriteria(EXACT, "query", List.of("elastic"))),
+            Map.of("ids", List.of("id1", "id2"))
+        );
+        AppliedQueryRules appliedQueryRules = new AppliedQueryRules();
+        rule.applyRule(appliedQueryRules, Map.of("query", "elastic"));
+        assertEquals(List.of("id1", "id2"), appliedQueryRules.pinnedIds());
+
+        appliedQueryRules = new AppliedQueryRules();
+        rule.applyRule(appliedQueryRules, Map.of("query", "elastic1"));
+        assertEquals(Collections.emptyList(), appliedQueryRules.pinnedIds());
+    }
+
+    public void testApplyRuleWithMultipleCriteria() {
+        QueryRule rule = new QueryRule(
+            randomAlphaOfLength(10),
+            QueryRule.QueryRuleType.PINNED,
+            List.of(new QueryRuleCriteria(PREFIX, "query", List.of("elastic")), new QueryRuleCriteria(SUFFIX, "query", List.of("search"))),
+            Map.of("ids", List.of("id1", "id2"))
+        );
+        AppliedQueryRules appliedQueryRules = new AppliedQueryRules();
+        rule.applyRule(appliedQueryRules, Map.of("query", "elastic - you know, for search"));
+        assertEquals(List.of("id1", "id2"), appliedQueryRules.pinnedIds());
+
+        appliedQueryRules = new AppliedQueryRules();
+        rule.applyRule(appliedQueryRules, Map.of("query", "elastic"));
+        assertEquals(Collections.emptyList(), appliedQueryRules.pinnedIds());
+    }
+
     private void assertXContent(QueryRule queryRule, boolean humanReadable) throws IOException {
         BytesReference originalBytes = toShuffledXContent(queryRule, XContentType.JSON, ToXContent.EMPTY_PARAMS, humanReadable);
         QueryRule parsed;

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

@@ -32,7 +32,7 @@ import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicReference;
 
 import static org.elasticsearch.xpack.application.rules.QueryRule.QueryRuleType;
-import static org.elasticsearch.xpack.application.rules.QueryRuleCriteria.CriteriaType;
+import static org.elasticsearch.xpack.application.rules.QueryRuleCriteriaType.EXACT;
 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;
@@ -69,7 +69,7 @@ public class QueryRulesIndexServiceTests extends ESSingleNodeTestCase {
             final QueryRule myQueryRule1 = new QueryRule(
                 "my_rule1",
                 QueryRuleType.PINNED,
-                List.of(new QueryRuleCriteria(CriteriaType.EXACT, "query_string", "foo")),
+                List.of(new QueryRuleCriteria(EXACT, "query_string", List.of("foo"))),
                 Map.of("ids", List.of("id1", "id2"))
             );
             final QueryRuleset myQueryRuleset = new QueryRuleset("my_ruleset", Collections.singletonList(myQueryRule1));
@@ -84,13 +84,13 @@ public class QueryRulesIndexServiceTests extends ESSingleNodeTestCase {
         final QueryRule myQueryRule1 = new QueryRule(
             "my_rule1",
             QueryRuleType.PINNED,
-            List.of(new QueryRuleCriteria(CriteriaType.EXACT, "query_string", "foo")),
+            List.of(new QueryRuleCriteria(EXACT, "query_string", List.of("foo"))),
             Map.of("docs", List.of(Map.of("_index", "my_index1", "_id", "id1"), Map.of("_index", "my_index2", "_id", "id2")))
         );
         final QueryRule myQueryRule2 = new QueryRule(
             "my_rule2",
             QueryRuleType.PINNED,
-            List.of(new QueryRuleCriteria(CriteriaType.EXACT, "query_string", "bar")),
+            List.of(new QueryRuleCriteria(EXACT, "query_string", List.of("bar"))),
             Map.of("docs", List.of(Map.of("_index", "my_index1", "_id", "id3"), Map.of("_index", "my_index2", "_id", "id4")))
         );
         final QueryRuleset myQueryRuleset = new QueryRuleset("my_ruleset", List.of(myQueryRule1, myQueryRule2));
@@ -108,13 +108,13 @@ public class QueryRulesIndexServiceTests extends ESSingleNodeTestCase {
                 new QueryRule(
                     "my_rule_" + i,
                     QueryRuleType.PINNED,
-                    List.of(new QueryRuleCriteria(CriteriaType.EXACT, "query_string", "foo" + i)),
+                    List.of(new QueryRuleCriteria(EXACT, "query_string", List.of("foo" + i))),
                     Map.of("ids", List.of("id1", "id2"))
                 ),
                 new QueryRule(
                     "my_rule_" + i + "_" + (i + 1),
                     QueryRuleType.PINNED,
-                    List.of(new QueryRuleCriteria(CriteriaType.EXACT, "query_string", "bar" + i)),
+                    List.of(new QueryRuleCriteria(EXACT, "query_string", List.of("bar" + i))),
                     Map.of("ids", List.of("id3", "id4"))
                 )
             );
@@ -158,13 +158,13 @@ public class QueryRulesIndexServiceTests extends ESSingleNodeTestCase {
             final QueryRule myQueryRule1 = new QueryRule(
                 "my_rule1",
                 QueryRuleType.PINNED,
-                List.of(new QueryRuleCriteria(CriteriaType.EXACT, "query_string", "foo")),
+                List.of(new QueryRuleCriteria(EXACT, "query_string", List.of("foo"))),
                 Map.of("ids", List.of("id1", "id2"))
             );
             final QueryRule myQueryRule2 = new QueryRule(
                 "my_rule2",
                 QueryRuleType.PINNED,
-                List.of(new QueryRuleCriteria(CriteriaType.EXACT, "query_string", "bar")),
+                List.of(new QueryRuleCriteria(EXACT, "query_string", List.of("bar"))),
                 Map.of("ids", List.of("id3", "id4"))
             );
             final QueryRuleset myQueryRuleset = new QueryRuleset("my_ruleset", List.of(myQueryRule1, myQueryRule2));

+ 4 - 4
x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/rules/QueryRulesetTests.java

@@ -55,7 +55,7 @@ public class QueryRulesetTests extends ESTestCase {
                 {
                   "rule_id": "my_query_rule1",
                   "type": "pinned",
-                  "criteria": [ {"type": "exact", "metadata": "query_string", "value": "foo"} ],
+                  "criteria": [ {"type": "exact", "metadata": "query_string", "values": ["foo", "bar"]} ],
                   "actions": {
                     "ids": ["id1", "id2"]
                   }
@@ -63,7 +63,7 @@ public class QueryRulesetTests extends ESTestCase {
                 {
                   "rule_id": "my_query_rule2",
                   "type": "pinned",
-                  "criteria": [ {"type": "exact", "metadata": "query_string", "value": "bar"} ],
+                  "criteria": [ {"type": "exact", "metadata": "query_string", "values": ["baz"]} ],
                   "actions": {
                     "ids": ["id3", "id4"]
                   }
@@ -89,7 +89,7 @@ public class QueryRulesetTests extends ESTestCase {
                 {
                   "rule_id": "my_query_rule1",
                   "type": "pinned",
-                  "criteria": [ {"type": "exact", "metadata": "query_string", "value": "foo"} ],
+                  "criteria": [ {"type": "exact", "metadata": "query_string", "values": ["foo", "bar"]} ],
                   "actions": {
                     "ids": ["id1", "id2"]
                   }
@@ -97,7 +97,7 @@ public class QueryRulesetTests extends ESTestCase {
                 {
                   "rule_id": "my_query_rule2",
                   "type": "pinned",
-                  "criteria": [ {"type": "exact", "metadata": "query_string", "value": "bar"} ],
+                  "criteria": [ {"type": "exact", "metadata": "query_string", "values": ["baz"]} ],
                   "actions": {
                     "ids": ["id3", "id4"]
                   }

+ 2 - 1
x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/rules/RuleQueryBuilderTests.java

@@ -41,6 +41,7 @@ import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
+import static org.elasticsearch.xpack.application.rules.QueryRuleCriteriaType.EXACT;
 import static org.hamcrest.CoreMatchers.instanceOf;
 
 public class RuleQueryBuilderTests extends AbstractQueryTestCase<RuleQueryBuilder> {
@@ -140,7 +141,7 @@ public class RuleQueryBuilderTests extends AbstractQueryTestCase<RuleQueryBuilde
                 new QueryRule(
                     "my_rule1",
                     QueryRule.QueryRuleType.PINNED,
-                    List.of(new QueryRuleCriteria(QueryRuleCriteria.CriteriaType.EXACT, "query_string", "elastic")),
+                    List.of(new QueryRuleCriteria(EXACT, "query_string", List.of("elastic"))),
                     Map.of("ids", List.of("id1", "id2"))
                 )
             );

+ 20 - 1
x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/rules/action/GetQueryRulesetActionResponseBWCSerializingTests.java

@@ -10,11 +10,16 @@ package org.elasticsearch.xpack.application.rules.action;
 import org.elasticsearch.TransportVersion;
 import org.elasticsearch.common.io.stream.Writeable;
 import org.elasticsearch.xcontent.XContentParser;
+import org.elasticsearch.xpack.application.rules.QueryRule;
+import org.elasticsearch.xpack.application.rules.QueryRuleCriteria;
 import org.elasticsearch.xpack.application.rules.QueryRuleset;
 import org.elasticsearch.xpack.core.ml.AbstractBWCSerializationTestCase;
 
 import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
 
+import static org.elasticsearch.xpack.application.rules.QueryRuleCriteria.CRITERIA_METADATA_VALUES_TRANSPORT_VERSION;
 import static org.elasticsearch.xpack.application.search.SearchApplicationTestUtils.randomQueryRuleset;
 
 public class GetQueryRulesetActionResponseBWCSerializingTests extends AbstractBWCSerializationTestCase<GetQueryRulesetAction.Response> {
@@ -43,6 +48,20 @@ public class GetQueryRulesetActionResponseBWCSerializingTests extends AbstractBW
 
     @Override
     protected GetQueryRulesetAction.Response mutateInstanceForVersion(GetQueryRulesetAction.Response instance, TransportVersion version) {
-        return new GetQueryRulesetAction.Response(instance.queryRuleset());
+        if (version.before(CRITERIA_METADATA_VALUES_TRANSPORT_VERSION)) {
+            List<QueryRule> rules = new ArrayList<>();
+            for (QueryRule rule : instance.queryRuleset().rules()) {
+                List<QueryRuleCriteria> newCriteria = new ArrayList<>();
+                for (QueryRuleCriteria criteria : rule.criteria()) {
+                    newCriteria.add(
+                        new QueryRuleCriteria(criteria.criteriaType(), criteria.criteriaMetadata(), criteria.criteriaValues().subList(0, 1))
+                    );
+                }
+                rules.add(new QueryRule(rule.id(), rule.type(), newCriteria, rule.actions()));
+            }
+            return new GetQueryRulesetAction.Response(new QueryRuleset(instance.queryRuleset().id(), rules));
+        }
+
+        return instance;
     }
 }

+ 23 - 1
x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/rules/action/PutQueryRulesetActionRequestBWCSerializingTests.java

@@ -10,11 +10,17 @@ package org.elasticsearch.xpack.application.rules.action;
 import org.elasticsearch.TransportVersion;
 import org.elasticsearch.common.io.stream.Writeable;
 import org.elasticsearch.xcontent.XContentParser;
+import org.elasticsearch.xpack.application.rules.QueryRule;
+import org.elasticsearch.xpack.application.rules.QueryRuleCriteria;
 import org.elasticsearch.xpack.application.rules.QueryRuleset;
 import org.elasticsearch.xpack.application.search.SearchApplicationTestUtils;
 import org.elasticsearch.xpack.core.ml.AbstractBWCSerializationTestCase;
 
 import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.elasticsearch.xpack.application.rules.QueryRuleCriteria.CRITERIA_METADATA_VALUES_TRANSPORT_VERSION;
 
 public class PutQueryRulesetActionRequestBWCSerializingTests extends AbstractBWCSerializationTestCase<PutQueryRulesetAction.Request> {
 
@@ -43,6 +49,22 @@ public class PutQueryRulesetActionRequestBWCSerializingTests extends AbstractBWC
 
     @Override
     protected PutQueryRulesetAction.Request mutateInstanceForVersion(PutQueryRulesetAction.Request instance, TransportVersion version) {
-        return new PutQueryRulesetAction.Request(instance.queryRuleset());
+
+        if (version.before(CRITERIA_METADATA_VALUES_TRANSPORT_VERSION)) {
+            List<QueryRule> rules = new ArrayList<>();
+            for (QueryRule rule : instance.queryRuleset().rules()) {
+                List<QueryRuleCriteria> newCriteria = new ArrayList<>();
+                for (QueryRuleCriteria criteria : rule.criteria()) {
+                    newCriteria.add(
+                        new QueryRuleCriteria(criteria.criteriaType(), criteria.criteriaMetadata(), criteria.criteriaValues().subList(0, 1))
+                    );
+                }
+                rules.add(new QueryRule(rule.id(), rule.type(), newCriteria, rule.actions()));
+            }
+            return new PutQueryRulesetAction.Request(new QueryRuleset(instance.queryRuleset().id(), rules));
+        }
+
+        // Default to current instance
+        return instance;
     }
 }

+ 7 - 5
x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/search/SearchApplicationTestUtils.java

@@ -13,10 +13,12 @@ import org.elasticsearch.script.ScriptType;
 import org.elasticsearch.test.ESTestCase;
 import org.elasticsearch.xpack.application.rules.QueryRule;
 import org.elasticsearch.xpack.application.rules.QueryRuleCriteria;
+import org.elasticsearch.xpack.application.rules.QueryRuleCriteriaType;
 import org.elasticsearch.xpack.application.rules.QueryRuleset;
 import org.elasticsearch.xpack.core.action.util.PageParams;
 
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
 import java.util.Locale;
@@ -28,8 +30,10 @@ import static org.elasticsearch.test.ESTestCase.randomBoolean;
 import static org.elasticsearch.test.ESTestCase.randomFrom;
 import static org.elasticsearch.test.ESTestCase.randomIdentifier;
 import static org.elasticsearch.test.ESTestCase.randomIntBetween;
+import static org.elasticsearch.test.ESTestCase.randomList;
 import static org.elasticsearch.test.ESTestCase.randomLongBetween;
 import static org.elasticsearch.test.ESTestCase.randomMap;
+import static org.elasticsearch.xpack.application.rules.QueryRuleCriteriaType.ALWAYS;
 
 // TODO - move this one package up and rename to EnterpriseSearchModuleTestUtils
 public final class SearchApplicationTestUtils {
@@ -79,11 +83,9 @@ public final class SearchApplicationTestUtils {
     }
 
     public static QueryRuleCriteria randomQueryRuleCriteria() {
-        return new QueryRuleCriteria(
-            randomFrom(QueryRuleCriteria.CriteriaType.values()),
-            randomAlphaOfLengthBetween(1, 10),
-            randomAlphaOfLengthBetween(1, 10)
-        );
+        // We intentionally don't allow ALWAYS criteria in this method, since we want to test parsing metadata and values
+        QueryRuleCriteriaType type = randomFrom(Arrays.stream(QueryRuleCriteriaType.values()).filter(t -> t != ALWAYS).toList());
+        return new QueryRuleCriteria(type, randomAlphaOfLengthBetween(1, 10), randomList(1, 5, () -> randomAlphaOfLengthBetween(1, 10)));
     }
 
     public static QueryRule randomQueryRule() {