From cfe76546f2aeb643367233dc943ee1275743b020 Mon Sep 17 00:00:00 2001 From: Martijn van Groningen Date: Fri, 14 Sep 2012 16:14:57 +0200 Subject: [PATCH] Added has_parent query The `has_parent` query works the same as the `has_parent` filter, by automatically wrapping the filter with a constant_score. It has the same syntax as the `has_parent` filter. Closes #2254 --- .../index/query/HasParentQueryBuilder.java | 92 ++++++++++++++ .../index/query/HasParentQueryParser.java | 119 ++++++++++++++++++ .../index/query/QueryBuilders.java | 11 ++ .../indices/query/IndicesQueriesRegistry.java | 1 + .../child/SimpleChildQuerySearchTests.java | 15 +++ 5 files changed, 238 insertions(+) create mode 100644 src/main/java/org/elasticsearch/index/query/HasParentQueryBuilder.java create mode 100644 src/main/java/org/elasticsearch/index/query/HasParentQueryParser.java diff --git a/src/main/java/org/elasticsearch/index/query/HasParentQueryBuilder.java b/src/main/java/org/elasticsearch/index/query/HasParentQueryBuilder.java new file mode 100644 index 00000000000..f8a4b5707be --- /dev/null +++ b/src/main/java/org/elasticsearch/index/query/HasParentQueryBuilder.java @@ -0,0 +1,92 @@ +/* + * Licensed to ElasticSearch and Shay Banon 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 org.elasticsearch.common.xcontent.XContentBuilder; + +import java.io.IOException; + +/** + * Builder for the 'has_parent' query. + */ +public class HasParentQueryBuilder extends BaseQueryBuilder implements BoostableQueryBuilder { + + private final QueryBuilder queryBuilder; + private final String parentType; + private String scope; + private String filterName; + private String executionType; + private float boost = 1.0f; + + /** + * @param parentType The parent type + * @param parentQuery The query that will be matched with parent documents + */ + public HasParentQueryBuilder(String parentType, QueryBuilder parentQuery) { + this.parentType = parentType; + this.queryBuilder = parentQuery; + } + + public HasParentQueryBuilder scope(String scope) { + this.scope = scope; + return this; + } + + public HasParentQueryBuilder filterName(String filterName) { + this.filterName = filterName; + return this; + } + + /** + * Expert: Sets the low level parent to child filtering implementation. Can be: 'indirect' or 'uid' + * + * This option is experimental and will be removed. + */ + public HasParentQueryBuilder executionType(String executionType) { + this.executionType = executionType; + return this; + } + + public HasParentQueryBuilder boost(float boost) { + this.boost = boost; + return this; + } + + protected void doXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(HasParentQueryParser.NAME); + builder.field("query"); + queryBuilder.toXContent(builder, params); + builder.field("parent_type", parentType); + if (scope != null) { + builder.field("_scope", scope); + } + if (filterName != null) { + builder.field("_name", filterName); + } + if (executionType != null) { + builder.field("execution_type", executionType); + } + if (boost != 1.0f) { + builder.field("boost", boost); + } + builder.endObject(); + } +} + diff --git a/src/main/java/org/elasticsearch/index/query/HasParentQueryParser.java b/src/main/java/org/elasticsearch/index/query/HasParentQueryParser.java new file mode 100644 index 00000000000..19dd5bfe14d --- /dev/null +++ b/src/main/java/org/elasticsearch/index/query/HasParentQueryParser.java @@ -0,0 +1,119 @@ +/* + * Licensed to ElasticSearch and Shay Banon 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 org.apache.lucene.search.ConstantScoreQuery; +import org.apache.lucene.search.FilteredQuery; +import org.apache.lucene.search.Query; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.inject.Inject; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.index.mapper.DocumentMapper; +import org.elasticsearch.index.search.child.HasParentFilter; +import org.elasticsearch.search.internal.SearchContext; + +import java.io.IOException; + +// Same parse logic HasParentQueryFilter, but also parses boost and wraps filter in constant score query +public class HasParentQueryParser implements QueryParser { + + public static final String NAME = "has_parent"; + + @Inject + public HasParentQueryParser() { + } + + @Override + public String[] names() { + return new String[]{NAME, Strings.toCamelCase(NAME)}; + } + + @Override + public Query parse(QueryParseContext parseContext) throws IOException, QueryParsingException { + XContentParser parser = parseContext.parser(); + + Query query = null; + boolean queryFound = false; + float boost = 1.0f; + String parentType = null; + String executionType = "uid"; + String scope = null; + + String currentFieldName = 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)) { + // TODO handle `query` element before `type` element... + String[] origTypes = QueryParseContext.setTypesWithPrevious(parentType == null ? null : new String[]{parentType}); + try { + query = parseContext.parseInnerQuery(); + queryFound = true; + } finally { + QueryParseContext.setTypes(origTypes); + } + } else { + throw new QueryParsingException(parseContext.index(), "[has_parent] filter does not support [" + currentFieldName + "]"); + } + } else if (token.isValue()) { + if ("type".equals(currentFieldName) || "parent_type".equals(currentFieldName) || "parentType".equals(currentFieldName)) { + parentType = parser.text(); + } else if ("_scope".equals(currentFieldName)) { + scope = parser.text(); + } else if ("execution_type".equals(currentFieldName) || "executionType".equals(currentFieldName)) { // This option is experimental and will most likely be removed. + executionType = parser.text(); + } else if ("boost".equals(currentFieldName)) { + boost = parser.floatValue(); + } else { + throw new QueryParsingException(parseContext.index(), "[has_parent] filter does not support [" + currentFieldName + "]"); + } + } + } + if (!queryFound) { + throw new QueryParsingException(parseContext.index(), "[parent] filter requires 'query' field"); + } + if (query == null) { + return null; + } + + if (parentType == null) { + throw new QueryParsingException(parseContext.index(), "[parent] filter requires 'parent_type' field"); + } + + DocumentMapper parentDocMapper = parseContext.mapperService().documentMapper(parentType); + if (parentDocMapper == null) { + throw new QueryParsingException(parseContext.index(), "[parent] filter configured 'parent_type' [" + parentType + "] is not a valid type"); + } + + query.setBoost(boost); + // wrap the query with type query + query = new FilteredQuery(query, parseContext.cacheFilter(parentDocMapper.typeFilter(), null)); + SearchContext searchContext = SearchContext.current(); + HasParentFilter parentFilter = HasParentFilter.create(executionType, query, scope, parentType, searchContext); + + ConstantScoreQuery parentQuery = new ConstantScoreQuery(parentFilter); + parentQuery.setBoost(boost); + searchContext.addScopePhase(parentFilter); + return parentQuery; + } + +} \ No newline at end of file diff --git a/src/main/java/org/elasticsearch/index/query/QueryBuilders.java b/src/main/java/org/elasticsearch/index/query/QueryBuilders.java index 53a323ba7f1..c106ea21faf 100644 --- a/src/main/java/org/elasticsearch/index/query/QueryBuilders.java +++ b/src/main/java/org/elasticsearch/index/query/QueryBuilders.java @@ -551,6 +551,17 @@ public abstract class QueryBuilders { return new HasChildQueryBuilder(type, query); } + /** + * Constructs a new NON scoring parent query, with the parent type and the query to run on the parent documents. The + * results of this query are the children docs that those parent docs matched. + * + * @param type The parent type. + * @param query The query. + */ + public static HasParentQueryBuilder hasParentQuery(String type, QueryBuilder query) { + return new HasParentQueryBuilder(type, query); + } + public static NestedQueryBuilder nestedQuery(String path, QueryBuilder query) { return new NestedQueryBuilder(path, query); } diff --git a/src/main/java/org/elasticsearch/indices/query/IndicesQueriesRegistry.java b/src/main/java/org/elasticsearch/indices/query/IndicesQueriesRegistry.java index 91483d33d0c..575d25e2850 100644 --- a/src/main/java/org/elasticsearch/indices/query/IndicesQueriesRegistry.java +++ b/src/main/java/org/elasticsearch/indices/query/IndicesQueriesRegistry.java @@ -45,6 +45,7 @@ public class IndicesQueriesRegistry { addQueryParser(queryParsers, new MultiMatchQueryParser()); addQueryParser(queryParsers, new NestedQueryParser()); addQueryParser(queryParsers, new HasChildQueryParser()); + addQueryParser(queryParsers, new HasParentQueryParser()); addQueryParser(queryParsers, new TopChildrenQueryParser()); addQueryParser(queryParsers, new DisMaxQueryParser()); addQueryParser(queryParsers, new IdsQueryParser()); diff --git a/src/test/java/org/elasticsearch/test/integration/search/child/SimpleChildQuerySearchTests.java b/src/test/java/org/elasticsearch/test/integration/search/child/SimpleChildQuerySearchTests.java index 2a8728718d1..53b7f912564 100644 --- a/src/test/java/org/elasticsearch/test/integration/search/child/SimpleChildQuerySearchTests.java +++ b/src/test/java/org/elasticsearch/test/integration/search/child/SimpleChildQuerySearchTests.java @@ -267,6 +267,21 @@ public class SimpleChildQuerySearchTests extends AbstractNodesTests { assertThat(searchResponse.hits().totalHits(), equalTo(2l)); assertThat(searchResponse.hits().getAt(0).id(), equalTo("c1")); assertThat(searchResponse.hits().getAt(1).id(), equalTo("c2")); + + // HAS PARENT QUERY + searchResponse = client.prepareSearch("test").setQuery(hasParentQuery("parent", termQuery("p_field", "p_value2")).executionType(getExecutionMethod())).execute().actionGet(); + assertThat("Failures " + Arrays.toString(searchResponse.shardFailures()), searchResponse.shardFailures().length, equalTo(0)); + assertThat(searchResponse.failedShards(), equalTo(0)); + assertThat(searchResponse.hits().totalHits(), equalTo(2l)); + assertThat(searchResponse.hits().getAt(0).id(), equalTo("c3")); + assertThat(searchResponse.hits().getAt(1).id(), equalTo("c4")); + + searchResponse = client.prepareSearch("test").setQuery(hasParentQuery("parent", termQuery("p_field", "p_value1")).executionType(getExecutionMethod())).execute().actionGet(); + assertThat("Failures " + Arrays.toString(searchResponse.shardFailures()), searchResponse.shardFailures().length, equalTo(0)); + assertThat(searchResponse.failedShards(), equalTo(0)); + assertThat(searchResponse.hits().totalHits(), equalTo(2l)); + assertThat(searchResponse.hits().getAt(0).id(), equalTo("c1")); + assertThat(searchResponse.hits().getAt(1).id(), equalTo("c2")); } @Test