Browse Source

Search - add case insensitive flag for "term" family of queries (#61596)

Adds  case insensitive flag for term, prefix, and wildcard queries

Closes #61546
markharwood 5 years ago
parent
commit
fe9145fa5e
46 changed files with 914 additions and 134 deletions
  1. 4 0
      docs/reference/query-dsl/prefix-query.asciidoc
  2. 6 2
      docs/reference/query-dsl/term-query.asciidoc
  3. 5 1
      docs/reference/query-dsl/wildcard-query.asciidoc
  4. 10 7
      modules/mapper-extras/src/main/java/org/elasticsearch/index/mapper/SearchAsYouTypeFieldMapper.java
  5. 3 1
      plugins/analysis-icu/src/main/java/org/elasticsearch/index/mapper/ICUCollationKeywordFieldMapper.java
  6. 14 0
      server/src/main/java/org/elasticsearch/common/Strings.java
  7. 152 0
      server/src/main/java/org/elasticsearch/common/lucene/search/AutomatonQueries.java
  8. 29 5
      server/src/main/java/org/elasticsearch/common/regex/Regex.java
  9. 18 6
      server/src/main/java/org/elasticsearch/index/mapper/ConstantFieldType.java
  10. 7 1
      server/src/main/java/org/elasticsearch/index/mapper/IndexFieldMapper.java
  11. 22 2
      server/src/main/java/org/elasticsearch/index/mapper/MappedFieldType.java
  12. 17 3
      server/src/main/java/org/elasticsearch/index/mapper/StringFieldType.java
  13. 11 0
      server/src/main/java/org/elasticsearch/index/mapper/TermBasedFieldType.java
  14. 14 5
      server/src/main/java/org/elasticsearch/index/mapper/TextFieldMapper.java
  15. 4 1
      server/src/main/java/org/elasticsearch/index/mapper/TypeFieldMapper.java
  16. 7 2
      server/src/main/java/org/elasticsearch/index/query/BaseTermQueryBuilder.java
  17. 44 5
      server/src/main/java/org/elasticsearch/index/query/PrefixQueryBuilder.java
  18. 75 2
      server/src/main/java/org/elasticsearch/index/query/TermQueryBuilder.java
  19. 43 5
      server/src/main/java/org/elasticsearch/index/query/WildcardQueryBuilder.java
  20. 7 0
      server/src/test/java/org/elasticsearch/common/regex/RegexTests.java
  21. 2 0
      server/src/test/java/org/elasticsearch/index/mapper/IndexFieldTypeTests.java
  22. 11 0
      server/src/test/java/org/elasticsearch/index/mapper/RangeFieldTypeTests.java
  23. 10 4
      server/src/test/java/org/elasticsearch/index/mapper/TextFieldTypeTests.java
  24. 8 0
      server/src/test/java/org/elasticsearch/index/mapper/TypeFieldTypeTests.java
  25. 15 1
      server/src/test/java/org/elasticsearch/index/query/PrefixQueryBuilderTests.java
  26. 26 4
      server/src/test/java/org/elasticsearch/index/query/TermQueryBuilderTests.java
  27. 15 1
      server/src/test/java/org/elasticsearch/index/query/WildcardQueryBuilderTests.java
  28. 2 2
      x-pack/plugin/mapper-constant-keyword/src/main/java/org/elasticsearch/xpack/constantkeyword/mapper/ConstantKeywordFieldMapper.java
  29. 15 6
      x-pack/plugin/mapper-constant-keyword/src/test/java/org/elasticsearch/xpack/constantkeyword/mapper/ConstantKeywordFieldTypeTests.java
  30. 14 0
      x-pack/plugin/mapper-flattened/src/main/java/org/elasticsearch/xpack/flattened/mapper/FlatObjectFieldMapper.java
  31. 13 3
      x-pack/plugin/mapper-flattened/src/test/java/org/elasticsearch/xpack/flattened/mapper/KeyedFlatObjectFieldTypeTests.java
  32. 5 0
      x-pack/plugin/mapper-flattened/src/test/java/org/elasticsearch/xpack/flattened/mapper/RootFlatObjectFieldTypeTests.java
  33. 2 2
      x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/mapper/AbstractScriptMappedFieldType.java
  34. 17 3
      x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/mapper/BooleanScriptMappedFieldType.java
  35. 37 6
      x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/mapper/KeywordScriptMappedFieldType.java
  36. 48 5
      x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/query/StringScriptFieldPrefixQuery.java
  37. 15 8
      x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/query/StringScriptFieldRegexpQuery.java
  38. 20 4
      x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/query/StringScriptFieldTermQuery.java
  39. 25 4
      x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/query/StringScriptFieldWildcardQuery.java
  40. 1 1
      x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/mapper/KeywordScriptMappedFieldTypeTests.java
  41. 22 4
      x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/query/StringScriptFieldPrefixQueryTests.java
  42. 36 5
      x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/query/StringScriptFieldRegexpQueryTests.java
  43. 15 5
      x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/query/StringScriptFieldTermQueryTests.java
  44. 22 6
      x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/query/StringScriptFieldWildcardQueryTests.java
  45. 17 6
      x-pack/plugin/wildcard/src/main/java/org/elasticsearch/xpack/wildcard/mapper/WildcardFieldMapper.java
  46. 9 6
      x-pack/plugin/wildcard/src/test/java/org/elasticsearch/xpack/wildcard/mapper/WildcardFieldMapperTests.java

+ 4 - 0
docs/reference/query-dsl/prefix-query.asciidoc

@@ -41,6 +41,10 @@ provided `<field>`.
 (Optional, string) Method used to rewrite the query. For valid values and more
 information, see the <<query-dsl-multi-term-rewrite, `rewrite` parameter>>.
 
+`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
 

+ 6 - 2
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/]
---
+--

+ 5 - 1
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
 <<query-dsl-multi-term-rewrite, `rewrite` parameter>>.
 
+`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

+ 10 - 7
modules/mapper-extras/src/main/java/org/elasticsearch/index/mapper/SearchAsYouTypeFieldMapper.java

@@ -281,11 +281,11 @@ public class SearchAsYouTypeFieldMapper extends FieldMapper {
         }
 
         @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 @@ public class SearchAsYouTypeFieldMapper extends FieldMapper {
         }
 
         @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<Automaton> automata = new ArrayList<>();
@@ -507,11 +510,11 @@ public class SearchAsYouTypeFieldMapper extends FieldMapper {
         }
 
         @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) {

+ 3 - 1
plugins/analysis-icu/src/main/java/org/elasticsearch/index/mapper/ICUCollationKeywordFieldMapper.java

@@ -136,13 +136,15 @@ public class ICUCollationKeywordFieldMapper extends FieldMapper {
         }
 
         @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.");
         }

