Skip to content

Commit 2a95ecb

Browse files
authored
Don't index ranges including NOW in percolator (#52748)
Currently, date ranges queries using NOW-based date math are rewritten to MatchAllDocs queries when being preprocessed for the percolator. However, since we added the verification step, this can result in incorrect matches when percolator queries are run without scores. This commit changes things to instead wrap date queries that use NOW with a new DateRangeIncludingNowQuery. This is a simple wrapper query that returns its delegate at rewrite time, but it can be detected by the percolator QueryAnalyzer and be dealt with accordingly. This also allows us to remove a method on QueryRewriteContext, and push all logic relating to NOW-based ranges into the DateFieldMapper. Fixes #52617
1 parent abeb837 commit 2a95ecb

File tree

10 files changed

+147
-60
lines changed

10 files changed

+147
-60
lines changed

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

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -407,14 +407,7 @@ public void parse(ParseContext context) throws IOException {
407407
Version indexVersion = context.mapperService().getIndexSettings().getIndexVersionCreated();
408408
createQueryBuilderField(indexVersion, queryBuilderField, queryBuilder, context);
409409

410-
QueryBuilder queryBuilderForProcessing = queryBuilder.rewrite(new QueryShardContext(queryShardContext) {
411-
412-
@Override
413-
public boolean convertNowRangeToMatchAll() {
414-
return true;
415-
}
416-
});
417-
Query query = toQuery(queryShardContext, isMapUnmappedFieldAsText(), queryBuilderForProcessing);
410+
Query query = toQuery(queryShardContext, isMapUnmappedFieldAsText(), queryBuilder);
418411
processQuery(query, context);
419412
}
420413

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
import org.apache.lucene.util.NumericUtils;
4141
import org.elasticsearch.Version;
4242
import org.elasticsearch.common.lucene.search.function.FunctionScoreQuery;
43+
import org.elasticsearch.index.query.DateRangeIncludingNowQuery;
4344

