@Override
 public void doXContent(XContentBuilder builder, Params params) throws IOException {
   builder.startObject(getName());
   builder.field(fieldName);
   XContentParser parser = XContentFactory.xContent(functionBytes).createParser(functionBytes);
   builder.copyCurrentStructure(parser);
   builder.field(DecayFunctionParser.MULTI_VALUE_MODE.getPreferredName(), multiValueMode.name());
   builder.endObject();
 }
 @Override
 @SuppressWarnings("rawtypes") // ValueSource uses a rawtype
 public FunctionValues getValues(Map context, LeafReaderContext leaf) throws IOException {
   AtomicNumericFieldData leafData = (AtomicNumericFieldData) fieldData.load(leaf);
   NumericDoubleValues docValues = multiValueMode.select(leafData.getDoubleValues(), 0d);
   return new DoubleDocValues(this) {
     @Override
     public double doubleVal(int doc) {
       return docValues.get(doc);
     }
   };
 }
  @Override
  public SortField parse(XContentParser parser, SearchContext context) throws Exception {
    String script = null;
    String scriptLang = null;
    String type = null;
    Map<String, Object> params = null;
    boolean reverse = false;
    MultiValueMode sortMode = null;
    NestedInnerQueryParseSupport nestedHelper = null;

    XContentParser.Token token;
    String currentName = parser.currentName();
    ScriptService.ScriptType scriptType = ScriptService.ScriptType.INLINE;
    while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
      if (token == XContentParser.Token.FIELD_NAME) {
        currentName = parser.currentName();
      } else if (token == XContentParser.Token.START_OBJECT) {
        if ("params".equals(currentName)) {
          params = parser.map();
        } else if ("nested_filter".equals(currentName) || "nestedFilter".equals(currentName)) {
          if (nestedHelper == null) {
            nestedHelper = new NestedInnerQueryParseSupport(parser, context);
          }
          nestedHelper.filter();
        }
      } else if (token.isValue()) {
        if ("reverse".equals(currentName)) {
          reverse = parser.booleanValue();
        } else if ("order".equals(currentName)) {
          reverse = "desc".equals(parser.text());
        } else if (ScriptService.SCRIPT_INLINE.match(currentName)) {
          script = parser.text();
          scriptType = ScriptService.ScriptType.INLINE;
        } else if (ScriptService.SCRIPT_ID.match(currentName)) {
          script = parser.text();
          scriptType = ScriptService.ScriptType.INDEXED;
        } else if (ScriptService.SCRIPT_FILE.match(currentName)) {
          script = parser.text();
          scriptType = ScriptService.ScriptType.FILE;
        } else if (ScriptService.SCRIPT_LANG.match(currentName)) {
          scriptLang = parser.text();
        } else if ("type".equals(currentName)) {
          type = parser.text();
        } else if ("mode".equals(currentName)) {
          sortMode = MultiValueMode.fromString(parser.text());
        } else if ("nested_path".equals(currentName) || "nestedPath".equals(currentName)) {
          if (nestedHelper == null) {
            nestedHelper = new NestedInnerQueryParseSupport(parser, context);
          }
          nestedHelper.setPath(parser.text());
        }
      }
    }

    if (script == null) {
      throw new SearchParseException(
          context, "_script sorting requires setting the script to sort by");
    }
    if (type == null) {
      throw new SearchParseException(
          context, "_script sorting requires setting the type of the script");
    }
    final SearchScript searchScript =
        context
            .scriptService()
            .search(
                context.lookup(),
                scriptLang,
                script,
                scriptType,
                ScriptContext.Standard.SEARCH,
                params);

    if (STRING_SORT_TYPE.equals(type)
        && (sortMode == MultiValueMode.SUM || sortMode == MultiValueMode.AVG)) {
      throw new SearchParseException(
          context, "type [string] doesn't support mode [" + sortMode + "]");
    }

    if (sortMode == null) {
      sortMode = reverse ? MultiValueMode.MAX : MultiValueMode.MIN;
    }

    // If nested_path is specified, then wrap the `fieldComparatorSource` in a
    // `NestedFieldComparatorSource`
    final Nested nested;
    if (nestedHelper != null && nestedHelper.getPath() != null) {
      FixedBitSetFilter rootDocumentsFilter =
          context.fixedBitSetFilterCache().getFixedBitSetFilter(NonNestedDocsFilter.INSTANCE);
      FixedBitSetFilter innerDocumentsFilter;
      if (nestedHelper.filterFound()) {
        innerDocumentsFilter =
            context.fixedBitSetFilterCache().getFixedBitSetFilter(nestedHelper.getInnerFilter());
      } else {
        innerDocumentsFilter =
            context
                .fixedBitSetFilterCache()
                .getFixedBitSetFilter(nestedHelper.getNestedObjectMapper().nestedTypeFilter());
      }
      nested = new Nested(rootDocumentsFilter, innerDocumentsFilter);
    } else {
      nested = null;
    }

    final IndexFieldData.XFieldComparatorSource fieldComparatorSource;
    switch (type) {
      case STRING_SORT_TYPE:
        fieldComparatorSource =
            new BytesRefFieldComparatorSource(null, null, sortMode, nested) {
              @Override
              protected SortedBinaryDocValues getValues(AtomicReaderContext context) {
                searchScript.setNextReader(context);
                final BinaryDocValues values =
                    new BinaryDocValues() {
                      final BytesRefBuilder spare = new BytesRefBuilder();

                      @Override
                      public BytesRef get(int docID) {
                        searchScript.setNextDocId(docID);
                        spare.copyChars(searchScript.run().toString());
                        return spare.get();
                      }
                    };
                return FieldData.singleton(values, null);
              }

              @Override
              protected void setScorer(Scorer scorer) {
                searchScript.setScorer(scorer);
              }
            };
        break;
      case NUMBER_SORT_TYPE:
        // TODO: should we rather sort missing values last?
        fieldComparatorSource =
            new DoubleValuesComparatorSource(null, Double.MAX_VALUE, sortMode, nested) {
              @Override
              protected SortedNumericDoubleValues getValues(AtomicReaderContext context) {
                searchScript.setNextReader(context);
                final NumericDoubleValues values =
                    new NumericDoubleValues() {
                      @Override
                      public double get(int docID) {
                        searchScript.setNextDocId(docID);
                        return searchScript.runAsDouble();
                      }
                    };
                return FieldData.singleton(values, null);
              }

              @Override
              protected void setScorer(Scorer scorer) {
                searchScript.setScorer(scorer);
              }
            };
        break;
      default:
        throw new SearchParseException(
            context, "custom script sort type [" + type + "] not supported");
    }

    return new SortField("_script", fieldComparatorSource, reverse);
  }
 @Override
 public int hashCode() {
   int result = fieldData.hashCode();
   result = 31 * result + multiValueMode.hashCode();
   return result;
 }
 @Override
 protected void doWriteTo(StreamOutput out) throws IOException {
   out.writeString(fieldName);
   out.writeBytesReference(functionBytes);
   multiValueMode.writeTo(out);
 }
 @Override
 protected DFB doReadFrom(StreamInput in) throws IOException {
   DFB decayFunctionBuilder = createFunctionBuilder(in.readString(), in.readBytesReference());
   decayFunctionBuilder.setMultiValueMode(MultiValueMode.readMultiValueModeFrom(in));
   return decayFunctionBuilder;
 }
  @Override
  public SortFieldAndFormat build(QueryShardContext context) throws IOException {
    final SearchScript searchScript =
        context.getSearchScript(script, ScriptContext.Standard.SEARCH, Collections.emptyMap());

    MultiValueMode valueMode = null;
    if (sortMode != null) {
      valueMode = MultiValueMode.fromString(sortMode.toString());
    }
    boolean reverse = (order == SortOrder.DESC);
    if (valueMode == null) {
      valueMode = reverse ? MultiValueMode.MAX : MultiValueMode.MIN;
    }

    final Nested nested = resolveNested(context, nestedPath, nestedFilter);
    final IndexFieldData.XFieldComparatorSource fieldComparatorSource;
    switch (type) {
      case STRING:
        fieldComparatorSource =
            new BytesRefFieldComparatorSource(null, null, valueMode, nested) {
              LeafSearchScript leafScript;

              @Override
              protected SortedBinaryDocValues getValues(LeafReaderContext context)
                  throws IOException {
                leafScript = searchScript.getLeafSearchScript(context);
                final BinaryDocValues values =
                    new BinaryDocValues() {
                      final BytesRefBuilder spare = new BytesRefBuilder();

                      @Override
                      public BytesRef get(int docID) {
                        leafScript.setDocument(docID);
                        spare.copyChars(leafScript.run().toString());
                        return spare.get();
                      }
                    };
                return FieldData.singleton(values, null);
              }

              @Override
              protected void setScorer(Scorer scorer) {
                leafScript.setScorer(scorer);
              }
            };
        break;
      case NUMBER:
        fieldComparatorSource =
            new DoubleValuesComparatorSource(null, Double.MAX_VALUE, valueMode, nested) {
              LeafSearchScript leafScript;

              @Override
              protected SortedNumericDoubleValues getValues(LeafReaderContext context)
                  throws IOException {
                leafScript = searchScript.getLeafSearchScript(context);
                final NumericDoubleValues values =
                    new NumericDoubleValues() {
                      @Override
                      public double get(int docID) {
                        leafScript.setDocument(docID);
                        return leafScript.runAsDouble();
                      }
                    };
                return FieldData.singleton(values, null);
              }

              @Override
              protected void setScorer(Scorer scorer) {
                leafScript.setScorer(scorer);
              }
            };
        break;
      default:
        throw new QueryShardException(
            context, "custom script sort type [" + type + "] not supported");
    }

    return new SortFieldAndFormat(
        new SortField("_script", fieldComparatorSource, reverse), DocValueFormat.RAW);
  }