+ 14 - 0
server/src/main/java/org/elasticsearch/common/Strings.java

@@ -879,4 +879,18 @@ public class Strings {
             return sb.toString();
         }
     }
+    
+    public static String toLowercaseAscii(String in) {
+        StringBuilder out = new StringBuilder();
+        Iterator<Integer> 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();
+    }    
 }

+ 152 - 0
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<Automaton> list = new ArrayList<>();
+        Iterator<Integer> 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<Automaton> 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<Automaton> list = new ArrayList<>();
+        Iterator<Integer> 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;
+    }
+}

+ 29 - 5
server/src/main/java/org/elasticsearch/common/regex/Regex.java

@@ -79,15 +79,39 @@ public class Regex {
      * 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 class Regex {
                 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 class Regex {
         }
         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

+ 18 - 6
server/src/main/java/org/elasticsearch/index/mapper/ConstantFieldType.java

@@ -59,7 +59,7 @@ public abstract class ConstantFieldType extends MappedFieldType {
      * 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 @@ public abstract class ConstantFieldType extends MappedFieldType {
     @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 abstract class ConstantFieldType extends MappedFieldType {
     @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();

+ 7 - 1
server/src/main/java/org/elasticsearch/index/mapper/IndexFieldMapper.java

@@ -21,6 +21,7 @@ package org.elasticsearch.index.mapper;
 
 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 class IndexFieldMapper extends MetadataFieldMapper {
         }
 
         @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);
         }
 

+ 22 - 2
server/src/main/java/org/elasticsearch/index/mapper/MappedFieldType.java

@@ -176,6 +176,13 @@ public abstract class MappedFieldType {
      */
     // 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 abstract class MappedFieldType {
             + "] 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() + "]");
     }

+ 17 - 3
server/src/main/java/org/elasticsearch/index/mapper/StringFieldType.java

@@ -21,6 +21,7 @@ package org.elasticsearch.index.mapper;
 
 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.BytesRef;
 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 abstract class StringFieldType extends TermBasedFieldType {
     }
 
     @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 abstract class StringFieldType extends TermBasedFieldType {
     }
 
     @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 abstract class StringFieldType extends TermBasedFieldType {
         } 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;

+ 11 - 0
server/src/main/java/org/elasticsearch/index/mapper/TermBasedFieldType.java

@@ -26,6 +26,7 @@ import org.apache.lucene.search.TermInSetQuery;
 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 @@ abstract class TermBasedFieldType extends SimpleMappedFieldType {
         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();

+ 14 - 5
server/src/main/java/org/elasticsearch/index/mapper/TextFieldMapper.java

@@ -60,6 +60,7 @@ import org.apache.lucene.util.automaton.Operations;
 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 @@ public class TextFieldMapper extends FieldMapper {
         }
 
         @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<Automaton> 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 class TextFieldMapper extends FieldMapper {
         }
 
         @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);

+ 4 - 1
server/src/main/java/org/elasticsearch/index/mapper/TypeFieldMapper.java

@@ -77,7 +77,10 @@ public class TypeFieldMapper extends MetadataFieldMapper {
         }
 
         @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);
         }
     }

