/* * 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.aggregations.metrics.tophits; import org.elasticsearch.common.Nullable; import org.elasticsearch.common.ParsingException; import org.elasticsearch.common.Strings; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.index.query.QueryParseContext; import org.elasticsearch.script.Script; import org.elasticsearch.script.ScriptContext; import org.elasticsearch.script.SearchScript; import org.elasticsearch.search.aggregations.AbstractAggregationBuilder; import org.elasticsearch.search.aggregations.AggregationInitializationException; import org.elasticsearch.search.aggregations.AggregatorFactories.Builder; import org.elasticsearch.search.aggregations.AggregatorFactory; import org.elasticsearch.search.builder.SearchSourceBuilder; import org.elasticsearch.search.builder.SearchSourceBuilder.ScriptField; import org.elasticsearch.search.fetch.StoredFieldsContext; import org.elasticsearch.search.fetch.subphase.FetchSourceContext; import org.elasticsearch.search.fetch.subphase.ScriptFieldsContext; import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder; import org.elasticsearch.search.internal.SearchContext; import org.elasticsearch.search.sort.ScoreSortBuilder; import org.elasticsearch.search.sort.SortAndFormats; import org.elasticsearch.search.sort.SortBuilder; import org.elasticsearch.search.sort.SortBuilders; import org.elasticsearch.search.sort.SortOrder; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.Set; public class TopHitsAggregationBuilder extends AbstractAggregationBuilder<TopHitsAggregationBuilder> { public static final String NAME = "top_hits"; private int from = 0; private int size = 3; private boolean explain = false; private boolean version = false; private boolean trackScores = false; private List<SortBuilder<?>> sorts = null; private HighlightBuilder highlightBuilder; private StoredFieldsContext storedFieldsContext; private List<String> fieldDataFields; private Set<ScriptField> scriptFields; private FetchSourceContext fetchSourceContext; public TopHitsAggregationBuilder(String name) { super(name); } /** * Read from a stream. */ public TopHitsAggregationBuilder(StreamInput in) throws IOException { super(in); explain = in.readBoolean(); fetchSourceContext = in.readOptionalWriteable(FetchSourceContext::new); if (in.readBoolean()) { int size = in.readVInt(); fieldDataFields = new ArrayList<>(size); for (int i = 0; i < size; i++) { fieldDataFields.add(in.readString()); } } storedFieldsContext = in.readOptionalWriteable(StoredFieldsContext::new); from = in.readVInt(); highlightBuilder = in.readOptionalWriteable(HighlightBuilder::new); if (in.readBoolean()) { int size = in.readVInt(); scriptFields = new HashSet<>(size); for (int i = 0; i < size; i++) { scriptFields.add(new ScriptField(in)); } } size = in.readVInt(); if (in.readBoolean()) { int size = in.readVInt(); sorts = new ArrayList<>(); for (int i = 0; i < size; i++) { sorts.add(in.readNamedWriteable(SortBuilder.class)); } } trackScores = in.readBoolean(); version = in.readBoolean(); } @Override protected void doWriteTo(StreamOutput out) throws IOException { out.writeBoolean(explain); out.writeOptionalWriteable(fetchSourceContext); boolean hasFieldDataFields = fieldDataFields != null; out.writeBoolean(hasFieldDataFields); if (hasFieldDataFields) { out.writeVInt(fieldDataFields.size()); for (String fieldName : fieldDataFields) { out.writeString(fieldName); } } out.writeOptionalWriteable(storedFieldsContext); out.writeVInt(from); out.writeOptionalWriteable(highlightBuilder); boolean hasScriptFields = scriptFields != null; out.writeBoolean(hasScriptFields); if (hasScriptFields) { out.writeVInt(scriptFields.size()); for (ScriptField scriptField : scriptFields) { scriptField.writeTo(out); } } out.writeVInt(size); boolean hasSorts = sorts != null; out.writeBoolean(hasSorts); if (hasSorts) { out.writeVInt(sorts.size()); for (SortBuilder<?> sort : sorts) { out.writeNamedWriteable(sort); } } out.writeBoolean(trackScores); out.writeBoolean(version); } /** * From index to start the search from. Defaults to <tt>0</tt>. */ public TopHitsAggregationBuilder from(int from) { if (from < 0) { throw new IllegalArgumentException("[from] must be greater than or equal to 0. Found [" + from + "] in [" + name + "]"); } this.from = from; return this; } /** * Gets the from index to start the search from. **/ public int from() { return from; } /** * The number of search hits to return. Defaults to <tt>10</tt>. */ public TopHitsAggregationBuilder size(int size) { if (size < 0) { throw new IllegalArgumentException("[size] must be greater than or equal to 0. Found [" + size + "] in [" + name + "]"); } this.size = size; return this; } /** * Gets the number of search hits to return. */ public int size() { return size; } /** * Adds a sort against the given field name and the sort ordering. * * @param name * The name of the field * @param order * The sort ordering */ public TopHitsAggregationBuilder sort(String name, SortOrder order) { if (name == null) { throw new IllegalArgumentException("sort [name] must not be null: [" + name + "]"); } if (order == null) { throw new IllegalArgumentException("sort [order] must not be null: [" + name + "]"); } if (name.equals(ScoreSortBuilder.NAME)) { sort(SortBuilders.scoreSort().order(order)); } sort(SortBuilders.fieldSort(name).order(order)); return this; } /** * Add a sort against the given field name. * * @param name * The name of the field to sort by */ public TopHitsAggregationBuilder sort(String name) { if (name == null) { throw new IllegalArgumentException("sort [name] must not be null: [" + name + "]"); } if (name.equals(ScoreSortBuilder.NAME)) { sort(SortBuilders.scoreSort()); } sort(SortBuilders.fieldSort(name)); return this; } /** * Adds a sort builder. */ public TopHitsAggregationBuilder sort(SortBuilder<?> sort) { if (sort == null) { throw new IllegalArgumentException("[sort] must not be null: [" + name + "]"); } if (sorts == null) { sorts = new ArrayList<>(); } sorts.add(sort); return this; } /** * Adds a sort builder. */ public TopHitsAggregationBuilder sorts(List<SortBuilder<?>> sorts) { if (sorts == null) { throw new IllegalArgumentException("[sorts] must not be null: [" + name + "]"); } if (this.sorts == null) { this.sorts = new ArrayList<>(); } for (SortBuilder<?> sort : sorts) { this.sorts.add(sort); } return this; } /** * Gets the bytes representing the sort builders for this request. */ public List<SortBuilder<?>> sorts() { return sorts; } /** * Adds highlight to perform as part of the search. */ public TopHitsAggregationBuilder highlighter(HighlightBuilder highlightBuilder) { if (highlightBuilder == null) { throw new IllegalArgumentException("[highlightBuilder] must not be null: [" + name + "]"); } this.highlightBuilder = highlightBuilder; return this; } /** * Gets the highlighter builder for this request. */ public HighlightBuilder highlighter() { return highlightBuilder; } /** * Indicates whether the response should contain the stored _source for * every hit */ public TopHitsAggregationBuilder fetchSource(boolean fetch) { FetchSourceContext fetchSourceContext = this.fetchSourceContext != null ? this.fetchSourceContext : FetchSourceContext.FETCH_SOURCE; this.fetchSourceContext = new FetchSourceContext(fetch, fetchSourceContext.includes(), fetchSourceContext.excludes()); 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 TopHitsAggregationBuilder fetchSource(@Nullable String include, @Nullable String exclude) { fetchSource(include == null ? Strings.EMPTY_ARRAY : new String[] { include }, exclude == null ? Strings.EMPTY_ARRAY : new String[] { 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 TopHitsAggregationBuilder fetchSource(@Nullable String[] includes, @Nullable String[] excludes) { FetchSourceContext fetchSourceContext = this.fetchSourceContext != null ? this.fetchSourceContext : FetchSourceContext.FETCH_SOURCE; this.fetchSourceContext = new FetchSourceContext(fetchSourceContext.fetchSource(), includes, excludes); return this; } /** * Indicate how the _source should be fetched. */ public TopHitsAggregationBuilder fetchSource(@Nullable FetchSourceContext fetchSourceContext) { if (fetchSourceContext == null) { throw new IllegalArgumentException("[fetchSourceContext] must not be null: [" + name + "]"); } this.fetchSourceContext = fetchSourceContext; return this; } /** * Gets the {@link FetchSourceContext} which defines how the _source * should be fetched. */ public FetchSourceContext fetchSource() { return fetchSourceContext; } /** * Adds a stored field to load and return (note, it must be stored) as part of the search request. * To disable the stored fields entirely (source and metadata fields) use {@code storedField("_none_")}. */ public TopHitsAggregationBuilder storedField(String field) { return storedFields(Collections.singletonList(field)); } /** * Sets the stored fields to load and return as part of the search request. * To disable the stored fields entirely (source and metadata fields) use {@code storedField("_none_")}. */ public TopHitsAggregationBuilder storedFields(List<String> fields) { if (fields == null) { throw new IllegalArgumentException("[fields] must not be null: [" + name + "]"); } if (storedFieldsContext == null) { storedFieldsContext = StoredFieldsContext.fromList(fields); } else { storedFieldsContext.addFieldNames(fields); } return this; } /** * Gets the stored fields context */ public StoredFieldsContext storedFields() { return storedFieldsContext; } /** * Adds a field to load from the field data cache and return as part of * the search request. */ public TopHitsAggregationBuilder fieldDataField(String fieldDataField) { if (fieldDataField == null) { throw new IllegalArgumentException("[fieldDataField] must not be null: [" + name + "]"); } if (fieldDataFields == null) { fieldDataFields = new ArrayList<>(); } fieldDataFields.add(fieldDataField); return this; } /** * Adds fields to load from the field data cache and return as part of * the search request. */ public TopHitsAggregationBuilder fieldDataFields(List<String> fieldDataFields) { if (fieldDataFields == null) { throw new IllegalArgumentException("[fieldDataFields] must not be null: [" + name + "]"); } if (this.fieldDataFields == null) { this.fieldDataFields = new ArrayList<>(); } this.fieldDataFields.addAll(fieldDataFields); return this; } /** * Gets the field-data fields. */ public List<String> fieldDataFields() { return fieldDataFields; } /** * Adds a script field under the given name with the provided script. * * @param name * The name of the field * @param script * The script */ public TopHitsAggregationBuilder scriptField(String name, Script script) { if (name == null) { throw new IllegalArgumentException("scriptField [name] must not be null: [" + name + "]"); } if (script == null) { throw new IllegalArgumentException("scriptField [script] must not be null: [" + name + "]"); } scriptField(name, script, false); return this; } /** * Adds a script field under the given name with the provided script. * * @param name * The name of the field * @param script * The script */ public TopHitsAggregationBuilder scriptField(String name, Script script, boolean ignoreFailure) { if (name == null) { throw new IllegalArgumentException("scriptField [name] must not be null: [" + name + "]"); } if (script == null) { throw new IllegalArgumentException("scriptField [script] must not be null: [" + name + "]"); } if (scriptFields == null) { scriptFields = new HashSet<>(); } scriptFields.add(new ScriptField(name, script, ignoreFailure)); return this; } public TopHitsAggregationBuilder scriptFields(List<ScriptField> scriptFields) { if (scriptFields == null) { throw new IllegalArgumentException("[scriptFields] must not be null: [" + name + "]"); } if (this.scriptFields == null) { this.scriptFields = new HashSet<>(); } this.scriptFields.addAll(scriptFields); return this; } /** * Gets the script fields. */ public Set<ScriptField> scriptFields() { return scriptFields; } /** * Should each {@link org.elasticsearch.search.SearchHit} be returned * with an explanation of the hit (ranking). */ public TopHitsAggregationBuilder explain(boolean explain) { this.explain = explain; return this; } /** * Indicates whether each search hit will be returned with an * explanation of the hit (ranking) */ public boolean explain() { return explain; } /** * Should each {@link org.elasticsearch.search.SearchHit} be returned * with a version associated with it. */ public TopHitsAggregationBuilder version(boolean version) { this.version = version; return this; } /** * Indicates whether the document's version will be included in the * search hits. */ public boolean version() { return version; } /** * Applies when sorting, and controls if scores will be tracked as well. * Defaults to <tt>false</tt>. */ public TopHitsAggregationBuilder trackScores(boolean trackScores) { this.trackScores = trackScores; return this; } /** * Indicates whether scores will be tracked for this request. */ public boolean trackScores() { return trackScores; } @Override public TopHitsAggregationBuilder subAggregations(Builder subFactories) { throw new AggregationInitializationException("Aggregator [" + name + "] of type [" + getType() + "] cannot accept sub-aggregations"); } @Override protected TopHitsAggregatorFactory doBuild(SearchContext context, AggregatorFactory<?> parent, Builder subfactoriesBuilder) throws IOException { List<ScriptFieldsContext.ScriptField> fields = new ArrayList<>(); if (scriptFields != null) { for (ScriptField field : scriptFields) { SearchScript searchScript = context.getQueryShardContext().getSearchScript(field.script(), ScriptContext.Standard.SEARCH); fields.add(new org.elasticsearch.search.fetch.subphase.ScriptFieldsContext.ScriptField( field.fieldName(), searchScript, field.ignoreFailure())); } } final Optional<SortAndFormats> optionalSort; if (sorts == null) { optionalSort = Optional.empty(); } else { optionalSort = SortBuilder.buildSort(sorts, context.getQueryShardContext()); } return new TopHitsAggregatorFactory(name, from, size, explain, version, trackScores, optionalSort, highlightBuilder, storedFieldsContext, fieldDataFields, fields, fetchSourceContext, context, parent, subfactoriesBuilder, metaData); } @Override protected XContentBuilder internalXContent(XContentBuilder builder, Params params) throws IOException { builder.startObject(); 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); if (fetchSourceContext != null) { builder.field(SearchSourceBuilder._SOURCE_FIELD.getPreferredName(), fetchSourceContext); } if (storedFieldsContext != null) { storedFieldsContext.toXContent(SearchSourceBuilder.STORED_FIELDS_FIELD.getPreferredName(), builder); } if (fieldDataFields != null) { builder.startArray(SearchSourceBuilder.DOCVALUE_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 (trackScores) { builder.field(SearchSourceBuilder.TRACK_SCORES_FIELD.getPreferredName(), true); } if (highlightBuilder != null) { builder.field(SearchSourceBuilder.HIGHLIGHT_FIELD.getPreferredName(), highlightBuilder); } builder.endObject(); return builder; } public static TopHitsAggregationBuilder parse(String aggregationName, QueryParseContext context) throws IOException { TopHitsAggregationBuilder factory = new TopHitsAggregationBuilder(aggregationName); XContentParser.Token token; String currentFieldName = null; XContentParser parser = context.parser(); while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { if (token == XContentParser.Token.FIELD_NAME) { currentFieldName = parser.currentName(); } else if (token.isValue()) { if (SearchSourceBuilder.FROM_FIELD.match(currentFieldName)) { factory.from(parser.intValue()); } else if (SearchSourceBuilder.SIZE_FIELD.match(currentFieldName)) { factory.size(parser.intValue()); } else if (SearchSourceBuilder.VERSION_FIELD.match(currentFieldName)) { factory.version(parser.booleanValue()); } else if (SearchSourceBuilder.EXPLAIN_FIELD.match(currentFieldName)) { factory.explain(parser.booleanValue()); } else if (SearchSourceBuilder.TRACK_SCORES_FIELD.match(currentFieldName)) { factory.trackScores(parser.booleanValue()); } else if (SearchSourceBuilder._SOURCE_FIELD.match(currentFieldName)) { factory.fetchSource(FetchSourceContext.fromXContent(context.parser())); } else if (SearchSourceBuilder.STORED_FIELDS_FIELD.match(currentFieldName)) { factory.storedFieldsContext = StoredFieldsContext.fromXContent(SearchSourceBuilder.STORED_FIELDS_FIELD.getPreferredName(), context); } else if (SearchSourceBuilder.SORT_FIELD.match(currentFieldName)) { factory.sort(parser.text()); } else { throw new ParsingException(parser.getTokenLocation(), "Unknown key for a " + token + " in [" + currentFieldName + "].", parser.getTokenLocation()); } } else if (token == XContentParser.Token.START_OBJECT) { if (SearchSourceBuilder._SOURCE_FIELD.match(currentFieldName)) { factory.fetchSource(FetchSourceContext.fromXContent(context.parser())); } else if (SearchSourceBuilder.SCRIPT_FIELDS_FIELD.match(currentFieldName)) { List<ScriptField> 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 (SearchSourceBuilder.SCRIPT_FIELD.match(currentFieldName)) { script = Script.parse(parser); } else if (SearchSourceBuilder.IGNORE_FAILURE_FIELD.match(currentFieldName)) { 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 (SearchSourceBuilder.SCRIPT_FIELD.match(currentFieldName)) { script = Script.parse(parser); } 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()); } } factory.scriptFields(scriptFields); } else if (SearchSourceBuilder.HIGHLIGHT_FIELD.match(currentFieldName)) { factory.highlighter(HighlightBuilder.fromXContent(context)); } else if (SearchSourceBuilder.SORT_FIELD.match(currentFieldName)) { List<SortBuilder<?>> sorts = SortBuilder.fromXContent(context); factory.sorts(sorts); } else { throw new ParsingException(parser.getTokenLocation(), "Unknown key for a " + token + " in [" + currentFieldName + "].", parser.getTokenLocation()); } } else if (token == XContentParser.Token.START_ARRAY) { if (SearchSourceBuilder.STORED_FIELDS_FIELD.match(currentFieldName)) { factory.storedFieldsContext = StoredFieldsContext.fromXContent(SearchSourceBuilder.STORED_FIELDS_FIELD.getPreferredName(), context); } else if (SearchSourceBuilder.DOCVALUE_FIELDS_FIELD.match(currentFieldName)) { List<String> fieldDataFields = new ArrayList<>(); while ((token = parser.nextToken()) != XContentParser.Token.END_ARRAY) { if (token == XContentParser.Token.VALUE_STRING) { fieldDataFields.add(parser.text()); } else { throw new ParsingException(parser.getTokenLocation(), "Expected [" + XContentParser.Token.VALUE_STRING + "] in [" + currentFieldName + "] but found [" + token + "]", parser.getTokenLocation()); } } factory.fieldDataFields(fieldDataFields); } else if (SearchSourceBuilder.SORT_FIELD.match(currentFieldName)) { List<SortBuilder<?>> sorts = SortBuilder.fromXContent(context); factory.sorts(sorts); } else if (SearchSourceBuilder._SOURCE_FIELD.match(currentFieldName)) { factory.fetchSource(FetchSourceContext.fromXContent(context.parser())); } 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()); } } return factory; } @Override protected int doHashCode() { return Objects.hash(explain, fetchSourceContext, fieldDataFields, storedFieldsContext, from, highlightBuilder, scriptFields, size, sorts, trackScores, version); } @Override protected boolean doEquals(Object obj) { TopHitsAggregationBuilder other = (TopHitsAggregationBuilder) obj; return Objects.equals(explain, other.explain) && Objects.equals(fetchSourceContext, other.fetchSourceContext) && Objects.equals(fieldDataFields, other.fieldDataFields) && 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); } @Override public String getType() { return NAME; } }