diff --git a/server/src/main/java/org/elasticsearch/index/mapper/DateFieldMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/DateFieldMapper.java index 20f1c0dadd592..e3499ec5c8fed 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/DateFieldMapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/DateFieldMapper.java @@ -893,7 +893,7 @@ public Relation isFieldWithinQuery( ++fromInclusive; } // we set the time range filter from during rewrite, because this may be the only time we ever parse it, - // in case the shard if filtered out and does not run the query phase or all its docs are within the bounds. + // in case the shard gets filtered out and does not run the query phase or all its docs are within the bounds. context.setTimeRangeFilterFromMillis(fieldName, fromInclusive, resolution); } diff --git a/server/src/main/java/org/elasticsearch/search/SearchService.java b/server/src/main/java/org/elasticsearch/search/SearchService.java index 4305e5cf15035..8bd95e128364b 100644 --- a/server/src/main/java/org/elasticsearch/search/SearchService.java +++ b/server/src/main/java/org/elasticsearch/search/SearchService.java @@ -721,13 +721,7 @@ public void executeQueryPhase(ShardSearchRequest request, CancellableTask task, if (orig.canReturnNullResponseIfMatchNoDocs()) { assert orig.scroll() == null; ShardSearchRequest clone = new ShardSearchRequest(orig); - CanMatchContext canMatchContext = new CanMatchContext( - clone, - indicesService::indexServiceSafe, - this::findReaderContext, - defaultKeepAlive, - maxKeepAlive - ); + CanMatchContext canMatchContext = createCanMatchContext(clone); CanMatchShardResponse canMatchResp = canMatch(canMatchContext, false); if (canMatchResp.canMatch() == false) { l.onResponse(QuerySearchResult.nullInstance()); @@ -1942,8 +1936,10 @@ public void canMatch(CanMatchNodeRequest request, ActionListener indexServiceLookup; @@ -1977,6 +1971,8 @@ static class CanMatchContext { private IndexService indexService; + private Long timeRangeFilterFromMillis; + CanMatchContext( ShardSearchRequest request, Function indexServiceLookup, @@ -2024,12 +2020,21 @@ IndexService getIndexService() { } return this.indexService; } + + void setTimeRangeFilterFromMillis(Long timeRangeFilterFromMillis) { + this.timeRangeFilterFromMillis = timeRangeFilterFromMillis; + } + + Long getTimeRangeFilterFromMillis() { + return timeRangeFilterFromMillis; + } } static CanMatchShardResponse canMatch(CanMatchContext canMatchContext, boolean checkRefreshPending) { assert canMatchContext.request.searchType() == SearchType.QUERY_THEN_FETCH : "unexpected search type: " + canMatchContext.request.searchType(); Releasable releasable = null; + QueryRewriteContext queryRewriteContext = null; try { IndexService indexService; final boolean hasRefreshPending; @@ -2042,7 +2047,7 @@ static CanMatchShardResponse canMatch(CanMatchContext canMatchContext, boolean c readerContext = canMatchContext.findReaderContext(); releasable = readerContext.markAsUsed(canMatchContext.getKeepAlive()); indexService = readerContext.indexService(); - QueryRewriteContext queryRewriteContext = canMatchContext.getQueryRewriteContext(indexService); + queryRewriteContext = canMatchContext.getQueryRewriteContext(indexService); if (queryStillMatchesAfterRewrite(canMatchContext.request, queryRewriteContext) == false) { return new CanMatchShardResponse(false, null); } @@ -2051,10 +2056,8 @@ static CanMatchShardResponse canMatch(CanMatchContext canMatchContext, boolean c if (canMatchContext.request.readerId().isRetryable() == false) { return new CanMatchShardResponse(true, null); } - if (queryStillMatchesAfterRewrite( - canMatchContext.request, - canMatchContext.getQueryRewriteContext(canMatchContext.getIndexService()) - ) == false) { + queryRewriteContext = canMatchContext.getQueryRewriteContext(canMatchContext.getIndexService()); + if (queryStillMatchesAfterRewrite(canMatchContext.request, queryRewriteContext) == false) { return new CanMatchShardResponse(false, null); } final Engine.SearcherSupplier searcherSupplier = canMatchContext.getShard().acquireSearcherSupplier(); @@ -2067,10 +2070,8 @@ static CanMatchShardResponse canMatch(CanMatchContext canMatchContext, boolean c } canMatchSearcher = searcher; } else { - if (queryStillMatchesAfterRewrite( - canMatchContext.request, - canMatchContext.getQueryRewriteContext(canMatchContext.getIndexService()) - ) == false) { + queryRewriteContext = canMatchContext.getQueryRewriteContext(canMatchContext.getIndexService()); + if (queryStillMatchesAfterRewrite(canMatchContext.request, queryRewriteContext) == false) { return new CanMatchShardResponse(false, null); } boolean needsWaitForRefresh = canMatchContext.request.waitForCheckpoint() != UNASSIGNED_SEQ_NO; @@ -2083,6 +2084,7 @@ static CanMatchShardResponse canMatch(CanMatchContext canMatchContext, boolean c } try (canMatchSearcher) { SearchExecutionContext context = canMatchContext.getSearchExecutionContext(canMatchSearcher); + queryRewriteContext = context; final boolean canMatch = queryStillMatchesAfterRewrite(canMatchContext.request, context); if (canMatch || hasRefreshPending) { FieldSortBuilder sortBuilder = FieldSortBuilder.getPrimaryFieldSortOrNull(canMatchContext.request.source()); @@ -2094,6 +2096,9 @@ static CanMatchShardResponse canMatch(CanMatchContext canMatchContext, boolean c } catch (Exception e) { return new CanMatchShardResponse(true, null); } finally { + if (queryRewriteContext != null) { + canMatchContext.setTimeRangeFilterFromMillis(queryRewriteContext.getTimeRangeFilterFromMillis()); + } Releasables.close(releasable); } } diff --git a/server/src/test/java/org/elasticsearch/search/SearchServiceTests.java b/server/src/test/java/org/elasticsearch/search/SearchServiceTests.java index 02d327285d6c0..de8d5d92f1aa4 100644 --- a/server/src/test/java/org/elasticsearch/search/SearchServiceTests.java +++ b/server/src/test/java/org/elasticsearch/search/SearchServiceTests.java @@ -31,6 +31,7 @@ import org.elasticsearch.index.fielddata.FieldDataContext; import org.elasticsearch.index.fielddata.IndexFieldData; import org.elasticsearch.index.fielddata.LeafFieldData; +import org.elasticsearch.index.mapper.DateFieldMapper; import org.elasticsearch.index.mapper.KeywordFieldMapper; import org.elasticsearch.index.mapper.MappedFieldType; import org.elasticsearch.index.mapper.MapperBuilderContext; @@ -43,10 +44,12 @@ import org.elasticsearch.index.query.MatchAllQueryBuilder; import org.elasticsearch.index.query.MatchNoneQueryBuilder; import org.elasticsearch.index.query.QueryRewriteContext; +import org.elasticsearch.index.query.RangeQueryBuilder; import org.elasticsearch.index.query.SearchExecutionContext; import org.elasticsearch.index.shard.IndexShard; import org.elasticsearch.index.shard.IndexShardTestCase; import org.elasticsearch.index.shard.ShardId; +import org.elasticsearch.script.ScriptCompiler; import org.elasticsearch.search.aggregations.support.ValuesSourceType; import org.elasticsearch.search.builder.SearchSourceBuilder; import org.elasticsearch.search.internal.AliasFilter; @@ -61,7 +64,10 @@ import org.elasticsearch.xcontent.XContentParserConfiguration; import java.io.IOException; +import java.time.LocalDateTime; +import java.time.ZoneOffset; import java.util.Collections; +import java.util.List; import java.util.Map; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; @@ -112,6 +118,17 @@ public void testCanMatchKeywordSortedQueryMatchAll() throws IOException { doTestCanMatch(searchRequest, sortField, true, expectedMinAndMax, false); } + public void testCanMatchTimeRangeFilter() throws IOException { + SearchRequest searchRequest = new SearchRequest().allowPartialSearchResults(false) + .source(new SearchSourceBuilder().query(new RangeQueryBuilder("@timestamp").from("2025-12-15"))); + SearchService.CanMatchContext canMatchContext = doTestCanMatch(searchRequest, null, false, null, false); + LocalDateTime localDateTime = LocalDateTime.of(2025, 12, 15, 0, 0); + assertEquals( + localDateTime.atZone(ZoneOffset.UTC).toInstant().toEpochMilli(), + canMatchContext.getTimeRangeFilterFromMillis().longValue() + ); + } + public void testCanMatchKeywordSortedQueryMatchNoneWithException() throws IOException { SearchRequest searchRequest = new SearchRequest().allowPartialSearchResults(false) .source(new SearchSourceBuilder().sort("field").query(new MatchNoneQueryBuilder())); @@ -370,7 +387,7 @@ public void testIsExecutorQueuedBeyondPrewarmingFactor() throws InterruptedExcep } } - private void doTestCanMatch( + private SearchService.CanMatchContext doTestCanMatch( SearchRequest searchRequest, SortField sortField, boolean expectedCanMatch, @@ -392,7 +409,7 @@ private void doTestCanMatch( IndexShard indexShard = newShard(true); try { recoverShardFromStore(indexShard); - assertTrue(indexDoc(indexShard, "_doc", "id", "{\"field\":\"value\"}").isCreated()); + assertTrue(indexDoc(indexShard, "_doc", "id", "{\"field\":\"value\", \"@timestamp\":\"2025-12-09\"}").isCreated()); assertTrue(indexShard.refresh("test").refreshed()); try (Engine.Searcher searcher = indexShard.acquireSearcher("test")) { SearchExecutionContext searchExecutionContext = createSearchExecutionContext( @@ -416,7 +433,7 @@ private void doTestCanMatch( assertEquals(expectedMinAndMax.getMin(), minAndMax.getMin()); assertEquals(expectedMinAndMax.getMin(), minAndMax.getMax()); } - + return canMatchContext; } } finally { closeShards(indexShard); @@ -443,9 +460,16 @@ private SearchExecutionContext createSearchExecutionContext( Collections.emptyMap() ); KeywordFieldMapper keywordFieldMapper = new KeywordFieldMapper.Builder("field", IndexVersion.current()).build(root); + DateFieldMapper dateFieldMapper = new DateFieldMapper.Builder( + "@timestamp", + DateFieldMapper.Resolution.MILLISECONDS, + null, + ScriptCompiler.NONE, + indexSettings + ).build(root); MappingLookup mappingLookup = MappingLookup.fromMappers( mapping, - Collections.singletonList(keywordFieldMapper), + List.of(keywordFieldMapper, dateFieldMapper), Collections.emptyList() ); return new SearchExecutionContext(