+ 7 - 2
server/src/main/java/org/elasticsearch/index/query/BaseTermQueryBuilder.java

@@ -152,18 +152,23 @@ public abstract class BaseTermQueryBuilder<QB extends BaseTermQueryBuilder<QB>>
         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);
     }

+ 44 - 5
server/src/main/java/org/elasticsearch/index/query/PrefixQueryBuilder.java

@@ -23,6 +23,7 @@ import org.apache.lucene.search.MatchAllDocsQuery;
 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<PrefixQueryBuilder>
     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 class PrefixQueryBuilder extends AbstractQueryBuilder<PrefixQueryBuilder>
         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 @@ public class PrefixQueryBuilder extends AbstractQueryBuilder<PrefixQueryBuilder>
         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 class PrefixQueryBuilder extends AbstractQueryBuilder<PrefixQueryBuilder>
     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 class PrefixQueryBuilder extends AbstractQueryBuilder<PrefixQueryBuilder>
         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 class PrefixQueryBuilder extends AbstractQueryBuilder<PrefixQueryBuilder>
 
         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 class PrefixQueryBuilder extends AbstractQueryBuilder<PrefixQueryBuilder>
                             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 class PrefixQueryBuilder extends AbstractQueryBuilder<PrefixQueryBuilder>
             }
         }
 
-        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 @@ public class PrefixQueryBuilder extends AbstractQueryBuilder<PrefixQueryBuilder>
                 // 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 @@ public class PrefixQueryBuilder extends AbstractQueryBuilder<PrefixQueryBuilder>
         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);
     }
 }

+ 75 - 2
server/src/main/java/org/elasticsearch/index/query/TermQueryBuilder.java