4445
import java.util.ArrayList;
4546
import java.util.Arrays;
@@ -149,6 +150,10 @@ Result getResult() {
149150

150151
@Override
151152
public QueryVisitor getSubVisitor(Occur occur, Query parent) {
153+
if (parent instanceof DateRangeIncludingNowQuery) {
154+
terms.add(Result.UNKNOWN);
155+
return QueryVisitor.EMPTY_VISITOR;
156+
}
152157
this.verified = isVerified(parent);
153158
if (occur == Occur.MUST || occur == Occur.FILTER) {
154159
ResultBuilder builder = new ResultBuilder(true);

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -490,6 +490,17 @@ public void testPercolatorFieldMapper() throws Exception {
490490
assertThat(doc.rootDoc().getFields(fieldType.queryBuilderField.name()).length, equalTo(1));
491491
qbSource = doc.rootDoc().getFields(fieldType.queryBuilderField.name())[0].binaryValue();
492492
assertQueryBuilder(qbSource, queryBuilder);
493+
494+
queryBuilder = rangeQuery("date_field").from("now");
495+
doc = mapperService.documentMapper().parse(new SourceToParse("test", "1", BytesReference.bytes(XContentFactory
496+
.jsonBuilder()
497+
.startObject()
498+
.field(fieldName, queryBuilder)
499+
.endObject()),
500+
XContentType.JSON));
501+
assertThat(doc.rootDoc().getFields(fieldType.extractionResultField.name()).length, equalTo(1));
502+
assertThat(doc.rootDoc().getFields(fieldType.extractionResultField.name())[0].stringValue(),
503+
equalTo(EXTRACTION_FAILED));
493504
}
494505

495506
public void testStoringQueries() throws Exception {

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

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
import static org.elasticsearch.common.xcontent.XContentFactory.smileBuilder;
4949
import static org.elasticsearch.common.xcontent.XContentFactory.yamlBuilder;
5050
import static org.elasticsearch.index.query.QueryBuilders.boolQuery;
51+
import static org.elasticsearch.index.query.QueryBuilders.constantScoreQuery;
5152
import static org.elasticsearch.index.query.QueryBuilders.geoBoundingBoxQuery;
5253
import static org.elasticsearch.index.query.QueryBuilders.geoDistanceQuery;
5354
import static org.elasticsearch.index.query.QueryBuilders.geoPolygonQuery;
@@ -930,4 +931,36 @@ public void testDisallowExpensiveQueries() throws IOException {
930931
assertAcked(client().admin().cluster().updateSettings(updateSettingsRequest).actionGet());
931932
}
932933
}
934+
935+
public void testWrappedWithConstantScore() throws Exception {
936+
937+
assertAcked(client().admin().indices().prepareCreate("test")
938+
.setMapping("d", "type=date", "q", "type=percolator")
939+
);
940+
941+
client().prepareIndex("test").setId("1")
942+
.setSource(jsonBuilder().startObject().field("q",
943+
boolQuery().must(rangeQuery("d").gt("now"))
944+
).endObject())
945+
.execute().actionGet();
946+
947+
client().prepareIndex("test").setId("2")
948+
.setSource(jsonBuilder().startObject().field("q",
949+
boolQuery().must(rangeQuery("d").lt("now"))
950+
).endObject())
951+
.execute().actionGet();
952+
953+
client().admin().indices().prepareRefresh().get();
954+
955+
SearchResponse response = client().prepareSearch("test").setQuery(new PercolateQueryBuilder("q",
956+
BytesReference.bytes(jsonBuilder().startObject().field("d", "2020-02-01T15:00:00.000+11:00").endObject()),
957+
XContentType.JSON)).get();
958+
assertEquals(1, response.getHits().getTotalHits().value);
959+
960+
response = client().prepareSearch("test").setQuery(constantScoreQuery(new PercolateQueryBuilder("q",
961+
BytesReference.bytes(jsonBuilder().startObject().field("d", "2020-02-01T15:00:00.000+11:00").endObject()),
962+
XContentType.JSON))).get();
963+
assertEquals(1, response.getHits().getTotalHits().value);
964+
965+
}
933966
}

server/src/main/java/org/elasticsearch/index/mapper/DateFieldMapper.java

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
import org.elasticsearch.index.fielddata.IndexFieldData;
5050
import org.elasticsearch.index.fielddata.IndexNumericFieldData.NumericType;
5151
import org.elasticsearch.index.fielddata.plain.DocValuesIndexFieldData;
52+
import org.elasticsearch.index.query.DateRangeIncludingNowQuery;
5253
import org.elasticsearch.index.query.QueryRewriteContext;
5354
import org.elasticsearch.index.query.QueryShardContext;
5455
import org.elasticsearch.search.DocValueFormat;
@@ -389,19 +390,24 @@ public Query rangeQuery(Object lowerTerm, Object upperTerm, boolean includeLower
389390
DateMathParser parser = forcedDateParser == null
390391
? dateMathParser
391392
: forcedDateParser;
393+
boolean[] nowUsed = new boolean[1];
394+
LongSupplier nowSupplier = () -> {
395+
nowUsed[0] = true;
396+
return context.nowInMillis();
397+
};
392398
long l, u;
393399
if (lowerTerm == null) {
394400
l = Long.MIN_VALUE;
395401
} else {
396-
l = parseToLong(lowerTerm, !includeLower, timeZone, parser, context::nowInMillis);
402+
l = parseToLong(lowerTerm, !includeLower, timeZone, parser, nowSupplier);
397403
if (includeLower == false) {
398404
++l;
399405
}
400406
}
401407
if (upperTerm == null) {
402408
u = Long.MAX_VALUE;
403409
} else {
404-
u = parseToLong(upperTerm, includeUpper, timeZone, parser, context::nowInMillis);
410+
u = parseToLong(upperTerm, includeUpper, timeZone, parser, nowSupplier);
405411
if (includeUpper == false) {
406412
--u;
407413
}
@@ -411,6 +417,9 @@ public Query rangeQuery(Object lowerTerm, Object upperTerm, boolean includeLower
411417
Query dvQuery = SortedNumericDocValuesField.newSlowRangeQuery(name(), l, u);
412418
query = new IndexOrDocValuesQuery(query, dvQuery);
413419
}
420+
if (nowUsed[0]) {
421+
query = new DateRangeIncludingNowQuery(query);
422+
}
414423
return query;
415424
}
416425

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/*
2+
* Licensed to Elasticsearch under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package org.elasticsearch.index.query;
21+
22+
import org.apache.lucene.index.IndexReader;
23+
import org.apache.lucene.search.BooleanClause;
24+
import org.apache.lucene.search.Query;
25+
import org.apache.lucene.search.QueryVisitor;
26+
27+
import java.io.IOException;
28+
import java.util.Objects;
29+
30+
/**
31+
* A simple wrapper class that indicates that the wrapped query has made use of NOW
32+
* when parsing its datemath. Useful for preprocessors such as the percolator that
33+
* need to know when not to extract dates from the query.
34+
*/
35+
public class DateRangeIncludingNowQuery extends Query {
36+
37+
private final Query in;
38+
39+
public DateRangeIncludingNowQuery(Query in) {
40+
this.in = in;
41+
}
42+
43+
public Query getQuery() {
44+
return in;
45+
}
46+
47+
@Override
48+
public Query rewrite(IndexReader reader) throws IOException {
49+
return in;
50+
}
51+
52+
@Override
53+
public String toString(String field) {
54+
return "DateRangeIncludingNowQuery(" + in + ")";
55+
}
56+
57+
@Override
58+
public void visit(QueryVisitor visitor) {
59+
in.visit(visitor.getSubVisitor(BooleanClause.Occur.MUST, this));
60+
}
61+
62+
@Override
63+
public boolean equals(Object o) {
64+
if (this == o) return true;
65+
if (o == null || getClass() != o.getClass()) return false;
66+
DateRangeIncludingNowQuery that = (DateRangeIncludingNowQuery) o;
67+
return Objects.equals(in, that.in);
68+
}
69+
70+
@Override
71+
public int hashCode() {
72+
return Objects.hash(in);
73+
}
74+
}

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

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

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

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

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

452452
@Override
453453
protected QueryBuilder doRewrite(QueryRewriteContext queryRewriteContext) throws IOException {
454-
// Percolator queries get rewritten and pre-processed at index time.
455-
// If a range query has a date range using 'now' and 'now' gets resolved at index time then
456-
// the pre-processing uses that to pre-process. This can then lead to mismatches at query time.
457-
if (queryRewriteContext.convertNowRangeToMatchAll()) {
458-
if ((from() != null && from().toString().contains("now")) ||
459-
(to() != null && to().toString().contains("now"))) {
460-
return new MatchAllQueryBuilder();
461-
}
462-
}
463-
464454
final MappedFieldType.Relation relation = getRelation(queryRewriteContext);
465455
switch (relation) {
466456
case DISJOINT:

server/src/test/java/org/elasticsearch/index/mapper/DateFieldTypeTests.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
import org.elasticsearch.index.mapper.DateFieldMapper.Resolution;
4949
import org.elasticsearch.index.mapper.MappedFieldType.Relation;
5050
import org.elasticsearch.index.mapper.ParseContext.Document;
51+
import org.elasticsearch.index.query.DateRangeIncludingNowQuery;
5152
import org.elasticsearch.index.query.QueryRewriteContext;
5253
import org.elasticsearch.index.query.QueryShardContext;
5354
import org.joda.time.DateTimeZone;
@@ -269,6 +270,15 @@ BigArrays.NON_RECYCLING_INSTANCE, null, null, null, null, null, xContentRegistry
269270
assertEquals(expected,
270271
ft.rangeQuery(date1, date2, true, true, null, null, null, context).rewrite(new MultiReader()));
271272

273+
instant1 = nowInMillis;
274+
instant2 = instant1 + 100;
275+
expected = new DateRangeIncludingNowQuery(new IndexOrDocValuesQuery(
276+
LongPoint.newRangeQuery("field", instant1, instant2),
277+
SortedNumericDocValuesField.newSlowRangeQuery("field", instant1, instant2)
278+
));
279+
assertEquals(expected,
280+
ft.rangeQuery("now", instant2, true, true, null, null, null, context));
281+
272282
ft.setIndexOptions(IndexOptions.NONE);
273283
IllegalArgumentException e = expectThrows(IllegalArgumentException.class,
274284
() -> ft.rangeQuery(date1, date2, true, true, null, null, null, context));

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

Lines changed: 2 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,8 @@ public void testDateRangeQueryTimezone() throws IOException {
343343
"}";
344344
QueryShardContext context = createShardContext();
345345
Query parsedQuery = parseQuery(query).toQuery(context);
346+
assertThat(parsedQuery, instanceOf(DateRangeIncludingNowQuery.class));
347+
parsedQuery = ((DateRangeIncludingNowQuery)parsedQuery).getQuery();
346348
assertThat(parsedQuery, instanceOf(IndexOrDocValuesQuery.class));
347349
parsedQuery = ((IndexOrDocValuesQuery) parsedQuery).getIndexQuery();
348350
assertThat(parsedQuery, instanceOf(PointRangeQuery.class));
@@ -565,35 +567,6 @@ public void testParseRelation() {
565567
assertEquals(ShapeRelation.INTERSECTS, builder.relation());
566568
}
567569

568-
public void testConvertNowRangeToMatchAll() throws IOException {
569-
RangeQueryBuilder query = new RangeQueryBuilder(DATE_FIELD_NAME);
570-
DateTime queryFromValue = new DateTime(2019, 1, 1, 0, 0, 0, ISOChronology.getInstanceUTC());
571-
DateTime queryToValue = new DateTime(2020, 1, 1, 0, 0, 0, ISOChronology.getInstanceUTC());
572-
if (randomBoolean()) {
573-
query.from("now");
574-
query.to(queryToValue);
575-
} else if (randomBoolean()) {
576-
query.from(queryFromValue);
577-
query.to("now");
578-
} else {
579-
query.from("now");
580-
query.to("now+1h");
581-
}
582-
QueryShardContext queryShardContext = createShardContext();
583-
QueryBuilder rewritten = query.rewrite(queryShardContext);
584-
assertThat(rewritten, instanceOf(RangeQueryBuilder.class));
585-
586-
queryShardContext = new QueryShardContext(queryShardContext) {
587-
588-
@Override
589-
public boolean convertNowRangeToMatchAll() {
590-
return true;
591-
}
592-
};
593-
rewritten = query.rewrite(queryShardContext);
594-
assertThat(rewritten, instanceOf(MatchAllQueryBuilder.class));
595-
}
596-
597570
public void testTypeField() throws IOException {
598571
RangeQueryBuilder builder = QueryBuilders.rangeQuery("_type")
599572
.from("value1");

0 commit comments

Comments
 (0)