Skip to content

Commit de38127

Browse files
authored
EQL: implement stringContains function (#54380) (#54923)
1 parent c7053ef commit de38127

File tree

13 files changed

+485
-58
lines changed

13 files changed

+485
-58
lines changed

x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/expression/function/EqlFunctionRegistry.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import org.elasticsearch.xpack.eql.expression.function.scalar.string.Length;
1212
import org.elasticsearch.xpack.eql.expression.function.scalar.string.StartsWith;
1313
import org.elasticsearch.xpack.eql.expression.function.scalar.string.Substring;
14+
import org.elasticsearch.xpack.eql.expression.function.scalar.string.StringContains;
1415
import org.elasticsearch.xpack.eql.expression.function.scalar.string.Wildcard;
1516
import org.elasticsearch.xpack.ql.expression.function.FunctionDefinition;
1617
import org.elasticsearch.xpack.ql.expression.function.FunctionRegistry;
@@ -32,6 +33,7 @@ private static FunctionDefinition[][] functions() {
3233
def(EndsWith.class, EndsWith::new, "endswith"),
3334
def(Length.class, Length::new, "length"),
3435
def(StartsWith.class, StartsWith::new, "startswith"),
36+
def(StringContains.class, StringContains::new, "stringcontains"),
3537
def(Substring.class, Substring::new, "substring"),
3638
def(Wildcard.class, Wildcard::new, "wildcard"),
3739
}
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
package org.elasticsearch.xpack.eql.expression.function.scalar.string;
8+
9+
import org.elasticsearch.xpack.ql.expression.Expression;
10+
import org.elasticsearch.xpack.ql.expression.Expressions;
11+
import org.elasticsearch.xpack.ql.expression.FieldAttribute;
12+
import org.elasticsearch.xpack.ql.expression.function.scalar.ScalarFunction;
13+
import org.elasticsearch.xpack.ql.expression.gen.pipeline.Pipe;
14+
import org.elasticsearch.xpack.ql.expression.gen.script.ScriptTemplate;
15+
import org.elasticsearch.xpack.ql.expression.gen.script.Scripts;
16+
import org.elasticsearch.xpack.ql.tree.NodeInfo;
17+
import org.elasticsearch.xpack.ql.tree.Source;
18+
import org.elasticsearch.xpack.ql.type.DataType;
19+
import org.elasticsearch.xpack.ql.type.DataTypes;
20+
21+
import java.util.Arrays;
22+
import java.util.List;
23+
import java.util.Locale;
24+
25+
import static java.lang.String.format;
26+
import static org.elasticsearch.xpack.eql.expression.function.scalar.string.StringContainsFunctionProcessor.doProcess;
27+
import static org.elasticsearch.xpack.ql.expression.TypeResolutions.isStringAndExact;
28+
import static org.elasticsearch.xpack.ql.expression.gen.script.ParamsBuilder.paramsBuilder;
29+
30+
/**
31+
* EQL specific stringContains function.
32+
* stringContains(a, b)
33+
* Returns true if b is a substring of a
34+
*/
35+
public class StringContains extends ScalarFunction {
36+
37+
private final Expression string, substring;
38+
39+
public StringContains(Source source, Expression string, Expression substring) {
40+
super(source, Arrays.asList(string, substring));
41+
this.string = string;
42+
this.substring = substring;
43+
}
44+
45+
@Override
46+
protected TypeResolution resolveType() {
47+
if (!childrenResolved()) {
48+
return new TypeResolution("Unresolved children");
49+
}
50+
51+
TypeResolution resolution = isStringAndExact(string, sourceText(), Expressions.ParamOrdinal.FIRST);
52+
if (resolution.unresolved()) {
53+
return resolution;
54+
}
55+
56+
return isStringAndExact(substring, sourceText(), Expressions.ParamOrdinal.SECOND);
57+
}
58+
59+
@Override
60+
protected Pipe makePipe() {
61+
return new StringContainsFunctionPipe(source(), this,
62+
Expressions.pipe(string), Expressions.pipe(substring));
63+
}
64+
65+
@Override
66+
public boolean foldable() {
67+
return string.foldable() && substring.foldable();
68+
}
69+
70+
@Override
71+
public Object fold() {
72+
return doProcess(string.fold(), substring.fold());
73+
}
74+
75+
@Override
76+
protected NodeInfo<? extends Expression> info() {
77+
return NodeInfo.create(this, StringContains::new, string, substring);
78+
}
79+
80+
@Override
81+
public ScriptTemplate asScript() {
82+
return asScriptFrom(asScript(string), asScript(substring));
83+
}
84+
85+
protected ScriptTemplate asScriptFrom(ScriptTemplate stringScript, ScriptTemplate substringScript) {
86+
return new ScriptTemplate(format(Locale.ROOT, formatTemplate("{eql}.%s(%s,%s)"),
87+
"stringContains",
88+
stringScript.template(),
89+
substringScript.template()),
90+
paramsBuilder()
91+
.script(stringScript.params())
92+
.script(substringScript.params())
93+
.build(), dataType());
94+
}
95+
96+
@Override
97+
public ScriptTemplate scriptWithField(FieldAttribute field) {
98+
return new ScriptTemplate(processScript(Scripts.DOC_VALUE),
99+
paramsBuilder().variable(field.exactAttribute().name()).build(),
100+
dataType());
101+
}
102+
103+
@Override
104+
public DataType dataType() {
105+
return DataTypes.BOOLEAN;
106+
}
107+
108+
@Override
109+
public Expression replaceChildren(List<Expression> newChildren) {
110+
if (newChildren.size() != 2) {
111+
throw new IllegalArgumentException("expected [2] children but received [" + newChildren.size() + "]");
112+
}
113+
114+
return new StringContains(source(), newChildren.get(0), newChildren.get(1));
115+
}
116+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
package org.elasticsearch.xpack.eql.expression.function.scalar.string;
8+
9+
import org.elasticsearch.xpack.ql.execution.search.QlSourceBuilder;
10+
import org.elasticsearch.xpack.ql.expression.Expression;
11+
import org.elasticsearch.xpack.ql.expression.gen.pipeline.Pipe;
12+
import org.elasticsearch.xpack.ql.tree.NodeInfo;
13+
import org.elasticsearch.xpack.ql.tree.Source;
14+
15+
import java.util.Arrays;
16+
import java.util.List;
17+
import java.util.Objects;
18+
19+
public class StringContainsFunctionPipe extends Pipe {
20+
21+
private final Pipe string, substring;
22+
23+
public StringContainsFunctionPipe(Source source, Expression expression, Pipe string, Pipe substring) {
24+
super(source, expression, Arrays.asList(string, substring));
25+
this.string = string;
26+
this.substring = substring;
27+
}
28+
29+
@Override
30+
public final Pipe replaceChildren(List<Pipe> newChildren) {
31+
if (newChildren.size() != 2) {
32+
throw new IllegalArgumentException("expected [2] children but received [" + newChildren.size() + "]");
33+
}
34+
return replaceChildren(newChildren.get(0), newChildren.get(1));
35+
}
36+
37+
@Override
38+
public final Pipe resolveAttributes(AttributeResolver resolver) {
39+
Pipe newString = string.resolveAttributes(resolver);
40+
Pipe newSubstring = substring.resolveAttributes(resolver);
41+
if (newString == string && newSubstring == substring) {
42+
return this;
43+
}
44+
return replaceChildren(newString, newSubstring);
45+
}
46+
47+
@Override
48+
public boolean supportedByAggsOnlyQuery() {
49+
return string.supportedByAggsOnlyQuery() && substring.supportedByAggsOnlyQuery();
50+
}
51+
52+
@Override
53+
public boolean resolved() {
54+
return string.resolved() && substring.resolved();
55+
}
56+
57+
protected Pipe replaceChildren(Pipe string, Pipe substring) {
58+
return new StringContainsFunctionPipe(source(), expression(), string, substring);
59+
}
60+
61+
@Override
62+
public final void collectFields(QlSourceBuilder sourceBuilder) {
63+
string.collectFields(sourceBuilder);
64+
substring.collectFields(sourceBuilder);
65+
}
66+
67+
@Override
68+
protected NodeInfo<StringContainsFunctionPipe> info() {
69+
return NodeInfo.create(this, StringContainsFunctionPipe::new, expression(), string, substring);
70+
}
71+
72+
@Override
73+
public StringContainsFunctionProcessor asProcessor() {
74+
return new StringContainsFunctionProcessor(string.asProcessor(), substring.asProcessor());
75+
}
76+
77+
public Pipe string() {
78+
return string;
79+
}
80+
81+
public Pipe substring() {
82+
return substring;
83+
}
84+
85+
86+
@Override
87+
public int hashCode() {
88+
return Objects.hash(source(), string(), substring());
89+
}
90+
91+
@Override
92+
public boolean equals(Object obj) {
93+
if (this == obj) {
94+
return true;
95+
}
96+
97+
if (obj == null || getClass() != obj.getClass()) {
98+
return false;
99+
}
100+
101+
StringContainsFunctionPipe other = (StringContainsFunctionPipe) obj;
102+
return Objects.equals(source(), other.source())
103+
&& Objects.equals(string(), other.string())
104+
&& Objects.equals(substring(), other.substring());
105+
}
106+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
package org.elasticsearch.xpack.eql.expression.function.scalar.string;
8+
9+
import org.elasticsearch.common.io.stream.StreamInput;
10+
import org.elasticsearch.common.io.stream.StreamOutput;
11+
import org.elasticsearch.xpack.eql.EqlIllegalArgumentException;
12+
import org.elasticsearch.xpack.ql.expression.gen.processor.Processor;
13+
14+
import java.io.IOException;
15+
import java.util.Objects;
16+
17+
public class StringContainsFunctionProcessor implements Processor {
18+
19+
public static final String NAME = "sstc";
20+
21+
private final Processor string, substring;
22+
23+
public StringContainsFunctionProcessor(Processor string, Processor substring) {
24+
this.string = string;
25+
this.substring = substring;
26+
}
27+
28+
public StringContainsFunctionProcessor(StreamInput in) throws IOException {
29+
string = in.readNamedWriteable(Processor.class);
30+
substring = in.readNamedWriteable(Processor.class);
31+
}
32+
33+
@Override
34+
public final void writeTo(StreamOutput out) throws IOException {
35+
out.writeNamedWriteable(string);
36+
out.writeNamedWriteable(substring);
37+
}
38+
39+
@Override
40+
public Object process(Object input) {
41+
return doProcess(string.process(input), substring.process(input));
42+
}
43+
44+
public static Object doProcess(Object string, Object substring) {
45+
if (string == null) {
46+
return null;
47+
}
48+
49+
throwIfNotString(string);
50+
throwIfNotString(substring);
51+
52+
String strString = string.toString();
53+
String strSubstring = substring.toString();
54+
return StringUtils.stringContains(strString, strSubstring);
55+
}
56+
57+
private static void throwIfNotString(Object obj) {
58+
if (!(obj instanceof String || obj instanceof Character)) {
59+
throw new EqlIllegalArgumentException("A string/char is required; received [{}]", obj);
60+
}
61+
}
62+
63+
protected Processor string() {
64+
return string;
65+
}
66+
67+
public Processor substring() {
68+
return substring;
69+
}
70+
71+
@Override
72+
public boolean equals(Object obj) {
73+
if (this == obj) {
74+
return true;
75+
}
76+
77+
if (obj == null || getClass() != obj.getClass()) {
78+
return false;
79+
}
80+
81+
StringContainsFunctionProcessor other = (StringContainsFunctionProcessor) obj;
82+
return Objects.equals(string(), other.string())
83+
&& Objects.equals(substring(), other.substring());
84+
}
85+
86+
@Override
87+
public int hashCode() {
88+
return Objects.hash(string(), substring());
89+
}
90+
91+
92+
@Override
93+
public String getWriteableName() {
94+
return NAME;
95+
}
96+
}

x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/expression/function/scalar/string/StringUtils.java

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,23 @@ static String between(String string, String left, String right, boolean greedy,
6060
return string.substring(start, idx);
6161
}
6262

63+
/**
64+
* Checks if {@code string} contains {@code substring} string.
65+
*
66+
* @param string string to search through.
67+
* @param substring string to search for.
68+
* @return {@code true} if {@code string} string contains {@code substring} string.
69+
*/
70+
static boolean stringContains(String string, String substring) {
71+
if (hasLength(string) == false || hasLength(substring) == false) {
72+
return false;
73+
}
74+
75+
string = string.toLowerCase(Locale.ROOT);
76+
substring = substring.toLowerCase(Locale.ROOT);
77+
return string.contains(substring);
78+
}
79+
6380
/**
6481
* Returns a substring using the Python slice semantics, meaning
6582
* start and end can be negative
@@ -70,7 +87,7 @@ static String substringSlice(String string, int start, int end) {
7087
}
7188

7289
int length = string.length();
73-
90+
7491
// handle first negative values
7592
if (start < 0) {
7693
start += length;

x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/expression/function/scalar/string/Substring.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,4 +127,4 @@ public Expression replaceChildren(List<Expression> newChildren) {
127127

128128
return new Substring(source(), newChildren.get(0), newChildren.get(1), newChildren.get(2));
129129
}
130-
}
130+
}

x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/expression/function/scalar/whitelist/InternalEqlScriptUtils.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import org.elasticsearch.xpack.eql.expression.function.scalar.string.LengthFunctionProcessor;
1212
import org.elasticsearch.xpack.eql.expression.function.scalar.string.StartsWithFunctionProcessor;
1313
import org.elasticsearch.xpack.eql.expression.function.scalar.string.SubstringFunctionProcessor;
14+
import org.elasticsearch.xpack.eql.expression.function.scalar.string.StringContainsFunctionProcessor;
1415
import org.elasticsearch.xpack.ql.expression.function.scalar.whitelist.InternalQlScriptUtils;
1516

1617
/*
@@ -38,6 +39,10 @@ public static Boolean startsWith(String s, String pattern) {
3839
return (Boolean) StartsWithFunctionProcessor.doProcess(s, pattern);
3940
}
4041

42+
public static Boolean stringContains(String string, String substring) {
43+
return (Boolean) StringContainsFunctionProcessor.doProcess(string, substring);
44+
}
45+
4146
public static String substring(String s, Number start, Number end) {
4247
return (String) SubstringFunctionProcessor.doProcess(s, start, end);
4348
}

0 commit comments

Comments
 (0)