@@ -22,20 +22,31 @@ package org.elasticsearch.index.query;
 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<TermQueryBuilder> {
     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 class TermQueryBuilder extends BaseTermQueryBuilder<TermQueryBuilder> {
     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 class TermQueryBuilder extends BaseTermQueryBuilder<TermQueryBuilder> {
                             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 class TermQueryBuilder extends BaseTermQueryBuilder<TermQueryBuilder> {
         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 @@ public class TermQueryBuilder extends BaseTermQueryBuilder<TermQueryBuilder> {
                 // 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 @@ public class TermQueryBuilder extends BaseTermQueryBuilder<TermQueryBuilder> {
         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);
+    }    
+    
 }

+ 43 - 5
server/src/main/java/org/elasticsearch/index/query/WildcardQueryBuilder.java

@@ -23,6 +23,7 @@ import org.apache.lucene.search.MatchAllDocsQuery;
 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<WildcardQueryBuil
 
     private String rewrite;
 
+    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;
+
     /**
      * Implements the wildcard search query. Supported wildcards are {@code *}, which
      * matches any character sequence (including the empty one), and {@code ?},
@@ -89,6 +94,9 @@ public class WildcardQueryBuilder extends AbstractQueryBuilder<WildcardQueryBuil
         fieldName = in.readString();
         value = in.readString();
         rewrite = in.readOptionalString();
+        if (in.getVersion().onOrAfter(Version.V_8_0_0)) {
+            caseInsensitive = in.readBoolean();
+        }        
     }
 
     @Override
@@ -96,6 +104,9 @@ public class WildcardQueryBuilder extends AbstractQueryBuilder<WildcardQueryBuil
         out.writeString(fieldName);
         out.writeString(value);
         out.writeOptionalString(rewrite);
+        if (out.getVersion().onOrAfter(Version.V_8_0_0)) {
+            out.writeBoolean(caseInsensitive);
+        }    
     }
 
     @Override
@@ -115,6 +126,18 @@ public class WildcardQueryBuilder extends AbstractQueryBuilder<WildcardQueryBuil
     public String rewrite() {
         return this.rewrite;
     }
+    
+    public WildcardQueryBuilder 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;
+    }        
 
     @Override
     public String getWriteableName() {
@@ -129,6 +152,9 @@ public class WildcardQueryBuilder extends AbstractQueryBuilder<WildcardQueryBuil
         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();
@@ -139,6 +165,7 @@ public class WildcardQueryBuilder extends AbstractQueryBuilder<WildcardQueryBuil
         String rewrite = null;
         String value = null;
         float boost = AbstractQueryBuilder.DEFAULT_BOOST;
+        boolean caseInsensitive = DEFAULT_CASE_INSENSITIVITY;        
         String queryName = null;
         String currentFieldName = null;
         XContentParser.Token token;
@@ -160,6 +187,12 @@ public class WildcardQueryBuilder extends AbstractQueryBuilder<WildcardQueryBuil
                             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 if (AbstractQueryBuilder.NAME_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
                             queryName = parser.text();
                         } else {
@@ -175,10 +208,14 @@ public class WildcardQueryBuilder extends AbstractQueryBuilder<WildcardQueryBuil
             }
         }
 
-        return new WildcardQueryBuilder(fieldName, value)
+        WildcardQueryBuilder result = new WildcardQueryBuilder(fieldName, value)
                 .rewrite(rewrite)
                 .boost(boost)
                 .queryName(queryName);
+        if (caseInsensitive) {
+            result.caseInsensitive(caseInsensitive);            
+        }
+        return result;
     }    
     
     @Override
@@ -192,7 +229,7 @@ public class WildcardQueryBuilder extends AbstractQueryBuilder<WildcardQueryBuil
                 // 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.wildcardQuery(value, null, context); // the rewrite method doesn't matter
+                Query query = fieldType.wildcardQuery(value, null, caseInsensitive, context); // the rewrite method doesn't matter
                 if (query instanceof MatchAllDocsQuery) {
                     return new MatchAllQueryBuilder();
                 } else if (query instanceof MatchNoDocsQuery) {
@@ -216,18 +253,19 @@ public class WildcardQueryBuilder extends AbstractQueryBuilder<WildcardQueryBuil
 
         MultiTermQuery.RewriteMethod method = QueryParsers.parseRewriteMethod(
             rewrite, null, LoggingDeprecationHandler.INSTANCE);
-        return fieldType.wildcardQuery(value, method, context);
+        return fieldType.wildcardQuery(value, method, caseInsensitive, context);
     }
 
     @Override
     protected int doHashCode() {
-        return Objects.hash(fieldName, value, rewrite);
+        return Objects.hash(fieldName, value, rewrite, caseInsensitive);
     }
 
     @Override
     protected boolean doEquals(WildcardQueryBuilder 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);
     }
 }

+ 7 - 0
server/src/test/java/org/elasticsearch/common/regex/RegexTests.java

@@ -20,6 +20,7 @@ package org.elasticsearch.common.regex;
 
 import org.elasticsearch.test.ESTestCase;
 
+import java.util.Locale;
 import java.util.Random;
 import java.util.regex.Pattern;
 
@@ -54,13 +55,17 @@ public class RegexTests extends ESTestCase {
 
     public void testDoubleWildcardMatch() {
         assertTrue(Regex.simpleMatch("ddd", "ddd"));
+        assertTrue(Regex.simpleMatch("ddd", "Ddd", true));
+        assertFalse(Regex.simpleMatch("ddd", "Ddd"));
         assertTrue(Regex.simpleMatch("d*d*d", "dadd"));
         assertTrue(Regex.simpleMatch("**ddd", "dddd"));
+        assertTrue(Regex.simpleMatch("**ddD", "dddd", true));
         assertFalse(Regex.simpleMatch("**ddd", "fff"));
         assertTrue(Regex.simpleMatch("fff*ddd", "fffabcddd"));
         assertTrue(Regex.simpleMatch("fff**ddd", "fffabcddd"));
         assertFalse(Regex.simpleMatch("fff**ddd", "fffabcdd"));
         assertTrue(Regex.simpleMatch("fff*******ddd", "fffabcddd"));
+        assertTrue(Regex.simpleMatch("fff*******ddd", "FffAbcdDd", true));
         assertFalse(Regex.simpleMatch("fff******ddd", "fffabcdd"));
     }
 
@@ -76,6 +81,8 @@ public class RegexTests extends ESTestCase {
                 pattern = pattern.substring(0, shrinkStart) + "*" + pattern.substring(shrinkEnd);
             }
             assertTrue("[" + pattern + "] should match [" + matchingString + "]", Regex.simpleMatch(pattern, matchingString));
+            assertTrue("[" + pattern + "] should match [" + matchingString.toUpperCase(Locale.ROOT) + "]", 
+                Regex.simpleMatch(pattern, matchingString.toUpperCase(Locale.ROOT), true));
 
             // construct a pattern that does not match this string by inserting a non-matching character (a digit)
             final int insertPos = between(0, pattern.length());

+ 2 - 0
server/src/test/java/org/elasticsearch/index/mapper/IndexFieldTypeTests.java

@@ -46,7 +46,9 @@ public class IndexFieldTypeTests extends ESTestCase {
         MappedFieldType ft = IndexFieldMapper.IndexFieldType.INSTANCE;
 
         assertEquals(new MatchAllDocsQuery(), ft.wildcardQuery("ind*x", null, createContext()));
+        assertEquals(new MatchAllDocsQuery(), ft.wildcardQuery("iNd*x", null, true, createContext()));
         assertEquals(new MatchNoDocsQuery(), ft.wildcardQuery("other_ind*x", null, createContext()));
+        assertEquals(new MatchNoDocsQuery(), ft.wildcardQuery("Other_ind*x", null, true, createContext()));
     }
 
     public void testRegexpQuery() {

+ 11 - 0
server/src/test/java/org/elasticsearch/index/mapper/RangeFieldTypeTests.java

@@ -42,6 +42,7 @@ import org.elasticsearch.index.IndexSettings;
 import org.elasticsearch.index.mapper.DateFieldMapper.DateFieldType;
 import org.elasticsearch.index.mapper.RangeFieldMapper.RangeFieldType;
 import org.elasticsearch.index.query.QueryShardContext;
+import org.elasticsearch.index.query.QueryShardException;
 import org.elasticsearch.test.IndexSettingsModule;
 import org.joda.time.DateTime;
 import org.junit.Before;
@@ -481,4 +482,14 @@ public class RangeFieldTypeTests extends FieldTypeTestCase {
         assertEquals(getExpectedRangeQuery(relation, value, value, includeLower, includeUpper),
             ft.termQuery(value, context));
     }
+    
+    public void testCaseInsensitiveQuery() throws Exception {
+        QueryShardContext context = createContext();
+        RangeFieldType ft = createDefaultFieldType(FIELDNAME);
+
+        Object value = nextFrom();
+        QueryShardException ex = expectThrows(QueryShardException.class,
+            () ->   ft.termQueryCaseInsensitive(value, context));
+        assertTrue(ex.getMessage().contains("does not support case insensitive term queries"));
+    }
 }

+ 10 - 4
server/src/test/java/org/elasticsearch/index/mapper/TextFieldTypeTests.java

@@ -36,6 +36,7 @@ import org.apache.lucene.util.automaton.Automaton;
 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 class TextFieldTypeTests extends FieldTypeTestCase {
         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()));
 

+ 8 - 0
server/src/test/java/org/elasticsearch/index/mapper/TypeFieldTypeTests.java

@@ -35,7 +35,15 @@ public class TypeFieldTypeTests extends ESTestCase {
         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);
+    
     }
 }

+ 15 - 1
server/src/test/java/org/elasticsearch/index/query/PrefixQueryBuilderTests.java

@@ -44,6 +44,13 @@ public class PrefixQueryBuilderTests extends AbstractQueryTestCase<PrefixQueryBu
         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 class PrefixQueryBuilderTests extends AbstractQueryTestCase<PrefixQueryBu
 
     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);

+ 26 - 4
server/src/test/java/org/elasticsearch/index/query/TermQueryBuilderTests.java

@@ -22,6 +22,7 @@ package org.elasticsearch.index.query;
 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 @@ public class TermQueryBuilderTests extends AbstractTermQueryTestCase<TermQueryBu
      */
     @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 @@ public class TermQueryBuilderTests extends AbstractTermQueryTestCase<TermQueryBu
             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 class TermQueryBuilderTests extends AbstractTermQueryTestCase<TermQueryBu
                 "    \"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" +
                 "}";

+ 15 - 1
server/src/test/java/org/elasticsearch/index/query/WildcardQueryBuilderTests.java

@@ -41,6 +41,13 @@ public class WildcardQueryBuilderTests extends AbstractQueryTestCase<WildcardQue
         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 class WildcardQueryBuilderTests extends AbstractQueryTestCase<WildcardQue
     }
 
     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());

