summaryrefslogtreecommitdiff
path: root/core/src/test/java/org/elasticsearch/search/fetch/subphase/highlight/HighlightBuilderTests.java
diff options
context:
space:
mode:
authorNik Everett <nik9000@gmail.com>2016-07-29 23:05:38 -0400
committerNik Everett <nik9000@gmail.com>2016-08-12 18:21:15 -0400
commitcf6e1a43623a26e012af4d73f6402a8c4ba8034c (patch)
tree213e9af2b6106f1e2e35888cd3db29c9309df978 /core/src/test/java/org/elasticsearch/search/fetch/subphase/highlight/HighlightBuilderTests.java
parent40d7ebc515978fa80d7a2bde3e72df0648f3b89a (diff)
Move all FetchSubPhases to `o.e.search.fetch.subphase`
As the most complicated `FetchSubPhase` highlighting gets its own package (`o.e.seach.fetch.subphase.highlight`. No other `FetchSubPhase`s get their own package. Instead they all reside together in `o.e.search.fetch.subphase`. Add package descriptions to `o.e.search.fetch` and subpackages.
Diffstat (limited to 'core/src/test/java/org/elasticsearch/search/fetch/subphase/highlight/HighlightBuilderTests.java')
-rw-r--r--core/src/test/java/org/elasticsearch/search/fetch/subphase/highlight/HighlightBuilderTests.java751
1 files changed, 751 insertions, 0 deletions
diff --git a/core/src/test/java/org/elasticsearch/search/fetch/subphase/highlight/HighlightBuilderTests.java b/core/src/test/java/org/elasticsearch/search/fetch/subphase/highlight/HighlightBuilderTests.java
new file mode 100644
index 0000000000..ed272040f5
--- /dev/null
+++ b/core/src/test/java/org/elasticsearch/search/fetch/subphase/highlight/HighlightBuilderTests.java
@@ -0,0 +1,751 @@
+/*
+ * 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.ParseFieldMatcher;
+import org.elasticsearch.common.ParsingException;
+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.common.settings.Settings;
+import org.elasticsearch.common.xcontent.ToXContent;
+import org.elasticsearch.common.xcontent.XContentBuilder;
+import org.elasticsearch.common.xcontent.XContentFactory;
+import org.elasticsearch.common.xcontent.XContentHelper;
+import org.elasticsearch.common.xcontent.XContentParser;
+import org.elasticsearch.common.xcontent.XContentType;
+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.indices.query.IndicesQueriesRegistry;
+import org.elasticsearch.search.SearchModule;
+import org.elasticsearch.search.fetch.subphase.highlight.AbstractHighlighterBuilder;
+import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
+import org.elasticsearch.search.fetch.subphase.highlight.SearchContextHighlight;
+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.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;
+ private static IndicesQueriesRegistry indicesQueriesRegistry;
+
+ /**
+ * 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());
+ indicesQueriesRegistry = searchModule.getQueryParserRegistry();
+ }
+
+ @AfterClass
+ public static void afterClass() throws Exception {
+ namedWriteableRegistry = null;
+ indicesQueriesRegistry = 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));
+ }
+ }
+
+ /**
+ * 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();
+ }
+ highlightBuilder.toXContent(builder, ToXContent.EMPTY_PARAMS);
+ XContentBuilder shuffled = shuffleXContent(builder);
+
+ XContentParser parser = XContentHelper.createParser(shuffled.bytes());
+ QueryParseContext context = new QueryParseContext(indicesQueriesRegistry, parser, ParseFieldMatcher.EMPTY);
+ 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 static <T extends Throwable> T expectParseThrows(Class<T> exceptionClass, String highlightElement) throws IOException {
+ XContentParser parser = XContentFactory.xContent(highlightElement).createParser(highlightElement);
+ QueryParseContext context = new QueryParseContext(indicesQueriesRegistry, parser, ParseFieldMatcher.STRICT);
+ 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(randomAsciiOfLengthBetween(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(idxSettings, null, null, null, null, null, indicesQueriesRegistry,
+ null, null, null) {
+ @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::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<String>(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 = XContentFactory.xContent(highlightElement).createParser(highlightElement);
+
+ QueryParseContext context = new QueryParseContext(indicesQueriesRegistry, parser, ParseFieldMatcher.EMPTY);
+ 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 = XContentFactory.xContent(highlightElement).createParser(highlightElement);
+
+ context = new QueryParseContext(indicesQueriesRegistry, parser, ParseFieldMatcher.EMPTY);
+ 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 = XContentFactory.xContent(highlightElement).createParser(highlightElement);
+
+ QueryParseContext context = new QueryParseContext(indicesQueriesRegistry, parser, ParseFieldMatcher.EMPTY);
+ HighlightBuilder highlightBuilder = HighlightBuilder.fromXContent(context);
+ assertEquals("expected plain HighlightBuilder", new HighlightBuilder(), highlightBuilder);
+
+ highlightElement = "{ \"fields\" : { } }";
+ parser = XContentFactory.xContent(highlightElement).createParser(highlightElement);
+
+ context = new QueryParseContext(indicesQueriesRegistry, parser, ParseFieldMatcher.EMPTY);
+ highlightBuilder = HighlightBuilder.fromXContent(context);
+ assertEquals("defining no field should return plain HighlightBuilder", new HighlightBuilder(), highlightBuilder);
+
+ highlightElement = "{ \"fields\" : { \"foo\" : { } } }";
+ parser = XContentFactory.xContent(highlightElement).createParser(highlightElement);
+
+ context = new QueryParseContext(indicesQueriesRegistry, parser, ParseFieldMatcher.EMPTY);
+ 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 + "_" + randomAsciiOfLengthBetween(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", "unchecked" })
+ 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(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()) {
+ 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()) {
+ 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", "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(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:
+ 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(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();
+ }
+ }
+
+ /**
+ * 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<String>(size);
+ for (int f = 0; f < size; f++) {
+ randomStrings.add(randomAsciiOfLengthBetween(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() + 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));
+ }
+ }
+ break;
+ }
+ }
+ return mutation;
+ }
+
+ private static HighlightBuilder serializedCopy(HighlightBuilder original) throws IOException {
+ try (BytesStreamOutput output = new BytesStreamOutput()) {
+ original.writeTo(output);
+ try (StreamInput in = new NamedWriteableAwareStreamInput(output.bytes().streamInput(), namedWriteableRegistry)) {
+ return new HighlightBuilder(in);
+ }
+ }
+ }
+}