Преглед на файлове

Merge pull request #15113 from cbuescher/highligh-builder-refactoring

Make HighlighterBuilder implement Writable, equals and hashCode
Christoph Büscher преди 10 години
родител
ревизия
6f2c36dcb7

+ 1 - 1
core/src/main/java/org/elasticsearch/search/builder/SearchSourceBuilder.java

@@ -408,7 +408,7 @@ public final class SearchSourceBuilder extends ToXContentToBytes implements Writ
         try {
             XContentBuilder builder = XContentFactory.jsonBuilder();
             builder.startObject();
-            highlightBuilder.innerXContent(builder, EMPTY_PARAMS);
+            highlightBuilder.innerXContent(builder);
             builder.endObject();
             this.highlightBuilder = builder.bytes();
             return this;

+ 509 - 0
core/src/main/java/org/elasticsearch/search/highlight/AbstractHighlighterBuilder.java

@@ -0,0 +1,509 @@
+/*
+ * 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.highlight;
+
+import org.apache.lucene.search.highlight.SimpleFragmenter;
+import org.apache.lucene.search.highlight.SimpleSpanFragmenter;
+import org.elasticsearch.common.io.stream.StreamInput;
+import org.elasticsearch.common.io.stream.StreamOutput;
+import org.elasticsearch.common.xcontent.XContentBuilder;
+import org.elasticsearch.index.query.QueryBuilder;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * This abstract class holds parameters shared by {@link HighlightBuilder} and {@link HighlightBuilder.Field}
+ * and provides the common setters, equality, hashCode calculation and common serialization
+ */
+public abstract class AbstractHighlighterBuilder<HB extends AbstractHighlighterBuilder> {
+
+    protected String[] preTags;
+
+    protected String[] postTags;
+
+    protected Integer fragmentSize;
+
+    protected Integer numOfFragments;
+
+    protected String highlighterType;
+
+    protected String fragmenter;
+
+    protected QueryBuilder highlightQuery;
+
+    protected String order;
+
+    protected Boolean highlightFilter;
+
+    protected Boolean forceSource;
+
+    protected Integer boundaryMaxScan;
+
+    protected char[] boundaryChars;
+
+    protected Integer noMatchSize;
+
+    protected Integer phraseLimit;
+
+    protected Map<String, Object> options;
+
+    protected Boolean requireFieldMatch;
+
+    /**
+     * Set the pre tags that will be used for highlighting.
+     */
+    @SuppressWarnings("unchecked")
+    public HB preTags(String... preTags) {
+        this.preTags = preTags;
+        return (HB) this;
+    }
+
+    /**
+     * @return the value set by {@link #preTags(String...)}
+     */
+    public String[] preTags() {
+        return this.preTags;
+    }
+
+    /**
+     * Set the post tags that will be used for highlighting.
+     */
+    @SuppressWarnings("unchecked")
+    public HB postTags(String... postTags) {
+        this.postTags = postTags;
+        return (HB) this;
+    }
+
+    /**
+     * @return the value set by {@link #postTags(String...)}
+     */
+    public String[] postTags() {
+        return this.postTags;
+    }
+
+    /**
+     * Set the fragment size in characters, defaults to {@link HighlighterParseElement#DEFAULT_FRAGMENT_CHAR_SIZE}
+     */
+    @SuppressWarnings("unchecked")
+    public HB fragmentSize(Integer fragmentSize) {
+        this.fragmentSize = fragmentSize;
+        return (HB) this;
+    }
+
+    /**
+     * @return the value set by {@link #fragmentSize(Integer)}
+     */
+    public Integer fragmentSize() {
+        return this.fragmentSize;
+    }
+
+    /**
+     * Set the number of fragments, defaults to {@link HighlighterParseElement#DEFAULT_NUMBER_OF_FRAGMENTS}
+     */
+    @SuppressWarnings("unchecked")
+    public HB numOfFragments(Integer numOfFragments) {
+        this.numOfFragments = numOfFragments;
+        return (HB) this;
+    }
+
+    /**
+     * @return the value set by {@link #numOfFragments(Integer)}
+     */
+    public Integer numOfFragments() {
+        return this.numOfFragments;
+    }
+
+    /**
+     * Set type of highlighter to use. Out of the box supported types
+     * are <tt>plain</tt>, <tt>fvh</tt> and <tt>postings</tt>.
+     * The default option selected is dependent on the mappings defined for your index.
+     * Details of the different highlighter types are covered in the reference guide.
+     */
+    @SuppressWarnings("unchecked")
+    public HB highlighterType(String highlighterType) {
+        this.highlighterType = highlighterType;
+        return (HB) this;
+    }
+
+    /**
+     * @return the value set by {@link #highlighterType(String)}
+     */
+    public String highlighterType() {
+        return this.highlighterType;
+    }
+
+    /**
+     * Sets what fragmenter to use to break up text that is eligible for highlighting.
+     * This option is only applicable when using the plain highlighterType <tt>highlighter</tt>.
+     * Permitted values are "simple" or "span" relating to {@link SimpleFragmenter} and
+     * {@link SimpleSpanFragmenter} implementations respectively with the default being "span"
+     */
+    @SuppressWarnings("unchecked")
+    public HB fragmenter(String fragmenter) {
+        this.fragmenter = fragmenter;
+        return (HB) this;
+    }
+
+    /**
+     * @return the value set by {@link #fragmenter(String)}
+     */
+    public String fragmenter() {
+        return this.fragmenter;
+    }
+
+    /**
+     * Sets a query to be used for highlighting instead of the search query.
+     */
+    @SuppressWarnings("unchecked")
+    public HB highlightQuery(QueryBuilder highlightQuery) {
+        this.highlightQuery = highlightQuery;
+        return (HB) this;
+    }
+
+    /**
+     * @return the value set by {@link #highlightQuery(QueryBuilder)}
+     */
+    public QueryBuilder highlightQuery() {
+        return this.highlightQuery;
+    }
+
+    /**
+     * The order of fragments per field. By default, ordered by the order in the
+     * highlighted text. Can be <tt>score</tt>, which then it will be ordered
+     * by score of the fragments.
+     */
+    @SuppressWarnings("unchecked")
+    public HB order(String order) {
+        this.order = order;
+        return (HB) this;
+    }
+
+    /**
+     * @return the value set by {@link #order(String)}
+     */
+    public String order() {
+        return this.order;
+    }
+
+    /**
+     * Set this to true when using the highlighterType <tt>fvh</tt>
+     * and you want to provide highlighting on filter clauses in your
+     * query. Default is <tt>false</tt>.
+     */
+    @SuppressWarnings("unchecked")
+    public HB highlightFilter(Boolean highlightFilter) {
+        this.highlightFilter = highlightFilter;
+        return (HB) this;
+    }
+
+    /**
+     * @return the value set by {@link #highlightFilter(Boolean)}
+     */
+    public Boolean highlightFilter() {
+        return this.highlightFilter;
+    }
+
+    /**
+     * When using the highlighterType <tt>fvh</tt> this setting
+     * controls how far to look for boundary characters, and defaults to 20.
+     */
+    @SuppressWarnings("unchecked")
+    public HB boundaryMaxScan(Integer boundaryMaxScan) {
+        this.boundaryMaxScan = boundaryMaxScan;
+        return (HB) this;
+    }
+
+    /**
+     * @return the value set by {@link #boundaryMaxScan(Integer)}
+     */
+    public Integer boundaryMaxScan() {
+        return this.boundaryMaxScan;
+    }
+
+    /**
+     * When using the highlighterType <tt>fvh</tt> this setting
+     * defines what constitutes a boundary for highlighting. It’s a single string with
+     * each boundary character defined in it. It defaults to .,!? \t\n
+     */
+    @SuppressWarnings("unchecked")
+    public HB boundaryChars(char[] boundaryChars) {
+        this.boundaryChars = boundaryChars;
+        return (HB) this;
+    }
+
+    /**
+     * @return the value set by {@link #boundaryChars(char[])}
+     */
+    public char[] boundaryChars() {
+        return this.boundaryChars;
+    }
+
+    /**
+     * Allows to set custom options for custom highlighters.
+     */
+    @SuppressWarnings("unchecked")
+    public HB options(Map<String, Object> options) {
+        this.options = options;
+        return (HB) this;
+    }
+
+    /**
+     * @return the value set by {@link #options(Map)}
+     */
+    public Map<String, Object> options() {
+        return this.options;
+    }
+
+    /**
+     * Set to true to cause a field to be highlighted only if a query matches that field.
+     * Default is false meaning that terms are highlighted on all requested fields regardless
+     * if the query matches specifically on them.
+     */
+    @SuppressWarnings("unchecked")
+    public HB requireFieldMatch(Boolean requireFieldMatch) {
+        this.requireFieldMatch = requireFieldMatch;
+        return (HB) this;
+    }
+
+    /**
+     * @return the value set by {@link #requireFieldMatch(Boolean)}
+     */
+    public Boolean requireFieldMatch() {
+        return this.requireFieldMatch;
+    }
+
+    /**
+     * Sets the size of the fragment to return from the beginning of the field if there are no matches to
+     * highlight and the field doesn't also define noMatchSize.
+     * @param noMatchSize integer to set or null to leave out of request.  default is null.
+     * @return this for chaining
+     */
+    @SuppressWarnings("unchecked")
+    public HB noMatchSize(Integer noMatchSize) {
+        this.noMatchSize = noMatchSize;
+        return (HB) this;
+    }
+
+    /**
+     * @return the value set by {@link #noMatchSize(Integer)}
+     */
+    public Integer noMatchSize() {
+        return this.noMatchSize;
+    }
+
+    /**
+     * Sets the maximum number of phrases the fvh will consider if the field doesn't also define phraseLimit.
+     * @param phraseLimit maximum number of phrases the fvh will consider
+     * @return this for chaining
+     */
+    @SuppressWarnings("unchecked")
+    public HB phraseLimit(Integer phraseLimit) {
+        this.phraseLimit = phraseLimit;
+        return (HB) this;
+    }
+
+    /**
+     * @return the value set by {@link #phraseLimit(Integer)}
+     */
+    public Integer phraseLimit() {
+        return this.noMatchSize;
+    }
+
+    /**
+     * Forces the highlighting to highlight fields based on the source even if fields are stored separately.
+     */
+    @SuppressWarnings("unchecked")
+    public HB forceSource(Boolean forceSource) {
+        this.forceSource = forceSource;
+        return (HB) this;
+    }
+
+    /**
+     * @return the value set by {@link #forceSource(Boolean)}
+     */
+    public Boolean forceSource() {
+        return this.forceSource;
+    }
+
+    void commonOptionsToXContent(XContentBuilder builder) throws IOException {
+        if (preTags != null) {
+            builder.array("pre_tags", preTags);
+        }
+        if (postTags != null) {
+            builder.array("post_tags", postTags);
+        }
+        if (fragmentSize != null) {
+            builder.field("fragment_size", fragmentSize);
+        }
+        if (numOfFragments != null) {
+            builder.field("number_of_fragments", numOfFragments);
+        }
+        if (highlighterType != null) {
+            builder.field("type", highlighterType);
+        }
+        if (fragmenter != null) {
+            builder.field("fragmenter", fragmenter);
+        }
+        if (highlightQuery != null) {
+            builder.field("highlight_query", highlightQuery);
+        }
+        if (order != null) {
+            builder.field("order", order);
+        }
+        if (highlightFilter != null) {
+            builder.field("highlight_filter", highlightFilter);
+        }
+        if (boundaryMaxScan != null) {
+            builder.field("boundary_max_scan", boundaryMaxScan);
+        }
+        if (boundaryChars != null) {
+            builder.field("boundary_chars", boundaryChars);
+        }
+        if (options != null && options.size() > 0) {
+            builder.field("options", options);
+        }
+        if (forceSource != null) {
+            builder.field("force_source", forceSource);
+        }
+        if (requireFieldMatch != null) {
+            builder.field("require_field_match", requireFieldMatch);
+        }
+        if (noMatchSize != null) {
+            builder.field("no_match_size", noMatchSize);
+        }
+        if (phraseLimit != null) {
+            builder.field("phrase_limit", phraseLimit);
+        }
+    }
+
+    @Override
+    public final int hashCode() {
+        return Objects.hash(getClass(), Arrays.hashCode(preTags), Arrays.hashCode(postTags), fragmentSize,
+                numOfFragments, highlighterType, fragmenter, highlightQuery, order, highlightFilter,
+                forceSource, boundaryMaxScan, Arrays.hashCode(boundaryChars), noMatchSize,
+                phraseLimit, options, requireFieldMatch, doHashCode());
+    }
+
+    /**
+     * internal hashCode calculation to overwrite for the implementing classes.
+     */
+    protected abstract int doHashCode();
+
+    @Override
+    public final boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null || getClass() != obj.getClass()) {
+            return false;
+        }
+        @SuppressWarnings("unchecked")
+        HB other = (HB) obj;
+        return Arrays.equals(preTags, other.preTags) &&
+               Arrays.equals(postTags, other.postTags) &&
+               Objects.equals(fragmentSize, other.fragmentSize) &&
+               Objects.equals(numOfFragments, other.numOfFragments) &&
+               Objects.equals(highlighterType, other.highlighterType) &&
+               Objects.equals(fragmenter, other.fragmenter) &&
+               Objects.equals(highlightQuery, other.highlightQuery) &&
+               Objects.equals(order, other.order) &&
+               Objects.equals(highlightFilter, other.highlightFilter) &&
+               Objects.equals(forceSource, other.forceSource) &&
+               Objects.equals(boundaryMaxScan, other.boundaryMaxScan) &&
+               Arrays.equals(boundaryChars, other.boundaryChars) &&
+               Objects.equals(noMatchSize, other.noMatchSize) &&
+               Objects.equals(phraseLimit, other.phraseLimit) &&
+               Objects.equals(options, other.options) &&
+               Objects.equals(requireFieldMatch, other.requireFieldMatch) &&
+               doEquals(other);
+    }
+
+    /**
+     * internal equals to overwrite for the implementing classes.
+     */
+    protected abstract boolean doEquals(HB other);
+
+    /**
+     * read common parameters from {@link StreamInput}
+     */
+    @SuppressWarnings("unchecked")
+    protected HB readOptionsFrom(StreamInput in) throws IOException {
+        preTags(in.readOptionalStringArray());
+        postTags(in.readOptionalStringArray());
+        fragmentSize(in.readOptionalVInt());
+        numOfFragments(in.readOptionalVInt());
+        highlighterType(in.readOptionalString());
+        fragmenter(in.readOptionalString());
+        if (in.readBoolean()) {
+            highlightQuery(in.readQuery());
+        }
+        order(in.readOptionalString());
+        highlightFilter(in.readOptionalBoolean());
+        forceSource(in.readOptionalBoolean());
+        boundaryMaxScan(in.readOptionalVInt());
+        if (in.readBoolean()) {
+            boundaryChars(in.readString().toCharArray());
+        }
+        noMatchSize(in.readOptionalVInt());
+        phraseLimit(in.readOptionalVInt());
+        if (in.readBoolean()) {
+            options(in.readMap());
+        }
+        requireFieldMatch(in.readOptionalBoolean());
+        return (HB) this;
+    }
+
+    /**
+     * write common parameters to {@link StreamOutput}
+     */
+    protected void writeOptionsTo(StreamOutput out) throws IOException {
+        out.writeOptionalStringArray(preTags);
+        out.writeOptionalStringArray(postTags);
+        out.writeOptionalVInt(fragmentSize);
+        out.writeOptionalVInt(numOfFragments);
+        out.writeOptionalString(highlighterType);
+        out.writeOptionalString(fragmenter);
+        boolean hasQuery = highlightQuery != null;
+        out.writeBoolean(hasQuery);
+        if (hasQuery) {
+            out.writeQuery(highlightQuery);
+        }
+        out.writeOptionalString(order);
+        out.writeOptionalBoolean(highlightFilter);
+        out.writeOptionalBoolean(forceSource);
+        out.writeOptionalVInt(boundaryMaxScan);
+        boolean hasBounaryChars = boundaryChars != null;
+        out.writeBoolean(hasBounaryChars);
+        if (hasBounaryChars) {
+            out.writeString(String.valueOf(boundaryChars));
+        }
+        out.writeOptionalVInt(noMatchSize);
+        out.writeOptionalVInt(phraseLimit);
+        boolean hasOptions = options != null;
+        out.writeBoolean(hasOptions);
+        if (hasOptions) {
+            out.writeMap(options);
+        }
+        out.writeOptionalBoolean(requireFieldMatch);
+    }
+}

