Skip to content

Commit 6060269

Browse files
Get basic case to work
1 parent aaee222 commit 6060269

File tree

8 files changed

+363
-5
lines changed

8 files changed

+363
-5
lines changed

server/src/main/java/org/elasticsearch/index/mapper/ConstantFieldType.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,10 @@ public final Query wildcardQuery(String value, boolean caseInsensitive, QueryRew
135135
}
136136
}
137137

138+
public Query wildcardLikeQuery(String value, boolean caseInsensitive, QueryRewriteContext context) {
139+
return wildcardQuery(value, caseInsensitive, context);
140+
}
141+
138142
@Override
139143
public final boolean fieldHasValue(FieldInfos fieldInfos) {
140144
// We consider constant field types to always have value.

server/src/main/java/org/elasticsearch/index/mapper/IndexFieldMapper.java

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,13 @@
1010
package org.elasticsearch.index.mapper;
1111

1212
import org.apache.lucene.search.MatchAllDocsQuery;
13+
import org.apache.lucene.search.MatchNoDocsQuery;
14+
import org.apache.lucene.search.MultiTermQuery;
1315
import org.apache.lucene.search.Query;
1416
import org.apache.lucene.util.BytesRef;
1517
import org.elasticsearch.common.Strings;
18+
import org.elasticsearch.common.regex.Regex;
19+
import org.elasticsearch.core.Nullable;
1620
import org.elasticsearch.index.fielddata.FieldData;
1721
import org.elasticsearch.index.fielddata.FieldDataContext;
1822
import org.elasticsearch.index.fielddata.IndexFieldData;
@@ -27,6 +31,7 @@
2731

2832
import java.util.Collections;
2933
import java.util.List;
34+
import java.util.Locale;
3035

3136
public class IndexFieldMapper extends MetadataFieldMapper {
3237

@@ -102,6 +107,33 @@ public StoredFieldsSpec storedFieldsSpec() {
102107
};
103108
}
104109

110+
@Override
111+
public Query wildcardLikeQuery(
112+
String value,
113+
@Nullable MultiTermQuery.RewriteMethod method,
114+
boolean caseInsensitve,
115+
SearchExecutionContext context
116+
) {
117+
String indexName = context.getFullyQualifiedIndex().getName();
118+
return getWildcardLikeQuery(value, caseInsensitve, indexName);
119+
}
120+
121+
private static Query getWildcardLikeQuery(String value, boolean caseInsensitve, String indexName) {
122+
if (caseInsensitve) {
123+
value = value.toLowerCase(Locale.ROOT);
124+
indexName = indexName.toLowerCase(Locale.ROOT);
125+
}
126+
if (Regex.simpleMatch(value, indexName)) {
127+
return new MatchAllDocsQuery();
128+
}
129+
return new MatchNoDocsQuery("The \"" + indexName + "\" query was rewritten to a \"match_none\" query.");
130+
}
131+
132+
@Override
133+
public Query wildcardLikeQuery(String value, boolean caseInsensitive, QueryRewriteContext context) {
134+
String indexName = context.getFullyQualifiedIndex().getName();
135+
return getWildcardLikeQuery(value, caseInsensitive, indexName);
136+
}
105137
}
106138