+ 2 - 2
x-pack/plugin/mapper-constant-keyword/src/main/java/org/elasticsearch/xpack/constantkeyword/mapper/ConstantKeywordFieldMapper.java

@@ -147,11 +147,11 @@ public class ConstantKeywordFieldMapper extends FieldMapper {
         }
 
         @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

+ 15 - 6
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 class ConstantKeywordFieldTypeTests extends FieldTypeTestCase {
 
     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() {

+ 14 - 0
x-pack/plugin/mapper-flattened/src/main/java/org/elasticsearch/xpack/flattened/mapper/FlatObjectFieldMapper.java

@@ -12,6 +12,7 @@ import org.apache.lucene.index.IndexOptions;
 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.SortField;
 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;
@@ -292,11 +294,23 @@ public final class FlatObjectFieldMapper extends DynamicKeyFieldMapper {
         @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) {

+ 13 - 3
x-pack/plugin/mapper-flattened/src/test/java/org/elasticsearch/xpack/flattened/mapper/KeyedFlatObjectFieldTypeTests.java

@@ -15,6 +15,7 @@ import org.apache.lucene.search.TermQuery;
 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 class KeyedFlatObjectFieldTypeTests extends FieldTypeTestCase {
         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 class KeyedFlatObjectFieldTypeTests extends FieldTypeTestCase {
         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 class KeyedFlatObjectFieldTypeTests extends FieldTypeTestCase {
         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());
     }
 }

+ 5 - 0
x-pack/plugin/mapper-flattened/src/test/java/org/elasticsearch/xpack/flattened/mapper/RootFlatObjectFieldTypeTests.java

@@ -16,6 +16,7 @@ import org.apache.lucene.search.TermRangeQuery;
 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 class RootFlatObjectFieldTypeTests extends FieldTypeTestCase {
         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,

+ 2 - 2
x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/mapper/AbstractScriptMappedFieldType.java

@@ -133,12 +133,12 @@ abstract class AbstractScriptMappedFieldType<LeafFactory> extends MappedFieldTyp
     }
 
     @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"));
     }
 

+ 17 - 3
x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/mapper/BooleanScriptMappedFieldType.java

@@ -10,6 +10,7 @@ import org.apache.lucene.search.MatchNoDocsQuery;
 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 class BooleanScriptMappedFieldType extends AbstractScriptMappedFieldType<
         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 class BooleanScriptMappedFieldType extends AbstractScriptMappedFieldType<
         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 @@ public class BooleanScriptMappedFieldType extends AbstractScriptMappedFieldType<
         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 @@ public class BooleanScriptMappedFieldType extends AbstractScriptMappedFieldType<
         } else {
             sValue = value.toString();
         }
+        if (caseInsensitive) {
+            sValue = Strings.toLowercaseAscii(sValue);
+        }
         return Booleans.parseBoolean(sValue);
     }
 }

