diff --git a/docs/reference/aggregations/metrics/tophits-aggregation.asciidoc b/docs/reference/aggregations/metrics/tophits-aggregation.asciidoc index 9e53b53c395a1..f02d1b700ed3e 100644 --- a/docs/reference/aggregations/metrics/tophits-aggregation.asciidoc +++ b/docs/reference/aggregations/metrics/tophits-aggregation.asciidoc @@ -24,6 +24,7 @@ The top_hits aggregation returns regular search hits, because of this many per h * <> * <> * <> +* <> * <> * <> * <> @@ -41,7 +42,7 @@ by aggregations, use a <> or ==== Example -In the following example we group the sales by type and per type we show the last sale. +In the following example we group the sales by type and per type we show the last sale. For each sale only the date and price fields are being included in the source. [source,console] diff --git a/server/src/internalClusterTest/java/org/elasticsearch/search/aggregations/metrics/TopHitsIT.java b/server/src/internalClusterTest/java/org/elasticsearch/search/aggregations/metrics/TopHitsIT.java index 8cda3d796d51d..29f549c9f63a4 100644 --- a/server/src/internalClusterTest/java/org/elasticsearch/search/aggregations/metrics/TopHitsIT.java +++ b/server/src/internalClusterTest/java/org/elasticsearch/search/aggregations/metrics/TopHitsIT.java @@ -166,6 +166,7 @@ public void setupSuiteScopeCluster() throws Exception { .field(SORT_FIELD, i + 1) .field("text", "some text to entertain") .field("field1", 5) + .field("field2", 2.71) .endObject())); } @@ -315,7 +316,7 @@ public void testBasics() throws Exception { assertThat((Long) hits.getAt(1).getSortValues()[0], equalTo(higestSortValue - 1)); assertThat((Long) hits.getAt(2).getSortValues()[0], equalTo(higestSortValue - 2)); - assertThat(hits.getAt(0).getSourceAsMap().size(), equalTo(4)); + assertThat(hits.getAt(0).getSourceAsMap().size(), equalTo(5)); } } @@ -402,7 +403,7 @@ public void testBreadthFirstWithScoreNeeded() throws Exception { assertThat(hits.getTotalHits().value, equalTo(10L)); assertThat(hits.getHits().length, equalTo(3)); - assertThat(hits.getAt(0).getSourceAsMap().size(), equalTo(4)); + assertThat(hits.getAt(0).getSourceAsMap().size(), equalTo(5)); } } @@ -433,7 +434,7 @@ public void testBreadthFirstWithAggOrderAndScoreNeeded() throws Exception { assertThat(hits.getTotalHits().value, equalTo(10L)); assertThat(hits.getHits().length, equalTo(3)); - assertThat(hits.getAt(0).getSourceAsMap().size(), equalTo(4)); + assertThat(hits.getAt(0).getSourceAsMap().size(), equalTo(5)); id--; } } @@ -597,6 +598,7 @@ public void testFetchFeatures() { .explain(true) .storedField("text") .docValueField("field1") + .fetchField("field2") .scriptField("script", new Script(ScriptType.INLINE, MockScriptEngine.NAME, "5", Collections.emptyMap())) .fetchSource("text", null) @@ -639,13 +641,16 @@ public void testFetchFeatures() { assertThat(hit.getMatchedQueries()[0], equalTo("test")); - DocumentField field = hit.field("field1"); - assertThat(field.getValue().toString(), equalTo("5")); + DocumentField field1 = hit.field("field1"); + assertThat(field1.getValue(), equalTo(5L)); + + DocumentField field2 = hit.field("field2"); + assertThat(field2.getValue(), equalTo(2.71f)); assertThat(hit.getSourceAsMap().get("text").toString(), equalTo("some text to entertain")); - field = hit.field("script"); - assertThat(field.getValue().toString(), equalTo("5")); + field2 = hit.field("script"); + assertThat(field2.getValue().toString(), equalTo("5")); assertThat(hit.getSourceAsMap().size(), equalTo(1)); assertThat(hit.getSourceAsMap().get("text").toString(), equalTo("some text to entertain")); diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/metrics/TopHitsAggregationBuilder.java b/server/src/main/java/org/elasticsearch/search/aggregations/metrics/TopHitsAggregationBuilder.java index edbc234efe3bc..c046afb35efb2 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/metrics/TopHitsAggregationBuilder.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/metrics/TopHitsAggregationBuilder.java @@ -19,6 +19,7 @@ package org.elasticsearch.search.aggregations.metrics; +import org.elasticsearch.Version; import org.elasticsearch.common.Nullable; import org.elasticsearch.common.ParsingException; import org.elasticsearch.common.Strings; @@ -71,6 +72,7 @@ public class TopHitsAggregationBuilder extends AbstractAggregationBuilder docValueFields; + private List fetchFields; private Set scriptFields; private FetchSourceContext fetchSourceContext; @@ -93,6 +95,7 @@ protected TopHitsAggregationBuilder(TopHitsAggregationBuilder clone, this.storedFieldsContext = clone.storedFieldsContext == null ? null : new StoredFieldsContext(clone.storedFieldsContext); this.docValueFields = clone.docValueFields == null ? null : new ArrayList<>(clone.docValueFields); + this.fetchFields = clone.fetchFields == null ? null : new ArrayList<>(clone.fetchFields); this.scriptFields = clone.scriptFields == null ? null : new HashSet<>(clone.scriptFields); this.fetchSourceContext = clone.fetchSourceContext == null ? null : new FetchSourceContext(clone.fetchSourceContext.fetchSource(), clone.fetchSourceContext.includes(), @@ -139,6 +142,11 @@ public TopHitsAggregationBuilder(StreamInput in) throws IOException { trackScores = in.readBoolean(); version = in.readBoolean(); seqNoAndPrimaryTerm = in.readBoolean(); + if (in.getVersion().onOrAfter(Version.CURRENT)) { + if (in.readBoolean()) { + fetchFields = in.readList(FieldAndFormat::new); + } + } } @Override @@ -167,6 +175,12 @@ protected void doWriteTo(StreamOutput out) throws IOException { out.writeBoolean(trackScores); out.writeBoolean(version); out.writeBoolean(seqNoAndPrimaryTerm); + if (out.getVersion().onOrAfter(Version.CURRENT)) { + out.writeBoolean(fetchFields != null); + if (fetchFields != null) { + out.writeList(fetchFields); + } + } } /** @@ -425,10 +439,38 @@ public TopHitsAggregationBuilder docValueField(String docValueField) { /** * Gets the field-data fields. */ - public List fieldDataFields() { + public List docValueFields() { return docValueFields; } + /** + * Adds a field to load and return as part of the search request. + */ + public TopHitsAggregationBuilder fetchField(String field, String format) { + if (field == null) { + throw new IllegalArgumentException("[docValueField] must not be null: [" + name + "]"); + } + if (fetchFields == null) { + fetchFields = new ArrayList<>(); + } + fetchFields.add(new FieldAndFormat(field, format)); + return this; + } + + /** + * Adds a field to load and return as part of the search request. + */ + public TopHitsAggregationBuilder fetchField(String field) { + return fetchField(field, null); + } + + /** + * Gets the fields to load and return as part of the search request. + */ + public List fetchFields() { + return fetchFields; + } + /** * Adds a script field under the given name with the provided script. * @@ -580,12 +622,12 @@ protected TopHitsAggregatorFactory doBuild(QueryShardContext queryShardContext, ); } - List fields = new ArrayList<>(); - if (scriptFields != null) { - for (ScriptField field : scriptFields) { + List scriptFields = new ArrayList<>(); + if (this.scriptFields != null) { + for (ScriptField field : this.scriptFields) { FieldScript.Factory factory = queryShardContext.compile(field.script(), FieldScript.CONTEXT); FieldScript.LeafFactory searchScript = factory.newFactory(field.script().getParams(), queryShardContext.lookup()); - fields.add(new org.elasticsearch.search.fetch.subphase.ScriptFieldsContext.ScriptField( + scriptFields.add(new org.elasticsearch.search.fetch.subphase.ScriptFieldsContext.ScriptField( field.fieldName(), searchScript, field.ignoreFailure())); } } @@ -597,7 +639,7 @@ protected TopHitsAggregatorFactory doBuild(QueryShardContext queryShardContext, optionalSort = SortBuilder.buildSort(sorts, queryShardContext); } return new TopHitsAggregatorFactory(name, from, size, explain, version, seqNoAndPrimaryTerm, trackScores, optionalSort, - highlightBuilder, storedFieldsContext, docValueFields, fields, fetchSourceContext, queryShardContext, parent, + highlightBuilder, storedFieldsContext, docValueFields, fetchFields, scriptFields, fetchSourceContext, queryShardContext, parent, subfactoriesBuilder, metadata); } @@ -615,18 +657,23 @@ protected XContentBuilder internalXContent(XContentBuilder builder, Params param if (storedFieldsContext != null) { storedFieldsContext.toXContent(SearchSourceBuilder.STORED_FIELDS_FIELD.getPreferredName(), builder); } + if (docValueFields != null) { builder.startArray(SearchSourceBuilder.DOCVALUE_FIELDS_FIELD.getPreferredName()); - for (FieldAndFormat dvField : docValueFields) { - builder.startObject() - .field("field", dvField.field); - if (dvField.format != null) { - builder.field("format", dvField.format); - } - builder.endObject(); + for (FieldAndFormat docValueField : docValueFields) { + docValueField.toXContent(builder, params); + } + builder.endArray(); + } + + if (fetchFields != null) { + builder.startArray(SearchSourceBuilder.FETCH_FIELDS_FIELD.getPreferredName()); + for (FieldAndFormat docValueField : fetchFields) { + docValueField.toXContent(builder, params); } builder.endArray(); } + if (scriptFields != null) { builder.startObject(SearchSourceBuilder.SCRIPT_FIELDS_FIELD.getPreferredName()); for (ScriptField scriptField : scriptFields) { @@ -746,6 +793,11 @@ public static TopHitsAggregationBuilder parse(String aggregationName, XContentPa FieldAndFormat ff = FieldAndFormat.fromXContent(parser); factory.docValueField(ff.field, ff.format); } + } else if (SearchSourceBuilder.FETCH_FIELDS_FIELD.match(currentFieldName, parser.getDeprecationHandler())) { + while ((token = parser.nextToken()) != XContentParser.Token.END_ARRAY) { + FieldAndFormat ff = FieldAndFormat.fromXContent(parser); + factory.fetchField(ff.field, ff.format); + } } else if (SearchSourceBuilder.SORT_FIELD.match(currentFieldName, parser.getDeprecationHandler())) { List> sorts = SortBuilder.fromXContent(parser); factory.sorts(sorts); @@ -764,31 +816,30 @@ public static TopHitsAggregationBuilder parse(String aggregationName, XContentPa } @Override - public int hashCode() { - return Objects.hash(super.hashCode(), explain, fetchSourceContext, docValueFields, - storedFieldsContext, from, highlightBuilder, - scriptFields, size, sorts, trackScores, version, - seqNoAndPrimaryTerm); + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + if (!super.equals(o)) return false; + TopHitsAggregationBuilder that = (TopHitsAggregationBuilder) o; + return from == that.from && + size == that.size && + explain == that.explain && + version == that.version && + seqNoAndPrimaryTerm == that.seqNoAndPrimaryTerm && + trackScores == that.trackScores && + Objects.equals(sorts, that.sorts) && + Objects.equals(highlightBuilder, that.highlightBuilder) && + Objects.equals(storedFieldsContext, that.storedFieldsContext) && + Objects.equals(docValueFields, that.docValueFields) && + Objects.equals(fetchFields, that.fetchFields) && + Objects.equals(scriptFields, that.scriptFields) && + Objects.equals(fetchSourceContext, that.fetchSourceContext); } @Override - public boolean equals(Object obj) { - if (this == obj) return true; - if (obj == null || getClass() != obj.getClass()) return false; - if (super.equals(obj) == false) return false; - TopHitsAggregationBuilder other = (TopHitsAggregationBuilder) obj; - return Objects.equals(explain, other.explain) - && Objects.equals(fetchSourceContext, other.fetchSourceContext) - && Objects.equals(docValueFields, other.docValueFields) - && Objects.equals(storedFieldsContext, other.storedFieldsContext) - && Objects.equals(from, other.from) - && Objects.equals(highlightBuilder, other.highlightBuilder) - && Objects.equals(scriptFields, other.scriptFields) - && Objects.equals(size, other.size) - && Objects.equals(sorts, other.sorts) - && Objects.equals(trackScores, other.trackScores) - && Objects.equals(version, other.version) - && Objects.equals(seqNoAndPrimaryTerm, other.seqNoAndPrimaryTerm); + public int hashCode() { + return Objects.hash(super.hashCode(), from, size, explain, version, seqNoAndPrimaryTerm, trackScores, sorts, highlightBuilder, + storedFieldsContext, docValueFields, fetchFields, scriptFields, fetchSourceContext); } @Override diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/metrics/TopHitsAggregatorFactory.java b/server/src/main/java/org/elasticsearch/search/aggregations/metrics/TopHitsAggregatorFactory.java index ffee18e81cc24..7b6c440d77a2b 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/metrics/TopHitsAggregatorFactory.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/metrics/TopHitsAggregatorFactory.java @@ -26,6 +26,7 @@ import org.elasticsearch.search.aggregations.CardinalityUpperBound; import org.elasticsearch.search.fetch.StoredFieldsContext; import org.elasticsearch.search.fetch.subphase.FetchDocValuesContext; +import org.elasticsearch.search.fetch.subphase.FetchFieldsContext; import org.elasticsearch.search.fetch.subphase.FetchSourceContext; import org.elasticsearch.search.fetch.subphase.FieldAndFormat; import org.elasticsearch.search.fetch.subphase.ScriptFieldsContext; @@ -51,6 +52,7 @@ class TopHitsAggregatorFactory extends AggregatorFactory { private final HighlightBuilder highlightBuilder; private final StoredFieldsContext storedFieldsContext; private final List docValueFields; + private final List fetchFields; private final List scriptFields; private final FetchSourceContext fetchSourceContext; @@ -65,6 +67,7 @@ class TopHitsAggregatorFactory extends AggregatorFactory { HighlightBuilder highlightBuilder, StoredFieldsContext storedFieldsContext, List docValueFields, + List fetchFields, List scriptFields, FetchSourceContext fetchSourceContext, QueryShardContext queryShardContext, @@ -82,6 +85,7 @@ class TopHitsAggregatorFactory extends AggregatorFactory { this.highlightBuilder = highlightBuilder; this.storedFieldsContext = storedFieldsContext; this.docValueFields = docValueFields; + this.fetchFields = fetchFields; this.scriptFields = scriptFields; this.fetchSourceContext = fetchSourceContext; } @@ -109,6 +113,11 @@ public Aggregator createInternal(SearchContext searchContext, FetchDocValuesContext docValuesContext = FetchDocValuesContext.create(searchContext.mapperService(), docValueFields); subSearchContext.docValuesContext(docValuesContext); } + if (fetchFields != null) { + String indexName = searchContext.indexShard().shardId().getIndexName(); + FetchFieldsContext fieldsContext = FetchFieldsContext.create(indexName, searchContext.mapperService(), fetchFields); + subSearchContext.fetchFieldsContext(fieldsContext); + } for (ScriptFieldsContext.ScriptField field : scriptFields) { subSearchContext.scriptFields().add(field); } diff --git a/server/src/test/java/org/elasticsearch/search/aggregations/metrics/TopHitsTests.java b/server/src/test/java/org/elasticsearch/search/aggregations/metrics/TopHitsTests.java index 6ec1ea1cad301..5d352e2187d95 100644 --- a/server/src/test/java/org/elasticsearch/search/aggregations/metrics/TopHitsTests.java +++ b/server/src/test/java/org/elasticsearch/search/aggregations/metrics/TopHitsTests.java @@ -86,6 +86,12 @@ protected final TopHitsAggregationBuilder createTestAggregatorBuilder() { factory.docValueField(randomAlphaOfLengthBetween(5, 50)); } } + if (randomBoolean()) { + int fetchFieldsSize = randomInt(25); + for (int i = 0; i < fetchFieldsSize; i++) { + factory.fetchField(randomAlphaOfLengthBetween(5, 50)); + } + } if (randomBoolean()) { int scriptFieldsSize = randomInt(25); for (int i = 0; i < scriptFieldsSize; i++) {