+ 136 - 458
core/src/main/java/org/elasticsearch/search/highlight/HighlightBuilder.java

@@ -19,16 +19,19 @@
 
 package org.elasticsearch.search.highlight;
 
-import org.apache.lucene.search.highlight.SimpleFragmenter;
-import org.apache.lucene.search.highlight.SimpleSpanFragmenter;
+import org.elasticsearch.ExceptionsHelper;
+import org.elasticsearch.common.io.stream.StreamInput;
+import org.elasticsearch.common.io.stream.StreamOutput;
+import org.elasticsearch.common.io.stream.Writeable;
 import org.elasticsearch.common.xcontent.ToXContent;
 import org.elasticsearch.common.xcontent.XContentBuilder;
-import org.elasticsearch.index.query.QueryBuilder;
+import org.elasticsearch.common.xcontent.XContentFactory;
 
 import java.io.IOException;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.List;
-import java.util.Map;
+import java.util.Objects;
 
 /**
  * A builder for search highlighting. Settings can control how large fields
@@ -36,46 +39,14 @@ import java.util.Map;
  *
  * @see org.elasticsearch.search.builder.SearchSourceBuilder#highlight()
  */
-public class HighlightBuilder implements ToXContent {
+public class HighlightBuilder extends AbstractHighlighterBuilder<HighlightBuilder> implements Writeable<HighlightBuilder>, ToXContent  {
 
-    private List<Field> fields;
+    public static final HighlightBuilder PROTOTYPE = new HighlightBuilder();
 
-    private String tagsSchema;
-
-    private Boolean highlightFilter;
-
-    private Integer fragmentSize;
-
-    private Integer numOfFragments;
-
-    private String[] preTags;
-
-    private String[] postTags;
-
-    private String order;
+    private final List<Field> fields = new ArrayList<>();
 
     private String encoder;
 
-    private Boolean requireFieldMatch;
-
-    private Integer boundaryMaxScan;
-
-    private char[] boundaryChars;
-
-    private String highlighterType;
-
-    private String fragmenter;
-
-    private QueryBuilder highlightQuery;
-
-    private Integer noMatchSize;
-
-    private Integer phraseLimit;
-
-    private Map<String, Object> options;
-
-    private Boolean forceSource;
-
     private boolean useExplicitFieldOrder = false;
 
     /**
@@ -85,14 +56,9 @@ public class HighlightBuilder implements ToXContent {
      * @param name The field to highlight
      */
     public HighlightBuilder field(String name) {
-        if (fields == null) {
-            fields = new ArrayList<>();
-        }
-        fields.add(new Field(name));
-        return this;
+        return field(new Field(name));
     }
 
-
     /**
      * Adds a field to be highlighted with a provided fragment size (in characters), and
      * default number of fragments of 5.
@@ -101,11 +67,7 @@ public class HighlightBuilder implements ToXContent {
      * @param fragmentSize The size of a fragment in characters
      */
     public HighlightBuilder field(String name, int fragmentSize) {
-        if (fields == null) {
-            fields = new ArrayList<>();
-        }
-        fields.add(new Field(name).fragmentSize(fragmentSize));
-        return this;
+        return field(new Field(name).fragmentSize(fragmentSize));
     }
 
 
@@ -118,14 +80,9 @@ public class HighlightBuilder implements ToXContent {
      * @param numberOfFragments The (maximum) number of fragments
      */
     public HighlightBuilder field(String name, int fragmentSize, int numberOfFragments) {
-        if (fields == null) {
-            fields = new ArrayList<>();
-        }
-        fields.add(new Field(name).fragmentSize(fragmentSize).numOfFragments(numberOfFragments));
-        return this;
+        return field(new Field(name).fragmentSize(fragmentSize).numOfFragments(numberOfFragments));
     }
 
-
     /**
      * Adds a field to be highlighted with a provided fragment size (in characters), and
      * a provided (maximum) number of fragments.
@@ -136,56 +93,38 @@ public class HighlightBuilder implements ToXContent {
      * @param fragmentOffset    The offset from the start of the fragment to the start of the highlight
      */
     public HighlightBuilder field(String name, int fragmentSize, int numberOfFragments, int fragmentOffset) {
-        if (fields == null) {
-            fields = new ArrayList<>();
-        }
-        fields.add(new Field(name).fragmentSize(fragmentSize).numOfFragments(numberOfFragments)
+        return field(new Field(name).fragmentSize(fragmentSize).numOfFragments(numberOfFragments)
                 .fragmentOffset(fragmentOffset));
-        return this;
     }
 
     public HighlightBuilder field(Field field) {
-        if (fields == null) {
-            fields = new ArrayList<>();
-        }
         fields.add(field);
         return this;
     }
 
+    public List<Field> fields() {
+        return this.fields;
+    }
+
     /**
-     * Set a tag scheme that encapsulates a built in pre and post tags. The allows schemes
+     * Set a tag scheme that encapsulates a built in pre and post tags. The allowed schemes
      * are <tt>styled</tt> and <tt>default</tt>.
      *
      * @param schemaName The tag scheme name
      */
     public HighlightBuilder tagsSchema(String schemaName) {
-        this.tagsSchema = schemaName;
-        return this;
-    }
-
-    /**
-     * Set this to true when using the highlighterType <tt>fvh</tt>
-     * and you want to provide highlighting on filter clauses in your
-     * query. Default is <tt>false</tt>.
-     */
-    public HighlightBuilder highlightFilter(boolean highlightFilter) {
-        this.highlightFilter = highlightFilter;
-        return this;
-    }
-
-    /**
-     * Sets the size of a fragment in characters (defaults to 100)
-     */
-    public HighlightBuilder fragmentSize(Integer fragmentSize) {
-        this.fragmentSize = fragmentSize;
-        return this;
-    }
-
-    /**
-     * Sets the maximum number of fragments returned
-     */
-    public HighlightBuilder numOfFragments(Integer numOfFragments) {
-        this.numOfFragments = numOfFragments;
+        switch (schemaName) {
+        case "default":
+            preTags(HighlighterParseElement.DEFAULT_PRE_TAGS);
+            postTags(HighlighterParseElement.DEFAULT_POST_TAGS);
+            break;
+        case "styled":
+            preTags(HighlighterParseElement.STYLED_PRE_TAG);
+            postTags(HighlighterParseElement.STYLED_POST_TAGS);
+            break;
+        default:
+            throw new IllegalArgumentException("Unknown tag schema ["+ schemaName +"]");
+        }
         return this;
     }
 
@@ -201,201 +140,44 @@ public class HighlightBuilder implements ToXContent {
     }
 
     /**
-     * Explicitly set the pre tags that will be used for highlighting.
+     * Getter for {@link #encoder(String)}
      */
-    public HighlightBuilder preTags(String... preTags) {
-        this.preTags = preTags;
-        return this;
-    }
-
-    /**
-     * Explicitly set the post tags that will be used for highlighting.
-     */
-    public HighlightBuilder postTags(String... postTags) {
-        this.postTags = postTags;
-        return this;
-    }
-
-    /**
-     * The order of fragments per field. By default, ordered by the order in the
-     * highlighted text. Can be <tt>score</tt>, which then it will be ordered
-     * by score of the fragments.
-     */
-    public HighlightBuilder order(String order) {
-        this.order = order;
-        return this;
+    public String encoder() {
+        return this.encoder;
     }
 
     /**
-     * Set to true to cause a field to be highlighted only if a query matches that field.
-     * Default is false meaning that terms are highlighted on all requested fields regardless
-     * if the query matches specifically on them.
-     */
-    public HighlightBuilder requireFieldMatch(boolean requireFieldMatch) {
-        this.requireFieldMatch = requireFieldMatch;
-        return this;
-    }
-
-    /**
-     * When using the highlighterType <tt>fvh</tt> this setting
-     * controls how far to look for boundary characters, and defaults to 20.
-     */
-    public HighlightBuilder boundaryMaxScan(Integer boundaryMaxScan) {
-        this.boundaryMaxScan = boundaryMaxScan;
-        return this;
-    }
-
-    /**
-     * When using the highlighterType <tt>fvh</tt> this setting
-     * defines what constitutes a boundary for highlighting. It’s a single string with
-     * each boundary character defined in it. It defaults to .,!? \t\n
-     */
-    public HighlightBuilder boundaryChars(char[] boundaryChars) {
-        this.boundaryChars = boundaryChars;
-        return this;
-    }
-
-    /**
-     * Set type of highlighter to use. Out of the box supported types
-     * are <tt>plain</tt>, <tt>fvh</tt> and <tt>postings</tt>.
-     * The default option selected is dependent on the mappings defined for your index.
-     * Details of the different highlighter types are covered in the reference guide.
-     */
-    public HighlightBuilder highlighterType(String highlighterType) {
-        this.highlighterType = highlighterType;
-        return this;
-    }
-
-    /**
-     * Sets what fragmenter to use to break up text that is eligible for highlighting.
-     * This option is only applicable when using the plain highlighterType <tt>highlighter</tt>.
-     * Permitted values are "simple" or "span" relating to {@link SimpleFragmenter} and
-     * {@link SimpleSpanFragmenter} implementations respectively with the default being "span"
-     */
-    public HighlightBuilder fragmenter(String fragmenter) {
-        this.fragmenter = fragmenter;
-        return this;
-    }
-
-    /**
-     * Sets a query to be used for highlighting all fields instead of the search query.
-     */
-    public HighlightBuilder highlightQuery(QueryBuilder highlightQuery) {
-        this.highlightQuery = highlightQuery;
-        return this;
-    }
-
-    /**
-     * Sets the size of the fragment to return from the beginning of the field if there are no matches to
-     * highlight and the field doesn't also define noMatchSize.
-     * @param noMatchSize integer to set or null to leave out of request.  default is null.
-     * @return this for chaining
-     */
-    public HighlightBuilder noMatchSize(Integer noMatchSize) {
-        this.noMatchSize = noMatchSize;
-        return this;
-    }
-
-    /**
-     * Sets the maximum number of phrases the fvh will consider if the field doesn't also define phraseLimit.
-     * @param phraseLimit maximum number of phrases the fvh will consider
+     * Send the fields to be highlighted using a syntax that is specific about the order in which they should be highlighted.
      * @return this for chaining
      */
-    public HighlightBuilder phraseLimit(Integer phraseLimit) {
-        this.phraseLimit = phraseLimit;
-        return this;
-    }
-
-    /**
-     * Allows to set custom options for custom highlighters.
-     */
-    public HighlightBuilder options(Map<String, Object> options) {
-        this.options = options;
-        return this;
-    }
-
-    /**
-     * Forces the highlighting to highlight fields based on the source even if fields are stored separately.
-     */
-    public HighlightBuilder forceSource(boolean forceSource) {
-        this.forceSource = forceSource;
+    public HighlightBuilder useExplicitFieldOrder(boolean useExplicitFieldOrder) {
+        this.useExplicitFieldOrder = useExplicitFieldOrder;
         return this;
     }
 
     /**
-     * Send the fields to be highlighted using a syntax that is specific about the order in which they should be highlighted.
-     * @return this for chaining
+     * Gets value set with {@link #useExplicitFieldOrder(boolean)}
      */
-    public HighlightBuilder useExplicitFieldOrder(boolean useExplicitFieldOrder) {
-        this.useExplicitFieldOrder = useExplicitFieldOrder;
-        return this;
+    public Boolean useExplicitFieldOrder() {
+        return this.useExplicitFieldOrder;
     }
 
     @Override
     public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
         builder.startObject("highlight");
-        innerXContent(builder, params);
+        innerXContent(builder);
         builder.endObject();
         return builder;
     }
 
-
-    public void innerXContent(XContentBuilder builder, Params params) throws IOException {
-        if (tagsSchema != null) {
-            builder.field("tags_schema", tagsSchema);
-        }
-        if (preTags != null) {
-            builder.array("pre_tags", preTags);
-        }
-        if (postTags != null) {
-            builder.array("post_tags", postTags);
-        }
-        if (order != null) {
-            builder.field("order", order);
-        }
-        if (highlightFilter != null) {
-            builder.field("highlight_filter", highlightFilter);
-        }
-        if (fragmentSize != null) {
-            builder.field("fragment_size", fragmentSize);
-        }
-        if (numOfFragments != null) {
-            builder.field("number_of_fragments", numOfFragments);
-        }
+    public void innerXContent(XContentBuilder builder) throws IOException {
+        // first write common options
+        commonOptionsToXContent(builder);
+        // special options for top-level highlighter
         if (encoder != null) {
             builder.field("encoder", encoder);
         }
-        if (requireFieldMatch != null) {
-            builder.field("require_field_match", requireFieldMatch);
-        }
-        if (boundaryMaxScan != null) {
-            builder.field("boundary_max_scan", boundaryMaxScan);
-        }
-        if (boundaryChars != null) {
-            builder.field("boundary_chars", boundaryChars);
-        }
-        if (highlighterType != null) {
-            builder.field("type", highlighterType);
-        }
-        if (fragmenter != null) {
-            builder.field("fragmenter", fragmenter);
-        }
-        if (highlightQuery != null) {
-            builder.field("highlight_query", highlightQuery);
-        }
-        if (noMatchSize != null) {
-            builder.field("no_match_size", noMatchSize);
-        }
-        if (phraseLimit != null) {
-            builder.field("phrase_limit", phraseLimit);
-        }
-        if (options != null && options.size() > 0) {
-            builder.field("options", options);
-        }
-        if (forceSource != null) {
-            builder.field("force_source", forceSource);
-        }
-        if (fields != null) {
+        if (fields.size() > 0) {
             if (useExplicitFieldOrder) {
                 builder.startArray("fields");
             } else {
@@ -405,63 +187,7 @@ public class HighlightBuilder implements ToXContent {
                 if (useExplicitFieldOrder) {
                     builder.startObject();
                 }
-                builder.startObject(field.name());
-                if (field.preTags != null) {
-                    builder.field("pre_tags", field.preTags);
-                }
-                if (field.postTags != null) {
-                    builder.field("post_tags", field.postTags);
-                }
-                if (field.fragmentSize != -1) {
-                    builder.field("fragment_size", field.fragmentSize);
-                }
-                if (field.numOfFragments != -1) {
-                    builder.field("number_of_fragments", field.numOfFragments);
-                }
-                if (field.fragmentOffset != -1) {
-                    builder.field("fragment_offset", field.fragmentOffset);
-                }
-                if (field.highlightFilter != null) {
-                    builder.field("highlight_filter", field.highlightFilter);
-                }
-                if (field.order != null) {
-                    builder.field("order", field.order);
-                }
-                if (field.requireFieldMatch != null) {
-                    builder.field("require_field_match", field.requireFieldMatch);
-                }
-                if (field.boundaryMaxScan != -1) {
-                    builder.field("boundary_max_scan", field.boundaryMaxScan);
-                }
-                if (field.boundaryChars != null) {
-                    builder.field("boundary_chars", field.boundaryChars);
-                }
-                if (field.highlighterType != null) {
-                    builder.field("type", field.highlighterType);
-                }
-                if (field.fragmenter != null) {
-                    builder.field("fragmenter", field.fragmenter);
-                }
-                if (field.highlightQuery != null) {
-                    builder.field("highlight_query", field.highlightQuery);
-                }
-                if (field.noMatchSize != null) {
-                    builder.field("no_match_size", field.noMatchSize);
-                }
-                if (field.matchedFields != null) {
-                    builder.field("matched_fields", field.matchedFields);
-                }
-                if (field.phraseLimit != null) {
-                    builder.field("phrase_limit", field.phraseLimit);
-                }
-                if (field.options != null && field.options.size() > 0) {
-                    builder.field("options", field.options);
-                }
-                if (field.forceSource != null) {
-                    builder.field("force_source", field.forceSource);
-                }
-
-                builder.endObject();
+                field.innerXContent(builder);
                 if (useExplicitFieldOrder) {
                     builder.endObject();
                 }
@@ -474,144 +200,73 @@ public class HighlightBuilder implements ToXContent {
         }
     }
 
-    public static class Field {
-        final String name;
-        String[] preTags;
-        String[] postTags;
-        int fragmentSize = -1;
-        int fragmentOffset = -1;
-        int numOfFragments = -1;
-        Boolean highlightFilter;
-        String order;
-        Boolean requireFieldMatch;
-        int boundaryMaxScan = -1;
-        char[] boundaryChars;
-        String highlighterType;
-        String fragmenter;
-        QueryBuilder highlightQuery;
-        Integer noMatchSize;
-        String[] matchedFields;
-        Integer phraseLimit;
-        Map<String, Object> options;
-        Boolean forceSource;
-
-        public Field(String name) {
-            this.name = name;
-        }
-
-        public String name() {
-            return name;
-        }
-
-        /**
-         * Explicitly set the pre tags for this field that will be used for highlighting.
-         * This overrides global settings set by {@link HighlightBuilder#preTags(String...)}.
-         */
-        public Field preTags(String... preTags) {
-            this.preTags = preTags;
-            return this;
-        }
-
-        /**
-         * Explicitly set the post tags for this field that will be used for highlighting.
-         * This overrides global settings set by {@link HighlightBuilder#postTags(String...)}.
-         */
-        public Field postTags(String... postTags) {
-            this.postTags = postTags;
-            return this;
-        }
-
-        public Field fragmentSize(int fragmentSize) {
-            this.fragmentSize = fragmentSize;
-            return this;
-        }
-
-        public Field fragmentOffset(int fragmentOffset) {
-            this.fragmentOffset = fragmentOffset;
-            return this;
+    @Override
+    public final String toString() {
+        try {
+            XContentBuilder builder = XContentFactory.jsonBuilder();
+            builder.prettyPrint();
+            toXContent(builder, ToXContent.EMPTY_PARAMS);
+            return builder.string();
+        } catch (Exception e) {
+            return "{ \"error\" : \"" + ExceptionsHelper.detailedMessage(e) + "\"}";
         }
+    }
 
-        public Field numOfFragments(int numOfFragments) {
-            this.numOfFragments = numOfFragments;
-            return this;
-        }
+    @Override
+    protected int doHashCode() {
+        return Objects.hash(encoder, useExplicitFieldOrder, fields);
+    }
 
-        public Field highlightFilter(boolean highlightFilter) {
-            this.highlightFilter = highlightFilter;
-            return this;
-        }
+    @Override
+    protected boolean doEquals(HighlightBuilder other) {
+        return Objects.equals(encoder, other.encoder) &&
+                Objects.equals(useExplicitFieldOrder, other.useExplicitFieldOrder) &&
+                Objects.equals(fields, other.fields);
+    }
 
-        /**
-         * The order of fragments per field. By default, ordered by the order in the
-         * highlighted text. Can be <tt>score</tt>, which then it will be ordered
-         * by score of the fragments.
-         * This overrides global settings set by {@link HighlightBuilder#order(String)}.
-         */
-        public Field order(String order) {
-            this.order = order;
-            return this;
-        }
+    @Override
+    public HighlightBuilder readFrom(StreamInput in) throws IOException {
+        HighlightBuilder highlightBuilder = new HighlightBuilder();
+        highlightBuilder.readOptionsFrom(in)
+                .encoder(in.readOptionalString())
+                .useExplicitFieldOrder(in.readBoolean());
+        int fields = in.readVInt();
+        for (int i = 0; i < fields; i++) {
+            highlightBuilder.field(Field.PROTOTYPE.readFrom(in));
+        }
+        return highlightBuilder;
+    }
 
-        public Field requireFieldMatch(boolean requireFieldMatch) {
-            this.requireFieldMatch = requireFieldMatch;
-            return this;
+    @Override
+    public void writeTo(StreamOutput out) throws IOException {
+        writeOptionsTo(out);
+        out.writeOptionalString(encoder);
+        out.writeBoolean(useExplicitFieldOrder);
+        out.writeVInt(fields.size());
+        for (int i = 0; i < fields.size(); i++) {
+            fields.get(i).writeTo(out);
         }
+    }
 
-        public Field boundaryMaxScan(int boundaryMaxScan) {
-            this.boundaryMaxScan = boundaryMaxScan;
-            return this;
-        }
+    public static class Field extends AbstractHighlighterBuilder<Field> implements Writeable<Field> {
+        static final Field PROTOTYPE = new Field("_na_");
 
-        public Field boundaryChars(char[] boundaryChars) {
-            this.boundaryChars = boundaryChars;
-            return this;
-        }
+        private final String name;
 
-        /**
-         * Set type of highlighter to use. Out of the box supported types
-         * are <tt>plain</tt>, <tt>fvh</tt> and <tt>postings</tt>.
-         * This overrides global settings set by {@link HighlightBuilder#highlighterType(String)}.
-         */
-        public Field highlighterType(String highlighterType) {
-            this.highlighterType = highlighterType;
-            return this;
-        }
+        int fragmentOffset = -1;
 
-        /**
-         * Sets what fragmenter to use to break up text that is eligible for highlighting.
-         * This option is only applicable when using plain / normal highlighter.
-         * This overrides global settings set by {@link HighlightBuilder#fragmenter(String)}.
-         */
-        public Field fragmenter(String fragmenter) {
-            this.fragmenter = fragmenter;
-            return this;
-        }
+        String[] matchedFields;
 
-        /**
-         * Sets a query to use for highlighting this field instead of the search query.
-         */
-        public Field highlightQuery(QueryBuilder highlightQuery) {
-            this.highlightQuery = highlightQuery;
-            return this;
+        public Field(String name) {
+            this.name = name;
         }
 
-        /**
-         * Sets the size of the fragment to return from the beginning of the field if there are no matches to
-         * highlight.
-         * @param noMatchSize integer to set or null to leave out of request.  default is null.
-         * @return this for chaining
-         */
-        public Field noMatchSize(Integer noMatchSize) {
-            this.noMatchSize = noMatchSize;
-            return this;
+        public String name() {
+            return name;
         }
 
-        /**
-         * Allows to set custom options for custom highlighters.
-         * This overrides global settings set by {@link HighlightBuilder#options(Map)}.
-         */
-        public Field options(Map<String, Object> options) {
-            this.options = options;
+        public Field fragmentOffset(int fragmentOffset) {
+            this.fragmentOffset = fragmentOffset;
             return this;
         }
 
@@ -625,24 +280,47 @@ public class HighlightBuilder implements ToXContent {
             return this;
         }
 
-        /**
-         * Sets the maximum number of phrases the fvh will consider.
-         * @param phraseLimit maximum number of phrases the fvh will consider
-         * @return this for chaining
-         */
-        public Field phraseLimit(Integer phraseLimit) {
-            this.phraseLimit = phraseLimit;
-            return this;
+        public void innerXContent(XContentBuilder builder) throws IOException {
+            builder.startObject(name);
+            // write common options
+            commonOptionsToXContent(builder);
+            // write special field-highlighter options
+            if (fragmentOffset != -1) {
+                builder.field("fragment_offset", fragmentOffset);
+            }
+            if (matchedFields != null) {
+                builder.field("matched_fields", matchedFields);
+            }
+            builder.endObject();
         }
 
+        @Override
+        protected int doHashCode() {
+            return Objects.hash(name, fragmentOffset, Arrays.hashCode(matchedFields));
+        }
 
-        /**
-         * Forces the highlighting to highlight this field based on the source even if this field is stored separately.
-         */
-        public Field forceSource(boolean forceSource) {
-            this.forceSource = forceSource;
-            return this;
+        @Override
+        protected boolean doEquals(Field other) {
+            return Objects.equals(name, other.name) &&
+                    Objects.equals(fragmentOffset, other.fragmentOffset) &&
+                    Arrays.equals(matchedFields, other.matchedFields);
         }
 
+        @Override
+        public Field readFrom(StreamInput in) throws IOException {
+            Field field = new Field(in.readString());
+            field.fragmentOffset(in.readVInt());
+            field.matchedFields(in.readOptionalStringArray());
+            field.readOptionsFrom(in);
+            return field;
+        }
+
+        @Override
+        public void writeTo(StreamOutput out) throws IOException {
+            out.writeString(name);
+            out.writeVInt(fragmentOffset);
+            out.writeOptionalStringArray(matchedFields);
+            writeOptionsTo(out);
+        }
     }
 }

+ 31 - 9
core/src/main/java/org/elasticsearch/search/highlight/HighlighterParseElement.java

@@ -52,16 +52,38 @@ import java.util.Set;
  */
 public class HighlighterParseElement implements SearchParseElement {
 
-    private static final String[] DEFAULT_PRE_TAGS = new String[]{"<em>"};
-    private static final String[] DEFAULT_POST_TAGS = new String[]{"</em>"};
-
-    private static final String[] STYLED_PRE_TAG = {
+    /** default for whether to highlight fields based on the source even if stored separately */
+    public static final boolean DEFAULT_FORCE_SOURCE = false;
+    /** default for whether a field should be highlighted only if a query matches that field */
+    public static final boolean DEFAULT_REQUIRE_FIELD_MATCH = true;
+    /** default for whether <tt>fvh</tt> should provide highlighting on filter clauses */
+    public static final boolean DEFAULT_HIGHLIGHT_FILTER = false;
+    /** default for highlight fragments being ordered by score */ 
+    public static final boolean DEFAULT_SCORE_ORDERED = false;
+    /** the default encoder setting */
+    public static final String DEFAULT_ENCODER = "default";
+    /** default for the maximum number of phrases the fvh will consider */
+    public static final int DEFAULT_PHRASE_LIMIT = 256;
+    /** default for fragment size when there are no matches */
+    public static final int DEFAULT_NO_MATCH_SIZE = 0;
+    /** the default number of fragments for highlighting */
+    public static final int DEFAULT_NUMBER_OF_FRAGMENTS = 5;
+    /** the default number of fragments size in characters */
+    public static final int DEFAULT_FRAGMENT_CHAR_SIZE = 100;
+    /** the default opening tag  */
+    public static final String[] DEFAULT_PRE_TAGS = new String[]{"<em>"};
+    /** the default closing tag  */
+    public static final String[] DEFAULT_POST_TAGS = new String[]{"</em>"};
+    
+    /** the default opening tags when <tt>tag_schema = "styled"</tt>  */
+    public static final String[] STYLED_PRE_TAG = {
             "<em class=\"hlt1\">", "<em class=\"hlt2\">", "<em class=\"hlt3\">",
             "<em class=\"hlt4\">", "<em class=\"hlt5\">", "<em class=\"hlt6\">",
             "<em class=\"hlt7\">", "<em class=\"hlt8\">", "<em class=\"hlt9\">",
             "<em class=\"hlt10\">"
     };
-    private static final String[] STYLED_POST_TAGS = {"</em>"};
+    /** the default closing tags when <tt>tag_schema = "styled"</tt>  */
+    public static final String[] STYLED_POST_TAGS = {"</em>"};
 
     @Override
     public void parse(XContentParser parser, SearchContext context) throws Exception {
@@ -78,11 +100,11 @@ public class HighlighterParseElement implements SearchParseElement {
         final List<Tuple<String, SearchContextHighlight.FieldOptions.Builder>> fieldsOptions = new ArrayList<>();
 
         final SearchContextHighlight.FieldOptions.Builder globalOptionsBuilder = new SearchContextHighlight.FieldOptions.Builder()
-                .preTags(DEFAULT_PRE_TAGS).postTags(DEFAULT_POST_TAGS).scoreOrdered(false).highlightFilter(false)
-                .requireFieldMatch(true).forceSource(false).fragmentCharSize(100).numberOfFragments(5)
-                .encoder("default").boundaryMaxScan(SimpleBoundaryScanner.DEFAULT_MAX_SCAN)
+                .preTags(DEFAULT_PRE_TAGS).postTags(DEFAULT_POST_TAGS).scoreOrdered(DEFAULT_SCORE_ORDERED).highlightFilter(DEFAULT_HIGHLIGHT_FILTER)
+                .requireFieldMatch(DEFAULT_REQUIRE_FIELD_MATCH).forceSource(DEFAULT_FORCE_SOURCE).fragmentCharSize(DEFAULT_FRAGMENT_CHAR_SIZE).numberOfFragments(DEFAULT_NUMBER_OF_FRAGMENTS)
+                .encoder(DEFAULT_ENCODER).boundaryMaxScan(SimpleBoundaryScanner.DEFAULT_MAX_SCAN)
                 .boundaryChars(SimpleBoundaryScanner.DEFAULT_BOUNDARY_CHARS)
-                .noMatchSize(0).phraseLimit(256);
+                .noMatchSize(DEFAULT_NO_MATCH_SIZE).phraseLimit(DEFAULT_PHRASE_LIMIT);
 
         while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
             if (token == XContentParser.Token.FIELD_NAME) {

+ 332 - 0
core/src/test/java/org/elasticsearch/search/highlight/HighlightBuilderTests.java

@@ -0,0 +1,332 @@
+/*
+ * 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.highlight;
+
+import org.elasticsearch.common.io.stream.BytesStreamOutput;
+import org.elasticsearch.common.io.stream.NamedWriteableAwareStreamInput;
+import org.elasticsearch.common.io.stream.NamedWriteableRegistry;
+import org.elasticsearch.common.io.stream.StreamInput;
+import org.elasticsearch.index.query.IdsQueryBuilder;
+import org.elasticsearch.index.query.MatchAllQueryBuilder;
+import org.elasticsearch.index.query.QueryBuilder;
+import org.elasticsearch.index.query.TermQueryBuilder;
+import org.elasticsearch.search.highlight.HighlightBuilder.Field;
+import org.elasticsearch.test.ESTestCase;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.not;
+
+public class HighlightBuilderTests extends ESTestCase {
+
+    private static final int NUMBER_OF_TESTBUILDERS = 20;
+    private static NamedWriteableRegistry namedWriteableRegistry;
+
+    /**
+     * setup for the whole base test class
+     */
+    @BeforeClass
+    public static void init() {
+        if (namedWriteableRegistry == null) {
+            namedWriteableRegistry = new NamedWriteableRegistry();
+            namedWriteableRegistry.registerPrototype(QueryBuilder.class, new MatchAllQueryBuilder());
+            namedWriteableRegistry.registerPrototype(QueryBuilder.class, new IdsQueryBuilder());
+            namedWriteableRegistry.registerPrototype(QueryBuilder.class, new TermQueryBuilder("field", "value"));
+        }
+    }
+
+    @AfterClass
+    public static void afterClass() throws Exception {
+        namedWriteableRegistry = null;
+    }
+
+    /**
+     * Test serialization and deserialization of the highlighter builder
+     */
+    public void testSerialization() throws IOException {
+        for (int runs = 0; runs < NUMBER_OF_TESTBUILDERS; runs++) {
+            HighlightBuilder original = randomHighlighterBuilder();
+            HighlightBuilder deserialized = serializedCopy(original);
+            assertEquals(deserialized, original);
+            assertEquals(deserialized.hashCode(), original.hashCode());
+            assertNotSame(deserialized, original);
+        }
+    }
+
+    /**
+     * Test equality and hashCode properties
+     */
+    public void testEqualsAndHashcode() throws IOException {
+        for (int runs = 0; runs < NUMBER_OF_TESTBUILDERS; runs++) {
+            HighlightBuilder firstBuilder = randomHighlighterBuilder();
+            assertFalse("highlighter is equal to null", firstBuilder.equals(null));
+            assertFalse("highlighter is equal to incompatible type", firstBuilder.equals(""));
+            assertTrue("highlighter is not equal to self", firstBuilder.equals(firstBuilder));
+            assertThat("same highlighter's hashcode returns different values if called multiple times", firstBuilder.hashCode(),
+                    equalTo(firstBuilder.hashCode()));
+            assertThat("different highlighters should not be equal", mutate(firstBuilder), not(equalTo(firstBuilder)));
+
+            HighlightBuilder secondBuilder = serializedCopy(firstBuilder);
+            assertTrue("highlighter is not equal to self", secondBuilder.equals(secondBuilder));
+            assertTrue("highlighter is not equal to its copy", firstBuilder.equals(secondBuilder));
+            assertTrue("equals is not symmetric", secondBuilder.equals(firstBuilder));
+            assertThat("highlighter copy's hashcode is different from original hashcode", secondBuilder.hashCode(), equalTo(firstBuilder.hashCode()));
+
+            HighlightBuilder thirdBuilder = serializedCopy(secondBuilder);
+            assertTrue("highlighter is not equal to self", thirdBuilder.equals(thirdBuilder));
+            assertTrue("highlighter is not equal to its copy", secondBuilder.equals(thirdBuilder));
+            assertThat("highlighter copy's hashcode is different from original hashcode", secondBuilder.hashCode(), equalTo(thirdBuilder.hashCode()));
+            assertTrue("equals is not transitive", firstBuilder.equals(thirdBuilder));
+            assertThat("highlighter copy's hashcode is different from original hashcode", firstBuilder.hashCode(), equalTo(thirdBuilder.hashCode()));
+            assertTrue("equals is not symmetric", thirdBuilder.equals(secondBuilder));
+            assertTrue("equals is not symmetric", thirdBuilder.equals(firstBuilder));
+        }
+    }
+
+    /**
+     * create random shape that is put under test
+     */
+    private static HighlightBuilder randomHighlighterBuilder() {
+        HighlightBuilder testHighlighter = new HighlightBuilder();
+        setRandomCommonOptions(testHighlighter);
+        testHighlighter.useExplicitFieldOrder(randomBoolean());
+        if (randomBoolean()) {
+            testHighlighter.encoder(randomFrom(Arrays.asList(new String[]{"default", "html"})));
+        }
+        int numberOfFields = randomIntBetween(1,5);
+        for (int i = 0; i < numberOfFields; i++) {
+            Field field = new Field(randomAsciiOfLengthBetween(1, 10));
+            setRandomCommonOptions(field);
+            if (randomBoolean()) {
+                field.fragmentOffset(randomIntBetween(1, 100));
+            }
+            if (randomBoolean()) {
+                field.matchedFields(randomStringArray(0, 4));
+            }
+            testHighlighter.field(field);
+        }
+        return testHighlighter;
+    }
+
+    private static void setRandomCommonOptions(AbstractHighlighterBuilder highlightBuilder) {
+        if (randomBoolean()) {
+            highlightBuilder.preTags(randomStringArray(0, 3));
+        }
+        if (randomBoolean()) {
+            highlightBuilder.postTags(randomStringArray(0, 3));
+        }
+        if (randomBoolean()) {
+            highlightBuilder.fragmentSize(randomIntBetween(0, 100));
+        }
+        if (randomBoolean()) {
+            highlightBuilder.numOfFragments(randomIntBetween(0, 10));
+        }
+        if (randomBoolean()) {
+            highlightBuilder.highlighterType(randomAsciiOfLengthBetween(1, 10));
+        }
+        if (randomBoolean()) {
+            highlightBuilder.fragmenter(randomAsciiOfLengthBetween(1, 10));
+        }
+        if (randomBoolean()) {
+            QueryBuilder highlightQuery;
+            switch (randomInt(2)) {
+            case 0:
+                highlightQuery = new MatchAllQueryBuilder();
+                break;
+            case 1:
+                highlightQuery = new IdsQueryBuilder();
+                break;
+            default:
+            case 2:
+                highlightQuery = new TermQueryBuilder(randomAsciiOfLengthBetween(1, 10), randomAsciiOfLengthBetween(1, 10));
+                break;
+            }
+            highlightQuery.boost((float) randomDoubleBetween(0, 10, false));
+            highlightBuilder.highlightQuery(highlightQuery);
+        }
+        if (randomBoolean()) {
+            highlightBuilder.order(randomAsciiOfLengthBetween(1, 10));
+        }
+        if (randomBoolean()) {
+            highlightBuilder.highlightFilter(randomBoolean());
+        }
+        if (randomBoolean()) {
+            highlightBuilder.forceSource(randomBoolean());
+        }
+        if (randomBoolean()) {
+            highlightBuilder.boundaryMaxScan(randomIntBetween(0, 10));
+        }
+        if (randomBoolean()) {
+            highlightBuilder.boundaryChars(randomAsciiOfLengthBetween(1, 10).toCharArray());
+        }
+        if (randomBoolean()) {
+            highlightBuilder.noMatchSize(randomIntBetween(0, 10));
+        }
+        if (randomBoolean()) {
+            highlightBuilder.phraseLimit(randomIntBetween(0, 10));
+        }
+        if (randomBoolean()) {
+            int items = randomIntBetween(0, 5);
+            Map<String, Object> options = new HashMap<String, Object>(items);
+            for (int i = 0; i < items; i++) {
+                Object value = null;
+                switch (randomInt(2)) {
+                case 0:
+                    value = randomAsciiOfLengthBetween(1, 10);
+                    break;
+                case 1:
+                    value = new Integer(randomInt(1000));
+                    break;
+                case 2:
+                    value = new Boolean(randomBoolean());
+                    break;
+                }
+                options.put(randomAsciiOfLengthBetween(1, 10), value);
+            }
+        }
+        if (randomBoolean()) {
+            highlightBuilder.requireFieldMatch(randomBoolean());
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    private static void mutateCommonOptions(AbstractHighlighterBuilder highlightBuilder) {
+        switch (randomIntBetween(1, 16)) {
+        case 1:
+            highlightBuilder.preTags(randomStringArray(4, 6));
+            break;
+        case 2:
+            highlightBuilder.postTags(randomStringArray(4, 6));
+            break;
+        case 3:
+            highlightBuilder.fragmentSize(randomIntBetween(101, 200));
+            break;
+        case 4:
+            highlightBuilder.numOfFragments(randomIntBetween(11, 20));
+            break;
+        case 5:
+            highlightBuilder.highlighterType(randomAsciiOfLengthBetween(11, 20));
+            break;
+        case 6:
+            highlightBuilder.fragmenter(randomAsciiOfLengthBetween(11, 20));
+            break;
+        case 7:
+            highlightBuilder.highlightQuery(new TermQueryBuilder(randomAsciiOfLengthBetween(11, 20), randomAsciiOfLengthBetween(11, 20)));
+            break;
+        case 8:
+            highlightBuilder.order(randomAsciiOfLengthBetween(11, 20));
+            break;
+        case 9:
+            highlightBuilder.highlightFilter(toggleOrSet(highlightBuilder.highlightFilter()));
+        case 10:
+            highlightBuilder.forceSource(toggleOrSet(highlightBuilder.forceSource()));
+            break;
+        case 11:
+            highlightBuilder.boundaryMaxScan(randomIntBetween(11, 20));
+            break;
+        case 12:
+            highlightBuilder.boundaryChars(randomAsciiOfLengthBetween(11, 20).toCharArray());
+            break;
+        case 13:
+            highlightBuilder.noMatchSize(randomIntBetween(11, 20));
+            break;
+        case 14:
+            highlightBuilder.phraseLimit(randomIntBetween(11, 20));
+            break;
+        case 15:
+            int items = 6;
+            Map<String, Object> options = new HashMap<String, Object>(items);
+            for (int i = 0; i < items; i++) {
+                options.put(randomAsciiOfLengthBetween(1, 10), randomAsciiOfLengthBetween(1, 10));
+            }
+            highlightBuilder.options(options);
+            break;
+        case 16:
+            highlightBuilder.requireFieldMatch(toggleOrSet(highlightBuilder.requireFieldMatch()));
+            break;
+        }
+    }
+
+    private static Boolean toggleOrSet(Boolean flag) {
+        if (flag == null) {
+            return randomBoolean();
+        } else {
+            return !flag.booleanValue();
+        }
+    }
+
+    private static String[] randomStringArray(int minSize, int maxSize) {
+        int size = randomIntBetween(minSize, maxSize);
+        String[] randomStrings = new String[size];
+        for (int f = 0; f < size; f++) {
+            randomStrings[f] = randomAsciiOfLengthBetween(1, 10);
+        }
+        return randomStrings;
+    }
+
+    /**
+     * mutate the given highlighter builder so the returned one is different in one aspect
+     */
+    private static HighlightBuilder mutate(HighlightBuilder original) throws IOException {
+        HighlightBuilder mutation = serializedCopy(original);
+        if (randomBoolean()) {
+            mutateCommonOptions(mutation);
+        } else {
+            switch (randomIntBetween(0, 2)) {
+                // change settings that only exists on top level
+                case 0:
+                    mutation.useExplicitFieldOrder(!original.useExplicitFieldOrder()); break;
+                case 1:
+                    mutation.encoder(original.encoder() + randomAsciiOfLength(2)); break;
+                case 2:
+                    if (randomBoolean()) {
+                        // add another field
+                        mutation.field(new Field(randomAsciiOfLength(10)));
+                    } else {
+                        // change existing fields
+                        List<Field> originalFields = original.fields();
+                        Field fieldToChange = originalFields.get(randomInt(originalFields.size() - 1));
+                        if (randomBoolean()) {
+                            fieldToChange.fragmentOffset(randomIntBetween(101, 200));
+                        } else {
+                            fieldToChange.matchedFields(randomStringArray(5, 10));
+                        }
+                    }
+            }
+        }
+        return mutation;
+    }
+
+    private static HighlightBuilder serializedCopy(HighlightBuilder original) throws IOException {
+        try (BytesStreamOutput output = new BytesStreamOutput()) {
+            original.writeTo(output);
+            try (StreamInput in = new NamedWriteableAwareStreamInput(StreamInput.wrap(output.bytes()), namedWriteableRegistry)) {
+                return HighlightBuilder.PROTOTYPE.readFrom(in);
+            }
+        }
+    }
+}