Skip to content

Commit a4e9f64

Browse files
committed
Refactoring of MissingQuery
Relates to #10217 Closes #12030 This PR is against the query-refactoring branch.
1 parent 93674b8 commit a4e9f64

File tree

4 files changed

+372
-129
lines changed

4 files changed

+372
-129
lines changed

core/src/main/java/org/apache/lucene/queryparser/classic/MissingFieldQueryExtension.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
import org.apache.lucene.search.ConstantScoreQuery;
2323
import org.apache.lucene.search.Query;
24+
import org.elasticsearch.index.query.MissingQueryBuilder;
2425
import org.elasticsearch.index.query.MissingQueryParser;
2526
import org.elasticsearch.index.query.QueryParseContext;
2627

@@ -33,7 +34,7 @@ public class MissingFieldQueryExtension implements FieldQueryExtension {
3334

3435
@Override
3536
public Query query(QueryParseContext parseContext, String queryText) {
36-
Query query = MissingQueryParser.newFilter(parseContext, queryText, MissingQueryParser.DEFAULT_EXISTENCE_VALUE, MissingQueryParser.DEFAULT_NULL_VALUE);
37+
Query query = MissingQueryBuilder.newFilter(parseContext, queryText, MissingQueryBuilder.DEFAULT_EXISTENCE_VALUE, MissingQueryBuilder.DEFAULT_NULL_VALUE);
3738
if (query != null) {
3839
return new ConstantScoreQuery(query);
3940
}

core/src/main/java/org/elasticsearch/index/query/MissingQueryBuilder.java

Lines changed: 177 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -19,29 +19,47 @@
1919

2020
package org.elasticsearch.index.query;
2121

22+
import org.apache.lucene.search.*;
23+
import org.elasticsearch.common.Strings;
24+
import org.elasticsearch.common.io.stream.StreamInput;
25+
import org.elasticsearch.common.io.stream.StreamOutput;
26+
import org.elasticsearch.common.lucene.search.Queries;
2227
import org.elasticsearch.common.xcontent.XContentBuilder;
28+
import org.elasticsearch.index.mapper.MappedFieldType;
29+
import org.elasticsearch.index.mapper.internal.FieldNamesFieldMapper;
30+
import org.elasticsearch.index.mapper.object.ObjectMapper;
2331

2432
import java.io.IOException;
33+
import java.util.Collection;
34+
import java.util.Objects;
2535

2636
/**
27-
* Constructs a filter that only match on documents that the field has a value in them.
37+
* Constructs a filter that have only null values or no value in the original field.
2838
*/
2939
public class MissingQueryBuilder extends AbstractQueryBuilder<MissingQueryBuilder> {
3040

3141
public static final String NAME = "missing";
3242

33-
private String name;
43+
public static final boolean DEFAULT_NULL_VALUE = false;
3444

35-
private Boolean nullValue;
45+
public static final boolean DEFAULT_EXISTENCE_VALUE = true;
3646

37-
private Boolean existence;
47+
private final String fieldPattern;
48+
49+
private boolean nullValue = DEFAULT_NULL_VALUE;
50+
51+
private boolean existence = DEFAULT_EXISTENCE_VALUE;
3852

3953
static final MissingQueryBuilder PROTOTYPE = new MissingQueryBuilder(null);
4054

41-
public MissingQueryBuilder(String name) {
42-
this.name = name;
55+
public MissingQueryBuilder(String fieldPattern) {
56+
this.fieldPattern = fieldPattern;
4357
}
44-
58+
59+
public String fieldPattern() {
60+
return this.fieldPattern;
61+
}
62+
4563
/**
4664
* Should the missing filter automatically include fields with null value configured in the
4765
* mappings. Defaults to <tt>false</tt>.
@@ -52,24 +70,36 @@ public MissingQueryBuilder nullValue(boolean nullValue) {
5270
}
5371

5472
/**
55-
* Should the missing filter include documents where the field doesn't exists in the docs.
73+
* Returns true if the missing filter will include documents where the field contains a null value, otherwise
74+
* these documents will not be included.
75+
*/
76+
public boolean nullValue() {
77+
return this.nullValue;
78+
}
79+
80+
/**
81+
* Should the missing filter include documents where the field doesn't exist in the docs.
5682
* Defaults to <tt>true</tt>.
5783
*/
5884
public MissingQueryBuilder existence(boolean existence) {
5985
this.existence = existence;
6086
return this;
6187
}
6288

89+
/**
90+
* Returns true if the missing filter will include documents where the field has no values, otherwise
91+
* these documents will not be included.
92+
*/
93+
public boolean existence() {
94+
return this.existence;
95+
}
96+
6397
@Override
6498
protected void doXContent(XContentBuilder builder, Params params) throws IOException {
6599
builder.startObject(NAME);
66-
builder.field("field", name);
67-
if (nullValue != null) {
68-
builder.field("null_value", nullValue);
69-
}
70-
if (existence != null) {
71-
builder.field("existence", existence);
72-
}
100+
builder.field("field", fieldPattern);
101+
builder.field("null_value", nullValue);
102+
builder.field("existence", existence);
73103
printBoostAndQueryName(builder);
74104
builder.endObject();
75105
}
@@ -78,4 +108,136 @@ protected void doXContent(XContentBuilder builder, Params params) throws IOExcep
78108
public String getName() {
79109
return NAME;
80110
}
111+
112+
@Override
113+
protected Query doToQuery(QueryParseContext parseContext) throws IOException {
114+
return newFilter(parseContext, fieldPattern, existence, nullValue);
115+
}
116+
117+
public static Query newFilter(QueryParseContext parseContext, String fieldPattern, boolean existence, boolean nullValue) {
118+
if (!existence && !nullValue) {
119+
throw new QueryParsingException(parseContext, "missing must have either existence, or null_value, or both set to true");
120+
}
121+
122+
final FieldNamesFieldMapper.FieldNamesFieldType fieldNamesFieldType = (FieldNamesFieldMapper.FieldNamesFieldType) parseContext.mapperService().fullName(FieldNamesFieldMapper.NAME);
123+
if (fieldNamesFieldType == null) {
124+
// can only happen when no types exist, so no docs exist either
125+
return Queries.newMatchNoDocsQuery();
126+
}
127+
128+
ObjectMapper objectMapper = parseContext.getObjectMapper(fieldPattern);
129+
if (objectMapper != null) {
130+
// automatic make the object mapper pattern
131+
fieldPattern = fieldPattern + ".*";
132+
}
133+
134+
Collection<String> fields = parseContext.simpleMatchToIndexNames(fieldPattern);
135+
if (fields.isEmpty()) {
136+
if (existence) {
137+
// if we ask for existence of fields, and we found none, then we should match on all
138+
return Queries.newMatchAllQuery();
139+
}
140+
return null;
141+
}
142+
143+
Query existenceFilter = null;
144+
Query nullFilter = null;
145+
146+
if (existence) {
147+
BooleanQuery boolFilter = new BooleanQuery();
148+
for (String field : fields) {
149+
MappedFieldType fieldType = parseContext.fieldMapper(field);
150+
Query filter = null;
151+
if (fieldNamesFieldType.isEnabled()) {
152+
final String f;
153+
if (fieldType != null) {
154+
f = fieldType.names().indexName();
155+
} else {
156+
f = field;
157+
}
158+
filter = fieldNamesFieldType.termQuery(f, parseContext);
159+
}
160+
// if _field_names are not indexed, we need to go the slow way
161+
if (filter == null && fieldType != null) {
162+
filter = fieldType.rangeQuery(null, null, true, true, parseContext);
163+
}
164+
if (filter == null) {
165+
filter = new TermRangeQuery(field, null, null, true, true);
166+
}
167+
boolFilter.add(filter, BooleanClause.Occur.SHOULD);
168+
}
169+
170+
existenceFilter = boolFilter;
171+
existenceFilter = Queries.not(existenceFilter);;
172+
}
173+
174+
if (nullValue) {
175+
for (String field : fields) {
176+
MappedFieldType fieldType = parseContext.fieldMapper(field);
177+
if (fieldType != null) {
178+
nullFilter = fieldType.nullValueQuery();
179+
}
180+
}
181+
}
182+
183+
Query filter;
184+
if (nullFilter != null) {
185+
if (existenceFilter != null) {
186+
BooleanQuery combined = new BooleanQuery();
187+
combined.add(existenceFilter, BooleanClause.Occur.SHOULD);
188+
combined.add(nullFilter, BooleanClause.Occur.SHOULD);
189+
// cache the not filter as well, so it will be faster
190+
filter = combined;
191+
} else {
192+
filter = nullFilter;
193+
}
194+
} else {
195+
filter = existenceFilter;
196+
}
197+
198+
if (filter == null) {
199+
return null;
200+
}
201+
202+
return new ConstantScoreQuery(filter);
203+
}
204+
205+
@Override
206+
public QueryValidationException validate() {
207+
QueryValidationException validationException = null;
208+
if (Strings.isEmpty(this.fieldPattern)) {
209+
validationException = addValidationError("missing must be provided with a [field]", validationException);
210+
}
211+
if (!existence && !nullValue) {
212+
validationException = addValidationError("missing must have either existence, or null_value, or both set to true", validationException);
213+
}
214+
return validationException;
215+
}
216+
217+
@Override
218+
protected MissingQueryBuilder doReadFrom(StreamInput in) throws IOException {
219+
MissingQueryBuilder missingQueryBuilder = new MissingQueryBuilder(in.readString());
220+
missingQueryBuilder.nullValue = in.readBoolean();
221+
missingQueryBuilder.existence = in.readBoolean();
222+
return missingQueryBuilder;
223+
}
224+
225+
@Override
226+
protected void doWriteTo(StreamOutput out) throws IOException {
227+
out.writeString(fieldPattern);
228+
out.writeBoolean(nullValue);
229+
out.writeBoolean(existence);
230+
}
231+
232+
@Override
233+
protected int doHashCode() {
234+
return Objects.hash(fieldPattern, nullValue, existence);
235+
}
236+
237+
@Override
238+
protected boolean doEquals(MissingQueryBuilder other) {
239+
return Objects.equals(fieldPattern, other.fieldPattern) &&
240+
Objects.equals(nullValue, other.nullValue) &&
241+
Objects.equals(existence, other.existence);
242+
}
81243
}

0 commit comments

Comments
 (0)