Skip to content

Commit 900e711

Browse files
committed
Ignore date ranges containing 'now' when pre-processing a percolator query (elastic#35160)
Today when a percolator query contains a date range then the query analyzer extracts that range, so that at search time the `percolate` query can exclude percolator queries efficiently that are never going to match. The problem is that if 'now' is used it is evaluated at index time. So the idea is to rewrite date ranges with 'now' to a match all query, so that the query analyzer can't extract it and the `percolate` query is then able to evaluate 'now' at query time.
1 parent e56aceb commit 900e711

File tree

5 files changed

+119
-1
lines changed

5 files changed

+119
-1
lines changed

modules/percolator/src/main/java/org/elasticsearch/percolator/PercolatorFieldMapper.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -423,7 +423,15 @@ public void parse(ParseContext context) throws IOException {
423423

424424
Version indexVersion = context.mapperService().getIndexSettings().getIndexVersionCreated();
425425
createQueryBuilderField(indexVersion, queryBuilderField, queryBuilder, context);
426-
Query query = toQuery(queryShardContext, isMapUnmappedFieldAsText(), queryBuilder);
426+
427+
QueryBuilder queryBuilderForProcessing = queryBuilder.rewrite(new QueryShardContext(queryShardContext) {
428+
429+
@Override
430+
public boolean convertNowRangeToMatchAll() {
431+
return true;
432+
}
433+
});
434+
Query query = toQuery(queryShardContext, isMapUnmappedFieldAsText(), queryBuilderForProcessing);
427435
processQuery(query, context);
428436
}
429437

modules/percolator/src/test/java/org/elasticsearch/percolator/PercolatorQuerySearchTests.java

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
*/
1919
package org.elasticsearch.percolator;
2020

21+
import org.apache.lucene.search.IndexSearcher;
22+
import org.apache.lucene.search.Query;
2123
import org.apache.lucene.search.join.ScoreMode;
2224
import org.elasticsearch.action.search.SearchResponse;
2325
import org.elasticsearch.action.support.WriteRequest;
@@ -28,10 +30,14 @@
2830
import org.elasticsearch.common.xcontent.XContentBuilder;
2931
import org.elasticsearch.common.xcontent.XContentFactory;
3032
import org.elasticsearch.common.xcontent.XContentType;
33+
import org.elasticsearch.index.IndexService;
3134
import org.elasticsearch.index.cache.bitset.BitsetFilterCache;
35+
import org.elasticsearch.index.engine.Engine;
3236
import org.elasticsearch.index.fielddata.ScriptDocValues;
3337
import org.elasticsearch.index.query.Operator;
38+
import org.elasticsearch.index.query.QueryBuilder;
3439
import org.elasticsearch.index.query.QueryBuilders;
40+
import org.elasticsearch.index.query.QueryShardContext;
3541
import org.elasticsearch.plugins.Plugin;
3642
import org.elasticsearch.script.MockScriptPlugin;
3743
import org.elasticsearch.script.Script;
@@ -49,9 +55,14 @@
4955
import java.util.function.Function;
5056

5157
import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder;
58+
import static org.elasticsearch.index.query.QueryBuilders.boolQuery;
5259
import static org.elasticsearch.index.query.QueryBuilders.matchQuery;
60+
import static org.elasticsearch.index.query.QueryBuilders.rangeQuery;
61+
import static org.elasticsearch.index.query.QueryBuilders.scriptQuery;
62+
import static org.elasticsearch.index.query.QueryBuilders.termQuery;
5363
import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertHitCount;
5464
import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertSearchHits;
65+
import static org.hamcrest.Matchers.equalTo;
5566

5667
public class PercolatorQuerySearchTests extends ESSingleNodeTestCase {
5768

@@ -241,4 +252,53 @@ public void testMapUnmappedFieldAsString() throws IOException {
241252
assertSettingDeprecationsAndWarnings(new Setting[]{PercolatorFieldMapper.INDEX_MAP_UNMAPPED_FIELDS_AS_STRING_SETTING});
242253
}
243254

255+
public void testRangeQueriesWithNow() throws Exception {
256+
IndexService indexService = createIndex("test", Settings.builder().put("index.number_of_shards", 1).build(), "_doc",
257+
"field1", "type=keyword", "field2", "type=date", "query", "type=percolator");
258+
259+
client().prepareIndex("test", "_doc", "1")
260+
.setSource(jsonBuilder().startObject().field("query", rangeQuery("field2").from("now-1h").to("now+1h")).endObject())
261+
.get();
262+
client().prepareIndex("test", "_doc", "2")
263+
.setSource(jsonBuilder().startObject().field("query", boolQuery()
264+
.filter(termQuery("field1", "value"))
265+
.filter(rangeQuery("field2").from("now-1h").to("now+1h"))
266+
).endObject())
267+
.get();
268+
269+
270+
Script script = new Script(ScriptType.INLINE, MockScriptPlugin.NAME, "1==1", Collections.emptyMap());
271+
client().prepareIndex("test", "_doc", "3")
272+
.setSource(jsonBuilder().startObject().field("query", boolQuery()
273+
.filter(scriptQuery(script))
274+
.filter(rangeQuery("field2").from("now-1h").to("now+1h"))
275+
).endObject())
276+
.get();
277+
client().admin().indices().prepareRefresh().get();
278+
279+
try (Engine.Searcher engineSearcher = indexService.getShard(0).acquireSearcher("test")) {
280+
IndexSearcher indexSearcher = engineSearcher.searcher();
281+
long[] currentTime = new long[] {System.currentTimeMillis()};
282+
QueryShardContext queryShardContext =
283+
indexService.newQueryShardContext(0, engineSearcher.reader(), () -> currentTime[0], null);
284+
285+
BytesReference source = BytesReference.bytes(jsonBuilder().startObject()
286+
.field("field1", "value")
287+
.field("field2", currentTime[0])
288+
.endObject());
289+
QueryBuilder queryBuilder = new PercolateQueryBuilder("query", source, XContentType.JSON);
290+
Query query = queryBuilder.toQuery(queryShardContext);
291+
assertThat(indexSearcher.count(query), equalTo(3));
292+
293+
currentTime[0] = currentTime[0] + 10800000; // + 3 hours
294+
source = BytesReference.bytes(jsonBuilder().startObject()
295+
.field("field1", "value")
296+
.field("field2", currentTime[0])
297+
.endObject());
298+
queryBuilder = new PercolateQueryBuilder("query", source, XContentType.JSON);
299+
query = queryBuilder.toQuery(queryShardContext);
300+
assertThat(indexSearcher.count(query), equalTo(3));
301+
}
302+
}
303+
244304
}

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,4 +124,15 @@ public void onFailure(Exception e) {
124124
}
125125
}
126126