+ 37 - 6
x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/mapper/KeywordScriptMappedFieldType.java

@@ -88,9 +88,14 @@ public final class KeywordScriptMappedFieldType extends AbstractScriptMappedFiel
     }
 
     @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 final class KeywordScriptMappedFieldType extends AbstractScriptMappedFiel
         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 final class KeywordScriptMappedFieldType extends AbstractScriptMappedFiel
     }
 
     @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);
     }
 }

+ 48 - 5
x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/query/StringScriptFieldPrefixQuery.java

@@ -9,7 +9,9 @@ package org.elasticsearch.xpack.runtimefields.query;
 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 @@ import java.util.Objects;
 
 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<String> values) {
         for (String value : values) {
-            if (value != null && value.startsWith(prefix)) {
+            if (startsWith(value, prefix, caseInsensitive)) {
                 return true;
             }
         }
         return false;
     }
 
+    /**
+     * <p>Check if a String starts with a specified prefix (optionally case insensitive).</p>
+     *
+     * @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 <code>true</code> if the String starts with the prefix or
+     *  both <code>null</code>
+     */
+    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 class StringScriptFieldPrefixQuery extends AbstractStringScriptFieldQuery
 
     @Override
     public int hashCode() {
-        return Objects.hash(super.hashCode(), prefix);
+        return Objects.hash(super.hashCode(), prefix, caseInsensitive);
     }
 
     @Override
@@ -60,10 +99,14 @@ public class StringScriptFieldPrefixQuery extends AbstractStringScriptFieldQuery
             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;
+    }
 }

