/*
* 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.fetch.subphase.highlight;
import org.apache.lucene.search.Query;
import org.elasticsearch.Version;
import org.elasticsearch.cluster.metadata.IndexMetaData;
import org.elasticsearch.common.ParsingException;
import org.elasticsearch.common.io.stream.BytesStreamOutput;
import org.elasticsearch.common.io.stream.NamedWriteableRegistry;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.xcontent.NamedXContentRegistry;
import org.elasticsearch.common.xcontent.ToXContent;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentFactory;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.common.xcontent.json.JsonXContent;
import org.elasticsearch.index.Index;
import org.elasticsearch.index.IndexSettings;
import org.elasticsearch.index.mapper.ContentPath;
import org.elasticsearch.index.mapper.MappedFieldType;
import org.elasticsearch.index.mapper.Mapper;
import org.elasticsearch.index.mapper.TextFieldMapper;
import org.elasticsearch.index.query.IdsQueryBuilder;
import org.elasticsearch.index.query.MatchAllQueryBuilder;
import org.elasticsearch.index.query.QueryBuilder;
import org.elasticsearch.index.query.QueryParseContext;
import org.elasticsearch.index.query.QueryShardContext;
import org.elasticsearch.index.query.TermQueryBuilder;
import org.elasticsearch.search.SearchModule;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder.BoundaryScannerType;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder.Field;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder.Order;
import org.elasticsearch.search.fetch.subphase.highlight.SearchContextHighlight.FieldOptions;
import org.elasticsearch.test.ESTestCase;
import org.elasticsearch.test.IndexSettingsModule;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import java.io.IOException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.function.BiConsumer;
import java.util.function.Function;
import static java.util.Collections.emptyList;
import static org.elasticsearch.test.EqualsHashCodeTestUtils.checkEqualsAndHashCode;
import static org.hamcrest.Matchers.equalTo;
public class HighlightBuilderTests extends ESTestCase {
private static final int NUMBER_OF_TESTBUILDERS = 20;
private static NamedWriteableRegistry namedWriteableRegistry;
private static NamedXContentRegistry xContentRegistry;
/**
* setup for the whole base test class
*/
@BeforeClass
public static void init() {
SearchModule searchModule = new SearchModule(Settings.EMPTY, false, emptyList());
namedWriteableRegistry = new NamedWriteableRegistry(searchModule.getNamedWriteables());
xContentRegistry = new NamedXContentRegistry(searchModule.getNamedXContents());
}
@AfterClass
public static void afterClass() throws Exception {
namedWriteableRegistry = null;
xContentRegistry = 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++) {
checkEqualsAndHashCode(randomHighlighterBuilder(), HighlightBuilderTests::serializedCopy, HighlightBuilderTests::mutate);
}
}
/**
* creates random highlighter, renders it to xContent and back to new instance that should be equal to original
*/
public void testFromXContent() throws IOException {
for (int runs = 0; runs < NUMBER_OF_TESTBUILDERS; runs++) {
HighlightBuilder highlightBuilder = randomHighlighterBuilder();
XContentBuilder builder = XContentFactory.contentBuilder(randomFrom(XContentType.values()));
if (randomBoolean()) {
builder.prettyPrint();
}
XContentBuilder shuffled;
if (randomBoolean()) {
//this way `fields` is printed out as a json array
highlightBuilder.useExplicitFieldOrder(true);
highlightBuilder.toXContent(builder, ToXContent.EMPTY_PARAMS);
shuffled = shuffleXContent(builder);
} else {
highlightBuilder.toXContent(builder, ToXContent.EMPTY_PARAMS);
shuffled = shuffleXContent(builder, "fields");
}
XContentParser parser = createParser(shuffled);
QueryParseContext context = new QueryParseContext(parser);
parser.nextToken();
HighlightBuilder secondHighlightBuilder;
try {
secondHighlightBuilder = HighlightBuilder.fromXContent(context);
} catch (RuntimeException e) {
throw new RuntimeException("Error parsing " + highlightBuilder, e);
}
assertNotSame(highlightBuilder, secondHighlightBuilder);
assertEquals(highlightBuilder, secondHighlightBuilder);
assertEquals(highlightBuilder.hashCode(), secondHighlightBuilder.hashCode());
}
}
/**
* test that unknown array fields cause exception
*/
public void testUnknownArrayNameExpection() throws IOException {
{
IllegalArgumentException e = expectParseThrows(IllegalArgumentException.class, "{\n" +
" \"bad_fieldname\" : [ \"field1\" 1 \"field2\" ]\n" +
"}\n");
assertEquals("[highlight] unknown field [bad_fieldname], parser not found", e.getMessage());
}
{
ParsingException e = expectParseThrows(ParsingException.class, "{\n" +
" \"fields\" : {\n" +
" \"body\" : {\n" +
" \"bad_fieldname\" : [ \"field1\" , \"field2\" ]\n" +
" }\n" +
" }\n" +
"}\n");
assertEquals("[highlight] failed to parse field [fields]", e.getMessage());
assertEquals("[fields] failed to parse field [body]", e.getCause().getMessage());
assertEquals("[highlight_field] unknown field [bad_fieldname], parser not found", e.getCause().getCause().getMessage());
}
}
private <T extends Throwable> T expectParseThrows(Class<T> exceptionClass, String highlightElement) throws IOException {
XContentParser parser = createParser(JsonXContent.jsonXContent, highlightElement);
QueryParseContext context = new QueryParseContext(parser);
return expectThrows(exceptionClass, () -> HighlightBuilder.fromXContent(context));
}
/**
* test that unknown field name cause exception
*/
public void testUnknownFieldnameExpection() throws IOException {
{
IllegalArgumentException e = expectParseThrows(IllegalArgumentException.class, "{\n" +
" \"bad_fieldname\" : \"value\"\n" +
"}\n");
assertEquals("[highlight] unknown field [bad_fieldname], parser not found", e.getMessage());
}
{
ParsingException e = expectParseThrows(ParsingException.class, "{\n" +
" \"fields\" : {\n" +
" \"body\" : {\n" +
" \"bad_fieldname\" : \"value\"\n" +
" }\n" +
" }\n" +
"}\n");
assertEquals("[highlight] failed to parse field [fields]", e.getMessage());
assertEquals("[fields] failed to parse field [body]", e.getCause().getMessage());
assertEquals("[highlight_field] unknown field [bad_fieldname], parser not found", e.getCause().getCause().getMessage());
}
}
/**
* test that unknown field name cause exception
*/
public void testUnknownObjectFieldnameExpection() throws IOException {
{
IllegalArgumentException e = expectParseThrows(IllegalArgumentException.class, "{\n" +
" \"bad_fieldname\" : { \"field\" : \"value\" }\n \n" +
"}\n");
assertEquals("[highlight] unknown field [bad_fieldname], parser not found", e.getMessage());
}
{
ParsingException e = expectParseThrows(ParsingException.class, "{\n" +
" \"fields\" : {\n" +
" \"body\" : {\n" +
" \"bad_fieldname\" : { \"field\" : \"value\" }\n" +
" }\n" +
" }\n" +
"}\n");
assertEquals("[highlight] failed to parse field [fields]", e.getMessage());
assertEquals("[fields] failed to parse field [body]", e.getCause().getMessage());
assertEquals("[highlight_field] unknown field [bad_fieldname], parser not found", e.getCause().getCause().getMessage());
}
}
public void testStringInFieldsArray() throws IOException {
ParsingException e = expectParseThrows(ParsingException.class, "{\"fields\" : [ \"junk\" ]}");
assertEquals("[highlight] failed to parse field [fields]", e.getMessage());
assertEquals(
"[fields] can be a single object with any number of fields or an array where each entry is an object with a single field",
e.getCause().getMessage());
}
public void testNoFieldsInObjectInFieldsArray() throws IOException {
ParsingException e = expectParseThrows(ParsingException.class, "{\n" +
" \"fields\" : [ {\n" +
" }] \n" +
"}\n");
assertEquals("[highlight] failed to parse field [fields]", e.getMessage());
assertEquals(
"[fields] can be a single object with any number of fields or an array where each entry is an object with a single field",
e.getCause().getMessage());
}
public void testTwoFieldsInObjectInFieldsArray() throws IOException {
ParsingException e = expectParseThrows(ParsingException.class, "{\n" +
" \"fields\" : [ {\n" +
" \"body\" : {},\n" +
" \"nope\" : {}\n" +
" }] \n" +
"}\n");
assertEquals("[highlight] failed to parse field [fields]", e.getMessage());
assertEquals(
"[fields] can be a single object with any number of fields or an array where each entry is an object with a single field",
e.getCause().getMessage()); }
/**
* test that build() outputs a {@link SearchContextHighlight} that is has similar parameters
* than what we have in the random {@link HighlightBuilder}
*/
public void testBuildSearchContextHighlight() throws IOException {
Settings indexSettings = Settings.builder()
.put(IndexMetaData.SETTING_VERSION_CREATED, Version.CURRENT).build();
Index index = new Index(randomAlphaOfLengthBetween(1, 10), "_na_");
IndexSettings idxSettings = IndexSettingsModule.newIndexSettings(index, indexSettings);
// shard context will only need indicesQueriesRegistry for building Query objects nested in highlighter
QueryShardContext mockShardContext = new QueryShardContext(0, idxSettings, null, null, null, null, null, xContentRegistry(),
null, null, System::currentTimeMillis) {
@Override
public MappedFieldType fieldMapper(String name) {
TextFieldMapper.Builder builder = new TextFieldMapper.Builder(name);
return builder.build(new Mapper.BuilderContext(idxSettings.getSettings(), new ContentPath(1))).fieldType();
}
};
mockShardContext.setMapUnmappedFieldAsString(true);
for (int runs = 0; runs < NUMBER_OF_TESTBUILDERS; runs++) {
HighlightBuilder highlightBuilder = randomHighlighterBuilder();
SearchContextHighlight highlight = highlightBuilder.build(mockShardContext);
for (SearchContextHighlight.Field field : highlight.fields()) {
String encoder = highlightBuilder.encoder() != null ? highlightBuilder.encoder() : HighlightBuilder.DEFAULT_ENCODER;
assertEquals(encoder, field.fieldOptions().encoder());
final Field fieldBuilder = getFieldBuilderByName(highlightBuilder, field.field());
assertNotNull("expected a highlight builder for field " + field.field(), fieldBuilder);
FieldOptions fieldOptions = field.fieldOptions();
BiConsumer<Function<AbstractHighlighterBuilder<?>, Object>, Function<FieldOptions, Object>> checkSame =
mergeBeforeChek(highlightBuilder, fieldBuilder, fieldOptions);
checkSame.accept(AbstractHighlighterBuilder::boundaryChars, FieldOptions::boundaryChars);
checkSame.accept(AbstractHighlighterBuilder::boundaryScannerType, FieldOptions::boundaryScannerType);
checkSame.accept(AbstractHighlighterBuilder::boundaryMaxScan, FieldOptions::boundaryMaxScan);
checkSame.accept(AbstractHighlighterBuilder::fragmentSize, FieldOptions::fragmentCharSize);
checkSame.accept(AbstractHighlighterBuilder::fragmenter, FieldOptions::fragmenter);
checkSame.accept(AbstractHighlighterBuilder::requireFieldMatch, FieldOptions::requireFieldMatch);
checkSame.accept(AbstractHighlighterBuilder::noMatchSize, FieldOptions::noMatchSize);
checkSame.accept(AbstractHighlighterBuilder::numOfFragments, FieldOptions::numberOfFragments);
checkSame.accept(AbstractHighlighterBuilder::phraseLimit, FieldOptions::phraseLimit);
checkSame.accept(AbstractHighlighterBuilder::highlighterType, FieldOptions::highlighterType);
checkSame.accept(AbstractHighlighterBuilder::highlightFilter, FieldOptions::highlightFilter);
checkSame.accept(AbstractHighlighterBuilder::preTags, FieldOptions::preTags);
checkSame.accept(AbstractHighlighterBuilder::postTags, FieldOptions::postTags);
checkSame.accept(AbstractHighlighterBuilder::options, FieldOptions::options);
checkSame.accept(AbstractHighlighterBuilder::order, op -> op.scoreOrdered() ? Order.SCORE : Order.NONE);
assertEquals(fieldBuilder.fragmentOffset, fieldOptions.fragmentOffset());
if (fieldBuilder.matchedFields != null) {
String[] copy = Arrays.copyOf(fieldBuilder.matchedFields, fieldBuilder.matchedFields.length);
Arrays.sort(copy);
assertArrayEquals(copy,
new TreeSet<>(fieldOptions.matchedFields()).toArray(new String[fieldOptions.matchedFields().size()]));
} else {
assertNull(fieldOptions.matchedFields());
}
Query expectedValue = null;
if (fieldBuilder.highlightQuery != null) {
expectedValue = QueryBuilder.rewriteQuery(fieldBuilder.highlightQuery, mockShardContext).toQuery(mockShardContext);
} else if (highlightBuilder.highlightQuery != null) {
expectedValue = QueryBuilder.rewriteQuery(highlightBuilder.highlightQuery, mockShardContext).toQuery(mockShardContext);
}
assertEquals(expectedValue, fieldOptions.highlightQuery());
}
}
}
/**
* Create a generic helper function that performs all the work of merging the global highlight builder parameter,
* the (potential) overwrite on the field level and the default value from {@link HighlightBuilder#defaultOptions}
* before making the assertion that the value in the highlight builder and the actual value in the {@link FieldOptions}
* passed in is the same.
*
* @param highlightBuilder provides the (optional) global builder parameter
* @param fieldBuilder provides the (optional) field level parameter, if present this overwrites the global value
* @param options the target field options that are checked
*/
private static BiConsumer<Function<AbstractHighlighterBuilder<?>, Object>, Function<FieldOptions, Object>> mergeBeforeChek(
HighlightBuilder highlightBuilder, Field fieldBuilder, FieldOptions options) {
return (highlightBuilderParameterAccessor, fieldOptionsParameterAccessor) -> {
Object expectedValue = null;
Object globalLevelValue = highlightBuilderParameterAccessor.apply(highlightBuilder);
Object fieldLevelValue = highlightBuilderParameterAccessor.apply(fieldBuilder);
if (fieldLevelValue != null) {
expectedValue = fieldLevelValue;
} else if (globalLevelValue != null) {
expectedValue = globalLevelValue;
} else {
expectedValue = fieldOptionsParameterAccessor.apply(HighlightBuilder.defaultOptions);
}
Object actualValue = fieldOptionsParameterAccessor.apply(options);
if (actualValue instanceof String[]) {
assertArrayEquals((String[]) expectedValue, (String[]) actualValue);
} else if (actualValue instanceof Character[]) {
if (expectedValue instanceof char[]) {
assertArrayEquals(HighlightBuilder.convertCharArray((char[]) expectedValue), (Character[]) actualValue);
} else {
assertArrayEquals((Character[]) expectedValue, (Character[]) actualValue);
}
} else {
assertEquals(expectedValue, actualValue);
}
};
}
private static Field getFieldBuilderByName(HighlightBuilder highlightBuilder, String fieldName) {
for (Field hbfield : highlightBuilder.fields()) {
if (hbfield.name().equals(fieldName)) {
return hbfield;
}
}
return null;
}
/**
* `tags_schema` is not produced by toXContent in the builder but should be parseable, so this
* adds a simple json test for this.
*/
public void testParsingTagsSchema() throws IOException {
String highlightElement = "{\n" +
" \"tags_schema\" : \"styled\"\n" +
"}\n";
XContentParser parser = createParser(JsonXContent.jsonXContent, highlightElement);
QueryParseContext context = new QueryParseContext(parser);
HighlightBuilder highlightBuilder = HighlightBuilder.fromXContent(context);
assertArrayEquals("setting tags_schema 'styled' should alter pre_tags", HighlightBuilder.DEFAULT_STYLED_PRE_TAG,
highlightBuilder.preTags());
assertArrayEquals("setting tags_schema 'styled' should alter post_tags", HighlightBuilder.DEFAULT_STYLED_POST_TAGS,
highlightBuilder.postTags());
highlightElement = "{\n" +
" \"tags_schema\" : \"default\"\n" +
"}\n";
parser = createParser(JsonXContent.jsonXContent, highlightElement);
context = new QueryParseContext(parser);
highlightBuilder = HighlightBuilder.fromXContent(context);
assertArrayEquals("setting tags_schema 'default' should alter pre_tags", HighlightBuilder.DEFAULT_PRE_TAGS,
highlightBuilder.preTags());
assertArrayEquals("setting tags_schema 'default' should alter post_tags", HighlightBuilder.DEFAULT_POST_TAGS,
highlightBuilder.postTags());
ParsingException e = expectParseThrows(ParsingException.class, "{\n" +
" \"tags_schema\" : \"somthing_else\"\n" +
"}\n");
assertEquals("[highlight] failed to parse field [tags_schema]", e.getMessage());
assertEquals("Unknown tag schema [somthing_else]", e.getCause().getMessage());
}
/**
* test parsing empty highlight or empty fields blocks
*/
public void testParsingEmptyStructure() throws IOException {
String highlightElement = "{ }";
XContentParser parser = createParser(JsonXContent.jsonXContent, highlightElement);
QueryParseContext context = new QueryParseContext(parser);
HighlightBuilder highlightBuilder = HighlightBuilder.fromXContent(context);
assertEquals("expected plain HighlightBuilder", new HighlightBuilder(), highlightBuilder);
highlightElement = "{ \"fields\" : { } }";
parser = createParser(JsonXContent.jsonXContent, highlightElement);
context = new QueryParseContext(parser);
highlightBuilder = HighlightBuilder.fromXContent(context);
assertEquals("defining no field should return plain HighlightBuilder", new HighlightBuilder(), highlightBuilder);
highlightElement = "{ \"fields\" : { \"foo\" : { } } }";
parser = createParser(JsonXContent.jsonXContent, highlightElement);
context = new QueryParseContext(parser);
highlightBuilder = HighlightBuilder.fromXContent(context);
assertEquals("expected HighlightBuilder with field", new HighlightBuilder().field(new Field("foo")), highlightBuilder);
}
public void testPreTagsWithoutPostTags() throws IOException {
ParsingException e = expectParseThrows(ParsingException.class, "{\n" +
" \"pre_tags\" : [\"<a>\"]\n" +
"}\n");
assertEquals("pre_tags are set but post_tags are not set", e.getMessage());
e = expectParseThrows(ParsingException.class, "{\n" +
" \"fields\" : {\n" +
" \"body\" : {\n" +
" \"pre_tags\" : [\"<a>\"]\n" +
" }\n" +
" }\n" +
"}\n");
assertEquals("[highlight] failed to parse field [fields]", e.getMessage());
assertEquals("[fields] failed to parse field [body]", e.getCause().getMessage());
assertEquals("pre_tags are set but post_tags are not set", e.getCause().getCause().getMessage());
}
/**
* test ordinals of {@link Order}, since serialization depends on it
*/
public void testValidOrderOrdinals() {
assertThat(Order.NONE.ordinal(), equalTo(0));
assertThat(Order.SCORE.ordinal(), equalTo(1));
}
public void testOrderSerialization() throws Exception {
try (BytesStreamOutput out = new BytesStreamOutput()) {
Order.NONE.writeTo(out);
try (StreamInput in = out.bytes().streamInput()) {
assertThat(in.readVInt(), equalTo(0));
}
}
try (BytesStreamOutput out = new BytesStreamOutput()) {
Order.SCORE.writeTo(out);
try (StreamInput in = out.bytes().streamInput()) {
assertThat(in.readVInt(), equalTo(1));
}
}
}
protected static XContentBuilder toXContent(HighlightBuilder highlight, XContentType contentType) throws IOException {
XContentBuilder builder = XContentFactory.contentBuilder(contentType);
if (randomBoolean()) {
builder.prettyPrint();
}
highlight.toXContent(builder, ToXContent.EMPTY_PARAMS);
return builder;
}
/**
* create random highlight builder that is put under test
*/
public 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(i + "_" + randomAlphaOfLengthBetween(1, 10));
setRandomCommonOptions(field);
if (randomBoolean()) {
field.fragmentOffset(randomIntBetween(1, 100));
}
if (randomBoolean()) {
field.matchedFields(randomStringArray(0, 4));
}
testHighlighter.field(field);
}
return testHighlighter;
}
@SuppressWarnings({ "rawtypes"})
private static void setRandomCommonOptions(AbstractHighlighterBuilder highlightBuilder) {
if (randomBoolean()) {
// need to set this together, otherwise parsing will complain
highlightBuilder.preTags(randomStringArray(0, 3));
highlightBuilder.postTags(randomStringArray(0, 3));
}
if (randomBoolean()) {
highlightBuilder.fragmentSize(randomIntBetween(0, 100));
}
if (randomBoolean()) {
highlightBuilder.numOfFragments(randomIntBetween(0, 10));
}
if (randomBoolean()) {
highlightBuilder.highlighterType(randomAlphaOfLengthBetween(1, 10));
}
if (randomBoolean()) {
highlightBuilder.fragmenter(randomAlphaOfLengthBetween(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(randomAlphaOfLengthBetween(1, 10), randomAlphaOfLengthBetween(1, 10));
break;
}
highlightQuery.boost((float) randomDoubleBetween(0, 10, false));
highlightBuilder.highlightQuery(highlightQuery);
}
if (randomBoolean()) {
if (randomBoolean()) {
highlightBuilder.order(randomFrom(Order.values()));
} else {
// also test the string setter
highlightBuilder.order(randomFrom(Order.values()).toString());
}
}
if (randomBoolean()) {
highlightBuilder.highlightFilter(randomBoolean());
}
if (randomBoolean()) {
highlightBuilder.forceSource(randomBoolean());
}
if (randomBoolean()) {
if (randomBoolean()) {
highlightBuilder.boundaryScannerType(randomFrom(BoundaryScannerType.values()));
} else {
// also test the string setter
highlightBuilder.boundaryScannerType(randomFrom(BoundaryScannerType.values()).toString());
}
}
if (randomBoolean()) {
highlightBuilder.boundaryMaxScan(randomIntBetween(0, 10));
}
if (randomBoolean()) {
highlightBuilder.boundaryChars(randomAlphaOfLengthBetween(1, 10).toCharArray());
}
if (randomBoolean()) {
highlightBuilder.boundaryScannerLocale(randomLocale(random()).toLanguageTag());
}
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<>(items);
for (int i = 0; i < items; i++) {
Object value = null;
switch (randomInt(2)) {
case 0:
value = randomAlphaOfLengthBetween(1, 10);
break;
case 1:
value = new Integer(randomInt(1000));
break;
case 2:
value = new Boolean(randomBoolean());
break;
}
options.put(randomAlphaOfLengthBetween(1, 10), value);
}
}
if (randomBoolean()) {
highlightBuilder.requireFieldMatch(randomBoolean());
}
}
@SuppressWarnings({ "unchecked", "rawtypes" })
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(randomAlphaOfLengthBetween(11, 20));
break;
case 6:
highlightBuilder.fragmenter(randomAlphaOfLengthBetween(11, 20));
break;
case 7:
highlightBuilder.highlightQuery(new TermQueryBuilder(randomAlphaOfLengthBetween(11, 20), randomAlphaOfLengthBetween(11, 20)));
break;
case 8:
if (highlightBuilder.order() == Order.NONE) {
highlightBuilder.order(Order.SCORE);
} else {
highlightBuilder.order(Order.NONE);
}
break;
case 9:
highlightBuilder.highlightFilter(toggleOrSet(highlightBuilder.highlightFilter()));
break;
case 10:
highlightBuilder.forceSource(toggleOrSet(highlightBuilder.forceSource()));
break;
case 11:
highlightBuilder.boundaryMaxScan(randomIntBetween(11, 20));
break;
case 12:
highlightBuilder.boundaryChars(randomAlphaOfLengthBetween(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<>(items);
for (int i = 0; i < items; i++) {
options.put(randomAlphaOfLengthBetween(1, 10), randomAlphaOfLengthBetween(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();
}
}
/**
* Create array of unique Strings. If not unique, e.g. duplicates field names
* would be dropped in {@link FieldOptions.Builder#matchedFields(Set)}, resulting in test glitches
*/
private static String[] randomStringArray(int minSize, int maxSize) {
int size = randomIntBetween(minSize, maxSize);
Set<String> randomStrings = new HashSet<>(size);
for (int f = 0; f < size; f++) {
randomStrings.add(randomAlphaOfLengthBetween(3, 10));
}
return randomStrings.toArray(new String[randomStrings.size()]);
}
/**
* 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() + randomAlphaOfLength(2)); break;
case 2:
if (randomBoolean()) {
// add another field
mutation.field(new Field(randomAlphaOfLength(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));
}
}
break;
}
}
return mutation;
}
private static HighlightBuilder serializedCopy(HighlightBuilder original) throws IOException {
return ESTestCase.copyWriteable(original, namedWriteableRegistry, HighlightBuilder::new);
}
@Override
protected NamedXContentRegistry xContentRegistry() {
return xContentRegistry;
}
}