107139
public IndexFieldMapper() {

server/src/main/java/org/elasticsearch/index/mapper/MappedFieldType.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -328,7 +328,14 @@ public Query prefixQuery(
328328
public final Query wildcardQuery(String value, @Nullable MultiTermQuery.RewriteMethod method, SearchExecutionContext context) {
329329
return wildcardQuery(value, method, false, context);
330330
}
331-
331+
public Query wildcardLikeQuery(
332+
String value,
333+
@Nullable MultiTermQuery.RewriteMethod method,
334+
boolean caseInsensitve,
335+
SearchExecutionContext context
336+
){
337+
return wildcardQuery(value, method, caseInsensitve, context);
338+
}
332339
public Query wildcardQuery(
333340
String value,
334341
@Nullable MultiTermQuery.RewriteMethod method,

server/src/main/java/org/elasticsearch/index/query/AutomatonQueryBuilder.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,11 @@ public String getWriteableName() {
5353
throw new UnsupportedOperationException("AutomatonQueryBuilder does not support getWriteableName");
5454
}
5555

56+
@Override
57+
public String getName() {
58+
return "AutomatonQueryBuilder";
59+
}
60+
5661
@Override
5762
protected void doWriteTo(StreamOutput out) throws IOException {
5863
throw new UnsupportedEncodingException("AutomatonQueryBuilder does not support doWriteTo");
Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
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+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
10+
package org.elasticsearch.index.query;
11+
12+
import org.apache.lucene.search.MatchAllDocsQuery;
13+
import org.apache.lucene.search.MatchNoDocsQuery;
14+
import org.apache.lucene.search.MultiTermQuery;
15+
import org.apache.lucene.search.Query;
16+
import org.elasticsearch.TransportVersion;
17+
import org.elasticsearch.TransportVersions;
18+
import org.elasticsearch.common.Strings;
19+
import org.elasticsearch.common.io.stream.StreamInput;
20+
import org.elasticsearch.common.io.stream.StreamOutput;
21+
import org.elasticsearch.common.xcontent.LoggingDeprecationHandler;
22+
import org.elasticsearch.core.Nullable;
23+
import org.elasticsearch.index.mapper.ConstantFieldType;
24+
import org.elasticsearch.index.mapper.MappedFieldType;
25+
import org.elasticsearch.index.query.support.QueryParsers;
26+
import org.elasticsearch.xcontent.ParseField;
27+
import org.elasticsearch.xcontent.XContentBuilder;
28+
import org.elasticsearch.xcontent.XContentParser;
29+
30+
import java.io.IOException;
31+
import java.util.Objects;
32+
33+
/**
34+
* Implements the wildcard search query. Supported wildcards are {@code *}, which
35+
* matches any character sequence (including the empty one), and {@code ?},
36+
* which matches any single character. Note this query can be slow, as it
37+
* needs to iterate over many terms. In order to prevent extremely slow WildcardQueries,
38+
* a Wildcard term should not start with one of the wildcards {@code *} or
39+
* {@code ?}.
40+
*/
41+
public class WildcardLikeQueryBuilder extends AbstractQueryBuilder<WildcardLikeQueryBuilder> implements MultiTermQueryBuilder {
42+
public static final String NAME = "wildcardlike";
43+
44+
private static final ParseField WILDCARD_FIELD = new ParseField("wildcard");
45+
private static final ParseField VALUE_FIELD = new ParseField("value");
46+
private static final ParseField REWRITE_FIELD = new ParseField("rewrite");
47+
48+
private final String fieldName;
49+
50+
private final String value;
51+
52+
private String rewrite;
53+
54+
public static final boolean DEFAULT_CASE_INSENSITIVITY = false;
55+
private static final ParseField CASE_INSENSITIVE_FIELD = new ParseField("case_insensitive");
56+
private boolean caseInsensitive = DEFAULT_CASE_INSENSITIVITY;
57+
58+
/**
59+
* Implements the wildcard search query. Supported wildcards are {@code *}, which
60+
* matches any character sequence (including the empty one), and {@code ?},
61+
* which matches any single character. Note this query can be slow, as it
62+
* needs to iterate over many terms. In order to prevent extremely slow WildcardQueries,
63+
* a Wildcard term should not start with one of the wildcards {@code *} or
64+
* {@code ?}.
65+
*
66+
* @param fieldName The field name
67+
* @param value The wildcard query string
68+
*/
69+
public WildcardLikeQueryBuilder(String fieldName, String value) {
70+
if (Strings.isEmpty(fieldName)) {
71+
throw new IllegalArgumentException("field name is null or empty");
72+
}
73+
if (value == null) {
74+
throw new IllegalArgumentException("value cannot be null");
75+
}
76+
this.fieldName = fieldName;
77+
this.value = value;
78+
}
79+
80+
/**
81+
* Read from a stream.
82+
*/
83+
public WildcardLikeQueryBuilder(StreamInput in) throws IOException {
84+
super(in);
85+
fieldName = in.readString();
86+
value = in.readString();
87+
rewrite = in.readOptionalString();
88+
caseInsensitive = in.readBoolean();
89+
}
90+
91+
@Override
92+
protected void doWriteTo(StreamOutput out) throws IOException {
93+
out.writeString(fieldName);
94+
out.writeString(value);
95+
out.writeOptionalString(rewrite);
96+
out.writeBoolean(caseInsensitive);
97+
}
98+
99+
@Override
100+
public String fieldName() {
101+
return fieldName;
102+
}
103+
104+
public String value() {
105+
return value;
106+
}
107+
108+
public WildcardLikeQueryBuilder rewrite(String rewrite) {
109+
this.rewrite = rewrite;
110+
return this;
111+
}
112+
113+
public String rewrite() {
114+
return this.rewrite;
115+
}
116+
117+
public WildcardLikeQueryBuilder caseInsensitive(boolean caseInsensitive) {
118+
this.caseInsensitive = caseInsensitive;
119+
return this;
120+
}
121+
122+
public boolean caseInsensitive() {
123+
return this.caseInsensitive;
124+
}
125+
126+
@Override
127+
public String getWriteableName() {
128+
return NAME;
129+
}
130+
131+
@Override
132+
protected void doXContent(XContentBuilder builder, Params params) throws IOException {
133+
builder.startObject(NAME);
134+
builder.startObject(fieldName);
135+
builder.field(WILDCARD_FIELD.getPreferredName(), value);
136+
if (rewrite != null) {
137+
builder.field(REWRITE_FIELD.getPreferredName(), rewrite);
138+
}
139+
if (caseInsensitive != DEFAULT_CASE_INSENSITIVITY) {
140+
builder.field(CASE_INSENSITIVE_FIELD.getPreferredName(), caseInsensitive);
141+
}
142+
printBoostAndQueryName(builder);
143+
builder.endObject();
144+
builder.endObject();
145+
}
146+
147+
public static WildcardLikeQueryBuilder fromXContent(XContentParser parser) throws IOException {
148+
throw new UnsupportedOperationException("WildcardLikeQueryBuilder does not support parsing from XContent");
149+
}
150+
151+
@Override
152+
protected QueryBuilder doIndexMetadataRewrite(QueryRewriteContext context) {
153+
MappedFieldType fieldType = context.getFieldType(this.fieldName);
154+
if (fieldType == null) {
155+
return new MatchNoneQueryBuilder("The \"" + getName() + "\" query is against a field that does not exist");
156+
}
157+
return maybeRewriteBasedOnConstantFields(fieldType, context);
158+
}
159+
160+
@Override
161+
protected QueryBuilder doCoordinatorRewrite(CoordinatorRewriteContext coordinatorRewriteContext) {
162+
MappedFieldType fieldType = coordinatorRewriteContext.getFieldType(this.fieldName);
163+
// we don't rewrite a null field type to `match_none` on the coordinator because the coordinator has access
164+
// to only a subset of fields see {@link CoordinatorRewriteContext#getFieldType}
165+
return maybeRewriteBasedOnConstantFields(fieldType, coordinatorRewriteContext);
166+
}
167+
168+
private QueryBuilder maybeRewriteBasedOnConstantFields(@Nullable MappedFieldType fieldType, QueryRewriteContext context) {
169+
if (fieldType instanceof ConstantFieldType constantFieldType) {
170+
// This logic is correct for all field types, but by only applying it to constant
171+
// fields we also have the guarantee that it doesn't perform I/O, which is important
172+
// since rewrites might happen on a network thread.
173+
Query query = constantFieldType.wildcardLikeQuery(value, caseInsensitive, context); // the rewrite method doesn't matter
174+
// Query query = constantFieldType.wildcardLikeQuery(value, null, caseInsensitive, context); // the rewrite method doesn't
175+
// matter
176+
if (query instanceof MatchAllDocsQuery) {
177+
return new MatchAllQueryBuilder();
178+
} else if (query instanceof MatchNoDocsQuery) {
179+
return new MatchNoneQueryBuilder("The \"" + getName() + "\" query was rewritten to a \"match_none\" query.");
180+
} else {
181+
assert false : "Constant fields must produce match-all or match-none queries, got " + query;
182+
}
183+
}
184+
return this;
185+
}
186+
187+
@Override
188+
protected Query doToQuery(SearchExecutionContext context) throws IOException {
189+
MappedFieldType fieldType = context.getFieldType(fieldName);
190+
191+
if (fieldType == null) {
192+
throw new IllegalStateException("Rewrite first");
193+
}
194+
195+
MultiTermQuery.RewriteMethod method = QueryParsers.parseRewriteMethod(rewrite, null, LoggingDeprecationHandler.INSTANCE);
196+
return fieldType.wildcardLikeQuery(value, method, caseInsensitive, context);
197+
}
198+
199+
@Override
200+
protected int doHashCode() {
201+
return Objects.hash(fieldName, value, rewrite, caseInsensitive);
202+
}
203+
204+
@Override
205+
protected boolean doEquals(WildcardLikeQueryBuilder other) {
206+
return Objects.equals(fieldName, other.fieldName)
207+
&& Objects.equals(value, other.value)
208+
&& Objects.equals(rewrite, other.rewrite)
209+
&& Objects.equals(caseInsensitive, other.caseInsensitive);
210+
}
211+
212+
@Override
213+
public TransportVersion getMinimalSupportedVersion() {
214+
return TransportVersions.ZERO;
215+
}
216+
}

server/src/main/java/org/elasticsearch/search/SearchModule.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@
6666
import org.elasticsearch.index.query.TermQueryBuilder;
6767
import org.elasticsearch.index.query.TermsQueryBuilder;
6868
import org.elasticsearch.index.query.TermsSetQueryBuilder;
69+
import org.elasticsearch.index.query.WildcardLikeQueryBuilder;
6970
import org.elasticsearch.index.query.WildcardQueryBuilder;
7071
import org.elasticsearch.index.query.WrapperQueryBuilder;
7172
import org.elasticsearch.index.query.functionscore.ExponentialDecayFunctionBuilder;
@@ -1117,6 +1118,9 @@ private void registerQueryParsers(List<SearchPlugin> plugins) {
11171118
registerQuery(new QuerySpec<>(RangeQueryBuilder.NAME, RangeQueryBuilder::new, RangeQueryBuilder::fromXContent));
11181119
registerQuery(new QuerySpec<>(PrefixQueryBuilder.NAME, PrefixQueryBuilder::new, PrefixQueryBuilder::fromXContent));
11191120
registerQuery(new QuerySpec<>(WildcardQueryBuilder.NAME, WildcardQueryBuilder::new, WildcardQueryBuilder::fromXContent));
1121+
registerQuery(
1122+
new QuerySpec<>(WildcardLikeQueryBuilder.NAME, WildcardLikeQueryBuilder::new, WildcardLikeQueryBuilder::fromXContent)
1123+
);
11201124
registerQuery(
11211125
new QuerySpec<>(ConstantScoreQueryBuilder.NAME, ConstantScoreQueryBuilder::new, ConstantScoreQueryBuilder::fromXContent)
11221126
);

x-pack/plugin/esql-core/src/main/java/org/elasticsearch/xpack/esql/core/querydsl/query/WildcardQuery.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
package org.elasticsearch.xpack.esql.core.querydsl.query;
88

99
import org.elasticsearch.index.query.QueryBuilder;
10+
import org.elasticsearch.index.query.WildcardLikeQueryBuilder;
1011
import org.elasticsearch.index.query.WildcardQueryBuilder;
1112
import org.elasticsearch.xpack.esql.core.tree.Source;
1213

@@ -44,7 +45,7 @@ public Boolean caseInsensitive() {
4445

4546
@Override
4647
protected QueryBuilder asBuilder() {
47-
WildcardQueryBuilder wb = wildcardQuery(field, query);
48+
WildcardLikeQueryBuilder wb = new WildcardLikeQueryBuilder(field, query);
4849
// ES does not allow case_insensitive to be set to "false", it should be either "true" or not specified
4950
return caseInsensitive == false ? wb : wb.caseInsensitive(caseInsensitive);
5051
}

0 commit comments

Comments
 (0)