From d8054ec299d63bbdf7fa1ef59a5f958f44c70374 Mon Sep 17 00:00:00 2001 From: Martijn van Groningen Date: Wed, 3 Dec 2014 18:18:34 +0100 Subject: [PATCH] inner_hits: Added another more compact syntax for inner hits. Closes #8770 --- .../search/request/inner-hits.asciidoc | 154 ++++--- .../TransportShardDeleteByQueryAction.java | 6 +- .../index/mapper/MapperService.java | 11 +- .../index/query/HasChildFilterBuilder.java | 16 +- .../index/query/HasChildFilterParser.java | 17 +- .../index/query/HasChildQueryBuilder.java | 16 + .../index/query/HasChildQueryParser.java | 20 +- .../index/query/HasParentFilterBuilder.java | 15 + .../index/query/HasParentFilterParser.java | 13 +- .../index/query/HasParentQueryBuilder.java | 16 +- .../index/query/HasParentQueryParser.java | 22 +- .../index/query/IndexQueryParserModule.java | 2 + .../index/query/NestedFilterBuilder.java | 16 + .../index/query/NestedFilterParser.java | 20 +- .../index/query/NestedQueryBuilder.java | 17 + .../index/query/NestedQueryParser.java | 20 +- .../index/query/ParsedQuery.java | 8 +- .../index/query/QueryParseContext.java | 23 +- .../query/support/BaseInnerHitBuilder.java | 394 ++++++++++++++++++ .../support/InnerHitsQueryParserHelper.java | 132 ++++++ .../query/support/QueryInnerHitBuilder.java | 51 +++ .../percolator/PercolatorService.java | 26 +- .../percolator/QueryCollector.java | 5 +- .../fetch/innerhits/InnerHitsBuilder.java | 368 +--------------- .../fetch/innerhits/InnerHitsContext.java | 6 +- .../innerhits/InnerHitsFetchSubPhase.java | 4 +- .../innerhits/InnerHitsParseElement.java | 73 +--- .../search/innerhits/InnerHitsTests.java | 336 +++++++++------ 28 files changed, 1153 insertions(+), 654 deletions(-) create mode 100644 src/main/java/org/elasticsearch/index/query/support/BaseInnerHitBuilder.java create mode 100644 src/main/java/org/elasticsearch/index/query/support/InnerHitsQueryParserHelper.java create mode 100644 src/main/java/org/elasticsearch/index/query/support/QueryInnerHitBuilder.java diff --git a/docs/reference/search/request/inner-hits.asciidoc b/docs/reference/search/request/inner-hits.asciidoc index 8a4a2f53b64..6903a9b6719 100644 --- a/docs/reference/search/request/inner-hits.asciidoc +++ b/docs/reference/search/request/inner-hits.asciidoc @@ -11,32 +11,21 @@ it's very useful to know which inner nested objects (in the case of nested or ch of parent/child) caused certain information to be returned. The inner hits feature can be used for this. This feature returns per search hit in the search response additional nested hits that caused a search hit to match in a different scope. -The following snippet explains the basic structure of inner hits: +Inner hits can be used by defining a `inner_hits` definition on a `nested`, `has_child` or `has_parent` query and filter. +The structure looks like this: [source,js] -------------------------------------------------- "inner_hits" : { - "" : { - "" : { - "" : { - - [,"inner_hits" : { []+ } ]? - } + "" : { + "inner_hits" : { + } } - [,"" : { ... } ]* } -------------------------------------------------- -Inside the `inner_hits` definition, first the name if the inner hit is defined then whether the inner_hit -is a nested by defining `path` or a parent/child based definition by defining `type`. The next object layer contains -the name of the nested object field if the inner_hits is nested or the parent or child type if the inner_hit definition -is parent/child based. - -Multiple inner hit definitions can be defined in a single request. In the `` any option for features -that `inner_hits` support can be defined. Optionally another `inner_hits` definition can be defined in the ``. - -If `inner_hits` is defined, each search will contain a `inner_hits` json object with the following structure: +If `_inner_hits` is defined on a query that supports it then each search hit will contain a `inner_hits` json object with the following structure: [source,js] -------------------------------------------------- @@ -71,15 +60,13 @@ If `inner_hits` is defined, each search will contain a `inner_hits` json object Inner hits support the following options: [horizontal] -`path`:: Defines the nested scope where hits will be collected from. -`type`:: Defines the parent or child type score where hits will be collected from. -`query`:: Defines the query that will run in the defined nested, parent or child scope to collect and score hits. By default all document in the scope will be matched. `from`:: The offset from where the first hit to fetch for each `inner_hits` in the returned regular search hits. `size`:: The maximum number of hits to return per `inner_hits`. By default the top three matching hits are returned. `sort`:: How the inner hits should be sorted per `inner_hits`. By default the hits are sorted by the score. - -Either `path` or `type` must be defined. The `path` or `type` defines the scope from where hits are fetched and -used as inner hits. +`name`:: The name to be used for the particular inner hit definition in the response. Useful when multiple inner hits + have been defined in a single search request. The default depends in which query the inner hit is defined. + For `has_child` query and filter this is the child type, `has_parent` query and filter this is the parent type + and the nested query and filter this is the nested path. Inner hits also supports the following per document features: @@ -105,29 +92,14 @@ The example below assumes that there is a nested object field defined with the n "path" : "comments", "query" : { "match" : {"comments.message" : "[actual query]"} - } - } - }, - "inner_hits" : { - "comment" : { - "path" : { <1> - "comments" : { <2> - "query" : { - "match" : {"comments.message" : "[actual query]"} - } - } - } + }, + "inner_hits" : {} <1> } } } -------------------------------------------------- -<1> The inner hit definition is nested and requires the `path` option. -<2> The path option refers to the nested object field `comments` - -Above, the query is repeated in both the query and the `comment` inner hit definition. At the moment there is -no query referencing support, so in order to make sure that only inner nested objects are returned that contributed to -the matching of the regular hits, the inner query in the `nested` query needs to also be defined on the inner hits definition. +<1> The inner hit definition in the nested query. No other options need to be defined. An example of a response snippet that could be generated from the above search request: @@ -143,7 +115,7 @@ An example of a response snippet that could be generated from the above search r "_id": "1", "_source": ..., "inner_hits": { - "comment": { + "comments": { <1> "hits": { "total": ..., "hits": [ @@ -165,6 +137,8 @@ An example of a response snippet that could be generated from the above search r ... -------------------------------------------------- +<1> The name used in the inner hit definition in the search request. A custom key can be used via the `name` option. + The `_nested` metadata is crucial in the above example, because it defines from what inner nested object this inner hit came from. The `field` defines the object array field the nested hit is from and the `offset` relative to its location in the `_source`. Due to sorting and scoring the actual location of the hit objects in the `inner_hits` is usually @@ -193,25 +167,14 @@ The examples below assumes that there is a `_parent` field mapping in the `comme "type" : "comment", "query" : { "match" : {"message" : "[actual query]"} - } - } - }, - "inner_hits" : { - "comment" : { - "type" : { <1> - "comment" : { <2> - "query" : { - "match" : {"message" : "[actual query]"} - } - } - } + }, + "inner_hits" : {} <1> } } } -------------------------------------------------- -<1> This is a parent/child inner hit definition and requires the `type` option. -<2> Refers to the document type `comment` +<1> The inner hit definition like in the nested example. An example of a response snippet that could be generated from the above search request: @@ -243,4 +206,81 @@ An example of a response snippet that could be generated from the above search r } }, ... --------------------------------------------------- \ No newline at end of file +-------------------------------------------------- + +[[top-level-inner-hits]] +==== top level inner hits + +Besides defining inner hits on query and filters, inner hits can also be defined as a top level construct alongside the +`query` and `aggregations` definition. The main reason for using the top level inner hits definition is to let the +inner hits return documents that don't match with the main query. Also inner hits definitions can be nested via the +top level notation. Other then that the inner hit definition inside the query should be used, because that is the most +compact way for defining inner hits. + +The following snippet explains the basic structure of inner hits defined at the top level of the search request body: + +[source,js] +-------------------------------------------------- +"inner_hits" : { + "" : { + "" : { + "" : { + + [,"inner_hits" : { []+ } ]? + } + } + } + [,"" : { ... } ]* +} +-------------------------------------------------- + +Inside the `inner_hits` definition, first the name if the inner hit is defined then whether the inner_hit +is a nested by defining `path` or a parent/child based definition by defining `type`. The next object layer contains +the name of the nested object field if the inner_hits is nested or the parent or child type if the inner_hit definition +is parent/child based. + +Multiple inner hit definitions can be defined in a single request. In the `` any option for features +that `inner_hits` support can be defined. Optionally another `inner_hits` definition can be defined in the ``. + +An example that shows the use of nested inner hits via the top level notation: + +[source,js] +-------------------------------------------------- +{ + "query" : { + "nested" : { + "path" : "comments", + "query" : { + "match" : {"comments.message" : "[actual query]"} + } + } + }, + "inner_hits" : { + "comment" : { + "path" : { <1> + "comments" : { <2> + "query" : { + "match" : {"comments.message" : "[different query]"} <3> + } + } + } + } + } +} +-------------------------------------------------- + +<1> The inner hit definition is nested and requires the `path` option. +<2> The path option refers to the nested object field `comments` +<3> A query that runs to collect the nested inner documents for each search hit returned. If no query is defined all nested + inner documents will be included belonging to a search hit. This shows that it only make sense to the top level + inner hit definition if no query or a different query is specified. + +Additional options that are only available when using the top level inner hits notation: + +[horizontal] +`path`:: Defines the nested scope where hits will be collected from. +`type`:: Defines the parent or child type score where hits will be collected from. +`query`:: Defines the query that will run in the defined nested, parent or child scope to collect and score hits. By default all document in the scope will be matched. + +Either `path` or `type` must be defined. The `path` or `type` defines the scope from where hits are fetched and +used as inner hits. \ No newline at end of file diff --git a/src/main/java/org/elasticsearch/action/deletebyquery/TransportShardDeleteByQueryAction.java b/src/main/java/org/elasticsearch/action/deletebyquery/TransportShardDeleteByQueryAction.java index 24bf5cc9a5f..dd74d4ba7c9 100644 --- a/src/main/java/org/elasticsearch/action/deletebyquery/TransportShardDeleteByQueryAction.java +++ b/src/main/java/org/elasticsearch/action/deletebyquery/TransportShardDeleteByQueryAction.java @@ -19,8 +19,6 @@ package org.elasticsearch.action.deletebyquery; -import com.google.common.collect.ImmutableMap; -import org.apache.lucene.search.Filter; import org.elasticsearch.ElasticsearchIllegalStateException; import org.elasticsearch.action.support.ActionFilters; import org.elasticsearch.action.support.replication.TransportShardReplicationOperationAction; @@ -110,7 +108,7 @@ public class TransportShardDeleteByQueryAction extends TransportShardReplication pageCacheRecycler, bigArrays, threadPool.estimatedTimeInMillisCounter())); try { Engine.DeleteByQuery deleteByQuery = indexShard.prepareDeleteByQuery(request.source(), request.filteringAliases(), Engine.Operation.Origin.PRIMARY, request.types()); - SearchContext.current().parsedQuery(new ParsedQuery(deleteByQuery.query(), ImmutableMap.of())); + SearchContext.current().parsedQuery(new ParsedQuery(deleteByQuery.query())); indexShard.deleteByQuery(deleteByQuery); } finally { try (SearchContext searchContext = SearchContext.current()) { @@ -132,7 +130,7 @@ public class TransportShardDeleteByQueryAction extends TransportShardReplication pageCacheRecycler, bigArrays, threadPool.estimatedTimeInMillisCounter())); try { Engine.DeleteByQuery deleteByQuery = indexShard.prepareDeleteByQuery(request.source(), request.filteringAliases(), Engine.Operation.Origin.REPLICA, request.types()); - SearchContext.current().parsedQuery(new ParsedQuery(deleteByQuery.query(), ImmutableMap.of())); + SearchContext.current().parsedQuery(new ParsedQuery(deleteByQuery.query())); indexShard.deleteByQuery(deleteByQuery); } finally { try (SearchContext searchContext = SearchContext.current()) { diff --git a/src/main/java/org/elasticsearch/index/mapper/MapperService.java b/src/main/java/org/elasticsearch/index/mapper/MapperService.java index 0d719975e83..7cbc13ec1cd 100755 --- a/src/main/java/org/elasticsearch/index/mapper/MapperService.java +++ b/src/main/java/org/elasticsearch/index/mapper/MapperService.java @@ -701,7 +701,16 @@ public class MapperService extends AbstractIndexComponent { } ObjectMappers mappers = objectMapper(smartName); if (mappers != null) { - return new SmartNameObjectMapper(mappers.mapper(), null); + return new SmartNameObjectMapper(mappers.mapper(), guessDocMapper(smartName)); + } + return null; + } + + private DocumentMapper guessDocMapper(String path) { + for (DocumentMapper documentMapper : docMappers(false)) { + if (documentMapper.objectMappers().containsKey(path)) { + return documentMapper; + } } return null; } diff --git a/src/main/java/org/elasticsearch/index/query/HasChildFilterBuilder.java b/src/main/java/org/elasticsearch/index/query/HasChildFilterBuilder.java index 5524aa19e78..365d30b4e29 100644 --- a/src/main/java/org/elasticsearch/index/query/HasChildFilterBuilder.java +++ b/src/main/java/org/elasticsearch/index/query/HasChildFilterBuilder.java @@ -19,6 +19,7 @@ package org.elasticsearch.index.query; import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.index.query.support.QueryInnerHitBuilder; import java.io.IOException; @@ -34,7 +35,7 @@ public class HasChildFilterBuilder extends BaseFilterBuilder { private Integer shortCircuitCutoff; private Integer minChildren; private Integer maxChildren; - + private QueryInnerHitBuilder innerHit = null; public HasChildFilterBuilder(String type, QueryBuilder queryBuilder) { this.childType = type; @@ -96,6 +97,14 @@ public class HasChildFilterBuilder extends BaseFilterBuilder { return this; } + /** + * Sets inner hit definition in the scope of this filter and reusing the defined type and query. + */ + public HasChildFilterBuilder innerHit(QueryInnerHitBuilder innerHit) { + this.innerHit = innerHit; + return this; + } + @Override protected void doXContent(XContentBuilder builder, Params params) throws IOException { builder.startObject(HasChildFilterParser.NAME); @@ -119,6 +128,11 @@ public class HasChildFilterBuilder extends BaseFilterBuilder { if (shortCircuitCutoff != null) { builder.field("short_circuit_cutoff", shortCircuitCutoff); } + if (innerHit != null) { + builder.startObject("inner_hits"); + builder.value(innerHit); + builder.endObject(); + } builder.endObject(); } } diff --git a/src/main/java/org/elasticsearch/index/query/HasChildFilterParser.java b/src/main/java/org/elasticsearch/index/query/HasChildFilterParser.java index 2e86e9f7a94..0f83e3408bb 100644 --- a/src/main/java/org/elasticsearch/index/query/HasChildFilterParser.java +++ b/src/main/java/org/elasticsearch/index/query/HasChildFilterParser.java @@ -23,17 +23,21 @@ import org.apache.lucene.search.FilteredQuery; import org.apache.lucene.search.Query; import org.apache.lucene.search.join.BitDocIdSetFilter; import org.elasticsearch.common.Strings; +import org.elasticsearch.common.collect.Tuple; import org.elasticsearch.common.inject.Inject; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.index.fielddata.plain.ParentChildIndexFieldData; import org.elasticsearch.index.mapper.DocumentMapper; import org.elasticsearch.index.mapper.internal.ParentFieldMapper; +import org.elasticsearch.index.query.support.InnerHitsQueryParserHelper; import org.elasticsearch.index.query.support.XContentStructure; import org.elasticsearch.index.search.child.ChildrenConstantScoreQuery; import org.elasticsearch.index.search.child.ChildrenQuery; import org.elasticsearch.index.search.child.CustomQueryWrappingFilter; import org.elasticsearch.index.search.child.ScoreType; import org.elasticsearch.index.search.nested.NonNestedDocsFilter; +import org.elasticsearch.search.fetch.innerhits.InnerHitsContext; +import org.elasticsearch.search.internal.SubSearchContext; import java.io.IOException; @@ -46,8 +50,11 @@ public class HasChildFilterParser implements FilterParser { public static final String NAME = "has_child"; + private final InnerHitsQueryParserHelper innerHitsQueryParserHelper; + @Inject - public HasChildFilterParser() { + public HasChildFilterParser(InnerHitsQueryParserHelper innerHitsQueryParserHelper) { + this.innerHitsQueryParserHelper = innerHitsQueryParserHelper; } @Override @@ -66,6 +73,7 @@ public class HasChildFilterParser implements FilterParser { int shortCircuitParentDocSet = 8192; // Tests show a cut of point between 8192 and 16384. int minChildren = 0; int maxChildren = 0; + Tuple innerHits = null; String filterName = null; String currentFieldName = null; @@ -86,6 +94,8 @@ public class HasChildFilterParser implements FilterParser { } else if ("filter".equals(currentFieldName)) { innerFilter = new XContentStructure.InnerFilter(parseContext, childType == null ? null : new String[] {childType}); filterFound = true; + } else if ("inner_hits".equals(currentFieldName)) { + innerHits = innerHitsQueryParserHelper.parse(parseContext); } else { throw new QueryParsingException(parseContext.index(), "[has_child] filter does not support [" + currentFieldName + "]"); } @@ -131,6 +141,11 @@ public class HasChildFilterParser implements FilterParser { if (childDocMapper == null) { throw new QueryParsingException(parseContext.index(), "No mapping for for type [" + childType + "]"); } + if (innerHits != null) { + InnerHitsContext.ParentChildInnerHits parentChildInnerHits = new InnerHitsContext.ParentChildInnerHits(innerHits.v2(), query, null, childDocMapper); + String name = innerHits.v1() != null ? innerHits.v1() : childType; + parseContext.addInnerHits(name, parentChildInnerHits); + } ParentFieldMapper parentFieldMapper = childDocMapper.parentFieldMapper(); if (!parentFieldMapper.active()) { throw new QueryParsingException(parseContext.index(), "Type [" + childType + "] does not have parent mapping"); diff --git a/src/main/java/org/elasticsearch/index/query/HasChildQueryBuilder.java b/src/main/java/org/elasticsearch/index/query/HasChildQueryBuilder.java index 80795eb1369..08a7fef7cb0 100644 --- a/src/main/java/org/elasticsearch/index/query/HasChildQueryBuilder.java +++ b/src/main/java/org/elasticsearch/index/query/HasChildQueryBuilder.java @@ -19,6 +19,7 @@ package org.elasticsearch.index.query; import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.index.query.support.QueryInnerHitBuilder; import java.io.IOException; @@ -43,6 +44,8 @@ public class HasChildQueryBuilder extends BaseQueryBuilder implements BoostableQ private String queryName; + private QueryInnerHitBuilder innerHit = null; + public HasChildQueryBuilder(String type, QueryBuilder queryBuilder) { this.childType = type; this.queryBuilder = queryBuilder; @@ -98,6 +101,14 @@ public class HasChildQueryBuilder extends BaseQueryBuilder implements BoostableQ return this; } + /** + * Sets inner hit definition in the scope of this query and reusing the defined type and query. + */ + public HasChildQueryBuilder innerHit(QueryInnerHitBuilder innerHit) { + this.innerHit = innerHit; + return this; + } + @Override protected void doXContent(XContentBuilder builder, Params params) throws IOException { builder.startObject(HasChildQueryParser.NAME); @@ -122,6 +133,11 @@ public class HasChildQueryBuilder extends BaseQueryBuilder implements BoostableQ if (queryName != null) { builder.field("_name", queryName); } + if (innerHit != null) { + builder.startObject("inner_hits"); + builder.value(innerHit); + builder.endObject(); + } builder.endObject(); } } diff --git a/src/main/java/org/elasticsearch/index/query/HasChildQueryParser.java b/src/main/java/org/elasticsearch/index/query/HasChildQueryParser.java index 891c3470852..c7e8f2567d4 100644 --- a/src/main/java/org/elasticsearch/index/query/HasChildQueryParser.java +++ b/src/main/java/org/elasticsearch/index/query/HasChildQueryParser.java @@ -19,22 +19,26 @@ package org.elasticsearch.index.query; -import org.apache.lucene.search.FilteredQuery; import org.apache.lucene.search.Filter; +import org.apache.lucene.search.FilteredQuery; import org.apache.lucene.search.Query; import org.apache.lucene.search.join.BitDocIdSetFilter; import org.elasticsearch.common.Strings; +import org.elasticsearch.common.collect.Tuple; import org.elasticsearch.common.inject.Inject; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.index.fielddata.plain.ParentChildIndexFieldData; import org.elasticsearch.index.mapper.DocumentMapper; import org.elasticsearch.index.mapper.internal.ParentFieldMapper; +import org.elasticsearch.index.query.support.InnerHitsQueryParserHelper; import org.elasticsearch.index.query.support.XContentStructure; import org.elasticsearch.index.search.child.ChildrenConstantScoreQuery; import org.elasticsearch.index.search.child.ChildrenQuery; import org.elasticsearch.index.search.child.CustomQueryWrappingFilter; import org.elasticsearch.index.search.child.ScoreType; import org.elasticsearch.index.search.nested.NonNestedDocsFilter; +import org.elasticsearch.search.fetch.innerhits.InnerHitsContext; +import org.elasticsearch.search.internal.SubSearchContext; import java.io.IOException; @@ -47,8 +51,11 @@ public class HasChildQueryParser implements QueryParser { public static final String NAME = "has_child"; + private final InnerHitsQueryParserHelper innerHitsQueryParserHelper; + @Inject - public HasChildQueryParser() { + public HasChildQueryParser(InnerHitsQueryParserHelper innerHitsQueryParserHelper) { + this.innerHitsQueryParserHelper = innerHitsQueryParserHelper; } @Override @@ -69,6 +76,7 @@ public class HasChildQueryParser implements QueryParser { int maxChildren = 0; int shortCircuitParentDocSet = 8192; String queryName = null; + Tuple innerHits = null; String currentFieldName = null; XContentParser.Token token; @@ -84,6 +92,8 @@ public class HasChildQueryParser implements QueryParser { if ("query".equals(currentFieldName)) { iq = new XContentStructure.InnerQuery(parseContext, childType == null ? null : new String[] { childType }); queryFound = true; + } else if ("inner_hits".equals(currentFieldName)) { + innerHits = innerHitsQueryParserHelper.parse(parseContext); } else { throw new QueryParsingException(parseContext.index(), "[has_child] query does not support [" + currentFieldName + "]"); } @@ -131,6 +141,12 @@ public class HasChildQueryParser implements QueryParser { throw new QueryParsingException(parseContext.index(), "[has_child] Type [" + childType + "] does not have parent mapping"); } + if (innerHits != null) { + InnerHitsContext.ParentChildInnerHits parentChildInnerHits = new InnerHitsContext.ParentChildInnerHits(innerHits.v2(), innerQuery, null, childDocMapper); + String name = innerHits.v1() != null ? innerHits.v1() : childType; + parseContext.addInnerHits(name, parentChildInnerHits); + } + ParentFieldMapper parentFieldMapper = childDocMapper.parentFieldMapper(); if (!parentFieldMapper.active()) { throw new QueryParsingException(parseContext.index(), "[has_child] _parent field not configured"); diff --git a/src/main/java/org/elasticsearch/index/query/HasParentFilterBuilder.java b/src/main/java/org/elasticsearch/index/query/HasParentFilterBuilder.java index 89287134714..62a96debf8c 100644 --- a/src/main/java/org/elasticsearch/index/query/HasParentFilterBuilder.java +++ b/src/main/java/org/elasticsearch/index/query/HasParentFilterBuilder.java @@ -19,6 +19,7 @@ package org.elasticsearch.index.query; import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.index.query.support.QueryInnerHitBuilder; import java.io.IOException; @@ -31,6 +32,7 @@ public class HasParentFilterBuilder extends BaseFilterBuilder { private final FilterBuilder filterBuilder; private final String parentType; private String filterName; + private QueryInnerHitBuilder innerHit = null; /** * @param parentType The parent type @@ -74,6 +76,14 @@ public class HasParentFilterBuilder extends BaseFilterBuilder { return this; } + /** + * Sets inner hit definition in the scope of this filter and reusing the defined type and query. + */ + public HasParentFilterBuilder innerHit(QueryInnerHitBuilder innerHit) { + this.innerHit = innerHit; + return this; + } + @Override protected void doXContent(XContentBuilder builder, Params params) throws IOException { builder.startObject(HasParentFilterParser.NAME); @@ -88,6 +98,11 @@ public class HasParentFilterBuilder extends BaseFilterBuilder { if (filterName != null) { builder.field("_name", filterName); } + if (innerHit != null) { + builder.startObject("inner_hits"); + builder.value(innerHit); + builder.endObject(); + } builder.endObject(); } } diff --git a/src/main/java/org/elasticsearch/index/query/HasParentFilterParser.java b/src/main/java/org/elasticsearch/index/query/HasParentFilterParser.java index 673ce801be2..2f549c2f674 100644 --- a/src/main/java/org/elasticsearch/index/query/HasParentFilterParser.java +++ b/src/main/java/org/elasticsearch/index/query/HasParentFilterParser.java @@ -21,10 +21,13 @@ package org.elasticsearch.index.query; import org.apache.lucene.search.Filter; import org.apache.lucene.search.Query; import org.elasticsearch.common.Strings; +import org.elasticsearch.common.collect.Tuple; import org.elasticsearch.common.inject.Inject; import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.index.query.support.InnerHitsQueryParserHelper; import org.elasticsearch.index.query.support.XContentStructure; import org.elasticsearch.index.search.child.CustomQueryWrappingFilter; +import org.elasticsearch.search.internal.SubSearchContext; import java.io.IOException; @@ -38,8 +41,11 @@ public class HasParentFilterParser implements FilterParser { public static final String NAME = "has_parent"; + private final InnerHitsQueryParserHelper innerHitsQueryParserHelper; + @Inject - public HasParentFilterParser() { + public HasParentFilterParser(InnerHitsQueryParserHelper innerHitsQueryParserHelper) { + this.innerHitsQueryParserHelper = innerHitsQueryParserHelper; } @Override @@ -55,6 +61,7 @@ public class HasParentFilterParser implements FilterParser { boolean queryFound = false; boolean filterFound = false; String parentType = null; + Tuple innerHits = null; String filterName = null; String currentFieldName = null; @@ -75,6 +82,8 @@ public class HasParentFilterParser implements FilterParser { } else if ("filter".equals(currentFieldName)) { innerFilter = new XContentStructure.InnerFilter(parseContext, parentType == null ? null : new String[] {parentType}); filterFound = true; + } else if ("inner_hits".equals(currentFieldName)) { + innerHits = innerHitsQueryParserHelper.parse(parseContext); } else { throw new QueryParsingException(parseContext.index(), "[has_parent] filter does not support [" + currentFieldName + "]"); } @@ -110,7 +119,7 @@ public class HasParentFilterParser implements FilterParser { return null; } - Query parentQuery = createParentQuery(innerQuery, parentType, false, parseContext); + Query parentQuery = createParentQuery(innerQuery, parentType, false, parseContext, innerHits); if (parentQuery == null) { return null; } diff --git a/src/main/java/org/elasticsearch/index/query/HasParentQueryBuilder.java b/src/main/java/org/elasticsearch/index/query/HasParentQueryBuilder.java index 8d2d7fd84f3..e0db92d5e6d 100644 --- a/src/main/java/org/elasticsearch/index/query/HasParentQueryBuilder.java +++ b/src/main/java/org/elasticsearch/index/query/HasParentQueryBuilder.java @@ -19,6 +19,7 @@ package org.elasticsearch.index.query; import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.index.query.support.QueryInnerHitBuilder; import java.io.IOException; @@ -32,6 +33,7 @@ public class HasParentQueryBuilder extends BaseQueryBuilder implements Boostable private String scoreType; private float boost = 1.0f; private String queryName; + private QueryInnerHitBuilder innerHit = null; /** * @param parentType The parent type @@ -63,6 +65,14 @@ public class HasParentQueryBuilder extends BaseQueryBuilder implements Boostable return this; } + /** + * Sets inner hit definition in the scope of this query and reusing the defined type and query. + */ + public HasParentQueryBuilder innerHit(QueryInnerHitBuilder innerHit) { + this.innerHit = innerHit; + return this; + } + protected void doXContent(XContentBuilder builder, Params params) throws IOException { builder.startObject(HasParentQueryParser.NAME); builder.field("query"); @@ -77,7 +87,11 @@ public class HasParentQueryBuilder extends BaseQueryBuilder implements Boostable if (queryName != null) { builder.field("_name", queryName); } - + if (innerHit != null) { + builder.startObject("inner_hits"); + builder.value(innerHit); + builder.endObject(); + } builder.endObject(); } } diff --git a/src/main/java/org/elasticsearch/index/query/HasParentQueryParser.java b/src/main/java/org/elasticsearch/index/query/HasParentQueryParser.java index 09869dc6881..a44d5a69917 100644 --- a/src/main/java/org/elasticsearch/index/query/HasParentQueryParser.java +++ b/src/main/java/org/elasticsearch/index/query/HasParentQueryParser.java @@ -23,6 +23,7 @@ import org.apache.lucene.search.Filter; import org.apache.lucene.search.FilteredQuery; import org.apache.lucene.search.Query; import org.elasticsearch.common.Strings; +import org.elasticsearch.common.collect.Tuple; import org.elasticsearch.common.inject.Inject; import org.elasticsearch.common.lucene.search.NotFilter; import org.elasticsearch.common.lucene.search.XBooleanFilter; @@ -30,10 +31,13 @@ import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.index.fielddata.plain.ParentChildIndexFieldData; import org.elasticsearch.index.mapper.DocumentMapper; import org.elasticsearch.index.mapper.internal.ParentFieldMapper; +import org.elasticsearch.index.query.support.InnerHitsQueryParserHelper; import org.elasticsearch.index.query.support.XContentStructure; import org.elasticsearch.index.search.child.CustomQueryWrappingFilter; import org.elasticsearch.index.search.child.ParentConstantScoreQuery; import org.elasticsearch.index.search.child.ParentQuery; +import org.elasticsearch.search.fetch.innerhits.InnerHitsContext; +import org.elasticsearch.search.internal.SubSearchContext; import java.io.IOException; import java.util.HashSet; @@ -45,8 +49,11 @@ public class HasParentQueryParser implements QueryParser { public static final String NAME = "has_parent"; + private final InnerHitsQueryParserHelper innerHitsQueryParserHelper; + @Inject - public HasParentQueryParser() { + public HasParentQueryParser(InnerHitsQueryParserHelper innerHitsQueryParserHelper) { + this.innerHitsQueryParserHelper = innerHitsQueryParserHelper; } @Override @@ -64,6 +71,7 @@ public class HasParentQueryParser implements QueryParser { String parentType = null; boolean score = false; String queryName = null; + Tuple innerHits = null; String currentFieldName = null; XContentParser.Token token; @@ -79,6 +87,8 @@ public class HasParentQueryParser implements QueryParser { if ("query".equals(currentFieldName)) { iq = new XContentStructure.InnerQuery(parseContext, parentType == null ? null : new String[] {parentType}); queryFound = true; + } else if ("inner_hits".equals(currentFieldName)) { + innerHits = innerHitsQueryParserHelper.parse(parseContext); } else { throw new QueryParsingException(parseContext.index(), "[has_parent] query does not support [" + currentFieldName + "]"); } @@ -122,7 +132,7 @@ public class HasParentQueryParser implements QueryParser { } innerQuery.setBoost(boost); - Query query = createParentQuery(innerQuery, parentType, score, parseContext); + Query query = createParentQuery(innerQuery, parentType, score, parseContext, innerHits); if (query == null) { return null; } @@ -134,12 +144,18 @@ public class HasParentQueryParser implements QueryParser { return query; } - static Query createParentQuery(Query innerQuery, String parentType, boolean score, QueryParseContext parseContext) { + static Query createParentQuery(Query innerQuery, String parentType, boolean score, QueryParseContext parseContext, Tuple innerHits) { DocumentMapper parentDocMapper = parseContext.mapperService().documentMapper(parentType); if (parentDocMapper == null) { throw new QueryParsingException(parseContext.index(), "[has_parent] query configured 'parent_type' [" + parentType + "] is not a valid type"); } + if (innerHits != null) { + InnerHitsContext.ParentChildInnerHits parentChildInnerHits = new InnerHitsContext.ParentChildInnerHits(innerHits.v2(), innerQuery, null, parentDocMapper); + String name = innerHits.v1() != null ? innerHits.v1() : parentType; + parseContext.addInnerHits(name, parentChildInnerHits); + } + Set parentTypes = new HashSet<>(5); parentTypes.add(parentDocMapper.type()); ParentChildIndexFieldData parentChildIndexFieldData = null; diff --git a/src/main/java/org/elasticsearch/index/query/IndexQueryParserModule.java b/src/main/java/org/elasticsearch/index/query/IndexQueryParserModule.java index 101e123233c..5f349881071 100644 --- a/src/main/java/org/elasticsearch/index/query/IndexQueryParserModule.java +++ b/src/main/java/org/elasticsearch/index/query/IndexQueryParserModule.java @@ -26,6 +26,7 @@ import org.elasticsearch.common.inject.Scopes; import org.elasticsearch.common.inject.assistedinject.FactoryProvider; import org.elasticsearch.common.inject.multibindings.MapBinder; import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.index.query.support.InnerHitsQueryParserHelper; import java.util.LinkedList; import java.util.Map; @@ -146,6 +147,7 @@ public class IndexQueryParserModule extends AbstractModule { protected void configure() { bind(IndexQueryParserService.class).asEagerSingleton(); + bind(InnerHitsQueryParserHelper.class).asEagerSingleton(); // handle XContenQueryParsers MapBinder queryBinder diff --git a/src/main/java/org/elasticsearch/index/query/NestedFilterBuilder.java b/src/main/java/org/elasticsearch/index/query/NestedFilterBuilder.java index 8e86f1ff4ce..0b15bbfcf14 100644 --- a/src/main/java/org/elasticsearch/index/query/NestedFilterBuilder.java +++ b/src/main/java/org/elasticsearch/index/query/NestedFilterBuilder.java @@ -20,6 +20,7 @@ package org.elasticsearch.index.query; import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.index.query.support.QueryInnerHitBuilder; import java.io.IOException; @@ -35,6 +36,8 @@ public class NestedFilterBuilder extends BaseFilterBuilder { private String cacheKey; private String filterName; + private QueryInnerHitBuilder innerHit = null; + public NestedFilterBuilder(String path, QueryBuilder queryBuilder) { this.path = path; this.queryBuilder = queryBuilder; @@ -73,6 +76,14 @@ public class NestedFilterBuilder extends BaseFilterBuilder { return this; } + /** + * Sets inner hit definition in the scope of this nested filter and reusing the defined path and query. + */ + public NestedFilterBuilder innerHit(QueryInnerHitBuilder innerHit) { + this.innerHit = innerHit; + return this; + } + @Override protected void doXContent(XContentBuilder builder, Params params) throws IOException { builder.startObject(NestedFilterParser.NAME); @@ -96,6 +107,11 @@ public class NestedFilterBuilder extends BaseFilterBuilder { if (cacheKey != null) { builder.field("_cache_key", cacheKey); } + if (innerHit != null) { + builder.startObject("inner_hits"); + builder.value(innerHit); + builder.endObject(); + } builder.endObject(); } } \ No newline at end of file diff --git a/src/main/java/org/elasticsearch/index/query/NestedFilterParser.java b/src/main/java/org/elasticsearch/index/query/NestedFilterParser.java index d87f3169ac0..1a0f9cf6bbb 100644 --- a/src/main/java/org/elasticsearch/index/query/NestedFilterParser.java +++ b/src/main/java/org/elasticsearch/index/query/NestedFilterParser.java @@ -27,13 +27,18 @@ import org.apache.lucene.search.join.BitDocIdSetFilter; import org.apache.lucene.search.join.ScoreMode; import org.apache.lucene.search.join.ToParentBlockJoinQuery; import org.elasticsearch.common.Strings; +import org.elasticsearch.common.collect.Tuple; import org.elasticsearch.common.inject.Inject; import org.elasticsearch.common.lucene.HashedBytesRef; import org.elasticsearch.common.lucene.search.Queries; import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.index.mapper.DocumentMapper; import org.elasticsearch.index.mapper.MapperService; import org.elasticsearch.index.mapper.object.ObjectMapper; +import org.elasticsearch.index.query.support.InnerHitsQueryParserHelper; import org.elasticsearch.index.search.nested.NonNestedDocsFilter; +import org.elasticsearch.search.fetch.innerhits.InnerHitsContext; +import org.elasticsearch.search.internal.SubSearchContext; import java.io.IOException; @@ -41,8 +46,11 @@ public class NestedFilterParser implements FilterParser { public static final String NAME = "nested"; + private final InnerHitsQueryParserHelper innerHitsQueryParserHelper; + @Inject - public NestedFilterParser() { + public NestedFilterParser(InnerHitsQueryParserHelper innerHitsQueryParserHelper) { + this.innerHitsQueryParserHelper = innerHitsQueryParserHelper; } @Override @@ -63,6 +71,7 @@ public class NestedFilterParser implements FilterParser { boolean cache = false; HashedBytesRef cacheKey = null; String filterName = null; + Tuple innerHits = null; // we need a late binding filter so we can inject a parent nested filter inner nested queries NestedQueryParser.LateBindingParentFilter currentParentFilterContext = NestedQueryParser.parentFilterContext.get(); @@ -83,6 +92,8 @@ public class NestedFilterParser implements FilterParser { } else if ("filter".equals(currentFieldName)) { filterFound = true; filter = parseContext.parseInnerFilter(); + } else if ("inner_hits".equals(currentFieldName)) { + innerHits = innerHitsQueryParserHelper.parse(parseContext); } else { throw new QueryParsingException(parseContext.index(), "[nested] filter does not support [" + currentFieldName + "]"); } @@ -135,6 +146,13 @@ public class NestedFilterParser implements FilterParser { usAsParentFilter.filter = childFilter; // wrap the child query to only work on the nested path type query = new FilteredQuery(query, childFilter); + if (innerHits != null) { + DocumentMapper childDocumentMapper = mapper.docMapper(); + ObjectMapper parentObjectMapper = childDocumentMapper.findParentObjectMapper(objectMapper); + InnerHitsContext.NestedInnerHits nestedInnerHits = new InnerHitsContext.NestedInnerHits(innerHits.v2(), query, null, parentObjectMapper, objectMapper); + String name = innerHits.v1() != null ? innerHits.v1() : path; + parseContext.addInnerHits(name, nestedInnerHits); + } BitDocIdSetFilter parentFilter = currentParentFilterContext; if (parentFilter == null) { diff --git a/src/main/java/org/elasticsearch/index/query/NestedQueryBuilder.java b/src/main/java/org/elasticsearch/index/query/NestedQueryBuilder.java index 88ded2a5a7f..e05789044b3 100644 --- a/src/main/java/org/elasticsearch/index/query/NestedQueryBuilder.java +++ b/src/main/java/org/elasticsearch/index/query/NestedQueryBuilder.java @@ -20,6 +20,7 @@ package org.elasticsearch.index.query; import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.index.query.support.QueryInnerHitBuilder; import java.io.IOException; @@ -36,6 +37,8 @@ public class NestedQueryBuilder extends BaseQueryBuilder implements BoostableQue private String queryName; + private QueryInnerHitBuilder innerHit; + public NestedQueryBuilder(String path, QueryBuilder queryBuilder) { this.path = path; this.queryBuilder = queryBuilder; @@ -73,6 +76,14 @@ public class NestedQueryBuilder extends BaseQueryBuilder implements BoostableQue return this; } + /** + * Sets inner hit definition in the scope of this nested query and reusing the defined path and query. + */ + public NestedQueryBuilder innerHit(QueryInnerHitBuilder innerHit) { + this.innerHit = innerHit; + return this; + } + @Override protected void doXContent(XContentBuilder builder, Params params) throws IOException { builder.startObject(NestedQueryParser.NAME); @@ -93,6 +104,12 @@ public class NestedQueryBuilder extends BaseQueryBuilder implements BoostableQue if (queryName != null) { builder.field("_name", queryName); } + if (innerHit != null) { + builder.startObject("inner_hits"); + builder.value(innerHit); + builder.endObject(); + } builder.endObject(); } + } diff --git a/src/main/java/org/elasticsearch/index/query/NestedQueryParser.java b/src/main/java/org/elasticsearch/index/query/NestedQueryParser.java index 3a87b10e83a..13db5da68cf 100644 --- a/src/main/java/org/elasticsearch/index/query/NestedQueryParser.java +++ b/src/main/java/org/elasticsearch/index/query/NestedQueryParser.java @@ -29,11 +29,16 @@ import org.apache.lucene.search.join.ScoreMode; import org.apache.lucene.search.join.ToParentBlockJoinQuery; import org.apache.lucene.util.BitDocIdSet; import org.elasticsearch.common.Strings; +import org.elasticsearch.common.collect.Tuple; import org.elasticsearch.common.inject.Inject; import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.index.mapper.DocumentMapper; import org.elasticsearch.index.mapper.MapperService; import org.elasticsearch.index.mapper.object.ObjectMapper; +import org.elasticsearch.index.query.support.InnerHitsQueryParserHelper; import org.elasticsearch.index.search.nested.NonNestedDocsFilter; +import org.elasticsearch.search.fetch.innerhits.InnerHitsContext.NestedInnerHits; +import org.elasticsearch.search.internal.SubSearchContext; import java.io.IOException; @@ -41,8 +46,11 @@ public class NestedQueryParser implements QueryParser { public static final String NAME = "nested"; + private final InnerHitsQueryParserHelper innerHitsQueryParserHelper; + @Inject - public NestedQueryParser() { + public NestedQueryParser(InnerHitsQueryParserHelper innerHitsQueryParserHelper) { + this.innerHitsQueryParserHelper = innerHitsQueryParserHelper; } @Override @@ -62,6 +70,7 @@ public class NestedQueryParser implements QueryParser { String path = null; ScoreMode scoreMode = ScoreMode.Avg; String queryName = null; + Tuple innerHits = null; // we need a late binding filter so we can inject a parent nested filter inner nested queries LateBindingParentFilter currentParentFilterContext = parentFilterContext.get(); @@ -82,6 +91,8 @@ public class NestedQueryParser implements QueryParser { } else if ("filter".equals(currentFieldName)) { filterFound = true; filter = parseContext.parseInnerFilter(); + } else if ("inner_hits".equals(currentFieldName)) { + innerHits = innerHitsQueryParserHelper.parse(parseContext); } else { throw new QueryParsingException(parseContext.index(), "[nested] query does not support [" + currentFieldName + "]"); } @@ -141,6 +152,13 @@ public class NestedQueryParser implements QueryParser { usAsParentFilter.filter = childFilter; // wrap the child query to only work on the nested path type query = new FilteredQuery(query, childFilter); + if (innerHits != null) { + DocumentMapper childDocumentMapper = mapper.docMapper(); + ObjectMapper parentObjectMapper = childDocumentMapper.findParentObjectMapper(objectMapper); + NestedInnerHits nestedInnerHits = new NestedInnerHits(innerHits.v2(), query, null, parentObjectMapper, objectMapper); + String name = innerHits.v1() != null ? innerHits.v1() : path; + parseContext.addInnerHits(name, nestedInnerHits); + } BitDocIdSetFilter parentFilter = currentParentFilterContext; if (parentFilter == null) { diff --git a/src/main/java/org/elasticsearch/index/query/ParsedQuery.java b/src/main/java/org/elasticsearch/index/query/ParsedQuery.java index 0bbae921eb8..0ffc4efb696 100644 --- a/src/main/java/org/elasticsearch/index/query/ParsedQuery.java +++ b/src/main/java/org/elasticsearch/index/query/ParsedQuery.java @@ -32,7 +32,6 @@ import org.elasticsearch.common.lucene.search.Queries; public class ParsedQuery { private final Query query; - private final ImmutableMap namedFilters; public ParsedQuery(Query query, ImmutableMap namedFilters) { @@ -45,6 +44,11 @@ public class ParsedQuery { this.namedFilters = parsedQuery.namedFilters; } + public ParsedQuery(Query query) { + this.query = query; + this.namedFilters = ImmutableMap.of(); + } + /** * The query parsed. */ @@ -55,7 +59,7 @@ public class ParsedQuery { public ImmutableMap namedFilters() { return this.namedFilters; } - + public static ParsedQuery parsedMatchAllQuery() { return new ParsedQuery(Queries.newMatchAllQuery(), ImmutableMap.of()); } diff --git a/src/main/java/org/elasticsearch/index/query/QueryParseContext.java b/src/main/java/org/elasticsearch/index/query/QueryParseContext.java index 8fdad9d86c0..830d2bb122e 100644 --- a/src/main/java/org/elasticsearch/index/query/QueryParseContext.java +++ b/src/main/java/org/elasticsearch/index/query/QueryParseContext.java @@ -21,7 +21,6 @@ package org.elasticsearch.index.query; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Maps; - import org.apache.lucene.index.LeafReaderContext; import org.apache.lucene.queryparser.classic.MapperQueryParser; import org.apache.lucene.queryparser.classic.QueryParserSettings; @@ -51,15 +50,12 @@ import org.elasticsearch.index.mapper.MapperService; import org.elasticsearch.index.search.child.CustomQueryWrappingFilter; import org.elasticsearch.index.similarity.SimilarityService; import org.elasticsearch.script.ScriptService; +import org.elasticsearch.search.fetch.innerhits.InnerHitsContext; import org.elasticsearch.search.internal.SearchContext; import org.elasticsearch.search.lookup.SearchLookup; import java.io.IOException; -import java.util.Arrays; -import java.util.Collection; -import java.util.EnumSet; -import java.util.List; -import java.util.Map; +import java.util.*; /** * @@ -251,12 +247,21 @@ public class QueryParseContext { } public ImmutableMap copyNamedFilters() { - if (namedFilters.isEmpty()) { - return ImmutableMap.of(); - } return ImmutableMap.copyOf(namedFilters); } + public void addInnerHits(String name, InnerHitsContext.BaseInnerHits context) { + SearchContext sc = SearchContext.current(); + InnerHitsContext innerHitsContext; + if (sc.innerHits() == null) { + innerHitsContext = new InnerHitsContext(new HashMap()); + sc.innerHits(innerHitsContext); + } else { + innerHitsContext = sc.innerHits(); + } + innerHitsContext.addInnerHitDefinition(name, context); + } + @Nullable public Query parseInnerQuery() throws IOException, QueryParsingException { // move to START object diff --git a/src/main/java/org/elasticsearch/index/query/support/BaseInnerHitBuilder.java b/src/main/java/org/elasticsearch/index/query/support/BaseInnerHitBuilder.java new file mode 100644 index 00000000000..d6de4ec19ed --- /dev/null +++ b/src/main/java/org/elasticsearch/index/query/support/BaseInnerHitBuilder.java @@ -0,0 +1,394 @@ +/* + * 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.support; + +import org.elasticsearch.common.Nullable; +import org.elasticsearch.common.xcontent.ToXContent; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.index.query.QueryBuilder; +import org.elasticsearch.search.builder.SearchSourceBuilder; +import org.elasticsearch.search.highlight.HighlightBuilder; +import org.elasticsearch.search.sort.SortBuilder; +import org.elasticsearch.search.sort.SortOrder; + +import java.io.IOException; +import java.util.Map; + +/** + */ +@SuppressWarnings("unchecked") +public abstract class BaseInnerHitBuilder implements ToXContent { + + protected SearchSourceBuilder sourceBuilder; + + /** + * The index to start to return hits from. Defaults to 0. + */ + public T setFrom(int from) { + sourceBuilder().from(from); + return (T) this; + } + + + /** + * The number of search hits to return. Defaults to 10. + */ + public T setSize(int size) { + sourceBuilder().size(size); + return (T) this; + } + + /** + * Applies when sorting, and controls if scores will be tracked as well. Defaults to + * false. + */ + public T setTrackScores(boolean trackScores) { + sourceBuilder().trackScores(trackScores); + return (T) this; + } + + /** + * Should each {@link org.elasticsearch.search.SearchHit} be returned with an + * explanation of the hit (ranking). + */ + public T setExplain(boolean explain) { + sourceBuilder().explain(explain); + return (T) this; + } + + /** + * Should each {@link org.elasticsearch.search.SearchHit} be returned with its + * version. + */ + public T setVersion(boolean version) { + sourceBuilder().version(version); + return (T) this; + } + + /** + * Sets no fields to be loaded, resulting in only id and type to be returned per field. + */ + public T setNoFields() { + sourceBuilder().noFields(); + return (T) this; + } + + /** + * Indicates whether the response should contain the stored _source for every hit + */ + public T setFetchSource(boolean fetch) { + sourceBuilder().fetchSource(fetch); + return (T) this; + } + + /** + * Indicate that _source should be returned with every hit, with an "include" and/or "exclude" set which can include simple wildcard + * elements. + * + * @param include An optional include (optionally wildcarded) pattern to filter the returned _source + * @param exclude An optional exclude (optionally wildcarded) pattern to filter the returned _source + */ + public T setFetchSource(@Nullable String include, @Nullable String exclude) { + sourceBuilder().fetchSource(include, exclude); + return (T) this; + } + + /** + * Indicate that _source should be returned with every hit, with an "include" and/or "exclude" set which can include simple wildcard + * elements. + * + * @param includes An optional list of include (optionally wildcarded) pattern to filter the returned _source + * @param excludes An optional list of exclude (optionally wildcarded) pattern to filter the returned _source + */ + public T setFetchSource(@Nullable String[] includes, @Nullable String[] excludes) { + sourceBuilder().fetchSource(includes, excludes); + return (T) this; + } + + /** + * Adds a field data based field to load and return. The field does not have to be stored, + * but its recommended to use non analyzed or numeric fields. + * + * @param name The field to get from the field data cache + */ + public T addFieldDataField(String name) { + sourceBuilder().fieldDataField(name); + return (T) this; + } + + /** + * Adds a script based field to load and return. The field does not have to be stored, + * but its recommended to use non analyzed or numeric fields. + * + * @param name The name that will represent this value in the return hit + * @param script The script to use + */ + public T addScriptField(String name, String script) { + sourceBuilder().scriptField(name, script); + return (T) this; + } + + /** + * Adds a script based field to load and return. The field does not have to be stored, + * but its recommended to use non analyzed or numeric fields. + * + * @param name The name that will represent this value in the return hit + * @param script The script to use + * @param params Parameters that the script can use. + */ + public T addScriptField(String name, String script, Map params) { + sourceBuilder().scriptField(name, script, params); + return (T) this; + } + + /** + * Adds a script based field to load and return. The field does not have to be stored, + * but its recommended to use non analyzed or numeric fields. + * + * @param name The name that will represent this value in the return hit + * @param lang The language of the script + * @param script The script to use + * @param params Parameters that the script can use (can be null). + */ + public T addScriptField(String name, String lang, String script, Map params) { + sourceBuilder().scriptField(name, lang, script, params); + return (T) this; + } + + /** + * Adds a sort against the given field name and the sort ordering. + * + * @param field The name of the field + * @param order The sort ordering + */ + public T addSort(String field, SortOrder order) { + sourceBuilder().sort(field, order); + return (T) this; + } + + /** + * Adds a generic sort builder. + * + * @see org.elasticsearch.search.sort.SortBuilders + */ + public T addSort(SortBuilder sort) { + sourceBuilder().sort(sort); + return (T) this; + } + + public HighlightBuilder highlightBuilder() { + return sourceBuilder().highlighter(); + } + + /** + * Adds a field to be highlighted with default fragment size of 100 characters, and + * default number of fragments of 5. + * + * @param name The field to highlight + */ + public T addHighlightedField(String name) { + highlightBuilder().field(name); + return (T) this; + } + + + /** + * Adds a field to be highlighted with a provided fragment size (in characters), and + * default number of fragments of 5. + * + * @param name The field to highlight + * @param fragmentSize The size of a fragment in characters + */ + public T addHighlightedField(String name, int fragmentSize) { + highlightBuilder().field(name, fragmentSize); + return (T) this; + } + + /** + * Adds a field to be highlighted with a provided fragment size (in characters), and + * a provided (maximum) number of fragments. + * + * @param name The field to highlight + * @param fragmentSize The size of a fragment in characters + * @param numberOfFragments The (maximum) number of fragments + */ + public T addHighlightedField(String name, int fragmentSize, int numberOfFragments) { + highlightBuilder().field(name, fragmentSize, numberOfFragments); + return (T) this; + } + + /** + * Adds a field to be highlighted with a provided fragment size (in characters), + * a provided (maximum) number of fragments and an offset for the highlight. + * + * @param name The field to highlight + * @param fragmentSize The size of a fragment in characters + * @param numberOfFragments The (maximum) number of fragments + */ + public T addHighlightedField(String name, int fragmentSize, int numberOfFragments, + int fragmentOffset) { + highlightBuilder().field(name, fragmentSize, numberOfFragments, fragmentOffset); + return (T) this; + } + + /** + * Adds a highlighted field. + */ + public T addHighlightedField(HighlightBuilder.Field field) { + highlightBuilder().field(field); + return (T) this; + } + + /** + * Set a tag scheme that encapsulates a built in pre and post tags. The allows schemes + * are styled and default. + * + * @param schemaName The tag scheme name + */ + public T setHighlighterTagsSchema(String schemaName) { + highlightBuilder().tagsSchema(schemaName); + return (T) this; + } + + public T setHighlighterFragmentSize(Integer fragmentSize) { + highlightBuilder().fragmentSize(fragmentSize); + return (T) this; + } + + public T setHighlighterNumOfFragments(Integer numOfFragments) { + highlightBuilder().numOfFragments(numOfFragments); + return (T) this; + } + + public T setHighlighterFilter(Boolean highlightFilter) { + highlightBuilder().highlightFilter(highlightFilter); + return (T) this; + } + + /** + * The encoder to set for highlighting + */ + public T setHighlighterEncoder(String encoder) { + highlightBuilder().encoder(encoder); + return (T) this; + } + + /** + * Explicitly set the pre tags that will be used for highlighting. + */ + public T setHighlighterPreTags(String... preTags) { + highlightBuilder().preTags(preTags); + return (T) this; + } + + /** + * Explicitly set the post tags that will be used for highlighting. + */ + public T setHighlighterPostTags(String... postTags) { + highlightBuilder().postTags(postTags); + return (T) this; + } + + /** + * The order of fragments per field. By default, ordered by the order in the + * highlighted text. Can be score, which then it will be ordered + * by score of the fragments. + */ + public T setHighlighterOrder(String order) { + highlightBuilder().order(order); + return (T) this; + } + + public T setHighlighterRequireFieldMatch(boolean requireFieldMatch) { + highlightBuilder().requireFieldMatch(requireFieldMatch); + return (T) this; + } + + public T setHighlighterBoundaryMaxScan(Integer boundaryMaxScan) { + highlightBuilder().boundaryMaxScan(boundaryMaxScan); + return (T) this; + } + + public T setHighlighterBoundaryChars(char[] boundaryChars) { + highlightBuilder().boundaryChars(boundaryChars); + return (T) this; + } + + /** + * The highlighter type to use. + */ + public T setHighlighterType(String type) { + highlightBuilder().highlighterType(type); + return (T) this; + } + + public T setHighlighterFragmenter(String fragmenter) { + highlightBuilder().fragmenter(fragmenter); + return (T) this; + } + + /** + * Sets a query to be used for highlighting all fields instead of the search query. + */ + public T setHighlighterQuery(QueryBuilder highlightQuery) { + highlightBuilder().highlightQuery(highlightQuery); + return (T) this; + } + + /** + * Sets the size of the fragment to return from the beginning of the field if there are no matches to + * highlight and the field doesn't also define noMatchSize. + * @param noMatchSize integer to set or null to leave out of request. default is null. + * @return this builder for chaining + */ + public T setHighlighterNoMatchSize(Integer noMatchSize) { + highlightBuilder().noMatchSize(noMatchSize); + return (T) this; + } + + /** + * Sets the maximum number of phrases the fvh will consider if the field doesn't also define phraseLimit. + */ + public T setHighlighterPhraseLimit(Integer phraseLimit) { + highlightBuilder().phraseLimit(phraseLimit); + return (T) this; + } + + public T setHighlighterOptions(Map options) { + highlightBuilder().options(options); + return (T) this; + } + + protected SearchSourceBuilder sourceBuilder() { + if (sourceBuilder == null) { + sourceBuilder = new SearchSourceBuilder(); + } + return sourceBuilder; + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + if (sourceBuilder != null) { + sourceBuilder.innerToXContent(builder, params); + } + return builder; + } + +} diff --git a/src/main/java/org/elasticsearch/index/query/support/InnerHitsQueryParserHelper.java b/src/main/java/org/elasticsearch/index/query/support/InnerHitsQueryParserHelper.java new file mode 100644 index 00000000000..5a571255ed9 --- /dev/null +++ b/src/main/java/org/elasticsearch/index/query/support/InnerHitsQueryParserHelper.java @@ -0,0 +1,132 @@ +/* + * 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.support; + +import org.elasticsearch.ElasticsearchIllegalArgumentException; +import org.elasticsearch.common.collect.Tuple; +import org.elasticsearch.common.inject.Inject; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.index.query.QueryParseContext; +import org.elasticsearch.index.query.QueryParsingException; +import org.elasticsearch.search.fetch.fielddata.FieldDataFieldsParseElement; +import org.elasticsearch.search.fetch.script.ScriptFieldsParseElement; +import org.elasticsearch.search.fetch.source.FetchSourceParseElement; +import org.elasticsearch.search.highlight.HighlighterParseElement; +import org.elasticsearch.search.internal.SearchContext; +import org.elasticsearch.search.internal.SubSearchContext; +import org.elasticsearch.search.sort.SortParseElement; + +import java.io.IOException; + +public class InnerHitsQueryParserHelper { + + private final SortParseElement sortParseElement; + private final FetchSourceParseElement sourceParseElement; + private final HighlighterParseElement highlighterParseElement; + private final ScriptFieldsParseElement scriptFieldsParseElement; + private final FieldDataFieldsParseElement fieldDataFieldsParseElement; + + @Inject + public InnerHitsQueryParserHelper(SortParseElement sortParseElement, FetchSourceParseElement sourceParseElement, HighlighterParseElement highlighterParseElement, ScriptFieldsParseElement scriptFieldsParseElement, FieldDataFieldsParseElement fieldDataFieldsParseElement) { + this.sortParseElement = sortParseElement; + this.sourceParseElement = sourceParseElement; + this.highlighterParseElement = highlighterParseElement; + this.scriptFieldsParseElement = scriptFieldsParseElement; + this.fieldDataFieldsParseElement = fieldDataFieldsParseElement; + } + + public Tuple parse(QueryParseContext parserContext) throws IOException, QueryParsingException { + String fieldName = null; + XContentParser.Token token; + String innerHitName = null; + SubSearchContext subSearchContext = new SubSearchContext(SearchContext.current()); + try { + XContentParser parser = parserContext.parser(); + while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { + if (token == XContentParser.Token.FIELD_NAME) { + fieldName = parser.currentName(); + } else if (token.isValue()) { + if ("name".equals(fieldName)) { + innerHitName = parser.textOrNull(); + } else { + parseCommonInnerHitOptions(parser, token, fieldName, subSearchContext, sortParseElement, sourceParseElement, highlighterParseElement, scriptFieldsParseElement, fieldDataFieldsParseElement); + } + } else { + parseCommonInnerHitOptions(parser, token, fieldName, subSearchContext, sortParseElement, sourceParseElement, highlighterParseElement, scriptFieldsParseElement, fieldDataFieldsParseElement); + } + } + } catch (Exception e) { + throw new QueryParsingException(parserContext.index(), "Failed to parse [_inner_hits]", e); + } + return new Tuple<>(innerHitName, subSearchContext); + } + + public static void parseCommonInnerHitOptions(XContentParser parser, XContentParser.Token token, String fieldName, SubSearchContext subSearchContext, + SortParseElement sortParseElement, FetchSourceParseElement sourceParseElement, HighlighterParseElement highlighterParseElement, + ScriptFieldsParseElement scriptFieldsParseElement, FieldDataFieldsParseElement fieldDataFieldsParseElement) throws Exception { + if ("sort".equals(fieldName)) { + sortParseElement.parse(parser, subSearchContext); + } else if ("_source".equals(fieldName)) { + sourceParseElement.parse(parser, subSearchContext); + } else if (token == XContentParser.Token.START_OBJECT) { + switch (fieldName) { + case "highlight": + highlighterParseElement.parse(parser, subSearchContext); + break; + case "scriptFields": + case "script_fields": + scriptFieldsParseElement.parse(parser, subSearchContext); + break; + default: + throw new ElasticsearchIllegalArgumentException("Unknown key for a " + token + " for nested query: [" + fieldName + "]."); + } + } else if (token == XContentParser.Token.START_ARRAY) { + switch (fieldName) { + case "fielddataFields": + case "fielddata_fields": + fieldDataFieldsParseElement.parse(parser, subSearchContext); + break; + default: + throw new ElasticsearchIllegalArgumentException("Unknown key for a " + token + " for nested query: [" + fieldName + "]."); + } + } else if (token.isValue()) { + switch (fieldName) { + case "from": + subSearchContext.from(parser.intValue()); + break; + case "size": + subSearchContext.size(parser.intValue()); + break; + case "track_scores": + case "trackScores": + subSearchContext.trackScores(parser.booleanValue()); + break; + case "version": + subSearchContext.version(parser.booleanValue()); + break; + case "explain": + subSearchContext.explain(parser.booleanValue()); + break; + default: + throw new ElasticsearchIllegalArgumentException("Unknown key for a " + token + " for nested query: [" + fieldName + "]."); + } + } + } +} diff --git a/src/main/java/org/elasticsearch/index/query/support/QueryInnerHitBuilder.java b/src/main/java/org/elasticsearch/index/query/support/QueryInnerHitBuilder.java new file mode 100644 index 00000000000..71229abe975 --- /dev/null +++ b/src/main/java/org/elasticsearch/index/query/support/QueryInnerHitBuilder.java @@ -0,0 +1,51 @@ +/* + * 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.support; + +import org.elasticsearch.common.xcontent.XContentBuilder; + +import java.io.IOException; + +/** + */ +public class QueryInnerHitBuilder extends BaseInnerHitBuilder { + + private String name; + + /** + * Set the key name to be used in the response. + * + * Defaults to the path if used in nested query, child type if used in has_child query and parent type if used in has_parent. + */ + public QueryInnerHitBuilder setName(String name) { + this.name = name; + return this; + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + super.toXContent(builder, params); + if (name != null) { + builder.field("name", name); + } + return builder; + } + +} diff --git a/src/main/java/org/elasticsearch/percolator/PercolatorService.java b/src/main/java/org/elasticsearch/percolator/PercolatorService.java index b64b706fe13..c2d3e84e07f 100644 --- a/src/main/java/org/elasticsearch/percolator/PercolatorService.java +++ b/src/main/java/org/elasticsearch/percolator/PercolatorService.java @@ -19,20 +19,11 @@ package org.elasticsearch.percolator; import com.carrotsearch.hppc.ByteObjectOpenHashMap; -import com.google.common.collect.ImmutableMap; - import org.apache.lucene.index.LeafReaderContext; import org.apache.lucene.index.ReaderUtil; import org.apache.lucene.index.memory.ExtendedMemoryIndex; import org.apache.lucene.index.memory.MemoryIndex; -import org.apache.lucene.search.Collector; -import org.apache.lucene.search.ConstantScoreQuery; -import org.apache.lucene.search.Filter; -import org.apache.lucene.search.FilteredQuery; -import org.apache.lucene.search.MatchAllDocsQuery; -import org.apache.lucene.search.Query; -import org.apache.lucene.search.ScoreDoc; -import org.apache.lucene.search.TopDocs; +import org.apache.lucene.search.*; import org.apache.lucene.util.BytesRef; import org.apache.lucene.util.CloseableThreadLocal; import org.elasticsearch.ElasticsearchException; @@ -63,6 +54,7 @@ import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentFactory; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.common.xcontent.XContentType; +import org.elasticsearch.index.IndexService; import org.elasticsearch.index.engine.Engine; import org.elasticsearch.index.fielddata.IndexFieldData; import org.elasticsearch.index.fielddata.SortedBinaryDocValues; @@ -74,13 +66,9 @@ import org.elasticsearch.index.mapper.internal.IdFieldMapper; import org.elasticsearch.index.percolator.stats.ShardPercolateService; import org.elasticsearch.index.query.ParsedQuery; import org.elasticsearch.index.search.nested.NonNestedDocsFilter; -import org.elasticsearch.index.IndexService; import org.elasticsearch.index.shard.IndexShard; import org.elasticsearch.indices.IndicesService; -import org.elasticsearch.percolator.QueryCollector.Count; -import org.elasticsearch.percolator.QueryCollector.Match; -import org.elasticsearch.percolator.QueryCollector.MatchAndScore; -import org.elasticsearch.percolator.QueryCollector.MatchAndSort; +import org.elasticsearch.percolator.QueryCollector.*; import org.elasticsearch.script.ScriptService; import org.elasticsearch.search.SearchParseElement; import org.elasticsearch.search.SearchShardTarget; @@ -98,9 +86,7 @@ import java.util.List; import java.util.Map; import static org.elasticsearch.index.mapper.SourceToParse.source; -import static org.elasticsearch.percolator.QueryCollector.count; -import static org.elasticsearch.percolator.QueryCollector.match; -import static org.elasticsearch.percolator.QueryCollector.matchAndScore; +import static org.elasticsearch.percolator.QueryCollector.*; /** */ @@ -539,7 +525,7 @@ public class PercolatorService extends AbstractComponent { for (Map.Entry entry : context.percolateQueries().entrySet()) { if (context.highlight() != null) { - context.parsedQuery(new ParsedQuery(entry.getValue(), ImmutableMap.of())); + context.parsedQuery(new ParsedQuery(entry.getValue())); context.hitContext().cache().clear(); } try { @@ -758,7 +744,7 @@ public class PercolatorService extends AbstractComponent { matches.add(BytesRef.deepCopyOf(bytes)); if (hls != null) { Query query = context.percolateQueries().get(bytes); - context.parsedQuery(new ParsedQuery(query, ImmutableMap.of())); + context.parsedQuery(new ParsedQuery(query)); context.hitContext().cache().clear(); highlightPhase.hitExecute(context, context.hitContext()); hls.add(i, context.hitContext().hit().getHighlightFields()); diff --git a/src/main/java/org/elasticsearch/percolator/QueryCollector.java b/src/main/java/org/elasticsearch/percolator/QueryCollector.java index 4149e86a385..ad24be5d9cd 100644 --- a/src/main/java/org/elasticsearch/percolator/QueryCollector.java +++ b/src/main/java/org/elasticsearch/percolator/QueryCollector.java @@ -20,7 +20,6 @@ package org.elasticsearch.percolator; import com.carrotsearch.hppc.FloatArrayList; import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; import org.apache.lucene.index.LeafReaderContext; import org.apache.lucene.search.*; import org.apache.lucene.util.BytesRef; @@ -186,7 +185,7 @@ abstract class QueryCollector extends SimpleCollector { // run the query try { if (context.highlight() != null) { - context.parsedQuery(new ParsedQuery(query, ImmutableMap.of())); + context.parsedQuery(new ParsedQuery(query)); context.hitContext().cache().clear(); } @@ -308,7 +307,7 @@ abstract class QueryCollector extends SimpleCollector { // run the query try { if (context.highlight() != null) { - context.parsedQuery(new ParsedQuery(query, ImmutableMap.of())); + context.parsedQuery(new ParsedQuery(query)); context.hitContext().cache().clear(); } if (isNestedDoc) { diff --git a/src/main/java/org/elasticsearch/search/fetch/innerhits/InnerHitsBuilder.java b/src/main/java/org/elasticsearch/search/fetch/innerhits/InnerHitsBuilder.java index bb54280188c..125f6355a06 100644 --- a/src/main/java/org/elasticsearch/search/fetch/innerhits/InnerHitsBuilder.java +++ b/src/main/java/org/elasticsearch/search/fetch/innerhits/InnerHitsBuilder.java @@ -19,14 +19,10 @@ package org.elasticsearch.search.fetch.innerhits; -import org.elasticsearch.common.Nullable; import org.elasticsearch.common.xcontent.ToXContent; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.index.query.QueryBuilder; -import org.elasticsearch.search.builder.SearchSourceBuilder; -import org.elasticsearch.search.highlight.HighlightBuilder; -import org.elasticsearch.search.sort.SortBuilder; -import org.elasticsearch.search.sort.SortOrder; +import org.elasticsearch.index.query.support.BaseInnerHitBuilder; import java.io.IOException; import java.util.HashMap; @@ -53,378 +49,44 @@ public class InnerHitsBuilder implements ToXContent { innerHits.put(name, innerHit); } - public static class InnerHit implements ToXContent { + public static class InnerHit extends BaseInnerHitBuilder { private String path; private String type; - private SearchSourceBuilder sourceBuilder; - + /** + * Sets the query to run for collecting the inner hits. + */ public InnerHit setQuery(QueryBuilder query) { sourceBuilder().query(query); return this; } + /** + * For parent/child inner hits the type to collect inner hits for. + */ public InnerHit setPath(String path) { this.path = path; return this; } + /** + * For nested inner hits the path to collect child nested docs for. + */ public InnerHit setType(String type) { this.type = type; return this; } /** - * The index to start to return hits from. Defaults to 0. + * Adds a nested inner hit definition that collects inner hits for hits + * on this inner hit level. */ - public InnerHit setFrom(int from) { - sourceBuilder().from(from); - return this; - } - - - /** - * The number of search hits to return. Defaults to 10. - */ - public InnerHit setSize(int size) { - sourceBuilder().size(size); - return this; - } - - /** - * Applies when sorting, and controls if scores will be tracked as well. Defaults to - * false. - */ - public InnerHit setTrackScores(boolean trackScores) { - sourceBuilder().trackScores(trackScores); - return this; - } - - /** - * Should each {@link org.elasticsearch.search.SearchHit} be returned with an - * explanation of the hit (ranking). - */ - public InnerHit setExplain(boolean explain) { - sourceBuilder().explain(explain); - return this; - } - - /** - * Should each {@link org.elasticsearch.search.SearchHit} be returned with its - * version. - */ - public InnerHit setVersion(boolean version) { - sourceBuilder().version(version); - return this; - } - - /** - * Sets no fields to be loaded, resulting in only id and type to be returned per field. - */ - public InnerHit setNoFields() { - sourceBuilder().noFields(); - return this; - } - - /** - * Indicates whether the response should contain the stored _source for every hit - */ - public InnerHit setFetchSource(boolean fetch) { - sourceBuilder().fetchSource(fetch); - return this; - } - - /** - * Indicate that _source should be returned with every hit, with an "include" and/or "exclude" set which can include simple wildcard - * elements. - * - * @param include An optional include (optionally wildcarded) pattern to filter the returned _source - * @param exclude An optional exclude (optionally wildcarded) pattern to filter the returned _source - */ - public InnerHit setFetchSource(@Nullable String include, @Nullable String exclude) { - sourceBuilder().fetchSource(include, exclude); - return this; - } - - /** - * Indicate that _source should be returned with every hit, with an "include" and/or "exclude" set which can include simple wildcard - * elements. - * - * @param includes An optional list of include (optionally wildcarded) pattern to filter the returned _source - * @param excludes An optional list of exclude (optionally wildcarded) pattern to filter the returned _source - */ - public InnerHit setFetchSource(@Nullable String[] includes, @Nullable String[] excludes) { - sourceBuilder().fetchSource(includes, excludes); - return this; - } - - /** - * Adds a field data based field to load and return. The field does not have to be stored, - * but its recommended to use non analyzed or numeric fields. - * - * @param name The field to get from the field data cache - */ - public InnerHit addFieldDataField(String name) { - sourceBuilder().fieldDataField(name); - return this; - } - - /** - * Adds a script based field to load and return. The field does not have to be stored, - * but its recommended to use non analyzed or numeric fields. - * - * @param name The name that will represent this value in the return hit - * @param script The script to use - */ - public InnerHit addScriptField(String name, String script) { - sourceBuilder().scriptField(name, script); - return this; - } - - /** - * Adds a script based field to load and return. The field does not have to be stored, - * but its recommended to use non analyzed or numeric fields. - * - * @param name The name that will represent this value in the return hit - * @param script The script to use - * @param params Parameters that the script can use. - */ - public InnerHit addScriptField(String name, String script, Map params) { - sourceBuilder().scriptField(name, script, params); - return this; - } - - /** - * Adds a script based field to load and return. The field does not have to be stored, - * but its recommended to use non analyzed or numeric fields. - * - * @param name The name that will represent this value in the return hit - * @param lang The language of the script - * @param script The script to use - * @param params Parameters that the script can use (can be null). - */ - public InnerHit addScriptField(String name, String lang, String script, Map params) { - sourceBuilder().scriptField(name, lang, script, params); - return this; - } - - /** - * Adds a sort against the given field name and the sort ordering. - * - * @param field The name of the field - * @param order The sort ordering - */ - public InnerHit addSort(String field, SortOrder order) { - sourceBuilder().sort(field, order); - return this; - } - - /** - * Adds a generic sort builder. - * - * @see org.elasticsearch.search.sort.SortBuilders - */ - public InnerHit addSort(SortBuilder sort) { - sourceBuilder().sort(sort); - return this; - } - - /** - * Adds a field to be highlighted with default fragment size of 100 characters, and - * default number of fragments of 5. - * - * @param name The field to highlight - */ - public InnerHit addHighlightedField(String name) { - highlightBuilder().field(name); - return this; - } - - - /** - * Adds a field to be highlighted with a provided fragment size (in characters), and - * default number of fragments of 5. - * - * @param name The field to highlight - * @param fragmentSize The size of a fragment in characters - */ - public InnerHit addHighlightedField(String name, int fragmentSize) { - highlightBuilder().field(name, fragmentSize); - return this; - } - - /** - * Adds a field to be highlighted with a provided fragment size (in characters), and - * a provided (maximum) number of fragments. - * - * @param name The field to highlight - * @param fragmentSize The size of a fragment in characters - * @param numberOfFragments The (maximum) number of fragments - */ - public InnerHit addHighlightedField(String name, int fragmentSize, int numberOfFragments) { - highlightBuilder().field(name, fragmentSize, numberOfFragments); - return this; - } - - /** - * Adds a field to be highlighted with a provided fragment size (in characters), - * a provided (maximum) number of fragments and an offset for the highlight. - * - * @param name The field to highlight - * @param fragmentSize The size of a fragment in characters - * @param numberOfFragments The (maximum) number of fragments - */ - public InnerHit addHighlightedField(String name, int fragmentSize, int numberOfFragments, - int fragmentOffset) { - highlightBuilder().field(name, fragmentSize, numberOfFragments, fragmentOffset); - return this; - } - - /** - * Adds a highlighted field. - */ - public InnerHit addHighlightedField(HighlightBuilder.Field field) { - highlightBuilder().field(field); - return this; - } - - /** - * Set a tag scheme that encapsulates a built in pre and post tags. The allows schemes - * are styled and default. - * - * @param schemaName The tag scheme name - */ - public InnerHit setHighlighterTagsSchema(String schemaName) { - highlightBuilder().tagsSchema(schemaName); - return this; - } - - public InnerHit setHighlighterFragmentSize(Integer fragmentSize) { - highlightBuilder().fragmentSize(fragmentSize); - return this; - } - - public InnerHit setHighlighterNumOfFragments(Integer numOfFragments) { - highlightBuilder().numOfFragments(numOfFragments); - return this; - } - - public InnerHit setHighlighterFilter(Boolean highlightFilter) { - highlightBuilder().highlightFilter(highlightFilter); - return this; - } - - /** - * The encoder to set for highlighting - */ - public InnerHit setHighlighterEncoder(String encoder) { - highlightBuilder().encoder(encoder); - return this; - } - - /** - * Explicitly set the pre tags that will be used for highlighting. - */ - public InnerHit setHighlighterPreTags(String... preTags) { - highlightBuilder().preTags(preTags); - return this; - } - - /** - * Explicitly set the post tags that will be used for highlighting. - */ - public InnerHit setHighlighterPostTags(String... postTags) { - highlightBuilder().postTags(postTags); - return this; - } - - /** - * The order of fragments per field. By default, ordered by the order in the - * highlighted text. Can be score, which then it will be ordered - * by score of the fragments. - */ - public InnerHit setHighlighterOrder(String order) { - highlightBuilder().order(order); - return this; - } - - public InnerHit setHighlighterRequireFieldMatch(boolean requireFieldMatch) { - highlightBuilder().requireFieldMatch(requireFieldMatch); - return this; - } - - public InnerHit setHighlighterBoundaryMaxScan(Integer boundaryMaxScan) { - highlightBuilder().boundaryMaxScan(boundaryMaxScan); - return this; - } - - public InnerHit setHighlighterBoundaryChars(char[] boundaryChars) { - highlightBuilder().boundaryChars(boundaryChars); - return this; - } - - /** - * The highlighter type to use. - */ - public InnerHit setHighlighterType(String type) { - highlightBuilder().highlighterType(type); - return this; - } - - public InnerHit setHighlighterFragmenter(String fragmenter) { - highlightBuilder().fragmenter(fragmenter); - return this; - } - - /** - * Sets a query to be used for highlighting all fields instead of the search query. - */ - public InnerHit setHighlighterQuery(QueryBuilder highlightQuery) { - highlightBuilder().highlightQuery(highlightQuery); - return this; - } - - /** - * Sets the size of the fragment to return from the beginning of the field if there are no matches to - * highlight and the field doesn't also define noMatchSize. - * @param noMatchSize integer to set or null to leave out of request. default is null. - * @return this builder for chaining - */ - public InnerHit setHighlighterNoMatchSize(Integer noMatchSize) { - highlightBuilder().noMatchSize(noMatchSize); - return this; - } - - /** - * Sets the maximum number of phrases the fvh will consider if the field doesn't also define phraseLimit. - */ - public InnerHit setHighlighterPhraseLimit(Integer phraseLimit) { - highlightBuilder().phraseLimit(phraseLimit); - return this; - } - - public InnerHit setHighlighterOptions(Map options) { - highlightBuilder().options(options); - return this; - } - public InnerHit addInnerHit(String name, InnerHit innerHit) { sourceBuilder().innerHitsBuilder().addInnerHit(name, innerHit); return this; } - private SearchSourceBuilder sourceBuilder() { - if (sourceBuilder == null) { - sourceBuilder = new SearchSourceBuilder(); - } - return sourceBuilder; - } - - public HighlightBuilder highlightBuilder() { - return sourceBuilder().highlighter(); - } - @Override public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { if (path != null) { @@ -432,9 +94,7 @@ public class InnerHitsBuilder implements ToXContent { } else { builder.startObject("type").startObject(type); } - if (sourceBuilder != null) { - sourceBuilder.innerToXContent(builder, params); - } + super.toXContent(builder, params); return builder.endObject().endObject(); } } diff --git a/src/main/java/org/elasticsearch/search/fetch/innerhits/InnerHitsContext.java b/src/main/java/org/elasticsearch/search/fetch/innerhits/InnerHitsContext.java index 5a6c87ce236..b51cd5634c6 100644 --- a/src/main/java/org/elasticsearch/search/fetch/innerhits/InnerHitsContext.java +++ b/src/main/java/org/elasticsearch/search/fetch/innerhits/InnerHitsContext.java @@ -49,7 +49,7 @@ import java.util.Map; */ public final class InnerHitsContext { - private Map innerHits; + private final Map innerHits; public InnerHitsContext(Map innerHits) { this.innerHits = innerHits; @@ -59,6 +59,10 @@ public final class InnerHitsContext { return innerHits; } + public void addInnerHitDefinition(String name, BaseInnerHits innerHit) { + innerHits.put(name, innerHit); + } + public static abstract class BaseInnerHits extends FilteredSearchContext { protected final Query query; diff --git a/src/main/java/org/elasticsearch/search/fetch/innerhits/InnerHitsFetchSubPhase.java b/src/main/java/org/elasticsearch/search/fetch/innerhits/InnerHitsFetchSubPhase.java index dcba7c95c13..8f58bdaf52e 100644 --- a/src/main/java/org/elasticsearch/search/fetch/innerhits/InnerHitsFetchSubPhase.java +++ b/src/main/java/org/elasticsearch/search/fetch/innerhits/InnerHitsFetchSubPhase.java @@ -76,10 +76,8 @@ public class InnerHitsFetchSubPhase implements FetchSubPhase { @Override public void hitExecute(SearchContext context, HitContext hitContext) throws ElasticsearchException { - InnerHitsContext innerHitsContext = context.innerHits(); Map results = new HashMap<>(); - Map innerHitsByKey = innerHitsContext.getInnerHits(); - for (Map.Entry entry : innerHitsByKey.entrySet()) { + for (Map.Entry entry : context.innerHits().getInnerHits().entrySet()) { InnerHitsContext.BaseInnerHits innerHits = entry.getValue(); TopDocs topDocs = innerHits.topDocs(context, hitContext); innerHits.queryResult().topDocs(topDocs); diff --git a/src/main/java/org/elasticsearch/search/fetch/innerhits/InnerHitsParseElement.java b/src/main/java/org/elasticsearch/search/fetch/innerhits/InnerHitsParseElement.java index b825287ff51..f373197c49b 100644 --- a/src/main/java/org/elasticsearch/search/fetch/innerhits/InnerHitsParseElement.java +++ b/src/main/java/org/elasticsearch/search/fetch/innerhits/InnerHitsParseElement.java @@ -39,6 +39,8 @@ import org.elasticsearch.search.sort.SortParseElement; import java.util.HashMap; import java.util.Map; +import static org.elasticsearch.index.query.support.InnerHitsQueryParserHelper.parseCommonInnerHitOptions; + /** */ public class InnerHitsParseElement implements SearchParseElement { @@ -70,7 +72,7 @@ public class InnerHitsParseElement implements SearchParseElement { Map innerHitsMap = null; while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { if (token != XContentParser.Token.FIELD_NAME) { - throw new ElasticsearchIllegalArgumentException("Unexpected token " + token + " in [inner_hits]: aggregations definitions must start with the name of the aggregation."); + throw new ElasticsearchIllegalArgumentException("Unexpected token " + token + " in [inner_hits]: inner_hit definitions must start with the name of the inner_hit."); } final String innerHitName = parser.currentName(); token = parser.nextToken(); @@ -138,63 +140,16 @@ public class InnerHitsParseElement implements SearchParseElement { while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { if (token == XContentParser.Token.FIELD_NAME) { fieldName = parser.currentName(); - } else if ("sort".equals(fieldName)) { - sortParseElement.parse(parser, subSearchContext); - } else if ("_source".equals(fieldName)) { - sourceParseElement.parse(parser, subSearchContext); } else if (token == XContentParser.Token.START_OBJECT) { - switch (fieldName) { - case "highlight": - highlighterParseElement.parse(parser, subSearchContext); - break; - case "scriptFields": - case "script_fields": - scriptFieldsParseElement.parse(parser, subSearchContext); - break; - case "inner_hits": - childInnerHits = parseInnerHits(parser, subSearchContext); - break; - case "query": - query = context.queryParserService().parse(parser).query(); - break; - default: - throw new ElasticsearchIllegalArgumentException("Unknown key for a " + token + " in [" + innerHitName + "]: [" + fieldName + "]."); - } - } else if (token == XContentParser.Token.START_ARRAY) { - switch (fieldName) { - case "fielddataFields": - case "fielddata_fields": - fieldDataFieldsParseElement.parse(parser, subSearchContext); - break; - default: - throw new ElasticsearchIllegalArgumentException("Unknown key for a " + token + " in [" + innerHitName + "]: [" + fieldName + "]."); - } - } else if (token.isValue()) { - switch (fieldName) { - case "query" : - query = context.queryParserService().parse(parser).query(); - break; - case "from": - subSearchContext.from(parser.intValue()); - break; - case "size": - subSearchContext.size(parser.intValue()); - break; - case "track_scores": - case "trackScores": - subSearchContext.trackScores(parser.booleanValue()); - break; - case "version": - subSearchContext.version(parser.booleanValue()); - break; - case "explain": - subSearchContext.explain(parser.booleanValue()); - break; - default: - throw new ElasticsearchIllegalArgumentException("Unknown key for a " + token + " in [" + innerHitName + "]: [" + fieldName + "]."); + if ("query".equals(fieldName)) { + query = context.queryParserService().parse(parser).query(); + } else if ("inner_hits".equals(fieldName)) { + childInnerHits = parseInnerHits(parser, context); + } else { + parseCommonInnerHitOptions(parser, token, fieldName, subSearchContext, sortParseElement, sourceParseElement, highlighterParseElement, scriptFieldsParseElement, fieldDataFieldsParseElement); } } else { - throw new ElasticsearchIllegalArgumentException("Unexpected token " + token + " in [" + innerHitName + "]."); + parseCommonInnerHitOptions(parser, token, fieldName, subSearchContext, sortParseElement, sourceParseElement, highlighterParseElement, scriptFieldsParseElement, fieldDataFieldsParseElement); } } @@ -224,14 +179,6 @@ public class InnerHitsParseElement implements SearchParseElement { throw new ElasticsearchIllegalArgumentException("path [" + nestedPath +"] isn't nested"); } DocumentMapper childDocumentMapper = smartNameObjectMapper.docMapper(); - if (childDocumentMapper == null) { - for (DocumentMapper documentMapper : context.mapperService().docMappers(false)) { - if (documentMapper.objectMappers().containsKey(nestedPath)) { - childDocumentMapper = documentMapper; - break; - } - } - } if (currentFilter != null && childDocumentMapper != null) { currentFilter.filter = context.bitsetFilterCache().getBitDocIdSetFilter(childObjectMapper.nestedTypeFilter()); NestedQueryParser.parentFilterContext.set(parentFilter); diff --git a/src/test/java/org/elasticsearch/search/innerhits/InnerHitsTests.java b/src/test/java/org/elasticsearch/search/innerhits/InnerHitsTests.java index 976b284612a..67b87d5225a 100644 --- a/src/test/java/org/elasticsearch/search/innerhits/InnerHitsTests.java +++ b/src/test/java/org/elasticsearch/search/innerhits/InnerHitsTests.java @@ -20,8 +20,11 @@ package org.elasticsearch.search.innerhits; import org.elasticsearch.action.index.IndexRequestBuilder; +import org.elasticsearch.action.search.SearchRequest; import org.elasticsearch.action.search.SearchResponse; import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.index.query.BoolQueryBuilder; +import org.elasticsearch.index.query.support.QueryInnerHitBuilder; import org.elasticsearch.search.SearchHit; import org.elasticsearch.search.SearchHits; import org.elasticsearch.search.fetch.innerhits.InnerHitsBuilder; @@ -34,11 +37,11 @@ import java.util.List; import java.util.Locale; import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder; +import static org.elasticsearch.index.query.FilterBuilders.hasChildFilter; +import static org.elasticsearch.index.query.FilterBuilders.nestedFilter; import static org.elasticsearch.index.query.QueryBuilders.*; import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.*; -import static org.hamcrest.Matchers.containsString; -import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.nullValue; +import static org.hamcrest.Matchers.*; /** */ @@ -79,63 +82,86 @@ public class InnerHitsTests extends ElasticsearchIntegrationTest { .endObject())); indexRandom(true, requests); - SearchResponse response = client().prepareSearch("articles") - .setQuery(nestedQuery("comments", matchQuery("comments.message", "fox"))) - .addInnerHit("comment", new InnerHitsBuilder.InnerHit().setPath("comments").setQuery(matchQuery("comments.message", "fox"))) - .get(); - assertNoFailures(response); - assertHitCount(response, 1); - assertSearchHit(response, 1, hasId("1")); - assertThat(response.getHits().getAt(0).getInnerHits().size(), equalTo(1)); - SearchHits innerHits = response.getHits().getAt(0).getInnerHits().get("comment"); - assertThat(innerHits.totalHits(), equalTo(2l)); - assertThat(innerHits.getHits().length, equalTo(2)); - assertThat(innerHits.getAt(0).getId(), equalTo("1")); - assertThat(innerHits.getAt(0).getNestedIdentity().getField().string(), equalTo("comments")); - assertThat(innerHits.getAt(0).getNestedIdentity().getOffset(), equalTo(0)); - assertThat(innerHits.getAt(1).getId(), equalTo("1")); - assertThat(innerHits.getAt(1).getNestedIdentity().getField().string(), equalTo("comments")); - assertThat(innerHits.getAt(1).getNestedIdentity().getOffset(), equalTo(1)); + // Inner hits can be defined in two ways: 1) with the query 2) as seperate inner_hit definition + SearchRequest[] searchRequests = new SearchRequest[]{ + client().prepareSearch("articles").setQuery(nestedQuery("comments", matchQuery("comments.message", "fox")).innerHit(new QueryInnerHitBuilder().setName("comment"))).request(), + client().prepareSearch("articles").setQuery(nestedQuery("comments", matchQuery("comments.message", "fox"))) + .addInnerHit("comment", new InnerHitsBuilder.InnerHit().setPath("comments").setQuery(matchQuery("comments.message", "fox"))).request() + }; + for (SearchRequest searchRequest : searchRequests) { + SearchResponse response = client().search(searchRequest).actionGet(); + assertNoFailures(response); + assertHitCount(response, 1); + assertSearchHit(response, 1, hasId("1")); + assertThat(response.getHits().getAt(0).getInnerHits().size(), equalTo(1)); + SearchHits innerHits = response.getHits().getAt(0).getInnerHits().get("comment"); + assertThat(innerHits.totalHits(), equalTo(2l)); + assertThat(innerHits.getHits().length, equalTo(2)); + assertThat(innerHits.getAt(0).getId(), equalTo("1")); + assertThat(innerHits.getAt(0).getNestedIdentity().getField().string(), equalTo("comments")); + assertThat(innerHits.getAt(0).getNestedIdentity().getOffset(), equalTo(0)); + assertThat(innerHits.getAt(1).getId(), equalTo("1")); + assertThat(innerHits.getAt(1).getNestedIdentity().getField().string(), equalTo("comments")); + assertThat(innerHits.getAt(1).getNestedIdentity().getOffset(), equalTo(1)); + } - response = client().prepareSearch("articles") - .setQuery(nestedQuery("comments", matchQuery("comments.message", "elephant"))) - .addInnerHit("comment", new InnerHitsBuilder.InnerHit().setPath("comments").setQuery(matchQuery("comments.message", "elephant"))) - .get(); - assertNoFailures(response); - assertHitCount(response, 1); - assertSearchHit(response, 1, hasId("2")); - assertThat(response.getHits().getAt(0).getInnerHits().size(), equalTo(1)); - innerHits = response.getHits().getAt(0).getInnerHits().get("comment"); - assertThat(innerHits.totalHits(), equalTo(3l)); - assertThat(innerHits.getHits().length, equalTo(3)); - assertThat(innerHits.getAt(0).getId(), equalTo("2")); - assertThat(innerHits.getAt(0).getNestedIdentity().getField().string(), equalTo("comments")); - assertThat(innerHits.getAt(0).getNestedIdentity().getOffset(), equalTo(0)); - assertThat(innerHits.getAt(1).getId(), equalTo("2")); - assertThat(innerHits.getAt(1).getNestedIdentity().getField().string(), equalTo("comments")); - assertThat(innerHits.getAt(1).getNestedIdentity().getOffset(), equalTo(1)); - assertThat(innerHits.getAt(2).getId(), equalTo("2")); - assertThat(innerHits.getAt(2).getNestedIdentity().getField().string(), equalTo("comments")); - assertThat(innerHits.getAt(2).getNestedIdentity().getOffset(), equalTo(2)); + searchRequests = new SearchRequest[] { + client().prepareSearch("articles") + .setQuery(nestedQuery("comments", matchQuery("comments.message", "elephant"))) + .addInnerHit("comment", new InnerHitsBuilder.InnerHit().setPath("comments").setQuery(matchQuery("comments.message", "elephant"))).request(), + client().prepareSearch("articles") + .setQuery(nestedQuery("comments", matchQuery("comments.message", "elephant")).innerHit(new QueryInnerHitBuilder().setName("comment"))).request() + }; + for (SearchRequest searchRequest : searchRequests) { + SearchResponse response = client().search(searchRequest).actionGet(); + assertNoFailures(response); + assertHitCount(response, 1); + assertSearchHit(response, 1, hasId("2")); + assertThat(response.getHits().getAt(0).getInnerHits().size(), equalTo(1)); + SearchHits innerHits = response.getHits().getAt(0).getInnerHits().get("comment"); + assertThat(innerHits.totalHits(), equalTo(3l)); + assertThat(innerHits.getHits().length, equalTo(3)); + assertThat(innerHits.getAt(0).getId(), equalTo("2")); + assertThat(innerHits.getAt(0).getNestedIdentity().getField().string(), equalTo("comments")); + assertThat(innerHits.getAt(0).getNestedIdentity().getOffset(), equalTo(0)); + assertThat(innerHits.getAt(1).getId(), equalTo("2")); + assertThat(innerHits.getAt(1).getNestedIdentity().getField().string(), equalTo("comments")); + assertThat(innerHits.getAt(1).getNestedIdentity().getOffset(), equalTo(1)); + assertThat(innerHits.getAt(2).getId(), equalTo("2")); + assertThat(innerHits.getAt(2).getNestedIdentity().getField().string(), equalTo("comments")); + assertThat(innerHits.getAt(2).getNestedIdentity().getOffset(), equalTo(2)); + } - response = client().prepareSearch("articles") - .setQuery(nestedQuery("comments", matchQuery("comments.message", "fox"))) - .addInnerHit("comment", new InnerHitsBuilder.InnerHit().setPath("comments") - .setQuery(matchQuery("comments.message", "fox")) - .addHighlightedField("comments.message") - .setExplain(true) - .addFieldDataField("comments.message") - .addScriptField("script", "doc['comments.message'].value") - .setSize(1) - ).get(); + searchRequests = new SearchRequest[] { + client().prepareSearch("articles") + .setQuery(nestedQuery("comments", matchQuery("comments.message", "fox"))) + .addInnerHit("comments", new InnerHitsBuilder.InnerHit().setPath("comments") + .setQuery(matchQuery("comments.message", "fox")) + .addHighlightedField("comments.message") + .setExplain(true) + .addFieldDataField("comments.message") + .addScriptField("script", "doc['comments.message'].value") + .setSize(1)).request(), + client().prepareSearch("articles") + .setQuery(nestedQuery("comments", matchQuery("comments.message", "fox")).innerHit(new QueryInnerHitBuilder() + .addHighlightedField("comments.message") + .setExplain(true) + .addFieldDataField("comments.message") + .addScriptField("script", "doc['comments.message'].value") + .setSize(1))).request() + }; - assertNoFailures(response); - innerHits = response.getHits().getAt(0).getInnerHits().get("comment"); - assertThat(innerHits.getHits().length, equalTo(1)); - assertThat(innerHits.getAt(0).getHighlightFields().get("comments.message").getFragments()[0].string(), equalTo("fox eat quick")); - assertThat(innerHits.getAt(0).explanation().toString(), containsString("(MATCH) weight(comments.message:fox in")); - assertThat(innerHits.getAt(0).getFields().get("comments.message").getValue().toString(), equalTo("eat")); - assertThat(innerHits.getAt(0).getFields().get("script").getValue().toString(), equalTo("eat")); + for (SearchRequest searchRequest : searchRequests) { + SearchResponse response = client().search(searchRequest).actionGet(); + assertNoFailures(response); + SearchHits innerHits = response.getHits().getAt(0).getInnerHits().get("comments"); + assertThat(innerHits.getTotalHits(), equalTo(2l)); + assertThat(innerHits.getHits().length, equalTo(1)); + assertThat(innerHits.getAt(0).getHighlightFields().get("comments.message").getFragments()[0].string(), equalTo("fox eat quick")); + assertThat(innerHits.getAt(0).explanation().toString(), containsString("(MATCH) weight(comments.message:fox in")); + assertThat(innerHits.getAt(0).getFields().get("comments.message").getValue().toString(), equalTo("eat")); + assertThat(innerHits.getAt(0).getFields().get("script").getValue().toString(), equalTo("eat")); + } } @Test @@ -147,29 +173,44 @@ public class InnerHitsTests extends ElasticsearchIntegrationTest { int[] field1InnerObjects = new int[numDocs]; int[] field2InnerObjects = new int[numDocs]; for (int i = 0; i < numDocs; i++) { - int numInnerObjects = field1InnerObjects[i] = scaledRandomIntBetween(0, numDocs); + int numInnerObjects = field1InnerObjects[i] = scaledRandomIntBetween(1, numDocs); XContentBuilder source = jsonBuilder().startObject().startArray("field1"); for (int j = 0; j < numInnerObjects; j++) { source.startObject().field("x", "y").endObject(); } - numInnerObjects = field2InnerObjects[i] = scaledRandomIntBetween(0, numDocs); + numInnerObjects = field2InnerObjects[i] = scaledRandomIntBetween(1, numDocs); source.endArray().startArray("field2"); for (int j = 0; j < numInnerObjects; j++) { source.startObject().field("x", "y").endObject(); } source.endArray().endObject(); - requestBuilders.add(client().prepareIndex("idx", "type", String.format(Locale.ENGLISH, "%03d", i)).setSource(source)); } - indexRandom(true, requestBuilders); - SearchResponse searchResponse = client().prepareSearch("idx") - .setSize(numDocs) - .addSort("_uid", SortOrder.ASC) - .addInnerHit("a", new InnerHitsBuilder.InnerHit().setPath("field1").addSort("_doc", SortOrder.DESC).setSize(numDocs)) // Sort order is DESC, because we reverse the inner objects during indexing! - .addInnerHit("b", new InnerHitsBuilder.InnerHit().setPath("field2").addSort("_doc", SortOrder.DESC).setSize(numDocs)) - .get(); + SearchResponse searchResponse; + if (randomBoolean()) { + searchResponse = client().prepareSearch("idx") + .setSize(numDocs) + .addSort("_uid", SortOrder.ASC) + .addInnerHit("a", new InnerHitsBuilder.InnerHit().setPath("field1").addSort("_doc", SortOrder.DESC).setSize(numDocs)) // Sort order is DESC, because we reverse the inner objects during indexing! + .addInnerHit("b", new InnerHitsBuilder.InnerHit().setPath("field2").addSort("_doc", SortOrder.DESC).setSize(numDocs)) + .get(); + } else { + BoolQueryBuilder boolQuery = new BoolQueryBuilder(); + if (randomBoolean()) { + boolQuery.should(nestedQuery("field1", matchAllQuery()).innerHit(new QueryInnerHitBuilder().setName("a").addSort("_doc", SortOrder.DESC).setSize(numDocs))); + boolQuery.should(nestedQuery("field2", matchAllQuery()).innerHit(new QueryInnerHitBuilder().setName("b").addSort("_doc", SortOrder.DESC).setSize(numDocs))); + } else { + boolQuery.should(constantScoreQuery(nestedFilter("field1", matchAllQuery()).innerHit(new QueryInnerHitBuilder().setName("a").addSort("_doc", SortOrder.DESC).setSize(numDocs)))); + boolQuery.should(constantScoreQuery(nestedFilter("field2", matchAllQuery()).innerHit(new QueryInnerHitBuilder().setName("b").addSort("_doc", SortOrder.DESC).setSize(numDocs)))); + } + searchResponse = client().prepareSearch("idx") + .setQuery(boolQuery) + .setSize(numDocs) + .addSort("_uid", SortOrder.ASC) + .get(); + } assertHitCount(searchResponse, numDocs); assertThat(searchResponse.getHits().getHits().length, equalTo(numDocs)); @@ -213,62 +254,89 @@ public class InnerHitsTests extends ElasticsearchIntegrationTest { requests.add(client().prepareIndex("articles", "comment", "6").setParent("2").setSource("message", "elephant scared by mice x y")); indexRandom(true, requests); - SearchResponse response = client().prepareSearch("articles") - .setQuery(hasChildQuery("comment", matchQuery("message", "fox"))) - .addInnerHit("comment", new InnerHitsBuilder.InnerHit().setType("comment").setQuery(matchQuery("message", "fox"))) - .get(); + SearchRequest[] searchRequests = new SearchRequest[]{ + client().prepareSearch("articles") + .setQuery(hasChildQuery("comment", matchQuery("message", "fox"))) + .addInnerHit("comment", new InnerHitsBuilder.InnerHit().setType("comment").setQuery(matchQuery("message", "fox"))) + .request(), + client().prepareSearch("articles") + .setQuery(hasChildQuery("comment", matchQuery("message", "fox")).innerHit(new QueryInnerHitBuilder().setName("comment"))) + .request() + }; + for (SearchRequest searchRequest : searchRequests) { + SearchResponse response = client().search(searchRequest).actionGet(); + assertNoFailures(response); + assertHitCount(response, 1); + assertSearchHit(response, 1, hasId("1")); - assertNoFailures(response); - assertHitCount(response, 1); - assertSearchHit(response, 1, hasId("1")); + assertThat(response.getHits().getAt(0).getInnerHits().size(), equalTo(1)); + SearchHits innerHits = response.getHits().getAt(0).getInnerHits().get("comment"); + assertThat(innerHits.totalHits(), equalTo(2l)); - assertThat(response.getHits().getAt(0).getInnerHits().size(), equalTo(1)); - SearchHits innerHits = response.getHits().getAt(0).getInnerHits().get("comment"); - assertThat(innerHits.totalHits(), equalTo(2l)); + assertThat(innerHits.getAt(0).getId(), equalTo("1")); + assertThat(innerHits.getAt(0).type(), equalTo("comment")); + assertThat(innerHits.getAt(1).getId(), equalTo("2")); + assertThat(innerHits.getAt(1).type(), equalTo("comment")); + } - assertThat(innerHits.getAt(0).getId(), equalTo("1")); - assertThat(innerHits.getAt(0).type(), equalTo("comment")); - assertThat(innerHits.getAt(1).getId(), equalTo("2")); - assertThat(innerHits.getAt(1).type(), equalTo("comment")); + searchRequests = new SearchRequest[] { + client().prepareSearch("articles") + .setQuery(hasChildQuery("comment", matchQuery("message", "elephant"))) + .addInnerHit("comment", new InnerHitsBuilder.InnerHit().setType("comment").setQuery(matchQuery("message", "elephant"))) + .request(), + client().prepareSearch("articles") + .setQuery(hasChildQuery("comment", matchQuery("message", "elephant")).innerHit(new QueryInnerHitBuilder())) + .request() + }; + for (SearchRequest searchRequest : searchRequests) { + SearchResponse response = client().search(searchRequest).actionGet(); + assertNoFailures(response); + assertHitCount(response, 1); + assertSearchHit(response, 1, hasId("2")); - response = client().prepareSearch("articles") - .setQuery(hasChildQuery("comment", matchQuery("message", "elephant"))) - .addInnerHit("comment", new InnerHitsBuilder.InnerHit().setType("comment").setQuery(matchQuery("message", "elephant"))) - .get(); + assertThat(response.getHits().getAt(0).getInnerHits().size(), equalTo(1)); + SearchHits innerHits = response.getHits().getAt(0).getInnerHits().get("comment"); + assertThat(innerHits.totalHits(), equalTo(3l)); - assertNoFailures(response); - assertHitCount(response, 1); - assertSearchHit(response, 1, hasId("2")); + assertThat(innerHits.getAt(0).getId(), equalTo("4")); + assertThat(innerHits.getAt(0).type(), equalTo("comment")); + assertThat(innerHits.getAt(1).getId(), equalTo("5")); + assertThat(innerHits.getAt(1).type(), equalTo("comment")); + assertThat(innerHits.getAt(2).getId(), equalTo("6")); + assertThat(innerHits.getAt(2).type(), equalTo("comment")); + } - assertThat(response.getHits().getAt(0).getInnerHits().size(), equalTo(1)); - innerHits = response.getHits().getAt(0).getInnerHits().get("comment"); - assertThat(innerHits.totalHits(), equalTo(3l)); + searchRequests = new SearchRequest[] { + client().prepareSearch("articles") + .setQuery(hasChildQuery("comment", matchQuery("message", "fox"))) + .addInnerHit("comment", new InnerHitsBuilder.InnerHit().setType("comment") + .setQuery(matchQuery("message", "fox")) + .addHighlightedField("message") + .setExplain(true) + .addFieldDataField("message") + .addScriptField("script", "doc['message'].value") + .setSize(1) + ).request(), + client().prepareSearch("articles") + .setQuery(hasChildQuery("comment", matchQuery("message", "fox")).innerHit(new QueryInnerHitBuilder() + .addHighlightedField("message") + .setExplain(true) + .addFieldDataField("message") + .addScriptField("script", "doc['message'].value") + .setSize(1)) + ).request() + }; - assertThat(innerHits.getAt(0).getId(), equalTo("4")); - assertThat(innerHits.getAt(0).type(), equalTo("comment")); - assertThat(innerHits.getAt(1).getId(), equalTo("5")); - assertThat(innerHits.getAt(1).type(), equalTo("comment")); - assertThat(innerHits.getAt(2).getId(), equalTo("6")); - assertThat(innerHits.getAt(2).type(), equalTo("comment")); - - response = client().prepareSearch("articles") - .setQuery(hasChildQuery("comment", matchQuery("message", "fox"))) - .addInnerHit("comment", new InnerHitsBuilder.InnerHit().setType("comment") - .setQuery(matchQuery("message", "fox")) - .addHighlightedField("message") - .setExplain(true) - .addFieldDataField("message") - .addScriptField("script", "doc['message'].value") - .setSize(1) - ).get(); - - assertNoFailures(response); - innerHits = response.getHits().getAt(0).getInnerHits().get("comment"); - assertThat(innerHits.getHits().length, equalTo(1)); - assertThat(innerHits.getAt(0).getHighlightFields().get("message").getFragments()[0].string(), equalTo("fox eat quick")); - assertThat(innerHits.getAt(0).explanation().toString(), containsString("(MATCH) weight(message:fox")); - assertThat(innerHits.getAt(0).getFields().get("message").getValue().toString(), equalTo("eat")); - assertThat(innerHits.getAt(0).getFields().get("script").getValue().toString(), equalTo("eat")); + for (SearchRequest searchRequest : searchRequests) { + SearchResponse response = client().search(searchRequest).actionGet(); + assertNoFailures(response); + SearchHits innerHits = response.getHits().getAt(0).getInnerHits().get("comment"); + assertThat(innerHits.getHits().length, equalTo(1)); + assertThat(innerHits.getAt(0).getHighlightFields().get("message").getFragments()[0].string(), equalTo("fox eat quick")); + assertThat(innerHits.getAt(0).explanation().toString(), containsString("(MATCH) weight(message:fox")); + assertThat(innerHits.getAt(0).getFields().get("message").getValue().toString(), equalTo("eat")); + assertThat(innerHits.getAt(0).getFields().get("script").getValue().toString(), equalTo("eat")); + } } @Test @@ -289,12 +357,12 @@ public class InnerHitsTests extends ElasticsearchIntegrationTest { String parentId = String.format(Locale.ENGLISH, "%03d", parent); requestBuilders.add(client().prepareIndex("idx", "parent", parentId).setSource("{}")); - int numChildDocs = child1InnerObjects[parent] = scaledRandomIntBetween(0, numDocs); + int numChildDocs = child1InnerObjects[parent] = scaledRandomIntBetween(1, numDocs); int limit = child1 + numChildDocs; for (; child1 < limit; child1++) { requestBuilders.add(client().prepareIndex("idx", "child1", String.format(Locale.ENGLISH, "%04d", child1)).setParent(parentId).setSource("{}")); } - numChildDocs = child2InnerObjects[parent] = scaledRandomIntBetween(0, numDocs); + numChildDocs = child2InnerObjects[parent] = scaledRandomIntBetween(1, numDocs); limit = child2 + numChildDocs; for (; child2 < limit; child2++) { requestBuilders.add(client().prepareIndex("idx", "child2", String.format(Locale.ENGLISH, "%04d", child2)).setParent(parentId).setSource("{}")); @@ -302,13 +370,31 @@ public class InnerHitsTests extends ElasticsearchIntegrationTest { } indexRandom(true, requestBuilders); - SearchResponse searchResponse = client().prepareSearch("idx") - .setSize(numDocs) - .setTypes("parent") - .addSort("_uid", SortOrder.ASC) - .addInnerHit("a", new InnerHitsBuilder.InnerHit().setType("child1").addSort("_uid", SortOrder.ASC).setSize(numDocs)) - .addInnerHit("b", new InnerHitsBuilder.InnerHit().setType("child2").addSort("_uid", SortOrder.ASC).setSize(numDocs)) - .get(); + SearchResponse searchResponse; + if (randomBoolean()) { + searchResponse = client().prepareSearch("idx") + .setSize(numDocs) + .setTypes("parent") + .addSort("_uid", SortOrder.ASC) + .addInnerHit("a", new InnerHitsBuilder.InnerHit().setType("child1").addSort("_uid", SortOrder.ASC).setSize(numDocs)) + .addInnerHit("b", new InnerHitsBuilder.InnerHit().setType("child2").addSort("_uid", SortOrder.ASC).setSize(numDocs)) + .get(); + } else { + BoolQueryBuilder boolQuery = new BoolQueryBuilder(); + if (randomBoolean()) { + boolQuery.should(hasChildQuery("child1", matchAllQuery()).innerHit(new QueryInnerHitBuilder().setName("a").addSort("_uid", SortOrder.ASC).setSize(numDocs))); + boolQuery.should(hasChildQuery("child2", matchAllQuery()).innerHit(new QueryInnerHitBuilder().setName("b").addSort("_uid", SortOrder.ASC).setSize(numDocs))); + } else { + boolQuery.should(constantScoreQuery(hasChildFilter("child1", matchAllQuery()).innerHit(new QueryInnerHitBuilder().setName("a").addSort("_uid", SortOrder.ASC).setSize(numDocs)))); + boolQuery.should(constantScoreQuery(hasChildFilter("child2", matchAllQuery()).innerHit(new QueryInnerHitBuilder().setName("b").addSort("_uid", SortOrder.ASC).setSize(numDocs)))); + } + searchResponse = client().prepareSearch("idx") + .setSize(numDocs) + .setTypes("parent") + .addSort("_uid", SortOrder.ASC) + .setQuery(boolQuery) + .get(); + } assertHitCount(searchResponse, numDocs); assertThat(searchResponse.getHits().getHits().length, equalTo(numDocs));