diff --git a/docs/reference/query-dsl/prefix-query.asciidoc b/docs/reference/query-dsl/prefix-query.asciidoc index 51212ff1b7f4d..e168bb4de0d17 100644 --- a/docs/reference/query-dsl/prefix-query.asciidoc +++ b/docs/reference/query-dsl/prefix-query.asciidoc @@ -41,6 +41,10 @@ provided ``. (Optional, string) Method used to rewrite the query. For valid values and more information, see the <>. +`case_insensitive`:: +(Optional, boolean) allows ASCII case insensitive matching of the +value with the indexed field values when set to true. Setting to false is disallowed. + [[prefix-query-notes]] ==== Notes diff --git a/docs/reference/query-dsl/term-query.asciidoc b/docs/reference/query-dsl/term-query.asciidoc index c11a0c34a4a87..62de4d835ed0e 100644 --- a/docs/reference/query-dsl/term-query.asciidoc +++ b/docs/reference/query-dsl/term-query.asciidoc @@ -62,6 +62,10 @@ Boost values are relative to the default value of `1.0`. A boost value between `0` and `1.0` decreases the relevance score. A value greater than `1.0` increases the relevance score. +`case_insensitive`:: +(Optional, boolean) allows ASCII case insensitive matching of the +value with the indexed field values when set to true. Setting to false is disallowed. + [[term-query-notes]] ==== Notes @@ -84,7 +88,7 @@ The `term` query does *not* analyze the search term. The `term` query only searches for the *exact* term you provide. This means the `term` query may return poor or no results when searching `text` fields. -To see the difference in search results, try the following example. +To see the difference in search results, try the following example. . Create an index with a `text` field called `full_text`. + @@ -213,4 +217,4 @@ in the results. } ---- // TESTRESPONSE[s/"took" : 1/"took" : $body.took/] --- \ No newline at end of file +-- diff --git a/docs/reference/query-dsl/wildcard-query.asciidoc b/docs/reference/query-dsl/wildcard-query.asciidoc index 37e8a23c7a7a3..8d3c0f2630a53 100644 --- a/docs/reference/query-dsl/wildcard-query.asciidoc +++ b/docs/reference/query-dsl/wildcard-query.asciidoc @@ -52,7 +52,7 @@ This parameter supports two wildcard operators: WARNING: Avoid beginning patterns with `*` or `?`. This can increase the iterations needed to find matching terms and slow search performance. --- +-- `boost`:: (Optional, float) Floating point number used to decrease or increase the @@ -69,6 +69,10 @@ increases the relevance score. (Optional, string) Method used to rewrite the query. For valid values and more information, see the <>. +`case_insensitive`:: +(Optional, boolean) allows case insensitive matching of the +pattern with the indexed field values when set to true. Setting to false is disallowed. + [[wildcard-query-notes]] ==== Notes ===== Allow expensive queries diff --git a/modules/mapper-extras/src/main/java/org/elasticsearch/index/mapper/SearchAsYouTypeFieldMapper.java b/modules/mapper-extras/src/main/java/org/elasticsearch/index/mapper/SearchAsYouTypeFieldMapper.java index 53e06860af776..e0c689b18c71e 100644 --- a/modules/mapper-extras/src/main/java/org/elasticsearch/index/mapper/SearchAsYouTypeFieldMapper.java +++ b/modules/mapper-extras/src/main/java/org/elasticsearch/index/mapper/SearchAsYouTypeFieldMapper.java @@ -281,11 +281,11 @@ public Query existsQuery(QueryShardContext context) { } @Override - public Query prefixQuery(String value, MultiTermQuery.RewriteMethod method, QueryShardContext context) { + public Query prefixQuery(String value, MultiTermQuery.RewriteMethod method, boolean caseInsensitive, QueryShardContext context) { if (prefixField == null || prefixField.termLengthWithinBounds(value.length()) == false) { - return super.prefixQuery(value, method, context); + return super.prefixQuery(value, method, caseInsensitive, context); } else { - final Query query = prefixField.prefixQuery(value, method, context); + final Query query = prefixField.prefixQuery(value, method, caseInsensitive, context); if (method == null || method == MultiTermQuery.CONSTANT_SCORE_REWRITE || method == MultiTermQuery.CONSTANT_SCORE_BOOLEAN_REWRITE) { @@ -365,8 +365,11 @@ boolean termLengthWithinBounds(int length) { } @Override - public Query prefixQuery(String value, MultiTermQuery.RewriteMethod method, QueryShardContext context) { + public Query prefixQuery(String value, MultiTermQuery.RewriteMethod method, boolean caseInsensitive, QueryShardContext context) { if (value.length() >= minChars) { + if(caseInsensitive) { + return super.termQueryCaseInsensitive(value, context); + } return super.termQuery(value, context); } List automata = new ArrayList<>(); @@ -507,11 +510,11 @@ public Query existsQuery(QueryShardContext context) { } @Override - public Query prefixQuery(String value, MultiTermQuery.RewriteMethod method, QueryShardContext context) { + public Query prefixQuery(String value, MultiTermQuery.RewriteMethod method, boolean caseInsensitive, QueryShardContext context) { if (prefixFieldType == null || prefixFieldType.termLengthWithinBounds(value.length()) == false) { - return super.prefixQuery(value, method, context); + return super.prefixQuery(value, method, caseInsensitive, context); } else { - final Query query = prefixFieldType.prefixQuery(value, method, context); + final Query query = prefixFieldType.prefixQuery(value, method, caseInsensitive, context); if (method == null || method == MultiTermQuery.CONSTANT_SCORE_REWRITE || method == MultiTermQuery.CONSTANT_SCORE_BOOLEAN_REWRITE) { diff --git a/plugins/analysis-icu/src/main/java/org/elasticsearch/index/mapper/ICUCollationKeywordFieldMapper.java b/plugins/analysis-icu/src/main/java/org/elasticsearch/index/mapper/ICUCollationKeywordFieldMapper.java index 2e0b67643837b..1b033d9933b01 100644 --- a/plugins/analysis-icu/src/main/java/org/elasticsearch/index/mapper/ICUCollationKeywordFieldMapper.java +++ b/plugins/analysis-icu/src/main/java/org/elasticsearch/index/mapper/ICUCollationKeywordFieldMapper.java @@ -136,13 +136,15 @@ public Query fuzzyQuery(Object value, Fuzziness fuzziness, int prefixLength, int } @Override - public Query prefixQuery(String value, MultiTermQuery.RewriteMethod method, QueryShardContext context) { + public Query prefixQuery(String value, MultiTermQuery.RewriteMethod method, + boolean caseInsensitive, QueryShardContext context) { throw new UnsupportedOperationException("[prefix] queries are not supported on [" + CONTENT_TYPE + "] fields."); } @Override public Query wildcardQuery(String value, @Nullable MultiTermQuery.RewriteMethod method, + boolean caseInsensitive, QueryShardContext context) { throw new UnsupportedOperationException("[wildcard] queries are not supported on [" + CONTENT_TYPE + "] fields."); } diff --git a/server/src/main/java/org/elasticsearch/common/Strings.java b/server/src/main/java/org/elasticsearch/common/Strings.java index 00849d0dcec6a..2e32aa7743d31 100644 --- a/server/src/main/java/org/elasticsearch/common/Strings.java +++ b/server/src/main/java/org/elasticsearch/common/Strings.java @@ -879,4 +879,18 @@ public static String padStart(String s, int minimumLength, char c) { return sb.toString(); } } + + public static String toLowercaseAscii(String in) { + StringBuilder out = new StringBuilder(); + Iterator iter = in.codePoints().iterator(); + while (iter.hasNext()) { + int codepoint = iter.next(); + if (codepoint > 128) { + out.appendCodePoint(codepoint); + } else { + out.appendCodePoint(Character.toLowerCase(codepoint)); + } + } + return out.toString(); + } } diff --git a/server/src/main/java/org/elasticsearch/common/lucene/search/AutomatonQueries.java b/server/src/main/java/org/elasticsearch/common/lucene/search/AutomatonQueries.java new file mode 100644 index 0000000000000..180cd09a74c7d --- /dev/null +++ b/server/src/main/java/org/elasticsearch/common/lucene/search/AutomatonQueries.java @@ -0,0 +1,152 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.common.lucene.search; + +import org.apache.lucene.index.Term; +import org.apache.lucene.search.AutomatonQuery; +import org.apache.lucene.util.BytesRef; +import org.apache.lucene.util.automaton.Automata; +import org.apache.lucene.util.automaton.Automaton; +import org.apache.lucene.util.automaton.MinimizationOperations; +import org.apache.lucene.util.automaton.Operations; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +/** + * Helper functions for creating various forms of {@link AutomatonQuery} + */ +public class AutomatonQueries { + + + + /** Build an automaton query accepting all terms with the specified prefix, ASCII case insensitive. */ + public static Automaton caseInsensitivePrefix(String s) { + List list = new ArrayList<>(); + Iterator iter = s.codePoints().iterator(); + while (iter.hasNext()) { + list.add(toCaseInsensitiveChar(iter.next(), Integer.MAX_VALUE)); + } + list.add(Automata.makeAnyString()); + + Automaton a = Operations.concatenate(list); + a = MinimizationOperations.minimize(a, Integer.MAX_VALUE); + return a; + } + + + /** Build an automaton query accepting all terms with the specified prefix, ASCII case insensitive. */ + public static AutomatonQuery caseInsensitivePrefixQuery(Term prefix) { + return new AutomatonQuery(prefix, caseInsensitivePrefix(prefix.text())); + } + + /** Build an automaton accepting all terms ASCII case insensitive. */ + public static AutomatonQuery caseInsensitiveTermQuery(Term term) { + BytesRef prefix = term.bytes(); + return new AutomatonQuery(term, toCaseInsensitiveString(prefix,Integer.MAX_VALUE)); + } + + + /** Build an automaton matching a wildcard pattern, ASCII case insensitive. */ + public static AutomatonQuery caseInsensitiveWildcardQuery(Term wildcardquery) { + return new AutomatonQuery(wildcardquery, toCaseInsensitiveWildcardAutomaton(wildcardquery,Integer.MAX_VALUE)); + } + + + /** String equality with support for wildcards */ + public static final char WILDCARD_STRING = '*'; + + /** Char equality with support for wildcards */ + public static final char WILDCARD_CHAR = '?'; + + /** Escape character */ + public static final char WILDCARD_ESCAPE = '\\'; + /** + * Convert Lucene wildcard syntax into an automaton. + */ + @SuppressWarnings("fallthrough") + public static Automaton toCaseInsensitiveWildcardAutomaton(Term wildcardquery, int maxDeterminizedStates) { + List automata = new ArrayList<>(); + + String wildcardText = wildcardquery.text(); + + for (int i = 0; i < wildcardText.length();) { + final int c = wildcardText.codePointAt(i); + int length = Character.charCount(c); + switch(c) { + case WILDCARD_STRING: + automata.add(Automata.makeAnyString()); + break; + case WILDCARD_CHAR: + automata.add(Automata.makeAnyChar()); + break; + case WILDCARD_ESCAPE: + // add the next codepoint instead, if it exists + if (i + length < wildcardText.length()) { + final int nextChar = wildcardText.codePointAt(i + length); + length += Character.charCount(nextChar); + automata.add(Automata.makeChar(nextChar)); + break; + } // else fallthru, lenient parsing with a trailing \ + default: + automata.add(toCaseInsensitiveChar(c, maxDeterminizedStates)); + } + i += length; + } + + return Operations.concatenate(automata); + } + + protected static Automaton toCaseInsensitiveString(BytesRef br, int maxDeterminizedStates) { + return toCaseInsensitiveString(br.utf8ToString(), maxDeterminizedStates); + } + + public static Automaton toCaseInsensitiveString(String s, int maxDeterminizedStates) { + List list = new ArrayList<>(); + Iterator iter = s.codePoints().iterator(); + while (iter.hasNext()) { + list.add(toCaseInsensitiveChar(iter.next(), maxDeterminizedStates)); + } + + Automaton a = Operations.concatenate(list); + a = MinimizationOperations.minimize(a, maxDeterminizedStates); + return a; + + + } + + protected static Automaton toCaseInsensitiveChar(int codepoint, int maxDeterminizedStates) { + Automaton case1 = Automata.makeChar(codepoint); + // For now we only work with ASCII characters + if (codepoint > 128) { + return case1; + } + int altCase = Character.isLowerCase(codepoint) ? Character.toUpperCase(codepoint) : Character.toLowerCase(codepoint); + Automaton result; + if (altCase != codepoint) { + result = Operations.union(case1, Automata.makeChar(altCase)); + result = MinimizationOperations.minimize(result, maxDeterminizedStates); + } else { + result = case1; + } + return result; + } +} diff --git a/server/src/main/java/org/elasticsearch/common/regex/Regex.java b/server/src/main/java/org/elasticsearch/common/regex/Regex.java index 8734ab1febe8b..65e5b5770fe6f 100644 --- a/server/src/main/java/org/elasticsearch/common/regex/Regex.java +++ b/server/src/main/java/org/elasticsearch/common/regex/Regex.java @@ -79,15 +79,39 @@ public static Automaton simpleMatchToAutomaton(String... patterns) { * Match a String against the given pattern, supporting the following simple * pattern styles: "xxx*", "*xxx", "*xxx*" and "xxx*yyy" matches (with an * arbitrary number of pattern parts), as well as direct equality. + * Matching is case sensitive. * * @param pattern the pattern to match against * @param str the String to match * @return whether the String matches the given pattern */ public static boolean simpleMatch(String pattern, String str) { + return simpleMatch(pattern, str, false); + } + + + /** + * Match a String against the given pattern, supporting the following simple + * pattern styles: "xxx*", "*xxx", "*xxx*" and "xxx*yyy" matches (with an + * arbitrary number of pattern parts), as well as direct equality. + * + * @param pattern the pattern to match against + * @param str the String to match + * @param caseInsensitive true if ASCII case differences should be ignored + * @return whether the String matches the given pattern + */ + public static boolean simpleMatch(String pattern, String str, boolean caseInsensitive) { if (pattern == null || str == null) { return false; } + if (caseInsensitive) { + pattern = Strings.toLowercaseAscii(pattern); + str = Strings.toLowercaseAscii(str); + } + return simpleMatchWithNormalizedStrings(pattern, str); + } + + private static boolean simpleMatchWithNormalizedStrings(String pattern, String str) { final int firstIndex = pattern.indexOf('*'); if (firstIndex == -1) { return pattern.equals(str); @@ -102,12 +126,12 @@ public static boolean simpleMatch(String pattern, String str) { return str.regionMatches(str.length() - pattern.length() + 1, pattern, 1, pattern.length() - 1); } else if (nextIndex == 1) { // Double wildcard "**" - skipping the first "*" - return simpleMatch(pattern.substring(1), str); + return simpleMatchWithNormalizedStrings(pattern.substring(1), str); } final String part = pattern.substring(1, nextIndex); int partIndex = str.indexOf(part); while (partIndex != -1) { - if (simpleMatch(pattern.substring(nextIndex), str.substring(partIndex + part.length()))) { + if (simpleMatchWithNormalizedStrings(pattern.substring(nextIndex), str.substring(partIndex + part.length()))) { return true; } partIndex = str.indexOf(part, partIndex + 1); @@ -116,9 +140,9 @@ public static boolean simpleMatch(String pattern, String str) { } return str.regionMatches(0, pattern, 0, firstIndex) && (firstIndex == pattern.length() - 1 // only wildcard in pattern is at the end, so no need to look at the rest of the string - || simpleMatch(pattern.substring(firstIndex), str.substring(firstIndex))); - } - + || simpleMatchWithNormalizedStrings(pattern.substring(firstIndex), str.substring(firstIndex))); + } + /** * Match a String against the given patterns, supporting the following simple * pattern styles: "xxx*", "*xxx", "*xxx*" and "xxx*yyy" matches (with an diff --git a/server/src/main/java/org/elasticsearch/index/mapper/ConstantFieldType.java b/server/src/main/java/org/elasticsearch/index/mapper/ConstantFieldType.java index 6d7b5847884bd..23ae4b5ac7902 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/ConstantFieldType.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/ConstantFieldType.java @@ -59,7 +59,7 @@ public final boolean isAggregatable() { * Return whether the constant value of this field matches the provided {@code pattern} * as documented in {@link Regex#simpleMatch}. */ - protected abstract boolean matches(String pattern, QueryShardContext context); + protected abstract boolean matches(String pattern, boolean caseInsensitive, QueryShardContext context); private static String valueToString(Object value) { return value instanceof BytesRef @@ -70,31 +70,42 @@ private static String valueToString(Object value) { @Override public final Query termQuery(Object value, QueryShardContext context) { String pattern = valueToString(value); - if (matches(pattern, context)) { + if (matches(pattern, false, context)) { return Queries.newMatchAllQuery(); } else { return new MatchNoDocsQuery(); } } + @Override + public final Query termQueryCaseInsensitive(Object value, QueryShardContext context) { + String pattern = valueToString(value); + if (matches(pattern, true, context)) { + return Queries.newMatchAllQuery(); + } else { + return new MatchNoDocsQuery(); + } + } + @Override public final Query termsQuery(List values, QueryShardContext context) { for (Object value : values) { String pattern = valueToString(value); - if (matches(pattern, context)) { + if (matches(pattern, false, context)) { // `terms` queries are a disjunction, so one matching term is enough return Queries.newMatchAllQuery(); } } return new MatchNoDocsQuery(); - } + } @Override public final Query prefixQuery(String prefix, @Nullable MultiTermQuery.RewriteMethod method, + boolean caseInsensitive, QueryShardContext context) { String pattern = prefix + "*"; - if (matches(pattern, context)) { + if (matches(pattern, caseInsensitive, context)) { return Queries.newMatchAllQuery(); } else { return new MatchNoDocsQuery(); @@ -104,8 +115,9 @@ public final Query prefixQuery(String prefix, @Override public final Query wildcardQuery(String value, @Nullable MultiTermQuery.RewriteMethod method, + boolean caseInsensitive, QueryShardContext context) { - if (matches(value, context)) { + if (matches(value, caseInsensitive, context)) { return Queries.newMatchAllQuery(); } else { return new MatchNoDocsQuery(); diff --git a/server/src/main/java/org/elasticsearch/index/mapper/IndexFieldMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/IndexFieldMapper.java index dcfa7e7e1c27c..2878c1b203d14 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/IndexFieldMapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/IndexFieldMapper.java @@ -21,6 +21,7 @@ import org.apache.lucene.search.MatchAllDocsQuery; import org.apache.lucene.search.Query; +import org.elasticsearch.common.Strings; import org.elasticsearch.index.fielddata.IndexFieldData; import org.elasticsearch.index.fielddata.plain.ConstantIndexFieldData; import org.elasticsearch.index.query.QueryShardContext; @@ -52,7 +53,12 @@ public String typeName() { } @Override - protected boolean matches(String pattern, QueryShardContext context) { + protected boolean matches(String pattern, boolean caseInsensitive, QueryShardContext context) { + if (caseInsensitive) { + // Thankfully, all index names are lower-cased so we don't have to pass a case_insensitive mode flag + // down to all the index name-matching logic. We just lower-case the search string + pattern = Strings.toLowercaseAscii(pattern); + } return context.indexMatches(pattern); } diff --git a/server/src/main/java/org/elasticsearch/index/mapper/MappedFieldType.java b/server/src/main/java/org/elasticsearch/index/mapper/MappedFieldType.java index cd1e229c4aae4..35ac0a9f11225 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/MappedFieldType.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/MappedFieldType.java @@ -176,6 +176,13 @@ public boolean isAggregatable() { */ // TODO: Standardize exception types public abstract Query termQuery(Object value, @Nullable QueryShardContext context); + + + // Case insensitive form of term query (not supported by all fields so must be overridden to enable) + public Query termQueryCaseInsensitive(Object value, @Nullable QueryShardContext context) { + throw new QueryShardException(context, "[" + name + "] field which is of type [" + typeName() + + "], does not support case insensitive term queries"); + } /** Build a constant-scoring query that matches all values. The default implementation uses a * {@link ConstantScoreQuery} around a {@link BooleanQuery} whose {@link Occur#SHOULD} clauses @@ -206,14 +213,27 @@ public Query fuzzyQuery(Object value, Fuzziness fuzziness, int prefixLength, int + "] which is of type [" + typeName() + "]"); } - public Query prefixQuery(String value, @Nullable MultiTermQuery.RewriteMethod method, QueryShardContext context) { + // Case sensitive form of prefix query + public final Query prefixQuery(String value, @Nullable MultiTermQuery.RewriteMethod method, QueryShardContext context) { + return prefixQuery(value, method, false, context); + } + + public Query prefixQuery(String value, @Nullable MultiTermQuery.RewriteMethod method, boolean caseInsensitve, + QueryShardContext context) { throw new QueryShardException(context, "Can only use prefix queries on keyword, text and wildcard fields - not on [" + name + "] which is of type [" + typeName() + "]"); } + // Case sensitive form of wildcard query + public final Query wildcardQuery(String value, + @Nullable MultiTermQuery.RewriteMethod method, QueryShardContext context + ) { + return wildcardQuery(value, method, false, context); + } + public Query wildcardQuery(String value, @Nullable MultiTermQuery.RewriteMethod method, - QueryShardContext context) { + boolean caseInsensitve, QueryShardContext context) { throw new QueryShardException(context, "Can only use wildcard queries on keyword, text and wildcard fields - not on [" + name + "] which is of type [" + typeName() + "]"); } diff --git a/server/src/main/java/org/elasticsearch/index/mapper/StringFieldType.java b/server/src/main/java/org/elasticsearch/index/mapper/StringFieldType.java index daa5f470c1def..49610488c98ae 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/StringFieldType.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/StringFieldType.java @@ -21,6 +21,7 @@ import org.apache.lucene.analysis.Analyzer; import org.apache.lucene.index.Term; +import org.apache.lucene.search.AutomatonQuery; import org.apache.lucene.search.FuzzyQuery; import org.apache.lucene.search.MultiTermQuery; import org.apache.lucene.search.PrefixQuery; @@ -32,6 +33,7 @@ import org.apache.lucene.util.BytesRefBuilder; import org.elasticsearch.ElasticsearchException; import org.elasticsearch.common.lucene.BytesRefs; +import org.elasticsearch.common.lucene.search.AutomatonQueries; import org.elasticsearch.common.unit.Fuzziness; import org.elasticsearch.index.query.QueryShardContext; import org.elasticsearch.index.query.support.QueryParsers; @@ -68,13 +70,21 @@ public Query fuzzyQuery(Object value, Fuzziness fuzziness, int prefixLength, int } @Override - public Query prefixQuery(String value, MultiTermQuery.RewriteMethod method, QueryShardContext context) { + public Query prefixQuery(String value, MultiTermQuery.RewriteMethod method, boolean caseInsensitive, QueryShardContext context) { if (context.allowExpensiveQueries() == false) { throw new ElasticsearchException("[prefix] queries cannot be executed when '" + ALLOW_EXPENSIVE_QUERIES.getKey() + "' is set to false. For optimised prefix queries on text " + "fields please enable [index_prefixes]."); } failIfNotIndexed(); + if (caseInsensitive) { + AutomatonQuery query = AutomatonQueries.caseInsensitivePrefixQuery((new Term(name(), indexedValueForSearch(value)))); + if (method != null) { + query.setRewriteMethod(method); + } + return query; + + } PrefixQuery query = new PrefixQuery(new Term(name(), indexedValueForSearch(value))); if (method != null) { query.setRewriteMethod(method); @@ -113,7 +123,7 @@ public static final String normalizeWildcardPattern(String fieldname, String val } @Override - public Query wildcardQuery(String value, MultiTermQuery.RewriteMethod method, QueryShardContext context) { + public Query wildcardQuery(String value, MultiTermQuery.RewriteMethod method, boolean caseInsensitive, QueryShardContext context) { failIfNotIndexed(); if (context.allowExpensiveQueries() == false) { throw new ElasticsearchException("[wildcard] queries cannot be executed when '" + @@ -127,7 +137,11 @@ public Query wildcardQuery(String value, MultiTermQuery.RewriteMethod method, Qu } else { term = new Term(name(), indexedValueForSearch(value)); } - + if (caseInsensitive) { + AutomatonQuery query = AutomatonQueries.caseInsensitiveWildcardQuery(term); + QueryParsers.setRewriteMethod(query, method); + return query; + } WildcardQuery query = new WildcardQuery(term); QueryParsers.setRewriteMethod(query, method); return query; diff --git a/server/src/main/java/org/elasticsearch/index/mapper/TermBasedFieldType.java b/server/src/main/java/org/elasticsearch/index/mapper/TermBasedFieldType.java index 0101b68fb88c8..ab7562bccbd96 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/TermBasedFieldType.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/TermBasedFieldType.java @@ -26,6 +26,7 @@ import org.apache.lucene.search.TermQuery; import org.apache.lucene.util.BytesRef; import org.elasticsearch.common.lucene.BytesRefs; +import org.elasticsearch.common.lucene.search.AutomatonQueries; import org.elasticsearch.index.query.QueryShardContext; import java.util.List; @@ -46,6 +47,16 @@ protected BytesRef indexedValueForSearch(Object value) { return BytesRefs.toBytesRef(value); } + @Override + public Query termQueryCaseInsensitive(Object value, QueryShardContext context) { + failIfNotIndexed(); + Query query = AutomatonQueries.caseInsensitiveTermQuery(new Term(name(), indexedValueForSearch(value))); + if (boost() != 1f) { + query = new BoostQuery(query, boost()); + } + return query; + } + @Override public Query termQuery(Object value, QueryShardContext context) { failIfNotIndexed(); diff --git a/server/src/main/java/org/elasticsearch/index/mapper/TextFieldMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/TextFieldMapper.java index 1448e5c60b860..d49fa72798a72 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/TextFieldMapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/TextFieldMapper.java @@ -60,6 +60,7 @@ import org.elasticsearch.Version; import org.elasticsearch.common.collect.Iterators; import org.elasticsearch.common.lucene.Lucene; +import org.elasticsearch.common.lucene.search.AutomatonQueries; import org.elasticsearch.common.lucene.search.MultiPhrasePrefixQuery; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.support.XContentMapValues; @@ -439,12 +440,20 @@ void doXContent(XContentBuilder builder) throws IOException { } @Override - public Query prefixQuery(String value, MultiTermQuery.RewriteMethod method, QueryShardContext context) { + public Query prefixQuery(String value, MultiTermQuery.RewriteMethod method, boolean caseInsensitive, QueryShardContext context) { if (value.length() >= minChars) { + if (caseInsensitive) { + return super.termQueryCaseInsensitive(value, context); + } return super.termQuery(value, context); } List automata = new ArrayList<>(); - automata.add(Automata.makeString(value)); + if (caseInsensitive) { + automata.add(AutomatonQueries.toCaseInsensitiveString(value, Integer.MAX_VALUE)); + } else { + automata.add(Automata.makeString(value)); + } + for (int i = value.length(); i < minChars; i++) { automata.add(Automata.makeAnyChar()); } @@ -632,11 +641,11 @@ public String typeName() { } @Override - public Query prefixQuery(String value, MultiTermQuery.RewriteMethod method, QueryShardContext context) { + public Query prefixQuery(String value, MultiTermQuery.RewriteMethod method, boolean caseInsensitive, QueryShardContext context) { if (prefixFieldType == null || prefixFieldType.accept(value.length()) == false) { - return super.prefixQuery(value, method, context); + return super.prefixQuery(value, method, caseInsensitive,context); } - Query tq = prefixFieldType.prefixQuery(value, method, context); + Query tq = prefixFieldType.prefixQuery(value, method, caseInsensitive, context); if (method == null || method == MultiTermQuery.CONSTANT_SCORE_REWRITE || method == MultiTermQuery.CONSTANT_SCORE_BOOLEAN_REWRITE) { return new ConstantScoreQuery(tq); diff --git a/server/src/main/java/org/elasticsearch/index/mapper/TypeFieldMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/TypeFieldMapper.java index 49638b26a9685..6b8e2fe7607b7 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/TypeFieldMapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/TypeFieldMapper.java @@ -77,7 +77,10 @@ public IndexFieldData.Builder fielddataBuilder(String fullyQualifiedIndexName, S } @Override - protected boolean matches(String pattern, QueryShardContext context) { + protected boolean matches(String pattern, boolean caseInsensitive, QueryShardContext context) { + if (caseInsensitive) { + return pattern.equalsIgnoreCase(MapperService.SINGLE_MAPPING_NAME); + } return pattern.equals(MapperService.SINGLE_MAPPING_NAME); } } diff --git a/server/src/main/java/org/elasticsearch/index/query/BaseTermQueryBuilder.java b/server/src/main/java/org/elasticsearch/index/query/BaseTermQueryBuilder.java index f06ee48d06b67..b11bf81123b0a 100644 --- a/server/src/main/java/org/elasticsearch/index/query/BaseTermQueryBuilder.java +++ b/server/src/main/java/org/elasticsearch/index/query/BaseTermQueryBuilder.java @@ -152,18 +152,23 @@ protected void doXContent(XContentBuilder builder, Params params) throws IOExcep builder.startObject(getName()); builder.startObject(fieldName); builder.field(VALUE_FIELD.getPreferredName(), maybeConvertToString(this.value)); + addExtraXContent(builder, params); printBoostAndQueryName(builder); builder.endObject(); builder.endObject(); } + + protected void addExtraXContent(XContentBuilder builder, Params params) throws IOException { + // Do nothing but allows subclasses to override. + } @Override - protected final int doHashCode() { + protected int doHashCode() { return Objects.hash(fieldName, value); } @Override - protected final boolean doEquals(QB other) { + protected boolean doEquals(QB other) { return Objects.equals(fieldName, other.fieldName) && Objects.equals(value, other.value); } diff --git a/server/src/main/java/org/elasticsearch/index/query/PrefixQueryBuilder.java b/server/src/main/java/org/elasticsearch/index/query/PrefixQueryBuilder.java index 44c8dd44b49c8..e2273a9dbf19d 100644 --- a/server/src/main/java/org/elasticsearch/index/query/PrefixQueryBuilder.java +++ b/server/src/main/java/org/elasticsearch/index/query/PrefixQueryBuilder.java @@ -23,6 +23,7 @@ import org.apache.lucene.search.MatchNoDocsQuery; import org.apache.lucene.search.MultiTermQuery; import org.apache.lucene.search.Query; +import org.elasticsearch.Version; import org.elasticsearch.common.ParseField; import org.elasticsearch.common.ParsingException; import org.elasticsearch.common.Strings; @@ -50,6 +51,11 @@ public class PrefixQueryBuilder extends AbstractQueryBuilder private final String fieldName; private final String value; + + public static final boolean DEFAULT_CASE_INSENSITIVITY = false; + private static final ParseField CASE_INSENSITIVE_FIELD = new ParseField("case_insensitive"); + private boolean caseInsensitive = DEFAULT_CASE_INSENSITIVITY; + private String rewrite; @@ -78,6 +84,9 @@ public PrefixQueryBuilder(StreamInput in) throws IOException { fieldName = in.readString(); value = in.readString(); rewrite = in.readOptionalString(); + if (in.getVersion().onOrAfter(Version.V_8_0_0)) { + caseInsensitive = in.readBoolean(); + } } @Override @@ -85,6 +94,9 @@ protected void doWriteTo(StreamOutput out) throws IOException { out.writeString(fieldName); out.writeString(value); out.writeOptionalString(rewrite); + if (out.getVersion().onOrAfter(Version.V_8_0_0)) { + out.writeBoolean(caseInsensitive); + } } @Override @@ -95,6 +107,18 @@ public String fieldName() { public String value() { return this.value; } + + public PrefixQueryBuilder caseInsensitive(boolean caseInsensitive) { + if (caseInsensitive == false) { + throw new IllegalArgumentException("The case insensitive setting cannot be set to false."); + } + this.caseInsensitive = caseInsensitive; + return this; + } + + public boolean caseInsensitive() { + return this.caseInsensitive; + } public PrefixQueryBuilder rewrite(String rewrite) { this.rewrite = rewrite; @@ -113,6 +137,9 @@ public void doXContent(XContentBuilder builder, Params params) throws IOExceptio if (rewrite != null) { builder.field(REWRITE_FIELD.getPreferredName(), rewrite); } + if (caseInsensitive != DEFAULT_CASE_INSENSITIVITY) { + builder.field(CASE_INSENSITIVE_FIELD.getPreferredName(), caseInsensitive); + } printBoostAndQueryName(builder); builder.endObject(); builder.endObject(); @@ -125,6 +152,7 @@ public static PrefixQueryBuilder fromXContent(XContentParser parser) throws IOEx String queryName = null; float boost = AbstractQueryBuilder.DEFAULT_BOOST; + boolean caseInsensitive = DEFAULT_CASE_INSENSITIVITY; String currentFieldName = null; XContentParser.Token token; while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { @@ -145,6 +173,12 @@ public static PrefixQueryBuilder fromXContent(XContentParser parser) throws IOEx boost = parser.floatValue(); } else if (REWRITE_FIELD.match(currentFieldName, parser.getDeprecationHandler())) { rewrite = parser.textOrNull(); + } else if (CASE_INSENSITIVE_FIELD.match(currentFieldName, parser.getDeprecationHandler())) { + caseInsensitive = parser.booleanValue(); + if (caseInsensitive == false) { + throw new ParsingException(parser.getTokenLocation(), + "[prefix] query does not support [" + currentFieldName + "] = false"); + } } else { throw new ParsingException(parser.getTokenLocation(), "[prefix] query does not support [" + currentFieldName + "]"); @@ -158,10 +192,14 @@ public static PrefixQueryBuilder fromXContent(XContentParser parser) throws IOEx } } - return new PrefixQueryBuilder(fieldName, value) + PrefixQueryBuilder result = new PrefixQueryBuilder(fieldName, value) .rewrite(rewrite) .boost(boost) .queryName(queryName); + if (caseInsensitive) { + result.caseInsensitive(caseInsensitive); + } + return result; } @Override @@ -180,7 +218,7 @@ protected QueryBuilder doRewrite(QueryRewriteContext queryRewriteContext) throws // This logic is correct for all field types, but by only applying it to constant // fields we also have the guarantee that it doesn't perform I/O, which is important // since rewrites might happen on a network thread. - Query query = fieldType.prefixQuery(value, null, context); // the rewrite method doesn't matter + Query query = fieldType.prefixQuery(value, null, caseInsensitive, context); // the rewrite method doesn't matter if (query instanceof MatchAllDocsQuery) { return new MatchAllQueryBuilder(); } else if (query instanceof MatchNoDocsQuery) { @@ -202,18 +240,19 @@ protected Query doToQuery(QueryShardContext context) throws IOException { if (fieldType == null) { throw new IllegalStateException("Rewrite first"); } - return fieldType.prefixQuery(value, method, context); + return fieldType.prefixQuery(value, method, caseInsensitive, context); } @Override protected final int doHashCode() { - return Objects.hash(fieldName, value, rewrite); + return Objects.hash(fieldName, value, rewrite, caseInsensitive); } @Override protected boolean doEquals(PrefixQueryBuilder other) { return Objects.equals(fieldName, other.fieldName) && Objects.equals(value, other.value) && - Objects.equals(rewrite, other.rewrite); + Objects.equals(rewrite, other.rewrite) && + Objects.equals(caseInsensitive, other.caseInsensitive); } } diff --git a/server/src/main/java/org/elasticsearch/index/query/TermQueryBuilder.java b/server/src/main/java/org/elasticsearch/index/query/TermQueryBuilder.java index 8a0118d26e8db..25270acd12ede 100644 --- a/server/src/main/java/org/elasticsearch/index/query/TermQueryBuilder.java +++ b/server/src/main/java/org/elasticsearch/index/query/TermQueryBuilder.java @@ -22,20 +22,31 @@ import org.apache.lucene.search.MatchAllDocsQuery; import org.apache.lucene.search.MatchNoDocsQuery; import org.apache.lucene.search.Query; +import org.elasticsearch.Version; import org.elasticsearch.common.ParseField; import org.elasticsearch.common.ParsingException; import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.common.xcontent.ToXContent.Params; import org.elasticsearch.index.mapper.MappedFieldType; import org.elasticsearch.index.mapper.ConstantFieldType; import java.io.IOException; +import java.util.Objects; /** * A Query that matches documents containing a term. */ public class TermQueryBuilder extends BaseTermQueryBuilder { public static final String NAME = "term"; + public static final boolean DEFAULT_CASE_INSENSITIVITY = false; + private static final ParseField CASE_INSENSITIVE_FIELD = new ParseField("case_insensitive"); + + + private boolean caseInsensitive = DEFAULT_CASE_INSENSITIVITY; + private static final ParseField TERM_FIELD = new ParseField("term"); private static final ParseField VALUE_FIELD = new ParseField("value"); @@ -74,19 +85,43 @@ public TermQueryBuilder(String fieldName, boolean value) { public TermQueryBuilder(String fieldName, Object value) { super(fieldName, value); } + + public TermQueryBuilder caseInsensitive(boolean caseInsensitive) { + if (caseInsensitive == false) { + throw new IllegalArgumentException("The case insensitive setting cannot be set to false."); + } + this.caseInsensitive = caseInsensitive; + return this; + } + + public boolean caseInsensitive() { + return this.caseInsensitive; + } + /** * Read from a stream. */ public TermQueryBuilder(StreamInput in) throws IOException { super(in); + if (in.getVersion().onOrAfter(Version.V_8_0_0)) { + caseInsensitive = in.readBoolean(); + } } + @Override + protected void doWriteTo(StreamOutput out) throws IOException { + super.doWriteTo(out); + if (out.getVersion().onOrAfter(Version.V_8_0_0)) { + out.writeBoolean(caseInsensitive); + } + } public static TermQueryBuilder fromXContent(XContentParser parser) throws IOException { String queryName = null; String fieldName = null; Object value = null; float boost = AbstractQueryBuilder.DEFAULT_BOOST; + boolean caseInsensitive = DEFAULT_CASE_INSENSITIVITY; String currentFieldName = null; XContentParser.Token token; while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { @@ -107,6 +142,12 @@ public static TermQueryBuilder fromXContent(XContentParser parser) throws IOExce queryName = parser.text(); } else if (AbstractQueryBuilder.BOOST_FIELD.match(currentFieldName, parser.getDeprecationHandler())) { boost = parser.floatValue(); + } else if (CASE_INSENSITIVE_FIELD.match(currentFieldName, parser.getDeprecationHandler())) { + caseInsensitive = parser.booleanValue(); + if (caseInsensitive == false) { + throw new ParsingException(parser.getTokenLocation(), + "[term] query does not support [" + currentFieldName + "] = false"); + } } else { throw new ParsingException(parser.getTokenLocation(), "[term] query does not support [" + currentFieldName + "]"); @@ -127,9 +168,19 @@ public static TermQueryBuilder fromXContent(XContentParser parser) throws IOExce if (queryName != null) { termQuery.queryName(queryName); } + if (caseInsensitive) { + termQuery.caseInsensitive(caseInsensitive); + } return termQuery; } + @Override + protected void addExtraXContent(XContentBuilder builder, Params params) throws IOException { + if (caseInsensitive != DEFAULT_CASE_INSENSITIVITY) { + builder.field(CASE_INSENSITIVE_FIELD.getPreferredName(), caseInsensitive); + } + } + @Override protected QueryBuilder doRewrite(QueryRewriteContext queryRewriteContext) throws IOException { QueryShardContext context = queryRewriteContext.convertToShardContext(); @@ -141,7 +192,13 @@ protected QueryBuilder doRewrite(QueryRewriteContext queryRewriteContext) throws // This logic is correct for all field types, but by only applying it to constant // fields we also have the guarantee that it doesn't perform I/O, which is important // since rewrites might happen on a network thread. - Query query = fieldType.termQuery(value, context); + Query query = null; + if (caseInsensitive) { + query = fieldType.termQueryCaseInsensitive(value, context); + } else { + query = fieldType.termQuery(value, context); + } + if (query instanceof MatchAllDocsQuery) { return new MatchAllQueryBuilder(); } else if (query instanceof MatchNoDocsQuery) { @@ -160,11 +217,27 @@ protected Query doToQuery(QueryShardContext context) throws IOException { if (mapper == null) { throw new IllegalStateException("Rewrite first"); } - return mapper.termQuery(this.value, context); + if (caseInsensitive) { + return mapper.termQueryCaseInsensitive(value, context); + } + return mapper.termQuery(value, context); } @Override public String getWriteableName() { return NAME; } + + + @Override + protected final int doHashCode() { + return Objects.hash(super.doHashCode(), caseInsensitive); + } + + @Override + protected final boolean doEquals(TermQueryBuilder other) { + return super.doEquals(other) && + Objects.equals(caseInsensitive, other.caseInsensitive); + } + } diff --git a/server/src/main/java/org/elasticsearch/index/query/WildcardQueryBuilder.java b/server/src/main/java/org/elasticsearch/index/query/WildcardQueryBuilder.java index 39ca3b0a45b6f..18876a5828dcf 100644 --- a/server/src/main/java/org/elasticsearch/index/query/WildcardQueryBuilder.java +++ b/server/src/main/java/org/elasticsearch/index/query/WildcardQueryBuilder.java @@ -23,6 +23,7 @@ import org.apache.lucene.search.MatchNoDocsQuery; import org.apache.lucene.search.MultiTermQuery; import org.apache.lucene.search.Query; +import org.elasticsearch.Version; import org.elasticsearch.common.ParseField; import org.elasticsearch.common.ParsingException; import org.elasticsearch.common.Strings; @@ -59,6 +60,10 @@ public class WildcardQueryBuilder extends AbstractQueryBuilder ft.termQueryCaseInsensitive(value, context)); + assertTrue(ex.getMessage().contains("does not support case insensitive term queries")); + } } diff --git a/server/src/test/java/org/elasticsearch/index/mapper/TextFieldTypeTests.java b/server/src/test/java/org/elasticsearch/index/mapper/TextFieldTypeTests.java index 73929a2eab896..476755218997a 100644 --- a/server/src/test/java/org/elasticsearch/index/mapper/TextFieldTypeTests.java +++ b/server/src/test/java/org/elasticsearch/index/mapper/TextFieldTypeTests.java @@ -36,6 +36,7 @@ import org.apache.lucene.util.automaton.Operations; import org.elasticsearch.ElasticsearchException; import org.elasticsearch.common.lucene.BytesRefs; +import org.elasticsearch.common.lucene.search.AutomatonQueries; import org.elasticsearch.common.unit.Fuzziness; import org.elasticsearch.index.mapper.TextFieldMapper.TextFieldType; @@ -52,6 +53,7 @@ public class TextFieldTypeTests extends FieldTypeTestCase { public void testTermQuery() { MappedFieldType ft = new TextFieldType("field"); assertEquals(new TermQuery(new Term("field", "foo")), ft.termQuery("foo", null)); + assertEquals(AutomatonQueries.caseInsensitiveTermQuery(new Term("field", "fOo")), ft.termQueryCaseInsensitive("fOo", null)); MappedFieldType unsearchable = new TextFieldType("field", false, Collections.emptyMap()); IllegalArgumentException e = expectThrows(IllegalArgumentException.class, @@ -121,18 +123,22 @@ public void testIndexPrefixes() { TextFieldType ft = new TextFieldType("field"); ft.setPrefixFieldType(new TextFieldMapper.PrefixFieldType(ft, "field._index_prefix", 2, 10, true)); - Query q = ft.prefixQuery("goin", CONSTANT_SCORE_REWRITE, randomMockShardContext()); + Query q = ft.prefixQuery("goin", CONSTANT_SCORE_REWRITE, false, randomMockShardContext()); assertEquals(new ConstantScoreQuery(new TermQuery(new Term("field._index_prefix", "goin"))), q); - q = ft.prefixQuery("internationalisatio", CONSTANT_SCORE_REWRITE, MOCK_QSC); + q = ft.prefixQuery("internationalisatio", CONSTANT_SCORE_REWRITE, false, MOCK_QSC); assertEquals(new PrefixQuery(new Term("field", "internationalisatio")), q); + q = ft.prefixQuery("Internationalisatio", CONSTANT_SCORE_REWRITE, true, MOCK_QSC); + assertEquals(AutomatonQueries.caseInsensitivePrefixQuery(new Term("field", "Internationalisatio")), q); + + ElasticsearchException ee = expectThrows(ElasticsearchException.class, - () -> ft.prefixQuery("internationalisatio", null, MOCK_QSC_DISALLOW_EXPENSIVE)); + () -> ft.prefixQuery("internationalisatio", null, false, MOCK_QSC_DISALLOW_EXPENSIVE)); assertEquals("[prefix] queries cannot be executed when 'search.allow_expensive_queries' is set to false. " + "For optimised prefix queries on text fields please enable [index_prefixes].", ee.getMessage()); - q = ft.prefixQuery("g", CONSTANT_SCORE_REWRITE, randomMockShardContext()); + q = ft.prefixQuery("g", CONSTANT_SCORE_REWRITE, false, randomMockShardContext()); Automaton automaton = Operations.concatenate(Arrays.asList(Automata.makeChar('g'), Automata.makeAnyChar())); diff --git a/server/src/test/java/org/elasticsearch/index/mapper/TypeFieldTypeTests.java b/server/src/test/java/org/elasticsearch/index/mapper/TypeFieldTypeTests.java index 98b11a0362da6..bf832f4131210 100644 --- a/server/src/test/java/org/elasticsearch/index/mapper/TypeFieldTypeTests.java +++ b/server/src/test/java/org/elasticsearch/index/mapper/TypeFieldTypeTests.java @@ -35,7 +35,15 @@ public void testTermsQuery() { Query query = ft.termQuery("_doc", context); assertEquals(new MatchAllDocsQuery(), query); + query = ft.termQueryCaseInsensitive("_dOc", context); + assertEquals(new MatchAllDocsQuery(), query); + + query = ft.termQuery("other_type", context); assertEquals(new MatchNoDocsQuery(), query); + + query = ft.termQueryCaseInsensitive("other_Type", context); + assertEquals(new MatchNoDocsQuery(), query); + } } diff --git a/server/src/test/java/org/elasticsearch/index/query/PrefixQueryBuilderTests.java b/server/src/test/java/org/elasticsearch/index/query/PrefixQueryBuilderTests.java index 5716cb9493a37..f90e5e4815b03 100644 --- a/server/src/test/java/org/elasticsearch/index/query/PrefixQueryBuilderTests.java +++ b/server/src/test/java/org/elasticsearch/index/query/PrefixQueryBuilderTests.java @@ -44,6 +44,13 @@ protected PrefixQueryBuilder doCreateTestQueryBuilder() { if (randomBoolean()) { query.rewrite(getRandomRewriteMethod()); } + //TODO code below is commented out while we do the Version dance for PR 61596. Steps are + // 1) Commit PR 61596 with this code commented out in master + // 2) Backport PR 61596 to 7.x, uncommented + // 3) New PR on master to uncomment this code now that 7.x has support for case insensitive flag. +// if (randomBoolean()) { +// query.caseInsensitive(true); +// } return query; } @@ -101,7 +108,14 @@ public void testBlendedRewriteMethod() throws IOException { public void testFromJson() throws IOException { String json = - "{ \"prefix\" : { \"user\" : { \"value\" : \"ki\", \"boost\" : 2.0 } }}"; + "{ \"prefix\" : { \"user\" : { \"value\" : \"ki\", \"boost\" : 2.0 " + //TODO code below is commented out while we do the Version dance for PR 61596. Steps are + // 1) Commit PR 61596 with this code commented out in master + // 2) Backport PR 61596 to 7.x, uncommented + // 3) New PR on master to uncomment this code now that 7.x has support for case insensitive flag. +// " \"case_insensitive\" : true\n" + + + + "} }}"; PrefixQueryBuilder parsed = (PrefixQueryBuilder) parseQuery(json); checkGeneratedJson(json, parsed); diff --git a/server/src/test/java/org/elasticsearch/index/query/TermQueryBuilderTests.java b/server/src/test/java/org/elasticsearch/index/query/TermQueryBuilderTests.java index 63472604291e9..8a85b90cd8ddb 100644 --- a/server/src/test/java/org/elasticsearch/index/query/TermQueryBuilderTests.java +++ b/server/src/test/java/org/elasticsearch/index/query/TermQueryBuilderTests.java @@ -22,6 +22,7 @@ import com.fasterxml.jackson.core.io.JsonStringEncoder; import org.apache.lucene.index.Term; +import org.apache.lucene.search.AutomatonQuery; import org.apache.lucene.search.MatchNoDocsQuery; import org.apache.lucene.search.PointRangeQuery; import org.apache.lucene.search.Query; @@ -87,12 +88,21 @@ protected TermQueryBuilder doCreateTestQueryBuilder() { */ @Override protected TermQueryBuilder createQueryBuilder(String fieldName, Object value) { - return new TermQueryBuilder(fieldName, value); + TermQueryBuilder result = new TermQueryBuilder(fieldName, value); + //TODO code below is commented out while we do the Version dance for PR 61596. Steps are + // 1) Commit PR 61596 with this code commented out in master + // 2) Backport PR 61596 to 7.x, uncommented + // 3) New PR on master to uncomment this code now that 7.x has support for case insensitive flag. +// if (randomBoolean()) { +// result.caseInsensitive(true); +// } + return result; } @Override protected void doAssertLuceneQuery(TermQueryBuilder queryBuilder, Query query, QueryShardContext context) throws IOException { - assertThat(query, either(instanceOf(TermQuery.class)).or(instanceOf(PointRangeQuery.class)).or(instanceOf(MatchNoDocsQuery.class))); + assertThat(query, either(instanceOf(TermQuery.class)).or(instanceOf(PointRangeQuery.class)).or(instanceOf(MatchNoDocsQuery.class)) + .or(instanceOf(AutomatonQuery.class))); MappedFieldType mapper = context.fieldMapper(queryBuilder.fieldName()); if (query instanceof TermQuery) { TermQuery termQuery = (TermQuery) query; @@ -100,14 +110,21 @@ protected void doAssertLuceneQuery(TermQueryBuilder queryBuilder, Query query, Q String expectedFieldName = expectedFieldName(queryBuilder.fieldName()); assertThat(termQuery.getTerm().field(), equalTo(expectedFieldName)); - Term term = ((TermQuery) mapper.termQuery(queryBuilder.value(), null)).getTerm(); + Term term = ((TermQuery) termQuery(mapper, queryBuilder.value(), queryBuilder.caseInsensitive())).getTerm(); assertThat(termQuery.getTerm(), equalTo(term)); } else if (mapper != null) { - assertEquals(query, mapper.termQuery(queryBuilder.value(), null)); + assertEquals(query, termQuery(mapper, queryBuilder.value(), queryBuilder.caseInsensitive())); } else { assertThat(query, instanceOf(MatchNoDocsQuery.class)); } } + + private Query termQuery(MappedFieldType mapper, Object value, boolean caseInsensitive) { + if (caseInsensitive) { + return mapper.termQueryCaseInsensitive(value, null); + } + return mapper.termQuery(value, null); + } public void testTermArray() throws IOException { String queryAsString = "{\n" + @@ -126,6 +143,11 @@ public void testFromJson() throws IOException { " \"exact_value\" : {\n" + " \"value\" : \"Quick Foxes!\",\n" + " \"boost\" : 1.0\n" + + //TODO code below is commented out while we do the Version dance for PR 61596. Steps are + // 1) Commit PR 61596 with this code commented out in master + // 2) Backport PR 61596 to 7.x, uncommented + // 3) New PR on master to uncomment this code now that 7.x has support for case insensitive flag. +// " \"case_insensitive\" : true\n" + " }\n" + " }\n" + "}"; diff --git a/server/src/test/java/org/elasticsearch/index/query/WildcardQueryBuilderTests.java b/server/src/test/java/org/elasticsearch/index/query/WildcardQueryBuilderTests.java index 5631fb2020232..f2706c2ad9b35 100644 --- a/server/src/test/java/org/elasticsearch/index/query/WildcardQueryBuilderTests.java +++ b/server/src/test/java/org/elasticsearch/index/query/WildcardQueryBuilderTests.java @@ -41,6 +41,13 @@ protected WildcardQueryBuilder doCreateTestQueryBuilder() { if (randomBoolean()) { query.rewrite(randomFrom(getRandomRewriteMethod())); } + //TODO code below is commented out while we do the Version dance for PR 61596. Steps are + // 1) Commit PR 61596 with this code commented out in master + // 2) Backport PR 61596 to 7.x, uncommented + // 3) New PR on master to uncomment this code now that 7.x has support for case insensitive flag. +// if (randomBoolean()) { +// query.caseInsensitive(true); +// } return query; } @@ -103,7 +110,14 @@ public void testEmptyValue() throws IOException { } public void testFromJson() throws IOException { - String json = "{ \"wildcard\" : { \"user\" : { \"wildcard\" : \"ki*y\", \"boost\" : 2.0 } }}"; + String json = "{ \"wildcard\" : { \"user\" : { \"wildcard\" : \"ki*y\", \"boost\" : 2.0" + //TODO code below is commented out while we do the Version dance for PR 61596. Steps are + // 1) Commit PR 61596 with this code commented out in master + // 2) Backport PR 61596 to 7.x, uncommented + // 3) New PR on master to uncomment this code now that 7.x has support for case insensitive flag. +// " \"case_insensitive\" : true\n" + + + + " } }}"; WildcardQueryBuilder parsed = (WildcardQueryBuilder) parseQuery(json); checkGeneratedJson(json, parsed); assertEquals(json, "ki*y", parsed.value()); diff --git a/x-pack/plugin/mapper-constant-keyword/src/main/java/org/elasticsearch/xpack/constantkeyword/mapper/ConstantKeywordFieldMapper.java b/x-pack/plugin/mapper-constant-keyword/src/main/java/org/elasticsearch/xpack/constantkeyword/mapper/ConstantKeywordFieldMapper.java index b354d7a2dc3ba..e0cc6e89e22be 100644 --- a/x-pack/plugin/mapper-constant-keyword/src/main/java/org/elasticsearch/xpack/constantkeyword/mapper/ConstantKeywordFieldMapper.java +++ b/x-pack/plugin/mapper-constant-keyword/src/main/java/org/elasticsearch/xpack/constantkeyword/mapper/ConstantKeywordFieldMapper.java @@ -147,11 +147,11 @@ public IndexFieldData.Builder fielddataBuilder(String fullyQualifiedIndexName, S } @Override - protected boolean matches(String pattern, QueryShardContext context) { + protected boolean matches(String pattern, boolean caseInsensitive, QueryShardContext context) { if (value == null) { return false; } - return Regex.simpleMatch(pattern, value); + return Regex.simpleMatch(pattern, value, caseInsensitive); } @Override diff --git a/x-pack/plugin/mapper-constant-keyword/src/test/java/org/elasticsearch/xpack/constantkeyword/mapper/ConstantKeywordFieldTypeTests.java b/x-pack/plugin/mapper-constant-keyword/src/test/java/org/elasticsearch/xpack/constantkeyword/mapper/ConstantKeywordFieldTypeTests.java index e5c721ef82b88..93fea8dbfad20 100644 --- a/x-pack/plugin/mapper-constant-keyword/src/test/java/org/elasticsearch/xpack/constantkeyword/mapper/ConstantKeywordFieldTypeTests.java +++ b/x-pack/plugin/mapper-constant-keyword/src/test/java/org/elasticsearch/xpack/constantkeyword/mapper/ConstantKeywordFieldTypeTests.java @@ -21,9 +21,12 @@ public class ConstantKeywordFieldTypeTests extends FieldTypeTestCase { public void testTermQuery() { ConstantKeywordFieldType ft = new ConstantKeywordFieldType("f", "foo"); assertEquals(new MatchAllDocsQuery(), ft.termQuery("foo", null)); + assertEquals(new MatchAllDocsQuery(), ft.termQueryCaseInsensitive("fOo", null)); assertEquals(new MatchNoDocsQuery(), ft.termQuery("bar", null)); + assertEquals(new MatchNoDocsQuery(), ft.termQueryCaseInsensitive("bAr", null)); ConstantKeywordFieldType bar = new ConstantKeywordFieldType("f", "bar"); assertEquals(new MatchNoDocsQuery(), bar.termQuery("foo", null)); + assertEquals(new MatchNoDocsQuery(), bar.termQueryCaseInsensitive("fOo", null)); } public void testTermsQuery() { @@ -39,18 +42,24 @@ public void testTermsQuery() { public void testWildcardQuery() { ConstantKeywordFieldType bar = new ConstantKeywordFieldType("f", "bar"); - assertEquals(new MatchNoDocsQuery(), bar.wildcardQuery("f*o", null, null)); + assertEquals(new MatchNoDocsQuery(), bar.wildcardQuery("f*o", null, false, null)); + assertEquals(new MatchNoDocsQuery(), bar.wildcardQuery("F*o", null, true, null)); ConstantKeywordFieldType ft = new ConstantKeywordFieldType("f", "foo"); - assertEquals(new MatchAllDocsQuery(), ft.wildcardQuery("f*o", null, null)); - assertEquals(new MatchNoDocsQuery(), ft.wildcardQuery("b*r", null, null)); + assertEquals(new MatchAllDocsQuery(), ft.wildcardQuery("f*o", null, false, null)); + assertEquals(new MatchAllDocsQuery(), ft.wildcardQuery("F*o", null, true, null)); + assertEquals(new MatchNoDocsQuery(), ft.wildcardQuery("b*r", null, false, null)); + assertEquals(new MatchNoDocsQuery(), ft.wildcardQuery("B*r", null, true, null)); } public void testPrefixQuery() { ConstantKeywordFieldType bar = new ConstantKeywordFieldType("f", "bar"); - assertEquals(new MatchNoDocsQuery(), bar.prefixQuery("fo", null, null)); + assertEquals(new MatchNoDocsQuery(), bar.prefixQuery("fo", null, false, null)); + assertEquals(new MatchNoDocsQuery(), bar.prefixQuery("fO", null, true, null)); ConstantKeywordFieldType ft = new ConstantKeywordFieldType("f", "foo"); - assertEquals(new MatchAllDocsQuery(), ft.prefixQuery("fo", null, null)); - assertEquals(new MatchNoDocsQuery(), ft.prefixQuery("ba", null, null)); + assertEquals(new MatchAllDocsQuery(), ft.prefixQuery("fo", null, false, null)); + assertEquals(new MatchAllDocsQuery(), ft.prefixQuery("fO", null, true, null)); + assertEquals(new MatchNoDocsQuery(), ft.prefixQuery("ba", null, false, null)); + assertEquals(new MatchNoDocsQuery(), ft.prefixQuery("Ba", null, true, null)); } public void testExistsQuery() { diff --git a/x-pack/plugin/mapper-flattened/src/main/java/org/elasticsearch/xpack/flattened/mapper/FlatObjectFieldMapper.java b/x-pack/plugin/mapper-flattened/src/main/java/org/elasticsearch/xpack/flattened/mapper/FlatObjectFieldMapper.java index 50873db863870..cf02f36ed4bd3 100644 --- a/x-pack/plugin/mapper-flattened/src/main/java/org/elasticsearch/xpack/flattened/mapper/FlatObjectFieldMapper.java +++ b/x-pack/plugin/mapper-flattened/src/main/java/org/elasticsearch/xpack/flattened/mapper/FlatObjectFieldMapper.java @@ -12,6 +12,7 @@ import org.apache.lucene.index.LeafReaderContext; import org.apache.lucene.index.OrdinalMap; import org.apache.lucene.index.Term; +import org.apache.lucene.search.BoostQuery; import org.apache.lucene.search.DocValuesFieldExistsQuery; import org.apache.lucene.search.MultiTermQuery; import org.apache.lucene.search.PrefixQuery; @@ -20,6 +21,7 @@ import org.apache.lucene.search.TermQuery; import org.apache.lucene.util.BytesRef; import org.elasticsearch.common.lucene.Lucene; +import org.elasticsearch.common.lucene.search.AutomatonQueries; import org.elasticsearch.common.unit.Fuzziness; import org.elasticsearch.common.util.BigArrays; import org.elasticsearch.common.xcontent.XContentBuilder; @@ -302,11 +304,23 @@ public Query regexpQuery(String value, int syntaxFlags, int matchFlags, int maxD @Override public Query wildcardQuery(String value, MultiTermQuery.RewriteMethod method, + boolean caseInsensitive, QueryShardContext context) { throw new UnsupportedOperationException("[wildcard] queries are not currently supported on keyed " + "[" + CONTENT_TYPE + "] fields."); } + + + @Override + public Query termQueryCaseInsensitive(Object value, QueryShardContext context) { + Query query = AutomatonQueries.caseInsensitiveTermQuery(new Term(name(), indexedValueForSearch(value))); + if (boost() != 1f) { + query = new BoostQuery(query, boost()); + } + return query; + } + @Override public BytesRef indexedValueForSearch(Object value) { if (value == null) { diff --git a/x-pack/plugin/mapper-flattened/src/test/java/org/elasticsearch/xpack/flattened/mapper/KeyedFlatObjectFieldTypeTests.java b/x-pack/plugin/mapper-flattened/src/test/java/org/elasticsearch/xpack/flattened/mapper/KeyedFlatObjectFieldTypeTests.java index 2ccb714d0bc2c..0ebf215262d7e 100644 --- a/x-pack/plugin/mapper-flattened/src/test/java/org/elasticsearch/xpack/flattened/mapper/KeyedFlatObjectFieldTypeTests.java +++ b/x-pack/plugin/mapper-flattened/src/test/java/org/elasticsearch/xpack/flattened/mapper/KeyedFlatObjectFieldTypeTests.java @@ -15,6 +15,7 @@ import org.apache.lucene.search.TermRangeQuery; import org.apache.lucene.util.BytesRef; import org.elasticsearch.ElasticsearchException; +import org.elasticsearch.common.lucene.search.AutomatonQueries; import org.elasticsearch.common.unit.Fuzziness; import org.elasticsearch.index.mapper.FieldTypeTestCase; import org.elasticsearch.xpack.flattened.mapper.FlatObjectFieldMapper.KeyedFlatObjectFieldType; @@ -48,6 +49,11 @@ public void testTermQuery() { Query expected = new TermQuery(new Term("field", "key\0value")); assertEquals(expected, ft.termQuery("value", null)); + expected = AutomatonQueries.caseInsensitiveTermQuery(new Term("field", "key\0value")); + assertEquals(expected, ft.termQueryCaseInsensitive("value", null)); + + + KeyedFlatObjectFieldType unsearchable = new KeyedFlatObjectFieldType("field", false, true, "key", false, Collections.emptyMap()); IllegalArgumentException e = expectThrows(IllegalArgumentException.class, @@ -81,10 +87,14 @@ public void testPrefixQuery() { KeyedFlatObjectFieldType ft = createFieldType(); Query expected = new PrefixQuery(new Term("field", "key\0val")); - assertEquals(expected, ft.prefixQuery("val", MultiTermQuery.CONSTANT_SCORE_REWRITE, MOCK_QSC)); + assertEquals(expected, ft.prefixQuery("val", MultiTermQuery.CONSTANT_SCORE_REWRITE, false, MOCK_QSC)); + expected = AutomatonQueries.caseInsensitivePrefixQuery(new Term("field", "key\0vAl")); + assertEquals(expected, ft.prefixQuery("vAl", MultiTermQuery.CONSTANT_SCORE_REWRITE, true, MOCK_QSC)); + + ElasticsearchException ee = expectThrows(ElasticsearchException.class, - () -> ft.prefixQuery("val", MultiTermQuery.CONSTANT_SCORE_REWRITE, MOCK_QSC_DISALLOW_EXPENSIVE)); + () -> ft.prefixQuery("val", MultiTermQuery.CONSTANT_SCORE_REWRITE, false, MOCK_QSC_DISALLOW_EXPENSIVE)); assertEquals("[prefix] queries cannot be executed when 'search.allow_expensive_queries' is set to false. " + "For optimised prefix queries on text fields please enable [index_prefixes].", ee.getMessage()); } @@ -138,7 +148,7 @@ public void testWildcardQuery() { KeyedFlatObjectFieldType ft = createFieldType(); UnsupportedOperationException e = expectThrows(UnsupportedOperationException.class, - () -> ft.wildcardQuery("valu*", null, randomMockShardContext())); + () -> ft.wildcardQuery("valu*", null, false, randomMockShardContext())); assertEquals("[wildcard] queries are not currently supported on keyed [flattened] fields.", e.getMessage()); } } diff --git a/x-pack/plugin/mapper-flattened/src/test/java/org/elasticsearch/xpack/flattened/mapper/RootFlatObjectFieldTypeTests.java b/x-pack/plugin/mapper-flattened/src/test/java/org/elasticsearch/xpack/flattened/mapper/RootFlatObjectFieldTypeTests.java index d8218e3bbe4fc..07a56f3171364 100644 --- a/x-pack/plugin/mapper-flattened/src/test/java/org/elasticsearch/xpack/flattened/mapper/RootFlatObjectFieldTypeTests.java +++ b/x-pack/plugin/mapper-flattened/src/test/java/org/elasticsearch/xpack/flattened/mapper/RootFlatObjectFieldTypeTests.java @@ -16,6 +16,7 @@ import org.apache.lucene.search.WildcardQuery; import org.apache.lucene.util.BytesRef; import org.elasticsearch.ElasticsearchException; +import org.elasticsearch.common.lucene.search.AutomatonQueries; import org.elasticsearch.common.unit.Fuzziness; import org.elasticsearch.index.mapper.FieldNamesFieldMapper; import org.elasticsearch.index.mapper.FieldTypeTestCase; @@ -43,6 +44,10 @@ public void testTermQuery() { Query expected = new TermQuery(new Term("field", "value")); assertEquals(expected, ft.termQuery("value", null)); + expected = AutomatonQueries.caseInsensitiveTermQuery(new Term("field", "Value")); + assertEquals(expected, ft.termQueryCaseInsensitive("Value", null)); + + RootFlatObjectFieldType unsearchable = new RootFlatObjectFieldType("field", false, true, Collections.emptyMap(), false); IllegalArgumentException e = expectThrows(IllegalArgumentException.class, diff --git a/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/mapper/AbstractScriptMappedFieldType.java b/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/mapper/AbstractScriptMappedFieldType.java index 4604c9f29109a..67f9757a4c0fa 100644 --- a/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/mapper/AbstractScriptMappedFieldType.java +++ b/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/mapper/AbstractScriptMappedFieldType.java @@ -133,12 +133,12 @@ public Query fuzzyQuery( } @Override - public Query prefixQuery(String value, MultiTermQuery.RewriteMethod method, QueryShardContext context) { + public Query prefixQuery(String value, MultiTermQuery.RewriteMethod method, boolean caseInsensitive, QueryShardContext context) { throw new IllegalArgumentException(unsupported("prefix", "keyword, text and wildcard")); } @Override - public Query wildcardQuery(String value, MultiTermQuery.RewriteMethod method, QueryShardContext context) { + public Query wildcardQuery(String value, MultiTermQuery.RewriteMethod method, boolean caseInsensitive, QueryShardContext context) { throw new IllegalArgumentException(unsupported("wildcard", "keyword, text and wildcard")); } diff --git a/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/mapper/BooleanScriptMappedFieldType.java b/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/mapper/BooleanScriptMappedFieldType.java index c1b31eefe2f45..68b222c9ee057 100644 --- a/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/mapper/BooleanScriptMappedFieldType.java +++ b/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/mapper/BooleanScriptMappedFieldType.java @@ -10,6 +10,7 @@ import org.apache.lucene.search.Query; import org.apache.lucene.util.BytesRef; import org.elasticsearch.common.Booleans; +import org.elasticsearch.common.Strings; import org.elasticsearch.common.lucene.search.Queries; import org.elasticsearch.common.time.DateMathParser; import org.elasticsearch.index.mapper.BooleanFieldMapper; @@ -138,10 +139,16 @@ public Query rangeQuery( return termsQuery(trueAllowed, falseAllowed, context); } + @Override + public Query termQueryCaseInsensitive(Object value, QueryShardContext context) { + checkAllowExpensiveQueries(context); + return new BooleanScriptFieldTermQuery(script, leafFactory(context.lookup()), name(), toBoolean(value, true)); + } + @Override public Query termQuery(Object value, QueryShardContext context) { checkAllowExpensiveQueries(context); - return new BooleanScriptFieldTermQuery(script, leafFactory(context), name(), toBoolean(value)); + return new BooleanScriptFieldTermQuery(script, leafFactory(context), name(), toBoolean(value, false)); } @Override @@ -152,7 +159,7 @@ public Query termsQuery(List values, QueryShardContext context) { boolean trueAllowed = false; boolean falseAllowed = false; for (Object value : values) { - if (toBoolean(value)) { + if (toBoolean(value, false)) { trueAllowed = true; } else { falseAllowed = true; @@ -177,10 +184,14 @@ private Query termsQuery(boolean trueAllowed, boolean falseAllowed, QueryShardCo return new MatchNoDocsQuery("neither true nor false allowed"); } + private static boolean toBoolean(Object value) { + return toBoolean(value, false); + } + /** * Convert the term into a boolean. Inspired by {@link BooleanFieldMapper.BooleanFieldType#indexedValueForSearch(Object)}. */ - private static boolean toBoolean(Object value) { + private static boolean toBoolean(Object value, boolean caseInsensitive) { if (value == null) { return false; } @@ -193,6 +204,9 @@ private static boolean toBoolean(Object value) { } else { sValue = value.toString(); } + if (caseInsensitive) { + sValue = Strings.toLowercaseAscii(sValue); + } return Booleans.parseBoolean(sValue); } } diff --git a/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/mapper/KeywordScriptMappedFieldType.java b/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/mapper/KeywordScriptMappedFieldType.java index 1eb15958330bf..24dc33fef759b 100644 --- a/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/mapper/KeywordScriptMappedFieldType.java +++ b/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/mapper/KeywordScriptMappedFieldType.java @@ -88,9 +88,14 @@ public Query fuzzyQuery( } @Override - public Query prefixQuery(String value, RewriteMethod method, org.elasticsearch.index.query.QueryShardContext context) { + public Query prefixQuery( + String value, + RewriteMethod method, + boolean caseInsensitive, + org.elasticsearch.index.query.QueryShardContext context + ) { checkAllowExpensiveQueries(context); - return new StringScriptFieldPrefixQuery(script, leafFactory(context), name(), value); + return new StringScriptFieldPrefixQuery(script, leafFactory(context), name(), value, caseInsensitive); } @Override @@ -128,13 +133,39 @@ public Query regexpQuery( if (matchFlags != 0) { throw new IllegalArgumentException("Match flags not yet implemented [" + matchFlags + "]"); } - return new StringScriptFieldRegexpQuery(script, leafFactory(context), name(), value, syntaxFlags, maxDeterminizedStates); + return new StringScriptFieldRegexpQuery( + script, + leafFactory(context), + name(), + value, + syntaxFlags, + matchFlags, + maxDeterminizedStates + ); + } + + @Override + public Query termQueryCaseInsensitive(Object value, QueryShardContext context) { + checkAllowExpensiveQueries(context); + return new StringScriptFieldTermQuery( + script, + leafFactory(context), + name(), + BytesRefs.toString(Objects.requireNonNull(value)), + true + ); } @Override public Query termQuery(Object value, QueryShardContext context) { checkAllowExpensiveQueries(context); - return new StringScriptFieldTermQuery(script, leafFactory(context), name(), BytesRefs.toString(Objects.requireNonNull(value))); + return new StringScriptFieldTermQuery( + script, + leafFactory(context), + name(), + BytesRefs.toString(Objects.requireNonNull(value)), + false + ); } @Override @@ -145,8 +176,8 @@ public Query termsQuery(List values, QueryShardContext context) { } @Override - public Query wildcardQuery(String value, RewriteMethod method, QueryShardContext context) { + public Query wildcardQuery(String value, RewriteMethod method, boolean caseInsensitive, QueryShardContext context) { checkAllowExpensiveQueries(context); - return new StringScriptFieldWildcardQuery(script, leafFactory(context), name(), value); + return new StringScriptFieldWildcardQuery(script, leafFactory(context), name(), value, caseInsensitive); } } diff --git a/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/query/StringScriptFieldPrefixQuery.java b/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/query/StringScriptFieldPrefixQuery.java index 1f46872bea0f6..712f4f7b49864 100644 --- a/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/query/StringScriptFieldPrefixQuery.java +++ b/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/query/StringScriptFieldPrefixQuery.java @@ -9,7 +9,9 @@ import org.apache.lucene.search.PrefixQuery; import org.apache.lucene.search.QueryVisitor; import org.apache.lucene.util.BytesRef; +import org.apache.lucene.util.automaton.Automaton; import org.apache.lucene.util.automaton.ByteRunAutomaton; +import org.elasticsearch.common.lucene.search.AutomatonQueries; import org.elasticsearch.script.Script; import org.elasticsearch.xpack.runtimefields.mapper.StringFieldScript; @@ -18,26 +20,63 @@ public class StringScriptFieldPrefixQuery extends AbstractStringScriptFieldQuery { private final String prefix; + private final boolean caseInsensitive; - public StringScriptFieldPrefixQuery(Script script, StringFieldScript.LeafFactory leafFactory, String fieldName, String prefix) { + public StringScriptFieldPrefixQuery( + Script script, + StringFieldScript.LeafFactory leafFactory, + String fieldName, + String prefix, + boolean caseInsensitive + ) { super(script, leafFactory, fieldName); this.prefix = Objects.requireNonNull(prefix); + this.caseInsensitive = caseInsensitive; } @Override protected boolean matches(List values) { for (String value : values) { - if (value != null && value.startsWith(prefix)) { + if (startsWith(value, prefix, caseInsensitive)) { return true; } } return false; } + /** + *

