Refactor HighlighterBuilder

This change pulls out the common fields that HighlighterBuilder shares with
its nested Field class into a new abstract CommonHighlighterOptions superclass
which also gets equals() and hashCode() method and methods to serialize the
common fields to a StreamOutput and read them from a stream.

Relates to #15044
This commit is contained in:
Christoph Büscher 2015-11-26 19:45:33 +01:00
parent a0fe93fa67
commit 027a9b1844
5 changed files with 1013 additions and 472 deletions

View File

@ -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;

View File

@ -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. Its 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);
}
}

View File

@ -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,125 +140,10 @@ 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;
}
/**
* 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. Its 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
* @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;
return this;
public String encoder() {
return this.encoder;
}
/**
@ -331,71 +155,29 @@ public class HighlightBuilder implements ToXContent {
return this;
}
/**
* Gets value set with {@link #useExplicitFieldOrder(boolean)}
*/
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,26 +200,62 @@ public class HighlightBuilder implements ToXContent {
}
}
public static class Field {
final String name;
String[] preTags;
String[] postTags;
int fragmentSize = -1;
@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) + "\"}";
}
}
@Override
protected int doHashCode() {
return Objects.hash(encoder, useExplicitFieldOrder, fields);
}
@Override
protected boolean doEquals(HighlightBuilder other) {
return Objects.equals(encoder, other.encoder) &&
Objects.equals(useExplicitFieldOrder, other.useExplicitFieldOrder) &&
Objects.equals(fields, other.fields);
}
@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;
}
@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 static class Field extends AbstractHighlighterBuilder<Field> implements Writeable<Field> {
static final Field PROTOTYPE = new Field("_na_");
private final String name;
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;
@ -503,118 +265,11 @@ public class HighlightBuilder implements ToXContent {
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;
}
public Field numOfFragments(int numOfFragments) {
this.numOfFragments = numOfFragments;
return this;
}
public Field highlightFilter(boolean highlightFilter) {
this.highlightFilter = highlightFilter;
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.
* This overrides global settings set by {@link HighlightBuilder#order(String)}.
*/
public Field order(String order) {
this.order = order;
return this;
}
public Field requireFieldMatch(boolean requireFieldMatch) {
this.requireFieldMatch = requireFieldMatch;
return this;
}
public Field boundaryMaxScan(int boundaryMaxScan) {
this.boundaryMaxScan = boundaryMaxScan;
return this;
}
public Field 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>.
* This overrides global settings set by {@link HighlightBuilder#highlighterType(String)}.
*/
public Field 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 plain / normal highlighter.
* This overrides global settings set by {@link HighlightBuilder#fragmenter(String)}.
*/
public Field fragmenter(String fragmenter) {
this.fragmenter = fragmenter;
return this;
}
/**
* Sets a query to use for highlighting this field instead of the search query.
*/
public Field 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.
* @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;
}
/**
* 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;
return this;
}
/**
* Set the matched fields to highlight against this field data. Default to null, meaning just
* the named field. If you provide a list of fields here then don't forget to include name as
@ -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();
}
/**
* 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 int doHashCode() {
return Objects.hash(name, fragmentOffset, Arrays.hashCode(matchedFields));
}
@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);
}
}
}

View File

@ -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) {

View File

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