|
@@ -19,6 +19,8 @@
|
|
|
|
|
|
package org.elasticsearch.search.highlight;
|
|
|
|
|
|
+import org.apache.lucene.search.Query;
|
|
|
+import org.apache.lucene.search.vectorhighlight.SimpleBoundaryScanner;
|
|
|
import org.elasticsearch.ExceptionsHelper;
|
|
|
import org.elasticsearch.common.ParsingException;
|
|
|
import org.elasticsearch.common.io.stream.StreamInput;
|
|
@@ -28,13 +30,20 @@ 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.index.query.QueryBuilder;
|
|
|
import org.elasticsearch.index.query.QueryParseContext;
|
|
|
-
|
|
|
+import org.elasticsearch.index.query.QueryShardContext;
|
|
|
+import org.elasticsearch.search.highlight.SearchContextHighlight.FieldOptions;
|
|
|
+import org.elasticsearch.search.highlight.SearchContextHighlight.FieldOptions.Builder;
|
|
|
import java.io.IOException;
|
|
|
import java.util.ArrayList;
|
|
|
import java.util.Arrays;
|
|
|
+import java.util.Collection;
|
|
|
+import java.util.Collections;
|
|
|
+import java.util.HashSet;
|
|
|
import java.util.List;
|
|
|
import java.util.Objects;
|
|
|
+import java.util.Set;
|
|
|
|
|
|
/**
|
|
|
* A builder for search highlighting. Settings can control how large fields
|
|
@@ -48,6 +57,51 @@ public class HighlightBuilder extends AbstractHighlighterBuilder<HighlightBuilde
|
|
|
|
|
|
public static final String HIGHLIGHT_ELEMENT_NAME = "highlight";
|
|
|
|
|
|
+ /** 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[] DEFAULT_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\">"
|
|
|
+ };
|
|
|
+ /** the default closing tags when <tt>tag_schema = "styled"</tt> */
|
|
|
+ public static final String[] DEFAULT_STYLED_POST_TAGS = {"</em>"};
|
|
|
+
|
|
|
+ /**
|
|
|
+ * a {@link FieldOptions.Builder} with default settings
|
|
|
+ */
|
|
|
+ public final static Builder defaultFieldOptions() {
|
|
|
+ return new SearchContextHighlight.FieldOptions.Builder()
|
|
|
+ .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(DEFAULT_NO_MATCH_SIZE).phraseLimit(DEFAULT_PHRASE_LIMIT);
|
|
|
+ }
|
|
|
+
|
|
|
private final List<Field> fields = new ArrayList<>();
|
|
|
|
|
|
private String encoder;
|
|
@@ -120,12 +174,12 @@ public class HighlightBuilder extends AbstractHighlighterBuilder<HighlightBuilde
|
|
|
public HighlightBuilder tagsSchema(String schemaName) {
|
|
|
switch (schemaName) {
|
|
|
case "default":
|
|
|
- preTags(HighlighterParseElement.DEFAULT_PRE_TAGS);
|
|
|
- postTags(HighlighterParseElement.DEFAULT_POST_TAGS);
|
|
|
+ preTags(DEFAULT_PRE_TAGS);
|
|
|
+ postTags(DEFAULT_POST_TAGS);
|
|
|
break;
|
|
|
case "styled":
|
|
|
- preTags(HighlighterParseElement.STYLED_PRE_TAG);
|
|
|
- postTags(HighlighterParseElement.STYLED_POST_TAGS);
|
|
|
+ preTags(DEFAULT_STYLED_PRE_TAG);
|
|
|
+ postTags(DEFAULT_STYLED_POST_TAGS);
|
|
|
break;
|
|
|
default:
|
|
|
throw new IllegalArgumentException("Unknown tag schema ["+ schemaName +"]");
|
|
@@ -289,7 +343,87 @@ public class HighlightBuilder extends AbstractHighlighterBuilder<HighlightBuilde
|
|
|
return highlightBuilder;
|
|
|
}
|
|
|
|
|
|
+ public SearchContextHighlight build(QueryShardContext context) throws IOException {
|
|
|
+ // create template global options that are later merged with any partial field options
|
|
|
+ final SearchContextHighlight.FieldOptions.Builder globalOptionsBuilder = new SearchContextHighlight.FieldOptions.Builder();
|
|
|
+ globalOptionsBuilder.encoder(this.encoder);
|
|
|
+ transferOptions(this, globalOptionsBuilder, context);
|
|
|
+
|
|
|
+ // overwrite unset global options by default values
|
|
|
+ globalOptionsBuilder.merge(defaultFieldOptions().build());
|
|
|
+
|
|
|
+ // create field options
|
|
|
+ Collection<org.elasticsearch.search.highlight.SearchContextHighlight.Field> fieldOptions = new ArrayList<>();
|
|
|
+ for (Field field : this.fields) {
|
|
|
+ final SearchContextHighlight.FieldOptions.Builder fieldOptionsBuilder = new SearchContextHighlight.FieldOptions.Builder();
|
|
|
+ fieldOptionsBuilder.fragmentOffset(field.fragmentOffset);
|
|
|
+ if (field.matchedFields != null) {
|
|
|
+ Set<String> matchedFields = new HashSet<String>(field.matchedFields.length);
|
|
|
+ Collections.addAll(matchedFields, field.matchedFields);
|
|
|
+ fieldOptionsBuilder.matchedFields(matchedFields);
|
|
|
+ }
|
|
|
+ transferOptions(field, fieldOptionsBuilder, context);
|
|
|
+ fieldOptions.add(new SearchContextHighlight.Field(field.name(), fieldOptionsBuilder.merge(globalOptionsBuilder.build()).build()));
|
|
|
+ }
|
|
|
+ return new SearchContextHighlight(fieldOptions);
|
|
|
+ }
|
|
|
|
|
|
+ /**
|
|
|
+ * Transfers field options present in the input {@link AbstractHighlighterBuilder} to the receiving
|
|
|
+ * {@link FieldOptions.Builder}, effectively overwriting existing settings
|
|
|
+ * @param targetOptionsBuilder the receiving options builder
|
|
|
+ * @param highlighterBuilder highlight builder with the input options
|
|
|
+ * @param context needed to convert {@link QueryBuilder} to {@link Query}
|
|
|
+ * @throws IOException on errors parsing any optional nested highlight query
|
|
|
+ */
|
|
|
+ @SuppressWarnings({ "rawtypes", "unchecked" })
|
|
|
+ private static void transferOptions(AbstractHighlighterBuilder highlighterBuilder, SearchContextHighlight.FieldOptions.Builder targetOptionsBuilder, QueryShardContext context) throws IOException {
|
|
|
+ targetOptionsBuilder.preTags(highlighterBuilder.preTags);
|
|
|
+ targetOptionsBuilder.postTags(highlighterBuilder.postTags);
|
|
|
+ targetOptionsBuilder.scoreOrdered("score".equals(highlighterBuilder.order));
|
|
|
+ if (highlighterBuilder.highlightFilter != null) {
|
|
|
+ targetOptionsBuilder.highlightFilter(highlighterBuilder.highlightFilter);
|
|
|
+ }
|
|
|
+ if (highlighterBuilder.fragmentSize != null) {
|
|
|
+ targetOptionsBuilder.fragmentCharSize(highlighterBuilder.fragmentSize);
|
|
|
+ }
|
|
|
+ if (highlighterBuilder.numOfFragments != null) {
|
|
|
+ targetOptionsBuilder.numberOfFragments(highlighterBuilder.numOfFragments);
|
|
|
+ }
|
|
|
+ if (highlighterBuilder.requireFieldMatch != null) {
|
|
|
+ targetOptionsBuilder.requireFieldMatch(highlighterBuilder.requireFieldMatch);
|
|
|
+ }
|
|
|
+ if (highlighterBuilder.boundaryMaxScan != null) {
|
|
|
+ targetOptionsBuilder.boundaryMaxScan(highlighterBuilder.boundaryMaxScan);
|
|
|
+ }
|
|
|
+ targetOptionsBuilder.boundaryChars(convertCharArray(highlighterBuilder.boundaryChars));
|
|
|
+ targetOptionsBuilder.highlighterType(highlighterBuilder.highlighterType);
|
|
|
+ targetOptionsBuilder.fragmenter(highlighterBuilder.fragmenter);
|
|
|
+ if (highlighterBuilder.noMatchSize != null) {
|
|
|
+ targetOptionsBuilder.noMatchSize(highlighterBuilder.noMatchSize);
|
|
|
+ }
|
|
|
+ if (highlighterBuilder.forceSource != null) {
|
|
|
+ targetOptionsBuilder.forceSource(highlighterBuilder.forceSource);
|
|
|
+ }
|
|
|
+ if (highlighterBuilder.phraseLimit != null) {
|
|
|
+ targetOptionsBuilder.phraseLimit(highlighterBuilder.phraseLimit);
|
|
|
+ }
|
|
|
+ targetOptionsBuilder.options(highlighterBuilder.options);
|
|
|
+ if (highlighterBuilder.highlightQuery != null) {
|
|
|
+ targetOptionsBuilder.highlightQuery(highlighterBuilder.highlightQuery.toQuery(context));
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private static Character[] convertCharArray(char[] array) {
|
|
|
+ if (array == null) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ Character[] charArray = new Character[array.length];
|
|
|
+ for (int i = 0; i < array.length; i++) {
|
|
|
+ charArray[i] = array[i];
|
|
|
+ }
|
|
|
+ return charArray;
|
|
|
+ }
|
|
|
|
|
|
public void innerXContent(XContentBuilder builder) throws IOException {
|
|
|
// first write common options
|