127+
/**
128+
* In pre-processing contexts that happen at index time 'now' date ranges should be replaced by a {@link MatchAllQueryBuilder}.
129+
* Otherwise documents that should match at query time would never match and the document that have fallen outside the
130+
* date range would continue to match.
131+
*
132+
* @return indicates whether range queries with date ranges using 'now' are rewritten to a {@link MatchAllQueryBuilder}.
133+
*/
134+
public boolean convertNowRangeToMatchAll() {
135+
return false;
136+
}
137+
127138
}

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -464,6 +464,16 @@ protected MappedFieldType.Relation getRelation(QueryRewriteContext queryRewriteC
464464

465465
@Override
466466
protected QueryBuilder doRewrite(QueryRewriteContext queryRewriteContext) throws IOException {
467+
// Percolator queries get rewritten and pre-processed at index time.
468+
// If a range query has a date range using 'now' and 'now' gets resolved at index time then
469+
// the pre-processing uses that to pre-process. This can then lead to mismatches at query time.
470+
if (queryRewriteContext.convertNowRangeToMatchAll()) {
471+
if ((from() != null && from().toString().contains("now")) ||
472+
(to() != null && to().toString().contains("now"))) {
473+
return new MatchAllQueryBuilder();
474+
}
475+
}
476+
467477
final MappedFieldType.Relation relation = getRelation(queryRewriteContext);
468478
switch (relation) {
469479
case DISJOINT:

server/src/test/java/org/elasticsearch/index/query/RangeQueryBuilderTests.java

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -575,4 +575,33 @@ public void testParseRelation() {
575575
builder.relation("intersects");
576576
assertEquals(ShapeRelation.INTERSECTS, builder.relation());
577577
}
578+
579+
public void testConvertNowRangeToMatchAll() throws IOException {
580+
RangeQueryBuilder query = new RangeQueryBuilder(DATE_FIELD_NAME);
581+
DateTime queryFromValue = new DateTime(2019, 1, 1, 0, 0, 0, ISOChronology.getInstanceUTC());
582+
DateTime queryToValue = new DateTime(2020, 1, 1, 0, 0, 0, ISOChronology.getInstanceUTC());
583+
if (randomBoolean()) {
584+
query.from("now");
585+
query.to(queryToValue);
586+
} else if (randomBoolean()) {
587+
query.from(queryFromValue);
588+
query.to("now");
589+
} else {
590+
query.from("now");
591+
query.to("now+1h");
592+
}
593+
QueryShardContext queryShardContext = createShardContext();
594+
QueryBuilder rewritten = query.rewrite(queryShardContext);
595+
assertThat(rewritten, instanceOf(RangeQueryBuilder.class));
596+
597+
queryShardContext = new QueryShardContext(queryShardContext) {
598+
599+
@Override
600+
public boolean convertNowRangeToMatchAll() {
601+
return true;
602+
}
603+
};
604+
rewritten = query.rewrite(queryShardContext);
605+
assertThat(rewritten, instanceOf(MatchAllQueryBuilder.class));
606+
}
578607
}

0 commit comments

Comments
 (0)