+ 15 - 8
x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/query/StringScriptFieldRegexpQuery.java

@@ -15,24 +15,27 @@ import java.util.Objects;
 
 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 class StringScriptFieldRegexpQuery extends AbstractStringScriptFieldAutom
 
     @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 class StringScriptFieldRegexpQuery extends AbstractStringScriptFieldAutom
             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;
     }
 }

+ 20 - 4
x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/query/StringScriptFieldTermQuery.java

@@ -16,16 +16,28 @@ import java.util.Objects;
 
 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<String> 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 class StringScriptFieldTermQuery extends AbstractStringScriptFieldQuery {
 
     @Override
     public int hashCode() {
-        return Objects.hash(super.hashCode(), term);
+        return Objects.hash(super.hashCode(), term, caseInsensitive);
     }
 
     @Override
@@ -56,10 +68,14 @@ public class StringScriptFieldTermQuery extends AbstractStringScriptFieldQuery {
             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;
+    }
 }

+ 25 - 4
x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/query/StringScriptFieldWildcardQuery.java

@@ -8,7 +8,9 @@ package org.elasticsearch.xpack.runtimefields.query;
 
 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 @@ import java.util.Objects;
 
 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 class StringScriptFieldWildcardQuery extends AbstractStringScriptFieldAut
 
     @Override
     public int hashCode() {
-        return Objects.hash(super.hashCode(), pattern);
+        return Objects.hash(super.hashCode(), pattern, caseInsensitive);
     }
 
     @Override
@@ -46,10 +63,14 @@ public class StringScriptFieldWildcardQuery extends AbstractStringScriptFieldAut
             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;
+    }
 }

+ 1 - 1
x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/mapper/KeywordScriptMappedFieldTypeTests.java

@@ -267,7 +267,7 @@ public class KeywordScriptMappedFieldTypeTests extends AbstractScriptMappedField
     }
 
     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