Check if a String starts with a specified prefix (optionally case insensitive).

+ * + * @see java.lang.String#startsWith(String) + * @param str the String to check, may be null + * @param prefix the prefix to find, may be null + * @param ignoreCase inidicates whether the compare should ignore case + * (case insensitive) or not. + * @return true if the String starts with the prefix or + * both null + */ + private static boolean startsWith(String str, String prefix, boolean ignoreCase) { + if (str == null || prefix == null) { + return (str == null && prefix == null); + } + if (prefix.length() > str.length()) { + return false; + } + return str.regionMatches(ignoreCase, 0, prefix, 0, prefix.length()); + } + @Override public void visit(QueryVisitor visitor) { if (visitor.acceptField(fieldName())) { - visitor.consumeTermsMatching(this, fieldName(), () -> new ByteRunAutomaton(PrefixQuery.toAutomaton(new BytesRef(prefix)))); + visitor.consumeTermsMatching(this, fieldName(), () -> new ByteRunAutomaton(buildAutomaton(new BytesRef(prefix)))); + } + } + + Automaton buildAutomaton(BytesRef prefix) { + if (caseInsensitive) { + return AutomatonQueries.caseInsensitivePrefix(prefix.utf8ToString()); + } else { + return PrefixQuery.toAutomaton(prefix); } } @@ -51,7 +90,7 @@ public final String toString(String field) { @Override public int hashCode() { - return Objects.hash(super.hashCode(), prefix); + return Objects.hash(super.hashCode(), prefix, caseInsensitive); } @Override @@ -60,10 +99,14 @@ public boolean equals(Object obj) { return false; } StringScriptFieldPrefixQuery other = (StringScriptFieldPrefixQuery) obj; - return prefix.equals(other.prefix); + return prefix.equals(other.prefix) && caseInsensitive == other.caseInsensitive; } String prefix() { return prefix; } + + boolean caseInsensitive() { + return caseInsensitive; + } } diff --git a/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/query/StringScriptFieldRegexpQuery.java b/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/query/StringScriptFieldRegexpQuery.java index 79eb10b22baa7..46da303aacd2b 100644 --- a/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/query/StringScriptFieldRegexpQuery.java +++ b/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/query/StringScriptFieldRegexpQuery.java @@ -15,24 +15,27 @@ public class StringScriptFieldRegexpQuery extends AbstractStringScriptFieldAutomatonQuery { private final String pattern; - private final int flags; + private final int syntaxFlags; + private final int matchFlags; public StringScriptFieldRegexpQuery( Script script, StringFieldScript.LeafFactory leafFactory, String fieldName, String pattern, - int flags, + int syntaxFlags, + int matchFlags, int maxDeterminizedStates ) { super( script, leafFactory, fieldName, - new ByteRunAutomaton(new RegExp(Objects.requireNonNull(pattern), flags).toAutomaton(maxDeterminizedStates)) + new ByteRunAutomaton(new RegExp(Objects.requireNonNull(pattern), syntaxFlags, matchFlags).toAutomaton(maxDeterminizedStates)) ); this.pattern = pattern; - this.flags = flags; + this.syntaxFlags = syntaxFlags; + this.matchFlags = matchFlags; } @Override @@ -46,7 +49,7 @@ public final String toString(String field) { @Override public int hashCode() { - return Objects.hash(super.hashCode(), pattern, flags); + return Objects.hash(super.hashCode(), pattern, syntaxFlags, matchFlags); } @Override @@ -55,14 +58,18 @@ public boolean equals(Object obj) { return false; } StringScriptFieldRegexpQuery other = (StringScriptFieldRegexpQuery) obj; - return pattern.equals(other.pattern) && flags == other.flags; + return pattern.equals(other.pattern) && syntaxFlags == other.syntaxFlags && matchFlags == other.matchFlags; } String pattern() { return pattern; } - int flags() { - return flags; + int syntaxFlags() { + return syntaxFlags; + } + + int matchFlags() { + return matchFlags; } } diff --git a/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/query/StringScriptFieldTermQuery.java b/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/query/StringScriptFieldTermQuery.java index a3b3762d1e9a0..a7f79c232f48c 100644 --- a/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/query/StringScriptFieldTermQuery.java +++ b/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/query/StringScriptFieldTermQuery.java @@ -16,16 +16,28 @@ public class StringScriptFieldTermQuery extends AbstractStringScriptFieldQuery { private final String term; + private final boolean caseInsensitive; - public StringScriptFieldTermQuery(Script script, StringFieldScript.LeafFactory leafFactory, String fieldName, String term) { + public StringScriptFieldTermQuery( + Script script, + StringFieldScript.LeafFactory leafFactory, + String fieldName, + String term, + boolean caseInsensitive + ) { super(script, leafFactory, fieldName); this.term = Objects.requireNonNull(term); + this.caseInsensitive = caseInsensitive; } @Override protected boolean matches(List values) { for (String value : values) { - if (term.equals(value)) { + if (caseInsensitive) { + if (term.equalsIgnoreCase(value)) { + return true; + } + } else if (term.equals(value)) { return true; } } @@ -47,7 +59,7 @@ public final String toString(String field) { @Override public int hashCode() { - return Objects.hash(super.hashCode(), term); + return Objects.hash(super.hashCode(), term, caseInsensitive); } @Override @@ -56,10 +68,14 @@ public boolean equals(Object obj) { return false; } StringScriptFieldTermQuery other = (StringScriptFieldTermQuery) obj; - return term.equals(other.term); + return term.equals(other.term) && caseInsensitive == other.caseInsensitive; } String term() { return term; } + + boolean caseInsensitive() { + return caseInsensitive; + } } diff --git a/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/query/StringScriptFieldWildcardQuery.java b/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/query/StringScriptFieldWildcardQuery.java index 17f671aa18c8b..c397fb6ef111b 100644 --- a/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/query/StringScriptFieldWildcardQuery.java +++ b/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/query/StringScriptFieldWildcardQuery.java @@ -8,7 +8,9 @@ import org.apache.lucene.index.Term; import org.apache.lucene.search.WildcardQuery; +import org.apache.lucene.util.automaton.Automaton; import org.apache.lucene.util.automaton.ByteRunAutomaton; +import org.elasticsearch.common.lucene.search.AutomatonQueries; import org.elasticsearch.script.Script; import org.elasticsearch.xpack.runtimefields.mapper.StringFieldScript; @@ -16,15 +18,30 @@ public class StringScriptFieldWildcardQuery extends AbstractStringScriptFieldAutomatonQuery { private final String pattern; + private final boolean caseInsensitive; - public StringScriptFieldWildcardQuery(Script script, StringFieldScript.LeafFactory leafFactory, String fieldName, String pattern) { + public StringScriptFieldWildcardQuery( + Script script, + StringFieldScript.LeafFactory leafFactory, + String fieldName, + String pattern, + boolean caseInsensitive + ) { super( script, leafFactory, fieldName, - new ByteRunAutomaton(WildcardQuery.toAutomaton(new Term(fieldName, Objects.requireNonNull(pattern)))) + new ByteRunAutomaton(buildAutomaton(new Term(fieldName, Objects.requireNonNull(pattern)), caseInsensitive)) ); this.pattern = pattern; + this.caseInsensitive = caseInsensitive; + } + + private static Automaton buildAutomaton(Term term, boolean caseInsensitive) { + if (caseInsensitive) { + return AutomatonQueries.toCaseInsensitiveWildcardAutomaton(term, Integer.MAX_VALUE); + } + return WildcardQuery.toAutomaton(term); } @Override @@ -37,7 +54,7 @@ public final String toString(String field) { @Override public int hashCode() { - return Objects.hash(super.hashCode(), pattern); + return Objects.hash(super.hashCode(), pattern, caseInsensitive); } @Override @@ -46,10 +63,14 @@ public boolean equals(Object obj) { return false; } StringScriptFieldWildcardQuery other = (StringScriptFieldWildcardQuery) obj; - return pattern.equals(other.pattern); + return pattern.equals(other.pattern) && caseInsensitive == other.caseInsensitive; } String pattern() { return pattern; } + + boolean caseInsensitive() { + return caseInsensitive; + } } diff --git a/x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/mapper/KeywordScriptMappedFieldTypeTests.java b/x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/mapper/KeywordScriptMappedFieldTypeTests.java index 05c3cefb40a30..51d97b6e4e65a 100644 --- a/x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/mapper/KeywordScriptMappedFieldTypeTests.java +++ b/x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/mapper/KeywordScriptMappedFieldTypeTests.java @@ -267,7 +267,7 @@ public void testRegexpQueryInLoop() throws IOException { } private Query randomRegexpQuery(MappedFieldType ft, QueryShardContext ctx) { - return ft.regexpQuery(randomAlphaOfLengthBetween(1, 1000), randomInt(0xFFFF), 0, Integer.MAX_VALUE, null, ctx); + return ft.regexpQuery(randomAlphaOfLengthBetween(1, 1000), randomInt(0xFF), 0, Integer.MAX_VALUE, null, ctx); } @Override diff --git a/x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/query/StringScriptFieldPrefixQueryTests.java b/x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/query/StringScriptFieldPrefixQueryTests.java index 8e63621b998a2..cf40f3b0e9646 100644 --- a/x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/query/StringScriptFieldPrefixQueryTests.java +++ b/x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/query/StringScriptFieldPrefixQueryTests.java @@ -18,12 +18,18 @@ public class StringScriptFieldPrefixQueryTests extends AbstractStringScriptFieldQueryTestCase { @Override protected StringScriptFieldPrefixQuery createTestInstance() { - return new StringScriptFieldPrefixQuery(randomScript(), leafFactory, randomAlphaOfLength(5), randomAlphaOfLength(6)); + return new StringScriptFieldPrefixQuery( + randomScript(), + leafFactory, + randomAlphaOfLength(5), + randomAlphaOfLength(6), + randomBoolean() + ); } @Override protected StringScriptFieldPrefixQuery copy(StringScriptFieldPrefixQuery orig) { - return new StringScriptFieldPrefixQuery(orig.script(), leafFactory, orig.fieldName(), orig.prefix()); + return new StringScriptFieldPrefixQuery(orig.script(), leafFactory, orig.fieldName(), orig.prefix(), orig.caseInsensitive()); } @Override @@ -31,6 +37,7 @@ protected StringScriptFieldPrefixQuery mutate(StringScriptFieldPrefixQuery orig) Script script = orig.script(); String fieldName = orig.fieldName(); String prefix = orig.prefix(); + boolean caseInsensitive = orig.caseInsensitive(); switch (randomInt(2)) { case 0: script = randomValueOtherThan(script, this::randomScript); @@ -41,19 +48,30 @@ protected StringScriptFieldPrefixQuery mutate(StringScriptFieldPrefixQuery orig) case 2: prefix += "modified"; break; + case 3: + caseInsensitive = !caseInsensitive; + break; default: fail(); } - return new StringScriptFieldPrefixQuery(script, leafFactory, fieldName, prefix); + return new StringScriptFieldPrefixQuery(script, leafFactory, fieldName, prefix, caseInsensitive); } @Override public void testMatches() { - StringScriptFieldPrefixQuery query = new StringScriptFieldPrefixQuery(randomScript(), leafFactory, "test", "foo"); + StringScriptFieldPrefixQuery query = new StringScriptFieldPrefixQuery(randomScript(), leafFactory, "test", "foo", false); assertTrue(query.matches(List.of("foo"))); + assertFalse(query.matches(List.of("Foo"))); assertTrue(query.matches(List.of("foooo"))); + assertFalse(query.matches(List.of("Foooo"))); assertFalse(query.matches(List.of("fo"))); assertTrue(query.matches(List.of("fo", "foo"))); + assertFalse(query.matches(List.of("Fo", "fOo"))); + + StringScriptFieldPrefixQuery ciQuery = new StringScriptFieldPrefixQuery(randomScript(), leafFactory, "test", "foo", true); + assertTrue(ciQuery.matches(List.of("fOo"))); + assertTrue(ciQuery.matches(List.of("Foooo"))); + assertTrue(ciQuery.matches(List.of("fo", "foO"))); } @Override diff --git a/x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/query/StringScriptFieldRegexpQueryTests.java b/x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/query/StringScriptFieldRegexpQueryTests.java index 14411b0ed70b7..7502e4f629c18 100644 --- a/x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/query/StringScriptFieldRegexpQueryTests.java +++ b/x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/query/StringScriptFieldRegexpQueryTests.java @@ -20,12 +20,14 @@ public class StringScriptFieldRegexpQueryTests extends AbstractStringScriptFieldQueryTestCase { @Override protected StringScriptFieldRegexpQuery createTestInstance() { + int matchFlags = randomBoolean() ? 0 : RegExp.ASCII_CASE_INSENSITIVE; return new StringScriptFieldRegexpQuery( randomScript(), leafFactory, randomAlphaOfLength(5), randomAlphaOfLength(6), randomInt(RegExp.ALL), + matchFlags, Operations.DEFAULT_MAX_DETERMINIZED_STATES ); } @@ -37,7 +39,8 @@ protected StringScriptFieldRegexpQuery copy(StringScriptFieldRegexpQuery orig) { leafFactory, orig.fieldName(), orig.pattern(), - orig.flags(), + orig.syntaxFlags(), + orig.matchFlags(), Operations.DEFAULT_MAX_DETERMINIZED_STATES ); } @@ -47,8 +50,9 @@ protected StringScriptFieldRegexpQuery mutate(StringScriptFieldRegexpQuery orig) Script script = orig.script(); String fieldName = orig.fieldName(); String pattern = orig.pattern(); - int flags = orig.flags(); - switch (randomInt(3)) { + int syntaxFlags = orig.syntaxFlags(); + int matchFlags = orig.matchFlags(); + switch (randomInt(4)) { case 0: script = randomValueOtherThan(script, this::randomScript); break; @@ -59,12 +63,23 @@ protected StringScriptFieldRegexpQuery mutate(StringScriptFieldRegexpQuery orig) pattern += "modified"; break; case 3: - flags = randomValueOtherThan(flags, () -> randomInt(RegExp.ALL)); + syntaxFlags = randomValueOtherThan(syntaxFlags, () -> randomInt(RegExp.ALL)); + break; + case 4: + matchFlags = (matchFlags & RegExp.ASCII_CASE_INSENSITIVE) != 0 ? 0 : RegExp.ASCII_CASE_INSENSITIVE; break; default: fail(); } - return new StringScriptFieldRegexpQuery(script, leafFactory, fieldName, pattern, flags, Operations.DEFAULT_MAX_DETERMINIZED_STATES); + return new StringScriptFieldRegexpQuery( + script, + leafFactory, + fieldName, + pattern, + syntaxFlags, + matchFlags, + Operations.DEFAULT_MAX_DETERMINIZED_STATES + ); } @Override @@ -75,14 +90,29 @@ public void testMatches() { "test", "a.+b", 0, + 0, Operations.DEFAULT_MAX_DETERMINIZED_STATES ); assertTrue(query.matches(List.of("astuffb"))); + assertFalse(query.matches(List.of("astuffB"))); assertFalse(query.matches(List.of("fffff"))); assertFalse(query.matches(List.of("ab"))); assertFalse(query.matches(List.of("aasdf"))); assertFalse(query.matches(List.of("dsfb"))); assertTrue(query.matches(List.of("astuffb", "fffff"))); + + StringScriptFieldRegexpQuery ciQuery = new StringScriptFieldRegexpQuery( + randomScript(), + leafFactory, + "test", + "a.+b", + 0, + RegExp.ASCII_CASE_INSENSITIVE, + Operations.DEFAULT_MAX_DETERMINIZED_STATES + ); + assertTrue(ciQuery.matches(List.of("astuffB"))); + assertTrue(ciQuery.matches(List.of("Astuffb", "fffff"))); + } @Override @@ -98,6 +128,7 @@ public void testVisit() { "test", "a.+b", 0, + 0, Operations.DEFAULT_MAX_DETERMINIZED_STATES ); ByteRunAutomaton automaton = visitForSingleAutomata(query); diff --git a/x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/query/StringScriptFieldTermQueryTests.java b/x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/query/StringScriptFieldTermQueryTests.java index bfdd4eba41089..2c0d632286c64 100644 --- a/x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/query/StringScriptFieldTermQueryTests.java +++ b/x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/query/StringScriptFieldTermQueryTests.java @@ -23,12 +23,12 @@ public class StringScriptFieldTermQueryTests extends AbstractStringScriptFieldQueryTestCase { @Override protected StringScriptFieldTermQuery createTestInstance() { - return new StringScriptFieldTermQuery(randomScript(), leafFactory, randomAlphaOfLength(5), randomAlphaOfLength(6)); + return new StringScriptFieldTermQuery(randomScript(), leafFactory, randomAlphaOfLength(5), randomAlphaOfLength(6), randomBoolean()); } @Override protected StringScriptFieldTermQuery copy(StringScriptFieldTermQuery orig) { - return new StringScriptFieldTermQuery(orig.script(), leafFactory, orig.fieldName(), orig.term()); + return new StringScriptFieldTermQuery(orig.script(), leafFactory, orig.fieldName(), orig.term(), orig.caseInsensitive()); } @Override @@ -36,7 +36,8 @@ protected StringScriptFieldTermQuery mutate(StringScriptFieldTermQuery orig) { Script script = orig.script(); String fieldName = orig.fieldName(); String term = orig.term(); - switch (randomInt(2)) { + boolean caseInsensitive = orig.caseInsensitive(); + switch (randomInt(3)) { case 0: script = randomValueOtherThan(script, this::randomScript); break; @@ -46,18 +47,27 @@ protected StringScriptFieldTermQuery mutate(StringScriptFieldTermQuery orig) { case 2: term += "modified"; break; + case 3: + caseInsensitive = !caseInsensitive; + break; default: fail(); } - return new StringScriptFieldTermQuery(script, leafFactory, fieldName, term); + return new StringScriptFieldTermQuery(script, leafFactory, fieldName, term, caseInsensitive); } @Override public void testMatches() { - StringScriptFieldTermQuery query = new StringScriptFieldTermQuery(randomScript(), leafFactory, "test", "foo"); + StringScriptFieldTermQuery query = new StringScriptFieldTermQuery(randomScript(), leafFactory, "test", "foo", false); assertTrue(query.matches(List.of("foo"))); + assertFalse(query.matches(List.of("foO"))); assertFalse(query.matches(List.of("bar"))); assertTrue(query.matches(List.of("foo", "bar"))); + + StringScriptFieldTermQuery ciQuery = new StringScriptFieldTermQuery(randomScript(), leafFactory, "test", "foo", true); + assertTrue(ciQuery.matches(List.of("Foo"))); + assertTrue(ciQuery.matches(List.of("fOo", "bar"))); + } @Override diff --git a/x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/query/StringScriptFieldWildcardQueryTests.java b/x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/query/StringScriptFieldWildcardQueryTests.java index e7c5a62911180..589666c79f013 100644 --- a/x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/query/StringScriptFieldWildcardQueryTests.java +++ b/x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/query/StringScriptFieldWildcardQueryTests.java @@ -17,12 +17,18 @@ public class StringScriptFieldWildcardQueryTests extends AbstractStringScriptFieldQueryTestCase { @Override protected StringScriptFieldWildcardQuery createTestInstance() { - return new StringScriptFieldWildcardQuery(randomScript(), leafFactory, randomAlphaOfLength(5), randomAlphaOfLength(6)); + return new StringScriptFieldWildcardQuery( + randomScript(), + leafFactory, + randomAlphaOfLength(5), + randomAlphaOfLength(6), + randomBoolean() + ); } @Override protected StringScriptFieldWildcardQuery copy(StringScriptFieldWildcardQuery orig) { - return new StringScriptFieldWildcardQuery(orig.script(), leafFactory, orig.fieldName(), orig.pattern()); + return new StringScriptFieldWildcardQuery(orig.script(), leafFactory, orig.fieldName(), orig.pattern(), orig.caseInsensitive()); } @Override @@ -30,7 +36,8 @@ protected StringScriptFieldWildcardQuery mutate(StringScriptFieldWildcardQuery o Script script = orig.script(); String fieldName = orig.fieldName(); String pattern = orig.pattern(); - switch (randomInt(2)) { + boolean caseInsensitive = orig.caseInsensitive(); + switch (randomInt(3)) { case 0: script = randomValueOtherThan(script, this::randomScript); break; @@ -40,22 +47,31 @@ protected StringScriptFieldWildcardQuery mutate(StringScriptFieldWildcardQuery o case 2: pattern += "modified"; break; + case 3: + caseInsensitive = !caseInsensitive; + break; default: fail(); } - return new StringScriptFieldWildcardQuery(script, leafFactory, fieldName, pattern); + return new StringScriptFieldWildcardQuery(script, leafFactory, fieldName, pattern, caseInsensitive); } @Override public void testMatches() { - StringScriptFieldWildcardQuery query = new StringScriptFieldWildcardQuery(randomScript(), leafFactory, "test", "a*b"); + StringScriptFieldWildcardQuery query = new StringScriptFieldWildcardQuery(randomScript(), leafFactory, "test", "a*b", false); assertTrue(query.matches(List.of("astuffb"))); + assertFalse(query.matches(List.of("Astuffb"))); assertFalse(query.matches(List.of("fffff"))); assertFalse(query.matches(List.of("a"))); assertFalse(query.matches(List.of("b"))); assertFalse(query.matches(List.of("aasdf"))); assertFalse(query.matches(List.of("dsfb"))); assertTrue(query.matches(List.of("astuffb", "fffff"))); + + StringScriptFieldWildcardQuery ciQuery = new StringScriptFieldWildcardQuery(randomScript(), leafFactory, "test", "a*b", true); + assertTrue(ciQuery.matches(List.of("Astuffb"))); + assertTrue(ciQuery.matches(List.of("astuffB", "fffff"))); + } @Override @@ -65,7 +81,7 @@ protected void assertToString(StringScriptFieldWildcardQuery query) { @Override public void testVisit() { - StringScriptFieldWildcardQuery query = new StringScriptFieldWildcardQuery(randomScript(), leafFactory, "test", "a*b"); + StringScriptFieldWildcardQuery query = new StringScriptFieldWildcardQuery(randomScript(), leafFactory, "test", "a*b", false); ByteRunAutomaton automaton = visitForSingleAutomata(query); BytesRef term = new BytesRef("astuffb"); assertTrue(automaton.run(term.bytes, term.offset, term.length)); diff --git a/x-pack/plugin/wildcard/src/main/java/org/elasticsearch/xpack/wildcard/mapper/WildcardFieldMapper.java b/x-pack/plugin/wildcard/src/main/java/org/elasticsearch/xpack/wildcard/mapper/WildcardFieldMapper.java index 397a6f3204f9d..026b3f441ec66 100644 --- a/x-pack/plugin/wildcard/src/main/java/org/elasticsearch/xpack/wildcard/mapper/WildcardFieldMapper.java +++ b/x-pack/plugin/wildcard/src/main/java/org/elasticsearch/xpack/wildcard/mapper/WildcardFieldMapper.java @@ -40,6 +40,7 @@ import org.elasticsearch.common.geo.ShapeRelation; import org.elasticsearch.common.lucene.BytesRefs; import org.elasticsearch.common.lucene.Lucene; +import org.elasticsearch.common.lucene.search.AutomatonQueries; import org.elasticsearch.common.time.DateMathParser; import org.elasticsearch.common.unit.Fuzziness; import org.elasticsearch.common.xcontent.XContentBuilder; @@ -218,7 +219,7 @@ public WildcardFieldType(String name, FieldType fieldType, Map m } @Override - public Query wildcardQuery(String wildcardPattern, RewriteMethod method, QueryShardContext context) { + public Query wildcardQuery(String wildcardPattern, RewriteMethod method, boolean caseInsensitive, QueryShardContext context) { String ngramIndexPattern = addLineEndChars(toLowerCase(wildcardPattern)); @@ -276,7 +277,11 @@ public Query wildcardQuery(String wildcardPattern, RewriteMethod method, QuerySh clauseCount++; } Supplier deferredAutomatonSupplier = () -> { - return WildcardQuery.toAutomaton(new Term(name(), wildcardPattern)); + if(caseInsensitive) { + return AutomatonQueries.toCaseInsensitiveWildcardAutomaton(new Term(name(), wildcardPattern), Integer.MAX_VALUE); + } else { + return WildcardQuery.toAutomaton(new Term(name(), wildcardPattern)); + } }; AutomatonQueryOnBinaryDv verifyingQuery = new AutomatonQueryOnBinaryDv(name(), wildcardPattern, deferredAutomatonSupplier); if (clauseCount > 0) { @@ -845,7 +850,7 @@ public Query existsQuery(QueryShardContext context) { @Override public Query termQuery(Object value, QueryShardContext context) { String searchTerm = BytesRefs.toString(value); - return wildcardQuery(escapeWildcardSyntax(searchTerm), MultiTermQuery.CONSTANT_SCORE_REWRITE, context); + return wildcardQuery(escapeWildcardSyntax(searchTerm), MultiTermQuery.CONSTANT_SCORE_REWRITE, false, context); } private String escapeWildcardSyntax(String term) { @@ -862,10 +867,16 @@ private String escapeWildcardSyntax(String term) { } return result.toString(); } + + @Override + public Query termQueryCaseInsensitive(Object value, QueryShardContext context) { + String searchTerm = BytesRefs.toString(value); + return wildcardQuery(escapeWildcardSyntax(searchTerm), MultiTermQuery.CONSTANT_SCORE_REWRITE, true, context); + } @Override - public Query prefixQuery(String value, MultiTermQuery.RewriteMethod method, QueryShardContext context) { - return wildcardQuery(escapeWildcardSyntax(value) + "*", method, context); + public Query prefixQuery(String value, MultiTermQuery.RewriteMethod method, boolean caseInsensitive, QueryShardContext context) { + return wildcardQuery(escapeWildcardSyntax(value) + "*", method, caseInsensitive, context); } @Override @@ -876,7 +887,7 @@ public Query termsQuery(List values, QueryShardContext context) { } return new ConstantScoreQuery(bq.build()); } - + @Override public IndexFieldData.Builder fielddataBuilder(String fullyQualifiedIndexName, Supplier searchLookup) { failIfNoDocValues(); diff --git a/x-pack/plugin/wildcard/src/test/java/org/elasticsearch/xpack/wildcard/mapper/WildcardFieldMapperTests.java b/x-pack/plugin/wildcard/src/test/java/org/elasticsearch/xpack/wildcard/mapper/WildcardFieldMapperTests.java index 3836547430c3f..fbeca28b9af0b 100644 --- a/x-pack/plugin/wildcard/src/test/java/org/elasticsearch/xpack/wildcard/mapper/WildcardFieldMapperTests.java +++ b/x-pack/plugin/wildcard/src/test/java/org/elasticsearch/xpack/wildcard/mapper/WildcardFieldMapperTests.java @@ -264,18 +264,21 @@ public void testSearchResultsVersusKeywordField() throws IOException { switch (randomInt(4)) { case 0: pattern = getRandomWildcardPattern(); - wildcardFieldQuery = wildcardFieldType.fieldType().wildcardQuery(pattern, null, MOCK_QSC); - keywordFieldQuery = keywordFieldType.fieldType().wildcardQuery(pattern, null, MOCK_QSC); + boolean caseInsensitive = randomBoolean(); + wildcardFieldQuery = wildcardFieldType.fieldType().wildcardQuery(pattern, null, caseInsensitive, MOCK_QSC); + keywordFieldQuery = keywordFieldType.fieldType().wildcardQuery(pattern, null, caseInsensitive, MOCK_QSC); break; case 1: pattern = getRandomRegexPattern(values); - wildcardFieldQuery = wildcardFieldType.fieldType().regexpQuery(pattern, RegExp.ALL, 0, 20000, null, MOCK_QSC); - keywordFieldQuery = keywordFieldType.fieldType().regexpQuery(pattern, RegExp.ALL, 0,20000, null, MOCK_QSC); + int matchFlags = randomBoolean()? 0 : RegExp.ASCII_CASE_INSENSITIVE; + wildcardFieldQuery = wildcardFieldType.fieldType().regexpQuery(pattern, RegExp.ALL, matchFlags, 20000, null, MOCK_QSC); + keywordFieldQuery = keywordFieldType.fieldType().regexpQuery(pattern, RegExp.ALL, matchFlags,20000, null, MOCK_QSC); break; case 2: pattern = randomABString(5); - wildcardFieldQuery = wildcardFieldType.fieldType().prefixQuery(pattern, null, MOCK_QSC); - keywordFieldQuery = keywordFieldType.fieldType().prefixQuery(pattern, null, MOCK_QSC); + boolean caseInsensitivePrefix = randomBoolean(); + wildcardFieldQuery = wildcardFieldType.fieldType().prefixQuery(pattern, null, caseInsensitivePrefix, MOCK_QSC); + keywordFieldQuery = keywordFieldType.fieldType().prefixQuery(pattern, null, caseInsensitivePrefix, MOCK_QSC); break; case 3: int edits = randomInt(2);