From 7e2696c57017a53b1dab3204228dce23857d899e Mon Sep 17 00:00:00 2001 From: Martijn van Groningen Date: Wed, 23 Mar 2016 15:21:35 +0100 Subject: [PATCH] Refactored inner hits parsing and intoduced InnerHitBuilder Both top level and inline inner hits are now covered by InnerHitBuilder. Although there are differences between top level and inline inner hits, they now make use of the same builder logic. The parsing of top level inner hits slightly changed to be more readable. Before the nested path or parent/child type had to be specified as encapsuting json object, now these settings are simple fields. Before this was required to allow streaming parsing of inner hits without missing contextual information. Once some issues are fixed with inline inner hits (around multi level hierachy of inner hits), top level inner hits will be deprecated and removed in the next major version. --- .../action/search/SearchRequestBuilder.java | 2 +- .../index/query/HasChildQueryBuilder.java | 66 +- .../index/query/HasChildQueryParser.java | 8 +- .../index/query/HasParentQueryBuilder.java | 47 +- .../index/query/HasParentQueryParser.java | 9 +- .../index/query/NestedQueryBuilder.java | 79 +-- .../index/query/NestedQueryParser.java | 8 +- .../index/query/QueryShardContext.java | 12 +- .../index/query/support/InnerHitBuilder.java | 597 ++++++++++++++++++ .../index/query/support/InnerHitsBuilder.java | 129 ++++ .../support/InnerHitsQueryParserHelper.java | 133 ---- .../index/query/support/QueryInnerHits.java | 113 ---- .../elasticsearch/search/SearchService.java | 28 +- .../tophits/TopHitsAggregatorFactory.java | 1 + .../search/builder/SearchSourceBuilder.java | 112 ++-- .../fetch/innerhits/InnerHitsBuilder.java | 305 --------- .../fetch/innerhits/InnerHitsContext.java | 65 +- .../innerhits/InnerHitsFetchSubPhase.java | 16 +- .../innerhits/InnerHitsParseElement.java | 221 ------- .../innerhits/InnerHitsSubSearchContext.java | 40 -- .../search/internal/SubSearchContext.java | 21 + .../query/HasChildQueryBuilderTests.java | 43 +- .../query/HasParentQueryBuilderTests.java | 17 +- .../index/query/NestedQueryBuilderTests.java | 10 +- .../query/support/InnerHitBuilderTests.java | 286 +++++++++ .../query/support/InnerHitsBuilderTests.java | 157 +++++ .../query/support/QueryInnerHitsTests.java | 79 --- .../percolator/PercolatorIT.java | 4 +- .../builder/SearchSourceBuilderTests.java | 12 +- .../search/innerhits/InnerHitsIT.java | 212 ++++--- .../migration/migrate_5_0/search.asciidoc | 5 + .../search/request/inner-hits.asciidoc | 13 +- 32 files changed, 1566 insertions(+), 1284 deletions(-) create mode 100644 core/src/main/java/org/elasticsearch/index/query/support/InnerHitBuilder.java create mode 100644 core/src/main/java/org/elasticsearch/index/query/support/InnerHitsBuilder.java delete mode 100644 core/src/main/java/org/elasticsearch/index/query/support/InnerHitsQueryParserHelper.java delete mode 100644 core/src/main/java/org/elasticsearch/index/query/support/QueryInnerHits.java delete mode 100644 core/src/main/java/org/elasticsearch/search/fetch/innerhits/InnerHitsBuilder.java delete mode 100644 core/src/main/java/org/elasticsearch/search/fetch/innerhits/InnerHitsParseElement.java delete mode 100644 core/src/main/java/org/elasticsearch/search/fetch/innerhits/InnerHitsSubSearchContext.java create mode 100644 core/src/test/java/org/elasticsearch/index/query/support/InnerHitBuilderTests.java create mode 100644 core/src/test/java/org/elasticsearch/index/query/support/InnerHitsBuilderTests.java delete mode 100644 core/src/test/java/org/elasticsearch/index/query/support/QueryInnerHitsTests.java diff --git a/core/src/main/java/org/elasticsearch/action/search/SearchRequestBuilder.java b/core/src/main/java/org/elasticsearch/action/search/SearchRequestBuilder.java index 931df24a256..2d3d9553090 100644 --- a/core/src/main/java/org/elasticsearch/action/search/SearchRequestBuilder.java +++ b/core/src/main/java/org/elasticsearch/action/search/SearchRequestBuilder.java @@ -31,7 +31,7 @@ import org.elasticsearch.search.Scroll; import org.elasticsearch.search.aggregations.AggregatorBuilder; import org.elasticsearch.search.aggregations.pipeline.PipelineAggregatorBuilder; import org.elasticsearch.search.builder.SearchSourceBuilder; -import org.elasticsearch.search.fetch.innerhits.InnerHitsBuilder; +import org.elasticsearch.index.query.support.InnerHitsBuilder; import org.elasticsearch.search.highlight.HighlightBuilder; import org.elasticsearch.search.rescore.RescoreBuilder; import org.elasticsearch.search.sort.SortBuilder; diff --git a/core/src/main/java/org/elasticsearch/index/query/HasChildQueryBuilder.java b/core/src/main/java/org/elasticsearch/index/query/HasChildQueryBuilder.java index 324552d7b54..a58f954ea18 100644 --- a/core/src/main/java/org/elasticsearch/index/query/HasChildQueryBuilder.java +++ b/core/src/main/java/org/elasticsearch/index/query/HasChildQueryBuilder.java @@ -31,17 +31,13 @@ import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.lucene.search.Queries; import org.elasticsearch.common.xcontent.XContentBuilder; -import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.index.fielddata.IndexParentChildFieldData; 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.QueryInnerHits; -import org.elasticsearch.search.fetch.innerhits.InnerHitsContext; -import org.elasticsearch.search.fetch.innerhits.InnerHitsSubSearchContext; +import org.elasticsearch.index.query.support.InnerHitBuilder; import java.io.IOException; -import java.util.Locale; import java.util.Objects; /** @@ -77,16 +73,20 @@ public class HasChildQueryBuilder extends AbstractQueryBuilder { int minChildren = HasChildQueryBuilder.DEFAULT_MIN_CHILDREN; int maxChildren = HasChildQueryBuilder.DEFAULT_MAX_CHILDREN; String queryName = null; - QueryInnerHits queryInnerHits = null; + InnerHitBuilder innerHitBuilder = null; String currentFieldName = null; XContentParser.Token token; QueryBuilder iqb = null; @@ -68,7 +68,7 @@ public class HasChildQueryParser implements QueryParser { if (parseContext.parseFieldMatcher().match(currentFieldName, QUERY_FIELD)) { iqb = parseContext.parseInnerQueryBuilder(); } else if (parseContext.parseFieldMatcher().match(currentFieldName, INNER_HITS_FIELD)) { - queryInnerHits = new QueryInnerHits(parser); + innerHitBuilder = InnerHitBuilder.fromXContent(parser, parseContext); } else { throw new ParsingException(parser.getTokenLocation(), "[has_child] query does not support [" + currentFieldName + "]"); } @@ -90,7 +90,7 @@ public class HasChildQueryParser implements QueryParser { } } } - HasChildQueryBuilder hasChildQueryBuilder = new HasChildQueryBuilder(childType, iqb, maxChildren, minChildren, scoreMode, queryInnerHits); + HasChildQueryBuilder hasChildQueryBuilder = new HasChildQueryBuilder(childType, iqb, maxChildren, minChildren, scoreMode, innerHitBuilder); hasChildQueryBuilder.queryName(queryName); hasChildQueryBuilder.boost(boost); return hasChildQueryBuilder; diff --git a/core/src/main/java/org/elasticsearch/index/query/HasParentQueryBuilder.java b/core/src/main/java/org/elasticsearch/index/query/HasParentQueryBuilder.java index da26fa90b35..0b24130b706 100644 --- a/core/src/main/java/org/elasticsearch/index/query/HasParentQueryBuilder.java +++ b/core/src/main/java/org/elasticsearch/index/query/HasParentQueryBuilder.java @@ -26,13 +26,10 @@ import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.lucene.search.Queries; import org.elasticsearch.common.xcontent.XContentBuilder; -import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.index.fielddata.plain.ParentChildIndexFieldData; import org.elasticsearch.index.mapper.DocumentMapper; import org.elasticsearch.index.mapper.internal.ParentFieldMapper; -import org.elasticsearch.index.query.support.QueryInnerHits; -import org.elasticsearch.search.fetch.innerhits.InnerHitsContext; -import org.elasticsearch.search.fetch.innerhits.InnerHitsSubSearchContext; +import org.elasticsearch.index.query.support.InnerHitBuilder; import java.io.IOException; import java.util.HashSet; @@ -49,7 +46,7 @@ public class HasParentQueryBuilder extends AbstractQueryBuilder childTypes = new HashSet<>(); @@ -200,7 +191,7 @@ public class HasParentQueryBuilder extends AbstractQueryBuilder public static final ParseField SCORE_MODE_FIELD = new ParseField("score_mode").withAllDeprecated("score"); public static final ParseField TYPE_FIELD = new ParseField("parent_type", "type"); public static final ParseField SCORE_FIELD = new ParseField("score"); + public static final ParseField INNER_HITS_FIELD = new ParseField("inner_hits"); @Override public String[] names() { @@ -48,7 +49,7 @@ public class HasParentQueryParser implements QueryParser String parentType = null; boolean score = HasParentQueryBuilder.DEFAULT_SCORE; String queryName = null; - QueryInnerHits innerHits = null; + InnerHitBuilder innerHits = null; String currentFieldName = null; XContentParser.Token token; @@ -59,8 +60,8 @@ public class HasParentQueryParser implements QueryParser } else if (token == XContentParser.Token.START_OBJECT) { if (parseContext.parseFieldMatcher().match(currentFieldName, QUERY_FIELD)) { iqb = parseContext.parseInnerQueryBuilder(); - } else if ("inner_hits".equals(currentFieldName)) { - innerHits = new QueryInnerHits(parser); + } else if (parseContext.parseFieldMatcher().match(currentFieldName, INNER_HITS_FIELD)) { + innerHits = InnerHitBuilder.fromXContent(parser, parseContext); } else { throw new ParsingException(parser.getTokenLocation(), "[has_parent] query does not support [" + currentFieldName + "]"); } diff --git a/core/src/main/java/org/elasticsearch/index/query/NestedQueryBuilder.java b/core/src/main/java/org/elasticsearch/index/query/NestedQueryBuilder.java index bd5f348db33..8132d2027cc 100644 --- a/core/src/main/java/org/elasticsearch/index/query/NestedQueryBuilder.java +++ b/core/src/main/java/org/elasticsearch/index/query/NestedQueryBuilder.java @@ -27,14 +27,10 @@ import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.lucene.search.Queries; import org.elasticsearch.common.xcontent.XContentBuilder; -import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.index.mapper.object.ObjectMapper; -import org.elasticsearch.index.query.support.QueryInnerHits; -import org.elasticsearch.search.fetch.innerhits.InnerHitsContext; -import org.elasticsearch.search.fetch.innerhits.InnerHitsSubSearchContext; +import org.elasticsearch.index.query.support.InnerHitBuilder; import java.io.IOException; -import java.util.Locale; import java.util.Objects; public class NestedQueryBuilder extends AbstractQueryBuilder { @@ -55,7 +51,7 @@ public class NestedQueryBuilder extends AbstractQueryBuilder private ScoreMode scoreMode = DEFAULT_SCORE_MODE; - private QueryInnerHits queryInnerHits; + private InnerHitBuilder innerHitBuilder; public NestedQueryBuilder(String path, QueryBuilder query) { if (path == null) { @@ -68,10 +64,14 @@ public class NestedQueryBuilder extends AbstractQueryBuilder this.query = query; } - public NestedQueryBuilder(String path, QueryBuilder query, ScoreMode scoreMode, QueryInnerHits queryInnerHits) { + public NestedQueryBuilder(String path, QueryBuilder query, ScoreMode scoreMode, InnerHitBuilder innerHitBuilder) { this(path, query); scoreMode(scoreMode); - this.queryInnerHits = queryInnerHits; + this.innerHitBuilder = innerHitBuilder; + if (this.innerHitBuilder != null) { + this.innerHitBuilder.setNestedPath(path); + this.innerHitBuilder.setQuery(query); + } } /** @@ -88,8 +88,10 @@ public class NestedQueryBuilder extends AbstractQueryBuilder /** * Sets inner hit definition in the scope of this nested query and reusing the defined path and query. */ - public NestedQueryBuilder innerHit(QueryInnerHits innerHit) { - this.queryInnerHits = innerHit; + public NestedQueryBuilder innerHit(InnerHitBuilder innerHit) { + this.innerHitBuilder = Objects.requireNonNull(innerHit); + this.innerHitBuilder.setNestedPath(path); + this.innerHitBuilder.setQuery(query); return this; } @@ -103,8 +105,8 @@ public class NestedQueryBuilder extends AbstractQueryBuilder /** * Returns inner hit definition in the scope of this query and reusing the defined type and query. */ - public QueryInnerHits innerHit() { - return queryInnerHits; + public InnerHitBuilder innerHit() { + return innerHitBuilder; } /** @@ -124,8 +126,8 @@ public class NestedQueryBuilder extends AbstractQueryBuilder builder.field(NestedQueryParser.SCORE_MODE_FIELD.getPreferredName(), HasChildQueryParser.scoreModeAsString(scoreMode)); } printBoostAndQueryName(builder); - if (queryInnerHits != null) { - queryInnerHits.toXContent(builder, params); + if (innerHitBuilder != null) { + builder.field(NestedQueryParser.INNER_HITS_FIELD.getPreferredName(), innerHitBuilder, params); } builder.endObject(); } @@ -140,12 +142,12 @@ public class NestedQueryBuilder extends AbstractQueryBuilder return Objects.equals(query, that.query) && Objects.equals(path, that.path) && Objects.equals(scoreMode, that.scoreMode) - && Objects.equals(queryInnerHits, that.queryInnerHits); + && Objects.equals(innerHitBuilder, that.innerHitBuilder); } @Override protected int doHashCode() { - return Objects.hash(query, path, scoreMode, queryInnerHits); + return Objects.hash(query, path, scoreMode, innerHitBuilder); } private NestedQueryBuilder(StreamInput in) throws IOException { @@ -153,9 +155,7 @@ public class NestedQueryBuilder extends AbstractQueryBuilder final int ordinal = in.readVInt(); scoreMode = ScoreMode.values()[ordinal]; query = in.readQuery(); - if (in.readBoolean()) { - queryInnerHits = new QueryInnerHits(in); - } + innerHitBuilder = InnerHitBuilder.optionalReadFromStream(in); } @Override @@ -163,9 +163,9 @@ public class NestedQueryBuilder extends AbstractQueryBuilder out.writeString(path); out.writeVInt(scoreMode.ordinal()); out.writeQuery(query); - if (queryInnerHits != null) { + if (innerHitBuilder != null) { out.writeBoolean(true); - queryInnerHits.writeTo(out); + innerHitBuilder.writeTo(out); } else { out.writeBoolean(false); } @@ -187,17 +187,19 @@ public class NestedQueryBuilder extends AbstractQueryBuilder } final BitSetProducer parentFilter; final Query childFilter; - final ObjectMapper parentObjectMapper; final Query innerQuery; ObjectMapper objectMapper = context.nestedScope().getObjectMapper(); + if (innerHitBuilder != null) { + context.addInnerHit(innerHitBuilder); + } + if (objectMapper == null) { + parentFilter = context.bitsetFilter(Queries.newNonNestedFilter()); + } else { + parentFilter = context.bitsetFilter(objectMapper.nestedTypeFilter()); + } + childFilter = nestedObjectMapper.nestedTypeFilter(); try { - if (objectMapper == null) { - parentFilter = context.bitsetFilter(Queries.newNonNestedFilter()); - } else { - parentFilter = context.bitsetFilter(objectMapper.nestedTypeFilter()); - } - childFilter = nestedObjectMapper.nestedTypeFilter(); - parentObjectMapper = context.nestedScope().nextLevel(nestedObjectMapper); + context.nestedScope().nextLevel(nestedObjectMapper); innerQuery = this.query.toQuery(context); if (innerQuery == null) { return null; @@ -205,23 +207,6 @@ public class NestedQueryBuilder extends AbstractQueryBuilder } finally { context.nestedScope().previousLevel(); } - - if (queryInnerHits != null) { - try (XContentParser parser = queryInnerHits.getXcontentParser()) { - XContentParser.Token token = parser.nextToken(); - if (token != XContentParser.Token.START_OBJECT) { - throw new IllegalStateException("start object expected but was: [" + token + "]"); - } - InnerHitsSubSearchContext innerHits = context.getInnerHitsContext(parser); - if (innerHits != null) { - ParsedQuery parsedQuery = new ParsedQuery(innerQuery, context.copyNamedQueries()); - - InnerHitsContext.NestedInnerHits nestedInnerHits = new InnerHitsContext.NestedInnerHits(innerHits.getSubSearchContext(), parsedQuery, null, parentObjectMapper, nestedObjectMapper); - String name = innerHits.getName() != null ? innerHits.getName() : path; - context.addInnerHits(name, nestedInnerHits); - } - } - } return new ToParentBlockJoinQuery(Queries.filtered(innerQuery, childFilter), parentFilter, scoreMode); } @@ -229,7 +214,7 @@ public class NestedQueryBuilder extends AbstractQueryBuilder protected QueryBuilder doRewrite(QueryRewriteContext queryRewriteContext) throws IOException { QueryBuilder rewrite = query.rewrite(queryRewriteContext); if (rewrite != query) { - return new NestedQueryBuilder(path, rewrite).scoreMode(scoreMode); + return new NestedQueryBuilder(path, rewrite).scoreMode(scoreMode).innerHit(innerHitBuilder); } return this; } diff --git a/core/src/main/java/org/elasticsearch/index/query/NestedQueryParser.java b/core/src/main/java/org/elasticsearch/index/query/NestedQueryParser.java index 218919f7ed2..0fb7649cf7b 100644 --- a/core/src/main/java/org/elasticsearch/index/query/NestedQueryParser.java +++ b/core/src/main/java/org/elasticsearch/index/query/NestedQueryParser.java @@ -25,7 +25,7 @@ import org.apache.lucene.search.join.ScoreMode; import org.elasticsearch.common.ParseField; import org.elasticsearch.common.ParsingException; import org.elasticsearch.common.xcontent.XContentParser; -import org.elasticsearch.index.query.support.QueryInnerHits; +import org.elasticsearch.index.query.support.InnerHitBuilder; public class NestedQueryParser implements QueryParser { @@ -49,7 +49,7 @@ public class NestedQueryParser implements QueryParser { QueryBuilder query = null; String path = null; String currentFieldName = null; - QueryInnerHits queryInnerHits = null; + InnerHitBuilder innerHitBuilder = null; XContentParser.Token token; while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { if (token == XContentParser.Token.FIELD_NAME) { @@ -58,7 +58,7 @@ public class NestedQueryParser implements QueryParser { if (parseContext.parseFieldMatcher().match(currentFieldName, QUERY_FIELD)) { query = parseContext.parseInnerQueryBuilder(); } else if (parseContext.parseFieldMatcher().match(currentFieldName, INNER_HITS_FIELD)) { - queryInnerHits = new QueryInnerHits(parser); + innerHitBuilder = InnerHitBuilder.fromXContent(parser, parseContext); } else { throw new ParsingException(parser.getTokenLocation(), "[nested] query does not support [" + currentFieldName + "]"); } @@ -76,7 +76,7 @@ public class NestedQueryParser implements QueryParser { } } } - return new NestedQueryBuilder(path, query, scoreMode, queryInnerHits).queryName(queryName).boost(boost); + return new NestedQueryBuilder(path, query, scoreMode, innerHitBuilder).queryName(queryName).boost(boost); } @Override diff --git a/core/src/main/java/org/elasticsearch/index/query/QueryShardContext.java b/core/src/main/java/org/elasticsearch/index/query/QueryShardContext.java index 63eff82ddb0..77aa1dc0c36 100644 --- a/core/src/main/java/org/elasticsearch/index/query/QueryShardContext.java +++ b/core/src/main/java/org/elasticsearch/index/query/QueryShardContext.java @@ -47,13 +47,13 @@ import org.elasticsearch.index.mapper.MapperService; import org.elasticsearch.index.mapper.core.TextFieldMapper; import org.elasticsearch.index.mapper.object.ObjectMapper; import org.elasticsearch.index.percolator.PercolatorQueryCache; -import org.elasticsearch.index.query.support.InnerHitsQueryParserHelper; +import org.elasticsearch.index.query.support.InnerHitBuilder; +import org.elasticsearch.index.query.support.InnerHitsBuilder; import org.elasticsearch.index.query.support.NestedScope; import org.elasticsearch.index.similarity.SimilarityService; import org.elasticsearch.indices.query.IndicesQueriesRegistry; import org.elasticsearch.script.ScriptService; import org.elasticsearch.search.fetch.innerhits.InnerHitsContext; -import org.elasticsearch.search.fetch.innerhits.InnerHitsSubSearchContext; import org.elasticsearch.search.internal.SearchContext; import org.elasticsearch.search.lookup.SearchLookup; @@ -141,10 +141,6 @@ public class QueryShardContext extends QueryRewriteContext { this.parseContext.reset(jp); } - public InnerHitsSubSearchContext getInnerHitsContext(XContentParser parser) throws IOException { - return InnerHitsQueryParserHelper.parse(parser); - } - public AnalysisService getAnalysisService() { return mapperService.analysisService(); } @@ -208,14 +204,14 @@ public class QueryShardContext extends QueryRewriteContext { return isFilter; } - public void addInnerHits(String name, InnerHitsContext.BaseInnerHits context) { + public void addInnerHit(InnerHitBuilder innerHitBuilder) throws IOException { SearchContext sc = SearchContext.current(); if (sc == null) { throw new QueryShardException(this, "inner_hits unsupported"); } InnerHitsContext innerHitsContext = sc.innerHits(); - innerHitsContext.addInnerHitDefinition(name, context); + innerHitsContext.addInnerHitDefinition(innerHitBuilder.buildInline(sc, this)); } public Collection simpleMatchToIndexNames(String pattern) { diff --git a/core/src/main/java/org/elasticsearch/index/query/support/InnerHitBuilder.java b/core/src/main/java/org/elasticsearch/index/query/support/InnerHitBuilder.java new file mode 100644 index 00000000000..2579a9171c3 --- /dev/null +++ b/core/src/main/java/org/elasticsearch/index/query/support/InnerHitBuilder.java @@ -0,0 +1,597 @@ +/* + * 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.apache.lucene.search.Sort; +import org.elasticsearch.action.support.ToXContentToBytes; +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.ParsingException; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.io.stream.Writeable; +import org.elasticsearch.common.xcontent.ObjectParser; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.index.mapper.DocumentMapper; +import org.elasticsearch.index.mapper.object.ObjectMapper; +import org.elasticsearch.index.query.MatchAllQueryBuilder; +import org.elasticsearch.index.query.ParsedQuery; +import org.elasticsearch.index.query.QueryBuilder; +import org.elasticsearch.index.query.QueryParseContext; +import org.elasticsearch.index.query.QueryShardContext; +import org.elasticsearch.script.Script; +import org.elasticsearch.script.ScriptContext; +import org.elasticsearch.script.SearchScript; +import org.elasticsearch.search.builder.SearchSourceBuilder; +import org.elasticsearch.search.builder.SearchSourceBuilder.ScriptField; +import org.elasticsearch.search.fetch.fielddata.FieldDataFieldsContext; +import org.elasticsearch.search.fetch.fielddata.FieldDataFieldsFetchSubPhase; +import org.elasticsearch.search.fetch.innerhits.InnerHitsContext; +import org.elasticsearch.search.fetch.source.FetchSourceContext; +import org.elasticsearch.search.highlight.HighlightBuilder; +import org.elasticsearch.search.internal.SearchContext; +import org.elasticsearch.search.sort.SortBuilder; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +import static org.elasticsearch.common.xcontent.XContentParser.Token.END_OBJECT; + +public final class InnerHitBuilder extends ToXContentToBytes implements Writeable { + + public static final ParseField NAME_FIELD = new ParseField("name"); + public static final ParseField NESTED_PATH_FIELD = new ParseField("path"); + public static final ParseField PARENT_CHILD_TYPE_FIELD = new ParseField("type"); + + private final static ObjectParser PARSER = new ObjectParser<>("inner_hits", InnerHitBuilder::new); + + static { + PARSER.declareString(InnerHitBuilder::setName, NAME_FIELD); + PARSER.declareString(InnerHitBuilder::setNestedPath, NESTED_PATH_FIELD); + PARSER.declareString(InnerHitBuilder::setParentChildType, PARENT_CHILD_TYPE_FIELD); + PARSER.declareInt(InnerHitBuilder::setFrom, SearchSourceBuilder.FROM_FIELD); + PARSER.declareInt(InnerHitBuilder::setSize, SearchSourceBuilder.SIZE_FIELD); + PARSER.declareBoolean(InnerHitBuilder::setExplain, SearchSourceBuilder.EXPLAIN_FIELD); + PARSER.declareBoolean(InnerHitBuilder::setVersion, SearchSourceBuilder.VERSION_FIELD); + PARSER.declareBoolean(InnerHitBuilder::setTrackScores, SearchSourceBuilder.TRACK_SCORES_FIELD); + PARSER.declareStringArray(InnerHitBuilder::setFieldNames, SearchSourceBuilder.FIELDS_FIELD); + PARSER.declareStringArray(InnerHitBuilder::setFieldDataFields, SearchSourceBuilder.FIELDDATA_FIELDS_FIELD); + PARSER.declareField((p, i, c) -> { + try { + List scriptFields = new ArrayList<>(); + for (XContentParser.Token token = p.nextToken(); token != END_OBJECT; token = p.nextToken()) { + scriptFields.add(new ScriptField(p, c)); + } + i.setScriptFields(scriptFields); + } catch (IOException e) { + throw new ParsingException(p.getTokenLocation(), "Could not parse inner script definition", e); + } + }, SearchSourceBuilder.SCRIPT_FIELDS_FIELD, ObjectParser.ValueType.OBJECT); + PARSER.declareField((p, i, c) -> i.setSorts(SortBuilder.fromXContent(c)), SearchSourceBuilder.SORT_FIELD, + ObjectParser.ValueType.OBJECT_ARRAY); + PARSER.declareField((p, i, c) -> { + try { + i.setFetchSourceContext(FetchSourceContext.parse(p, c)); + } catch (IOException e) { + throw new ParsingException(p.getTokenLocation(), "Could not parse inner _source definition", e); + } + }, SearchSourceBuilder._SOURCE_FIELD, ObjectParser.ValueType.OBJECT_OR_BOOLEAN); + PARSER.declareObject(InnerHitBuilder::setHighlightBuilder, (p, c) -> { + try { + return HighlightBuilder.PROTOTYPE.fromXContent(c); + } catch (IOException e) { + throw new ParsingException(p.getTokenLocation(), "Could not parse inner highlight definition", e); + } + }, SearchSourceBuilder.HIGHLIGHT_FIELD); + PARSER.declareObject(InnerHitBuilder::setQuery, (p, c) ->{ + try { + return c.parseInnerQueryBuilder(); + } catch (IOException e) { + throw new ParsingException(p.getTokenLocation(), "Could not parse inner query definition", e); + } + }, SearchSourceBuilder.QUERY_FIELD); + PARSER.declareObject(InnerHitBuilder::setInnerHitsBuilder, (p, c) -> { + try { + return InnerHitsBuilder.fromXContent(p, c); + } catch (IOException e) { + throw new ParsingException(p.getTokenLocation(), "Could not parse inner query definition", e); + } + }, SearchSourceBuilder.INNER_HITS_FIELD); + } + + public static InnerHitBuilder optionalReadFromStream(StreamInput in) throws IOException { + if (in.readBoolean()) { + return new InnerHitBuilder(in); + } else { + return null; + } + } + + private String name; + private String nestedPath; + private String parentChildType; + + private int from; + private int size = 3; + private boolean explain; + private boolean version; + private boolean trackScores; + + private List fieldNames; + private QueryBuilder query = new MatchAllQueryBuilder(); + private List> sorts; + private List fieldDataFields; + private List scriptFields; + private HighlightBuilder highlightBuilder; + private InnerHitsBuilder innerHitsBuilder; + private FetchSourceContext fetchSourceContext; + + // pkg protected, because is used in InnerHitsBuilder + InnerHitBuilder(StreamInput in) throws IOException { + name = in.readOptionalString(); + nestedPath = in.readOptionalString(); + parentChildType = in.readOptionalString(); + from = in.readVInt(); + size = in.readVInt(); + explain = in.readBoolean(); + version = in.readBoolean(); + trackScores = in.readBoolean(); + fieldNames = (List) in.readGenericValue(); + fieldDataFields = (List) in.readGenericValue(); + if (in.readBoolean()) { + scriptFields = in.readList(t -> ScriptField.PROTOTYPE.readFrom(in)); + } + fetchSourceContext = FetchSourceContext.optionalReadFromStream(in); + if (in.readBoolean()) { + int size = in.readVInt(); + sorts = new ArrayList<>(size); + for (int i = 0; i < size; i++) { + sorts.add(in.readSortBuilder()); + } + } + highlightBuilder = in.readOptionalWriteable(HighlightBuilder.PROTOTYPE::readFrom); + query = in.readQuery(); + innerHitsBuilder = in.readOptionalWriteable(InnerHitsBuilder.PROTO::readFrom); + } + + public InnerHitBuilder() { + } + + public InnerHitBuilder setParentChildType(String parentChildType) { + this.parentChildType = parentChildType; + return this; + } + + public InnerHitBuilder setNestedPath(String nestedPath) { + this.nestedPath = nestedPath; + return this; + } + + public String getName() { + return name; + } + + public InnerHitBuilder setName(String name) { + this.name = Objects.requireNonNull(name); + return this; + } + + public int getFrom() { + return from; + } + + public InnerHitBuilder setFrom(int from) { + if (from < 0) { + throw new IllegalArgumentException("illegal from value, at least 0 or higher"); + } + this.from = from; + return this; + } + + public int getSize() { + return size; + } + + public InnerHitBuilder setSize(int size) { + if (size < 0) { + throw new IllegalArgumentException("illegal size value, at least 0 or higher"); + } + this.size = size; + return this; + } + + public boolean isExplain() { + return explain; + } + + public InnerHitBuilder setExplain(boolean explain) { + this.explain = explain; + return this; + } + + public boolean isVersion() { + return version; + } + + public InnerHitBuilder setVersion(boolean version) { + this.version = version; + return this; + } + + public boolean isTrackScores() { + return trackScores; + } + + public InnerHitBuilder setTrackScores(boolean trackScores) { + this.trackScores = trackScores; + return this; + } + + public List getFieldNames() { + return fieldNames; + } + + public InnerHitBuilder setFieldNames(List fieldNames) { + this.fieldNames = fieldNames; + return this; + } + + public List getFieldDataFields() { + return fieldDataFields; + } + + public InnerHitBuilder setFieldDataFields(List fieldDataFields) { + this.fieldDataFields = fieldDataFields; + return this; + } + + public InnerHitBuilder addFieldDataField(String field) { + if (fieldDataFields == null) { + fieldDataFields = new ArrayList<>(); + } + fieldDataFields.add(field); + return this; + } + + public List getScriptFields() { + return scriptFields; + } + + public InnerHitBuilder setScriptFields(List scriptFields) { + this.scriptFields = scriptFields; + return this; + } + + public InnerHitBuilder addScriptField(String name, Script script) { + if (scriptFields == null) { + scriptFields = new ArrayList<>(); + } + scriptFields.add(new ScriptField(name, script, false)); + return this; + } + + public FetchSourceContext getFetchSourceContext() { + return fetchSourceContext; + } + + public InnerHitBuilder setFetchSourceContext(FetchSourceContext fetchSourceContext) { + this.fetchSourceContext = fetchSourceContext; + return this; + } + + public List> getSorts() { + return sorts; + } + + public InnerHitBuilder setSorts(List> sorts) { + this.sorts = sorts; + return this; + } + + public InnerHitBuilder addSort(SortBuilder sort) { + if (sorts == null) { + sorts = new ArrayList<>(); + } + sorts.add(sort); + return this; + } + + public HighlightBuilder getHighlightBuilder() { + return highlightBuilder; + } + + public InnerHitBuilder setHighlightBuilder(HighlightBuilder highlightBuilder) { + this.highlightBuilder = highlightBuilder; + return this; + } + + public QueryBuilder getQuery() { + return query; + } + + public InnerHitBuilder setQuery(QueryBuilder query) { + this.query = Objects.requireNonNull(query); + return this; + } + + public InnerHitBuilder setInnerHitsBuilder(InnerHitsBuilder innerHitsBuilder) { + this.innerHitsBuilder = innerHitsBuilder; + return this; + } + + public InnerHitsContext.BaseInnerHits buildInline(SearchContext parentSearchContext, QueryShardContext context) throws IOException { + InnerHitsContext.BaseInnerHits innerHitsContext; + if (nestedPath != null) { + ObjectMapper nestedObjectMapper = context.getObjectMapper(nestedPath); + ObjectMapper parentObjectMapper = context.nestedScope().getObjectMapper(); + innerHitsContext = new InnerHitsContext.NestedInnerHits( + name, parentSearchContext, parentObjectMapper, nestedObjectMapper + ); + } else if (parentChildType != null) { + DocumentMapper documentMapper = context.getMapperService().documentMapper(parentChildType); + innerHitsContext = new InnerHitsContext.ParentChildInnerHits( + name, parentSearchContext, context.getMapperService(), documentMapper + ); + } else { + throw new IllegalStateException("Neither a nested or parent/child inner hit"); + } + setupInnerHitsContext(context, innerHitsContext); + return innerHitsContext; + } + + /** + * Top level inner hits are different than inline inner hits: + * 1) Nesting. Top level inner hits can be hold nested inner hits, that why this method is recursive (via buildChildInnerHits) + * 2) Top level inner hits query is an option, whereas with inline inner hits that is based on the nested, has_child + * or has_parent's inner query. + * + * Because of these changes there are different methods for building inline (which is simpler) and top level inner + * hits. Also top level inner hits will soon be deprecated. + */ + public InnerHitsContext.BaseInnerHits buildTopLevel(SearchContext parentSearchContext, QueryShardContext context, + InnerHitsContext innerHitsContext) throws IOException { + if (nestedPath != null) { + ObjectMapper nestedObjectMapper = context.getObjectMapper(nestedPath); + ObjectMapper parentObjectMapper = context.nestedScope().nextLevel(nestedObjectMapper); + InnerHitsContext.NestedInnerHits nestedInnerHits = new InnerHitsContext.NestedInnerHits( + name, parentSearchContext, parentObjectMapper, nestedObjectMapper + ); + setupInnerHitsContext(context, nestedInnerHits); + if (innerHitsBuilder != null) { + buildChildInnerHits(parentSearchContext, context, nestedInnerHits); + } + context.nestedScope().previousLevel(); + innerHitsContext.addInnerHitDefinition(nestedInnerHits); + return nestedInnerHits; + } else if (parentChildType != null) { + DocumentMapper documentMapper = context.getMapperService().documentMapper(parentChildType); + InnerHitsContext.ParentChildInnerHits parentChildInnerHits = new InnerHitsContext.ParentChildInnerHits( + name, parentSearchContext, context.getMapperService(), documentMapper + ); + setupInnerHitsContext(context, parentChildInnerHits); + if (innerHitsBuilder != null) { + buildChildInnerHits(parentSearchContext, context, parentChildInnerHits); + } + innerHitsContext.addInnerHitDefinition( parentChildInnerHits); + return parentChildInnerHits; + } else { + throw new IllegalStateException("Neither a nested or parent/child inner hit"); + } + } + + private void buildChildInnerHits(SearchContext parentSearchContext, QueryShardContext context, + InnerHitsContext.BaseInnerHits innerHits) throws IOException { + Map childInnerHits = new HashMap<>(); + for (Map.Entry entry : innerHitsBuilder.getInnerHitsBuilders().entrySet()) { + InnerHitsContext.BaseInnerHits childInnerHit = entry.getValue().buildTopLevel( + parentSearchContext, context, new InnerHitsContext() + ); + childInnerHits.put(entry.getKey(), childInnerHit); + } + innerHits.setChildInnerHits(childInnerHits); + } + + @Override + public InnerHitBuilder readFrom(StreamInput in) throws IOException { + return new InnerHitBuilder(in); + } + + private void setupInnerHitsContext(QueryShardContext context, InnerHitsContext.BaseInnerHits innerHitsContext) throws IOException { + innerHitsContext.from(from); + innerHitsContext.size(size); + innerHitsContext.explain(explain); + innerHitsContext.version(version); + innerHitsContext.trackScores(trackScores); + if (fieldNames != null) { + if (fieldNames.isEmpty()) { + innerHitsContext.emptyFieldNames(); + } else { + for (String fieldName : fieldNames) { + innerHitsContext.fieldNames().add(fieldName); + } + } + } + if (fieldDataFields != null) { + FieldDataFieldsContext fieldDataFieldsContext = innerHitsContext + .getFetchSubPhaseContext(FieldDataFieldsFetchSubPhase.CONTEXT_FACTORY); + for (String field : fieldDataFields) { + fieldDataFieldsContext.add(new FieldDataFieldsContext.FieldDataField(field)); + } + fieldDataFieldsContext.setHitExecutionNeeded(true); + } + if (scriptFields != null) { + for (ScriptField field : scriptFields) { + SearchScript searchScript = innerHitsContext.scriptService().search(innerHitsContext.lookup(), field.script(), + ScriptContext.Standard.SEARCH, Collections.emptyMap()); + innerHitsContext.scriptFields().add(new org.elasticsearch.search.fetch.script.ScriptFieldsContext.ScriptField( + field.fieldName(), searchScript, field.ignoreFailure())); + } + } + if (fetchSourceContext != null) { + innerHitsContext.fetchSourceContext(fetchSourceContext); + } + if (sorts != null) { + Optional optionalSort = SortBuilder.buildSort(sorts, context); + if (optionalSort.isPresent()) { + innerHitsContext.sort(optionalSort.get()); + } + } + if (highlightBuilder != null) { + innerHitsContext.highlight(highlightBuilder.build(context)); + } + ParsedQuery parsedQuery = new ParsedQuery(query.toQuery(context), context.copyNamedQueries()); + innerHitsContext.parsedQuery(parsedQuery); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeOptionalString(name); + out.writeOptionalString(nestedPath); + out.writeOptionalString(parentChildType); + out.writeVInt(from); + out.writeVInt(size); + out.writeBoolean(explain); + out.writeBoolean(version); + out.writeBoolean(trackScores); + out.writeGenericValue(fieldNames); + out.writeGenericValue(fieldDataFields); + boolean hasScriptFields = scriptFields != null; + out.writeBoolean(hasScriptFields); + if (hasScriptFields) { + out.writeList(scriptFields); + } + FetchSourceContext.optionalWriteToStream(fetchSourceContext, out); + boolean hasSorts = sorts != null; + out.writeBoolean(hasSorts); + if (hasSorts) { + out.writeVInt(sorts.size()); + for (SortBuilder sort : sorts) { + out.writeSortBuilder(sort); + } + } + out.writeOptionalWriteable(highlightBuilder); + out.writeQuery(query); + out.writeOptionalWriteable(innerHitsBuilder); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + + if (nestedPath != null) { + builder.field(NESTED_PATH_FIELD.getPreferredName(), nestedPath); + } + if (parentChildType != null) { + builder.field(PARENT_CHILD_TYPE_FIELD.getPreferredName(), parentChildType); + } + if (name != null) { + builder.field(NAME_FIELD.getPreferredName(), name); + } + builder.field(SearchSourceBuilder.FROM_FIELD.getPreferredName(), from); + builder.field(SearchSourceBuilder.SIZE_FIELD.getPreferredName(), size); + builder.field(SearchSourceBuilder.VERSION_FIELD.getPreferredName(), version); + builder.field(SearchSourceBuilder.EXPLAIN_FIELD.getPreferredName(), explain); + builder.field(SearchSourceBuilder.TRACK_SCORES_FIELD.getPreferredName(), trackScores); + if (fetchSourceContext != null) { + builder.field(SearchSourceBuilder._SOURCE_FIELD.getPreferredName(), fetchSourceContext, params); + } + if (fieldNames != null) { + if (fieldNames.size() == 1) { + builder.field(SearchSourceBuilder.FIELDS_FIELD.getPreferredName(), fieldNames.get(0)); + } else { + builder.startArray(SearchSourceBuilder.FIELDS_FIELD.getPreferredName()); + for (String fieldName : fieldNames) { + builder.value(fieldName); + } + builder.endArray(); + } + } + if (fieldDataFields != null) { + builder.startArray(SearchSourceBuilder.FIELDDATA_FIELDS_FIELD.getPreferredName()); + for (String fieldDataField : fieldDataFields) { + builder.value(fieldDataField); + } + builder.endArray(); + } + if (scriptFields != null) { + builder.startObject(SearchSourceBuilder.SCRIPT_FIELDS_FIELD.getPreferredName()); + for (ScriptField scriptField : scriptFields) { + scriptField.toXContent(builder, params); + } + builder.endObject(); + } + if (sorts != null) { + builder.startArray(SearchSourceBuilder.SORT_FIELD.getPreferredName()); + for (SortBuilder sort : sorts) { + sort.toXContent(builder, params); + } + builder.endArray(); + } + if (highlightBuilder != null) { + builder.field(SearchSourceBuilder.HIGHLIGHT_FIELD.getPreferredName(), highlightBuilder, params); + } + builder.field(SearchSourceBuilder.QUERY_FIELD.getPreferredName(), query, params); + if (innerHitsBuilder != null) { + builder.field(SearchSourceBuilder.INNER_HITS_FIELD.getPreferredName(), innerHitsBuilder, params); + } + builder.endObject(); + return builder; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + InnerHitBuilder that = (InnerHitBuilder) o; + return Objects.equals(name, that.name) && + Objects.equals(nestedPath, that.nestedPath) && + Objects.equals(parentChildType, that.parentChildType) && + Objects.equals(from, that.from) && + Objects.equals(size, that.size) && + Objects.equals(explain, that.explain) && + Objects.equals(version, that.version) && + Objects.equals(trackScores, that.trackScores) && + Objects.equals(fieldNames, that.fieldNames) && + Objects.equals(fieldDataFields, that.fieldDataFields) && + Objects.equals(scriptFields, that.scriptFields) && + Objects.equals(fetchSourceContext, that.fetchSourceContext) && + Objects.equals(sorts, that.sorts) && + Objects.equals(highlightBuilder, that.highlightBuilder) && + Objects.equals(query, that.query) && + Objects.equals(innerHitsBuilder, that.innerHitsBuilder); + } + + @Override + public int hashCode() { + return Objects.hash(name, nestedPath, parentChildType, from, size, explain, version, trackScores, fieldNames, + fieldDataFields, scriptFields, fetchSourceContext, sorts, highlightBuilder, query, innerHitsBuilder); + } + + public static InnerHitBuilder fromXContent(XContentParser parser, QueryParseContext context) throws IOException { + return PARSER.parse(parser, new InnerHitBuilder(), context); + } + +} diff --git a/core/src/main/java/org/elasticsearch/index/query/support/InnerHitsBuilder.java b/core/src/main/java/org/elasticsearch/index/query/support/InnerHitsBuilder.java new file mode 100644 index 00000000000..5b1e9c16133 --- /dev/null +++ b/core/src/main/java/org/elasticsearch/index/query/support/InnerHitsBuilder.java @@ -0,0 +1,129 @@ +/* + * 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.action.support.ToXContentToBytes; +import org.elasticsearch.common.ParsingException; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.io.stream.Writeable; +import org.elasticsearch.common.xcontent.ToXContent; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.common.xcontent.XContentParser.Token; +import org.elasticsearch.index.query.QueryParseContext; + +import java.io.IOException; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +public final class InnerHitsBuilder extends ToXContentToBytes implements Writeable { + + public final static InnerHitsBuilder PROTO = new InnerHitsBuilder(Collections.emptyMap()); + + private final Map innerHitsBuilders; + + public InnerHitsBuilder() { + this.innerHitsBuilders = new HashMap<>(); + } + + public InnerHitsBuilder(Map innerHitsBuilders) { + this.innerHitsBuilders = Objects.requireNonNull(innerHitsBuilders); + } + + public InnerHitsBuilder addInnerHit(String name, InnerHitBuilder builder) { + Objects.requireNonNull(name); + Objects.requireNonNull(builder); + this.innerHitsBuilders.put(name, builder.setName(name)); + return this; + } + + public Map getInnerHitsBuilders() { + return innerHitsBuilders; + } + + @Override + public InnerHitsBuilder readFrom(StreamInput in) throws IOException { + int size = in.readVInt(); + Map innerHitsBuilders = new HashMap<>(size); + for (int i = 0; i < size; i++) { + innerHitsBuilders.put(in.readString(), new InnerHitBuilder(in)); + } + return new InnerHitsBuilder(innerHitsBuilders); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + for (Map.Entry entry : innerHitsBuilders.entrySet()) { + builder.field(entry.getKey(), entry.getValue(), params); + } + builder.endObject(); + return builder; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeVInt(innerHitsBuilders.size()); + for (Map.Entry entry : innerHitsBuilders.entrySet()) { + out.writeString(entry.getKey()); + entry.getValue().writeTo(out); + } + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + InnerHitsBuilder that = (InnerHitsBuilder) o; + return innerHitsBuilders.equals(that.innerHitsBuilders); + + } + + @Override + public int hashCode() { + return innerHitsBuilders.hashCode(); + } + + public static InnerHitsBuilder fromXContent(XContentParser parser, QueryParseContext context) throws IOException { + Map innerHitBuilders = new HashMap<>(); + String innerHitName = null; + for (Token token = parser.nextToken(); token != Token.END_OBJECT; token = parser.nextToken()) { + switch (token) { + case START_OBJECT: + InnerHitBuilder innerHitBuilder = InnerHitBuilder.fromXContent(parser, context); + innerHitBuilder.setName(innerHitName); + innerHitBuilders.put(innerHitName, innerHitBuilder); + break; + case FIELD_NAME: + innerHitName = parser.currentName(); + break; + default: + throw new ParsingException(parser.getTokenLocation(), "Expected [" + XContentParser.Token.START_OBJECT + "] in [" + + parser.currentName() + "] but found [" + token + "]", parser.getTokenLocation()); + } + } + return new InnerHitsBuilder(innerHitBuilders); + } + + +} diff --git a/core/src/main/java/org/elasticsearch/index/query/support/InnerHitsQueryParserHelper.java b/core/src/main/java/org/elasticsearch/index/query/support/InnerHitsQueryParserHelper.java deleted file mode 100644 index 0467e459718..00000000000 --- a/core/src/main/java/org/elasticsearch/index/query/support/InnerHitsQueryParserHelper.java +++ /dev/null @@ -1,133 +0,0 @@ -/* - * 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.XContentParser; -import org.elasticsearch.search.fetch.fielddata.FieldDataFieldsParseElement; -import org.elasticsearch.search.fetch.innerhits.InnerHitsSubSearchContext; -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.SortBuilder; - -import java.io.IOException; - -public class InnerHitsQueryParserHelper { - - public static final InnerHitsQueryParserHelper INSTANCE = new InnerHitsQueryParserHelper(); - - private static final FetchSourceParseElement sourceParseElement = new FetchSourceParseElement(); - private static final HighlighterParseElement highlighterParseElement = new HighlighterParseElement(); - private static final ScriptFieldsParseElement scriptFieldsParseElement = new ScriptFieldsParseElement(); - private static final FieldDataFieldsParseElement fieldDataFieldsParseElement = new FieldDataFieldsParseElement(); - - public static InnerHitsSubSearchContext parse(XContentParser parser) throws IOException { - String fieldName = null; - XContentParser.Token token; - String innerHitName = null; - SubSearchContext subSearchContext = new SubSearchContext(SearchContext.current()); - try { - 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, sourceParseElement, highlighterParseElement, scriptFieldsParseElement, fieldDataFieldsParseElement); - } - } else { - parseCommonInnerHitOptions(parser, token, fieldName, subSearchContext, sourceParseElement, highlighterParseElement, scriptFieldsParseElement, fieldDataFieldsParseElement); - } - } - } catch (Exception e) { - throw new IOException("Failed to parse [_inner_hits]", e); - } - return new InnerHitsSubSearchContext(innerHitName, subSearchContext); - } - - public static void parseCommonInnerHitOptions(XContentParser parser, XContentParser.Token token, String fieldName, SubSearchContext subSearchContext, - FetchSourceParseElement sourceParseElement, HighlighterParseElement highlighterParseElement, - ScriptFieldsParseElement scriptFieldsParseElement, FieldDataFieldsParseElement fieldDataFieldsParseElement) throws Exception { - if ("sort".equals(fieldName)) { - SortBuilder.parseSort(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 IllegalArgumentException("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; - case "fields": - boolean added = false; - while ((token = parser.nextToken()) != XContentParser.Token.END_ARRAY) { - String name = parser.text(); - added = true; - subSearchContext.fieldNames().add(name); - } - if (!added) { - subSearchContext.emptyFieldNames(); - } - break; - default: - throw new IllegalArgumentException("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; - case "fields": - subSearchContext.fieldNames().add(parser.text()); - break; - default: - throw new IllegalArgumentException("Unknown key for a " + token + " for nested query: [" + fieldName + "]."); - } - } - } -} diff --git a/core/src/main/java/org/elasticsearch/index/query/support/QueryInnerHits.java b/core/src/main/java/org/elasticsearch/index/query/support/QueryInnerHits.java deleted file mode 100644 index c8e019bc8af..00000000000 --- a/core/src/main/java/org/elasticsearch/index/query/support/QueryInnerHits.java +++ /dev/null @@ -1,113 +0,0 @@ -/* - * 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.ElasticsearchException; -import org.elasticsearch.action.support.ToXContentToBytes; -import org.elasticsearch.common.bytes.BytesReference; -import org.elasticsearch.common.io.stream.BytesStreamOutput; -import org.elasticsearch.common.io.stream.StreamInput; -import org.elasticsearch.common.io.stream.StreamOutput; -import org.elasticsearch.common.io.stream.Writeable; -import org.elasticsearch.common.xcontent.ToXContent; -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.search.fetch.innerhits.InnerHitsBuilder; - -import java.io.IOException; - -/** - */ -public class QueryInnerHits extends ToXContentToBytes implements Writeable { - private final BytesReference queryInnerHitsSearchSource; - - public QueryInnerHits(StreamInput input) throws IOException { - queryInnerHitsSearchSource = input.readBytesReference(); - } - - public QueryInnerHits(XContentParser parser) throws IOException { - BytesStreamOutput out = new BytesStreamOutput(); - try (XContentBuilder builder = XContentFactory.cborBuilder(out)) { - builder.copyCurrentStructure(parser); - queryInnerHitsSearchSource = builder.bytes(); - } - } - - public QueryInnerHits() { - this(null, null); - } - - public QueryInnerHits(String name, InnerHitsBuilder.InnerHit innerHit) { - BytesStreamOutput out = new BytesStreamOutput(); - try (XContentBuilder builder = XContentFactory.cborBuilder(out)) { - builder.startObject(); - if (name != null) { - builder.field("name", name); - } - if (innerHit != null) { - innerHit.toXContent(builder, ToXContent.EMPTY_PARAMS); - } - builder.endObject(); - this.queryInnerHitsSearchSource = builder.bytes(); - } catch (IOException e) { - throw new ElasticsearchException("failed to build xcontent", e); - } - } - - @Override - public QueryInnerHits readFrom(StreamInput in) throws IOException { - return new QueryInnerHits(in); - } - - @Override - public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { - builder.field("inner_hits"); - try (XContentParser parser = XContentType.CBOR.xContent().createParser(queryInnerHitsSearchSource)) { - builder.copyCurrentStructure(parser); - } - return builder; - } - - @Override - public void writeTo(StreamOutput out) throws IOException { - out.writeBytesReference(queryInnerHitsSearchSource); - } - - public XContentParser getXcontentParser() throws IOException { - return XContentType.CBOR.xContent().createParser(queryInnerHitsSearchSource); - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - QueryInnerHits that = (QueryInnerHits) o; - - return queryInnerHitsSearchSource.equals(that.queryInnerHitsSearchSource); - - } - - @Override - public int hashCode() { - return queryInnerHitsSearchSource.hashCode(); - } -} diff --git a/core/src/main/java/org/elasticsearch/search/SearchService.java b/core/src/main/java/org/elasticsearch/search/SearchService.java index b45242c7944..e2c59206436 100644 --- a/core/src/main/java/org/elasticsearch/search/SearchService.java +++ b/core/src/main/java/org/elasticsearch/search/SearchService.java @@ -51,6 +51,7 @@ import org.elasticsearch.index.engine.Engine; import org.elasticsearch.index.fieldstats.FieldStatsProvider; import org.elasticsearch.index.query.QueryParseContext; import org.elasticsearch.index.query.QueryShardContext; +import org.elasticsearch.index.query.support.InnerHitBuilder; import org.elasticsearch.index.search.stats.StatsGroupsParseElement; import org.elasticsearch.index.shard.IndexEventListener; import org.elasticsearch.index.shard.IndexShard; @@ -76,6 +77,7 @@ import org.elasticsearch.search.fetch.ShardFetchRequest; import org.elasticsearch.search.fetch.fielddata.FieldDataFieldsContext; import org.elasticsearch.search.fetch.fielddata.FieldDataFieldsContext.FieldDataField; import org.elasticsearch.search.fetch.fielddata.FieldDataFieldsFetchSubPhase; +import org.elasticsearch.search.fetch.innerhits.InnerHitsContext; import org.elasticsearch.search.fetch.script.ScriptFieldsContext.ScriptField; import org.elasticsearch.search.highlight.HighlightBuilder; import org.elasticsearch.search.internal.DefaultSearchContext; @@ -754,20 +756,22 @@ public class SearchService extends AbstractLifecycleComponent imp } } if (source.innerHits() != null) { - XContentParser innerHitsParser = null; - try { - innerHitsParser = XContentFactory.xContent(source.innerHits()).createParser(source.innerHits()); - innerHitsParser.nextToken(); - this.elementParsers.get("inner_hits").parse(innerHitsParser, context); - } catch (Exception e) { - String sSource = "_na_"; + for (Map.Entry entry : source.innerHits().getInnerHitsBuilders().entrySet()) { try { - sSource = source.toString(); - } catch (Throwable e1) { - // ignore + // This is the same logic in QueryShardContext#toQuery() where we reset also twice. + // Personally I think a reset at the end is sufficient, but I kept the logic consistent with this method. + + // The reason we need to invoke reset at all here is because inner hits may modify the QueryShardContext#nestedScope, + // so we need to reset at the end. + queryShardContext.reset(); + InnerHitBuilder innerHitBuilder = entry.getValue(); + InnerHitsContext innerHitsContext = context.innerHits(); + innerHitBuilder.buildTopLevel(context, queryShardContext, innerHitsContext); + } catch (IOException e) { + throw new SearchContextException(context, "failed to create InnerHitsContext", e); + } finally { + queryShardContext.reset(); } - XContentLocation location = innerHitsParser != null ? innerHitsParser.getTokenLocation() : null; - throw new SearchParseException(context, "failed to parse suggest source [" + sSource + "]", location, e); } } if (source.scriptFields() != null) { diff --git a/core/src/main/java/org/elasticsearch/search/aggregations/metrics/tophits/TopHitsAggregatorFactory.java b/core/src/main/java/org/elasticsearch/search/aggregations/metrics/tophits/TopHitsAggregatorFactory.java index be10f4ce878..5b16301d3be 100644 --- a/core/src/main/java/org/elasticsearch/search/aggregations/metrics/tophits/TopHitsAggregatorFactory.java +++ b/core/src/main/java/org/elasticsearch/search/aggregations/metrics/tophits/TopHitsAggregatorFactory.java @@ -79,6 +79,7 @@ public class TopHitsAggregatorFactory extends AggregatorFactory pipelineAggregators, Map metaData) throws IOException { SubSearchContext subSearchContext = new SubSearchContext(context.searchContext()); + subSearchContext.parsedQuery(context.searchContext().parsedQuery()); subSearchContext.explain(explain); subSearchContext.version(version); subSearchContext.trackScores(trackScores); diff --git a/core/src/main/java/org/elasticsearch/search/builder/SearchSourceBuilder.java b/core/src/main/java/org/elasticsearch/search/builder/SearchSourceBuilder.java index c83794ced90..fcb8289849c 100644 --- a/core/src/main/java/org/elasticsearch/search/builder/SearchSourceBuilder.java +++ b/core/src/main/java/org/elasticsearch/search/builder/SearchSourceBuilder.java @@ -46,7 +46,7 @@ import org.elasticsearch.search.aggregations.AggregatorBuilder; import org.elasticsearch.search.aggregations.AggregatorFactories; import org.elasticsearch.search.aggregations.AggregatorParsers; import org.elasticsearch.search.aggregations.pipeline.PipelineAggregatorBuilder; -import org.elasticsearch.search.fetch.innerhits.InnerHitsBuilder; +import org.elasticsearch.index.query.support.InnerHitsBuilder; import org.elasticsearch.search.fetch.source.FetchSourceContext; import org.elasticsearch.search.highlight.HighlightBuilder; import org.elasticsearch.search.internal.SearchContext; @@ -162,7 +162,7 @@ public final class SearchSourceBuilder extends ToXContentToBytes implements Writ private SuggestBuilder suggestBuilder; - private BytesReference innerHitsBuilder; + private InnerHitsBuilder innerHitsBuilder; private List> rescoreBuilders; @@ -457,22 +457,11 @@ public final class SearchSourceBuilder extends ToXContentToBytes implements Writ } public SearchSourceBuilder innerHits(InnerHitsBuilder innerHitsBuilder) { - try { - XContentBuilder builder = XContentFactory.jsonBuilder(); - builder.startObject(); - innerHitsBuilder.innerXContent(builder, EMPTY_PARAMS); - builder.endObject(); - this.innerHitsBuilder = builder.bytes(); - return this; - } catch (IOException e) { - throw new RuntimeException(e); - } + this.innerHitsBuilder = innerHitsBuilder; + return this; } - /** - * Gets the bytes representing the inner hits builder for this request. - */ - public BytesReference innerHits() { + public InnerHitsBuilder innerHits() { return innerHitsBuilder; } @@ -857,40 +846,7 @@ public final class SearchSourceBuilder extends ToXContentToBytes implements Writ } else if (context.parseFieldMatcher().match(currentFieldName, SCRIPT_FIELDS_FIELD)) { scriptFields = new ArrayList<>(); while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { - String scriptFieldName = parser.currentName(); - token = parser.nextToken(); - if (token == XContentParser.Token.START_OBJECT) { - Script script = null; - boolean ignoreFailure = false; - while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { - if (token == XContentParser.Token.FIELD_NAME) { - currentFieldName = parser.currentName(); - } else if (token.isValue()) { - if (context.parseFieldMatcher().match(currentFieldName, SCRIPT_FIELD)) { - script = Script.parse(parser, context.parseFieldMatcher()); - } else if (context.parseFieldMatcher().match(currentFieldName, IGNORE_FAILURE_FIELD)) { - ignoreFailure = parser.booleanValue(); - } else { - throw new ParsingException(parser.getTokenLocation(), "Unknown key for a " + token + " in [" + currentFieldName - + "].", parser.getTokenLocation()); - } - } else if (token == XContentParser.Token.START_OBJECT) { - if (context.parseFieldMatcher().match(currentFieldName, SCRIPT_FIELD)) { - script = Script.parse(parser, context.parseFieldMatcher()); - } else { - throw new ParsingException(parser.getTokenLocation(), "Unknown key for a " + token + " in [" + currentFieldName - + "].", parser.getTokenLocation()); - } - } else { - throw new ParsingException(parser.getTokenLocation(), "Unknown key for a " + token + " in [" + currentFieldName - + "].", parser.getTokenLocation()); - } - } - scriptFields.add(new ScriptField(scriptFieldName, script, ignoreFailure)); - } else { - throw new ParsingException(parser.getTokenLocation(), "Expected [" + XContentParser.Token.START_OBJECT + "] in [" - + currentFieldName + "] but found [" + token + "]", parser.getTokenLocation()); - } + scriptFields.add(new ScriptField(parser, context)); } } else if (context.parseFieldMatcher().match(currentFieldName, INDICES_BOOST_FIELD)) { indexBoost = new ObjectFloatHashMap(); @@ -909,8 +865,7 @@ public final class SearchSourceBuilder extends ToXContentToBytes implements Writ } else if (context.parseFieldMatcher().match(currentFieldName, HIGHLIGHT_FIELD)) { highlightBuilder = HighlightBuilder.PROTOTYPE.fromXContent(context); } else if (context.parseFieldMatcher().match(currentFieldName, INNER_HITS_FIELD)) { - XContentBuilder xContentBuilder = XContentFactory.jsonBuilder().copyCurrentStructure(parser); - innerHitsBuilder = xContentBuilder.bytes(); + innerHitsBuilder = InnerHitsBuilder.fromXContent(parser, context); } else if (context.parseFieldMatcher().match(currentFieldName, SUGGEST_FIELD)) { suggestBuilder = SuggestBuilder.fromXContent(context, suggesters); } else if (context.parseFieldMatcher().match(currentFieldName, SORT_FIELD)) { @@ -1094,10 +1049,7 @@ public final class SearchSourceBuilder extends ToXContentToBytes implements Writ } if (innerHitsBuilder != null) { - builder.field(INNER_HITS_FIELD.getPreferredName()); - XContentParser parser = XContentFactory.xContent(XContentType.JSON).createParser(innerHitsBuilder); - parser.nextToken(); - builder.copyCurrentStructure(parser); + builder.field(INNER_HITS_FIELD.getPreferredName(), innerHitsBuilder, params); } if (suggestBuilder != null) { @@ -1126,7 +1078,7 @@ public final class SearchSourceBuilder extends ToXContentToBytes implements Writ public static class ScriptField implements Writeable, ToXContent { - public static final ScriptField PROTOTYPE = new ScriptField(null, null); + public static final ScriptField PROTOTYPE = new ScriptField((String) null, (Script) null); private final boolean ignoreFailure; private final String fieldName; @@ -1142,6 +1094,48 @@ public final class SearchSourceBuilder extends ToXContentToBytes implements Writ this.ignoreFailure = ignoreFailure; } + public ScriptField(XContentParser parser, QueryParseContext context) throws IOException { + boolean ignoreFailure = false; + String scriptFieldName = parser.currentName(); + Script script = null; + + XContentParser.Token token; + token = parser.nextToken(); + if (token == XContentParser.Token.START_OBJECT) { + String currentFieldName = null; + while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { + if (token == XContentParser.Token.FIELD_NAME) { + currentFieldName = parser.currentName(); + } else if (token.isValue()) { + if (context.parseFieldMatcher().match(currentFieldName, SCRIPT_FIELD)) { + script = Script.parse(parser, context.parseFieldMatcher()); + } else if (context.parseFieldMatcher().match(currentFieldName, IGNORE_FAILURE_FIELD)) { + ignoreFailure = parser.booleanValue(); + } else { + throw new ParsingException(parser.getTokenLocation(), "Unknown key for a " + token + " in [" + currentFieldName + + "].", parser.getTokenLocation()); + } + } else if (token == XContentParser.Token.START_OBJECT) { + if (context.parseFieldMatcher().match(currentFieldName, SCRIPT_FIELD)) { + script = Script.parse(parser, context.parseFieldMatcher()); + } else { + throw new ParsingException(parser.getTokenLocation(), "Unknown key for a " + token + " in [" + currentFieldName + + "].", parser.getTokenLocation()); + } + } else { + throw new ParsingException(parser.getTokenLocation(), "Unknown key for a " + token + " in [" + currentFieldName + + "].", parser.getTokenLocation()); + } + } + this.ignoreFailure = ignoreFailure; + this.fieldName = scriptFieldName; + this.script = script; + } else { + throw new ParsingException(parser.getTokenLocation(), "Expected [" + XContentParser.Token.START_OBJECT + "] in [" + + parser.currentName() + "] but found [" + token + "]", parser.getTokenLocation()); + } + } + public String fieldName() { return fieldName; } @@ -1235,7 +1229,7 @@ public final class SearchSourceBuilder extends ToXContentToBytes implements Writ builder.indexBoost = indexBoost; } if (in.readBoolean()) { - builder.innerHitsBuilder = in.readBytesReference(); + builder.innerHitsBuilder = InnerHitsBuilder.PROTO.readFrom(in); } if (in.readBoolean()) { builder.minScore = in.readFloat(); @@ -1343,7 +1337,7 @@ public final class SearchSourceBuilder extends ToXContentToBytes implements Writ boolean hasInnerHitsBuilder = innerHitsBuilder != null; out.writeBoolean(hasInnerHitsBuilder); if (hasInnerHitsBuilder) { - out.writeBytesReference(innerHitsBuilder); + innerHitsBuilder.writeTo(out); } boolean hasMinScore = minScore != null; out.writeBoolean(hasMinScore); diff --git a/core/src/main/java/org/elasticsearch/search/fetch/innerhits/InnerHitsBuilder.java b/core/src/main/java/org/elasticsearch/search/fetch/innerhits/InnerHitsBuilder.java deleted file mode 100644 index 2e76a4c3703..00000000000 --- a/core/src/main/java/org/elasticsearch/search/fetch/innerhits/InnerHitsBuilder.java +++ /dev/null @@ -1,305 +0,0 @@ -/* - * 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.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.script.Script; -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.HashMap; -import java.util.Map; - -/** - */ -public class InnerHitsBuilder implements ToXContent { - - private final Map innerHits = new HashMap<>(); - - @Override - public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { - builder.startObject("inner_hits"); - innerXContent(builder, params); - return builder.endObject(); - } - - public void innerXContent(XContentBuilder builder, Params params) throws IOException { - for (Map.Entry entry : innerHits.entrySet()) { - builder.startObject(entry.getKey()); - entry.getValue().toXContent(builder, params); - builder.endObject(); - } - } - - /** - * For nested inner hits the path to collect child nested docs for. - * @param name the name / key of the inner hits in the response - * @param path the path into the nested to collect inner hits for - * @param innerHit the inner hits definition - */ - public void addNestedInnerHits(String name, String path, InnerHit innerHit) { - if (innerHits.containsKey(name)) { - throw new IllegalArgumentException("inner hits for name: [" + name +"] is already registered"); - } - innerHits.put(name, new NestedInnerHitsHolder(path, innerHit)); - } - - /** - * For parent/child inner hits the type to collect inner hits for. - * @param name the name / key of the inner hits in the response - * @param type the document type to collect inner hits for - * @param innerHit the inner hits definition - */ - public void addParentChildInnerHits(String name, String type, InnerHit innerHit) { - innerHits.put(name, new ParentChildInnerHitsHolder(type, innerHit)); - } - - private static class InnerHitsHolder implements ToXContent{ - private final InnerHit hits; - - private InnerHitsHolder(InnerHit hits) { - this.hits = hits; - } - - @Override - public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { - return hits.toXContent(builder, params); - } - } - - private static class ParentChildInnerHitsHolder extends InnerHitsHolder { - - private final String type; - - private ParentChildInnerHitsHolder(String type, InnerHit hits) { - super(hits); - this.type = type; - } - - @Override - public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { - builder.startObject("type").startObject(type); - super.toXContent(builder, params); - return builder.endObject().endObject(); - } - } - - private static class NestedInnerHitsHolder extends InnerHitsHolder { - - private final String path; - - private NestedInnerHitsHolder(String path, InnerHit hits) { - super(hits); - this.path = path; - } - - @Override - public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { - builder.startObject("path").startObject(path); - super.toXContent(builder, params); - return builder.endObject().endObject(); - } - } - - public static class InnerHit implements ToXContent { - - private SearchSourceBuilder sourceBuilder; - private String path; - private String type; - - /** - * The index to start to return hits from. Defaults to 0. - */ - 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; - } - - /** - * Add a stored field to be loaded and returned with the inner hit. - */ - public InnerHit field(String name) { - sourceBuilder().field(name); - 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, Script script) { - sourceBuilder().scriptField(name, script); - 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; - } - - public HighlightBuilder highlighter() { - return sourceBuilder().highlighter(); - } - - public InnerHit highlighter(HighlightBuilder highlightBuilder) { - sourceBuilder().highlighter(highlightBuilder); - return this; - } - - protected SearchSourceBuilder sourceBuilder() { - if (sourceBuilder == null) { - sourceBuilder = new SearchSourceBuilder(); - } - return sourceBuilder; - } - - /** - * Sets the query to run for collecting the inner hits. - */ - public InnerHit setQuery(QueryBuilder query) { - sourceBuilder().query(query); - return this; - } - - public InnerHit innerHits(InnerHitsBuilder innerHitsBuilder) { - sourceBuilder().innerHits(innerHitsBuilder); - return this; - } - - @Override - public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { - if (sourceBuilder != null) { - sourceBuilder.innerToXContent(builder, params); - } - return builder; - } - } -} diff --git a/core/src/main/java/org/elasticsearch/search/fetch/innerhits/InnerHitsContext.java b/core/src/main/java/org/elasticsearch/search/fetch/innerhits/InnerHitsContext.java index be4dd7cdade..56059fc0d01 100644 --- a/core/src/main/java/org/elasticsearch/search/fetch/innerhits/InnerHitsContext.java +++ b/core/src/main/java/org/elasticsearch/search/fetch/innerhits/InnerHitsContext.java @@ -48,16 +48,16 @@ import org.elasticsearch.index.mapper.Uid; import org.elasticsearch.index.mapper.internal.ParentFieldMapper; import org.elasticsearch.index.mapper.internal.UidFieldMapper; import org.elasticsearch.index.mapper.object.ObjectMapper; -import org.elasticsearch.index.query.ParsedQuery; import org.elasticsearch.search.SearchHitField; import org.elasticsearch.search.fetch.FetchSubPhase; -import org.elasticsearch.search.internal.FilteredSearchContext; import org.elasticsearch.search.internal.InternalSearchHit; import org.elasticsearch.search.internal.SearchContext; +import org.elasticsearch.search.internal.SubSearchContext; import java.io.IOException; import java.util.HashMap; import java.util.Map; +import java.util.Objects; /** */ @@ -70,59 +70,46 @@ public final class InnerHitsContext { } public InnerHitsContext(Map innerHits) { - this.innerHits = innerHits; + this.innerHits = Objects.requireNonNull(innerHits); } public Map getInnerHits() { return innerHits; } - public void addInnerHitDefinition(String name, BaseInnerHits innerHit) { - if (innerHits.containsKey(name)) { - throw new IllegalArgumentException("inner_hit definition with the name [" + name + "] already exists. Use a different inner_hit name"); + public void addInnerHitDefinition(BaseInnerHits innerHit) { + if (innerHits.containsKey(innerHit.getName())) { + throw new IllegalArgumentException("inner_hit definition with the name [" + innerHit.getName() + + "] already exists. Use a different inner_hit name"); } - innerHits.put(name, innerHit); + innerHits.put(innerHit.getName(), innerHit); } - public void addInnerHitDefinitions(Map innerHits) { - for (Map.Entry entry : innerHits.entrySet()) { - addInnerHitDefinition(entry.getKey(), entry.getValue()); - } - } + public static abstract class BaseInnerHits extends SubSearchContext { - public static abstract class BaseInnerHits extends FilteredSearchContext { + private final String name; + private InnerHitsContext childInnerHits; - protected final ParsedQuery query; - private final InnerHitsContext childInnerHits; - - protected BaseInnerHits(SearchContext context, ParsedQuery query, Map childInnerHits) { + protected BaseInnerHits(String name, SearchContext context) { super(context); - this.query = query; - if (childInnerHits != null && !childInnerHits.isEmpty()) { - this.childInnerHits = new InnerHitsContext(childInnerHits); - } else { - this.childInnerHits = null; - } - } - - @Override - public Query query() { - return query.query(); - } - - @Override - public ParsedQuery parsedQuery() { - return query; + this.name = name; } public abstract TopDocs topDocs(SearchContext context, FetchSubPhase.HitContext hitContext) throws IOException; + public String getName() { + return name; + } + @Override public InnerHitsContext innerHits() { return childInnerHits; } + public void setChildInnerHits(Map childInnerHits) { + this.childInnerHits = new InnerHitsContext(childInnerHits); + } } public static final class NestedInnerHits extends BaseInnerHits { @@ -130,8 +117,8 @@ public final class InnerHitsContext { private final ObjectMapper parentObjectMapper; private final ObjectMapper childObjectMapper; - public NestedInnerHits(SearchContext context, ParsedQuery query, Map childInnerHits, ObjectMapper parentObjectMapper, ObjectMapper childObjectMapper) { - super(context, query, childInnerHits); + public NestedInnerHits(String name, SearchContext context, ObjectMapper parentObjectMapper, ObjectMapper childObjectMapper) { + super(name != null ? name : childObjectMapper.fullPath(), context); this.parentObjectMapper = parentObjectMapper; this.childObjectMapper = childObjectMapper; } @@ -146,7 +133,7 @@ public final class InnerHitsContext { } BitSetProducer parentFilter = context.bitsetFilterCache().getBitSetProducer(rawParentFilter); Query childFilter = childObjectMapper.nestedTypeFilter(); - Query q = Queries.filtered(query.query(), new NestedChildrenQuery(parentFilter, childFilter, hitContext)); + Query q = Queries.filtered(query(), new NestedChildrenQuery(parentFilter, childFilter, hitContext)); if (size() == 0) { return new TopDocs(context.searcher().count(q), Lucene.EMPTY_SCORE_DOCS, 0); @@ -292,8 +279,8 @@ public final class InnerHitsContext { private final MapperService mapperService; private final DocumentMapper documentMapper; - public ParentChildInnerHits(SearchContext context, ParsedQuery query, Map childInnerHits, MapperService mapperService, DocumentMapper documentMapper) { - super(context, query, childInnerHits); + public ParentChildInnerHits(String name, SearchContext context, MapperService mapperService, DocumentMapper documentMapper) { + super(name != null ? name : documentMapper.type(), context); this.mapperService = mapperService; this.documentMapper = documentMapper; } @@ -317,7 +304,7 @@ public final class InnerHitsContext { } BooleanQuery q = new BooleanQuery.Builder() - .add(query.query(), Occur.MUST) + .add(query(), Occur.MUST) // Only include docs that have the current hit as parent .add(hitQuery, Occur.FILTER) // Only include docs that have this inner hits type diff --git a/core/src/main/java/org/elasticsearch/search/fetch/innerhits/InnerHitsFetchSubPhase.java b/core/src/main/java/org/elasticsearch/search/fetch/innerhits/InnerHitsFetchSubPhase.java index f9cf3f09f39..cd849a65aa8 100644 --- a/core/src/main/java/org/elasticsearch/search/fetch/innerhits/InnerHitsFetchSubPhase.java +++ b/core/src/main/java/org/elasticsearch/search/fetch/innerhits/InnerHitsFetchSubPhase.java @@ -28,36 +28,30 @@ import org.elasticsearch.search.SearchParseElement; import org.elasticsearch.search.fetch.FetchPhase; import org.elasticsearch.search.fetch.FetchSearchResult; import org.elasticsearch.search.fetch.FetchSubPhase; -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.InternalSearchHit; import org.elasticsearch.search.internal.InternalSearchHits; import org.elasticsearch.search.internal.SearchContext; import java.io.IOException; +import java.util.Collections; import java.util.HashMap; import java.util.Map; -import static java.util.Collections.singletonMap; - /** */ public class InnerHitsFetchSubPhase implements FetchSubPhase { - private final Map parseElements; private FetchPhase fetchPhase; @Inject - public InnerHitsFetchSubPhase(FetchSourceParseElement sourceParseElement, HighlighterParseElement highlighterParseElement, FieldDataFieldsParseElement fieldDataFieldsParseElement, ScriptFieldsParseElement scriptFieldsParseElement) { - parseElements = singletonMap("inner_hits", new InnerHitsParseElement(sourceParseElement, highlighterParseElement, - fieldDataFieldsParseElement, scriptFieldsParseElement)); + public InnerHitsFetchSubPhase() { } @Override public Map parseElements() { - return parseElements; + // SearchParse elements needed because everything is parsed by InnerHitBuilder and eventually put + // into the search context. + return Collections.emptyMap(); } @Override diff --git a/core/src/main/java/org/elasticsearch/search/fetch/innerhits/InnerHitsParseElement.java b/core/src/main/java/org/elasticsearch/search/fetch/innerhits/InnerHitsParseElement.java deleted file mode 100644 index fc1b2bf399f..00000000000 --- a/core/src/main/java/org/elasticsearch/search/fetch/innerhits/InnerHitsParseElement.java +++ /dev/null @@ -1,221 +0,0 @@ -/* - * 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.search.fetch.innerhits; - -import org.apache.lucene.search.Query; -import org.elasticsearch.common.xcontent.XContentParser; -import org.elasticsearch.index.mapper.DocumentMapper; -import org.elasticsearch.index.mapper.object.ObjectMapper; -import org.elasticsearch.index.query.ParsedQuery; -import org.elasticsearch.index.query.QueryShardContext; -import org.elasticsearch.search.SearchParseElement; -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 java.util.HashMap; -import java.util.Map; - -import static org.elasticsearch.index.query.support.InnerHitsQueryParserHelper.parseCommonInnerHitOptions; - -/** - */ -public class InnerHitsParseElement implements SearchParseElement { - - private final FetchSourceParseElement sourceParseElement; - private final HighlighterParseElement highlighterParseElement; - private final FieldDataFieldsParseElement fieldDataFieldsParseElement; - private final ScriptFieldsParseElement scriptFieldsParseElement; - - public InnerHitsParseElement(FetchSourceParseElement sourceParseElement, HighlighterParseElement highlighterParseElement, FieldDataFieldsParseElement fieldDataFieldsParseElement, ScriptFieldsParseElement scriptFieldsParseElement) { - this.sourceParseElement = sourceParseElement; - this.highlighterParseElement = highlighterParseElement; - this.fieldDataFieldsParseElement = fieldDataFieldsParseElement; - this.scriptFieldsParseElement = scriptFieldsParseElement; - } - - @Override - public void parse(XContentParser parser, SearchContext searchContext) throws Exception { - QueryShardContext context = searchContext.getQueryShardContext(); - context.reset(parser); - Map topLevelInnerHits = parseInnerHits(parser, context, searchContext); - if (topLevelInnerHits != null) { - InnerHitsContext innerHitsContext = searchContext.innerHits(); - innerHitsContext.addInnerHitDefinitions(topLevelInnerHits); - } - } - - private Map parseInnerHits(XContentParser parser, QueryShardContext context, SearchContext searchContext) throws Exception { - XContentParser.Token token; - Map innerHitsMap = null; - while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { - if (token != XContentParser.Token.FIELD_NAME) { - throw new IllegalArgumentException("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(); - if (token != XContentParser.Token.START_OBJECT) { - throw new IllegalArgumentException("Inner hit definition for [" + innerHitName + " starts with a [" + token + "], expected a [" + XContentParser.Token.START_OBJECT + "]."); - } - InnerHitsContext.BaseInnerHits innerHits = parseInnerHit(parser, context, searchContext, innerHitName); - if (innerHitsMap == null) { - innerHitsMap = new HashMap<>(); - } - innerHitsMap.put(innerHitName, innerHits); - } - return innerHitsMap; - } - - private InnerHitsContext.BaseInnerHits parseInnerHit(XContentParser parser, QueryShardContext context, SearchContext searchContext, String innerHitName) throws Exception { - XContentParser.Token token = parser.nextToken(); - if (token != XContentParser.Token.FIELD_NAME) { - throw new IllegalArgumentException("Unexpected token " + token + " inside inner hit definition. Either specify [path] or [type] object"); - } - String fieldName = parser.currentName(); - token = parser.nextToken(); - if (token != XContentParser.Token.START_OBJECT) { - throw new IllegalArgumentException("Inner hit definition for [" + innerHitName + " starts with a [" + token + "], expected a [" + XContentParser.Token.START_OBJECT + "]."); - } - - String nestedPath = null; - String type = null; - switch (fieldName) { - case "path": - nestedPath = parser.currentName(); - break; - case "type": - type = parser.currentName(); - break; - default: - throw new IllegalArgumentException("Either path or type object must be defined"); - } - token = parser.nextToken(); - if (token != XContentParser.Token.FIELD_NAME) { - throw new IllegalArgumentException("Unexpected token " + token + " inside inner hit definition. Either specify [path] or [type] object"); - } - fieldName = parser.currentName(); - token = parser.nextToken(); - if (token != XContentParser.Token.START_OBJECT) { - throw new IllegalArgumentException("Inner hit definition for [" + innerHitName + " starts with a [" + token + "], expected a [" + XContentParser.Token.START_OBJECT + "]."); - } - - final InnerHitsContext.BaseInnerHits innerHits; - if (nestedPath != null) { - innerHits = parseNested(parser, context, searchContext, fieldName); - } else if (type != null) { - innerHits = parseParentChild(parser, context, searchContext, fieldName); - } else { - throw new IllegalArgumentException("Either [path] or [type] must be defined"); - } - - // Completely consume all json objects: - token = parser.nextToken(); - if (token != XContentParser.Token.END_OBJECT) { - throw new IllegalArgumentException("Expected [" + XContentParser.Token.END_OBJECT + "] token, but got a [" + token + "] token."); - } - token = parser.nextToken(); - if (token != XContentParser.Token.END_OBJECT) { - throw new IllegalArgumentException("Expected [" + XContentParser.Token.END_OBJECT + "] token, but got a [" + token + "] token."); - } - - return innerHits; - } - - private InnerHitsContext.ParentChildInnerHits parseParentChild(XContentParser parser, QueryShardContext context, SearchContext searchContext, String type) throws Exception { - ParseResult parseResult = parseSubSearchContext(searchContext, context, parser); - DocumentMapper documentMapper = searchContext.mapperService().documentMapper(type); - if (documentMapper == null) { - throw new IllegalArgumentException("type [" + type + "] doesn't exist"); - } - return new InnerHitsContext.ParentChildInnerHits(parseResult.context(), parseResult.query(), parseResult.childInnerHits(), context.getMapperService(), documentMapper); - } - - private InnerHitsContext.NestedInnerHits parseNested(XContentParser parser, QueryShardContext context, SearchContext searchContext, String nestedPath) throws Exception { - ObjectMapper objectMapper = searchContext.getObjectMapper(nestedPath); - if (objectMapper == null) { - throw new IllegalArgumentException("path [" + nestedPath +"] doesn't exist"); - } - if (objectMapper.nested().isNested() == false) { - throw new IllegalArgumentException("path [" + nestedPath +"] isn't nested"); - } - ObjectMapper parentObjectMapper = context.nestedScope().nextLevel(objectMapper); - ParseResult parseResult = parseSubSearchContext(searchContext, context, parser); - context.nestedScope().previousLevel(); - - return new InnerHitsContext.NestedInnerHits(parseResult.context(), parseResult.query(), parseResult.childInnerHits(), parentObjectMapper, objectMapper); - } - - private ParseResult parseSubSearchContext(SearchContext searchContext, QueryShardContext context, XContentParser parser) throws Exception { - ParsedQuery query = null; - Map childInnerHits = null; - SubSearchContext subSearchContext = new SubSearchContext(searchContext); - String fieldName = null; - XContentParser.Token token; - while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { - if (token == XContentParser.Token.FIELD_NAME) { - fieldName = parser.currentName(); - } else if (token == XContentParser.Token.START_OBJECT) { - if ("query".equals(fieldName)) { - Query q = context.parseInnerQuery(); - query = new ParsedQuery(q, context.copyNamedQueries()); - } else if ("inner_hits".equals(fieldName)) { - childInnerHits = parseInnerHits(parser, context, searchContext); - } else { - parseCommonInnerHitOptions(parser, token, fieldName, subSearchContext, sourceParseElement, highlighterParseElement, scriptFieldsParseElement, fieldDataFieldsParseElement); - } - } else { - parseCommonInnerHitOptions(parser, token, fieldName, subSearchContext, sourceParseElement, highlighterParseElement, scriptFieldsParseElement, fieldDataFieldsParseElement); - } - } - - if (query == null) { - query = ParsedQuery.parsedMatchAllQuery(); - } - return new ParseResult(subSearchContext, query, childInnerHits); - } - - private static final class ParseResult { - - private final SubSearchContext context; - private final ParsedQuery query; - private final Map childInnerHits; - - private ParseResult(SubSearchContext context, ParsedQuery query, Map childInnerHits) { - this.context = context; - this.query = query; - this.childInnerHits = childInnerHits; - } - - public SubSearchContext context() { - return context; - } - - public ParsedQuery query() { - return query; - } - - public Map childInnerHits() { - return childInnerHits; - } - } -} diff --git a/core/src/main/java/org/elasticsearch/search/fetch/innerhits/InnerHitsSubSearchContext.java b/core/src/main/java/org/elasticsearch/search/fetch/innerhits/InnerHitsSubSearchContext.java deleted file mode 100644 index 35ab2e1bba7..00000000000 --- a/core/src/main/java/org/elasticsearch/search/fetch/innerhits/InnerHitsSubSearchContext.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * 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.search.fetch.innerhits; - -import org.elasticsearch.search.internal.SubSearchContext; - -public class InnerHitsSubSearchContext { - private final String name; - private final SubSearchContext subSearchContext; - - public InnerHitsSubSearchContext(String name, SubSearchContext subSearchContext) { - this.name = name; - this.subSearchContext = subSearchContext; - } - - public String getName() { - return name; - } - - public SubSearchContext getSubSearchContext() { - return subSearchContext; - } -} diff --git a/core/src/main/java/org/elasticsearch/search/internal/SubSearchContext.java b/core/src/main/java/org/elasticsearch/search/internal/SubSearchContext.java index c4880814fcc..c57502741e9 100644 --- a/core/src/main/java/org/elasticsearch/search/internal/SubSearchContext.java +++ b/core/src/main/java/org/elasticsearch/search/internal/SubSearchContext.java @@ -49,6 +49,8 @@ public class SubSearchContext extends FilteredSearchContext { private int from; private int size = DEFAULT_SIZE; private Sort sort; + private ParsedQuery parsedQuery; + private Query query; private final FetchSearchResult fetchSearchResult; private final QuerySearchResult querySearchResult; @@ -185,6 +187,25 @@ public class SubSearchContext extends FilteredSearchContext { return sort; } + @Override + public SearchContext parsedQuery(ParsedQuery parsedQuery) { + this.parsedQuery = parsedQuery; + if (parsedQuery != null) { + this.query = parsedQuery.query(); + } + return this; + } + + @Override + public ParsedQuery parsedQuery() { + return parsedQuery; + } + + @Override + public Query query() { + return query; + } + @Override public SearchContext trackScores(boolean trackScores) { this.trackScores = trackScores; diff --git a/core/src/test/java/org/elasticsearch/index/query/HasChildQueryBuilderTests.java b/core/src/test/java/org/elasticsearch/index/query/HasChildQueryBuilderTests.java index 675ad954e03..1bec2f9fd50 100644 --- a/core/src/test/java/org/elasticsearch/index/query/HasChildQueryBuilderTests.java +++ b/core/src/test/java/org/elasticsearch/index/query/HasChildQueryBuilderTests.java @@ -28,7 +28,6 @@ import org.apache.lucene.search.ConstantScoreQuery; import org.apache.lucene.search.Query; import org.apache.lucene.search.TermQuery; import org.apache.lucene.search.join.ScoreMode; -import org.apache.lucene.search.similarities.DFISimilarity; import org.apache.lucene.search.similarities.PerFieldSimilarityWrapper; import org.apache.lucene.search.similarities.Similarity; import org.apache.lucene.util.BytesRef; @@ -42,18 +41,17 @@ import org.elasticsearch.index.mapper.MapperService; import org.elasticsearch.index.mapper.Uid; import org.elasticsearch.index.mapper.internal.TypeFieldMapper; import org.elasticsearch.index.mapper.internal.UidFieldMapper; -import org.elasticsearch.index.query.support.QueryInnerHits; +import org.elasticsearch.index.query.support.InnerHitBuilder; import org.elasticsearch.index.similarity.SimilarityService; import org.elasticsearch.script.Script.ScriptParseException; -import org.elasticsearch.search.fetch.innerhits.InnerHitsBuilder; import org.elasticsearch.search.fetch.innerhits.InnerHitsContext; import org.elasticsearch.search.internal.SearchContext; +import org.elasticsearch.search.sort.FieldSortBuilder; import org.elasticsearch.search.sort.SortOrder; import org.elasticsearch.test.TestSearchContext; import org.junit.BeforeClass; import java.io.IOException; -import java.util.ArrayList; import java.util.Collections; import static org.hamcrest.CoreMatchers.equalTo; @@ -118,11 +116,13 @@ public class HasChildQueryBuilderTests extends AbstractQueryTestCase randomAsciiOfLengthBetween(1, 16))); + innerHits.setFieldDataFields(randomListStuff(16, () -> randomAsciiOfLengthBetween(1, 16))); + innerHits.setScriptFields(randomListStuff(16, InnerHitBuilderTests::randomScript)); + FetchSourceContext randomFetchSourceContext; + if (randomBoolean()) { + randomFetchSourceContext = new FetchSourceContext(randomBoolean()); + } else { + randomFetchSourceContext = new FetchSourceContext( + generateRandomStringArray(12, 16, false), + generateRandomStringArray(12, 16, false) + ); + } + innerHits.setFetchSourceContext(randomFetchSourceContext); + if (randomBoolean()) { + innerHits.setSorts(randomListStuff(16, + () -> SortBuilders.fieldSort(randomAsciiOfLengthBetween(5, 20)).order(randomFrom(SortOrder.values()))) + ); + } + innerHits.setHighlightBuilder(HighlightBuilderTests.randomHighlighterBuilder()); + if (randomBoolean()) { + innerHits.setQuery(new MatchQueryBuilder(randomAsciiOfLengthBetween(1, 16), randomAsciiOfLengthBetween(1, 16))); + } + if (recursive && randomBoolean()) { + InnerHitsBuilder innerHitsBuilder = new InnerHitsBuilder(); + int size = randomIntBetween(1, 16); + for (int i = 0; i < size; i++) { + innerHitsBuilder.addInnerHit(randomAsciiOfLengthBetween(1, 16), randomInnerHits(false)); + } + innerHits.setInnerHitsBuilder(innerHitsBuilder); + } + + return innerHits; + } + + static InnerHitBuilder mutate(InnerHitBuilder innerHits) throws IOException { + InnerHitBuilder copy = serializedCopy(innerHits); + int surprise = randomIntBetween(0, 10); + switch (surprise) { + case 0: + copy.setFrom(randomValueOtherThan(innerHits.getFrom(), () -> randomIntBetween(0, 128))); + break; + case 1: + copy.setSize(randomValueOtherThan(innerHits.getSize(), () -> randomIntBetween(0, 128))); + break; + case 2: + copy.setExplain(!copy.isExplain()); + break; + case 3: + copy.setVersion(!copy.isVersion()); + break; + case 4: + copy.setTrackScores(!copy.isTrackScores()); + break; + case 5: + copy.setName(randomValueOtherThan(innerHits.getName(), () -> randomAsciiOfLengthBetween(1, 16))); + break; + case 6: + copy.setFieldDataFields(randomValueOtherThan(copy.getFieldDataFields(), () -> { + return randomListStuff(16, () -> randomAsciiOfLengthBetween(1, 16)); + })); + break; + case 7: + copy.setScriptFields(randomValueOtherThan(copy.getScriptFields(), () -> { + return randomListStuff(16, InnerHitBuilderTests::randomScript);})); + break; + case 8: + copy.setFetchSourceContext(randomValueOtherThan(copy.getFetchSourceContext(), () -> { + FetchSourceContext randomFetchSourceContext; + if (randomBoolean()) { + randomFetchSourceContext = new FetchSourceContext(randomBoolean()); + } else { + randomFetchSourceContext = new FetchSourceContext( + generateRandomStringArray(12, 16, false), + generateRandomStringArray(12, 16, false) + ); + } + return randomFetchSourceContext; + })); + break; + case 9: + copy.setSorts(randomValueOtherThan(copy.getSorts(), () -> { + return randomListStuff(16, + () -> SortBuilders.fieldSort(randomAsciiOfLengthBetween(5, 20)).order(randomFrom(SortOrder.values()))); + })); + break; + case 10: + copy.setHighlightBuilder(randomValueOtherThan(copy.getHighlightBuilder(), + HighlightBuilderTests::randomHighlighterBuilder)); + break; + default: + throw new IllegalStateException("unexpected surprise [" + surprise + "]"); + } + return copy; + } + + static SearchSourceBuilder.ScriptField randomScript() { + ScriptService.ScriptType randomScriptType = randomFrom(ScriptService.ScriptType.values()); + Map randomMap = null; + if (randomBoolean()) { + randomMap = new HashMap<>(); + int numEntries = randomIntBetween(0, 32); + for (int i = 0; i < numEntries; i++) { + randomMap.put(String.valueOf(i), randomAsciiOfLength(16)); + } + } + Script script = new Script(randomAsciiOfLength(128), randomScriptType, randomAsciiOfLengthBetween(1, 4),randomMap); + return new SearchSourceBuilder.ScriptField(randomAsciiOfLengthBetween(1, 32), script, randomBoolean()); + } + + static List randomListStuff(int maxSize, Supplier valueSupplier) { + int size = randomIntBetween(0, maxSize); + List list = new ArrayList<>(size); + for (int i = 0; i < size; i++) { + list.add(valueSupplier.get()); + } + return list; + } + + private static InnerHitBuilder serializedCopy(InnerHitBuilder original) throws IOException { + try (BytesStreamOutput output = new BytesStreamOutput()) { + original.writeTo(output); + try (StreamInput in = new NamedWriteableAwareStreamInput(StreamInput.wrap(output.bytes()), namedWriteableRegistry)) { + return new InnerHitBuilder(in); + } + } + } + +} diff --git a/core/src/test/java/org/elasticsearch/index/query/support/InnerHitsBuilderTests.java b/core/src/test/java/org/elasticsearch/index/query/support/InnerHitsBuilderTests.java new file mode 100644 index 00000000000..9a959f90c57 --- /dev/null +++ b/core/src/test/java/org/elasticsearch/index/query/support/InnerHitsBuilderTests.java @@ -0,0 +1,157 @@ +/* + * 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.ParseFieldMatcher; +import org.elasticsearch.common.io.stream.BytesStreamOutput; +import org.elasticsearch.common.io.stream.NamedWriteableAwareStreamInput; +import org.elasticsearch.common.io.stream.NamedWriteableRegistry; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.xcontent.ToXContent; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentFactory; +import org.elasticsearch.common.xcontent.XContentHelper; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.common.xcontent.XContentType; +import org.elasticsearch.index.query.MatchQueryBuilder; +import org.elasticsearch.index.query.QueryParseContext; +import org.elasticsearch.indices.query.IndicesQueriesRegistry; +import org.elasticsearch.script.Script; +import org.elasticsearch.script.ScriptService; +import org.elasticsearch.search.SearchModule; +import org.elasticsearch.search.builder.SearchSourceBuilder; +import org.elasticsearch.search.fetch.source.FetchSourceContext; +import org.elasticsearch.search.highlight.HighlightBuilderTests; +import org.elasticsearch.search.sort.FieldSortBuilder; +import org.elasticsearch.search.sort.SortBuilder; +import org.elasticsearch.search.sort.SortBuilders; +import org.elasticsearch.search.sort.SortOrder; +import org.elasticsearch.test.ESTestCase; +import org.junit.AfterClass; +import org.junit.BeforeClass; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Supplier; + +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.sameInstance; + +public class InnerHitsBuilderTests extends ESTestCase { + + private static final int NUMBER_OF_TESTBUILDERS = 20; + private static NamedWriteableRegistry namedWriteableRegistry; + private static IndicesQueriesRegistry indicesQueriesRegistry; + + @BeforeClass + public static void init() { + namedWriteableRegistry = new NamedWriteableRegistry(); + indicesQueriesRegistry = new SearchModule(Settings.EMPTY, namedWriteableRegistry).buildQueryParserRegistry(); + } + + @AfterClass + public static void afterClass() throws Exception { + namedWriteableRegistry = null; + indicesQueriesRegistry = null; + } + + public void testSerialization() throws Exception { + for (int runs = 0; runs < NUMBER_OF_TESTBUILDERS; runs++) { + InnerHitsBuilder original = randomInnerHits(); + InnerHitsBuilder deserialized = serializedCopy(original); + assertEquals(deserialized, original); + assertEquals(deserialized.hashCode(), original.hashCode()); + assertNotSame(deserialized, original); + } + } + + public void testFromAndToXContent() throws Exception { + QueryParseContext context = new QueryParseContext(indicesQueriesRegistry); + context.parseFieldMatcher(new ParseFieldMatcher(Settings.EMPTY)); + for (int runs = 0; runs < NUMBER_OF_TESTBUILDERS; runs++) { + InnerHitsBuilder innerHits = randomInnerHits(); + XContentBuilder builder = XContentFactory.contentBuilder(randomFrom(XContentType.values())); + if (randomBoolean()) { + builder.prettyPrint(); + } + innerHits.toXContent(builder, ToXContent.EMPTY_PARAMS); + + XContentParser parser = XContentHelper.createParser(builder.bytes()); + context.reset(parser); + parser.nextToken(); + InnerHitsBuilder secondInnerHits = InnerHitsBuilder.fromXContent(parser, context); + assertThat(innerHits, not(sameInstance(secondInnerHits))); + assertThat(innerHits, equalTo(secondInnerHits)); + assertThat(innerHits.hashCode(), equalTo(secondInnerHits.hashCode())); + } + } + + public void testEqualsAndHashcode() throws IOException { + for (int runs = 0; runs < NUMBER_OF_TESTBUILDERS; runs++) { + InnerHitsBuilder firstInnerHits = randomInnerHits(); + assertFalse("inner hit is equal to null", firstInnerHits.equals(null)); + assertFalse("inner hit is equal to incompatible type", firstInnerHits.equals("")); + assertTrue("inner it is not equal to self", firstInnerHits.equals(firstInnerHits)); + assertThat("same inner hit's hashcode returns different values if called multiple times", firstInnerHits.hashCode(), + equalTo(firstInnerHits.hashCode())); + + InnerHitsBuilder secondBuilder = serializedCopy(firstInnerHits); + assertTrue("inner hit is not equal to self", secondBuilder.equals(secondBuilder)); + assertTrue("inner hit is not equal to its copy", firstInnerHits.equals(secondBuilder)); + assertTrue("equals is not symmetric", secondBuilder.equals(firstInnerHits)); + assertThat("inner hits copy's hashcode is different from original hashcode", secondBuilder.hashCode(), + equalTo(firstInnerHits.hashCode())); + + InnerHitsBuilder thirdBuilder = serializedCopy(secondBuilder); + assertTrue("inner hit is not equal to self", thirdBuilder.equals(thirdBuilder)); + assertTrue("inner hit is not equal to its copy", secondBuilder.equals(thirdBuilder)); + assertThat("inner hit copy's hashcode is different from original hashcode", secondBuilder.hashCode(), + equalTo(thirdBuilder.hashCode())); + assertTrue("equals is not transitive", firstInnerHits.equals(thirdBuilder)); + assertThat("inner hit copy's hashcode is different from original hashcode", firstInnerHits.hashCode(), + equalTo(thirdBuilder.hashCode())); + assertTrue("equals is not symmetric", thirdBuilder.equals(secondBuilder)); + assertTrue("equals is not symmetric", thirdBuilder.equals(firstInnerHits)); + } + } + + public static InnerHitsBuilder randomInnerHits() { + InnerHitsBuilder innerHits = new InnerHitsBuilder(); + int numInnerHits = randomIntBetween(0, 12); + for (int i = 0; i < numInnerHits; i++) { + innerHits.addInnerHit(randomAsciiOfLength(5), InnerHitBuilderTests.randomInnerHits()); + } + return innerHits; + } + + private static InnerHitsBuilder serializedCopy(InnerHitsBuilder original) throws IOException { + try (BytesStreamOutput output = new BytesStreamOutput()) { + original.writeTo(output); + try (StreamInput in = new NamedWriteableAwareStreamInput(StreamInput.wrap(output.bytes()), namedWriteableRegistry)) { + return InnerHitsBuilder.PROTO.readFrom(in); + } + } + } + +} diff --git a/core/src/test/java/org/elasticsearch/index/query/support/QueryInnerHitsTests.java b/core/src/test/java/org/elasticsearch/index/query/support/QueryInnerHitsTests.java deleted file mode 100644 index efcf17b4af5..00000000000 --- a/core/src/test/java/org/elasticsearch/index/query/support/QueryInnerHitsTests.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * 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.io.stream.BytesStreamOutput; -import org.elasticsearch.common.io.stream.StreamInput; -import org.elasticsearch.common.xcontent.ToXContent; -import org.elasticsearch.common.xcontent.XContentBuilder; -import org.elasticsearch.common.xcontent.XContentFactory; -import org.elasticsearch.common.xcontent.XContentParser; -import org.elasticsearch.search.fetch.innerhits.InnerHitsBuilder; -import org.elasticsearch.test.ESTestCase; - -import java.io.IOException; - -public class QueryInnerHitsTests extends ESTestCase { - - public void testSerialize() throws IOException { - copyAndAssert(new QueryInnerHits()); - copyAndAssert(new QueryInnerHits("foo", new InnerHitsBuilder.InnerHit())); - copyAndAssert(new QueryInnerHits("foo", null)); - copyAndAssert(new QueryInnerHits("foo", new InnerHitsBuilder.InnerHit().setSize(randomIntBetween(0, 100)))); - } - - public void testToXContent() throws IOException { - assertJson("{\"inner_hits\":{}}", new QueryInnerHits()); - assertJson("{\"inner_hits\":{\"name\":\"foo\"}}", new QueryInnerHits("foo", new InnerHitsBuilder.InnerHit())); - assertJson("{\"inner_hits\":{\"name\":\"bar\"}}", new QueryInnerHits("bar", null)); - assertJson("{\"inner_hits\":{\"name\":\"foo\",\"size\":42}}", new QueryInnerHits("foo", new InnerHitsBuilder.InnerHit().setSize(42))); - assertJson("{\"inner_hits\":{\"name\":\"boom\",\"from\":66,\"size\":666}}", new QueryInnerHits("boom", new InnerHitsBuilder.InnerHit().setFrom(66).setSize(666))); - } - - private void assertJson(String expected, QueryInnerHits hits) throws IOException { - QueryInnerHits queryInnerHits = copyAndAssert(hits); - String actual; - if (randomBoolean()) { - actual = oneLineJSON(queryInnerHits); - } else { - actual = oneLineJSON(hits); - } - assertEquals(expected, actual); - XContentParser parser = hits.getXcontentParser(); - assertEquals(XContentParser.Token.START_OBJECT, parser.nextToken()); - QueryInnerHits other = copyAndAssert(new QueryInnerHits(parser)); - assertEquals(expected, oneLineJSON(other)); - } - - public QueryInnerHits copyAndAssert(QueryInnerHits hits) throws IOException { - BytesStreamOutput out = new BytesStreamOutput(); - hits.writeTo(out); - QueryInnerHits copy = randomBoolean() ? hits.readFrom(StreamInput.wrap(out.bytes())) : new QueryInnerHits(StreamInput.wrap(out.bytes())); - assertEquals(copy.toString() + " vs. " + hits.toString(), copy, hits); - return copy; - } - - private String oneLineJSON(QueryInnerHits hits) throws IOException { - XContentBuilder builder = XContentFactory.jsonBuilder(); - builder.startObject(); - hits.toXContent(builder, ToXContent.EMPTY_PARAMS); - builder.endObject(); - return builder.string().trim(); - } -} diff --git a/core/src/test/java/org/elasticsearch/percolator/PercolatorIT.java b/core/src/test/java/org/elasticsearch/percolator/PercolatorIT.java index f6f5d260550..80a0e5ce502 100644 --- a/core/src/test/java/org/elasticsearch/percolator/PercolatorIT.java +++ b/core/src/test/java/org/elasticsearch/percolator/PercolatorIT.java @@ -43,7 +43,7 @@ import org.elasticsearch.index.query.Operator; import org.elasticsearch.index.query.QueryBuilders; import org.elasticsearch.index.query.QueryShardException; import org.elasticsearch.index.query.functionscore.weight.WeightBuilder; -import org.elasticsearch.index.query.support.QueryInnerHits; +import org.elasticsearch.index.query.support.InnerHitBuilder; import org.elasticsearch.search.highlight.HighlightBuilder; import org.elasticsearch.test.ESIntegTestCase; @@ -1789,7 +1789,7 @@ public class PercolatorIT extends ESIntegTestCase { assertAcked(prepareCreate("index").addMapping("mapping", mapping)); try { client().prepareIndex("index", PercolatorFieldMapper.TYPE_NAME, "1") - .setSource(jsonBuilder().startObject().field("query", nestedQuery("nested", matchQuery("nested.name", "value")).innerHit(new QueryInnerHits())).endObject()) + .setSource(jsonBuilder().startObject().field("query", nestedQuery("nested", matchQuery("nested.name", "value")).innerHit(new InnerHitBuilder())).endObject()) .execute().actionGet(); fail("Expected a parse error, because inner_hits isn't supported in the percolate api"); } catch (Exception e) { diff --git a/core/src/test/java/org/elasticsearch/search/builder/SearchSourceBuilderTests.java b/core/src/test/java/org/elasticsearch/search/builder/SearchSourceBuilderTests.java index 974e8f0c519..b2461ff3578 100644 --- a/core/src/test/java/org/elasticsearch/search/builder/SearchSourceBuilderTests.java +++ b/core/src/test/java/org/elasticsearch/search/builder/SearchSourceBuilderTests.java @@ -52,6 +52,7 @@ import org.elasticsearch.index.query.AbstractQueryTestCase; import org.elasticsearch.index.query.EmptyQueryBuilder; import org.elasticsearch.index.query.QueryBuilders; import org.elasticsearch.index.query.QueryParseContext; +import org.elasticsearch.index.query.support.InnerHitBuilderTests; import org.elasticsearch.indices.IndicesModule; import org.elasticsearch.indices.breaker.CircuitBreakerService; import org.elasticsearch.indices.breaker.NoneCircuitBreakerService; @@ -68,8 +69,7 @@ import org.elasticsearch.script.ScriptSettings; import org.elasticsearch.search.SearchModule; import org.elasticsearch.search.aggregations.AggregationBuilders; import org.elasticsearch.search.aggregations.AggregatorParsers; -import org.elasticsearch.search.fetch.innerhits.InnerHitsBuilder; -import org.elasticsearch.search.fetch.innerhits.InnerHitsBuilder.InnerHit; +import org.elasticsearch.index.query.support.InnerHitsBuilder; import org.elasticsearch.search.fetch.source.FetchSourceContext; import org.elasticsearch.search.highlight.HighlightBuilderTests; import org.elasticsearch.search.rescore.QueryRescoreBuilderTests; @@ -417,11 +417,11 @@ public class SearchSourceBuilderTests extends ESTestCase { builder.suggest(SuggestBuilderTests.randomSuggestBuilder()); } if (randomBoolean()) { - // NORELEASE need a random inner hits builder method InnerHitsBuilder innerHitsBuilder = new InnerHitsBuilder(); - InnerHit innerHit = new InnerHit(); - innerHit.field(randomAsciiOfLengthBetween(5, 20)); - innerHitsBuilder.addNestedInnerHits(randomAsciiOfLengthBetween(5, 20), randomAsciiOfLengthBetween(5, 20), innerHit); + int num = randomIntBetween(0, 3); + for (int i = 0; i < num; i++) { + innerHitsBuilder.addInnerHit(randomAsciiOfLengthBetween(5, 20), InnerHitBuilderTests.randomInnerHits()); + } builder.innerHits(innerHitsBuilder); } if (randomBoolean()) { diff --git a/core/src/test/java/org/elasticsearch/search/innerhits/InnerHitsIT.java b/core/src/test/java/org/elasticsearch/search/innerhits/InnerHitsIT.java index fe7aa4e9613..630cd37f7e7 100644 --- a/core/src/test/java/org/elasticsearch/search/innerhits/InnerHitsIT.java +++ b/core/src/test/java/org/elasticsearch/search/innerhits/InnerHitsIT.java @@ -27,15 +27,16 @@ import org.elasticsearch.cluster.health.ClusterHealthStatus; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.index.query.BoolQueryBuilder; import org.elasticsearch.index.query.MatchAllQueryBuilder; -import org.elasticsearch.index.query.support.QueryInnerHits; +import org.elasticsearch.index.query.support.InnerHitBuilder; +import org.elasticsearch.index.query.support.InnerHitsBuilder; import org.elasticsearch.plugins.Plugin; import org.elasticsearch.script.MockScriptEngine; import org.elasticsearch.script.Script; import org.elasticsearch.script.ScriptService; import org.elasticsearch.search.SearchHit; import org.elasticsearch.search.SearchHits; -import org.elasticsearch.search.fetch.innerhits.InnerHitsBuilder; import org.elasticsearch.search.highlight.HighlightBuilder; +import org.elasticsearch.search.sort.FieldSortBuilder; import org.elasticsearch.search.sort.SortBuilders; import org.elasticsearch.search.sort.SortOrder; import org.elasticsearch.test.ESIntegTestCase; @@ -75,7 +76,8 @@ public class InnerHitsIT extends ESIntegTestCase { } public void testSimpleNested() throws Exception { - assertAcked(prepareCreate("articles").addMapping("article", jsonBuilder().startObject().startObject("article").startObject("properties") + assertAcked(prepareCreate("articles").addMapping("article", jsonBuilder().startObject().startObject("article") + .startObject("properties") .startObject("comments") .field("type", "nested") .startObject("properties") @@ -110,11 +112,14 @@ public class InnerHitsIT extends ESIntegTestCase { indexRandom(true, requests); InnerHitsBuilder innerHitsBuilder = new InnerHitsBuilder(); - innerHitsBuilder.addNestedInnerHits("comment", "comments", - new InnerHitsBuilder.InnerHit().setQuery(matchQuery("comments.message", "fox"))); + innerHitsBuilder.addInnerHit("comment", new InnerHitBuilder() + .setNestedPath("comments") + .setQuery(matchQuery("comments.message", "fox")) + ); // Inner hits can be defined in two ways: 1) with the query 2) as separate inner_hit definition SearchRequest[] searchRequests = new SearchRequest[]{ - client().prepareSearch("articles").setQuery(nestedQuery("comments", matchQuery("comments.message", "fox")).innerHit(new QueryInnerHits("comment", null))).request(), + client().prepareSearch("articles").setQuery(nestedQuery("comments", matchQuery("comments.message", "fox")) + .innerHit(new InnerHitBuilder().setName("comment"))).request(), client().prepareSearch("articles").setQuery(nestedQuery("comments", matchQuery("comments.message", "fox"))) .innerHits(innerHitsBuilder).request() }; @@ -136,8 +141,9 @@ public class InnerHitsIT extends ESIntegTestCase { } innerHitsBuilder = new InnerHitsBuilder(); - innerHitsBuilder.addNestedInnerHits("comment", "comments", - new InnerHitsBuilder.InnerHit().setQuery(matchQuery("comments.message", "elephant"))); + innerHitsBuilder.addInnerHit("comment", new InnerHitBuilder() + .setQuery(matchQuery("comments.message", "elephant")).setNestedPath("comments") + ); // Inner hits can be defined in two ways: 1) with the query 2) as // separate inner_hit definition searchRequests = new SearchRequest[] { @@ -145,9 +151,9 @@ public class InnerHitsIT extends ESIntegTestCase { .setQuery(nestedQuery("comments", matchQuery("comments.message", "elephant"))) .innerHits(innerHitsBuilder).request(), client().prepareSearch("articles") - .setQuery(nestedQuery("comments", matchQuery("comments.message", "elephant")).innerHit(new QueryInnerHits("comment", null))).request(), + .setQuery(nestedQuery("comments", matchQuery("comments.message", "elephant")).innerHit(new InnerHitBuilder().setName("comment"))).request(), client().prepareSearch("articles") - .setQuery(nestedQuery("comments", matchQuery("comments.message", "elephant")).innerHit(new QueryInnerHits("comment", new InnerHitsBuilder.InnerHit().addSort("_doc", SortOrder.DESC)))).request() + .setQuery(nestedQuery("comments", matchQuery("comments.message", "elephant")).innerHit(new InnerHitBuilder().setName("comment").addSort(new FieldSortBuilder("_doc").order(SortOrder.DESC)))).request() }; for (SearchRequest searchRequest : searchRequests) { SearchResponse response = client().search(searchRequest).actionGet(); @@ -169,26 +175,28 @@ public class InnerHitsIT extends ESIntegTestCase { assertThat(innerHits.getAt(2).getNestedIdentity().getField().string(), equalTo("comments")); assertThat(innerHits.getAt(2).getNestedIdentity().getOffset(), equalTo(2)); } - InnerHitsBuilder.InnerHit innerHit = new InnerHitsBuilder.InnerHit(); - innerHit.highlighter(new HighlightBuilder().field("comments.message")); + InnerHitBuilder innerHit = new InnerHitBuilder(); + innerHit.setNestedPath("comments"); + innerHit.setQuery(matchQuery("comments.message", "fox")); + innerHit.setHighlightBuilder(new HighlightBuilder().field("comments.message")); innerHit.setExplain(true); innerHit.addFieldDataField("comments.message"); innerHit.addScriptField("script", new Script("5", ScriptService.ScriptType.INLINE, MockScriptEngine.NAME, Collections.emptyMap())); innerHit.setSize(1); innerHitsBuilder = new InnerHitsBuilder(); - innerHitsBuilder.addNestedInnerHits("comments", "comments", new InnerHitsBuilder.InnerHit() - .setQuery(matchQuery("comments.message", "fox")) - .highlighter(new HighlightBuilder().field("comments.message")) - .setExplain(true) - .addFieldDataField("comments.message") - .addScriptField("script", new Script("5", ScriptService.ScriptType.INLINE, MockScriptEngine.NAME, Collections.emptyMap())) - .setSize(1)); + innerHitsBuilder.addInnerHit("comments", innerHit); searchRequests = new SearchRequest[] { client().prepareSearch("articles") .setQuery(nestedQuery("comments", matchQuery("comments.message", "fox"))) .innerHits(innerHitsBuilder).request(), client().prepareSearch("articles") - .setQuery(nestedQuery("comments", matchQuery("comments.message", "fox")).innerHit(new QueryInnerHits(null, innerHit))).request() + .setQuery(nestedQuery("comments", matchQuery("comments.message", "fox")).innerHit( + new InnerHitBuilder().setHighlightBuilder(new HighlightBuilder().field("comments.message")) + .setExplain(true) + .addFieldDataField("comments.message") + .addScriptField("script", new Script("5", ScriptService.ScriptType.INLINE, MockScriptEngine.NAME, Collections.emptyMap())) + .setSize(1) + )).request() }; for (SearchRequest searchRequest : searchRequests) { @@ -231,8 +239,11 @@ public class InnerHitsIT extends ESIntegTestCase { SearchResponse searchResponse; if (randomBoolean()) { InnerHitsBuilder innerHitsBuilder = new InnerHitsBuilder(); - innerHitsBuilder.addNestedInnerHits("a", "field1", new InnerHitsBuilder.InnerHit().addSort("_doc", SortOrder.DESC).setSize(size)); // Sort order is DESC, because we reverse the inner objects during indexing! - innerHitsBuilder.addNestedInnerHits("b", "field2", new InnerHitsBuilder.InnerHit().addSort("_doc", SortOrder.DESC).setSize(size)); + innerHitsBuilder.addInnerHit("a", new InnerHitBuilder().setNestedPath("field1") + // Sort order is DESC, because we reverse the inner objects during indexing! + .addSort(new FieldSortBuilder("_doc").order(SortOrder.DESC)).setSize(size)); + innerHitsBuilder.addInnerHit("b", new InnerHitBuilder().setNestedPath("field2") + .addSort(new FieldSortBuilder("_doc").order(SortOrder.DESC)).setSize(size)); searchResponse = client().prepareSearch("idx") .setSize(numDocs) .addSort("_uid", SortOrder.ASC) @@ -241,11 +252,15 @@ public class InnerHitsIT extends ESIntegTestCase { } else { BoolQueryBuilder boolQuery = new BoolQueryBuilder(); if (randomBoolean()) { - boolQuery.should(nestedQuery("field1", matchAllQuery()).innerHit(new QueryInnerHits("a", new InnerHitsBuilder.InnerHit().addSort("_doc", SortOrder.DESC).setSize(size)))); - boolQuery.should(nestedQuery("field2", matchAllQuery()).innerHit(new QueryInnerHits("b", new InnerHitsBuilder.InnerHit().addSort("_doc", SortOrder.DESC).setSize(size)))); + boolQuery.should(nestedQuery("field1", matchAllQuery()).innerHit(new InnerHitBuilder().setName("a").setSize(size) + .addSort(new FieldSortBuilder("_doc").order(SortOrder.DESC)))); + boolQuery.should(nestedQuery("field2", matchAllQuery()).innerHit(new InnerHitBuilder().setName("b") + .addSort(new FieldSortBuilder("_doc").order(SortOrder.DESC)).setSize(size))); } else { - boolQuery.should(constantScoreQuery(nestedQuery("field1", matchAllQuery()).innerHit(new QueryInnerHits("a", new InnerHitsBuilder.InnerHit().addSort("_doc", SortOrder.DESC).setSize(size))))); - boolQuery.should(constantScoreQuery(nestedQuery("field2", matchAllQuery()).innerHit(new QueryInnerHits("b", new InnerHitsBuilder.InnerHit().addSort("_doc", SortOrder.DESC).setSize(size))))); + boolQuery.should(constantScoreQuery(nestedQuery("field1", matchAllQuery()).innerHit(new InnerHitBuilder().setName("a") + .setSize(size).addSort(new FieldSortBuilder("_doc").order(SortOrder.DESC))))); + boolQuery.should(constantScoreQuery(nestedQuery("field2", matchAllQuery()).innerHit(new InnerHitBuilder().setName("b") + .setSize(size).addSort(new FieldSortBuilder("_doc").order(SortOrder.DESC))))); } searchResponse = client().prepareSearch("idx") .setQuery(boolQuery) @@ -298,14 +313,15 @@ public class InnerHitsIT extends ESIntegTestCase { indexRandom(true, requests); InnerHitsBuilder innerHitsBuilder = new InnerHitsBuilder(); - innerHitsBuilder.addParentChildInnerHits("comment", "comment", new InnerHitsBuilder.InnerHit().setQuery(matchQuery("message", "fox"))); + innerHitsBuilder.addInnerHit("comment", new InnerHitBuilder().setParentChildType("comment") + .setQuery(matchQuery("message", "fox"))); SearchRequest[] searchRequests = new SearchRequest[]{ client().prepareSearch("articles") .setQuery(hasChildQuery("comment", matchQuery("message", "fox"))) .innerHits(innerHitsBuilder) .request(), client().prepareSearch("articles") - .setQuery(hasChildQuery("comment", matchQuery("message", "fox")).innerHit(new QueryInnerHits("comment", null))) + .setQuery(hasChildQuery("comment", matchQuery("message", "fox")).innerHit(new InnerHitBuilder().setName("comment"))) .request() }; for (SearchRequest searchRequest : searchRequests) { @@ -326,14 +342,15 @@ public class InnerHitsIT extends ESIntegTestCase { } innerHitsBuilder = new InnerHitsBuilder(); - innerHitsBuilder.addParentChildInnerHits("comment", "comment", new InnerHitsBuilder.InnerHit().setQuery(matchQuery("message", "elephant"))); + innerHitsBuilder.addInnerHit("comment", new InnerHitBuilder().setParentChildType("comment") + .setQuery(matchQuery("message", "elephant"))); searchRequests = new SearchRequest[] { client().prepareSearch("articles") .setQuery(hasChildQuery("comment", matchQuery("message", "elephant"))) .innerHits(innerHitsBuilder) .request(), client().prepareSearch("articles") - .setQuery(hasChildQuery("comment", matchQuery("message", "elephant")).innerHit(new QueryInnerHits())) + .setQuery(hasChildQuery("comment", matchQuery("message", "elephant")).innerHit(new InnerHitBuilder())) .request() }; for (SearchRequest searchRequest : searchRequests) { @@ -353,20 +370,16 @@ public class InnerHitsIT extends ESIntegTestCase { assertThat(innerHits.getAt(2).getId(), equalTo("6")); assertThat(innerHits.getAt(2).type(), equalTo("comment")); } - InnerHitsBuilder.InnerHit innerHit = new InnerHitsBuilder.InnerHit(); - innerHit.highlighter(new HighlightBuilder().field("message")); + InnerHitBuilder innerHit = new InnerHitBuilder(); + innerHit.setQuery(matchQuery("message", "fox")); + innerHit.setParentChildType("comment"); + innerHit.setHighlightBuilder(new HighlightBuilder().field("message")); innerHit.setExplain(true); innerHit.addFieldDataField("message"); innerHit.addScriptField("script", new Script("5", ScriptService.ScriptType.INLINE, MockScriptEngine.NAME, Collections.emptyMap())); innerHit.setSize(1); innerHitsBuilder = new InnerHitsBuilder(); - innerHitsBuilder.addParentChildInnerHits("comment", "comment", new InnerHitsBuilder.InnerHit() - .setQuery(matchQuery("message", "fox")) - .highlighter(new HighlightBuilder().field("message")) - .setExplain(true) - .addFieldDataField("message") - .addScriptField("script", new Script("5", ScriptService.ScriptType.INLINE, MockScriptEngine.NAME, Collections.emptyMap())) - .setSize(1)); + innerHitsBuilder.addInnerHit("comment", innerHit); searchRequests = new SearchRequest[] { client().prepareSearch("articles") .setQuery(hasChildQuery("comment", matchQuery("message", "fox"))) @@ -376,7 +389,14 @@ public class InnerHitsIT extends ESIntegTestCase { client().prepareSearch("articles") .setQuery( hasChildQuery("comment", matchQuery("message", "fox")).innerHit( - new QueryInnerHits(null, innerHit))).request() }; + new InnerHitBuilder() + .addFieldDataField("message") + .setHighlightBuilder(new HighlightBuilder().field("message")) + .setExplain(true).setSize(1) + .addScriptField("script", new Script("5", ScriptService.ScriptType.INLINE, + MockScriptEngine.NAME, Collections.emptyMap())) + ) + ).request() }; for (SearchRequest searchRequest : searchRequests) { SearchResponse response = client().search(searchRequest).actionGet(); @@ -422,8 +442,8 @@ public class InnerHitsIT extends ESIntegTestCase { int size = randomIntBetween(0, numDocs); InnerHitsBuilder innerHitsBuilder = new InnerHitsBuilder(); - innerHitsBuilder.addParentChildInnerHits("a", "child1", new InnerHitsBuilder.InnerHit().addSort("_uid", SortOrder.ASC).setSize(size)); - innerHitsBuilder.addParentChildInnerHits("b", "child2", new InnerHitsBuilder.InnerHit().addSort("_uid", SortOrder.ASC).setSize(size)); + innerHitsBuilder.addInnerHit("a", new InnerHitBuilder().setParentChildType("child1").addSort(new FieldSortBuilder("_uid").order(SortOrder.ASC)).setSize(size)); + innerHitsBuilder.addInnerHit("b", new InnerHitBuilder().setParentChildType("child2").addSort(new FieldSortBuilder("_uid").order(SortOrder.ASC)).setSize(size)); SearchResponse searchResponse; if (randomBoolean()) { searchResponse = client().prepareSearch("idx") @@ -435,11 +455,11 @@ public class InnerHitsIT extends ESIntegTestCase { } else { BoolQueryBuilder boolQuery = new BoolQueryBuilder(); if (randomBoolean()) { - boolQuery.should(hasChildQuery("child1", matchAllQuery()).innerHit(new QueryInnerHits("a", new InnerHitsBuilder.InnerHit().addSort("_uid", SortOrder.ASC).setSize(size)))); - boolQuery.should(hasChildQuery("child2", matchAllQuery()).innerHit(new QueryInnerHits("b", new InnerHitsBuilder.InnerHit().addSort("_uid", SortOrder.ASC).setSize(size)))); + boolQuery.should(hasChildQuery("child1", matchAllQuery()).innerHit(new InnerHitBuilder().setName("a").addSort(new FieldSortBuilder("_uid").order(SortOrder.ASC)).setSize(size))); + boolQuery.should(hasChildQuery("child2", matchAllQuery()).innerHit(new InnerHitBuilder().setName("b").addSort(new FieldSortBuilder("_uid").order(SortOrder.ASC)).setSize(size))); } else { - boolQuery.should(constantScoreQuery(hasChildQuery("child1", matchAllQuery()).innerHit(new QueryInnerHits("a", new InnerHitsBuilder.InnerHit().addSort("_uid", SortOrder.ASC).setSize(size))))); - boolQuery.should(constantScoreQuery(hasChildQuery("child2", matchAllQuery()).innerHit(new QueryInnerHits("b", new InnerHitsBuilder.InnerHit().addSort("_uid", SortOrder.ASC).setSize(size))))); + boolQuery.should(constantScoreQuery(hasChildQuery("child1", matchAllQuery()).innerHit(new InnerHitBuilder().setName("a").addSort(new FieldSortBuilder("_uid").order(SortOrder.ASC)).setSize(size)))); + boolQuery.should(constantScoreQuery(hasChildQuery("child2", matchAllQuery()).innerHit(new InnerHitBuilder().setName("b").addSort(new FieldSortBuilder("_uid").order(SortOrder.ASC)).setSize(size)))); } searchResponse = client().prepareSearch("idx") .setSize(numDocs) @@ -485,22 +505,6 @@ public class InnerHitsIT extends ESIntegTestCase { } } - @AwaitsFix(bugUrl = "need validation of type or path defined in InnerHitsBuilder") - public void testPathOrTypeMustBeDefined() { - createIndex("articles"); - ensureGreen("articles"); - try { - InnerHitsBuilder innerHitsBuilder = new InnerHitsBuilder(); - innerHitsBuilder.addParentChildInnerHits("comment", null, new InnerHitsBuilder.InnerHit()); - client().prepareSearch("articles") - .innerHits(innerHitsBuilder) - .get(); - } catch (Exception e) { - assertThat(e.getMessage(), containsString("Failed to build")); - } - - } - public void testInnerHitsOnHasParent() throws Exception { assertAcked(prepareCreate("stack") .addMapping("question", "body", "type=text") @@ -519,7 +523,7 @@ public class InnerHitsIT extends ESIntegTestCase { .setQuery( boolQuery() .must(matchQuery("body", "fail2ban")) - .must(hasParentQuery("question", matchAllQuery()).innerHit(new QueryInnerHits())) + .must(hasParentQuery("question", matchAllQuery()).innerHit(new InnerHitBuilder())) ).get(); assertNoFailures(response); assertHitCount(response, 2); @@ -556,11 +560,15 @@ public class InnerHitsIT extends ESIntegTestCase { indexRandom(true, requests); InnerHitsBuilder innerInnerHitsBuilder = new InnerHitsBuilder(); - innerInnerHitsBuilder.addParentChildInnerHits("remark", "remark", new InnerHitsBuilder.InnerHit().setQuery(matchQuery("message", "good"))); + innerInnerHitsBuilder.addInnerHit("remark", new InnerHitBuilder() + .setParentChildType("remark") + .setQuery(matchQuery("message", "good")) + ); InnerHitsBuilder innerHitsBuilder = new InnerHitsBuilder(); - innerHitsBuilder.addParentChildInnerHits("comment", "comment", new InnerHitsBuilder.InnerHit() + innerHitsBuilder.addInnerHit("comment", new InnerHitBuilder() + .setParentChildType("comment") .setQuery(hasChildQuery("remark", matchQuery("message", "good"))) - .innerHits(innerInnerHitsBuilder)); + .setInnerHitsBuilder(innerInnerHitsBuilder)); SearchResponse response = client().prepareSearch("articles") .setQuery(hasChildQuery("comment", hasChildQuery("remark", matchQuery("message", "good")))) .innerHits(innerHitsBuilder) @@ -582,11 +590,14 @@ public class InnerHitsIT extends ESIntegTestCase { assertThat(innerHits.getAt(0).type(), equalTo("remark")); innerInnerHitsBuilder = new InnerHitsBuilder(); - innerInnerHitsBuilder.addParentChildInnerHits("remark", "remark", new InnerHitsBuilder.InnerHit().setQuery(matchQuery("message", "bad"))); + innerInnerHitsBuilder.addInnerHit("remark", new InnerHitBuilder() + .setParentChildType("remark") + .setQuery(matchQuery("message", "bad"))); innerHitsBuilder = new InnerHitsBuilder(); - innerHitsBuilder.addParentChildInnerHits("comment", "comment", new InnerHitsBuilder.InnerHit() + innerHitsBuilder.addInnerHit("comment", new InnerHitBuilder() + .setParentChildType("comment") .setQuery(hasChildQuery("remark", matchQuery("message", "bad"))) - .innerHits(innerInnerHitsBuilder)); + .setInnerHitsBuilder(innerInnerHitsBuilder)); response = client().prepareSearch("articles") .setQuery(hasChildQuery("comment", hasChildQuery("remark", matchQuery("message", "bad")))) .innerHits(innerHitsBuilder) @@ -651,11 +662,14 @@ public class InnerHitsIT extends ESIntegTestCase { indexRandom(true, requests); InnerHitsBuilder innerInnerHitsBuilder = new InnerHitsBuilder(); - innerInnerHitsBuilder.addNestedInnerHits("remark", "comments.remarks", new InnerHitsBuilder.InnerHit().setQuery(matchQuery("comments.remarks.message", "good"))); + innerInnerHitsBuilder.addInnerHit("remark", new InnerHitBuilder() + .setNestedPath("comments.remarks") + .setQuery(matchQuery("comments.remarks.message", "good"))); InnerHitsBuilder innerHitsBuilder = new InnerHitsBuilder(); - innerHitsBuilder.addNestedInnerHits("comment", "comments", new InnerHitsBuilder.InnerHit() + innerHitsBuilder.addInnerHit("comment", new InnerHitBuilder() + .setNestedPath("comments") .setQuery(nestedQuery("comments.remarks", matchQuery("comments.remarks.message", "good"))) - .innerHits(innerInnerHitsBuilder) + .setInnerHitsBuilder(innerInnerHitsBuilder) ); SearchResponse response = client().prepareSearch("articles") .setQuery(nestedQuery("comments", nestedQuery("comments.remarks", matchQuery("comments.remarks.message", "good")))) @@ -681,7 +695,7 @@ public class InnerHitsIT extends ESIntegTestCase { // Directly refer to the second level: response = client().prepareSearch("articles") - .setQuery(nestedQuery("comments.remarks", matchQuery("comments.remarks.message", "bad")).innerHit(new QueryInnerHits())) + .setQuery(nestedQuery("comments.remarks", matchQuery("comments.remarks.message", "bad")).innerHit(new InnerHitBuilder())) .get(); assertNoFailures(response); assertHitCount(response, 1); @@ -697,11 +711,14 @@ public class InnerHitsIT extends ESIntegTestCase { assertThat(innerHits.getAt(0).getNestedIdentity().getChild().getOffset(), equalTo(0)); innerInnerHitsBuilder = new InnerHitsBuilder(); - innerInnerHitsBuilder.addNestedInnerHits("remark", "comments.remarks", new InnerHitsBuilder.InnerHit().setQuery(matchQuery("comments.remarks.message", "bad"))); + innerInnerHitsBuilder.addInnerHit("remark", new InnerHitBuilder() + .setNestedPath("comments.remarks") + .setQuery(matchQuery("comments.remarks.message", "bad"))); innerHitsBuilder = new InnerHitsBuilder(); - innerHitsBuilder.addNestedInnerHits("comment", "comments", new InnerHitsBuilder.InnerHit() - .setQuery(nestedQuery("comments.remarks", matchQuery("comments.remarks.message", "bad"))) - .innerHits(innerInnerHitsBuilder)); + innerHitsBuilder.addInnerHit("comment", new InnerHitBuilder() + .setNestedPath("comments") + .setQuery(nestedQuery("comments.remarks", matchQuery("comments.remarks.message", "bad"))) + .setInnerHitsBuilder(innerInnerHitsBuilder)); response = client().prepareSearch("articles") .setQuery(nestedQuery("comments", nestedQuery("comments.remarks", matchQuery("comments.remarks.message", "bad")))) .innerHits(innerHitsBuilder) @@ -738,7 +755,7 @@ public class InnerHitsIT extends ESIntegTestCase { indexRandom(true, requests); SearchResponse response = client().prepareSearch("articles") - .setQuery(nestedQuery("comments", matchQuery("comments.message", "fox")).innerHit(new QueryInnerHits())) + .setQuery(nestedQuery("comments", matchQuery("comments.message", "fox")).innerHit(new InnerHitBuilder())) .get(); assertNoFailures(response); assertHitCount(response, 1); @@ -778,7 +795,7 @@ public class InnerHitsIT extends ESIntegTestCase { indexRandom(true, requests); SearchResponse response = client().prepareSearch("articles") - .setQuery(nestedQuery("comments.messages", matchQuery("comments.messages.message", "fox")).innerHit(new QueryInnerHits())) + .setQuery(nestedQuery("comments.messages", matchQuery("comments.messages.message", "fox")).innerHit(new InnerHitBuilder())) .get(); assertNoFailures(response); assertHitCount(response, 1); @@ -790,7 +807,7 @@ public class InnerHitsIT extends ESIntegTestCase { assertThat(response.getHits().getAt(0).getInnerHits().get("comments.messages").getAt(0).getNestedIdentity().getChild(), nullValue()); response = client().prepareSearch("articles") - .setQuery(nestedQuery("comments.messages", matchQuery("comments.messages.message", "bear")).innerHit(new QueryInnerHits())) + .setQuery(nestedQuery("comments.messages", matchQuery("comments.messages.message", "bear")).innerHit(new InnerHitBuilder())) .get(); assertNoFailures(response); assertHitCount(response, 1); @@ -809,7 +826,7 @@ public class InnerHitsIT extends ESIntegTestCase { .endObject())); indexRandom(true, requests); response = client().prepareSearch("articles") - .setQuery(nestedQuery("comments.messages", matchQuery("comments.messages.message", "fox")).innerHit(new QueryInnerHits())) + .setQuery(nestedQuery("comments.messages", matchQuery("comments.messages.message", "fox")).innerHit(new InnerHitBuilder())) .get(); assertNoFailures(response); assertHitCount(response, 1); @@ -846,19 +863,18 @@ public class InnerHitsIT extends ESIntegTestCase { indexRandom(true, requests); InnerHitsBuilder innerInnerHitsBuilder = new InnerHitsBuilder(); - innerInnerHitsBuilder.addParentChildInnerHits("barons", "baron", new InnerHitsBuilder.InnerHit()); + innerInnerHitsBuilder.addInnerHit("barons", new InnerHitBuilder().setParentChildType("baron")); InnerHitsBuilder innerHitsBuilder = new InnerHitsBuilder(); - innerHitsBuilder.addParentChildInnerHits("earls", "earl", new InnerHitsBuilder.InnerHit() - .addSort(SortBuilders.fieldSort("_uid").order(SortOrder.ASC)) - .setSize(4) - .innerHits(innerInnerHitsBuilder) + innerHitsBuilder.addInnerHit("earls", new InnerHitBuilder() + .setParentChildType("earl") + .addSort(SortBuilders.fieldSort("_uid").order(SortOrder.ASC)) + .setSize(4) + .setInnerHitsBuilder(innerInnerHitsBuilder) ); innerInnerHitsBuilder = new InnerHitsBuilder(); - innerInnerHitsBuilder.addParentChildInnerHits("kings", "king", new InnerHitsBuilder.InnerHit()); - innerHitsBuilder.addParentChildInnerHits("princes", "prince", - new InnerHitsBuilder.InnerHit() - .innerHits(innerInnerHitsBuilder) - ); + innerInnerHitsBuilder.addInnerHit("kings", new InnerHitBuilder().setParentChildType("king")); + innerHitsBuilder.addInnerHit("princes", new InnerHitBuilder().setParentChildType("prince") + .setInnerHitsBuilder(innerInnerHitsBuilder)); SearchResponse response = client().prepareSearch("royals") .setTypes("duke") .innerHits(innerHitsBuilder) @@ -972,7 +988,7 @@ public class InnerHitsIT extends ESIntegTestCase { .should(termQuery("nested1.n_field1", "n_value1_1").queryName("test1")) .should(termQuery("nested1.n_field1", "n_value1_3").queryName("test2")) .should(termQuery("nested1.n_field2", "n_value2_2").queryName("test3")) - ).innerHit(new QueryInnerHits(null, new InnerHitsBuilder.InnerHit().addSort("nested1.n_field1", SortOrder.ASC)))) + ).innerHit(new InnerHitBuilder().addSort(new FieldSortBuilder("nested1.n_field1").order(SortOrder.ASC)))) .setSize(numDocs) .addSort("field1", SortOrder.ASC) .get(); @@ -1011,7 +1027,7 @@ public class InnerHitsIT extends ESIntegTestCase { indexRandom(true, requests); SearchResponse response = client().prepareSearch("index") - .setQuery(hasChildQuery("child", matchQuery("field", "value1").queryName("_name1")).innerHit(new QueryInnerHits())) + .setQuery(hasChildQuery("child", matchQuery("field", "value1").queryName("_name1")).innerHit(new InnerHitBuilder())) .addSort("_uid", SortOrder.ASC) .get(); assertHitCount(response, 2); @@ -1026,7 +1042,7 @@ public class InnerHitsIT extends ESIntegTestCase { assertThat(response.getHits().getAt(1).getInnerHits().get("child").getAt(0).getMatchedQueries()[0], equalTo("_name1")); response = client().prepareSearch("index") - .setQuery(hasChildQuery("child", matchQuery("field", "value2").queryName("_name2")).innerHit(new QueryInnerHits())) + .setQuery(hasChildQuery("child", matchQuery("field", "value2").queryName("_name2")).innerHit(new InnerHitBuilder())) .addSort("_uid", SortOrder.ASC) .get(); assertHitCount(response, 1); @@ -1044,7 +1060,7 @@ public class InnerHitsIT extends ESIntegTestCase { indexRandom(true, requests); SearchResponse response = client().prepareSearch("index1") - .setQuery(hasChildQuery("child", matchQuery("field", "value1")).innerHit(new QueryInnerHits(null, new InnerHitsBuilder.InnerHit().setSize(ArrayUtil.MAX_ARRAY_LENGTH - 1)))) + .setQuery(hasChildQuery("child", matchQuery("field", "value1")).innerHit(new InnerHitBuilder().setSize(ArrayUtil.MAX_ARRAY_LENGTH - 1))) .addSort("_uid", SortOrder.ASC) .get(); assertNoFailures(response); @@ -1062,7 +1078,7 @@ public class InnerHitsIT extends ESIntegTestCase { .get(); response = client().prepareSearch("index2") - .setQuery(nestedQuery("nested", matchQuery("nested.field", "value1")).innerHit(new QueryInnerHits(null, new InnerHitsBuilder.InnerHit().setSize(ArrayUtil.MAX_ARRAY_LENGTH - 1)))) + .setQuery(nestedQuery("nested", matchQuery("nested.field", "value1")).innerHit(new InnerHitBuilder().setSize(ArrayUtil.MAX_ARRAY_LENGTH - 1))) .addSort("_uid", SortOrder.ASC) .get(); assertNoFailures(response); @@ -1079,9 +1095,9 @@ public class InnerHitsIT extends ESIntegTestCase { indexRandom(true, requests); InnerHitsBuilder innerHitsBuilder = new InnerHitsBuilder(); - innerHitsBuilder.addParentChildInnerHits("my-inner-hit", "child", new InnerHitsBuilder.InnerHit()); + innerHitsBuilder.addInnerHit("my-inner-hit", new InnerHitBuilder().setParentChildType("child")); SearchResponse response = client().prepareSearch("index1") - .setQuery(hasChildQuery("child", new MatchAllQueryBuilder()).innerHit(new QueryInnerHits())) + .setQuery(hasChildQuery("child", new MatchAllQueryBuilder()).innerHit(new InnerHitBuilder())) .innerHits(innerHitsBuilder) .get(); assertHitCount(response, 1); diff --git a/docs/reference/migration/migrate_5_0/search.asciidoc b/docs/reference/migration/migrate_5_0/search.asciidoc index 418486d600e..ce1e0fbef5d 100644 --- a/docs/reference/migration/migrate_5_0/search.asciidoc +++ b/docs/reference/migration/migrate_5_0/search.asciidoc @@ -146,3 +146,8 @@ vectors don't support distributed document frequencies anymore. The `reverse` parameter has been removed, in favour of explicitly specifying the sort order with the `order` option. + +==== Inner hits + +* The format of top level inner hits has been changed to be more readable. All options are now set on the same level. + So the `path` and `type` options are specified on the same level where `query` and other options are specified. diff --git a/docs/reference/search/request/inner-hits.asciidoc b/docs/reference/search/request/inner-hits.asciidoc index 4b47c883ce0..b79f2dbd570 100644 --- a/docs/reference/search/request/inner-hits.asciidoc +++ b/docs/reference/search/request/inner-hits.asciidoc @@ -276,20 +276,17 @@ An example that shows the use of nested inner hits via the top level notation: } }, "inner_hits" : { - "comment" : { - "path" : { <1> - "comments" : { <2> - "query" : { - "match" : {"comments.message" : "[different query]"} <3> - } - } + "comment" : { <1> + "path" : "comments", <2> + "query" : { + "match" : {"comments.message" : "[different query]"} <3> } } } } -------------------------------------------------- -<1> The inner hit definition is nested and requires the `path` option. +<1> The inner hit definition with the name `comment`. <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