Skip to content
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
72e72e4
New Func Added
julian-elastic Jun 10, 2025
0518bbc
Add more tests and fix bug
julian-elastic Jun 11, 2025
f39ef12
Switch to old function where possible, add more UTs
julian-elastic Jun 11, 2025
495d6b8
[CI] Auto commit changes from spotless
Jun 11, 2025
561d325
Merge branch 'main' into first
julian-elastic Jun 12, 2025
c2c7274
Regenerate parse files after merge
julian-elastic Jun 12, 2025
683da23
Fix merge error
julian-elastic Jun 12, 2025
d5e8071
Add more unit tests, fix bugs
julian-elastic Jun 12, 2025
6c6760e
Change grammar to use string instead of valueExpression to pass unit …
julian-elastic Jun 12, 2025
099d1cf
Code cleanup
julian-elastic Jun 12, 2025
9d6a6b0
Documentation, More UT, Bugfix
julian-elastic Jun 13, 2025
8020ce6
Add more comments
julian-elastic Jun 16, 2025
9dcf413
Merge branch 'main' into first
julian-elastic Jun 16, 2025
59095ff
Fix merge conflicts
julian-elastic Jun 16, 2025
7a02bb7
Update docs/changelog/129170.yaml
julian-elastic Jun 16, 2025
78eb25f
Address code review feedback
julian-elastic Jun 16, 2025
ea84211
Merge remote-tracking branch 'origin/first' into first
julian-elastic Jun 16, 2025
4980b1b
[CI] Auto commit changes from spotless
Jun 16, 2025
931b7c6
Merge remote-tracking branch 'origin/main' into first
julian-elastic Jun 16, 2025
408fba0
Merge remote-tracking branch 'origin/first' into first
julian-elastic Jun 16, 2025
dbc6f30
Address more code review feedback
julian-elastic Jun 16, 2025
72d4531
Merge branch 'main' into first
julian-elastic Jun 17, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions docs/changelog/129170.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
pr: 129170
summary: Add Support for LIKE (LIST)
area: ES|QL
type: enhancement
issues: []
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ ROW message = "foo * bar"
```


```esql
ROW message = "foobar"
| WHERE message like ("foo*", "bar?")
```


To reduce the overhead of escaping, we suggest using triple quotes strings `"""`

```esql
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,12 @@
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 org.elasticsearch.xpack.esql.core.util.StringUtils;

import java.io.IOException;
import java.util.Objects;

import static org.elasticsearch.xpack.esql.core.util.StringUtils.luceneWildcardToRegExp;
Expand All @@ -24,7 +28,7 @@
* Allows escaping based on a regular char
*
*/
public class WildcardPattern extends AbstractStringPattern {
public class WildcardPattern extends AbstractStringPattern implements Writeable {

private final String wildcard;
private final String regex;
Expand All @@ -35,6 +39,15 @@ public WildcardPattern(String pattern) {
this.regex = StringUtils.wildcardToJavaPattern(pattern, '\\');
}

public WildcardPattern(StreamInput in) throws IOException {
this(in.readString());
}

@Override
public void writeTo(StreamOutput out) throws IOException {
out.writeString(wildcard);
}

public String pattern() {
return wildcard;
}
Expand Down Expand Up @@ -87,4 +100,5 @@ public boolean equals(Object obj) {
WildcardPattern other = (WildcardPattern) obj;
return Objects.equals(wildcard, other.wildcard);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
/*
* 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;

/**
* A list of wildcard patterns. Each pattern is a {@link WildcardPattern} that can be used to match strings and is
* similar to basic regex, supporting '?' wildcard for single character (same as regex ".")
* and '*' wildcard for multiple characters (same as regex ".*")
* <p>
* Allows escaping based on a regular char
*
*/
public class WildcardPatternList extends AbstractStringPattern implements Writeable {
public static final String NAME = "WildcardPatternList";
private final List<WildcardPattern> patternList;

public WildcardPatternList(List<WildcardPattern> patterns) {
this.patternList = patterns;
}

public WildcardPatternList(StreamInput in) throws IOException {
this(in.readCollectionAsList(WildcardPattern::new));
}

@Override
public void writeTo(StreamOutput out) throws IOException {
out.writeCollection(patternList, (o, pattern) -> pattern.writeTo(o));
}

public static WildcardPatternList readFrom(StreamInput in) throws IOException {
return new WildcardPatternList(in);
}

public List<WildcardPattern> 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 automata 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(WildcardPattern::asJavaRegex).collect(Collectors.joining("|"));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like we never us Java's regex pattern with this - it's just a description string. In a follow up, can you rename this to, like description?

}

/**
* Returns a string that matches any of the patterns in the list.
* The patterns are joined with the '|' operator to create a single wildcard string.
*/
@Override
public String pattern() {
if (patternList.isEmpty()) {
return "";
}
if (patternList.size() == 1) {
return patternList.getFirst().pattern();
}
return "(\"" + patternList.stream().map(WildcardPattern::pattern).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;
}

WildcardPatternList other = (WildcardPatternList) obj;
return patternList.equals(other.patternList);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,42 @@ public void testCaseInsensitiveEquality() throws IOException {
testPushQuery(value, esqlQuery, List.of(luceneQuery), dataNodeSignature, true);
}

public void testLike() throws IOException {
String value = "v".repeat(between(0, 256));
String esqlQuery = """
FROM test
| WHERE test like "%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 testLikeList() throws IOException {
String value = "v".repeat(between(0, 256));
String esqlQuery = """
FROM test
| WHERE test like ("%value*", "abc*")
""";
String luceneQuery = switch (type) {
case KEYWORD, 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 -> ComputeSignature.FILTER_IN_QUERY;
case AUTO, KEYWORD, 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")
Expand Down
Loading
Loading