Split NestedQueryParser into toQuery and formXContent

This commit splits NestedQueryParser into toQuery and fromXContent.

Relates to #10217
This commit is contained in:
Simon Willnauer 2015-09-09 14:51:52 +02:00
parent 1dc985a2c4
commit 8d2a2f8d66
8 changed files with 341 additions and 103 deletions

View File

@ -19,43 +19,69 @@
package org.elasticsearch.index.query;
import org.apache.lucene.search.Filter;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.join.BitSetProducer;
import org.apache.lucene.search.join.ScoreMode;
import org.apache.lucene.search.join.ToParentBlockJoinQuery;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
import org.elasticsearch.common.lucene.search.Queries;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.index.mapper.object.ObjectMapper;
import org.elasticsearch.index.query.support.QueryInnerHits;
import org.elasticsearch.search.fetch.innerhits.InnerHitsContext;
import org.elasticsearch.search.fetch.innerhits.InnerHitsSubSearchContext;
import java.io.IOException;
import java.util.Locale;
import java.util.Objects;
public class NestedQueryBuilder extends AbstractQueryBuilder<NestedQueryBuilder> {
/**
* The default score move for nested queries.
*/
public static final ScoreMode DEFAULT_SCORE_MODE = ScoreMode.Avg;
/**
* The queries name used while parsing
*/
public static final String NAME = "nested";
private final QueryBuilder queryBuilder;
private final QueryBuilder query;
private final String path;
private String scoreMode;
private ScoreMode scoreMode = DEFAULT_SCORE_MODE;
private QueryInnerHits innerHit;
private QueryInnerHits queryInnerHits;
static final NestedQueryBuilder PROTOTYPE = new NestedQueryBuilder();
public NestedQueryBuilder(String path, QueryBuilder queryBuilder) {
public NestedQueryBuilder(String path, QueryBuilder query) {
if (path == null) {
throw new IllegalArgumentException("[" + NAME + "] requires 'path' field");
}
if (query == null) {
throw new IllegalArgumentException("[" + NAME + "] requires 'query' field");
}
this.path = path;
this.queryBuilder = Objects.requireNonNull(queryBuilder);
this.query = query;
}
public NestedQueryBuilder(String path, QueryBuilder query, ScoreMode scoreMode, QueryInnerHits queryInnerHits) {
this(path, query);
scoreMode(scoreMode);
this.queryInnerHits = queryInnerHits;
}
/**
* private constructor only used internally
* The score mode how the scores from the matching child documents are mapped into the nested parent document.
*/
private NestedQueryBuilder() {
this.path = null;
this.queryBuilder = null;
}
/**
* The score mode.
*/
public NestedQueryBuilder scoreMode(String scoreMode) {
public NestedQueryBuilder scoreMode(ScoreMode scoreMode) {
if (scoreMode == null) {
throw new IllegalArgumentException("[" + NAME + "] requires 'score_mode' field");
}
this.scoreMode = scoreMode;
return this;
}
@ -64,22 +90,43 @@ public class NestedQueryBuilder extends AbstractQueryBuilder<NestedQueryBuilder>
* Sets inner hit definition in the scope of this nested query and reusing the defined path and query.
*/
public NestedQueryBuilder innerHit(QueryInnerHits innerHit) {
this.innerHit = innerHit;
this.queryInnerHits = innerHit;
return this;
}
/**
* Returns the nested query to execute.
*/
public QueryBuilder query() {
return query;
}
/**
* Returns inner hit definition in the scope of this query and reusing the defined type and query.
*/
public QueryInnerHits innerHit() {
return queryInnerHits;
}
/**
* Returns how the scores from the matching child documents are mapped into the nested parent document.
*/
public ScoreMode scoreMode() {
return scoreMode;
}
@Override
protected void doXContent(XContentBuilder builder, Params params) throws IOException {
builder.startObject(NAME);
builder.field("query");
queryBuilder.toXContent(builder, params);
query.toXContent(builder, params);
builder.field("path", path);
if (scoreMode != null) {
builder.field("score_mode", scoreMode);
builder.field("score_mode", scoreMode.name().toLowerCase(Locale.ROOT));
}
printBoostAndQueryName(builder);
if (innerHit != null) {
innerHit.toXContent(builder, params);
if (queryInnerHits != null) {
queryInnerHits.toXContent(builder, params);
}
builder.endObject();
}
@ -88,4 +135,95 @@ public class NestedQueryBuilder extends AbstractQueryBuilder<NestedQueryBuilder>
public final String getWriteableName() {
return NAME;
}
@Override
protected boolean doEquals(NestedQueryBuilder that) {
return Objects.equals(query, that.query)
&& Objects.equals(path, that.path)
&& Objects.equals(scoreMode, that.scoreMode)
&& Objects.equals(queryInnerHits, that.queryInnerHits);
}
@Override
protected int doHashCode() {
return Objects.hash(query, path, scoreMode, queryInnerHits);
}
private NestedQueryBuilder(StreamInput in) throws IOException {
path = in.readString();
final int ordinal = in.readVInt();
scoreMode = ScoreMode.values()[ordinal];
query = in.readQuery();
if (in.readBoolean()) {
queryInnerHits = new QueryInnerHits(in);
}
}
@Override
protected void doWriteTo(StreamOutput out) throws IOException {
out.writeString(path);
out.writeVInt(scoreMode.ordinal());
out.writeQuery(query);
if (queryInnerHits != null) {
out.writeBoolean(true);
queryInnerHits.writeTo(out);
} else {
out.writeBoolean(false);
}
}
@Override
protected NestedQueryBuilder doReadFrom(StreamInput in) throws IOException {
return new NestedQueryBuilder(in);
}
@Override
protected Query doToQuery(QueryShardContext context) throws IOException {
ObjectMapper nestedObjectMapper = context.getObjectMapper(path);
if (nestedObjectMapper == null) {
throw new IllegalStateException("[" + NAME + "] failed to find nested object under path [" + path + "]");
}
if (!nestedObjectMapper.nested().isNested()) {
throw new IllegalStateException("[" + NAME + "] nested object under path [" + path + "] is not of nested type");
}
final BitSetProducer parentFilter;
final Filter childFilter;
final ObjectMapper parentObjectMapper;
final Query innerQuery;
ObjectMapper objectMapper = context.nestedScope().getObjectMapper();
try {
if (objectMapper == null) {
parentFilter = context.bitsetFilter(Queries.newNonNestedFilter());
} else {
parentFilter = context.bitsetFilter(objectMapper.nestedTypeFilter());
}
childFilter = nestedObjectMapper.nestedTypeFilter();
parentObjectMapper = context.nestedScope().nextLevel(nestedObjectMapper);
innerQuery = this.query.toQuery(context);
if (innerQuery == null) {
return null;
}
} finally {
context.nestedScope().previousLevel();
}
if (queryInnerHits != null) {
try (XContentParser parser = queryInnerHits.getXcontentParser()) {
XContentParser.Token token = parser.nextToken();
if (token != XContentParser.Token.START_OBJECT) {
throw new IllegalStateException("start object expected but was: [" + token + "]");
}
InnerHitsSubSearchContext innerHits = context.indexQueryParserService().getInnerHitsQueryParserHelper().parse(parser);
if (innerHits != null) {
ParsedQuery parsedQuery = new ParsedQuery(innerQuery, context.copyNamedQueries());
InnerHitsContext.NestedInnerHits nestedInnerHits = new InnerHitsContext.NestedInnerHits(innerHits.getSubSearchContext(), parsedQuery, null, parentObjectMapper, nestedObjectMapper);
String name = innerHits.getName() != null ? innerHits.getName() : path;
context.addInnerHits(name, nestedInnerHits);
}
}
}
return new ToParentBlockJoinQuery(Queries.filtered(innerQuery, childFilter), parentFilter, scoreMode);
}
}

View File

@ -31,21 +31,17 @@ import org.elasticsearch.common.lucene.search.Queries;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.index.query.support.InnerHitsQueryParserHelper;
import org.elasticsearch.index.query.support.NestedInnerQueryParseSupport;
import org.elasticsearch.index.query.support.QueryInnerHits;
import org.elasticsearch.search.fetch.innerhits.InnerHitsContext;
import org.elasticsearch.search.fetch.innerhits.InnerHitsSubSearchContext;
import java.io.IOException;
public class NestedQueryParser extends BaseQueryParserTemp {
public class NestedQueryParser extends BaseQueryParser<NestedQueryBuilder> {
private static final ParseField FILTER_FIELD = new ParseField("filter").withAllDeprecated("query");
private static final NestedQueryBuilder PROTOTYPE = new NestedQueryBuilder("", EmptyQueryBuilder.PROTOTYPE);
private final InnerHitsQueryParserHelper innerHitsQueryParserHelper;
@Inject
public NestedQueryParser(InnerHitsQueryParserHelper innerHitsQueryParserHelper) {
this.innerHitsQueryParserHelper = innerHitsQueryParserHelper;
}
@Override
public String[] names() {
@ -53,33 +49,32 @@ public class NestedQueryParser extends BaseQueryParserTemp {
}
@Override
public Query parse(QueryShardContext context) throws IOException, QueryParsingException {
QueryParseContext parseContext = context.parseContext();
public NestedQueryBuilder fromXContent(QueryParseContext parseContext) throws IOException, QueryParsingException {
XContentParser parser = parseContext.parser();
final ToBlockJoinQueryBuilder builder = new ToBlockJoinQueryBuilder(context);
float boost = AbstractQueryBuilder.DEFAULT_BOOST;
ScoreMode scoreMode = ScoreMode.Avg;
ScoreMode scoreMode = NestedQueryBuilder.DEFAULT_SCORE_MODE;
String queryName = null;
QueryBuilder query = null;
String path = null;
String currentFieldName = null;
QueryInnerHits queryInnerHits = null;
XContentParser.Token token;
while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
if (token == XContentParser.Token.FIELD_NAME) {
currentFieldName = parser.currentName();
} else if (token == XContentParser.Token.START_OBJECT) {
if ("query".equals(currentFieldName)) {
builder.query();
query = parseContext.parseInnerQueryBuilder();
} else if (parseContext.parseFieldMatcher().match(currentFieldName, FILTER_FIELD)) {
builder.filter();
query = parseContext.parseInnerFilterToQueryBuilder();
} else if ("inner_hits".equals(currentFieldName)) {
builder.setInnerHits(innerHitsQueryParserHelper.parse(parser));
queryInnerHits = new QueryInnerHits(parser);
} else {
throw new QueryParsingException(parseContext, "[nested] query does not support [" + currentFieldName + "]");
}
} else if (token.isValue()) {
if ("path".equals(currentFieldName)) {
builder.setPath(parser.text());
path = parser.text();
} else if ("boost".equals(currentFieldName)) {
boost = parser.floatValue();
} else if ("score_mode".equals(currentFieldName) || "scoreMode".equals(currentFieldName)) {
@ -104,69 +99,11 @@ public class NestedQueryParser extends BaseQueryParserTemp {
}
}
}
builder.setScoreMode(scoreMode);
ToParentBlockJoinQuery joinQuery = builder.build();
if (joinQuery != null) {
joinQuery.setBoost(boost);
if (queryName != null) {
context.addNamedQuery(queryName, joinQuery);
}
}
return joinQuery;
}
public static class ToBlockJoinQueryBuilder extends NestedInnerQueryParseSupport {
private ScoreMode scoreMode;
private InnerHitsSubSearchContext innerHits;
public ToBlockJoinQueryBuilder(QueryShardContext context) throws IOException {
super(context);
}
public void setScoreMode(ScoreMode scoreMode) {
this.scoreMode = scoreMode;
}
public void setInnerHits(InnerHitsSubSearchContext innerHits) {
this.innerHits = innerHits;
}
@Nullable
public ToParentBlockJoinQuery build() throws IOException {
Query innerQuery;
if (queryFound) {
innerQuery = getInnerQuery();
} else if (filterFound) {
Query innerFilter = getInnerFilter();
if (innerFilter != null) {
innerQuery = new ConstantScoreQuery(getInnerFilter());
} else {
innerQuery = null;
}
} else {
throw new QueryShardException(shardContext, "[nested] requires either 'query' or 'filter' field");
}
if (innerHits != null) {
ParsedQuery parsedQuery = new ParsedQuery(innerQuery, shardContext.copyNamedQueries());
InnerHitsContext.NestedInnerHits nestedInnerHits = new InnerHitsContext.NestedInnerHits(innerHits.getSubSearchContext(), parsedQuery, null, getParentObjectMapper(), nestedObjectMapper);
String name = innerHits.getName() != null ? innerHits.getName() : path;
shardContext.addInnerHits(name, nestedInnerHits);
}
if (innerQuery != null) {
return new ToParentBlockJoinQuery(Queries.filtered(innerQuery, childFilter), parentFilter, scoreMode);
} else {
return null;
}
}
return new NestedQueryBuilder(path, query, scoreMode, queryInnerHits).queryName(queryName).boost(boost);
}
@Override
public NestedQueryBuilder getBuilderPrototype() {
return NestedQueryBuilder.PROTOTYPE;
return PROTOTYPE;
}
}

View File

@ -115,7 +115,7 @@ public class QueryShardContext {
return parseFieldMatcher;
}
private void reset() {
public void reset() {
allowUnmappedFields = indexQueryParser.defaultAllowUnmappedFields();
this.parseFieldMatcher = ParseFieldMatcher.EMPTY;
this.lookup = null;

View File

@ -448,6 +448,7 @@ public abstract class AbstractQueryTestCase<QB extends AbstractQueryBuilder<QB>>
*/
protected static QueryShardContext createShardContext() {
QueryShardContext queryCreationContext = new QueryShardContext(index, queryParserService);
queryCreationContext.reset();
queryCreationContext.parseFieldMatcher(ParseFieldMatcher.EMPTY);
return queryCreationContext;
}

View File

@ -0,0 +1,159 @@
/*
* Licensed to Elasticsearch under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.elasticsearch.index.query;
import com.carrotsearch.randomizedtesting.generators.RandomPicks;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.join.ScoreMode;
import org.apache.lucene.search.join.ToParentBlockJoinQuery;
import org.elasticsearch.action.admin.indices.mapping.put.PutMappingRequest;
import org.elasticsearch.common.ParseFieldMatcher;
import org.elasticsearch.common.compress.CompressedXContent;
import org.elasticsearch.common.xcontent.*;
import org.elasticsearch.index.fielddata.IndexFieldDataService;
import org.elasticsearch.index.mapper.MapperService;
import org.elasticsearch.index.query.support.QueryInnerHits;
import org.elasticsearch.search.fetch.innerhits.InnerHitsBuilder;
import org.elasticsearch.search.fetch.innerhits.InnerHitsContext;
import org.elasticsearch.search.internal.SearchContext;
import org.elasticsearch.search.sort.SortOrder;
import org.elasticsearch.test.TestSearchContext;
import java.io.IOException;
import java.util.Arrays;
import static org.hamcrest.CoreMatchers.instanceOf;
public class NestedQueryBuilderTests extends AbstractQueryTestCase<NestedQueryBuilder> {
public void setUp() throws Exception {
super.setUp();
MapperService mapperService = queryParserService().mapperService;
mapperService.merge("nested_doc", new CompressedXContent(PutMappingRequest.buildFromSimplifiedDef("nested_doc",
STRING_FIELD_NAME, "type=string",
INT_FIELD_NAME, "type=integer",
DOUBLE_FIELD_NAME, "type=double",
BOOLEAN_FIELD_NAME, "type=boolean",
DATE_FIELD_NAME, "type=date",
OBJECT_FIELD_NAME, "type=object",
"nested1", "type=nested"
).string()), false, false);
}
protected void setSearchContext(String[] types) {
final MapperService mapperService = queryParserService().mapperService;
final IndexFieldDataService fieldData = queryParserService().fieldDataService;
TestSearchContext testSearchContext = new TestSearchContext() {
private InnerHitsContext context;
@Override
public void innerHits(InnerHitsContext innerHitsContext) {
context = innerHitsContext;
}
@Override
public InnerHitsContext innerHits() {
return context;
}
@Override
public MapperService mapperService() {
return mapperService; // need to build / parse inner hits sort fields
}
@Override
public IndexFieldDataService fieldData() {
return fieldData; // need to build / parse inner hits sort fields
}
};
testSearchContext.setTypes(types);
SearchContext.setCurrent(testSearchContext);
}
/**
* @return a {@link HasChildQueryBuilder} with random values all over the place
*/
@Override
protected NestedQueryBuilder doCreateTestQueryBuilder() {
InnerHitsBuilder.InnerHit innerHit = new InnerHitsBuilder.InnerHit().setSize(100).addSort(STRING_FIELD_NAME, SortOrder.ASC);
return new NestedQueryBuilder("nested1", RandomQueryBuilder.createQuery(random()),
RandomPicks.randomFrom(random(), ScoreMode.values()),
SearchContext.current() == null ? null : new QueryInnerHits("inner_hits_name", innerHit));
}
@Override
protected void doAssertLuceneQuery(NestedQueryBuilder queryBuilder, Query query, QueryShardContext context) throws IOException {
QueryBuilder innerQueryBuilder = queryBuilder.query();
if (innerQueryBuilder instanceof EmptyQueryBuilder) {
assertNull(query);
} else {
assertThat(query, instanceOf(ToParentBlockJoinQuery.class));
ToParentBlockJoinQuery parentBlockJoinQuery = (ToParentBlockJoinQuery) query;
//TODO how to assert this?
}
if (queryBuilder.innerHit() != null) {
assertNotNull(SearchContext.current());
if (query != null) {
assertNotNull(SearchContext.current().innerHits());
assertEquals(1, SearchContext.current().innerHits().getInnerHits().size());
assertTrue(SearchContext.current().innerHits().getInnerHits().containsKey("inner_hits_name"));
InnerHitsContext.BaseInnerHits innerHits = SearchContext.current().innerHits().getInnerHits().get("inner_hits_name");
assertEquals(innerHits.size(), 100);
assertEquals(innerHits.sort().getSort().length, 1);
assertEquals(innerHits.sort().getSort()[0].getField(), STRING_FIELD_NAME);
} else {
assertNull(SearchContext.current().innerHits());
}
}
}
public void testParseDeprecatedFilter() throws IOException {
XContentBuilder builder = XContentFactory.jsonBuilder().prettyPrint();
builder.startObject();
builder.startObject("nested");
builder.startObject("filter");
builder.startObject("terms").array(STRING_FIELD_NAME, "a", "b").endObject();// deprecated
builder.endObject();
builder.field("path", "foo.bar");
builder.endObject();
builder.endObject();
QueryShardContext shardContext = createShardContext();
QueryParseContext context = shardContext.parseContext();
XContentParser parser = XContentFactory.xContent(XContentType.JSON).createParser(builder.string());
context.reset(parser);
context.parseFieldMatcher(ParseFieldMatcher.STRICT);
try {
context.parseInnerQueryBuilder();
fail("filter is deprecated");
} catch (IllegalArgumentException ex) {
assertEquals("Deprecated field [filter] used, replaced by [query]", ex.getMessage());
}
parser = XContentFactory.xContent(XContentType.JSON).createParser(builder.string());
context.reset(parser);
NestedQueryBuilder queryBuilder = (NestedQueryBuilder) context.parseInnerQueryBuilder();
QueryBuilder query = queryBuilder.query();
assertTrue(query instanceof TermsQueryBuilder);
TermsQueryBuilder tqb = (TermsQueryBuilder) query;
assertEquals(tqb.values(), Arrays.asList("a", "b"));
}
}

View File

@ -20,6 +20,7 @@
package org.elasticsearch.nested;
import org.apache.lucene.search.Explanation;
import org.apache.lucene.search.join.ScoreMode;
import org.elasticsearch.action.admin.cluster.health.ClusterHealthStatus;
import org.elasticsearch.action.admin.cluster.stats.ClusterStatsResponse;
import org.elasticsearch.action.admin.indices.stats.IndicesStatsResponse;
@ -311,7 +312,7 @@ public class SimpleNestedIT extends ESIntegTestCase {
.execute().actionGet();
SearchResponse searchResponse = client().prepareSearch("test")
.setQuery(nestedQuery("nested1", termQuery("nested1.n_field1", "n_value1")).scoreMode("total"))
.setQuery(nestedQuery("nested1", termQuery("nested1.n_field1", "n_value1")).scoreMode(ScoreMode.Total))
.setExplain(true)
.execute().actionGet();
assertNoFailures(searchResponse);

View File

@ -18,6 +18,7 @@
*/
package org.elasticsearch.percolator;
import org.apache.lucene.search.join.ScoreMode;
import org.elasticsearch.action.ShardOperationFailedException;
import org.elasticsearch.action.percolate.MultiPercolateRequestBuilder;
import org.elasticsearch.action.percolate.MultiPercolateResponse;
@ -361,7 +362,7 @@ public class MultiPercolatorIT extends ESIntegTestCase {
ensureGreen("nestedindex");
client().prepareIndex("nestedindex", PercolatorService.TYPE_NAME, "Q").setSource(jsonBuilder().startObject()
.field("query", QueryBuilders.nestedQuery("employee", QueryBuilders.matchQuery("employee.name", "virginia potts").operator(Operator.AND)).scoreMode("avg")).endObject()).get();
.field("query", QueryBuilders.nestedQuery("employee", QueryBuilders.matchQuery("employee.name", "virginia potts").operator(Operator.AND)).scoreMode(ScoreMode.Avg)).endObject()).get();
refresh();

View File

@ -18,6 +18,7 @@
*/
package org.elasticsearch.percolator;
import org.apache.lucene.search.join.ScoreMode;
import org.elasticsearch.action.ShardOperationFailedException;
import org.elasticsearch.action.admin.cluster.node.stats.NodeStats;
import org.elasticsearch.action.admin.cluster.node.stats.NodesStatsResponse;
@ -1812,7 +1813,7 @@ public class PercolatorIT extends ESIntegTestCase {
ensureGreen("nestedindex");
client().prepareIndex("nestedindex", PercolatorService.TYPE_NAME, "Q").setSource(jsonBuilder().startObject()
.field("query", QueryBuilders.nestedQuery("employee", QueryBuilders.matchQuery("employee.name", "virginia potts").operator(Operator.AND)).scoreMode("avg")).endObject()).get();
.field("query", QueryBuilders.nestedQuery("employee", QueryBuilders.matchQuery("employee.name", "virginia potts").operator(Operator.AND)).scoreMode(ScoreMode.Avg)).endObject()).get();
refresh();