Pārlūkot izejas kodu

Ignore fields with no content when querying wildcard fields (#81985)

The query_string, simple_query_string, combined_fields and multi_match
queries all allow you to query a large number of fields, based on wildcard field name
matches. By default, the wildcard match is *, meaning that these queries will try
and match against every single field in your index. This can cause problems if you
have a very large number of fields defined, and your elasticsearch instance has a
fairly low maximum query clause count.

In many cases, users may have many more fields defined in their mappings than are
actually populated in their index. For example, indexes using ECS mappings may
well only use a small subset of these mapped fields for their data. In these situations,
we can put a limit on the number of fields being searched by doing a quick check of
the Lucene index metadata to see if a mapped field actually has content in the index;
if it doesn't exist, we can trivially skip it.

This commit adds a check to QueryParserHelper.resolveMappingField() that strips
out fields with no content if the field name to resolve contains a wildcard. The check
is delegated down to MappedFieldType and by default returns `true`, but the standard
indexable field types (numeric, text, keyword, range, etc) will check their fieldnames
against the names in the underlying lucene FieldInfos and return `false` if they do not
appear there.
Alan Woodward 3 gadi atpakaļ
vecāks
revīzija
d11973b96d
31 mainītis faili ar 1040 papildinājumiem un 535 dzēšanām
  1. 200 0
      benchmarks/src/main/java/org/elasticsearch/benchmark/search/QueryParserHelperBenchmark.java
  2. 5 0
      modules/mapper-extras/src/main/java/org/elasticsearch/index/mapper/extras/ScaledFloatFieldMapper.java
  3. 10 0
      modules/mapper-extras/src/main/java/org/elasticsearch/index/mapper/extras/SearchAsYouTypeFieldMapper.java
  4. 0 90
      server/src/internalClusterTest/java/org/elasticsearch/search/query/QueryStringIT.java
  5. 5 54
      server/src/internalClusterTest/java/org/elasticsearch/search/query/SimpleQueryStringIT.java
  6. 4 1
      server/src/internalClusterTest/java/org/elasticsearch/validate/SimpleValidateQueryIT.java
  7. 5 0
      server/src/main/java/org/elasticsearch/index/mapper/DateFieldMapper.java
  8. 11 0
      server/src/main/java/org/elasticsearch/index/mapper/IdFieldMapper.java
  9. 5 0
      server/src/main/java/org/elasticsearch/index/mapper/IpFieldMapper.java
  10. 12 0
      server/src/main/java/org/elasticsearch/index/mapper/MappedFieldType.java
  11. 5 0
      server/src/main/java/org/elasticsearch/index/mapper/NestedPathFieldMapper.java
  12. 4 0
      server/src/main/java/org/elasticsearch/index/mapper/NumberFieldMapper.java
  13. 5 0
      server/src/main/java/org/elasticsearch/index/mapper/RangeFieldMapper.java
  14. 5 0
      server/src/main/java/org/elasticsearch/index/mapper/SeqNoFieldMapper.java
  15. 5 0
      server/src/main/java/org/elasticsearch/index/mapper/TermBasedFieldType.java
  16. 5 0
      server/src/main/java/org/elasticsearch/index/mapper/TextFieldMapper.java
  17. 5 0
      server/src/main/java/org/elasticsearch/index/mapper/flattened/FlattenedFieldMapper.java
  18. 23 0
      server/src/main/java/org/elasticsearch/index/query/SearchExecutionContext.java
  19. 1 1
      server/src/main/java/org/elasticsearch/index/search/QueryParserHelper.java
  20. 18 7
      server/src/test/java/org/elasticsearch/index/query/CombinedFieldsQueryParsingTests.java
  21. 157 0
      server/src/test/java/org/elasticsearch/index/query/MultiMatchQueryBuilderMultiFieldTests.java
  22. 0 98
      server/src/test/java/org/elasticsearch/index/query/MultiMatchQueryBuilderTests.java
  23. 250 0
      server/src/test/java/org/elasticsearch/index/query/QueryStringQueryBuilderMultiFieldTests.java
  24. 0 122
      server/src/test/java/org/elasticsearch/index/query/QueryStringQueryBuilderTests.java
  25. 198 0
      server/src/test/java/org/elasticsearch/index/query/SimpleQueryStringBuilderMultiFieldTests.java
  26. 0 139
      server/src/test/java/org/elasticsearch/index/query/SimpleQueryStringBuilderTests.java
  27. 71 20
      server/src/test/java/org/elasticsearch/index/search/QueryParserHelperTests.java
  28. 16 3
      test/framework/src/main/java/org/elasticsearch/index/mapper/MapperServiceTestCase.java
  29. 5 0
      x-pack/plugin/mapper-aggregate-metric/src/main/java/org/elasticsearch/xpack/aggregatemetric/mapper/AggregateDoubleMetricFieldMapper.java
  30. 5 0
      x-pack/plugin/mapper-unsigned-long/src/main/java/org/elasticsearch/xpack/unsignedlong/UnsignedLongFieldMapper.java
  31. 5 0
      x-pack/plugin/wildcard/src/main/java/org/elasticsearch/xpack/wildcard/mapper/WildcardFieldMapper.java

+ 200 - 0
benchmarks/src/main/java/org/elasticsearch/benchmark/search/QueryParserHelperBenchmark.java

@@ -0,0 +1,200 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+package org.elasticsearch.benchmark.search;
+
+import org.apache.logging.log4j.util.Strings;
+import org.apache.lucene.analysis.standard.StandardAnalyzer;
+import org.apache.lucene.index.DirectoryReader;
+import org.apache.lucene.index.IndexReader;
+import org.apache.lucene.index.IndexWriter;
+import org.apache.lucene.index.IndexWriterConfig;
+import org.apache.lucene.search.IndexSearcher;
+import org.apache.lucene.store.ByteBuffersDirectory;
+import org.apache.lucene.store.Directory;
+import org.elasticsearch.Version;
+import org.elasticsearch.cluster.ClusterModule;
+import org.elasticsearch.cluster.metadata.IndexMetadata;
+import org.elasticsearch.common.bytes.BytesArray;
+import org.elasticsearch.common.compress.CompressedXContent;
+import org.elasticsearch.common.io.stream.NamedWriteableRegistry;
+import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.core.internal.io.IOUtils;
+import org.elasticsearch.index.IndexSettings;
+import org.elasticsearch.index.analysis.AnalyzerScope;
+import org.elasticsearch.index.analysis.IndexAnalyzers;
+import org.elasticsearch.index.analysis.NamedAnalyzer;
+import org.elasticsearch.index.fielddata.IndexFieldDataCache;
+import org.elasticsearch.index.mapper.IdFieldMapper;
+import org.elasticsearch.index.mapper.MapperRegistry;
+import org.elasticsearch.index.mapper.MapperService;
+import org.elasticsearch.index.mapper.ParsedDocument;
+import org.elasticsearch.index.mapper.SourceToParse;
+import org.elasticsearch.index.query.SearchExecutionContext;
+import org.elasticsearch.index.search.QueryParserHelper;
+import org.elasticsearch.index.shard.IndexShard;
+import org.elasticsearch.index.similarity.SimilarityService;
+import org.elasticsearch.indices.IndicesModule;
+import org.elasticsearch.indices.breaker.NoneCircuitBreakerService;
+import org.elasticsearch.script.Script;
+import org.elasticsearch.script.ScriptCompiler;
+import org.elasticsearch.script.ScriptContext;
+import org.elasticsearch.xcontent.NamedXContentRegistry;
+import org.elasticsearch.xcontent.XContentType;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.BenchmarkMode;
+import org.openjdk.jmh.annotations.Fork;
+import org.openjdk.jmh.annotations.Measurement;
+import org.openjdk.jmh.annotations.Mode;
+import org.openjdk.jmh.annotations.OutputTimeUnit;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.annotations.TearDown;
+import org.openjdk.jmh.annotations.Warmup;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+@Fork(1)
+@Warmup(iterations = 5)
+@Measurement(iterations = 5)
+@State(Scope.Benchmark)
+@OutputTimeUnit(TimeUnit.MICROSECONDS)
+@BenchmarkMode(Mode.AverageTime)
+public class QueryParserHelperBenchmark {
+
+    private static final int NUMBER_OF_MAPPING_FIELDS = 1000;
+
+    private Directory directory;
+    private IndexReader indexReader;
+    private MapperService mapperService;
+
+    @Setup
+    public void setup() throws IOException {
+        // pre: set up MapperService and SearchExecutionContext
+        List<String> fields = new ArrayList<>();
+        for (int i = 0; i < NUMBER_OF_MAPPING_FIELDS; i++) {
+            fields.add(String.format("""
+                "field%d":{"type":"long"}""", i));
+        }
+        String mappings = """
+            {"_doc":{"properties":{""" + Strings.join(fields, ',') + "}}}";
+
+        mapperService = createMapperService(mappings);
+        IndexWriterConfig iwc = new IndexWriterConfig(IndexShard.buildIndexAnalyzer(mapperService));
+        directory = new ByteBuffersDirectory();
+        IndexWriter iw = new IndexWriter(directory, iwc);
+
+        for (int i = 0; i < 2000; i++) {
+            ParsedDocument doc = mapperService.documentMapper().parse(buildDoc(i));
+            iw.addDocument(doc.rootDoc());
+            if (i % 100 == 0) {
+                iw.commit();
+            }
+        }
+        iw.close();
+
+        indexReader = DirectoryReader.open(directory);
+    }
+
+    private SourceToParse buildDoc(int docId) {
+        List<String> fields = new ArrayList<>();
+        for (int i = 0; i < NUMBER_OF_MAPPING_FIELDS; i++) {
+            if (i % 2 == 0) continue;
+            if (i % 3 == 0 && (docId < (NUMBER_OF_MAPPING_FIELDS / 2))) continue;
+            fields.add(String.format("""
+                "field%d":1""", i));
+        }
+        String source = "{" + String.join(",", fields) + "}";
+        return new SourceToParse("" + docId, new BytesArray(source), XContentType.JSON);
+    }
+
+    @TearDown
+    public void tearDown() {
+        IOUtils.closeWhileHandlingException(indexReader, directory);
+    }
+
+    @Benchmark
+    public void expand() {
+        Map<String, Float> fields = QueryParserHelper.resolveMappingFields(buildSearchExecutionContext(), Map.of("*", 1f));
+        assert fields.size() > 0 && fields.size() < NUMBER_OF_MAPPING_FIELDS;
+    }
+
+    protected SearchExecutionContext buildSearchExecutionContext() {
+        final SimilarityService similarityService = new SimilarityService(mapperService.getIndexSettings(), null, Map.of());
+        final long nowInMillis = 1;
+        return new SearchExecutionContext(
+            0,
+            0,
+            mapperService.getIndexSettings(),
+            null,
+            (ft, idxName, lookup) -> ft.fielddataBuilder(idxName, lookup)
+                .build(new IndexFieldDataCache.None(), new NoneCircuitBreakerService()),
+            mapperService,
+            mapperService.mappingLookup(),
+            similarityService,
+            null,
+            new NamedXContentRegistry(ClusterModule.getNamedXWriteables()),
+            new NamedWriteableRegistry(ClusterModule.getNamedWriteables()),
+            null,
+            new IndexSearcher(indexReader),
+            () -> nowInMillis,
+            null,
+            null,
+            () -> true,
+            null,
+            Collections.emptyMap()
+        );
+    }
+
+    protected final MapperService createMapperService(String mappings) {
+        Settings settings = Settings.builder()
+            .put("index.number_of_replicas", 0)
+            .put("index.number_of_shards", 1)
+            .put("index.version.created", Version.CURRENT)
+            .build();
+        IndexMetadata meta = IndexMetadata.builder("index").settings(settings).build();
+        IndexSettings indexSettings = new IndexSettings(meta, settings);
+        MapperRegistry mapperRegistry = new IndicesModule(Collections.emptyList()).getMapperRegistry();
+
+        SimilarityService similarityService = new SimilarityService(indexSettings, null, Map.of());
+        MapperService mapperService = new MapperService(
+            indexSettings,
+            new IndexAnalyzers(
+                Map.of("default", new NamedAnalyzer("default", AnalyzerScope.INDEX, new StandardAnalyzer())),
+                Map.of(),
+                Map.of()
+            ),
+            new NamedXContentRegistry(ClusterModule.getNamedXWriteables()),
+            similarityService,
+            mapperRegistry,
+            () -> { throw new UnsupportedOperationException(); },
+            new IdFieldMapper(() -> true),
+            new ScriptCompiler() {
+                @Override
+                public <T> T compile(Script script, ScriptContext<T> scriptContext) {
+                    throw new UnsupportedOperationException();
+                }
+            }
+        );
+
+        try {
+            mapperService.merge("_doc", new CompressedXContent(mappings), MapperService.MergeReason.MAPPING_UPDATE);
+            return mapperService;
+        } catch (IOException e) {
+            throw new UncheckedIOException(e);
+        }
+    }
+
+}

+ 5 - 0
modules/mapper-extras/src/main/java/org/elasticsearch/index/mapper/extras/ScaledFloatFieldMapper.java

@@ -214,6 +214,11 @@ public class ScaledFloatFieldMapper extends FieldMapper {
             return CONTENT_TYPE;
         }
 
+        @Override
+        public boolean mayExistInIndex(SearchExecutionContext context) {
+            return context.fieldExistsInIndex(name());
+        }
+
         @Override
         public Query termQuery(Object value, SearchExecutionContext context) {
             failIfNotIndexedNorDocValuesFallback(context);

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

@@ -436,6 +436,11 @@ public class SearchAsYouTypeFieldMapper extends FieldMapper {
             return length >= minChars - 1 && length <= maxChars;
         }
 
+        @Override
+        public boolean mayExistInIndex(SearchExecutionContext context) {
+            return false;
+        }
+
         @Override
         public Query prefixQuery(
             String value,
@@ -569,6 +574,11 @@ public class SearchAsYouTypeFieldMapper extends FieldMapper {
             this.prefixFieldType = prefixFieldType;
         }
 
+        @Override
+        public boolean mayExistInIndex(SearchExecutionContext context) {
+            return false;
+        }
+
         @Override
         public ValueFetcher valueFetcher(SearchExecutionContext context, String format) {
             // Because this internal field is modelled as a multi-field, SourceValueFetcher will look up its

+ 0 - 90
server/src/internalClusterTest/java/org/elasticsearch/search/query/QueryStringIT.java

@@ -8,18 +8,14 @@
 
 package org.elasticsearch.search.query;
 
-import org.apache.lucene.search.IndexSearcher;
 import org.elasticsearch.ExceptionsHelper;
 import org.elasticsearch.action.index.IndexRequestBuilder;
 import org.elasticsearch.action.search.SearchResponse;
 import org.elasticsearch.common.settings.Settings;
-import org.elasticsearch.index.mapper.MapperService;
 import org.elasticsearch.index.query.Operator;
-import org.elasticsearch.index.query.QueryStringQueryBuilder;
 import org.elasticsearch.search.SearchHit;
 import org.elasticsearch.search.SearchHits;
 import org.elasticsearch.test.ESIntegTestCase;
-import org.elasticsearch.xcontent.XContentBuilder;
 import org.elasticsearch.xcontent.XContentType;
 import org.junit.Before;
 
@@ -31,10 +27,8 @@ import java.util.Set;
 
 import static org.elasticsearch.index.query.QueryBuilders.queryStringQuery;
 import static org.elasticsearch.test.StreamsUtils.copyToStringFromClasspath;
-import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertAcked;
 import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertHitCount;
 import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertNoFailures;
-import static org.elasticsearch.xcontent.XContentFactory.jsonBuilder;
 import static org.hamcrest.Matchers.containsInAnyOrder;
 import static org.hamcrest.Matchers.containsString;
 import static org.hamcrest.Matchers.equalTo;
@@ -234,90 +228,6 @@ public class QueryStringIT extends ESIntegTestCase {
         assertThat(e.getCause().getMessage(), containsString("unit [D] not supported for date math [-2D]"));
     }
 
-    public void testLimitOnExpandedFields() throws Exception {
-
-        final int maxClauseCount = randomIntBetween(50, 100);
-
-        XContentBuilder builder = jsonBuilder();
-        builder.startObject();
-        {
-            builder.startObject("_doc");
-            {
-                builder.startObject("properties");
-                {
-                    for (int i = 0; i < maxClauseCount; i++) {
-                        builder.startObject("field_A" + i).field("type", "text").endObject();
-                        builder.startObject("field_B" + i).field("type", "text").endObject();
-                    }
-                    builder.endObject();
-                }
-                builder.endObject();
-            }
-            builder.endObject();
-        }
-
-        assertAcked(
-            prepareCreate("testindex").setSettings(
-                Settings.builder().put(MapperService.INDEX_MAPPING_TOTAL_FIELDS_LIMIT_SETTING.getKey(), maxClauseCount + 100)
-            ).setMapping(builder)
-        );
-
-        client().prepareIndex("testindex").setId("1").setSource("field_A0", "foo bar baz").get();
-        refresh();
-
-        int originalMaxClauses = IndexSearcher.getMaxClauseCount();
-        try {
-
-            IndexSearcher.setMaxClauseCount(maxClauseCount);
-
-            // single field shouldn't trigger the limit
-            doAssertOneHitForQueryString("field_A0:foo");
-            // expanding to the limit should work
-            doAssertOneHitForQueryString("field_A\\*:foo");
-
-            // adding a non-existing field on top shouldn't overshoot the limit
-            doAssertOneHitForQueryString("field_A\\*:foo unmapped:something");
-
-            // the following should exceed the limit
-            doAssertLimitExceededException("foo", IndexSearcher.getMaxClauseCount() * 2, "*");
-            doAssertLimitExceededException("*:foo", IndexSearcher.getMaxClauseCount() * 2, "*");
-            doAssertLimitExceededException("field_\\*:foo", IndexSearcher.getMaxClauseCount() * 2, "field_*");
-
-        } finally {
-            IndexSearcher.setMaxClauseCount(originalMaxClauses);
-        }
-    }
-
-    private void doAssertOneHitForQueryString(String queryString) {
-        QueryStringQueryBuilder qb = queryStringQuery(queryString);
-        if (randomBoolean()) {
-            qb.defaultField("*");
-        }
-        SearchResponse response = client().prepareSearch("testindex").setQuery(qb).get();
-        assertHitCount(response, 1);
-    }
-
-    private void doAssertLimitExceededException(String queryString, int exceedingFieldCount, String inputFieldPattern) {
-        Exception e = expectThrows(Exception.class, () -> {
-            QueryStringQueryBuilder qb = queryStringQuery(queryString);
-            if (randomBoolean()) {
-                qb.defaultField("*");
-            }
-            client().prepareSearch("testindex").setQuery(qb).get();
-        });
-        assertThat(
-            ExceptionsHelper.unwrap(e, IllegalArgumentException.class).getMessage(),
-            containsString(
-                "field expansion for ["
-                    + inputFieldPattern
-                    + "] matches too many fields, limit: "
-                    + IndexSearcher.getMaxClauseCount()
-                    + ", got: "
-                    + exceedingFieldCount
-            )
-        );
-    }
-
     public void testFieldAlias() throws Exception {
         List<IndexRequestBuilder> indexRequests = new ArrayList<>();
         indexRequests.add(client().prepareIndex("test").setId("1").setSource("f3", "text", "f2", "one"));

+ 5 - 54
server/src/internalClusterTest/java/org/elasticsearch/search/query/SimpleQueryStringIT.java

@@ -11,8 +11,6 @@ package org.elasticsearch.search.query;
 import org.apache.lucene.analysis.TokenFilter;
 import org.apache.lucene.analysis.TokenStream;
 import org.apache.lucene.analysis.tokenattributes.CharTermAttribute;
-import org.apache.lucene.search.IndexSearcher;
-import org.elasticsearch.ExceptionsHelper;
 import org.elasticsearch.action.admin.indices.create.CreateIndexRequestBuilder;
 import org.elasticsearch.action.index.IndexRequestBuilder;
 import org.elasticsearch.action.search.SearchPhaseExecutionException;
@@ -20,11 +18,9 @@ import org.elasticsearch.action.search.SearchResponse;
 import org.elasticsearch.common.Strings;
 import org.elasticsearch.common.settings.Settings;
 import org.elasticsearch.index.analysis.PreConfiguredTokenFilter;
-import org.elasticsearch.index.mapper.MapperService;
 import org.elasticsearch.index.query.BoolQueryBuilder;
 import org.elasticsearch.index.query.Operator;
 import org.elasticsearch.index.query.QueryBuilders;
-import org.elasticsearch.index.query.QueryStringQueryBuilder;
 import org.elasticsearch.index.query.SimpleQueryStringFlag;
 import org.elasticsearch.plugins.AnalysisPlugin;
 import org.elasticsearch.plugins.Plugin;
@@ -32,7 +28,6 @@ import org.elasticsearch.search.SearchHit;
 import org.elasticsearch.search.SearchHits;
 import org.elasticsearch.search.builder.SearchSourceBuilder;
 import org.elasticsearch.test.ESIntegTestCase;
-import org.elasticsearch.xcontent.XContentBuilder;
 import org.elasticsearch.xcontent.XContentFactory;
 import org.elasticsearch.xcontent.XContentType;
 
@@ -549,11 +544,15 @@ public class SimpleQueryStringIT extends ESIntegTestCase {
         assertHitCount(resp, 2L);
     }
 
-    public void testAllFieldsWithSpecifiedLeniency() throws IOException {
+    public void testAllFieldsWithSpecifiedLeniency() throws Exception {
         String indexBody = copyToStringFromClasspath("/org/elasticsearch/search/query/all-query-index.json");
         prepareCreate("test").setSource(indexBody, XContentType.JSON).get();
         ensureGreen("test");
 
+        List<IndexRequestBuilder> reqs = new ArrayList<>();
+        reqs.add(client().prepareIndex("test").setId("1").setSource("f_long", 1));
+        indexRandom(true, false, reqs);
+
         SearchPhaseExecutionException e = expectThrows(
             SearchPhaseExecutionException.class,
             () -> client().prepareSearch("test").setQuery(simpleQueryStringQuery("foo123").lenient(false)).get()
@@ -561,54 +560,6 @@ public class SimpleQueryStringIT extends ESIntegTestCase {
         assertThat(e.getDetailedMessage(), containsString("NumberFormatException: For input string: \"foo123\""));
     }
 
-    public void testLimitOnExpandedFields() throws Exception {
-
-        final int maxClauseCount = randomIntBetween(50, 100);
-
-        XContentBuilder builder = jsonBuilder();
-        builder.startObject();
-        builder.startObject("_doc");
-        builder.startObject("properties");
-        for (int i = 0; i < maxClauseCount + 1; i++) {
-            builder.startObject("field" + i).field("type", "text").endObject();
-        }
-        builder.endObject(); // properties
-        builder.endObject(); // type1
-        builder.endObject();
-
-        assertAcked(
-            prepareCreate("toomanyfields").setSettings(
-                Settings.builder().put(MapperService.INDEX_MAPPING_TOTAL_FIELDS_LIMIT_SETTING.getKey(), maxClauseCount + 100)
-            ).setMapping(builder)
-        );
-
-        client().prepareIndex("toomanyfields").setId("1").setSource("field1", "foo bar baz").get();
-        refresh();
-
-        int originalMaxClauses = IndexSearcher.getMaxClauseCount();
-        try {
-            IndexSearcher.setMaxClauseCount(maxClauseCount);
-            doAssertLimitExceededException("*", maxClauseCount + 1);
-            doAssertLimitExceededException("field*", maxClauseCount + 1);
-        } finally {
-            IndexSearcher.setMaxClauseCount(originalMaxClauses);
-        }
-    }
-
-    private void doAssertLimitExceededException(String field, int exceedingFieldCount) {
-        Exception e = expectThrows(Exception.class, () -> {
-            QueryStringQueryBuilder qb = queryStringQuery("bar");
-            qb.field(field);
-            client().prepareSearch("toomanyfields").setQuery(qb).get();
-        });
-        assertThat(
-            ExceptionsHelper.unwrap(e, IllegalArgumentException.class).getMessage(),
-            containsString(
-                "field expansion matches too many fields, limit: " + IndexSearcher.getMaxClauseCount() + ", got: " + exceedingFieldCount
-            )
-        );
-    }
-
     public void testFieldAlias() throws Exception {
         String indexBody = copyToStringFromClasspath("/org/elasticsearch/search/query/all-query-index.json");
         assertAcked(prepareCreate("test").setSource(indexBody, XContentType.JSON));

+ 4 - 1
server/src/internalClusterTest/java/org/elasticsearch/validate/SimpleValidateQueryIT.java

@@ -148,7 +148,7 @@ public class SimpleValidateQueryIT extends ESIntegTestCase {
     }
 
     public void testExplainValidateQueryTwoNodes() throws IOException {
-        createIndex("test");
+        createIndex("test", Settings.builder().put(SETTING_NUMBER_OF_SHARDS, 2).build());
         ensureGreen();
         client().admin()
             .indices()
@@ -182,6 +182,9 @@ public class SimpleValidateQueryIT extends ESIntegTestCase {
             .execute()
             .actionGet();
 
+        for (int i = 0; i < 10; i++) {
+            client().prepareIndex("test").setSource("foo", "text", "bar", i, "baz", "blort").execute().actionGet();
+        }
         refresh();
 
         for (Client client : internalCluster().getClients()) {

+ 5 - 0
server/src/main/java/org/elasticsearch/index/mapper/DateFieldMapper.java

@@ -444,6 +444,11 @@ public final class DateFieldMapper extends FieldMapper {
             NUMBER_FORMAT.setMaximumFractionDigits(6);
         }
 
+        @Override
+        public boolean mayExistInIndex(SearchExecutionContext context) {
+            return context.fieldExistsInIndex(this.name());
+        }
+
         @Override
         public ValueFetcher valueFetcher(SearchExecutionContext context, String format) {
             DateFormatter defaultFormatter = dateTimeFormatter();

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

@@ -106,6 +106,17 @@ public class IdFieldMapper extends MetadataFieldMapper {
             return CONTENT_TYPE;
         }
 
+        @Override
+        public boolean isSearchable() {
+            // The _id field is always searchable.
+            return true;
+        }
+
+        @Override
+        public boolean mayExistInIndex(SearchExecutionContext context) {
+            return true;
+        }
+
         @Override
         public ValueFetcher valueFetcher(SearchExecutionContext context, String format) {
             return new StoredValueFetcher(context.lookup(), NAME);

+ 5 - 0
server/src/main/java/org/elasticsearch/index/mapper/IpFieldMapper.java

@@ -212,6 +212,11 @@ public class IpFieldMapper extends FieldMapper {
             return CONTENT_TYPE;
         }
 
+        @Override
+        public boolean mayExistInIndex(SearchExecutionContext context) {
+            return context.fieldExistsInIndex(name());
+        }
+
         private static InetAddress parse(Object value) {
             if (value instanceof InetAddress) {
                 return (InetAddress) value;

+ 12 - 0
server/src/main/java/org/elasticsearch/index/mapper/MappedFieldType.java

@@ -479,6 +479,18 @@ public abstract class MappedFieldType {
         return false;
     }
 
+    /**
+     * @return if the field may have values in the underlying index
+     *
+     * Note that this should only return {@code false} if it is not possible for it to
+     * match on a term query.
+     *
+     * @see org.elasticsearch.index.search.QueryParserHelper
+     */
+    public boolean mayExistInIndex(SearchExecutionContext context) {
+        return true;
+    }
+
     /**
      * Pick a {@link DocValueFormat} that can be used to display and parse
      * values of fields of this type.

+ 5 - 0
server/src/main/java/org/elasticsearch/index/mapper/NestedPathFieldMapper.java

@@ -81,6 +81,11 @@ public class NestedPathFieldMapper extends MetadataFieldMapper {
         public ValueFetcher valueFetcher(SearchExecutionContext context, String format) {
             throw new UnsupportedOperationException("Cannot fetch values for internal field [" + name() + "].");
         }
+
+        @Override
+        public boolean mayExistInIndex(SearchExecutionContext context) {
+            return false;
+        }
     }
 
     private NestedPathFieldMapper(String name) {

+ 4 - 0
server/src/main/java/org/elasticsearch/index/mapper/NumberFieldMapper.java

@@ -1246,6 +1246,10 @@ public class NumberFieldMapper extends FieldMapper {
         }
 
         @Override
+        public boolean mayExistInIndex(SearchExecutionContext context) {
+            return context.fieldExistsInIndex(this.name());
+        }
+
         public boolean isSearchable() {
             return isIndexed() || hasDocValues();
         }

+ 5 - 0
server/src/main/java/org/elasticsearch/index/mapper/RangeFieldMapper.java

@@ -224,6 +224,11 @@ public class RangeFieldMapper extends FieldMapper {
             return new BinaryIndexFieldData.Builder(name(), CoreValuesSourceType.RANGE);
         }
 
+        @Override
+        public boolean mayExistInIndex(SearchExecutionContext context) {
+            return context.fieldExistsInIndex(this.name());
+        }
+
         @Override
         public ValueFetcher valueFetcher(SearchExecutionContext context, String format) {
             DateFormatter defaultFormatter = dateTimeFormatter();

+ 5 - 0
server/src/main/java/org/elasticsearch/index/mapper/SeqNoFieldMapper.java

@@ -133,6 +133,11 @@ public class SeqNoFieldMapper extends MetadataFieldMapper {
             return Long.parseLong(value.toString());
         }
 
+        @Override
+        public boolean mayExistInIndex(SearchExecutionContext context) {
+            return false;
+        }
+
         @Override
         public ValueFetcher valueFetcher(SearchExecutionContext context, String format) {
             throw new UnsupportedOperationException("Cannot fetch values for internal field [" + name() + "].");

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

@@ -48,6 +48,11 @@ public abstract class TermBasedFieldType extends SimpleMappedFieldType {
         return AutomatonQueries.caseInsensitiveTermQuery(new Term(name(), indexedValueForSearch(value)));
     }
 
+    @Override
+    public boolean mayExistInIndex(SearchExecutionContext context) {
+        return context.fieldExistsInIndex(name());
+    }
+
     @Override
     public Query termQuery(Object value, SearchExecutionContext context) {
         failIfNotIndexed();

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

@@ -528,6 +528,11 @@ public class TextFieldMapper extends FieldMapper {
             throw new UnsupportedOperationException();
         }
 
+        @Override
+        public boolean mayExistInIndex(SearchExecutionContext context) {
+            return false;
+        }
+
         boolean accept(int length) {
             return length >= minChars - 1 && length <= maxChars;
         }

+ 5 - 0
server/src/main/java/org/elasticsearch/index/mapper/flattened/FlattenedFieldMapper.java

@@ -637,6 +637,11 @@ public final class FlattenedFieldMapper extends FieldMapper {
             return eagerGlobalOrdinals;
         }
 
+        @Override
+        public boolean mayExistInIndex(SearchExecutionContext context) {
+            return context.fieldExistsInIndex(name());
+        }
+
         @Override
         public Object valueForDisplay(Object value) {
             if (value == null) {

+ 23 - 0
server/src/main/java/org/elasticsearch/index/query/SearchExecutionContext.java

@@ -10,7 +10,10 @@ package org.elasticsearch.index.query;
 
 import org.apache.lucene.analysis.Analyzer;
 import org.apache.lucene.analysis.DelegatingAnalyzerWrapper;
+import org.apache.lucene.index.FieldInfo;
+import org.apache.lucene.index.FieldInfos;
 import org.apache.lucene.index.IndexReader;
+import org.apache.lucene.index.LeafReaderContext;
 import org.apache.lucene.search.IndexSearcher;
 import org.apache.lucene.search.Query;
 import org.apache.lucene.search.join.BitSetProducer;
@@ -97,6 +100,7 @@ public class SearchExecutionContext extends QueryRewriteContext {
     private final IndexSearcher searcher;
     private boolean cacheable = true;
     private final SetOnce<Boolean> frozen = new SetOnce<>();
+    private Set<String> fieldsInIndex = null;
 
     private final Index fullyQualifiedIndex;
     private final Predicate<String> indexNameMatcher;
@@ -645,6 +649,25 @@ public class SearchExecutionContext extends QueryRewriteContext {
         return searcher;
     }
 
+    /**
+     * Is this field present in the underlying lucene index for the current shard?
+     */
+    public boolean fieldExistsInIndex(String fieldname) {
+        if (searcher == null) {
+            return false;
+        }
+        if (fieldsInIndex == null) {
+            fieldsInIndex = new HashSet<>();
+            for (LeafReaderContext ctx : searcher.getIndexReader().leaves()) {
+                FieldInfos fis = ctx.reader().getFieldInfos();
+                for (FieldInfo fi : fis) {
+                    fieldsInIndex.add(fi.name);
+                }
+            }
+        }
+        return fieldsInIndex.contains(fieldname);
+    }
+
     /**
      * Returns the fully qualified index including a remote cluster alias if applicable, and the index uuid
      */

+ 1 - 1
server/src/main/java/org/elasticsearch/index/search/QueryParserHelper.java

@@ -134,7 +134,7 @@ public final class QueryParserHelper {
             }
 
             if (acceptAllTypes == false) {
-                if (fieldType.getTextSearchInfo() == TextSearchInfo.NONE) {
+                if (fieldType.getTextSearchInfo() == TextSearchInfo.NONE || fieldType.mayExistInIndex(context) == false) {
                     continue;
                 }
             }

+ 18 - 7
server/src/test/java/org/elasticsearch/index/query/CombinedFieldsQueryParsingTests.java

@@ -17,6 +17,7 @@ import org.apache.lucene.sandbox.search.CombinedFieldQuery;
 import org.apache.lucene.search.BooleanClause;
 import org.apache.lucene.search.BooleanQuery;
 import org.apache.lucene.search.BoostQuery;
+import org.apache.lucene.search.IndexSearcher;
 import org.apache.lucene.search.MatchNoDocsQuery;
 import org.apache.lucene.search.PhraseQuery;
 import org.apache.lucene.search.Query;
@@ -28,6 +29,7 @@ import org.elasticsearch.index.analysis.IndexAnalyzers;
 import org.elasticsearch.index.analysis.NamedAnalyzer;
 import org.elasticsearch.index.mapper.MapperService;
 import org.elasticsearch.index.mapper.MapperServiceTestCase;
+import org.elasticsearch.index.mapper.ParsedDocument;
 import org.elasticsearch.xcontent.XContentFactory;
 import org.hamcrest.CoreMatchers;
 import org.junit.Before;
@@ -42,10 +44,11 @@ import static org.hamcrest.Matchers.instanceOf;
 
 public class CombinedFieldsQueryParsingTests extends MapperServiceTestCase {
     private SearchExecutionContext context;
+    private MapperService mapperService;
 
     @Before
     public void createSearchExecutionContext() throws IOException {
-        MapperService mapperService = createMapperService(
+        this.mapperService = createMapperService(
             XContentFactory.jsonBuilder()
                 .startObject()
                 .startObject(MapperService.SINGLE_MAPPING_NAME)
@@ -130,13 +133,21 @@ public class CombinedFieldsQueryParsingTests extends MapperServiceTestCase {
     }
 
     public void testWildcardFieldPattern() throws Exception {
-        Query query = combinedFieldsQuery("quick fox").field("field*").toQuery(context);
-        assertThat(query, instanceOf(BooleanQuery.class));
 
-        BooleanQuery booleanQuery = (BooleanQuery) query;
-        assertThat(booleanQuery.clauses().size(), equalTo(2));
-        assertThat(booleanQuery.clauses().get(0).getQuery(), instanceOf(CombinedFieldQuery.class));
-        assertThat(booleanQuery.clauses().get(1).getQuery(), instanceOf(CombinedFieldQuery.class));
+        ParsedDocument doc = mapperService.documentMapper().parse(source("""
+            { "field1" : "foo", "field2" : "foo" }
+            """));
+
+        withLuceneIndex(mapperService, iw -> iw.addDocument(doc.rootDoc()), ir -> {
+            SearchExecutionContext searcherContext = createSearchExecutionContext(mapperService, new IndexSearcher(ir));
+            Query query = combinedFieldsQuery("quick fox").field("field*").toQuery(searcherContext);
+            assertThat(query, instanceOf(BooleanQuery.class));
+
+            BooleanQuery booleanQuery = (BooleanQuery) query;
+            assertThat(booleanQuery.clauses().size(), equalTo(2));
+            assertThat(booleanQuery.clauses().get(0).getQuery(), instanceOf(CombinedFieldQuery.class));
+            assertThat(booleanQuery.clauses().get(1).getQuery(), instanceOf(CombinedFieldQuery.class));
+        });
     }
 
     public void testOperator() throws Exception {

+ 157 - 0
server/src/test/java/org/elasticsearch/index/query/MultiMatchQueryBuilderMultiFieldTests.java

@@ -0,0 +1,157 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+package org.elasticsearch.index.query;
+
+import org.apache.lucene.index.Term;
+import org.apache.lucene.search.BoostQuery;
+import org.apache.lucene.search.DisjunctionMaxQuery;
+import org.apache.lucene.search.IndexSearcher;
+import org.apache.lucene.search.MatchNoDocsQuery;
+import org.apache.lucene.search.Query;
+import org.apache.lucene.search.TermQuery;
+import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.index.mapper.MapperService;
+import org.elasticsearch.index.mapper.MapperServiceTestCase;
+import org.elasticsearch.index.mapper.ParsedDocument;
+
+import java.util.List;
+
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.equalTo;
+
+public class MultiMatchQueryBuilderMultiFieldTests extends MapperServiceTestCase {
+
+    public void testDefaultField() throws Exception {
+
+        MapperService mapperService = createMapperService("""
+            { "_doc" : { "properties" : {
+                "f_text1" : { "type" : "text" },
+                "f_text2" : { "type" : "text" },
+                "f_keyword1" : { "type" : "keyword" },
+                "f_keyword2" : { "type" : "keyword" },
+                "f_date" : { "type" : "date" }
+            }}}
+            """);
+
+        ParsedDocument doc = mapperService.documentMapper().parse(source("""
+            { "f_text1" : "foo", "f_text2" : "bar", "f_keyword1" : "baz", "f_keyword2" : "buz", "f_date" : "2021-12-20T00:00:00" }
+            """));
+
+        withLuceneIndex(mapperService, iw -> iw.addDocument(doc.rootDoc()), ir -> {
+
+            IndexSearcher searcher = new IndexSearcher(ir);
+
+            {
+                // default value 'index.query.default_field = *' sets leniency to true
+                SearchExecutionContext context = createSearchExecutionContext(mapperService, searcher);
+                Query query = new MultiMatchQueryBuilder("hello").toQuery(context);
+                Query expected = new DisjunctionMaxQuery(
+                    List.of(
+                        new TermQuery(new Term("f_text1", "hello")),
+                        new TermQuery(new Term("f_text2", "hello")),
+                        new TermQuery(new Term("f_keyword1", "hello")),
+                        new TermQuery(new Term("f_keyword2", "hello")),
+                        new MatchNoDocsQuery()
+                    ),
+                    0f
+                );
+                assertThat(query, equalTo(expected));
+            }
+
+            {
+                // default field list contains '*' sets leniency to true
+                Settings settings = Settings.builder().putList("index.query.default_field", "f_text1", "*").build();
+                SearchExecutionContext context = createSearchExecutionContext(mapperService, searcher, settings);
+                Query query = new MultiMatchQueryBuilder("hello").toQuery(context);
+                Query expected = new DisjunctionMaxQuery(
+                    List.of(
+                        new TermQuery(new Term("f_text1", "hello")),
+                        new TermQuery(new Term("f_text2", "hello")),
+                        new TermQuery(new Term("f_keyword1", "hello")),
+                        new TermQuery(new Term("f_keyword2", "hello")),
+                        new MatchNoDocsQuery()
+                    ),
+                    0f
+                );
+                assertThat(query, equalTo(expected));
+            }
+
+            {
+                // default field list contains no wildcards, leniency = false
+                Settings settings = Settings.builder().putList("index.query.default_field", "f_text1", "f_date").build();
+                SearchExecutionContext context = createSearchExecutionContext(mapperService, searcher, settings);
+                Exception e = expectThrows(Exception.class, () -> new MultiMatchQueryBuilder("hello").toQuery(context));
+                assertThat(e.getMessage(), containsString("failed to parse date field [hello]"));
+            }
+
+            {
+                // default field list contains boost
+                Settings settings = Settings.builder().putList("index.query.default_field", "f_text1", "f_text2^4").build();
+                SearchExecutionContext context = createSearchExecutionContext(mapperService, searcher, settings);
+                Query query = new MultiMatchQueryBuilder("hello").toQuery(context);
+                Query expected = new DisjunctionMaxQuery(
+                    List.of(new TermQuery(new Term("f_text1", "hello")), new BoostQuery(new TermQuery(new Term("f_text2", "hello")), 4f)),
+                    0f
+                );
+                assertThat(query, equalTo(expected));
+            }
+
+            {
+                // set tiebreaker
+                SearchExecutionContext context = createSearchExecutionContext(mapperService, searcher);
+                Query query = new MultiMatchQueryBuilder("hello").tieBreaker(0.5f).toQuery(context);
+                Query expected = new DisjunctionMaxQuery(
+                    List.of(
+                        new TermQuery(new Term("f_text1", "hello")),
+                        new TermQuery(new Term("f_text2", "hello")),
+                        new TermQuery(new Term("f_keyword1", "hello")),
+                        new TermQuery(new Term("f_keyword2", "hello")),
+                        new MatchNoDocsQuery()
+                    ),
+                    0.5f
+                );
+                assertThat(query, equalTo(expected));
+            }
+
+        });
+    }
+
+    public void testFieldListIncludesWildcard() throws Exception {
+
+        MapperService mapperService = createMapperService("""
+            { "_doc" : { "properties" : {
+                "f_text1" : { "type" : "text" },
+                "f_text2" : { "type" : "text" },
+                "f_keyword1" : { "type" : "keyword" },
+                "f_keyword2" : { "type" : "keyword" },
+                "f_date" : { "type" : "date" }
+            }}}
+            """);
+
+        ParsedDocument doc = mapperService.documentMapper().parse(source("""
+            { "f_text1" : "foo", "f_text2" : "bar", "f_keyword1" : "baz", "f_keyword2" : "buz", "f_date" : "2021-12-20T00:00:00" }
+            """));
+
+        withLuceneIndex(mapperService, iw -> iw.addDocument(doc.rootDoc()), ir -> {
+            SearchExecutionContext context = createSearchExecutionContext(mapperService, new IndexSearcher(ir));
+            Query expected = new DisjunctionMaxQuery(
+                List.of(
+                    new TermQuery(new Term("f_text1", "hello")),
+                    new TermQuery(new Term("f_text2", "hello")),
+                    new TermQuery(new Term("f_keyword1", "hello")),
+                    new TermQuery(new Term("f_keyword2", "hello")),
+                    new MatchNoDocsQuery()
+                ),
+                0f
+            );
+            assertEquals(expected, new MultiMatchQueryBuilder("hello").field("*").toQuery(context));
+            assertEquals(expected, new MultiMatchQueryBuilder("hello").field("f_text1").field("*").toQuery(context));
+        });
+    }
+}

+ 0 - 98
server/src/test/java/org/elasticsearch/index/query/MultiMatchQueryBuilderTests.java

@@ -231,15 +231,6 @@ public class MultiMatchQueryBuilderTests extends AbstractQueryTestCase<MultiMatc
         assertEquals(expected, query);
     }
 
-    public void testToQueryFieldsWildcard() throws Exception {
-        Query query = multiMatchQuery("test").field("mapped_str*").tieBreaker(1.0f).toQuery(createSearchExecutionContext());
-        Query expected = new DisjunctionMaxQuery(
-            List.of(new TermQuery(new Term(TEXT_FIELD_NAME, "test")), new TermQuery(new Term(KEYWORD_FIELD_NAME, "test"))),
-            1
-        );
-        assertEquals(expected, query);
-    }
-
     public void testToQueryFieldMissing() throws Exception {
         assertThat(
             multiMatchQuery("test").field(MISSING_WILDCARD_FIELD_NAME).toQuery(createSearchExecutionContext()),
@@ -384,95 +375,6 @@ public class MultiMatchQueryBuilderTests extends AbstractQueryTestCase<MultiMatc
         assertEquals(expected, query);
     }
 
-    public void testDefaultField() throws Exception {
-        SearchExecutionContext context = createSearchExecutionContext();
-        MultiMatchQueryBuilder builder = new MultiMatchQueryBuilder("hello");
-        // default value `*` sets leniency to true
-        Query query = builder.toQuery(context);
-        assertQueryWithAllFieldsWildcard(query);
-
-        try {
-            // `*` is in the list of the default_field => leniency set to true
-            context.getIndexSettings()
-                .updateIndexMetadata(
-                    newIndexMeta(
-                        "index",
-                        context.getIndexSettings().getSettings(),
-                        Settings.builder().putList("index.query.default_field", TEXT_FIELD_NAME, "*", KEYWORD_FIELD_NAME).build()
-                    )
-                );
-            query = new MultiMatchQueryBuilder("hello").toQuery(context);
-            assertQueryWithAllFieldsWildcard(query);
-
-            context.getIndexSettings()
-                .updateIndexMetadata(
-                    newIndexMeta(
-                        "index",
-                        context.getIndexSettings().getSettings(),
-                        Settings.builder().putList("index.query.default_field", TEXT_FIELD_NAME, KEYWORD_FIELD_NAME + "^5").build()
-                    )
-                );
-            MultiMatchQueryBuilder qb = new MultiMatchQueryBuilder("hello");
-            query = qb.toQuery(context);
-            DisjunctionMaxQuery expected = new DisjunctionMaxQuery(
-                Arrays.asList(
-                    new TermQuery(new Term(TEXT_FIELD_NAME, "hello")),
-                    new BoostQuery(new TermQuery(new Term(KEYWORD_FIELD_NAME, "hello")), 5.0f)
-                ),
-                0.0f
-            );
-            assertEquals(expected, query);
-
-            context.getIndexSettings()
-                .updateIndexMetadata(
-                    newIndexMeta(
-                        "index",
-                        context.getIndexSettings().getSettings(),
-                        Settings.builder()
-                            .putList("index.query.default_field", TEXT_FIELD_NAME, KEYWORD_FIELD_NAME + "^5", INT_FIELD_NAME)
-                            .build()
-                    )
-                );
-            // should fail because lenient defaults to false
-            IllegalArgumentException exc = expectThrows(IllegalArgumentException.class, () -> qb.toQuery(context));
-            assertThat(exc, instanceOf(NumberFormatException.class));
-            assertThat(exc.getMessage(), equalTo("For input string: \"hello\""));
-
-            // explicitly sets lenient
-            qb.lenient(true);
-            query = qb.toQuery(context);
-            expected = new DisjunctionMaxQuery(
-                Arrays.asList(
-                    new MatchNoDocsQuery("failed [mapped_int] query, caused by number_format_exception:[For input string: \"hello\"]"),
-                    new TermQuery(new Term(TEXT_FIELD_NAME, "hello")),
-                    new BoostQuery(new TermQuery(new Term(KEYWORD_FIELD_NAME, "hello")), 5.0f)
-                ),
-                0.0f
-            );
-            assertEquals(expected, query);
-
-        } finally {
-            // Reset to the default value
-            context.getIndexSettings()
-                .updateIndexMetadata(
-                    newIndexMeta(
-                        "index",
-                        context.getIndexSettings().getSettings(),
-                        Settings.builder().putNull("index.query.default_field").build()
-                    )
-                );
-        }
-    }
-
-    public void testAllFieldsWildcard() throws Exception {
-        SearchExecutionContext context = createSearchExecutionContext();
-        Query query = new MultiMatchQueryBuilder("hello").field("*").toQuery(context);
-        assertQueryWithAllFieldsWildcard(query);
-
-        query = new MultiMatchQueryBuilder("hello").field(TEXT_FIELD_NAME).field("*").field(KEYWORD_FIELD_NAME).toQuery(context);
-        assertQueryWithAllFieldsWildcard(query);
-    }
-
     public void testWithStopWords() throws Exception {
         Query query = new MultiMatchQueryBuilder("the quick fox").field(TEXT_FIELD_NAME)
             .analyzer("stop")

+ 250 - 0
server/src/test/java/org/elasticsearch/index/query/QueryStringQueryBuilderMultiFieldTests.java

@@ -0,0 +1,250 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+package org.elasticsearch.index.query;
+
+import org.apache.lucene.index.Term;
+import org.apache.lucene.search.BoostQuery;
+import org.apache.lucene.search.DisjunctionMaxQuery;
+import org.apache.lucene.search.IndexSearcher;
+import org.apache.lucene.search.MatchNoDocsQuery;
+import org.apache.lucene.search.Query;
+import org.apache.lucene.search.TermQuery;
+import org.elasticsearch.action.support.PlainActionFuture;
+import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.index.mapper.MapperService;
+import org.elasticsearch.index.mapper.MapperServiceTestCase;
+import org.elasticsearch.index.mapper.ParsedDocument;
+
+import java.io.IOException;
+import java.util.List;
+
+import static org.elasticsearch.index.query.QueryBuilders.queryStringQuery;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.equalTo;
+
+public class QueryStringQueryBuilderMultiFieldTests extends MapperServiceTestCase {
+
+    public void testToQueryFieldsWildcard() throws Exception {
+
+        MapperService mapperService = createMapperService("""
+            { "_doc" : { "properties" : {
+                "f_text" : { "type" : "text" },
+                "f_keyword" : { "type" : "keyword" }
+            }}}
+            """);
+
+        ParsedDocument doc = mapperService.documentMapper().parse(source("""
+            { "f_text" : "foo", "f_keyword" : "bar" }
+            """));
+
+        withLuceneIndex(mapperService, iw -> iw.addDocument(doc.rootDoc()), ir -> {
+            Query query = queryStringQuery("test").field("f*").toQuery(createSearchExecutionContext(mapperService, new IndexSearcher(ir)));
+            Query expected = new DisjunctionMaxQuery(
+                List.of(new TermQuery(new Term("f_text", "test")), new TermQuery(new Term("f_keyword", "test"))),
+                0
+            );
+            assertEquals(expected, query);
+        });
+    }
+
+    public void testDefaultField() throws Exception {
+
+        MapperService mapperService = createMapperService("""
+            { "_doc" : { "properties" : {
+                "f_text1" : { "type" : "text" },
+                "f_text2" : { "type" : "text" },
+                "f_keyword1" : { "type" : "keyword" },
+                "f_keyword2" : { "type" : "keyword" },
+                "f_date" : { "type" : "date" }
+            }}}
+            """);
+
+        ParsedDocument doc = mapperService.documentMapper().parse(source("""
+            { "f_text1" : "foo", "f_text2" : "bar", "f_keyword1" : "baz", "f_keyword2" : "buz", "f_date" : "2021-12-20T00:00:00" }
+            """));
+
+        withLuceneIndex(mapperService, iw -> iw.addDocument(doc.rootDoc()), ir -> {
+
+            IndexSearcher searcher = new IndexSearcher(ir);
+
+            {
+                // default value 'index.query.default_field = *' sets leniency to true
+                SearchExecutionContext context = createSearchExecutionContext(mapperService, searcher);
+                Query query = new QueryStringQueryBuilder("hello").toQuery(context);
+                Query expected = new DisjunctionMaxQuery(
+                    List.of(
+                        new TermQuery(new Term("f_text1", "hello")),
+                        new TermQuery(new Term("f_text2", "hello")),
+                        new TermQuery(new Term("f_keyword1", "hello")),
+                        new TermQuery(new Term("f_keyword2", "hello")),
+                        new MatchNoDocsQuery()
+                    ),
+                    0f
+                );
+                assertThat(query, equalTo(expected));
+            }
+
+            {
+                // default field list contains '*' sets leniency to true
+                Settings settings = Settings.builder().putList("index.query.default_field", "f_text1", "*").build();
+                SearchExecutionContext context = createSearchExecutionContext(mapperService, searcher, settings);
+                Query query = new QueryStringQueryBuilder("hello").toQuery(context);
+                Query expected = new DisjunctionMaxQuery(
+                    List.of(
+                        new TermQuery(new Term("f_text1", "hello")),
+                        new TermQuery(new Term("f_text2", "hello")),
+                        new TermQuery(new Term("f_keyword1", "hello")),
+                        new TermQuery(new Term("f_keyword2", "hello")),
+                        new MatchNoDocsQuery()
+                    ),
+                    0f
+                );
+                assertThat(query, equalTo(expected));
+            }
+
+            {
+                // default field list contains no wildcards, leniency = false
+                Settings settings = Settings.builder().putList("index.query.default_field", "f_text1", "f_date").build();
+                SearchExecutionContext context = createSearchExecutionContext(mapperService, searcher, settings);
+                Exception e = expectThrows(Exception.class, () -> new QueryStringQueryBuilder("hello").toQuery(context));
+                assertThat(e.getMessage(), containsString("failed to parse date field [hello]"));
+            }
+
+            {
+                // default field list contains boost
+                Settings settings = Settings.builder().putList("index.query.default_field", "f_text1", "f_text2^4").build();
+                SearchExecutionContext context = createSearchExecutionContext(mapperService, searcher, settings);
+                Query query = new QueryStringQueryBuilder("hello").toQuery(context);
+                Query expected = new DisjunctionMaxQuery(
+                    List.of(new TermQuery(new Term("f_text1", "hello")), new BoostQuery(new TermQuery(new Term("f_text2", "hello")), 4f)),
+                    0f
+                );
+                assertThat(query, equalTo(expected));
+            }
+
+        });
+    }
+
+    public void testFieldListIncludesWildcard() throws Exception {
+
+        MapperService mapperService = createMapperService("""
+            { "_doc" : { "properties" : {
+                "f_text1" : { "type" : "text" },
+                "f_text2" : { "type" : "text" },
+                "f_keyword1" : { "type" : "keyword" },
+                "f_keyword2" : { "type" : "keyword" },
+                "f_date" : { "type" : "date" }
+            }}}
+            """);
+
+        ParsedDocument doc = mapperService.documentMapper().parse(source("""
+            { "f_text1" : "foo", "f_text2" : "bar", "f_keyword1" : "baz", "f_keyword2" : "buz", "f_date" : "2021-12-20T00:00:00" }
+            """));
+
+        withLuceneIndex(mapperService, iw -> iw.addDocument(doc.rootDoc()), ir -> {
+            SearchExecutionContext context = createSearchExecutionContext(mapperService, new IndexSearcher(ir));
+            Query expected = new DisjunctionMaxQuery(
+                List.of(
+                    new TermQuery(new Term("f_text1", "hello")),
+                    new TermQuery(new Term("f_text2", "hello")),
+                    new TermQuery(new Term("f_keyword1", "hello")),
+                    new TermQuery(new Term("f_keyword2", "hello")),
+                    new MatchNoDocsQuery()
+                ),
+                0f
+            );
+            assertEquals(expected, new QueryStringQueryBuilder("hello").field("*").toQuery(context));
+            assertEquals(expected, new QueryStringQueryBuilder("hello").field("f_text1").field("*").toQuery(context));
+        });
+    }
+
+    public void testMergeBoosts() throws IOException {
+
+        MapperService mapperService = createMapperService("""
+            { "_doc" : { "properties" : {
+                "f_text" : { "type" : "text" },
+                "f_keyword" : { "type" : "keyword" }
+            }}}
+            """);
+
+        ParsedDocument doc = mapperService.documentMapper().parse(source("""
+            { "f_text" : "foo", "f_keyword" : "bar" }
+            """));
+
+        withLuceneIndex(mapperService, iw -> iw.addDocument(doc.rootDoc()), ir -> {
+            Query query = queryStringQuery("first").type(MultiMatchQueryBuilder.Type.MOST_FIELDS)
+                .field("f_text", 0.3f)
+                .field("f*", 0.5f)
+                .toQuery(createSearchExecutionContext(mapperService, new IndexSearcher(ir)));
+            Query expected = new DisjunctionMaxQuery(
+                List.of(
+                    new BoostQuery(new TermQuery(new Term("f_text", "first")), 0.15f),
+                    new BoostQuery(new TermQuery(new Term("f_keyword", "first")), 0.5f)
+                ),
+                1
+            );
+            assertEquals(expected, query);
+        });
+    }
+
+    /**
+     * Query terms that contain "now" can trigger a query to not be cacheable.
+     * This test checks the search context cacheable flag is updated accordingly.
+     */
+    public void testCachingStrategiesWithNow() throws IOException {
+
+        MapperService mapperService = createMapperService("""
+            { "_doc" : { "properties" : {
+                "f_text1" : { "type" : "text" },
+                "f_date" : { "type" : "date" }
+            }}}
+            """);
+
+        ParsedDocument doc = mapperService.documentMapper().parse(source("""
+            { "f_text1" : "foo", "f_date" : "2021-12-20T00:00:00" }
+            """));
+
+        withLuceneIndex(mapperService, iw -> iw.addDocument(doc.rootDoc()), ir -> {
+            IndexSearcher searcher = new IndexSearcher(ir);
+
+            // if we hit all fields, this should contain a date field and should disable cachability
+            String query = "now " + randomAlphaOfLengthBetween(4, 10);
+            QueryStringQueryBuilder queryBuilder = new QueryStringQueryBuilder(query);
+            assertQueryCachability(queryBuilder, createSearchExecutionContext(mapperService, searcher), false);
+
+            // querying the date field directly with 'now' disables cachability
+            queryBuilder = new QueryStringQueryBuilder("now").field("f_date");
+            assertQueryCachability(queryBuilder, createSearchExecutionContext(mapperService, searcher), false);
+
+            // but it's only lower case that matters
+            query = randomFrom("NoW", "nOw", "NOW") + " " + randomAlphaOfLengthBetween(4, 10);
+            queryBuilder = new QueryStringQueryBuilder(query);
+            assertQueryCachability(queryBuilder, createSearchExecutionContext(mapperService, searcher), true);
+        });
+    }
+
+    private void assertQueryCachability(QueryStringQueryBuilder qb, SearchExecutionContext context, boolean cachingExpected)
+        throws IOException {
+        assert context.isCacheable();
+        /*
+         * We use a private rewrite context here since we want the most realistic way of asserting that we are cacheable or not. We do it
+         * this way in SearchService where we first rewrite the query with a private context, then reset the context and then build the
+         * actual lucene query
+         */
+        PlainActionFuture<QueryBuilder> future = new PlainActionFuture<>();
+        Rewriteable.rewriteAndFetch(qb, new SearchExecutionContext(context), future);
+        QueryBuilder rewritten = future.actionGet();
+        assertNotNull(rewritten.toQuery(context));
+        assertEquals(
+            "query should " + (cachingExpected ? "" : "not") + " be cacheable: " + qb.toString(),
+            cachingExpected,
+            context.isCacheable()
+        );
+    }
+}

+ 0 - 122
server/src/test/java/org/elasticsearch/index/query/QueryStringQueryBuilderTests.java

@@ -60,7 +60,6 @@ import java.time.DateTimeException;
 import java.time.ZoneId;
 import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.Collection;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Locale;
@@ -500,15 +499,6 @@ public class QueryStringQueryBuilderTests extends AbstractQueryTestCase<QueryStr
         assertEquals(expected, query);
     }
 
-    public void testToQueryFieldsWildcard() throws Exception {
-        Query query = queryStringQuery("test").field("mapped_str*").toQuery(createSearchExecutionContext());
-        Query expected = new DisjunctionMaxQuery(
-            List.of(new TermQuery(new Term(TEXT_FIELD_NAME, "test")), new TermQuery(new Term(KEYWORD_FIELD_NAME, "test"))),
-            0
-        );
-        assertEquals(expected, query);
-    }
-
     /**
      * Test that dissalowing leading wildcards causes exception
      */
@@ -1205,64 +1195,6 @@ public class QueryStringQueryBuilderTests extends AbstractQueryTestCase<QueryStr
 
     }
 
-    public void testDefaultField() throws Exception {
-        SearchExecutionContext context = createSearchExecutionContext();
-        // default value `*` sets leniency to true
-        Query query = new QueryStringQueryBuilder("hello").toQuery(context);
-        assertQueryWithAllFieldsWildcard(query);
-
-        try {
-            // `*` is in the list of the default_field => leniency set to true
-            context.getIndexSettings()
-                .updateIndexMetadata(
-                    newIndexMeta(
-                        "index",
-                        context.getIndexSettings().getSettings(),
-                        Settings.builder().putList("index.query.default_field", TEXT_FIELD_NAME, "*", KEYWORD_FIELD_NAME).build()
-                    )
-                );
-            query = new QueryStringQueryBuilder("hello").toQuery(context);
-            assertQueryWithAllFieldsWildcard(query);
-
-            context.getIndexSettings()
-                .updateIndexMetadata(
-                    newIndexMeta(
-                        "index",
-                        context.getIndexSettings().getSettings(),
-                        Settings.builder().putList("index.query.default_field", TEXT_FIELD_NAME, KEYWORD_FIELD_NAME + "^5").build()
-                    )
-                );
-            query = new QueryStringQueryBuilder("hello").toQuery(context);
-            Query expected = new DisjunctionMaxQuery(
-                Arrays.asList(
-                    new TermQuery(new Term(TEXT_FIELD_NAME, "hello")),
-                    new BoostQuery(new TermQuery(new Term(KEYWORD_FIELD_NAME, "hello")), 5.0f)
-                ),
-                0.0f
-            );
-            assertEquals(expected, query);
-        } finally {
-            // Reset the default value
-            context.getIndexSettings()
-                .updateIndexMetadata(
-                    newIndexMeta(
-                        "index",
-                        context.getIndexSettings().getSettings(),
-                        Settings.builder().putList("index.query.default_field", "*").build()
-                    )
-                );
-        }
-    }
-
-    public void testAllFieldsWildcard() throws Exception {
-        SearchExecutionContext context = createSearchExecutionContext();
-        Query query = new QueryStringQueryBuilder("hello").field("*").toQuery(context);
-        assertQueryWithAllFieldsWildcard(query);
-
-        query = new QueryStringQueryBuilder("hello").field(TEXT_FIELD_NAME).field("*").field(KEYWORD_FIELD_NAME).toQuery(context);
-        assertQueryWithAllFieldsWildcard(query);
-    }
-
     /**
      * the quote analyzer should overwrite any other forced analyzer in quoted parts of the query
      */
@@ -1472,22 +1404,6 @@ public class QueryStringQueryBuilderTests extends AbstractQueryTestCase<QueryStr
         assertThat(exc.getMessage(), CoreMatchers.containsString("negative [boost]"));
     }
 
-    public void testMergeBoosts() throws IOException {
-        Query query = new QueryStringQueryBuilder("first").type(MultiMatchQueryBuilder.Type.MOST_FIELDS)
-            .field(TEXT_FIELD_NAME, 0.3f)
-            .field(TEXT_FIELD_NAME.substring(0, TEXT_FIELD_NAME.length() - 2) + "*", 0.5f)
-            .toQuery(createSearchExecutionContext());
-        assertThat(query, instanceOf(DisjunctionMaxQuery.class));
-        Collection<Query> disjuncts = ((DisjunctionMaxQuery) query).getDisjuncts();
-        assertThat(
-            disjuncts,
-            hasItems(
-                new BoostQuery(new TermQuery(new Term(TEXT_FIELD_NAME, "first")), 0.075f),
-                new BoostQuery(new TermQuery(new Term(KEYWORD_FIELD_NAME, "first")), 0.5f)
-            )
-        );
-    }
-
     private static IndexMetadata newIndexMeta(String name, Settings oldIndexSettings, Settings indexSettings) {
         Settings build = Settings.builder().put(oldIndexSettings).put(indexSettings).build();
         return IndexMetadata.builder(name).settings(build).build();
@@ -1509,44 +1425,6 @@ public class QueryStringQueryBuilderTests extends AbstractQueryTestCase<QueryStr
         );
     }
 
-    /**
-     * Query terms that contain "now" can trigger a query to not be cacheable.
-     * This test checks the search context cacheable flag is updated accordingly.
-     */
-    public void testCachingStrategiesWithNow() throws IOException {
-        // if we hit all fields, this should contain a date field and should diable cachability
-        String query = "now " + randomAlphaOfLengthBetween(4, 10);
-        QueryStringQueryBuilder queryStringQueryBuilder = new QueryStringQueryBuilder(query);
-        assertQueryCachability(queryStringQueryBuilder, false);
-
-        // if we hit a date field with "now", this should diable cachability
-        queryStringQueryBuilder = new QueryStringQueryBuilder("now");
-        queryStringQueryBuilder.field(DATE_FIELD_NAME);
-        assertQueryCachability(queryStringQueryBuilder, false);
-
-        // everything else is fine on all fields
-        query = randomFrom("NoW", "nOw", "NOW") + " " + randomAlphaOfLengthBetween(4, 10);
-        queryStringQueryBuilder = new QueryStringQueryBuilder(query);
-        assertQueryCachability(queryStringQueryBuilder, true);
-    }
-
-    private void assertQueryCachability(QueryStringQueryBuilder qb, boolean cachingExpected) throws IOException {
-        SearchExecutionContext context = createSearchExecutionContext();
-        assert context.isCacheable();
-        /*
-         * We use a private rewrite context here since we want the most realistic way of asserting that we are cacheable or not. We do it
-         * this way in SearchService where we first rewrite the query with a private context, then reset the context and then build the
-         * actual lucene query
-         */
-        QueryBuilder rewritten = rewriteQuery(qb, new SearchExecutionContext(context));
-        assertNotNull(rewritten.toQuery(context));
-        assertEquals(
-            "query should " + (cachingExpected ? "" : "not") + " be cacheable: " + qb.toString(),
-            cachingExpected,
-            context.isCacheable()
-        );
-    }
-
     public void testWhitespaceKeywordQueries() throws IOException {
         String query = "\"query with spaces\"";
         QueryStringQueryBuilder b = new QueryStringQueryBuilder(query);

+ 198 - 0
server/src/test/java/org/elasticsearch/index/query/SimpleQueryStringBuilderMultiFieldTests.java

@@ -0,0 +1,198 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+package org.elasticsearch.index.query;
+
+import org.apache.lucene.index.Term;
+import org.apache.lucene.search.BoostQuery;
+import org.apache.lucene.search.DisjunctionMaxQuery;
+import org.apache.lucene.search.IndexSearcher;
+import org.apache.lucene.search.MatchNoDocsQuery;
+import org.apache.lucene.search.Query;
+import org.apache.lucene.search.TermQuery;
+import org.elasticsearch.action.support.PlainActionFuture;
+import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.index.mapper.MapperService;
+import org.elasticsearch.index.mapper.MapperServiceTestCase;
+import org.elasticsearch.index.mapper.ParsedDocument;
+
+import java.io.IOException;
+import java.util.List;
+
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.equalTo;
+
+public class SimpleQueryStringBuilderMultiFieldTests extends MapperServiceTestCase {
+
+    public void testDefaultField() throws Exception {
+
+        MapperService mapperService = createMapperService("""
+            { "_doc" : { "properties" : {
+                "f_text1" : { "type" : "text" },
+                "f_text2" : { "type" : "text" },
+                "f_keyword1" : { "type" : "keyword" },
+                "f_keyword2" : { "type" : "keyword" },
+                "f_date" : { "type" : "date" }
+            }}}
+            """);
+
+        ParsedDocument doc = mapperService.documentMapper().parse(source("""
+            { "f_text1" : "foo", "f_text2" : "bar", "f_keyword1" : "baz", "f_keyword2" : "buz", "f_date" : "2021-12-20T00:00:00" }
+            """));
+
+        withLuceneIndex(mapperService, iw -> iw.addDocument(doc.rootDoc()), ir -> {
+
+            IndexSearcher searcher = new IndexSearcher(ir);
+
+            {
+                // default value 'index.query.default_field = *' sets leniency to true
+                SearchExecutionContext context = createSearchExecutionContext(mapperService, searcher);
+                Query query = new SimpleQueryStringBuilder("hello").toQuery(context);
+                Query expected = new DisjunctionMaxQuery(
+                    List.of(
+                        new TermQuery(new Term("f_text1", "hello")),
+                        new TermQuery(new Term("f_text2", "hello")),
+                        new TermQuery(new Term("f_keyword1", "hello")),
+                        new TermQuery(new Term("f_keyword2", "hello")),
+                        new MatchNoDocsQuery()
+                    ),
+                    1f
+                );
+                assertThat(query, equalTo(expected));
+            }
+
+            {
+                // default field list contains '*' sets leniency to true
+                Settings settings = Settings.builder().putList("index.query.default_field", "f_text1", "*").build();
+                SearchExecutionContext context = createSearchExecutionContext(mapperService, searcher, settings);
+                Query query = new SimpleQueryStringBuilder("hello").toQuery(context);
+                Query expected = new DisjunctionMaxQuery(
+                    List.of(
+                        new TermQuery(new Term("f_text1", "hello")),
+                        new TermQuery(new Term("f_text2", "hello")),
+                        new TermQuery(new Term("f_keyword1", "hello")),
+                        new TermQuery(new Term("f_keyword2", "hello")),
+                        new MatchNoDocsQuery()
+                    ),
+                    1f
+                );
+                assertThat(query, equalTo(expected));
+            }
+
+            {
+                // default field list contains no wildcards, leniency = false
+                Settings settings = Settings.builder().putList("index.query.default_field", "f_text1", "f_date").build();
+                SearchExecutionContext context = createSearchExecutionContext(mapperService, searcher, settings);
+                Exception e = expectThrows(Exception.class, () -> new SimpleQueryStringBuilder("hello").toQuery(context));
+                assertThat(e.getMessage(), containsString("failed to parse date field [hello]"));
+            }
+
+            {
+                // default field list contains boost
+                Settings settings = Settings.builder().putList("index.query.default_field", "f_text1", "f_text2^4").build();
+                SearchExecutionContext context = createSearchExecutionContext(mapperService, searcher, settings);
+                Query query = new SimpleQueryStringBuilder("hello").toQuery(context);
+                Query expected = new DisjunctionMaxQuery(
+                    List.of(new TermQuery(new Term("f_text1", "hello")), new BoostQuery(new TermQuery(new Term("f_text2", "hello")), 4f)),
+                    1f
+                );
+                assertThat(query, equalTo(expected));
+            }
+
+        });
+    }
+
+    public void testFieldListIncludesWildcard() throws Exception {
+
+        MapperService mapperService = createMapperService("""
+            { "_doc" : { "properties" : {
+                "f_text1" : { "type" : "text" },
+                "f_text2" : { "type" : "text" },
+                "f_keyword1" : { "type" : "keyword" },
+                "f_keyword2" : { "type" : "keyword" },
+                "f_date" : { "type" : "date" }
+            }}}
+            """);
+
+        ParsedDocument doc = mapperService.documentMapper().parse(source("""
+            { "f_text1" : "foo", "f_text2" : "bar", "f_keyword1" : "baz", "f_keyword2" : "buz", "f_date" : "2021-12-20T00:00:00" }
+            """));
+
+        withLuceneIndex(mapperService, iw -> iw.addDocument(doc.rootDoc()), ir -> {
+            SearchExecutionContext context = createSearchExecutionContext(mapperService, new IndexSearcher(ir));
+            Query expected = new DisjunctionMaxQuery(
+                List.of(
+                    new TermQuery(new Term("f_text1", "hello")),
+                    new TermQuery(new Term("f_text2", "hello")),
+                    new TermQuery(new Term("f_keyword1", "hello")),
+                    new TermQuery(new Term("f_keyword2", "hello")),
+                    new MatchNoDocsQuery()
+                ),
+                1f
+            );
+            assertEquals(expected, new SimpleQueryStringBuilder("hello").field("*").toQuery(context));
+            assertEquals(expected, new SimpleQueryStringBuilder("hello").field("f_text1").field("*").toQuery(context));
+        });
+    }
+
+    /**
+     * Query terms that contain "now" can trigger a query to not be cacheable.
+     * This test checks the search context cacheable flag is updated accordingly.
+     */
+    public void testCachingStrategiesWithNow() throws IOException {
+
+        MapperService mapperService = createMapperService("""
+            { "_doc" : { "properties" : {
+                "f_text1" : { "type" : "text" },
+                "f_date" : { "type" : "date" }
+            }}}
+            """);
+
+        ParsedDocument doc = mapperService.documentMapper().parse(source("""
+            { "f_text1" : "foo", "f_date" : "2021-12-20T00:00:00" }
+            """));
+
+        withLuceneIndex(mapperService, iw -> iw.addDocument(doc.rootDoc()), ir -> {
+            IndexSearcher searcher = new IndexSearcher(ir);
+
+            // if we hit all fields, this should contain a date field and should disable cachability
+            String query = "now " + randomAlphaOfLengthBetween(4, 10);
+            SimpleQueryStringBuilder queryBuilder = new SimpleQueryStringBuilder(query);
+            assertQueryCachability(queryBuilder, createSearchExecutionContext(mapperService, searcher), false);
+
+            // querying the date field directly with 'now' disables cachability
+            queryBuilder = new SimpleQueryStringBuilder("now").field("f_date");
+            assertQueryCachability(queryBuilder, createSearchExecutionContext(mapperService, searcher), false);
+
+            // but it's only lower case that matters
+            query = randomFrom("NoW", "nOw", "NOW") + " " + randomAlphaOfLengthBetween(4, 10);
+            queryBuilder = new SimpleQueryStringBuilder(query);
+            assertQueryCachability(queryBuilder, createSearchExecutionContext(mapperService, searcher), true);
+        });
+    }
+
+    private void assertQueryCachability(SimpleQueryStringBuilder qb, SearchExecutionContext context, boolean cachingExpected)
+        throws IOException {
+        assert context.isCacheable();
+        /*
+         * We use a private rewrite context here since we want the most realistic way of asserting that we are cacheable or not. We do it
+         * this way in SearchService where we first rewrite the query with a private context, then reset the context and then build the
+         * actual lucene query
+         */
+        PlainActionFuture<QueryBuilder> future = new PlainActionFuture<>();
+        Rewriteable.rewriteAndFetch(qb, new SearchExecutionContext(context), future);
+        QueryBuilder rewritten = future.actionGet();
+        assertNotNull(rewritten.toQuery(context));
+        assertEquals(
+            "query should " + (cachingExpected ? "" : "not") + " be cacheable: " + qb.toString(),
+            cachingExpected,
+            context.isCacheable()
+        );
+    }
+
+}

+ 0 - 139
server/src/test/java/org/elasticsearch/index/query/SimpleQueryStringBuilderTests.java

@@ -27,8 +27,6 @@ import org.apache.lucene.search.Query;
 import org.apache.lucene.search.SynonymQuery;
 import org.apache.lucene.search.TermQuery;
 import org.apache.lucene.util.TestUtil;
-import org.elasticsearch.cluster.metadata.IndexMetadata;
-import org.elasticsearch.common.settings.Settings;
 import org.elasticsearch.index.search.SimpleQueryStringQueryParser;
 import org.elasticsearch.test.AbstractQueryTestCase;
 
@@ -44,8 +42,6 @@ import java.util.Map;
 import java.util.Set;
 
 import static org.hamcrest.CoreMatchers.containsString;
-import static org.hamcrest.CoreMatchers.hasItems;
-import static org.hamcrest.Matchers.anyOf;
 import static org.hamcrest.Matchers.either;
 import static org.hamcrest.Matchers.equalTo;
 import static org.hamcrest.Matchers.instanceOf;
@@ -248,24 +244,6 @@ public class SimpleQueryStringBuilderTests extends AbstractQueryTestCase<SimpleQ
         assertEquals("fields cannot be null", e.getMessage());
     }
 
-    public void testDefaultFieldParsing() throws IOException {
-        String query = randomAlphaOfLengthBetween(1, 10).toLowerCase(Locale.ROOT);
-        String contentString = """
-            {
-              "simple_query_string": {
-                "query": "%s"
-              }
-            }""".formatted(query);
-        SimpleQueryStringBuilder queryBuilder = (SimpleQueryStringBuilder) parseQuery(contentString);
-        assertThat(queryBuilder.value(), equalTo(query));
-        assertThat(queryBuilder.fields(), notNullValue());
-        assertThat(queryBuilder.fields().size(), equalTo(0));
-        SearchExecutionContext searchExecutionContext = createSearchExecutionContext();
-
-        Query luceneQuery = queryBuilder.toQuery(searchExecutionContext);
-        assertThat(luceneQuery, anyOf(instanceOf(BooleanQuery.class), instanceOf(DisjunctionMaxQuery.class)));
-    }
-
     /*
      * This assumes that Lucene query parsing is being checked already, adding
      * checks only for our parsing extensions.
@@ -621,64 +599,6 @@ public class SimpleQueryStringBuilderTests extends AbstractQueryTestCase<SimpleQ
         assertEquals(new TermQuery(new Term(TEXT_FIELD_NAME, "bar")), parser.parse("\"bar\""));
     }
 
-    public void testDefaultField() throws Exception {
-        SearchExecutionContext context = createSearchExecutionContext();
-        // default value `*` sets leniency to true
-        Query query = new SimpleQueryStringBuilder("hello").toQuery(context);
-        assertQueryWithAllFieldsWildcard(query);
-
-        try {
-            // `*` is in the list of the default_field => leniency set to true
-            context.getIndexSettings()
-                .updateIndexMetadata(
-                    newIndexMeta(
-                        "index",
-                        context.getIndexSettings().getSettings(),
-                        Settings.builder().putList("index.query.default_field", TEXT_FIELD_NAME, "*", KEYWORD_FIELD_NAME).build()
-                    )
-                );
-            query = new SimpleQueryStringBuilder("hello").toQuery(context);
-            assertQueryWithAllFieldsWildcard(query);
-
-            context.getIndexSettings()
-                .updateIndexMetadata(
-                    newIndexMeta(
-                        "index",
-                        context.getIndexSettings().getSettings(),
-                        Settings.builder().putList("index.query.default_field", TEXT_FIELD_NAME, KEYWORD_FIELD_NAME + "^5").build()
-                    )
-                );
-            query = new SimpleQueryStringBuilder("hello").toQuery(context);
-            Query expected = new DisjunctionMaxQuery(
-                Arrays.asList(
-                    new TermQuery(new Term(TEXT_FIELD_NAME, "hello")),
-                    new BoostQuery(new TermQuery(new Term(KEYWORD_FIELD_NAME, "hello")), 5.0f)
-                ),
-                1.0f
-            );
-            assertEquals(expected, query);
-        } finally {
-            // Reset to the default value
-            context.getIndexSettings()
-                .updateIndexMetadata(
-                    newIndexMeta(
-                        "index",
-                        context.getIndexSettings().getSettings(),
-                        Settings.builder().putList("index.query.default_field", "*").build()
-                    )
-                );
-        }
-    }
-
-    public void testAllFieldsWildcard() throws Exception {
-        SearchExecutionContext context = createSearchExecutionContext();
-        Query query = new SimpleQueryStringBuilder("hello").field("*").toQuery(context);
-        assertQueryWithAllFieldsWildcard(query);
-
-        query = new SimpleQueryStringBuilder("hello").field(TEXT_FIELD_NAME).field("*").field(KEYWORD_FIELD_NAME).toQuery(context);
-        assertQueryWithAllFieldsWildcard(query);
-    }
-
     public void testToFuzzyQuery() throws Exception {
         Query query = new SimpleQueryStringBuilder("text~2").field(TEXT_FIELD_NAME)
             .fuzzyPrefixLength(2)
@@ -799,65 +719,6 @@ public class SimpleQueryStringBuilderTests extends AbstractQueryTestCase<SimpleQ
         assertThat(exc.getMessage(), containsString("negative [boost]"));
     }
 
-    private static IndexMetadata newIndexMeta(String name, Settings oldIndexSettings, Settings indexSettings) {
-        Settings build = Settings.builder().put(oldIndexSettings).put(indexSettings).build();
-        return IndexMetadata.builder(name).settings(build).build();
-    }
-
-    private void assertQueryWithAllFieldsWildcard(Query query) {
-        assertEquals(DisjunctionMaxQuery.class, query.getClass());
-        DisjunctionMaxQuery disjunctionMaxQuery = (DisjunctionMaxQuery) query;
-        int noMatchNoDocsQueries = 0;
-        for (Query q : disjunctionMaxQuery.getDisjuncts()) {
-            if (q.getClass() == MatchNoDocsQuery.class) {
-                noMatchNoDocsQueries++;
-            }
-        }
-        assertEquals(9, noMatchNoDocsQueries);
-        assertThat(
-            disjunctionMaxQuery.getDisjuncts(),
-            hasItems(new TermQuery(new Term(TEXT_FIELD_NAME, "hello")), new TermQuery(new Term(KEYWORD_FIELD_NAME, "hello")))
-        );
-    }
-
-    /**
-     * Query terms that contain "now" can trigger a query to not be cacheable.
-     * This test checks the search context cacheable flag is updated accordingly.
-     */
-    public void testCachingStrategiesWithNow() throws IOException {
-        // if we hit all fields, this should contain a date field and should diable cachability
-        String query = "now " + randomAlphaOfLengthBetween(4, 10);
-        SimpleQueryStringBuilder queryBuilder = new SimpleQueryStringBuilder(query);
-        assertQueryCachability(queryBuilder, false);
-
-        // if we hit a date field with "now", this should diable cachability
-        queryBuilder = new SimpleQueryStringBuilder("now");
-        queryBuilder.field(DATE_FIELD_NAME);
-        assertQueryCachability(queryBuilder, false);
-
-        // everything else is fine on all fields
-        query = randomFrom("NoW", "nOw", "NOW") + " " + randomAlphaOfLengthBetween(4, 10);
-        queryBuilder = new SimpleQueryStringBuilder(query);
-        assertQueryCachability(queryBuilder, true);
-    }
-
-    private void assertQueryCachability(SimpleQueryStringBuilder qb, boolean cachingExpected) throws IOException {
-        SearchExecutionContext context = createSearchExecutionContext();
-        assert context.isCacheable();
-        /*
-         * We use a private rewrite context here since we want the most realistic way of asserting that we are cacheable or not. We do it
-         * this way in SearchService where we first rewrite the query with a private context, then reset the context and then build the
-         * actual lucene query
-         */
-        QueryBuilder rewritten = rewriteQuery(qb, new SearchExecutionContext(context));
-        assertNotNull(rewritten.toQuery(context));
-        assertEquals(
-            "query should " + (cachingExpected ? "" : "not") + " be cacheable: " + qb.toString(),
-            cachingExpected,
-            context.isCacheable()
-        );
-    }
-
     public void testLenientFlag() throws Exception {
         SimpleQueryStringBuilder query = new SimpleQueryStringBuilder("test").field(BINARY_FIELD_NAME);
         SearchExecutionContext context = createSearchExecutionContext();

+ 71 - 20
server/src/test/java/org/elasticsearch/index/search/QueryParserHelperTests.java

@@ -11,6 +11,7 @@ package org.elasticsearch.index.search;
 import org.apache.lucene.search.IndexSearcher;
 import org.elasticsearch.index.mapper.MapperService;
 import org.elasticsearch.index.mapper.MapperServiceTestCase;
+import org.elasticsearch.index.mapper.ParsedDocument;
 import org.elasticsearch.index.query.SearchExecutionContext;
 
 import java.io.IOException;
@@ -22,52 +23,102 @@ import static org.hamcrest.Matchers.hasSize;
 
 public class QueryParserHelperTests extends MapperServiceTestCase {
 
-    public void testUnmappedFieldsDoNotContributeToFieldCount() throws IOException {
+    public void testEmptyFieldResolution() throws IOException {
 
         MapperService mapperService = createMapperService(mapping(b -> {
             b.startObject("field1").field("type", "text").endObject();
             b.startObject("field2").field("type", "text").endObject();
         }));
 
+        // We check that expanded fields are actually present in the underlying lucene index,
+        // so a resolution against an empty index will result in 0 fields
         SearchExecutionContext context = createSearchExecutionContext(mapperService);
         {
             Map<String, Float> resolvedFields = QueryParserHelper.resolveMappingFields(context, Map.of("*", 1.0f));
-            assertThat(resolvedFields.keySet(), containsInAnyOrder("field1", "field2"));
+            assertTrue(resolvedFields.isEmpty());
         }
 
+        // If you ask for a specific mapped field, we will resolve it
         {
-            Map<String, Float> resolvedFields = QueryParserHelper.resolveMappingFields(context, Map.of("*", 1.0f, "unmapped", 2.0f));
-            assertThat(resolvedFields.keySet(), containsInAnyOrder("field1", "field2"));
-            assertFalse(resolvedFields.containsKey("unmapped"));
+            Map<String, Float> resolvedFields = QueryParserHelper.resolveMappingFields(context, Map.of("field1", 1.0f));
+            assertThat(resolvedFields.keySet(), containsInAnyOrder("field1"));
         }
 
+        // But unmapped fields will not be resolved
         {
             Map<String, Float> resolvedFields = QueryParserHelper.resolveMappingFields(context, Map.of("unmapped", 1.0f));
             assertTrue(resolvedFields.isEmpty());
         }
     }
 
+    public void testIndexedFieldResolution() throws IOException {
+
+        MapperService mapperService = createMapperService(mapping(b -> {
+            b.startObject("field1").field("type", "text").endObject();
+            b.startObject("field2").field("type", "text").endObject();
+            b.startObject("field3").field("type", "text").endObject();
+        }));
+
+        ParsedDocument doc = mapperService.documentMapper().parse(source(b -> {
+            b.field("field1", "foo");
+            b.field("field2", "bar");
+        }));
+
+        withLuceneIndex(mapperService, iw -> iw.addDocument(doc.rootDoc()), ir -> {
+
+            SearchExecutionContext context = createSearchExecutionContext(mapperService, new IndexSearcher(ir));
+
+            // field1 and field2 are present in the index, so they get resolved; field3 is in the mappings but
+            // not in the actual index, so it is ignored
+            {
+                Map<String, Float> resolvedFields = QueryParserHelper.resolveMappingFields(context, Map.of("field*", 1.0f));
+                assertThat(resolvedFields.keySet(), containsInAnyOrder("field1", "field2"));
+                assertFalse(resolvedFields.containsKey("field3"));
+            }
+
+            // unmapped fields get ignored
+            {
+                Map<String, Float> resolvedFields = QueryParserHelper.resolveMappingFields(context, Map.of("*", 1.0f, "unmapped", 2.0f));
+                assertThat(resolvedFields.keySet(), containsInAnyOrder("field1", "field2"));
+                assertFalse(resolvedFields.containsKey("unmapped"));
+            }
+
+            {
+                Map<String, Float> resolvedFields = QueryParserHelper.resolveMappingFields(context, Map.of("unmapped", 1.0f));
+                assertTrue(resolvedFields.isEmpty());
+            }
+        });
+    }
+
     public void testFieldExpansionAboveLimitThrowsException() throws IOException {
         MapperService mapperService = createMapperService(mapping(b -> {
             for (int i = 0; i < 10; i++) {
                 b.startObject("field" + i).field("type", "long").endObject();
             }
         }));
-        SearchExecutionContext context = createSearchExecutionContext(mapperService);
+        ParsedDocument doc = mapperService.documentMapper().parse(source(b -> {
+            for (int i = 0; i < 10; i++) {
+                b.field("field" + i, 1L);
+            }
+        }));
 
-        int originalMaxClauseCount = IndexSearcher.getMaxClauseCount();
-        try {
-            IndexSearcher.setMaxClauseCount(4);
-            Exception e = expectThrows(
-                IllegalArgumentException.class,
-                () -> QueryParserHelper.resolveMappingFields(context, Map.of("field*", 1.0f))
-            );
-            assertThat(e.getMessage(), containsString("field expansion matches too many fields"));
-
-            IndexSearcher.setMaxClauseCount(10);
-            assertThat(QueryParserHelper.resolveMappingFields(context, Map.of("field*", 1.0f)).keySet(), hasSize(10));
-        } finally {
-            IndexSearcher.setMaxClauseCount(originalMaxClauseCount);
-        }
+        withLuceneIndex(mapperService, iw -> iw.addDocument(doc.rootDoc()), ir -> {
+            SearchExecutionContext context = createSearchExecutionContext(mapperService, new IndexSearcher(ir));
+
+            int originalMaxClauseCount = IndexSearcher.getMaxClauseCount();
+            try {
+                IndexSearcher.setMaxClauseCount(4);
+                Exception e = expectThrows(
+                    IllegalArgumentException.class,
+                    () -> QueryParserHelper.resolveMappingFields(context, Map.of("field*", 1.0f))
+                );
+                assertThat(e.getMessage(), containsString("field expansion matches too many fields"));
+
+                IndexSearcher.setMaxClauseCount(10);
+                assertThat(QueryParserHelper.resolveMappingFields(context, Map.of("field*", 1.0f)).keySet(), hasSize(10));
+            } finally {
+                IndexSearcher.setMaxClauseCount(originalMaxClauseCount);
+            }
+        });
     }
 }

+ 16 - 3
test/framework/src/main/java/org/elasticsearch/index/mapper/MapperServiceTestCase.java

@@ -572,12 +572,25 @@ public abstract class MapperServiceTestCase extends ESTestCase {
     }
 
     protected SearchExecutionContext createSearchExecutionContext(MapperService mapperService) {
-        final SimilarityService similarityService = new SimilarityService(mapperService.getIndexSettings(), null, Map.of());
+        return createSearchExecutionContext(mapperService, null, Settings.EMPTY);
+    }
+
+    protected SearchExecutionContext createSearchExecutionContext(MapperService mapperService, IndexSearcher searcher) {
+        return createSearchExecutionContext(mapperService, searcher, Settings.EMPTY);
+    }
+
+    protected SearchExecutionContext createSearchExecutionContext(MapperService mapperService, IndexSearcher searcher, Settings settings) {
+        Settings mergedSettings = Settings.builder().put(mapperService.getIndexSettings().getSettings()).put(settings).build();
+        IndexMetadata indexMetadata = IndexMetadata.builder(mapperService.getIndexSettings().getIndexMetadata())
+            .settings(mergedSettings)
+            .build();
+        IndexSettings indexSettings = new IndexSettings(indexMetadata, Settings.EMPTY);
+        final SimilarityService similarityService = new SimilarityService(indexSettings, null, Map.of());
         final long nowInMillis = randomNonNegativeLong();
         return new SearchExecutionContext(
             0,
             0,
-            mapperService.getIndexSettings(),
+            indexSettings,
             null,
             (ft, idxName, lookup) -> ft.fielddataBuilder(idxName, lookup)
                 .build(new IndexFieldDataCache.None(), new NoneCircuitBreakerService()),
@@ -588,7 +601,7 @@ public abstract class MapperServiceTestCase extends ESTestCase {
             parserConfig(),
             writableRegistry(),
             null,
-            null,
+            searcher,
             () -> nowInMillis,
             null,
             null,

+ 5 - 0
x-pack/plugin/mapper-aggregate-metric/src/main/java/org/elasticsearch/xpack/aggregatemetric/mapper/AggregateDoubleMetricFieldMapper.java

@@ -323,6 +323,11 @@ public class AggregateDoubleMetricFieldMapper extends FieldMapper {
             return defaultMetric;
         }
 
+        @Override
+        public boolean mayExistInIndex(SearchExecutionContext context) {
+            return delegateFieldType().mayExistInIndex(context);    // TODO how does searching actually work here?
+        }
+
         @Override
         public Query existsQuery(SearchExecutionContext context) {
             return delegateFieldType().existsQuery(context);

+ 5 - 0
x-pack/plugin/mapper-unsigned-long/src/main/java/org/elasticsearch/xpack/unsignedlong/UnsignedLongFieldMapper.java

@@ -218,6 +218,11 @@ public class UnsignedLongFieldMapper extends FieldMapper {
             return CONTENT_TYPE;
         }
 
+        @Override
+        public boolean mayExistInIndex(SearchExecutionContext context) {
+            return context.fieldExistsInIndex(name());
+        }
+
         @Override
         public Query termQuery(Object value, SearchExecutionContext context) {
             failIfNotIndexed();

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

@@ -265,6 +265,11 @@ public class WildcardFieldMapper extends FieldMapper {
             this.ignoreAbove = ignoreAbove;
         }
 
+        @Override
+        public boolean mayExistInIndex(SearchExecutionContext context) {
+            return context.fieldExistsInIndex(name());
+        }
+
         @Override
         public Query normalizedWildcardQuery(String value, MultiTermQuery.RewriteMethod method, SearchExecutionContext context) {
             return wildcardQuery(value, method, false, context);