/*
* Licensed to Elastic Search and Shay Banon under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. Elastic Search 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.internal;
import org.apache.lucene.search.Explanation;
import org.elasticsearch.ElasticSearchParseException;
import org.elasticsearch.common.Nullable;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.Unicode;
import org.elasticsearch.common.collect.ImmutableMap;
import org.elasticsearch.common.compress.lzf.LZF;
import org.elasticsearch.common.compress.lzf.LZFDecoder;
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.XContentBuilderString;
import org.elasticsearch.common.xcontent.XContentFactory;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.rest.action.support.RestXContentBuilder;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.SearchHitField;
import org.elasticsearch.search.SearchShardTarget;
import org.elasticsearch.search.highlight.HighlightField;
import java.io.IOException;
import java.util.Iterator;
import java.util.Map;
import static org.elasticsearch.common.lucene.Lucene.*;
import static org.elasticsearch.search.SearchShardTarget.*;
import static org.elasticsearch.search.highlight.HighlightField.*;
import static org.elasticsearch.search.internal.InternalSearchHitField.*;
/**
* @author kimchy (shay.banon)
*/
public class InternalSearchHit implements SearchHit {
private static final Object[] EMPTY_SORT_VALUES = new Object[0];
private transient int docId;
private float score = Float.NEGATIVE_INFINITY;
private String id;
private String type;
private long version = -1;
private byte[] source;
private Map<String, SearchHitField> fields = ImmutableMap.of();
private Map<String, HighlightField> highlightFields = ImmutableMap.of();
private Object[] sortValues = EMPTY_SORT_VALUES;
private String[] matchedFilters = Strings.EMPTY_ARRAY;
private Explanation explanation;
@Nullable private SearchShardTarget shard;
private Map<String, Object> sourceAsMap;
private InternalSearchHit() {
}
public InternalSearchHit(int docId, String id, String type, byte[] source, Map<String, SearchHitField> fields) {
this.docId = docId;
this.id = id;
this.type = type;
this.source = source;
this.fields = fields;
}
public int docId() {
return this.docId;
}
public void shardTarget(SearchShardTarget shardTarget) {
this.shard = shardTarget;
}
public void score(float score) {
this.score = score;
}
@Override public float score() {
return this.score;
}
@Override public float getScore() {
return score();
}
public void version(long version) {
this.version = version;
}
@Override public long version() {
return this.version;
}
@Override public long getVersion() {
return this.version;
}
@Override public String index() {
return shard.index();
}
@Override public String getIndex() {
return index();
}
@Override public String id() {
return id;
}
@Override public String getId() {
return id();
}
@Override public String type() {
return type;
}
@Override public String getType() {
return type();
}
@Override public byte[] source() {
if (source == null) {
return null;
}
if (LZF.isCompressed(source)) {
try {
this.source = LZFDecoder.decode(source);
} catch (IOException e) {
throw new ElasticSearchParseException("failed to decompress source", e);
}
}
return this.source;
}
@Override public boolean isSourceEmpty() {
return source == null;
}
@Override public Map<String, Object> getSource() {
return sourceAsMap();
}
@Override public String sourceAsString() {
if (source == null) {
return null;
}
return Unicode.fromBytes(source());
}
@SuppressWarnings({"unchecked"})
@Override public Map<String, Object> sourceAsMap() throws ElasticSearchParseException {
if (source == null) {
return null;
}
if (sourceAsMap != null) {
return sourceAsMap;
}
byte[] source = source();
XContentParser parser = null;
try {
parser = XContentFactory.xContent(source).createParser(source);
sourceAsMap = parser.map();
parser.close();
return sourceAsMap;
} catch (Exception e) {
throw new ElasticSearchParseException("Failed to parse source to map", e);
} finally {
if (parser != null) {
parser.close();
}
}
}
@Override public Iterator<SearchHitField> iterator() {
return fields.values().iterator();
}
@Override public SearchHitField field(String fieldName) {
return fields().get(fieldName);
}
@Override public Map<String, SearchHitField> fields() {
if (fields == null) {
return ImmutableMap.of();
}
return fields;
}
// returns the fields without handling null cases
public Map<String, SearchHitField> fieldsOrNull() {
return this.fields;
}
@Override public Map<String, SearchHitField> getFields() {
return fields();
}
public void fields(Map<String, SearchHitField> fields) {
this.fields = fields;
}
@Override public Map<String, HighlightField> highlightFields() {
return this.highlightFields;
}
@Override public Map<String, HighlightField> getHighlightFields() {
return highlightFields();
}
public void highlightFields(Map<String, HighlightField> highlightFields) {
this.highlightFields = highlightFields;
}
public void sortValues(Object[] sortValues) {
this.sortValues = sortValues;
}
@Override public Object[] sortValues() {
return sortValues;
}
@Override public Object[] getSortValues() {
return sortValues();
}
@Override public Explanation explanation() {
return explanation;
}
@Override public Explanation getExplanation() {
return explanation();
}
public void explanation(Explanation explanation) {
this.explanation = explanation;
}
@Override public SearchShardTarget shard() {
return shard;
}
@Override public SearchShardTarget getShard() {
return shard();
}
public void shard(SearchShardTarget target) {
this.shard = target;
}
public void matchedFilters(String[] matchedFilters) {
this.matchedFilters = matchedFilters;
}
public String[] matchedFilters() {
return this.matchedFilters;
}
@Override public String[] getMatchedFilters() {
return this.matchedFilters;
}
public static class Fields {
static final XContentBuilderString _INDEX = new XContentBuilderString("_index");
static final XContentBuilderString _TYPE = new XContentBuilderString("_type");
static final XContentBuilderString _ID = new XContentBuilderString("_id");
static final XContentBuilderString _VERSION = new XContentBuilderString("_version");
static final XContentBuilderString _SCORE = new XContentBuilderString("_score");
static final XContentBuilderString FIELDS = new XContentBuilderString("fields");
static final XContentBuilderString HIGHLIGHT = new XContentBuilderString("highlight");
static final XContentBuilderString SORT = new XContentBuilderString("sort");
static final XContentBuilderString MATCH_FILTERS = new XContentBuilderString("matched_filters");
static final XContentBuilderString _EXPLANATION = new XContentBuilderString("_explanation");
static final XContentBuilderString VALUE = new XContentBuilderString("value");
static final XContentBuilderString DESCRIPTION = new XContentBuilderString("description");
static final XContentBuilderString DETAILS = new XContentBuilderString("details");
}
@Override public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
builder.startObject();
if (explanation() != null) {
builder.field("_shard", shard.shardId());
builder.field("_node", shard.nodeId());
}
builder.field(Fields._INDEX, shard.index());
builder.field(Fields._TYPE, type);
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);
}
if (source != null) {
RestXContentBuilder.restDocumentSource(source, builder, params);
}
if (fields != null && !fields.isEmpty()) {
builder.startObject(Fields.FIELDS);
for (SearchHitField field : fields.values()) {
if (field.values().isEmpty()) {
continue;
}
if (field.values().size() == 1) {
builder.field(field.name(), field.values().get(0));
} else {
builder.field(field.name());
builder.startArray();
for (Object value : field.values()) {
builder.value(value);
}
builder.endArray();
}
}
builder.endObject();
}
if (highlightFields != null && !highlightFields.isEmpty()) {
builder.startObject(Fields.HIGHLIGHT);
for (HighlightField field : highlightFields.values()) {
builder.field(field.name());
if (field.fragments() == null) {
builder.nullValue();
} else {
builder.startArray();
for (String fragment : field.fragments()) {
builder.value(fragment);
}
builder.endArray();
}
}
builder.endObject();
}
if (sortValues != null && sortValues.length > 0) {
builder.startArray(Fields.SORT);
for (Object sortValue : sortValues) {
builder.value(sortValue);
}
builder.endArray();
}
if (matchedFilters.length > 0) {
builder.startArray(Fields.MATCH_FILTERS);
for (String matchedFilter : matchedFilters) {
builder.value(matchedFilter);
}
builder.endArray();
}
if (explanation() != null) {
builder.field(Fields._EXPLANATION);
buildExplanation(builder, explanation());
}
builder.endObject();
return builder;
}
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 InternalSearchHit readSearchHit(StreamInput in, InternalSearchHits.StreamContext context) throws IOException {
InternalSearchHit hit = new InternalSearchHit();
hit.readFrom(in, context);
return hit;
}
@Override public void readFrom(StreamInput in) throws IOException {
readFrom(in, InternalSearchHits.streamContext().streamShardTarget(InternalSearchHits.StreamContext.ShardTargetType.STREAM));
}
public void readFrom(StreamInput in, InternalSearchHits.StreamContext context) throws IOException {
score = in.readFloat();
id = in.readUTF();
type = in.readUTF();
version = in.readLong();
int size = in.readVInt();
if (size > 0) {
source = new byte[size];
in.readFully(source);
}
if (in.readBoolean()) {
explanation = readExplanation(in);
}
size = in.readVInt();
if (size == 0) {
fields = ImmutableMap.of();
} else if (size == 1) {
SearchHitField hitField = readSearchHitField(in);
fields = ImmutableMap.of(hitField.name(), hitField);
} else if (size == 2) {
SearchHitField hitField1 = readSearchHitField(in);
SearchHitField hitField2 = readSearchHitField(in);
fields = ImmutableMap.of(hitField1.name(), hitField1, hitField2.name(), hitField2);
} else if (size == 3) {
SearchHitField hitField1 = readSearchHitField(in);
SearchHitField hitField2 = readSearchHitField(in);
SearchHitField hitField3 = readSearchHitField(in);
fields = ImmutableMap.of(hitField1.name(), hitField1, hitField2.name(), hitField2, hitField3.name(), hitField3);
} else if (size == 4) {
SearchHitField hitField1 = readSearchHitField(in);
SearchHitField hitField2 = readSearchHitField(in);
SearchHitField hitField3 = readSearchHitField(in);
SearchHitField hitField4 = readSearchHitField(in);
fields = ImmutableMap.of(hitField1.name(), hitField1, hitField2.name(), hitField2, hitField3.name(), hitField3, hitField4.name(), hitField4);
} else if (size == 5) {
SearchHitField hitField1 = readSearchHitField(in);
SearchHitField hitField2 = readSearchHitField(in);
SearchHitField hitField3 = readSearchHitField(in);
SearchHitField hitField4 = readSearchHitField(in);
SearchHitField hitField5 = readSearchHitField(in);
fields = ImmutableMap.of(hitField1.name(), hitField1, hitField2.name(), hitField2, hitField3.name(), hitField3, hitField4.name(), hitField4, hitField5.name(), hitField5);
} else {
ImmutableMap.Builder<String, SearchHitField> builder = ImmutableMap.builder();
for (int i = 0; i < size; i++) {
SearchHitField hitField = readSearchHitField(in);
builder.put(hitField.name(), hitField);
}
fields = builder.build();
}
size = in.readVInt();
if (size == 0) {
highlightFields = ImmutableMap.of();
} else if (size == 1) {
HighlightField field = readHighlightField(in);
highlightFields = ImmutableMap.of(field.name(), field);
} else if (size == 2) {
HighlightField field1 = readHighlightField(in);
HighlightField field2 = readHighlightField(in);
highlightFields = ImmutableMap.of(field1.name(), field1, field2.name(), field2);
} else if (size == 3) {
HighlightField field1 = readHighlightField(in);
HighlightField field2 = readHighlightField(in);
HighlightField field3 = readHighlightField(in);
highlightFields = ImmutableMap.of(field1.name(), field1, field2.name(), field2, field3.name(), field3);
} else if (size == 4) {
HighlightField field1 = readHighlightField(in);
HighlightField field2 = readHighlightField(in);
HighlightField field3 = readHighlightField(in);
HighlightField field4 = readHighlightField(in);
highlightFields = ImmutableMap.of(field1.name(), field1, field2.name(), field2, field3.name(), field3, field4.name(), field4);
} else {
ImmutableMap.Builder<String, HighlightField> builder = ImmutableMap.builder();
for (int i = 0; i < size; i++) {
HighlightField field = readHighlightField(in);
builder.put(field.name(), field);
}
highlightFields = builder.build();
}
size = in.readVInt();
if (size > 0) {
sortValues = new Object[size];
for (int i = 0; i < sortValues.length; i++) {
byte type = in.readByte();
if (type == 0) {
sortValues[i] = null;
} else if (type == 1) {
sortValues[i] = in.readUTF();
} else if (type == 2) {
sortValues[i] = in.readInt();
} else if (type == 3) {
sortValues[i] = in.readLong();
} else if (type == 4) {
sortValues[i] = in.readFloat();
} else if (type == 5) {
sortValues[i] = in.readDouble();
} else if (type == 6) {
sortValues[i] = in.readByte();
} else if (type == 7) {
sortValues[i] = in.readShort();
} else if (type == 8) {
sortValues[i] = in.readBoolean();
} else {
throw new IOException("Can't match type [" + type + "]");
}
}
}
size = in.readVInt();
if (size > 0) {
matchedFilters = new String[size];
for (int i = 0; i < size; i++) {
matchedFilters[i] = in.readUTF();
}
}
if (context.streamShardTarget() == InternalSearchHits.StreamContext.ShardTargetType.STREAM) {
if (in.readBoolean()) {
shard = readSearchShardTarget(in);
}
} else if (context.streamShardTarget() == InternalSearchHits.StreamContext.ShardTargetType.LOOKUP) {
int lookupId = in.readVInt();
if (lookupId > 0) {
shard = context.handleShardLookup().get(lookupId);
}
}
}
@Override public void writeTo(StreamOutput out) throws IOException {
writeTo(out, InternalSearchHits.streamContext().streamShardTarget(InternalSearchHits.StreamContext.ShardTargetType.STREAM));
}
public void writeTo(StreamOutput out, InternalSearchHits.StreamContext context) throws IOException {
out.writeFloat(score);
out.writeUTF(id);
out.writeUTF(type);
out.writeLong(version);
if (source == null) {
out.writeVInt(0);
} else {
out.writeVInt(source.length);
out.writeBytes(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 : fields().values()) {
hitField.writeTo(out);
}
}
if (highlightFields == null) {
out.writeVInt(0);
} else {
out.writeVInt(highlightFields.size());
for (HighlightField highlightField : highlightFields.values()) {
highlightField.writeTo(out);
}
}
if (sortValues.length == 0) {
out.writeVInt(0);
} else {
out.writeVInt(sortValues.length);
for (Object sortValue : sortValues) {
if (sortValue == null) {
out.writeByte((byte) 0);
} else {
Class type = sortValue.getClass();
if (type == String.class) {
out.writeByte((byte) 1);
out.writeUTF((String) sortValue);
} else if (type == Integer.class) {
out.writeByte((byte) 2);
out.writeInt((Integer) sortValue);
} else if (type == Long.class) {
out.writeByte((byte) 3);
out.writeLong((Long) sortValue);
} else if (type == Float.class) {
out.writeByte((byte) 4);
out.writeFloat((Float) sortValue);
} else if (type == Double.class) {
out.writeByte((byte) 5);
out.writeDouble((Double) sortValue);
} else if (type == Byte.class) {
out.writeByte((byte) 6);
out.writeByte((Byte) sortValue);
} else if (type == Short.class) {
out.writeByte((byte) 7);
out.writeShort((Short) sortValue);
} else if (type == Boolean.class) {
out.writeByte((byte) 8);
out.writeBoolean((Boolean) sortValue);
} else {
throw new IOException("Can't handle sort field value of type [" + type + "]");
}
}
}
}
if (matchedFilters.length == 0) {
out.writeVInt(0);
} else {
out.writeVInt(matchedFilters.length);
for (String matchedFilter : matchedFilters) {
out.writeUTF(matchedFilter);
}
}
if (context.streamShardTarget() == InternalSearchHits.StreamContext.ShardTargetType.STREAM) {
if (shard == null) {
out.writeBoolean(false);
} else {
out.writeBoolean(true);
shard.writeTo(out);
}
} else if (context.streamShardTarget() == InternalSearchHits.StreamContext.ShardTargetType.LOOKUP) {
if (shard == null) {
out.writeVInt(0);
} else {
out.writeVInt(context.shardHandleLookup().get(shard));
}
}
}
}