+ 22 - 4
x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/query/StringScriptFieldPrefixQueryTests.java

@@ -18,12 +18,18 @@ import static org.hamcrest.Matchers.is;
 public class StringScriptFieldPrefixQueryTests extends AbstractStringScriptFieldQueryTestCase<StringScriptFieldPrefixQuery> {
     @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 @@ public class StringScriptFieldPrefixQueryTests extends AbstractStringScriptField
         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 @@ public class StringScriptFieldPrefixQueryTests extends AbstractStringScriptField
             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

+ 36 - 5
x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/query/StringScriptFieldRegexpQueryTests.java

@@ -20,12 +20,14 @@ import static org.hamcrest.Matchers.is;
 public class StringScriptFieldRegexpQueryTests extends AbstractStringScriptFieldQueryTestCase<StringScriptFieldRegexpQuery> {
     @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 @@ public class StringScriptFieldRegexpQueryTests extends AbstractStringScriptField
             leafFactory,
             orig.fieldName(),
             orig.pattern(),
-            orig.flags(),
+            orig.syntaxFlags(),
+            orig.matchFlags(),
             Operations.DEFAULT_MAX_DETERMINIZED_STATES
         );
     }
@@ -47,8 +50,9 @@ public class StringScriptFieldRegexpQueryTests extends AbstractStringScriptField
         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 @@ public class StringScriptFieldRegexpQueryTests extends AbstractStringScriptField
                 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 class StringScriptFieldRegexpQueryTests extends AbstractStringScriptField
             "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 class StringScriptFieldRegexpQueryTests extends AbstractStringScriptField
             "test",
             "a.+b",
             0,
+            0,
             Operations.DEFAULT_MAX_DETERMINIZED_STATES
         );
         ByteRunAutomaton automaton = visitForSingleAutomata(query);

+ 15 - 5
x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/query/StringScriptFieldTermQueryTests.java

@@ -23,12 +23,12 @@ import static org.hamcrest.Matchers.equalTo;
 public class StringScriptFieldTermQueryTests extends AbstractStringScriptFieldQueryTestCase<StringScriptFieldTermQuery> {
     @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 @@ public class StringScriptFieldTermQueryTests extends AbstractStringScriptFieldQu
         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 @@ public class StringScriptFieldTermQueryTests extends AbstractStringScriptFieldQu
             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

+ 22 - 6
x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/query/StringScriptFieldWildcardQueryTests.java

@@ -17,12 +17,18 @@ import static org.hamcrest.Matchers.equalTo;
 public class StringScriptFieldWildcardQueryTests extends AbstractStringScriptFieldQueryTestCase<StringScriptFieldWildcardQuery> {
     @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 @@ public class StringScriptFieldWildcardQueryTests extends AbstractStringScriptFie
         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 @@ public class StringScriptFieldWildcardQueryTests extends AbstractStringScriptFie
             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 @@ public class StringScriptFieldWildcardQueryTests extends AbstractStringScriptFie
 
     @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));

+ 17 - 6
x-pack/plugin/wildcard/src/main/java/org/elasticsearch/xpack/wildcard/mapper/WildcardFieldMapper.java

@@ -40,6 +40,7 @@ import org.elasticsearch.ElasticsearchParseException;
 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 class WildcardFieldMapper extends FieldMapper {
         }
 
         @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 class WildcardFieldMapper extends FieldMapper {
                 clauseCount++;
             }
             Supplier<Automaton> 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 class WildcardFieldMapper extends FieldMapper {
         @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 @@ public class WildcardFieldMapper extends FieldMapper {
             }
             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 class WildcardFieldMapper extends FieldMapper {
             }
             return new ConstantScoreQuery(bq.build());
         }
-
+        
         @Override
         public IndexFieldData.Builder fielddataBuilder(String fullyQualifiedIndexName, Supplier<SearchLookup> searchLookup) {
             failIfNoDocValues();

+ 9 - 6
x-pack/plugin/wildcard/src/test/java/org/elasticsearch/xpack/wildcard/mapper/WildcardFieldMapperTests.java

@@ -264,18 +264,21 @@ public class WildcardFieldMapperTests extends ESTestCase {
             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);