Browse Source

Add support for RLIKE (LIST) with pushdown (#129929)

Adds support for RLIKE function alternative syntax with a list of patterns.
Examples:

ROW message = "foobar"
| WHERE message RLIKE ("foo.*", "bar.")
The new syntax is documented as part of the existing RLIKE function documentation. We will use the existing RLike java implementation for existing cases using the old syntax and one list argument case to improve mixed cluster compatibility.
The RLikeList is pushed down as a single Automaton to improve performance.
Julian Kiryakov 3 months ago
parent
commit
f0c30f272d
24 changed files with 1645 additions and 364 deletions
  1. 5 0
      docs/changelog/129929.yaml
  2. 13 0
      docs/reference/query-languages/esql/_snippets/operators/detailedDescription/rlike.md
  3. 14 1
      x-pack/plugin/esql-core/src/main/java/org/elasticsearch/xpack/esql/core/expression/predicate/regex/RLikePattern.java
  4. 92 0
      x-pack/plugin/esql-core/src/main/java/org/elasticsearch/xpack/esql/core/expression/predicate/regex/RLikePatternList.java
  5. 68 18
      x-pack/plugin/esql/qa/server/multi-clusters/src/javaRestTest/java/org/elasticsearch/xpack/esql/ccq/MultiClustersIT.java
  6. 36 0
      x-pack/plugin/esql/qa/server/single-node/src/javaRestTest/java/org/elasticsearch/xpack/esql/qa/single_node/PushQueriesIT.java
  7. 452 0
      x-pack/plugin/esql/qa/testFixtures/src/main/resources/where-like.csv-spec
  8. 1 0
      x-pack/plugin/esql/src/main/antlr/parser/Expression.g4
  9. 7 0
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java
  10. 2 0
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/ExpressionWritables.java
  11. 9 0
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/regex/RLike.java
  12. 158 0
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/regex/RLikeList.java
  13. 3 2
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/ReplaceStringCasingWithInsensitiveRegexMatch.java
  14. 0 0
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/EsqlBaseParser.interp
  15. 419 341
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/EsqlBaseParser.java
  16. 12 0
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/EsqlBaseParserBaseListener.java
  17. 7 0
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/EsqlBaseParserBaseVisitor.java
  18. 12 0
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/EsqlBaseParserListener.java
  19. 7 0
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/EsqlBaseParserVisitor.java
  20. 18 1
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/ExpressionBuilder.java
  21. 49 0
      x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/RLikeListErrorTests.java
  22. 54 0
      x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/RLikeListSerializationTests.java
  23. 206 0
      x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/RLikeListTests.java
  24. 1 1
      x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/parser/StatementParserTests.java

+ 5 - 0
docs/changelog/129929.yaml

@@ -0,0 +1,5 @@
+pr: 129929
+summary: Add support for RLIKE (LIST) with pushdown
+area: ES|QL
+type: enhancement
+issues: []

+ 13 - 0
docs/reference/query-languages/esql/_snippets/operators/detailedDescription/rlike.md

@@ -17,4 +17,17 @@ ROW message = "foo ( bar"
 | WHERE message RLIKE """foo \( bar"""
 ```
 
+```{applies_to}
+stack: ga 9.1
+serverless: ga
+```
+
+Both a single pattern or a list of patterns are supported. If a list of patterns is provided,
+the expression will return true if any of the patterns match.
+
+```esql
+ROW message = "foobar"
+| WHERE message RLIKE ("foo.*", "bar.")
+```
+
 

+ 14 - 1
x-pack/plugin/esql-core/src/main/java/org/elasticsearch/xpack/esql/core/expression/predicate/regex/RLikePattern.java

@@ -9,10 +9,14 @@ package org.elasticsearch.xpack.esql.core.expression.predicate.regex;
 import org.apache.lucene.util.automaton.Automaton;
 import org.apache.lucene.util.automaton.Operations;
 import org.apache.lucene.util.automaton.RegExp;
+import org.elasticsearch.common.io.stream.StreamInput;
+import org.elasticsearch.common.io.stream.StreamOutput;
+import org.elasticsearch.common.io.stream.Writeable;
 
+import java.io.IOException;
 import java.util.Objects;
 
-public class RLikePattern extends AbstractStringPattern {
+public class RLikePattern extends AbstractStringPattern implements Writeable {
 
     private final String regexpPattern;
 
@@ -20,6 +24,15 @@ public class RLikePattern extends AbstractStringPattern {
         this.regexpPattern = regexpPattern;
     }
 
+    public RLikePattern(StreamInput in) throws IOException {
+        this(in.readString());
+    }
+
+    @Override
+    public void writeTo(StreamOutput out) throws IOException {
+        out.writeString(regexpPattern);
+    }
+
     @Override
     public Automaton createAutomaton(boolean ignoreCase) {
         int matchFlags = ignoreCase ? RegExp.CASE_INSENSITIVE : 0;

+ 92 - 0
x-pack/plugin/esql-core/src/main/java/org/elasticsearch/xpack/esql/core/expression/predicate/regex/RLikePatternList.java

@@ -0,0 +1,92 @@
+/*
+ * 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.esql.core.expression.predicate.regex;
+
+import org.apache.lucene.util.automaton.Automaton;
+import org.apache.lucene.util.automaton.Operations;
+import org.elasticsearch.common.io.stream.StreamInput;
+import org.elasticsearch.common.io.stream.StreamOutput;
+import org.elasticsearch.common.io.stream.Writeable;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+public class RLikePatternList extends AbstractStringPattern implements Writeable {
+
+    private final List<RLikePattern> patternList;
+
+    public RLikePatternList(List<RLikePattern> patternList) {
+        this.patternList = patternList;
+    }
+
+    public RLikePatternList(StreamInput in) throws IOException {
+        this(in.readCollectionAsList(RLikePattern::new));
+    }
+
+    @Override
+    public void writeTo(StreamOutput out) throws IOException {
+        out.writeCollection(patternList, (o, pattern) -> pattern.writeTo(o));
+    }
+
+    public List<RLikePattern> patternList() {
+        return patternList;
+    }
+
+    /**
+     * Creates an automaton that matches any of the patterns in the list.
+     * We create a single automaton that is the union of all individual automatons to improve performance
+     */
+    @Override
+    public Automaton createAutomaton(boolean ignoreCase) {
+        List<Automaton> automatonList = patternList.stream().map(x -> x.createAutomaton(ignoreCase)).toList();
+        Automaton result = Operations.union(automatonList);
+        return Operations.determinize(result, Operations.DEFAULT_DETERMINIZE_WORK_LIMIT);
+    }
+
+    /**
+     * Returns a Java regex that matches any of the patterns in the list.
+     * The patterns are joined with the '|' operator to create a single regex.
+     */
+    @Override
+    public String asJavaRegex() {
+        return patternList.stream().map(RLikePattern::asJavaRegex).collect(Collectors.joining("|"));
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(patternList);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null || getClass() != obj.getClass()) {
+            return false;
+        }
+        RLikePatternList other = (RLikePatternList) obj;
+        return patternList.equals(other.patternList);
+    }
+
+    /**
+     * Returns a string that matches any of the patterns in the list.
+     * The patterns are joined with the '|' operator to create a single regex string.
+     */
+    @Override
+    public String pattern() {
+        if (patternList.isEmpty()) {
+            return "";
+        }
+        if (patternList.size() == 1) {
+            return patternList.get(0).pattern();
+        }
+        return "(\"" + patternList.stream().map(RLikePattern::pattern).collect(Collectors.joining("\", \"")) + "\")";
+    }
+}

+ 68 - 18
x-pack/plugin/esql/qa/server/multi-clusters/src/javaRestTest/java/org/elasticsearch/xpack/esql/ccq/MultiClustersIT.java

@@ -205,25 +205,24 @@ public class MultiClustersIT extends ESRestTestCase {
         }
     }
 
-    private <C, V> void assertResultMapForLike(
+    private <C, V> void assertResultMapWithCapabilities(
         boolean includeCCSMetadata,
         Map<String, Object> result,
         C columns,
         V values,
         boolean remoteOnly,
-        boolean requireLikeListCapability
+        List<String> fullResultCapabilities
     ) throws IOException {
-        List<String> requiredCapabilities = new ArrayList<>(List.of("like_on_index_fields"));
-        if (requireLikeListCapability) {
-            requiredCapabilities.add("like_list_on_index_fields");
-        }
         // the feature is completely supported if both local and remote clusters support it
-        boolean isSupported = capabilitiesSupportedNewAndOld(requiredCapabilities);
-
+        // otherwise we expect a partial result, and will not check the data
+        boolean isSupported = capabilitiesSupportedNewAndOld(fullResultCapabilities);
         if (isSupported) {
             assertResultMap(includeCCSMetadata, result, columns, values, remoteOnly);
         } else {
-            logger.info("-->  skipping data check for like index test, cluster does not support like index feature");
+            logger.info(
+                "-->  skipping data check for a test, cluster does not support all of [{}] capabilities",
+                String.join(",", fullResultCapabilities)
+            );
             // just verify that we did not get a partial result
             var clusters = result.get("_clusters");
             var reason = "unexpected partial results" + (clusters != null ? ": _clusters=" + clusters : "");
@@ -526,7 +525,7 @@ public class MultiClustersIT extends ESRestTestCase {
             """, includeCCSMetadata);
         var columns = List.of(Map.of("name", "c", "type", "long"), Map.of("name", "_index", "type", "keyword"));
         var values = List.of(List.of(remoteDocs.size(), REMOTE_CLUSTER_NAME + ":" + remoteIndex));
-        assertResultMapForLike(includeCCSMetadata, result, columns, values, false, false);
+        assertResultMapWithCapabilities(includeCCSMetadata, result, columns, values, false, List.of("like_on_index_fields"));
     }
 
     public void testLikeIndexLegacySettingNoResults() throws Exception {
@@ -548,7 +547,7 @@ public class MultiClustersIT extends ESRestTestCase {
             var columns = List.of(Map.of("name", "c", "type", "long"), Map.of("name", "_index", "type", "keyword"));
             // we expect empty result, since the setting is false
             var values = List.of();
-            assertResultMapForLike(includeCCSMetadata, result, columns, values, false, false);
+            assertResultMapWithCapabilities(includeCCSMetadata, result, columns, values, false, List.of("like_on_index_fields"));
         }
     }
 
@@ -572,7 +571,7 @@ public class MultiClustersIT extends ESRestTestCase {
             var columns = List.of(Map.of("name", "c", "type", "long"), Map.of("name", "_index", "type", "keyword"));
             // we expect results, since the setting is false, but there is : in the LIKE query
             var values = List.of(List.of(remoteDocs.size(), REMOTE_CLUSTER_NAME + ":" + remoteIndex));
-            assertResultMapForLike(includeCCSMetadata, result, columns, values, false, false);
+            assertResultMapWithCapabilities(includeCCSMetadata, result, columns, values, false, List.of("like_on_index_fields"));
         }
     }
 
@@ -586,7 +585,7 @@ public class MultiClustersIT extends ESRestTestCase {
             """, includeCCSMetadata);
         var columns = List.of(Map.of("name", "c", "type", "long"), Map.of("name", "_index", "type", "keyword"));
         var values = List.of(List.of(localDocs.size(), localIndex));
-        assertResultMapForLike(includeCCSMetadata, result, columns, values, false, false);
+        assertResultMapWithCapabilities(includeCCSMetadata, result, columns, values, false, List.of("like_on_index_fields"));
     }
 
     public void testLikeListIndex() throws Exception {
@@ -601,7 +600,14 @@ public class MultiClustersIT extends ESRestTestCase {
             """, includeCCSMetadata);
         var columns = List.of(Map.of("name", "c", "type", "long"), Map.of("name", "_index", "type", "keyword"));
         var values = List.of(List.of(remoteDocs.size(), REMOTE_CLUSTER_NAME + ":" + remoteIndex));
-        assertResultMapForLike(includeCCSMetadata, result, columns, values, false, true);
+        assertResultMapWithCapabilities(
+            includeCCSMetadata,
+            result,
+            columns,
+            values,
+            false,
+            List.of("like_on_index_fields", "like_list_on_index_fields")
+        );
     }
 
     public void testNotLikeListIndex() throws Exception {
@@ -615,7 +621,14 @@ public class MultiClustersIT extends ESRestTestCase {
             """, includeCCSMetadata);
         var columns = List.of(Map.of("name", "c", "type", "long"), Map.of("name", "_index", "type", "keyword"));
         var values = List.of(List.of(localDocs.size(), localIndex));
-        assertResultMapForLike(includeCCSMetadata, result, columns, values, false, true);
+        assertResultMapWithCapabilities(
+            includeCCSMetadata,
+            result,
+            columns,
+            values,
+            false,
+            List.of("like_on_index_fields", "like_list_on_index_fields")
+        );
     }
 
     public void testNotLikeListKeyword() throws Exception {
@@ -639,7 +652,14 @@ public class MultiClustersIT extends ESRestTestCase {
         if (localCount > 0) {
             values.add(List.of(localCount, localIndex));
         }
-        assertResultMapForLike(includeCCSMetadata, result, columns, values, false, true);
+        assertResultMapWithCapabilities(
+            includeCCSMetadata,
+            result,
+            columns,
+            values,
+            false,
+            List.of("like_on_index_fields", "like_list_on_index_fields")
+        );
     }
 
     public void testRLikeIndex() throws Exception {
@@ -652,7 +672,7 @@ public class MultiClustersIT extends ESRestTestCase {
             """, includeCCSMetadata);
         var columns = List.of(Map.of("name", "c", "type", "long"), Map.of("name", "_index", "type", "keyword"));
         var values = List.of(List.of(remoteDocs.size(), REMOTE_CLUSTER_NAME + ":" + remoteIndex));
-        assertResultMapForLike(includeCCSMetadata, result, columns, values, false, false);
+        assertResultMapWithCapabilities(includeCCSMetadata, result, columns, values, false, List.of("like_on_index_fields"));
     }
 
     public void testNotRLikeIndex() throws Exception {
@@ -665,7 +685,37 @@ public class MultiClustersIT extends ESRestTestCase {
             """, includeCCSMetadata);
         var columns = List.of(Map.of("name", "c", "type", "long"), Map.of("name", "_index", "type", "keyword"));
         var values = List.of(List.of(localDocs.size(), localIndex));
-        assertResultMapForLike(includeCCSMetadata, result, columns, values, false, false);
+        assertResultMapWithCapabilities(includeCCSMetadata, result, columns, values, false, List.of("like_on_index_fields"));
+    }
+
+    public void testRLikeListIndex() throws Exception {
+        assumeTrue("not supported", capabilitiesSupportedNewAndOld(List.of("rlike_with_list_of_patterns")));
+        boolean includeCCSMetadata = includeCCSMetadata();
+        Map<String, Object> result = run("""
+            FROM test-local-index,*:test-remote-index METADATA _index
+            | WHERE _index RLIKE (".*remote.*", ".*not-exist.*")
+            | STATS c = COUNT(*) BY _index
+            | SORT _index ASC
+            """, includeCCSMetadata);
+        var columns = List.of(Map.of("name", "c", "type", "long"), Map.of("name", "_index", "type", "keyword"));
+        var values = List.of(List.of(remoteDocs.size(), REMOTE_CLUSTER_NAME + ":" + remoteIndex));
+        // we depend on the code in like_on_index_fields to serialize an ExpressionQueryBuilder
+        assertResultMapWithCapabilities(includeCCSMetadata, result, columns, values, false, List.of("like_on_index_fields"));
+    }
+
+    public void testNotRLikeListIndex() throws Exception {
+        assumeTrue("not supported", capabilitiesSupportedNewAndOld(List.of("rlike_with_list_of_patterns")));
+        boolean includeCCSMetadata = includeCCSMetadata();
+        Map<String, Object> result = run("""
+            FROM test-local-index,*:test-remote-index METADATA _index
+            | WHERE _index NOT RLIKE (".*remote.*", ".*not-exist.*")
+            | STATS c = COUNT(*) BY _index
+            | SORT _index ASC
+            """, includeCCSMetadata);
+        var columns = List.of(Map.of("name", "c", "type", "long"), Map.of("name", "_index", "type", "keyword"));
+        var values = List.of(List.of(localDocs.size(), localIndex));
+        // we depend on the code in like_on_index_fields to serialize an ExpressionQueryBuilder
+        assertResultMapWithCapabilities(includeCCSMetadata, result, columns, values, false, List.of("like_on_index_fields"));
     }
 
     private RestClient remoteClusterClient() throws IOException {

+ 36 - 0
x-pack/plugin/esql/qa/server/single-node/src/javaRestTest/java/org/elasticsearch/xpack/esql/qa/single_node/PushQueriesIT.java

@@ -275,6 +275,42 @@ public class PushQueriesIT extends ESRestTestCase {
         testPushQuery(value, esqlQuery, List.of(luceneQuery), dataNodeSignature, true);
     }
 
+    public void testRLike() throws IOException {
+        String value = "v".repeat(between(1, 256));
+        String esqlQuery = """
+            FROM test
+            | WHERE test rlike "%value.*"
+            """;
+        String luceneQuery = switch (type) {
+            case KEYWORD -> "test:/%value.*/";
+            case CONSTANT_KEYWORD, MATCH_ONLY_TEXT_WITH_KEYWORD, AUTO, TEXT_WITH_KEYWORD -> "*:*";
+            case SEMANTIC_TEXT_WITH_KEYWORD -> "FieldExistsQuery [field=_primary_term]";
+        };
+        ComputeSignature dataNodeSignature = switch (type) {
+            case CONSTANT_KEYWORD, KEYWORD -> ComputeSignature.FILTER_IN_QUERY;
+            case AUTO, TEXT_WITH_KEYWORD, MATCH_ONLY_TEXT_WITH_KEYWORD, SEMANTIC_TEXT_WITH_KEYWORD -> ComputeSignature.FILTER_IN_COMPUTE;
+        };
+        testPushQuery(value, esqlQuery, List.of(luceneQuery), dataNodeSignature, true);
+    }
+
+    public void testRLikeList() throws IOException {
+        String value = "v".repeat(between(1, 256));
+        String esqlQuery = """
+            FROM test
+            | WHERE test rlike ("%value.*", "abc.*")
+            """;
+        String luceneQuery = switch (type) {
+            case CONSTANT_KEYWORD, MATCH_ONLY_TEXT_WITH_KEYWORD, AUTO, TEXT_WITH_KEYWORD -> "*:*";
+            case SEMANTIC_TEXT_WITH_KEYWORD -> "FieldExistsQuery [field=_primary_term]";
+            case KEYWORD -> "test:RLIKE(\"%value.*\", \"abc.*\"), caseInsensitive=false";
+        };
+        ComputeSignature dataNodeSignature = switch (type) {
+            case CONSTANT_KEYWORD, KEYWORD -> ComputeSignature.FILTER_IN_QUERY;
+            case AUTO, TEXT_WITH_KEYWORD, MATCH_ONLY_TEXT_WITH_KEYWORD, SEMANTIC_TEXT_WITH_KEYWORD -> ComputeSignature.FILTER_IN_COMPUTE;
+        };
+        testPushQuery(value, esqlQuery, List.of(luceneQuery), dataNodeSignature, true);
+    }
+
     enum ComputeSignature {
         FILTER_IN_COMPUTE(
             matchesList().item("LuceneSourceOperator")

+ 452 - 0
x-pack/plugin/esql/qa/testFixtures/src/main/resources/where-like.csv-spec

@@ -534,6 +534,458 @@ emp_no:integer | first_name:keyword
 10055          | Georgy    
 ;
 
+rlikeListEmptyArgWildcard
+required_capability: rlike_with_list_of_patterns
+FROM employees 
+| WHERE first_name rlike ("") 
+| KEEP emp_no, first_name;
+
+emp_no:integer | first_name:keyword    
+;
+
+rlikeListSingleArgWildcard
+required_capability: rlike_with_list_of_patterns
+FROM employees 
+| WHERE first_name RLIKE ("Eberhardt.*")
+| KEEP emp_no, first_name;
+
+emp_no:integer | first_name:keyword
+10013          | Eberhardt      
+;
+
+rlikeListTwoArgWildcard
+required_capability: rlike_with_list_of_patterns
+FROM employees 
+| WHERE first_name rlike ("Eberhardt.*", "testString.*") 
+| KEEP emp_no, first_name;
+
+emp_no:integer | first_name:keyword
+10013          | Eberhardt       
+;
+
+rlikeListDocExample
+required_capability: rlike_with_list_of_patterns
+// tag::rlikeListDocExample[]
+ROW message = "foobar" 
+| WHERE message RLIKE ("foo.*", "bar.")
+// end::rlikeListDocExample[]
+;
+
+message:string
+foobar   
+;
+
+rlikeListThreeArgWildcard
+required_capability: rlike_with_list_of_patterns
+FROM employees 
+| WHERE first_name rlike ("Eberhardt.*", "Ot.*", "Part.") 
+| KEEP emp_no, first_name  
+| SORT emp_no;
+
+emp_no:integer | first_name:keyword
+10003          | Parto
+10013          | Eberhardt
+10029          | Otmar     
+;
+
+rlikeListMultipleWhere
+required_capability: rlike_with_list_of_patterns
+FROM employees 
+| WHERE first_name RLIKE ("Eberhardt.*", "Ot.*", "Part.") 
+| WHERE first_name RLIKE ("Eberhard.", "Otm.*")
+| KEEP emp_no, first_name
+| SORT emp_no;
+
+emp_no:integer | first_name:keyword
+10013          | Eberhardt
+10029          | Otmar
+;
+
+rlikeListAllWildcard
+required_capability: rlike_with_list_of_patterns
+FROM employees 
+| WHERE first_name rlike (".*") 
+| KEEP emp_no, first_name 
+| SORT emp_no 
+| LIMIT 2;
+
+emp_no:integer | first_name:keyword
+10001          | Georgi
+10002          | Bezalel
+;
+
+rlikeListOverlappingPatterns
+required_capability: rlike_with_list_of_patterns
+FROM employees 
+| WHERE first_name rlike ("Eber.*", "Eberhardt") 
+| KEEP emp_no, first_name;
+
+emp_no:integer | first_name:keyword
+10013          | Eberhardt
+;
+
+rlikeListCaseSensitive
+required_capability: rlike_with_list_of_patterns
+FROM employees 
+| WHERE first_name RLIKE ("eberhardt", "EBERHARDT") 
+| KEEP emp_no, first_name;
+
+emp_no:integer | first_name:keyword
+;
+
+rlikeListSpecialCharacters
+required_capability: rlike_with_list_of_patterns
+FROM employees 
+| WHERE first_name rlike (".*ar.*", ".*eor.*") 
+| KEEP emp_no, first_name 
+| SORT emp_no;
+
+emp_no:integer | first_name:keyword
+10001          | Georgi
+10003          | Parto
+10011          | Mary
+10013          | Eberhardt
+10029          | Otmar
+10055          | Georgy
+10058          | Berhard
+10068          | Charlene
+10069          | Margareta
+10074          | Mokhtar
+10082          | Parviz
+10089          | Sudharsan
+10095          | Hilari
+;
+
+rlikeListEscapedWildcard
+required_capability: rlike_with_list_of_patterns
+FROM employees 
+| WHERE first_name rlike ("Eberhar\\*") 
+| KEEP emp_no, first_name;
+
+emp_no:integer | first_name:keyword
+;
+
+rlikeListNineOrMoreLetters
+required_capability: rlike_with_list_of_patterns
+FROM employees 
+| WHERE first_name rlike (".{9,}.*") 
+| KEEP emp_no, first_name 
+| SORT emp_no;
+
+emp_no:integer | first_name:keyword
+10004          | Chirstian
+10010          | Duangkaew
+10013          | Eberhardt
+10017          | Cristinel
+10025          | Prasadram
+10059          | Alejandro
+10069          | Margareta
+10089          | Sudharsan
+10092          | Valdiodio
+10098          | Sreekrishna
+;
+
+notRlikeListThreeArgWildcardNotOtherFilter
+required_capability: rlike_with_list_of_patterns
+FROM employees 
+| WHERE first_name not rlike ("Eberhardt.*", "Ot.*", "Part.") and emp_no < 10010 
+| KEEP emp_no, first_name
+| SORT emp_no;
+
+emp_no:integer | first_name:keyword
+10001          | Georgi
+10002          | Bezalel
+10004          | Chirstian
+10005          | Kyoichi
+10006          | Anneke
+10007          | Tzvetan
+10008          | Saniya
+10009          | Sumant   
+;
+
+rlikeListBeginningWithWildcard
+required_capability: rlike_with_list_of_patterns
+FROM employees 
+| WHERE first_name rlike ("A.*", "B.*", "C.*") 
+| KEEP emp_no, first_name 
+| SORT emp_no;
+
+emp_no:integer | first_name:keyword
+10002          | Bezalel
+10004          | Chirstian
+10006          | Anneke
+10014          | Berni
+10017          | Cristinel
+10023          | Bojan
+10049          | Basil
+10056          | Brendon
+10058          | Berhard
+10059          | Alejandro
+10060          | Breannda
+10062          | Anoosh
+10067          | Claudi
+10068          | Charlene
+10091          | Amabile
+10094          | Arumugam 
+;
+
+notRlikeListThreeArgWildcardOtherFirst
+required_capability: rlike_with_list_of_patterns
+FROM employees 
+| WHERE emp_no < 10010 and first_name not rlike ("Eberhardt.*", "Ot.*", "Part.") 
+| KEEP emp_no, first_name  
+| SORT emp_no;
+
+emp_no:integer | first_name:keyword
+10001          | Georgi
+10002          | Bezalel
+10004          | Chirstian
+10005          | Kyoichi
+10006          | Anneke
+10007          | Tzvetan
+10008          | Saniya
+10009          | Sumant
+;
+
+notRlikeFiveOrLessLetters
+required_capability: rlike_with_list_of_patterns
+FROM employees 
+| WHERE first_name not rlike (".{6,}.*") 
+| KEEP emp_no, first_name 
+| SORT emp_no;
+
+emp_no:integer | first_name:keyword
+10003          | Parto
+10011          | Mary
+10014          | Berni
+10021          | Ramzi
+10023          | Bojan
+10029          | Otmar
+10040          | Weiyi
+10041          | Uri
+10042          | Magy
+10045          | Moss
+10049          | Basil
+10057          | Ebbe
+10061          | Tse
+10063          | Gino
+10064          | Udi
+10066          | Kwee
+10071          | Hisao
+10073          | Shir
+10075          | Gao
+10076          | Erez
+10077          | Mona
+10078          | Danel
+10083          | Vishv
+10084          | Tuval
+10097          | Remzi
+;
+
+notRlikeListMultipleWhere
+required_capability: rlike_with_list_of_patterns
+FROM employees 
+| WHERE first_name NOT RLIKE ("Eberhardt.*", "Ot.*", "Part.", "A.*", "B.*", "C.*", "D.*") 
+| WHERE first_name NOT RLIKE ("Eberhard.", "Otm.*", "F.*", "G.*", "H.*", "I.*", "J.*", "K.*", "L.*")
+| KEEP emp_no, first_name 
+| SORT emp_no;
+
+emp_no:integer | first_name:keyword
+10007          | Tzvetan
+10008          | Saniya
+10009          | Sumant
+10011          | Mary
+10012          | Patricio
+10020          | Mayuko
+10021          | Ramzi
+10022          | Shahaf
+10024          | Suzette
+10025          | Prasadram
+10026          | Yongqiao
+10040          | Weiyi
+10041          | Uri
+10042          | Magy
+10043          | Yishay
+10044          | Mingsen
+10045          | Moss
+10047          | Zvonko
+10050          | Yinghua
+10053          | Sanjiv
+10054          | Mayumi
+10057          | Ebbe
+10061          | Tse
+10064          | Udi
+10065          | Satosi
+10069          | Margareta
+10070          | Reuven
+10073          | Shir
+10074          | Mokhtar
+10076          | Erez
+10077          | Mona
+10080          | Premal
+10081          | Zhongwei
+10082          | Parviz
+10083          | Vishv
+10084          | Tuval
+10086          | Somnath
+10087          | Xinglin
+10089          | Sudharsan
+10092          | Valdiodio
+10093          | Sailaja
+10097          | Remzi
+10098          | Sreekrishna
+10099          | Valter
+;
+
+notRlikeListNotField
+required_capability: rlike_with_list_of_patterns
+FROM employees 
+| WHERE NOT first_name RLIKE ("Eberhardt.*", "Ot.*", "Part.", "A.*", "B.*", "C.*", "D.*") 
+| WHERE first_name NOT RLIKE ("Eberhard.", "Otm.*", "F.*", "G.*", "H.*", "I.*", "J.*", "K.*", "L.*")
+| KEEP emp_no, first_name 
+| SORT emp_no;
+
+emp_no:integer | first_name:keyword
+10007          | Tzvetan
+10008          | Saniya
+10009          | Sumant
+10011          | Mary
+10012          | Patricio
+10020          | Mayuko
+10021          | Ramzi
+10022          | Shahaf
+10024          | Suzette
+10025          | Prasadram
+10026          | Yongqiao
+10040          | Weiyi
+10041          | Uri
+10042          | Magy
+10043          | Yishay
+10044          | Mingsen
+10045          | Moss
+10047          | Zvonko
+10050          | Yinghua
+10053          | Sanjiv
+10054          | Mayumi
+10057          | Ebbe
+10061          | Tse
+10064          | Udi
+10065          | Satosi
+10069          | Margareta
+10070          | Reuven
+10073          | Shir
+10074          | Mokhtar
+10076          | Erez
+10077          | Mona
+10080          | Premal
+10081          | Zhongwei
+10082          | Parviz
+10083          | Vishv
+10084          | Tuval
+10086          | Somnath
+10087          | Xinglin
+10089          | Sudharsan
+10092          | Valdiodio
+10093          | Sailaja
+10097          | Remzi
+10098          | Sreekrishna
+10099          | Valter
+;
+
+notRlikeListAllWildcard
+required_capability: rlike_with_list_of_patterns
+FROM employees 
+| WHERE first_name NOT RLIKE (".*") 
+| KEEP emp_no, first_name 
+| SORT emp_no 
+| LIMIT 2;
+
+emp_no:integer | first_name:keyword
+10030          | null
+10031          | null
+;
+
+notRlikeListWildcard
+required_capability: rlike_with_list_of_patterns
+FROM employees 
+| WHERE first_name NOT RLIKE ("A.*","B.*", "C.*", "D.*","E.*", "F.*", "G.*", "H.*", "I.*", "J.*", "K.*") 
+| KEEP emp_no, first_name  
+| SORT emp_no;
+
+emp_no:integer | first_name:keyword
+10003          | Parto
+10007          | Tzvetan
+10008          | Saniya
+10009          | Sumant
+10011          | Mary
+10012          | Patricio
+10019          | Lillian
+10020          | Mayuko
+10021          | Ramzi
+10022          | Shahaf
+10024          | Suzette
+10025          | Prasadram
+10026          | Yongqiao
+10029          | Otmar
+10040          | Weiyi
+10041          | Uri
+10042          | Magy
+10043          | Yishay
+10044          | Mingsen
+10045          | Moss
+10046          | Lucien
+10047          | Zvonko
+10050          | Yinghua
+10053          | Sanjiv
+10054          | Mayumi
+10061          | Tse
+10064          | Udi
+10065          | Satosi
+10069          | Margareta
+10070          | Reuven
+10073          | Shir
+10074          | Mokhtar
+10077          | Mona
+10080          | Premal
+10081          | Zhongwei
+10082          | Parviz
+10083          | Vishv
+10084          | Tuval
+10086          | Somnath
+10087          | Xinglin
+10089          | Sudharsan
+10092          | Valdiodio
+10093          | Sailaja
+10097          | Remzi
+10098          | Sreekrishna
+10099          | Valter
+;
+
+rlikeListWithUpperTurnedInsensitive
+required_capability: rlike_with_list_of_patterns
+FROM employees 
+| WHERE TO_UPPER(first_name) RLIKE ("GEOR.*") 
+| KEEP emp_no, first_name 
+| SORT emp_no;
+
+emp_no:integer | first_name:keyword
+10001          | Georgi
+10055          | Georgy     
+;
+
+rlikeListWithUpperTurnedInsensitiveMult
+required_capability: rlike_with_list_of_patterns
+FROM employees 
+| WHERE TO_UPPER(first_name) RLIKE ("GEOR.*", "WE.*")  
+| KEEP emp_no, first_name 
+| SORT emp_no;
+
+emp_no:integer | first_name:keyword
+10001          | Georgi
+10040          | Weiyi
+10055          | Georgy   
+;
+
 likeAll
 from employees | where first_name like "*" and emp_no > 10028 | sort emp_no | keep emp_no, first_name | limit 2;
 

+ 1 - 0
x-pack/plugin/esql/src/main/antlr/parser/Expression.g4

@@ -21,6 +21,7 @@ regexBooleanExpression
     : valueExpression (NOT)? LIKE string                               #likeExpression
     | valueExpression (NOT)? RLIKE string                              #rlikeExpression
     | valueExpression (NOT)? LIKE LP string  (COMMA string )* RP       #likeListExpression
+    | valueExpression (NOT)? RLIKE LP string  (COMMA string )* RP      #rlikeListExpression
     ;
 
 matchBooleanExpression

+ 7 - 0
x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java

@@ -1215,6 +1215,9 @@ public class EsqlCapabilities {
          */
         KNN_FUNCTION_V2(Build.current().isSnapshot()),
 
+        /**
+         * Support for the LIKE operator with a list of wildcards.
+         */
         LIKE_WITH_LIST_OF_PATTERNS,
 
         LIKE_LIST_ON_INDEX_FIELDS,
@@ -1236,6 +1239,10 @@ public class EsqlCapabilities {
          * (Re)Added EXPLAIN command
          */
         EXPLAIN(Build.current().isSnapshot()),
+        /**
+         * Support for the RLIKE operator with a list of regexes.
+         */
+        RLIKE_WITH_LIST_OF_PATTERNS,
 
         /**
          * FUSE command

+ 2 - 0
x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/ExpressionWritables.java

@@ -81,6 +81,7 @@ import org.elasticsearch.xpack.esql.expression.function.scalar.string.RTrim;
 import org.elasticsearch.xpack.esql.expression.function.scalar.string.Space;
 import org.elasticsearch.xpack.esql.expression.function.scalar.string.Trim;
 import org.elasticsearch.xpack.esql.expression.function.scalar.string.regex.RLike;
+import org.elasticsearch.xpack.esql.expression.function.scalar.string.regex.RLikeList;
 import org.elasticsearch.xpack.esql.expression.function.scalar.string.regex.WildcardLike;
 import org.elasticsearch.xpack.esql.expression.function.scalar.string.regex.WildcardLikeList;
 import org.elasticsearch.xpack.esql.expression.function.scalar.util.Delay;
@@ -181,6 +182,7 @@ public class ExpressionWritables {
         entries.add(Neg.ENTRY);
         entries.add(Not.ENTRY);
         entries.add(RLike.ENTRY);
+        entries.add(RLikeList.ENTRY);
         entries.add(RTrim.ENTRY);
         entries.add(Scalb.ENTRY);
         entries.add(Signum.ENTRY);

+ 9 - 0
x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/regex/RLike.java

@@ -43,6 +43,15 @@ public class RLike extends RegexMatch<RLikePattern> {
         To reduce the overhead of escaping, we suggest using triple quotes strings `\"\"\"`
 
         <<load-esql-example, file=string tag=rlikeEscapingTripleQuotes>>
+        ```{applies_to}
+        stack: ga 9.1
+        serverless: ga
+        ```
+
+        Both a single pattern or a list of patterns are supported. If a list of patterns is provided,
+        the expression will return true if any of the patterns match.
+
+        <<load-esql-example, file=where-like tag=rlikeListDocExample>>
         """, operator = NAME, examples = @Example(file = "docs", tag = "rlike"))
     public RLike(
         Source source,

+ 158 - 0
x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/regex/RLikeList.java

@@ -0,0 +1,158 @@
+/*
+ * 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.esql.expression.function.scalar.string.regex;
+
+import org.apache.lucene.search.MultiTermQuery;
+import org.apache.lucene.util.automaton.Automaton;
+import org.apache.lucene.util.automaton.CharacterRunAutomaton;
+import org.elasticsearch.common.io.stream.NamedWriteableRegistry;
+import org.elasticsearch.common.io.stream.StreamInput;
+import org.elasticsearch.common.io.stream.StreamOutput;
+import org.elasticsearch.index.mapper.MappedFieldType;
+import org.elasticsearch.index.query.SearchExecutionContext;
+import org.elasticsearch.xpack.esql.core.expression.Expression;
+import org.elasticsearch.xpack.esql.core.expression.FieldAttribute;
+import org.elasticsearch.xpack.esql.core.expression.predicate.regex.RLikePattern;
+import org.elasticsearch.xpack.esql.core.expression.predicate.regex.RLikePatternList;
+import org.elasticsearch.xpack.esql.core.querydsl.query.Query;
+import org.elasticsearch.xpack.esql.core.tree.NodeInfo;
+import org.elasticsearch.xpack.esql.core.tree.Source;
+import org.elasticsearch.xpack.esql.expression.function.Param;
+import org.elasticsearch.xpack.esql.io.stream.ExpressionQuery;
+import org.elasticsearch.xpack.esql.io.stream.PlanStreamInput;
+import org.elasticsearch.xpack.esql.optimizer.rules.physical.local.LucenePushdownPredicates;
+import org.elasticsearch.xpack.esql.planner.TranslatorHandler;
+
+import java.io.IOException;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
+
+public class RLikeList extends RegexMatch<RLikePatternList> {
+    public static final NamedWriteableRegistry.Entry ENTRY = new NamedWriteableRegistry.Entry(
+        Expression.class,
+        "RLikeList",
+        RLikeList::new
+    );
+
+    Supplier<Automaton> automatonSupplier = new Supplier<>() {
+        Automaton cached;
+
+        @Override
+        public Automaton get() {
+            if (cached == null) {
+                cached = pattern().createAutomaton(caseInsensitive());
+            }
+            return cached;
+        }
+    };
+
+    Supplier<CharacterRunAutomaton> characterRunAutomatonSupplier = new Supplier<>() {
+        CharacterRunAutomaton cached;
+
+        @Override
+        public CharacterRunAutomaton get() {
+            if (cached == null) {
+                cached = new CharacterRunAutomaton(automatonSupplier.get());
+            }
+            return cached;
+        }
+    };
+
+    /**
+     * The documentation for this function is in RLike, and shown to the users as `RLIKE` in the docs.
+     */
+    public RLikeList(
+        Source source,
+        @Param(name = "str", type = { "keyword", "text" }, description = "A literal value.") Expression value,
+        @Param(name = "patterns", type = { "keyword", "text" }, description = "A list of regular expressions.") RLikePatternList patterns
+    ) {
+        this(source, value, patterns, false);
+    }
+
+    public RLikeList(Source source, Expression field, RLikePatternList rLikePattern, boolean caseInsensitive) {
+        super(source, field, rLikePattern, caseInsensitive);
+    }
+
+    private RLikeList(StreamInput in) throws IOException {
+        this(
+            Source.readFrom((PlanStreamInput) in),
+            in.readNamedWriteable(Expression.class),
+            new RLikePatternList(in),
+            deserializeCaseInsensitivity(in)
+        );
+    }
+
+    @Override
+    public void writeTo(StreamOutput out) throws IOException {
+        source().writeTo(out);
+        out.writeNamedWriteable(field());
+        pattern().writeTo(out);
+        serializeCaseInsensitivity(out);
+    }
+
+    @Override
+    public String name() {
+        return ENTRY.name;
+    }
+
+    @Override
+    public String getWriteableName() {
+        return ENTRY.name;
+    }
+
+    @Override
+    protected RLikeList replaceChild(Expression newChild) {
+        return new RLikeList(source(), newChild, pattern(), caseInsensitive());
+    }
+
+    @Override
+    public Translatable translatable(LucenePushdownPredicates pushdownPredicates) {
+        return pushdownPredicates.isPushableAttribute(field()) ? Translatable.YES : Translatable.NO;
+    }
+
+    /**
+     * Returns a {@link Query} that matches the field against the provided patterns.
+     * For now, we only support a single pattern in the list for pushdown.
+     */
+    @Override
+    public Query asQuery(LucenePushdownPredicates pushdownPredicates, TranslatorHandler handler) {
+        var field = field();
+        LucenePushdownPredicates.checkIsPushableAttribute(field);
+        return translateField(handler.nameOf(field instanceof FieldAttribute fa ? fa.exactAttribute() : field));
+    }
+
+    private Query translateField(String targetFieldName) {
+        return new ExpressionQuery(source(), targetFieldName, this);
+    }
+
+    @Override
+    public org.apache.lucene.search.Query asLuceneQuery(
+        MappedFieldType fieldType,
+        MultiTermQuery.RewriteMethod constantScoreRewrite,
+        SearchExecutionContext context
+    ) {
+        return fieldType.automatonQuery(
+            automatonSupplier,
+            characterRunAutomatonSupplier,
+            constantScoreRewrite,
+            context,
+            getLuceneQueryDescription()
+        );
+    }
+
+    @Override
+    protected NodeInfo<? extends Expression> info() {
+        return NodeInfo.create(this, RLikeList::new, field(), pattern(), caseInsensitive());
+    }
+
+    private String getLuceneQueryDescription() {
+        // we use the information used to create the automaton to describe the query here
+        String patternDesc = pattern().patternList().stream().map(RLikePattern::pattern).collect(Collectors.joining("\", \""));
+        return "RLIKE(\"" + patternDesc + "\"), caseInsensitive=" + caseInsensitive();
+    }
+}

+ 3 - 2
x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/ReplaceStringCasingWithInsensitiveRegexMatch.java

@@ -9,6 +9,7 @@ package org.elasticsearch.xpack.esql.optimizer.rules.logical;
 
 import org.elasticsearch.xpack.esql.core.expression.Expression;
 import org.elasticsearch.xpack.esql.core.expression.Literal;
+import org.elasticsearch.xpack.esql.core.expression.predicate.regex.RLikePatternList;
 import org.elasticsearch.xpack.esql.core.expression.predicate.regex.RegexMatch;
 import org.elasticsearch.xpack.esql.core.expression.predicate.regex.StringPattern;
 import org.elasticsearch.xpack.esql.core.expression.predicate.regex.WildcardPatternList;
@@ -29,8 +30,8 @@ public class ReplaceStringCasingWithInsensitiveRegexMatch extends OptimizerRules
     @Override
     protected Expression rule(RegexMatch<? extends StringPattern> regexMatch, LogicalOptimizerContext unused) {
         Expression e = regexMatch;
-        if (regexMatch.pattern() instanceof WildcardPatternList) {
-            // This optimization is not supported for WildcardPatternList for now
+        if (regexMatch.pattern() instanceof WildcardPatternList || regexMatch.pattern() instanceof RLikePatternList) {
+            // This optimization is not supported for WildcardPatternList and RLikePatternList for now
             return e;
         }
         if (regexMatch.field() instanceof ChangeCase changeCase) {

File diff suppressed because it is too large
+ 0 - 0
x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/EsqlBaseParser.interp


File diff suppressed because it is too large
+ 419 - 341
x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/EsqlBaseParser.java


+ 12 - 0
x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/EsqlBaseParserBaseListener.java

@@ -980,6 +980,18 @@ public class EsqlBaseParserBaseListener implements EsqlBaseParserListener {
    * <p>The default implementation does nothing.</p>
    */
   @Override public void exitLikeListExpression(EsqlBaseParser.LikeListExpressionContext ctx) { }
+  /**
+   * {@inheritDoc}
+   *
+   * <p>The default implementation does nothing.</p>
+   */
+  @Override public void enterRlikeListExpression(EsqlBaseParser.RlikeListExpressionContext ctx) { }
+  /**
+   * {@inheritDoc}
+   *
+   * <p>The default implementation does nothing.</p>
+   */
+  @Override public void exitRlikeListExpression(EsqlBaseParser.RlikeListExpressionContext ctx) { }
   /**
    * {@inheritDoc}
    *

+ 7 - 0
x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/EsqlBaseParserBaseVisitor.java

@@ -580,6 +580,13 @@ public class EsqlBaseParserBaseVisitor<T> extends AbstractParseTreeVisitor<T> im
    * {@link #visitChildren} on {@code ctx}.</p>
    */
   @Override public T visitLikeListExpression(EsqlBaseParser.LikeListExpressionContext ctx) { return visitChildren(ctx); }
+  /**
+   * {@inheritDoc}
+   *
+   * <p>The default implementation returns the result of calling
+   * {@link #visitChildren} on {@code ctx}.</p>
+   */
+  @Override public T visitRlikeListExpression(EsqlBaseParser.RlikeListExpressionContext ctx) { return visitChildren(ctx); }
   /**
    * {@inheritDoc}
    *

+ 12 - 0
x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/EsqlBaseParserListener.java

@@ -855,6 +855,18 @@ public interface EsqlBaseParserListener extends ParseTreeListener {
    * @param ctx the parse tree
    */
   void exitLikeListExpression(EsqlBaseParser.LikeListExpressionContext ctx);
+  /**
+   * Enter a parse tree produced by the {@code rlikeListExpression}
+   * labeled alternative in {@link EsqlBaseParser#regexBooleanExpression}.
+   * @param ctx the parse tree
+   */
+  void enterRlikeListExpression(EsqlBaseParser.RlikeListExpressionContext ctx);
+  /**
+   * Exit a parse tree produced by the {@code rlikeListExpression}
+   * labeled alternative in {@link EsqlBaseParser#regexBooleanExpression}.
+   * @param ctx the parse tree
+   */
+  void exitRlikeListExpression(EsqlBaseParser.RlikeListExpressionContext ctx);
   /**
    * Enter a parse tree produced by {@link EsqlBaseParser#matchBooleanExpression}.
    * @param ctx the parse tree

+ 7 - 0
x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/EsqlBaseParserVisitor.java

@@ -518,6 +518,13 @@ public interface EsqlBaseParserVisitor<T> extends ParseTreeVisitor<T> {
    * @return the visitor result
    */
   T visitLikeListExpression(EsqlBaseParser.LikeListExpressionContext ctx);
+  /**
+   * Visit a parse tree produced by the {@code rlikeListExpression}
+   * labeled alternative in {@link EsqlBaseParser#regexBooleanExpression}.
+   * @param ctx the parse tree
+   * @return the visitor result
+   */
+  T visitRlikeListExpression(EsqlBaseParser.RlikeListExpressionContext ctx);
   /**
    * Visit a parse tree produced by {@link EsqlBaseParser#matchBooleanExpression}.
    * @param ctx the parse tree

+ 18 - 1
x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/ExpressionBuilder.java

@@ -30,6 +30,7 @@ import org.elasticsearch.xpack.esql.core.expression.UnresolvedAttribute;
 import org.elasticsearch.xpack.esql.core.expression.UnresolvedStar;
 import org.elasticsearch.xpack.esql.core.expression.function.Function;
 import org.elasticsearch.xpack.esql.core.expression.predicate.regex.RLikePattern;
+import org.elasticsearch.xpack.esql.core.expression.predicate.regex.RLikePatternList;
 import org.elasticsearch.xpack.esql.core.expression.predicate.regex.WildcardPattern;
 import org.elasticsearch.xpack.esql.core.expression.predicate.regex.WildcardPatternList;
 import org.elasticsearch.xpack.esql.core.tree.Source;
@@ -44,6 +45,7 @@ import org.elasticsearch.xpack.esql.expression.function.UnresolvedFunction;
 import org.elasticsearch.xpack.esql.expression.function.aggregate.FilteredExpression;
 import org.elasticsearch.xpack.esql.expression.function.fulltext.MatchOperator;
 import org.elasticsearch.xpack.esql.expression.function.scalar.string.regex.RLike;
+import org.elasticsearch.xpack.esql.expression.function.scalar.string.regex.RLikeList;
 import org.elasticsearch.xpack.esql.expression.function.scalar.string.regex.WildcardLike;
 import org.elasticsearch.xpack.esql.expression.function.scalar.string.regex.WildcardLikeList;
 import org.elasticsearch.xpack.esql.expression.predicate.logical.And;
@@ -748,7 +750,7 @@ public abstract class ExpressionBuilder extends IdentifierBuilder {
             RLike rLike = new RLike(source, left, new RLikePattern(BytesRefs.toString(patternLiteral.fold(FoldContext.small()))));
             return ctx.NOT() == null ? rLike : new Not(source, rLike);
         } catch (InvalidArgumentException e) {
-            throw new ParsingException(source, "Invalid pattern for LIKE [{}]: [{}]", patternLiteral, e.getMessage());
+            throw new ParsingException(source, "Invalid pattern for RLIKE [{}]: [{}]", patternLiteral, e.getMessage());
         }
     }
 
@@ -781,6 +783,21 @@ public abstract class ExpressionBuilder extends IdentifierBuilder {
         return ctx.NOT() == null ? e : new Not(source, e);
     }
 
+    @Override
+    public Expression visitRlikeListExpression(EsqlBaseParser.RlikeListExpressionContext ctx) {
+        Source source = source(ctx);
+        Expression left = expression(ctx.valueExpression());
+        List<RLikePattern> rLikePatterns = ctx.string()
+            .stream()
+            .map(x -> new RLikePattern(BytesRefs.toString(visitString(x).fold(FoldContext.small()))))
+            .toList();
+        // for now we will use the old WildcardLike function for one argument case to allow compatibility in mixed version deployments
+        Expression e = rLikePatterns.size() == 1
+            ? new RLike(source, left, rLikePatterns.getFirst())
+            : new RLikeList(source, left, new RLikePatternList(rLikePatterns));
+        return ctx.NOT() == null ? e : new Not(source, e);
+    }
+
     @Override
     public Order visitOrderExpression(EsqlBaseParser.OrderExpressionContext ctx) {
         return new Order(

+ 49 - 0
x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/RLikeListErrorTests.java

@@ -0,0 +1,49 @@
+/*
+ * 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.esql.expression.function.scalar.string;
+
+import org.elasticsearch.xpack.esql.core.expression.Expression;
+import org.elasticsearch.xpack.esql.core.tree.Source;
+import org.elasticsearch.xpack.esql.core.type.DataType;
+import org.elasticsearch.xpack.esql.expression.function.ErrorsForCasesWithoutExamplesTestCase;
+import org.elasticsearch.xpack.esql.expression.function.TestCaseSupplier;
+import org.hamcrest.Matcher;
+
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Stream;
+
+import static org.hamcrest.Matchers.equalTo;
+
+public class RLikeListErrorTests extends ErrorsForCasesWithoutExamplesTestCase {
+    @Override
+    protected List<TestCaseSupplier> cases() {
+        return paramsToSuppliers(RLikeListTests.parameters());
+    }
+
+    @Override
+    protected Stream<List<DataType>> testCandidates(List<TestCaseSupplier> cases, Set<List<DataType>> valid) {
+        /*
+         * We can't support certain signatures, and it's safe not to test them because
+         * you can't even build them.... The building comes directly from the parser
+         * and can only make certain types.
+         */
+        return super.testCandidates(cases, valid).filter(sig -> sig.get(1) == DataType.KEYWORD)
+            .filter(sig -> sig.size() > 2 && sig.get(2) == DataType.BOOLEAN);
+    }
+
+    @Override
+    protected Expression build(Source source, List<Expression> args) {
+        return RLikeTests.buildRLike(logger, source, args);
+    }
+
+    @Override
+    protected Matcher<String> expectedTypeErrorMatcher(List<Set<DataType>> validPerPosition, List<DataType> signature) {
+        return equalTo(typeErrorMessage(false, validPerPosition, signature, (v, p) -> "string"));
+    }
+}

+ 54 - 0
x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/RLikeListSerializationTests.java

@@ -0,0 +1,54 @@
+/*
+ * 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.esql.expression.function.scalar.string;
+
+import org.elasticsearch.xpack.esql.core.expression.Expression;
+import org.elasticsearch.xpack.esql.core.expression.predicate.regex.RLikePattern;
+import org.elasticsearch.xpack.esql.core.expression.predicate.regex.RLikePatternList;
+import org.elasticsearch.xpack.esql.core.tree.Source;
+import org.elasticsearch.xpack.esql.expression.AbstractExpressionSerializationTests;
+import org.elasticsearch.xpack.esql.expression.function.scalar.string.regex.RLikeList;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+public class RLikeListSerializationTests extends AbstractExpressionSerializationTests<RLikeList> {
+    @Override
+    protected RLikeList createTestInstance() {
+        Source source = randomSource();
+        Expression child = randomChild();
+        return new RLikeList(source, child, generateRandomPatternList());
+    }
+
+    @Override
+    protected RLikeList mutateInstance(RLikeList instance) throws IOException {
+        Source source = instance.source();
+        Expression child = instance.field();
+        List<RLikePattern> patterns = new ArrayList<>(instance.pattern().patternList());
+        int childToModify = randomIntBetween(0, patterns.size() - 1);
+        RLikePattern pattern = patterns.get(childToModify);
+        if (randomBoolean()) {
+            child = randomValueOtherThan(child, AbstractExpressionSerializationTests::randomChild);
+        } else {
+            pattern = randomValueOtherThan(pattern, () -> new RLikePattern(randomAlphaOfLength(4)));
+        }
+        patterns.set(childToModify, pattern);
+        return new RLikeList(source, child, new RLikePatternList(patterns));
+    }
+
+    private RLikePatternList generateRandomPatternList() {
+        int numChildren = randomIntBetween(1, 10); // Ensure at least one child
+        List<RLikePattern> patterns = new ArrayList<>(numChildren);
+        for (int i = 0; i < numChildren; i++) {
+            RLikePattern pattern = new RLikePattern(randomAlphaOfLength(4));
+            patterns.add(pattern);
+        }
+        return new RLikePatternList(patterns);
+    }
+}

+ 206 - 0
x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/RLikeListTests.java

@@ -0,0 +1,206 @@
+/*
+ * 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.esql.expression.function.scalar.string;
+
+import com.carrotsearch.randomizedtesting.annotations.Name;
+import com.carrotsearch.randomizedtesting.annotations.ParametersFactory;
+
+import org.apache.logging.log4j.Logger;
+import org.apache.lucene.util.BytesRef;
+import org.elasticsearch.xpack.esql.core.expression.Expression;
+import org.elasticsearch.xpack.esql.core.expression.FoldContext;
+import org.elasticsearch.xpack.esql.core.expression.Literal;
+import org.elasticsearch.xpack.esql.core.expression.predicate.regex.RLikePattern;
+import org.elasticsearch.xpack.esql.core.expression.predicate.regex.RLikePatternList;
+import org.elasticsearch.xpack.esql.core.tree.Source;
+import org.elasticsearch.xpack.esql.core.type.DataType;
+import org.elasticsearch.xpack.esql.expression.function.AbstractScalarFunctionTestCase;
+import org.elasticsearch.xpack.esql.expression.function.TestCaseSupplier;
+import org.elasticsearch.xpack.esql.expression.function.scalar.string.regex.RLike;
+import org.elasticsearch.xpack.esql.expression.function.scalar.string.regex.RLikeList;
+import org.junit.AfterClass;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.function.Supplier;
+
+import static org.elasticsearch.xpack.esql.core.util.TestUtils.randomCasing;
+import static org.elasticsearch.xpack.esql.expression.function.DocsV3Support.renderNegatedOperator;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.nullValue;
+import static org.hamcrest.Matchers.startsWith;
+
+public class RLikeListTests extends AbstractScalarFunctionTestCase {
+    public RLikeListTests(@Name("TestCase") Supplier<TestCaseSupplier.TestCase> testCaseSupplier) {
+        this.testCase = testCaseSupplier.get();
+    }
+
+    @ParametersFactory
+    public static Iterable<Object[]> parameters() {
+        final Function<String, String> escapeString = str -> {
+            for (String syntax : new String[] { "\\", ".", "?", "+", "*", "|", "{", "}", "[", "]", "(", ")", "\"", "<", ">", "#", "&" }) {
+                str = str.replace(syntax, "\\" + syntax);
+            }
+            return str;
+        };
+        return parameters(escapeString, () -> randomAlphaOfLength(1) + "?");
+    }
+
+    static Iterable<Object[]> parameters(Function<String, String> escapeString, Supplier<String> optionalPattern) {
+        List<TestCaseSupplier> cases = new ArrayList<>();
+        cases.add(
+            new TestCaseSupplier(
+                "null",
+                List.of(DataType.NULL, DataType.KEYWORD, DataType.BOOLEAN),
+                () -> new TestCaseSupplier.TestCase(
+                    List.of(
+                        new TestCaseSupplier.TypedData(null, DataType.NULL, "e"),
+                        new TestCaseSupplier.TypedData(new BytesRef(randomAlphaOfLength(10)), DataType.KEYWORD, "pattern").forceLiteral(),
+                        new TestCaseSupplier.TypedData(false, DataType.BOOLEAN, "caseInsensitive").forceLiteral()
+                    ),
+                    "LiteralsEvaluator[lit=null]",
+                    DataType.BOOLEAN,
+                    nullValue()
+                )
+            )
+        );
+        casesForString(cases, "empty string", () -> "", false, escapeString, optionalPattern);
+        casesForString(cases, "single ascii character", () -> randomAlphaOfLength(1), true, escapeString, optionalPattern);
+        casesForString(cases, "ascii string", () -> randomAlphaOfLengthBetween(2, 100), true, escapeString, optionalPattern);
+        casesForString(cases, "3 bytes, 1 code point", () -> "☕", false, escapeString, optionalPattern);
+        casesForString(cases, "6 bytes, 2 code points", () -> "❗️", false, escapeString, optionalPattern);
+        casesForString(cases, "100 random code points", () -> randomUnicodeOfCodepointLength(100), true, escapeString, optionalPattern);
+        return parameterSuppliersFromTypedData(cases);
+    }
+
+    record TextAndPattern(String text, String pattern) {}
+
+    private static void casesForString(
+        List<TestCaseSupplier> cases,
+        String title,
+        Supplier<String> textSupplier,
+        boolean canGenerateDifferent,
+        Function<String, String> escapeString,
+        Supplier<String> optionalPattern
+    ) {
+        cases(cases, title + " matches self", () -> {
+            String text = textSupplier.get();
+            return new TextAndPattern(text, escapeString.apply(text));
+        }, true);
+        cases(cases, title + " matches self case insensitive", () -> {
+            // RegExp doesn't support case-insensitive matching for Unicodes whose length changes when the case changes.
+            // Example: a case-insensitive ES regexp query for the pattern `weiß` won't match the value `WEISS` (but will match `WEIß`).
+            // Or `ʼn` (U+0149) vs. `ʼN` (U+02BC U+004E).
+            String text, caseChanged;
+            for (text = textSupplier.get(), caseChanged = randomCasing(text); text.length() != caseChanged.length();) {
+                text = textSupplier.get();
+                caseChanged = randomCasing(text);
+            }
+            return new TextAndPattern(caseChanged, escapeString.apply(text));
+        }, true, true);
+        cases(cases, title + " doesn't match self with trailing", () -> {
+            String text = textSupplier.get();
+            return new TextAndPattern(text, escapeString.apply(text) + randomAlphaOfLength(1));
+        }, false);
+        cases(cases, title + " doesn't match self with trailing case insensitive", () -> {
+            String text = textSupplier.get();
+            return new TextAndPattern(randomCasing(text), escapeString.apply(text) + randomAlphaOfLength(1));
+        }, true, false);
+        cases(cases, title + " matches self with optional trailing", () -> {
+            String text = randomAlphaOfLength(1);
+            return new TextAndPattern(text, escapeString.apply(text) + optionalPattern.get());
+        }, true);
+        cases(cases, title + " matches self with optional trailing case insensitive", () -> {
+            String text = randomAlphaOfLength(1);
+            return new TextAndPattern(randomCasing(text), escapeString.apply(text) + optionalPattern.get());
+        }, true, true);
+        if (canGenerateDifferent) {
+            cases(cases, title + " doesn't match different", () -> {
+                String text = textSupplier.get();
+                String different = escapeString.apply(randomValueOtherThan(text, textSupplier));
+                return new TextAndPattern(text, different);
+            }, false);
+            cases(cases, title + " doesn't match different case insensitive", () -> {
+                String text = textSupplier.get();
+                Predicate<String> predicate = t -> t.toLowerCase(Locale.ROOT).equals(text.toLowerCase(Locale.ROOT));
+                String different = escapeString.apply(randomValueOtherThanMany(predicate, textSupplier));
+                return new TextAndPattern(text, different);
+            }, true, false);
+        }
+    }
+
+    private static void cases(List<TestCaseSupplier> cases, String title, Supplier<TextAndPattern> textAndPattern, boolean expected) {
+        cases(cases, title, textAndPattern, false, expected);
+    }
+
+    private static void cases(
+        List<TestCaseSupplier> cases,
+        String title,
+        Supplier<TextAndPattern> textAndPattern,
+        boolean caseInsensitive,
+        boolean expected
+    ) {
+        for (DataType type : DataType.stringTypes()) {
+            cases.add(new TestCaseSupplier(title + " with " + type.esType(), List.of(type, DataType.KEYWORD, DataType.BOOLEAN), () -> {
+                TextAndPattern v = textAndPattern.get();
+                return new TestCaseSupplier.TestCase(
+                    List.of(
+                        new TestCaseSupplier.TypedData(new BytesRef(v.text), type, "e"),
+                        new TestCaseSupplier.TypedData(new BytesRef(v.pattern), DataType.KEYWORD, "pattern").forceLiteral(),
+                        new TestCaseSupplier.TypedData(caseInsensitive, DataType.BOOLEAN, "caseInsensitive").forceLiteral()
+                    ),
+                    startsWith("AutomataMatchEvaluator[input=Attribute[channel=0], pattern=digraph Automaton {\n"),
+                    DataType.BOOLEAN,
+                    equalTo(expected)
+                );
+            }));
+            if (caseInsensitive == false) {
+                cases.add(new TestCaseSupplier(title + " with " + type.esType(), List.of(type, DataType.KEYWORD), () -> {
+                    TextAndPattern v = textAndPattern.get();
+                    return new TestCaseSupplier.TestCase(
+                        List.of(
+                            new TestCaseSupplier.TypedData(new BytesRef(v.text), type, "e"),
+                            new TestCaseSupplier.TypedData(new BytesRef(v.pattern), DataType.KEYWORD, "pattern").forceLiteral()
+                        ),
+                        startsWith("AutomataMatchEvaluator[input=Attribute[channel=0], pattern=digraph Automaton {\n"),
+                        DataType.BOOLEAN,
+                        equalTo(expected)
+                    );
+                }));
+            }
+        }
+    }
+
+    @Override
+    protected Expression build(Source source, List<Expression> args) {
+        return buildRLikeList(logger, source, args);
+    }
+
+    static Expression buildRLikeList(Logger logger, Source source, List<Expression> args) {
+        Expression expression = args.get(0);
+        Literal pattern = (Literal) args.get(1);
+        Literal caseInsensitive = args.size() > 2 ? (Literal) args.get(2) : null;
+        String patternString = ((BytesRef) pattern.fold(FoldContext.small())).utf8ToString();
+        boolean caseInsensitiveBool = caseInsensitive != null ? (boolean) caseInsensitive.fold(FoldContext.small()) : false;
+        logger.info("pattern={} caseInsensitive={}", patternString, caseInsensitiveBool);
+
+        return caseInsensitiveBool
+            ? new RLikeList(source, expression, new RLikePatternList(List.of(new RLikePattern(patternString))), true)
+            : (randomBoolean()
+                ? new RLikeList(source, expression, new RLikePatternList(List.of(new RLikePattern(patternString))))
+                : new RLikeList(source, expression, new RLikePatternList(List.of(new RLikePattern(patternString))), false));
+    }
+
+    @AfterClass
+    public static void renderNotRLike() throws Exception {
+        renderNegatedOperator(constructorWithFunctionInfo(RLike.class), "RLIKE", d -> d, getTestClass());
+    }
+}

+ 1 - 1
x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/parser/StatementParserTests.java

@@ -1227,7 +1227,7 @@ public class StatementParserTests extends AbstractStatementParserTests {
         assertEquals(".*bar.*", rlike.pattern().asJavaRegex());
 
         expectError("from a | where foo like 12", "no viable alternative at input 'foo like 12'");
-        expectError("from a | where foo rlike 12", "mismatched input '12'");
+        expectError("from a | where foo rlike 12", "no viable alternative at input 'foo rlike 12'");
 
         expectError(
             "from a | where foo like \"(?i)(^|[^a-zA-Z0-9_-])nmap($|\\\\.)\"",

Some files were not shown because too many files changed in this diff