@Override
  public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
    List<SearchHitField> metaFields = Lists.newArrayList();
    List<SearchHitField> otherFields = Lists.newArrayList();
    if (fields != null && !fields.isEmpty()) {
      for (SearchHitField field : fields.values()) {
        if (field.values().isEmpty()) {
          continue;
        }
        if (field.isMetadataField()) {
          metaFields.add(field);
        } else {
          otherFields.add(field);
        }
      }
    }

    builder.startObject();
    // 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 (explanation() != null && shard != null) {
      builder.field("_shard", shard.shardId());
      builder.field("_node", shard.nodeIdText());
    }
    if (shard != null) {
      builder.field(Fields._INDEX, shard.indexText());
    }
    builder.field(Fields._TYPE, type);
    builder.field(Fields._ID, id);
    if (nestedIdentity != null) {
      nestedIdentity.toXContent(builder, params);
    }
    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) {
      builder.field(field.name(), field.value());
    }
    if (source != null) {
      XContentHelper.writeRawField("_source", source, builder, params);
    }
    if (!otherFields.isEmpty()) {
      builder.startObject(Fields.FIELDS);
      for (SearchHitField field : otherFields) {
        builder.startArray(field.name());
        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()) {
        builder.field(field.name());
        if (field.fragments() == null) {
          builder.nullValue();
        } else {
          builder.startArray();
          for (Text fragment : field.fragments()) {
            builder.value(fragment);
          }
          builder.endArray();
        }
      }
      builder.endObject();
    }
    if (sortValues != null && sortValues.length > 0) {
      builder.startArray(Fields.SORT);
      for (Object sortValue : sortValues) {
        if (sortValue != null && sortValue.equals(MAX_TERM_AS_TEXT)) {
          // We don't display MAX_TERM in JSON responses in case some clients have UTF-8 parsers
          // that wouldn't accept a
          // non-character in the response, even though this is valid UTF-8
          builder.nullValue();
        } else {
          builder.value(sortValue);
        }
      }
      builder.endArray();
    }
    if (matchedQueries.length > 0) {
      builder.startArray(Fields.MATCHED_QUERIES);
      for (String matchedFilter : matchedQueries) {
        builder.value(matchedFilter);
      }
      builder.endArray();
    }
    if (explanation() != null) {
      builder.field(Fields._EXPLANATION);
      buildExplanation(builder, explanation());
    }
    if (innerHits != null) {
      builder.startObject(Fields.INNER_HITS);
      for (Map.Entry<String, InternalSearchHits> entry : innerHits.entrySet()) {
        builder.startObject(entry.getKey());
        entry.getValue().toXContent(builder, params);
        builder.endObject();
      }
      builder.endObject();
    }
    builder.endObject();
    return builder;
  }
  public void readFrom(StreamInput in, InternalSearchHits.StreamContext context)
      throws IOException {
    score = in.readFloat();
    id = in.readText();
    type = in.readText();
    nestedIdentity = in.readOptionalStreamable(new InternalNestedIdentity());
    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 = 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.readString();
        } 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 if (type == 9) {
          sortValues[i] = in.readText();
        } else {
          throw new IOException("Can't match type [" + type + "]");
        }
      }
    }

    size = in.readVInt();
    if (size > 0) {
      matchedQueries = new String[size];
      for (int i = 0; i < size; i++) {
        matchedQueries[i] = in.readString();
      }
    }

    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);
      }
    }

    size = in.readVInt();
    if (size > 0) {
      innerHits = new HashMap<>(size);
      for (int i = 0; i < size; i++) {
        String key = in.readString();
        InternalSearchHits value =
            InternalSearchHits.readSearchHits(
                in,
                InternalSearchHits.streamContext()
                    .streamShardTarget(InternalSearchHits.StreamContext.ShardTargetType.NO_STREAM));
        innerHits.put(key, value);
      }
    }
  }
  public void readFrom(StreamInput in, InternalSearchHits.StreamContext context)
      throws IOException {
    score = in.readFloat();
    id = in.readText();
    type = in.readText();
    nestedIdentity = in.readOptionalStreamable(InternalNestedIdentity::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 = readSearchHitField(in);
      fields = singletonMap(hitField.name(), hitField);
    } else {
      Map<String, SearchHitField> fields = new HashMap<>();
      for (int i = 0; i < size; i++) {
        SearchHitField hitField = readSearchHitField(in);
        fields.put(hitField.name(), 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);
    }

    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.readString();
        } 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) {
      matchedQueries = new String[size];
      for (int i = 0; i < size; i++) {
        matchedQueries[i] = in.readString();
      }
    }

    if (context.streamShardTarget() == ShardTargetType.STREAM) {
      if (in.readBoolean()) {
        shard = new SearchShardTarget(in);
      }
    } else if (context.streamShardTarget() == ShardTargetType.LOOKUP) {
      int lookupId = in.readVInt();
      if (lookupId > 0) {
        shard = context.handleShardLookup().get(lookupId);
      }
    }

    size = in.readVInt();
    if (size > 0) {
      innerHits = new HashMap<>(size);
      for (int i = 0; i < size; i++) {
        String key = in.readString();
        ShardTargetType shardTarget = context.streamShardTarget();
        InternalSearchHits value =
            InternalSearchHits.readSearchHits(
                in, context.streamShardTarget(ShardTargetType.NO_STREAM));
        context.streamShardTarget(shardTarget);
        innerHits.put(key, value);
      }
    }
  }