/* * 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; import org.apache.lucene.search.Explanation; import org.elasticsearch.ElasticsearchParseException; import org.elasticsearch.action.OriginalIndices; import org.elasticsearch.common.Nullable; import org.elasticsearch.common.ParseField; import org.elasticsearch.common.ParsingException; import org.elasticsearch.common.Strings; import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.compress.CompressorFactory; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.io.stream.Streamable; import org.elasticsearch.common.io.stream.Writeable; import org.elasticsearch.common.text.Text; import org.elasticsearch.common.xcontent.ConstructingObjectParser; import org.elasticsearch.common.xcontent.ObjectParser; import org.elasticsearch.common.xcontent.ObjectParser.ValueType; import org.elasticsearch.common.xcontent.ToXContent; import org.elasticsearch.common.xcontent.ToXContentObject; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentHelper; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.index.mapper.MapperService; import org.elasticsearch.index.mapper.SourceFieldMapper; import org.elasticsearch.index.shard.ShardId; import org.elasticsearch.search.fetch.subphase.highlight.HighlightField; import org.elasticsearch.search.lookup.SourceLookup; import org.elasticsearch.search.suggest.completion.CompletionSuggestion; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Objects; import static java.util.Collections.emptyMap; import static java.util.Collections.singletonMap; import static java.util.Collections.unmodifiableMap; import static org.elasticsearch.common.lucene.Lucene.readExplanation; import static org.elasticsearch.common.lucene.Lucene.writeExplanation; import static org.elasticsearch.common.xcontent.ConstructingObjectParser.constructorArg; import static org.elasticsearch.common.xcontent.ConstructingObjectParser.optionalConstructorArg; import static org.elasticsearch.common.xcontent.XContentParserUtils.ensureExpectedToken; import static org.elasticsearch.common.xcontent.XContentParserUtils.parseStoredFieldsValue; import static org.elasticsearch.common.xcontent.XContentParserUtils.throwUnknownField; import static org.elasticsearch.search.fetch.subphase.highlight.HighlightField.readHighlightField; /** * A single search hit. * * @see SearchHits */ public final class SearchHit implements Streamable, ToXContentObject, Iterable<SearchHitField> { private transient int docId; private static final float DEFAULT_SCORE = Float.NEGATIVE_INFINITY; private float score = DEFAULT_SCORE; private Text id; private Text type; private NestedIdentity nestedIdentity; private long version = -1; private BytesReference source; private Map<String, SearchHitField> fields = emptyMap(); private Map<String, HighlightField> highlightFields = null; private SearchSortValues sortValues = SearchSortValues.EMPTY; private String[] matchedQueries = Strings.EMPTY_ARRAY; private Explanation explanation; @Nullable private SearchShardTarget shard; private transient String index; private Map<String, Object> sourceAsMap; private Map<String, SearchHits> innerHits; private SearchHit() { } public SearchHit(int docId) { this(docId, null, null, null); } public SearchHit(int docId, String id, Text type, Map<String, SearchHitField> fields) { this(docId, id, type, null, fields); } public SearchHit(int nestedTopDocId, String id, Text type, NestedIdentity nestedIdentity, Map<String, SearchHitField> fields) { this.docId = nestedTopDocId; if (id != null) { this.id = new Text(id); } else { this.id = null; } this.type = type; this.nestedIdentity = nestedIdentity; this.fields = fields; } public int docId() { return this.docId; } public void score(float score) { this.score = score; } /** * The score. */ public float getScore() { return this.score; } public void version(long version) { this.version = version; } /** * The version of the hit. */ public long getVersion() { return this.version; } /** * The index of the hit. */ public String getIndex() { return this.index; } /** * The id of the document. */ public String getId() { return id != null ? id.string() : null; } /** * The type of the document. */ public String getType() { return type != null ? type.string() : null; } /** * If this is a nested hit then nested reference information is returned otherwise <code>null</code> is returned. */ public NestedIdentity getNestedIdentity() { return nestedIdentity; } /** * Returns bytes reference, also un compress the source if needed. */ public BytesReference getSourceRef() { if (this.source == null) { return null; } try { this.source = CompressorFactory.uncompressIfNeeded(this.source); return this.source; } catch (IOException e) { throw new ElasticsearchParseException("failed to decompress source", e); } } /** * Sets representation, might be compressed.... */ public SearchHit sourceRef(BytesReference source) { this.source = source; this.sourceAsMap = null; return this; } /** * Is the source available or not. A source with no fields will return true. This will return false if {@code fields} doesn't contain * {@code _source} or if source is disabled in the mapping. */ public boolean hasSource() { return source != null; } /** * The source of the document as string (can be <tt>null</tt>). */ public String getSourceAsString() { if (source == null) { return null; } try { return XContentHelper.convertToJson(getSourceRef(), false); } catch (IOException e) { throw new ElasticsearchParseException("failed to convert source to a json string"); } } /** * The source of the document as a map (can be <tt>null</tt>). */ public Map<String, Object> getSourceAsMap() { if (source == null) { return null; } if (sourceAsMap != null) { return sourceAsMap; } sourceAsMap = SourceLookup.sourceAsMap(source); return sourceAsMap; } @Override public Iterator<SearchHitField> iterator() { return fields.values().iterator(); } /** * The hit field matching the given field name. */ public SearchHitField field(String fieldName) { return getFields().get(fieldName); } /** * A map of hit fields (from field name to hit fields) if additional fields * were required to be loaded. */ public Map<String, SearchHitField> getFields() { return fields == null ? emptyMap() : fields; } // returns the fields without handling null cases public Map<String, SearchHitField> fieldsOrNull() { return fields; } public void fields(Map<String, SearchHitField> fields) { this.fields = fields; } /** * A map of highlighted fields. */ public Map<String, HighlightField> getHighlightFields() { return highlightFields == null ? emptyMap() : highlightFields; } public void highlightFields(Map<String, HighlightField> highlightFields) { this.highlightFields = highlightFields; } public void sortValues(Object[] sortValues, DocValueFormat[] sortValueFormats) { sortValues(new SearchSortValues(sortValues, sortValueFormats)); } public void sortValues(SearchSortValues sortValues) { this.sortValues = sortValues; } /** * An array of the sort values used. */ public Object[] getSortValues() { return sortValues.sortValues(); } /** * If enabled, the explanation of the search hit. */ public Explanation getExplanation() { return explanation; } public void explanation(Explanation explanation) { this.explanation = explanation; } /** * The shard of the search hit. */ public SearchShardTarget getShard() { return shard; } public void shard(SearchShardTarget target) { this.shard = target; if (target != null) { this.index = target.getIndex(); } } public void matchedQueries(String[] matchedQueries) { this.matchedQueries = matchedQueries; } /** * The set of query and filter names the query matched with. Mainly makes sense for compound filters and queries. */ public String[] getMatchedQueries() { return this.matchedQueries; } /** * @return Inner hits or <code>null</code> if there are none */ public Map<String, SearchHits> getInnerHits() { return innerHits; } public void setInnerHits(Map<String, SearchHits> innerHits) { this.innerHits = innerHits; } public static class Fields { static final String _INDEX = "_index"; static final String _TYPE = "_type"; static final String _ID = "_id"; static final String _VERSION = "_version"; static final String _SCORE = "_score"; static final String FIELDS = "fields"; static final String HIGHLIGHT = "highlight"; static final String SORT = "sort"; static final String MATCHED_QUERIES = "matched_queries"; static final String _EXPLANATION = "_explanation"; static final String VALUE = "value"; static final String DESCRIPTION = "description"; static final String DETAILS = "details"; static final String INNER_HITS = "inner_hits"; static final String _SHARD = "_shard"; static final String _NODE = "_node"; } @Override public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { builder.startObject(); toInnerXContent(builder, params); builder.endObject(); return builder; } // public because we render hit as part of completion suggestion option public XContentBuilder toInnerXContent(XContentBuilder builder, Params params) throws IOException { List<SearchHitField> metaFields = new ArrayList<>(); List<SearchHitField> otherFields = new ArrayList<>(); if (fields != null && !fields.isEmpty()) { for (SearchHitField field : fields.values()) { if (field.getValues().isEmpty()) { continue; } if (field.isMetadataField()) { metaFields.add(field); } else { otherFields.add(field); } } } // For inner_hit hits shard is null and that is ok, because the parent search hit has all this information. // Even if this was included in the inner_hit hits this would be the same, so better leave it out. if (getExplanation() != null && shard != null) { builder.field(Fields._SHARD, shard.getShardId()); builder.field(Fields._NODE, shard.getNodeIdText()); } if (nestedIdentity != null) { nestedIdentity.toXContent(builder, params); } else { if (index != null) { builder.field(Fields._INDEX, index); } if (type != null) { builder.field(Fields._TYPE, type); } if (id != null) { builder.field(Fields._ID, id); } } if (version != -1) { builder.field(Fields._VERSION, version); } if (Float.isNaN(score)) { builder.nullField(Fields._SCORE); } else { builder.field(Fields._SCORE, score); } for (SearchHitField field : metaFields) { Object value = field.getValue(); builder.field(field.getName(), value); } if (source != null) { XContentHelper.writeRawField(SourceFieldMapper.NAME, source, builder, params); } if (!otherFields.isEmpty()) { builder.startObject(Fields.FIELDS); for (SearchHitField field : otherFields) { builder.startArray(field.getName()); for (Object value : field.getValues()) { builder.value(value); } builder.endArray(); } builder.endObject(); } if (highlightFields != null && !highlightFields.isEmpty()) { builder.startObject(Fields.HIGHLIGHT); for (HighlightField field : highlightFields.values()) { field.toXContent(builder, params); } builder.endObject(); } sortValues.toXContent(builder, params); if (matchedQueries.length > 0) { builder.startArray(Fields.MATCHED_QUERIES); for (String matchedFilter : matchedQueries) { builder.value(matchedFilter); } builder.endArray(); } if (getExplanation() != null) { builder.field(Fields._EXPLANATION); buildExplanation(builder, getExplanation()); } if (innerHits != null) { builder.startObject(Fields.INNER_HITS); for (Map.Entry<String, SearchHits> entry : innerHits.entrySet()) { builder.startObject(entry.getKey()); entry.getValue().toXContent(builder, params); builder.endObject(); } builder.endObject(); } return builder; } /** * This parser outputs a temporary map of the objects needed to create the * SearchHit instead of directly creating the SearchHit. The reason for this * is that this way we can reuse the parser when parsing xContent from * {@link CompletionSuggestion.Entry.Option} which unfortunately inlines the * output of * {@link #toInnerXContent(XContentBuilder, org.elasticsearch.common.xcontent.ToXContent.Params)} * of the included search hit. The output of the map is used to create the * actual SearchHit instance via {@link #createFromMap(Map)} */ private static ObjectParser<Map<String, Object>, Void> MAP_PARSER = new ObjectParser<>("innerHitsParser", HashMap::new); static { declareInnerHitsParseFields(MAP_PARSER); } public static SearchHit fromXContent(XContentParser parser) { return createFromMap(MAP_PARSER.apply(parser, null)); } public static void declareInnerHitsParseFields(ObjectParser<Map<String, Object>, Void> parser) { declareMetaDataFields(parser); parser.declareString((map, value) -> map.put(Fields._TYPE, new Text(value)), new ParseField(Fields._TYPE)); parser.declareString((map, value) -> map.put(Fields._INDEX, value), new ParseField(Fields._INDEX)); parser.declareString((map, value) -> map.put(Fields._ID, value), new ParseField(Fields._ID)); parser.declareString((map, value) -> map.put(Fields._NODE, value), new ParseField(Fields._NODE)); parser.declareField((map, value) -> map.put(Fields._SCORE, value), SearchHit::parseScore, new ParseField(Fields._SCORE), ValueType.FLOAT_OR_NULL); parser.declareLong((map, value) -> map.put(Fields._VERSION, value), new ParseField(Fields._VERSION)); parser.declareField((map, value) -> map.put(Fields._SHARD, value), (p, c) -> ShardId.fromString(p.text()), new ParseField(Fields._SHARD), ValueType.STRING); parser.declareObject((map, value) -> map.put(SourceFieldMapper.NAME, value), (p, c) -> parseSourceBytes(p), new ParseField(SourceFieldMapper.NAME)); parser.declareObject((map, value) -> map.put(Fields.HIGHLIGHT, value), (p, c) -> parseHighlightFields(p), new ParseField(Fields.HIGHLIGHT)); parser.declareObject((map, value) -> { Map<String, SearchHitField> fieldMap = get(Fields.FIELDS, map, new HashMap<String, SearchHitField>()); fieldMap.putAll(value); map.put(Fields.FIELDS, fieldMap); }, (p, c) -> parseFields(p), new ParseField(Fields.FIELDS)); parser.declareObject((map, value) -> map.put(Fields._EXPLANATION, value), (p, c) -> parseExplanation(p), new ParseField(Fields._EXPLANATION)); parser.declareObject((map, value) -> map.put(NestedIdentity._NESTED, value), NestedIdentity::fromXContent, new ParseField(NestedIdentity._NESTED)); parser.declareObject((map, value) -> map.put(Fields.INNER_HITS, value), (p,c) -> parseInnerHits(p), new ParseField(Fields.INNER_HITS)); parser.declareStringArray((map, list) -> map.put(Fields.MATCHED_QUERIES, list), new ParseField(Fields.MATCHED_QUERIES)); parser.declareField((map, list) -> map.put(Fields.SORT, list), SearchSortValues::fromXContent, new ParseField(Fields.SORT), ValueType.OBJECT_ARRAY); } public static SearchHit createFromMap(Map<String, Object> values) { String id = get(Fields._ID, values, null); Text type = get(Fields._TYPE, values, null); NestedIdentity nestedIdentity = get(NestedIdentity._NESTED, values, null); Map<String, SearchHitField> fields = get(Fields.FIELDS, values, null); SearchHit searchHit = new SearchHit(-1, id, type, nestedIdentity, fields); searchHit.index = get(Fields._INDEX, values, null); searchHit.score(get(Fields._SCORE, values, DEFAULT_SCORE)); searchHit.version(get(Fields._VERSION, values, -1L)); searchHit.sortValues(get(Fields.SORT, values, SearchSortValues.EMPTY)); searchHit.highlightFields(get(Fields.HIGHLIGHT, values, null)); searchHit.sourceRef(get(SourceFieldMapper.NAME, values, null)); searchHit.explanation(get(Fields._EXPLANATION, values, null)); searchHit.setInnerHits(get(Fields.INNER_HITS, values, null)); List<String> matchedQueries = get(Fields.MATCHED_QUERIES, values, null); if (matchedQueries != null) { searchHit.matchedQueries(matchedQueries.toArray(new String[matchedQueries.size()])); } ShardId shardId = get(Fields._SHARD, values, null); String nodeId = get(Fields._NODE, values, null); if (shardId != null && nodeId != null) { searchHit.shard(new SearchShardTarget(nodeId, shardId, null, OriginalIndices.NONE)); } searchHit.fields(fields); return searchHit; } @SuppressWarnings("unchecked") private static <T> T get(String key, Map<String, Object> map, T defaultValue) { return (T) map.getOrDefault(key, defaultValue); } private static float parseScore(XContentParser parser) throws IOException { if (parser.currentToken() == XContentParser.Token.VALUE_NUMBER || parser.currentToken() == XContentParser.Token.VALUE_STRING) { return parser.floatValue(); } else { return Float.NaN; } } private static BytesReference parseSourceBytes(XContentParser parser) throws IOException { try (XContentBuilder builder = XContentBuilder.builder(parser.contentType().xContent())) { // the original document gets slightly modified: whitespaces or // pretty printing are not preserved, // it all depends on the current builder settings builder.copyCurrentStructure(parser); return builder.bytes(); } } /** * we need to declare parse fields for each metadata field, except for _ID, _INDEX and _TYPE which are * handled individually. All other fields are parsed to an entry in the fields map */ private static void declareMetaDataFields(ObjectParser<Map<String, Object>, Void> parser) { for (String metadatafield : MapperService.getAllMetaFields()) { if (metadatafield.equals(Fields._ID) == false && metadatafield.equals(Fields._INDEX) == false && metadatafield.equals(Fields._TYPE) == false) { parser.declareField((map, field) -> { @SuppressWarnings("unchecked") Map<String, SearchHitField> fieldMap = (Map<String, SearchHitField>) map.computeIfAbsent(Fields.FIELDS, v -> new HashMap<String, SearchHitField>()); fieldMap.put(field.getName(), field); }, (p, c) -> { List<Object> values = new ArrayList<>(); values.add(parseStoredFieldsValue(p)); return new SearchHitField(metadatafield, values); }, new ParseField(metadatafield), ValueType.VALUE); } } } private static Map<String, SearchHitField> parseFields(XContentParser parser) throws IOException { Map<String, SearchHitField> fields = new HashMap<>(); while ((parser.nextToken()) != XContentParser.Token.END_OBJECT) { String fieldName = parser.currentName(); ensureExpectedToken(XContentParser.Token.START_ARRAY, parser.nextToken(), parser::getTokenLocation); List<Object> values = new ArrayList<>(); while ((parser.nextToken()) != XContentParser.Token.END_ARRAY) { values.add(parseStoredFieldsValue(parser)); } fields.put(fieldName, new SearchHitField(fieldName, values)); } return fields; } private static Map<String, SearchHits> parseInnerHits(XContentParser parser) throws IOException { Map<String, SearchHits> innerHits = new HashMap<>(); while ((parser.nextToken()) != XContentParser.Token.END_OBJECT) { ensureExpectedToken(XContentParser.Token.FIELD_NAME, parser.currentToken(), parser::getTokenLocation); innerHits.put(parser.currentName(), SearchHits.fromXContent(parser)); ensureExpectedToken(XContentParser.Token.END_OBJECT, parser.nextToken(), parser::getTokenLocation); } return innerHits; } private static Map<String, HighlightField> parseHighlightFields(XContentParser parser) throws IOException { Map<String, HighlightField> highlightFields = new HashMap<>(); while((parser.nextToken()) != XContentParser.Token.END_OBJECT) { HighlightField highlightField = HighlightField.fromXContent(parser); highlightFields.put(highlightField.getName(), highlightField); } return highlightFields; } private static Explanation parseExplanation(XContentParser parser) throws IOException { ensureExpectedToken(XContentParser.Token.START_OBJECT, parser.currentToken(), parser::getTokenLocation); XContentParser.Token token; Float value = null; String description = null; List<Explanation> details = new ArrayList<>(); while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { ensureExpectedToken(XContentParser.Token.FIELD_NAME, token, () -> parser.getTokenLocation()); String currentFieldName = parser.currentName(); token = parser.nextToken(); if (Fields.VALUE.equals(currentFieldName)) { value = parser.floatValue(); } else if (Fields.DESCRIPTION.equals(currentFieldName)) { description = parser.textOrNull(); } else if (Fields.DETAILS.equals(currentFieldName)) { ensureExpectedToken(XContentParser.Token.START_ARRAY, token, () -> parser.getTokenLocation()); while ((token = parser.nextToken()) != XContentParser.Token.END_ARRAY) { details.add(parseExplanation(parser)); } } else { throwUnknownField(currentFieldName, parser.getTokenLocation()); } } if (value == null) { throw new ParsingException(parser.getTokenLocation(), "missing explanation value"); } if (description == null) { throw new ParsingException(parser.getTokenLocation(), "missing explanation description"); } return Explanation.match(value, description, details); } private void buildExplanation(XContentBuilder builder, Explanation explanation) throws IOException { builder.startObject(); builder.field(Fields.VALUE, explanation.getValue()); builder.field(Fields.DESCRIPTION, explanation.getDescription()); Explanation[] innerExps = explanation.getDetails(); if (innerExps != null) { builder.startArray(Fields.DETAILS); for (Explanation exp : innerExps) { buildExplanation(builder, exp); } builder.endArray(); } builder.endObject(); } public static SearchHit readSearchHit(StreamInput in) throws IOException { SearchHit hit = new SearchHit(); hit.readFrom(in); return hit; } @Override public void readFrom(StreamInput in) throws IOException { score = in.readFloat(); id = in.readOptionalText(); type = in.readOptionalText(); nestedIdentity = in.readOptionalWriteable(NestedIdentity::new); version = in.readLong(); source = in.readBytesReference(); if (source.length() == 0) { source = null; } if (in.readBoolean()) { explanation = readExplanation(in); } int size = in.readVInt(); if (size == 0) { fields = emptyMap(); } else if (size == 1) { SearchHitField hitField = SearchHitField.readSearchHitField(in); fields = singletonMap(hitField.getName(), hitField); } else { Map<String, SearchHitField> fields = new HashMap<>(); for (int i = 0; i < size; i++) { SearchHitField hitField = SearchHitField.readSearchHitField(in); fields.put(hitField.getName(), hitField); } this.fields = unmodifiableMap(fields); } size = in.readVInt(); if (size == 0) { highlightFields = emptyMap(); } else if (size == 1) { HighlightField field = readHighlightField(in); highlightFields = singletonMap(field.name(), field); } else { Map<String, HighlightField> highlightFields = new HashMap<>(); for (int i = 0; i < size; i++) { HighlightField field = readHighlightField(in); highlightFields.put(field.name(), field); } this.highlightFields = unmodifiableMap(highlightFields); } sortValues = new SearchSortValues(in); size = in.readVInt(); if (size > 0) { matchedQueries = new String[size]; for (int i = 0; i < size; i++) { matchedQueries[i] = in.readString(); } } // we call the setter here because that also sets the local index parameter shard(in.readOptionalWriteable(SearchShardTarget::new)); size = in.readVInt(); if (size > 0) { innerHits = new HashMap<>(size); for (int i = 0; i < size; i++) { String key = in.readString(); SearchHits value = SearchHits.readSearchHits(in); innerHits.put(key, value); } } } @Override public void writeTo(StreamOutput out) throws IOException { out.writeFloat(score); out.writeOptionalText(id); out.writeOptionalText(type); out.writeOptionalWriteable(nestedIdentity); out.writeLong(version); out.writeBytesReference(source); if (explanation == null) { out.writeBoolean(false); } else { out.writeBoolean(true); writeExplanation(out, explanation); } if (fields == null) { out.writeVInt(0); } else { out.writeVInt(fields.size()); for (SearchHitField hitField : getFields().values()) { hitField.writeTo(out); } } if (highlightFields == null) { out.writeVInt(0); } else { out.writeVInt(highlightFields.size()); for (HighlightField highlightField : highlightFields.values()) { highlightField.writeTo(out); } } sortValues.writeTo(out); if (matchedQueries.length == 0) { out.writeVInt(0); } else { out.writeVInt(matchedQueries.length); for (String matchedFilter : matchedQueries) { out.writeString(matchedFilter); } } out.writeOptionalWriteable(shard); if (innerHits == null) { out.writeVInt(0); } else { out.writeVInt(innerHits.size()); for (Map.Entry<String, SearchHits> entry : innerHits.entrySet()) { out.writeString(entry.getKey()); entry.getValue().writeTo(out); } } } @Override public boolean equals(Object obj) { if (obj == null || getClass() != obj.getClass()) { return false; } SearchHit other = (SearchHit) obj; return Objects.equals(id, other.id) && Objects.equals(type, other.type) && Objects.equals(nestedIdentity, other.nestedIdentity) && Objects.equals(version, other.version) && Objects.equals(source, other.source) && Objects.equals(fields, other.fields) && Objects.equals(getHighlightFields(), other.getHighlightFields()) && Arrays.equals(matchedQueries, other.matchedQueries) && Objects.equals(explanation, other.explanation) && Objects.equals(shard, other.shard) && Objects.equals(innerHits, other.innerHits); } @Override public int hashCode() { return Objects.hash(id, type, nestedIdentity, version, source, fields, getHighlightFields(), Arrays.hashCode(matchedQueries), explanation, shard, innerHits); } /** * Encapsulates the nested identity of a hit. */ public static final class NestedIdentity implements Writeable, ToXContent { private static final String _NESTED = "_nested"; private static final String FIELD = "field"; private static final String OFFSET = "offset"; private Text field; private int offset; private NestedIdentity child; public NestedIdentity(String field, int offset, NestedIdentity child) { this.field = new Text(field); this.offset = offset; this.child = child; } NestedIdentity(StreamInput in) throws IOException { field = in.readOptionalText(); offset = in.readInt(); child = in.readOptionalWriteable(NestedIdentity::new); } /** * Returns the nested field in the source this hit originates from */ public Text getField() { return field; } /** * Returns the offset in the nested array of objects in the source this hit */ public int getOffset() { return offset; } /** * Returns the next child nested level if there is any, otherwise <code>null</code> is returned. * * In the case of mappings with multiple levels of nested object fields */ public NestedIdentity getChild() { return child; } @Override public void writeTo(StreamOutput out) throws IOException { out.writeOptionalText(field); out.writeInt(offset); out.writeOptionalWriteable(child); } @Override public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { builder.field(_NESTED); return innerToXContent(builder, params); } /** * Rendering of the inner XContent object without the leading field name. This way the structure innerToXContent renders and * fromXContent parses correspond to each other. */ XContentBuilder innerToXContent(XContentBuilder builder, Params params) throws IOException { builder.startObject(); if (field != null) { builder.field(FIELD, field); } if (offset != -1) { builder.field(OFFSET, offset); } if (child != null) { builder = child.toXContent(builder, params); } builder.endObject(); return builder; } private static final ConstructingObjectParser<NestedIdentity, Void> PARSER = new ConstructingObjectParser<>( "nested_identity", ctorArgs -> new NestedIdentity((String) ctorArgs[0], (int) ctorArgs[1], (NestedIdentity) ctorArgs[2])); static { PARSER.declareString(constructorArg(), new ParseField(FIELD)); PARSER.declareInt(constructorArg(), new ParseField(OFFSET)); PARSER.declareObject(optionalConstructorArg(), PARSER, new ParseField(_NESTED)); } static NestedIdentity fromXContent(XContentParser parser, Void context) { return fromXContent(parser); } public static NestedIdentity fromXContent(XContentParser parser) { return PARSER.apply(parser, null); } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null || getClass() != obj.getClass()) { return false; } NestedIdentity other = (NestedIdentity) obj; return Objects.equals(field, other.field) && Objects.equals(offset, other.offset) && Objects.equals(child, other.child); } @Override public int hashCode() { return Objects.hash(field, offset, child); } } }