Skip to content

Commit 80cbc9f

Browse files
Fix behavior for _index LIKE for ESQL (#130849)
Fixes _index LIKE <pattern> to always have normal text matching semantics. Implement a generic ExpressionQuery and ExpressionQueryBuilder that can be serialized to the data node. Then the ExpressionQueryBuilder can build an Automaton using TranslationAware.asLuceneQuery() and execute it in Lucine. Introduces a breaking change for LIKE on _index fields. The old like behavior is not correct and does not have normal like semantics from ESQL. Customers upgrading from old build to new build might see a regression, where the data changes due to the like filters on clustering produces different results, but the new results are correct. Behavior for ESQL New CCS to New => New behavior everywhere Old CCS to New => Old behavior everywhere (the isForESQL flag is not passed in from old) New CCS to Old => New behavior for new, old behavior for old (the isForESQL cannot be passed, old does not know about it). Old CCS to Old => Old behavior everywhere Closes #129511 (cherry picked from commit 8c4eaf9) # Conflicts: # server/src/main/java/org/elasticsearch/TransportVersions.java # x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java # x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LocalPhysicalPlanOptimizerTests.java
1 parent 024228d commit 80cbc9f

File tree

93 files changed

+1107
-334
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

93 files changed

+1107
-334
lines changed

benchmarks/src/main/java/org/elasticsearch/benchmark/_nightly/esql/QueryPlanningBenchmark.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,11 +70,11 @@ public class QueryPlanningBenchmark {
7070
private EsqlParser defaultParser;
7171
private Analyzer manyFieldsAnalyzer;
7272
private LogicalPlanOptimizer defaultOptimizer;
73+
private Configuration config;
7374

7475
@Setup
7576
public void setup() {
76-
77-
var config = new Configuration(
77+
this.config = new Configuration(
7878
DateUtils.UTC,
7979
Locale.US,
8080
null,
@@ -116,7 +116,7 @@ public void setup() {
116116
}
117117

118118
private LogicalPlan plan(EsqlParser parser, Analyzer analyzer, LogicalPlanOptimizer optimizer, String query) {
119-
var parsed = parser.createStatement(query, new QueryParams(), telemetry);
119+
var parsed = parser.createStatement(query, new QueryParams(), telemetry, config);
120120
var analyzed = analyzer.analyze(parsed);
121121
var optimized = optimizer.optimize(analyzed);
122122
return optimized;

docs/changelog/130849.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
pr: 130849
2+
summary: Fix behavior for `_index` LIKE for ESQL
3+
area: ES|QL
4+
type: bug
5+
issues:
6+
- 129511

server/src/main/java/org/elasticsearch/TransportVersions.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,7 @@ static TransportVersion def(int id) {
328328
public static final TransportVersion ESQL_PROFILE_INCLUDE_PLAN = def(9_111_0_00);
329329
public static final TransportVersion MAPPINGS_IN_DATA_STREAMS = def(9_112_0_00);
330330
public static final TransportVersion ESQL_SPLIT_ON_BIG_VALUES_9_1 = def(9_112_0_01);
331+
public static final TransportVersion ESQL_FIXED_INDEX_LIKE = def(9_119_0_00);
331332

332333
/*
333334
* STOP! READ THIS FIRST! No, really,

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

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
import org.apache.lucene.search.MultiTermQuery;
1616
import org.apache.lucene.search.Query;
1717
import org.apache.lucene.util.BytesRef;
18+
import org.apache.lucene.util.automaton.Automaton;
19+
import org.apache.lucene.util.automaton.CharacterRunAutomaton;
1820
import org.elasticsearch.common.lucene.search.Queries;
1921
import org.elasticsearch.common.regex.Regex;
2022
import org.elasticsearch.core.Nullable;
@@ -23,6 +25,7 @@
2325

2426
import java.util.Collection;
2527
import java.util.Map;
28+
import java.util.function.Supplier;
2629

2730
/**
2831
* A {@link MappedFieldType} that has the same value for all documents.
@@ -135,9 +138,47 @@ public final Query wildcardQuery(String value, boolean caseInsensitive, QueryRew
135138
}
136139
}
137140

141+
/**
142+
* Returns a query that matches all documents or no documents
143+
* It usually calls {@link #wildcardQuery(String, boolean, QueryRewriteContext)}
144+
* except for IndexFieldType which overrides this method to use its own matching logic.
145+
*/
146+
public Query wildcardLikeQuery(String value, boolean caseInsensitive, QueryRewriteContext context) {
147+
return wildcardQuery(value, caseInsensitive, context);
148+
}
149+
138150
@Override
139151
public final boolean fieldHasValue(FieldInfos fieldInfos) {
140152
// We consider constant field types to always have value.
141153
return true;
142154
}
155+
156+
/**
157+
* Returns the constant value of this field as a string.
158+
* Based on the field type, we need to get it in a different way.
159+
*/
160+
public abstract String getConstantFieldValue(SearchExecutionContext context);
161+
162+
/**
163+
* Returns a query that matches all documents or no documents
164+
* depending on whether the constant value of this field matches or not
165+
*/
166+
@Override
167+
public Query automatonQuery(
168+
Supplier<Automaton> automatonSupplier,
169+
Supplier<CharacterRunAutomaton> characterRunAutomatonSupplier,
170+
@Nullable MultiTermQuery.RewriteMethod method,
171+
SearchExecutionContext context,
172+
String description
173+
) {
174+
CharacterRunAutomaton compiled = characterRunAutomatonSupplier.get();
175+
boolean matches = compiled.run(getConstantFieldValue(context));
176+
if (matches) {
177+
return new MatchAllDocsQuery();
178+
} else {
179+
return new MatchNoDocsQuery(
180+
"The \"" + context.getFullyQualifiedIndex().getName() + "\" query was rewritten to a \"match_none\" query."
181+
);
182+
}
183+
}
143184
}

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

Lines changed: 37 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,38 @@ 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+
@Override
122+
public Query wildcardLikeQuery(String value, boolean caseInsensitive, QueryRewriteContext context) {
123+
String indexName = context.getFullyQualifiedIndex().getName();
124+
return getWildcardLikeQuery(value, caseInsensitive, indexName);
125+
}
126+
127+
private static Query getWildcardLikeQuery(String value, boolean caseInsensitve, String indexName) {
128+
if (caseInsensitve) {
129+
value = value.toLowerCase(Locale.ROOT);
130+
indexName = indexName.toLowerCase(Locale.ROOT);
131+
}
132+
if (Regex.simpleMatch(value, indexName)) {
133+
return new MatchAllDocsQuery();
134+
}
135+
return new MatchNoDocsQuery("The \"" + indexName + "\" query was rewritten to a \"match_none\" query.");
136+
}
137+
138+
@Override
139+
public String getConstantFieldValue(SearchExecutionContext context) {
140+
return context.getFullyQualifiedIndex().getName();
141+
}
105142
}
106143

107144
public IndexFieldMapper() {

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,11 @@ protected boolean matches(String pattern, boolean caseInsensitive, QueryRewriteC
5757
return Regex.simpleMatch(pattern, indexMode, caseInsensitive);
5858
}
5959

60+
@Override
61+
public String getConstantFieldValue(SearchExecutionContext context) {
62+
return context.getIndexSettings().getMode().getName();
63+
}
64+
6065
@Override
6166
public Query existsQuery(SearchExecutionContext context) {
6267
return new MatchAllDocsQuery();

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,15 @@
2424
import org.apache.lucene.index.IndexReader;
2525
import org.apache.lucene.index.LeafReaderContext;
2626
import org.apache.lucene.index.MultiTerms;
27+
import org.apache.lucene.index.Term;
2728
import org.apache.lucene.index.Terms;
2829
import org.apache.lucene.index.TermsEnum;
2930
import org.apache.lucene.search.MultiTermQuery;
3031
import org.apache.lucene.search.Query;
3132
import org.apache.lucene.util.BytesRef;
3233
import org.apache.lucene.util.automaton.Automata;
3334
import org.apache.lucene.util.automaton.Automaton;
35+
import org.apache.lucene.util.automaton.CharacterRunAutomaton;
3436
import org.apache.lucene.util.automaton.CompiledAutomaton;
3537
import org.apache.lucene.util.automaton.CompiledAutomaton.AUTOMATON_TYPE;
3638
import org.apache.lucene.util.automaton.Operations;
@@ -51,6 +53,7 @@
5153
import org.elasticsearch.index.fielddata.SourceValueFetcherSortedBinaryIndexFieldData;
5254
import org.elasticsearch.index.fielddata.StoredFieldSortedBinaryIndexFieldData;
5355
import org.elasticsearch.index.fielddata.plain.SortedSetOrdinalsIndexFieldData;
56+
import org.elasticsearch.index.query.AutomatonQueryWithDescription;
5457
import org.elasticsearch.index.query.SearchExecutionContext;
5558
import org.elasticsearch.index.similarity.SimilarityProvider;
5659
import org.elasticsearch.script.Script;
@@ -82,6 +85,7 @@
8285
import java.util.Map;
8386
import java.util.Objects;
8487
import java.util.Set;
88+
import java.util.function.Supplier;
8589

8690
import static org.apache.lucene.index.IndexWriter.MAX_TERM_LENGTH;
8791
import static org.elasticsearch.core.Strings.format;
@@ -1042,6 +1046,17 @@ public IndexSortConfig getIndexSortConfig() {
10421046
public boolean hasDocValuesSkipper() {
10431047
return hasDocValuesSkipper;
10441048
}
1049+
1050+
@Override
1051+
public Query automatonQuery(
1052+
Supplier<Automaton> automatonSupplier,
1053+
Supplier<CharacterRunAutomaton> characterRunAutomatonSupplier,
1054+
@Nullable MultiTermQuery.RewriteMethod method,
1055+
SearchExecutionContext context,
1056+
String description
1057+
) {
1058+
return new AutomatonQueryWithDescription(new Term(name()), automatonSupplier.get(), description);
1059+
}
10451060
}
10461061

10471062
private final boolean indexed;

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

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
import org.apache.lucene.search.Query;
2626
import org.apache.lucene.search.TermQuery;
2727
import org.apache.lucene.util.BytesRef;
28+
import org.apache.lucene.util.automaton.Automaton;
29+
import org.apache.lucene.util.automaton.CharacterRunAutomaton;
2830
import org.elasticsearch.ElasticsearchException;
2931
import org.elasticsearch.ElasticsearchParseException;
3032
import org.elasticsearch.cluster.metadata.IndexMetadata;
@@ -54,6 +56,7 @@
5456
import java.util.Objects;
5557
import java.util.Set;
5658
import java.util.function.Function;
59+
import java.util.function.Supplier;
5760

5861
import static org.elasticsearch.search.SearchService.ALLOW_EXPENSIVE_QUERIES;
5962

@@ -329,6 +332,19 @@ public final Query wildcardQuery(String value, @Nullable MultiTermQuery.RewriteM
329332
return wildcardQuery(value, method, false, context);
330333
}
331334

335+
/**
336+
* Similar to wildcardQuery, except that we change the behavior for ESQL
337+
* to behave like a string LIKE query, where the value is matched as a string
338+
*/
339+
public Query wildcardLikeQuery(
340+
String value,
341+
@Nullable MultiTermQuery.RewriteMethod method,
342+
boolean caseInsensitve,
343+
SearchExecutionContext context
344+
) {
345+
return wildcardQuery(value, method, caseInsensitve, context);
346+
}
347+
332348
public Query wildcardQuery(
333349
String value,
334350
@Nullable MultiTermQuery.RewriteMethod method,
@@ -370,6 +386,23 @@ public Query regexpQuery(
370386
);
371387
}
372388

389+
/**
390+
* Returns a Lucine pushable Query for the current field
391+
* For now can only be AutomatonQuery or MatchAllDocsQuery() or MatchNoDocsQuery()
392+
*/
393+
public Query automatonQuery(
394+
Supplier<Automaton> automatonSupplier,
395+
Supplier<CharacterRunAutomaton> characterRunAutomatonSupplier,
396+
@Nullable MultiTermQuery.RewriteMethod method,
397+
SearchExecutionContext context,
398+
String description
399+
) {
400+
throw new QueryShardException(
401+
context,
402+
"Can only use automaton queries on keyword fields - not on [" + name + "] which is of type [" + typeName() + "]"
403+
);
404+
}
405+
373406
public Query existsQuery(SearchExecutionContext context) {
374407
if (hasDocValues() || getTextSearchInfo().hasNorms()) {
375408
return new FieldExistsQuery(name());

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

Lines changed: 0 additions & 108 deletions
This file was deleted.

0 commit comments

Comments
 (0)