Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Highlight fields in request order #6178

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
17 changes: 17 additions & 0 deletions docs/reference/search/request/highlighting.asciidoc
Expand Up @@ -547,3 +547,20 @@ keep in mind that scoring more phrases consumes more time and memory.

If using `matched_fields` keep in mind that `phrase_limit` phrases per
matched field are considered.

[[explicit-field-order]]
=== Field Highlight Order
Elasticsearch highlights the fields in the order that they are sent. Per the
json spec objects are unordered but if you need to be explicit about the order
that fields are highlighted then you can use an array for `fields` like this:
[source,js]
--------------------------------------------------
"highlight": {
"fields": [
{"title":{ /*params*/ }},
{"text":{ /*params*/ }}
]
}
--------------------------------------------------
None of the highlighters built into Elasticsearch care about the order that the
fields are highlighted but a plugin may.
Expand Up @@ -806,6 +806,15 @@ public SearchRequestBuilder setHighlighterForceSource(Boolean forceSource) {
return this;
}

/**
* Send the fields to be highlighted using a syntax that is specific about the order in which they should be highlighted.
* @return this for chaining
*/
public SearchRequestBuilder setHighlighterExplicitFieldOrder(boolean explicitFieldOrder) {
highlightBuilder().useExplicitFieldOrder(explicitFieldOrder);
return this;
}

/**
* Delegates to {@link org.elasticsearch.search.suggest.SuggestBuilder#setText(String)}.
*/
Expand Down
Expand Up @@ -74,6 +74,8 @@ public class HighlightBuilder implements ToXContent {

private Boolean forceSource;

private boolean useExplicitFieldOrder = false;

/**
* Adds a field to be highlighted with default fragment size of 100 characters, and
* default number of fragments of 5 using the default encoder
Expand Down Expand Up @@ -289,6 +291,15 @@ public HighlightBuilder forceSource(boolean forceSource) {
return this;
}

/**
* Send the fields to be highlighted using a syntax that is specific about the order in which they should be highlighted.
* @return this for chaining
*/
public HighlightBuilder useExplicitFieldOrder(boolean useExplicitFieldOrder) {
this.useExplicitFieldOrder = useExplicitFieldOrder;
return this;
}

@Override
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
builder.startObject("highlight");
Expand Down Expand Up @@ -347,8 +358,15 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws
builder.field("force_source", forceSource);
}
if (fields != null) {
builder.startObject("fields");
if (useExplicitFieldOrder) {
builder.startArray("fields");
} else {
builder.startObject("fields");
}
for (Field field : fields) {
if (useExplicitFieldOrder) {
builder.startObject();
}
builder.startObject(field.name());
if (field.preTags != null) {
builder.field("pre_tags", field.preTags);
Expand Down Expand Up @@ -406,10 +424,16 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws
}

builder.endObject();
if (useExplicitFieldOrder) {
builder.endObject();
}
}
if (useExplicitFieldOrder) {
builder.endArray();
} else {
builder.endObject();
}
builder.endObject();
}

builder.endObject();
return builder;
}
Expand Down
Expand Up @@ -28,6 +28,7 @@
import org.elasticsearch.search.SearchParseException;
import org.elasticsearch.search.internal.SearchContext;

import java.io.IOException;
import java.util.List;
import java.util.Set;

Expand Down Expand Up @@ -92,6 +93,24 @@ public void parse(XContentParser parser, SearchContext context) throws Exception
postTagsList.add(parser.text());
}
globalOptionsBuilder.postTags(postTagsList.toArray(new String[postTagsList.size()]));
} else if ("fields".equals(topLevelFieldName)) {
while ((token = parser.nextToken()) != XContentParser.Token.END_ARRAY) {
if (token == XContentParser.Token.START_OBJECT) {
String highlightFieldName = null;
while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
if (token == XContentParser.Token.FIELD_NAME) {
if (highlightFieldName != null) {
throw new SearchParseException(context, "If highlighter fields is an array it must contain objects containing a single field");
}
highlightFieldName = parser.currentName();
} else if (token == XContentParser.Token.START_OBJECT) {
fieldsOptions.add(Tuple.tuple(highlightFieldName, parseFields(parser, context)));
}
}
} else {
throw new SearchParseException(context, "If highlighter fields is an array it must contain objects containing a single field");
}
}
}
} else if (token.isValue()) {
if ("order".equals(topLevelFieldName)) {
Expand Down Expand Up @@ -141,73 +160,7 @@ public void parse(XContentParser parser, SearchContext context) throws Exception
if (token == XContentParser.Token.FIELD_NAME) {
highlightFieldName = parser.currentName();
} else if (token == XContentParser.Token.START_OBJECT) {
SearchContextHighlight.FieldOptions.Builder fieldOptionsBuilder = new SearchContextHighlight.FieldOptions.Builder();
String fieldName = null;
while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
if (token == XContentParser.Token.FIELD_NAME) {
fieldName = parser.currentName();
} else if (token == XContentParser.Token.START_ARRAY) {
if ("pre_tags".equals(fieldName) || "preTags".equals(fieldName)) {
List<String> preTagsList = Lists.newArrayList();
while ((token = parser.nextToken()) != XContentParser.Token.END_ARRAY) {
preTagsList.add(parser.text());
}
fieldOptionsBuilder.preTags(preTagsList.toArray(new String[preTagsList.size()]));
} else if ("post_tags".equals(fieldName) || "postTags".equals(fieldName)) {
List<String> postTagsList = Lists.newArrayList();
while ((token = parser.nextToken()) != XContentParser.Token.END_ARRAY) {
postTagsList.add(parser.text());
}
fieldOptionsBuilder.postTags(postTagsList.toArray(new String[postTagsList.size()]));
} else if ("matched_fields".equals(fieldName) || "matchedFields".equals(fieldName)) {
Set<String> matchedFields = Sets.newHashSet();
while ((token = parser.nextToken()) != XContentParser.Token.END_ARRAY) {
matchedFields.add(parser.text());
}
fieldOptionsBuilder.matchedFields(matchedFields);
}
} else if (token.isValue()) {
if ("fragment_size".equals(fieldName) || "fragmentSize".equals(fieldName)) {
fieldOptionsBuilder.fragmentCharSize(parser.intValue());
} else if ("number_of_fragments".equals(fieldName) || "numberOfFragments".equals(fieldName)) {
fieldOptionsBuilder.numberOfFragments(parser.intValue());
} else if ("fragment_offset".equals(fieldName) || "fragmentOffset".equals(fieldName)) {
fieldOptionsBuilder.fragmentOffset(parser.intValue());
} else if ("highlight_filter".equals(fieldName) || "highlightFilter".equals(fieldName)) {
fieldOptionsBuilder.highlightFilter(parser.booleanValue());
} else if ("order".equals(fieldName)) {
fieldOptionsBuilder.scoreOrdered("score".equals(parser.text()));
} else if ("require_field_match".equals(fieldName) || "requireFieldMatch".equals(fieldName)) {
fieldOptionsBuilder.requireFieldMatch(parser.booleanValue());
} else if ("boundary_max_scan".equals(topLevelFieldName) || "boundaryMaxScan".equals(topLevelFieldName)) {
fieldOptionsBuilder.boundaryMaxScan(parser.intValue());
} else if ("boundary_chars".equals(topLevelFieldName) || "boundaryChars".equals(topLevelFieldName)) {
char[] charsArr = parser.text().toCharArray();
Character[] boundaryChars = new Character[charsArr.length];
for (int i = 0; i < charsArr.length; i++) {
boundaryChars[i] = charsArr[i];
}
fieldOptionsBuilder.boundaryChars(boundaryChars);
} else if ("type".equals(fieldName)) {
fieldOptionsBuilder.highlighterType(parser.text());
} else if ("fragmenter".equals(fieldName)) {
fieldOptionsBuilder.fragmenter(parser.text());
} else if ("no_match_size".equals(fieldName) || "noMatchSize".equals(fieldName)) {
fieldOptionsBuilder.noMatchSize(parser.intValue());
} else if ("force_source".equals(fieldName) || "forceSource".equals(fieldName)) {
fieldOptionsBuilder.forceSource(parser.booleanValue());
} else if ("phrase_limit".equals(fieldName) || "phraseLimit".equals(fieldName)) {
fieldOptionsBuilder.phraseLimit(parser.intValue());
}
} else if (token == XContentParser.Token.START_OBJECT) {
if ("highlight_query".equals(fieldName) || "highlightQuery".equals(fieldName)) {
fieldOptionsBuilder.highlightQuery(context.queryParserService().parse(parser).query());
} else if ("options".equals(fieldName)) {
fieldOptionsBuilder.options(parser.map());
}
}
}
fieldsOptions.add(Tuple.tuple(highlightFieldName, fieldOptionsBuilder));
fieldsOptions.add(Tuple.tuple(highlightFieldName, parseFields(parser, context)));
}
}
} else if ("highlight_query".equals(topLevelFieldName) || "highlightQuery".equals(topLevelFieldName)) {
Expand All @@ -229,4 +182,76 @@ public void parse(XContentParser parser, SearchContext context) throws Exception

context.highlight(new SearchContextHighlight(fields));
}

private SearchContextHighlight.FieldOptions.Builder parseFields(XContentParser parser, SearchContext context) throws IOException {
XContentParser.Token token;

SearchContextHighlight.FieldOptions.Builder fieldOptionsBuilder = new SearchContextHighlight.FieldOptions.Builder();
String fieldName = null;
while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
if (token == XContentParser.Token.FIELD_NAME) {
fieldName = parser.currentName();
} else if (token == XContentParser.Token.START_ARRAY) {
if ("pre_tags".equals(fieldName) || "preTags".equals(fieldName)) {
List<String> preTagsList = Lists.newArrayList();
while ((token = parser.nextToken()) != XContentParser.Token.END_ARRAY) {
preTagsList.add(parser.text());
}
fieldOptionsBuilder.preTags(preTagsList.toArray(new String[preTagsList.size()]));
} else if ("post_tags".equals(fieldName) || "postTags".equals(fieldName)) {
List<String> postTagsList = Lists.newArrayList();
while ((token = parser.nextToken()) != XContentParser.Token.END_ARRAY) {
postTagsList.add(parser.text());
}
fieldOptionsBuilder.postTags(postTagsList.toArray(new String[postTagsList.size()]));
} else if ("matched_fields".equals(fieldName) || "matchedFields".equals(fieldName)) {
Set<String> matchedFields = Sets.newHashSet();
while ((token = parser.nextToken()) != XContentParser.Token.END_ARRAY) {
matchedFields.add(parser.text());
}
fieldOptionsBuilder.matchedFields(matchedFields);
}
} else if (token.isValue()) {
if ("fragment_size".equals(fieldName) || "fragmentSize".equals(fieldName)) {
fieldOptionsBuilder.fragmentCharSize(parser.intValue());
} else if ("number_of_fragments".equals(fieldName) || "numberOfFragments".equals(fieldName)) {
fieldOptionsBuilder.numberOfFragments(parser.intValue());
} else if ("fragment_offset".equals(fieldName) || "fragmentOffset".equals(fieldName)) {
fieldOptionsBuilder.fragmentOffset(parser.intValue());
} else if ("highlight_filter".equals(fieldName) || "highlightFilter".equals(fieldName)) {
fieldOptionsBuilder.highlightFilter(parser.booleanValue());
} else if ("order".equals(fieldName)) {
fieldOptionsBuilder.scoreOrdered("score".equals(parser.text()));
} else if ("require_field_match".equals(fieldName) || "requireFieldMatch".equals(fieldName)) {
fieldOptionsBuilder.requireFieldMatch(parser.booleanValue());
} else if ("boundary_max_scan".equals(fieldName) || "boundaryMaxScan".equals(fieldName)) {
fieldOptionsBuilder.boundaryMaxScan(parser.intValue());
} else if ("boundary_chars".equals(fieldName) || "boundaryChars".equals(fieldName)) {
char[] charsArr = parser.text().toCharArray();
Character[] boundaryChars = new Character[charsArr.length];
for (int i = 0; i < charsArr.length; i++) {
boundaryChars[i] = charsArr[i];
}
fieldOptionsBuilder.boundaryChars(boundaryChars);
} else if ("type".equals(fieldName)) {
fieldOptionsBuilder.highlighterType(parser.text());
} else if ("fragmenter".equals(fieldName)) {
fieldOptionsBuilder.fragmenter(parser.text());
} else if ("no_match_size".equals(fieldName) || "noMatchSize".equals(fieldName)) {
fieldOptionsBuilder.noMatchSize(parser.intValue());
} else if ("force_source".equals(fieldName) || "forceSource".equals(fieldName)) {
fieldOptionsBuilder.forceSource(parser.booleanValue());
} else if ("phrase_limit".equals(fieldName) || "phraseLimit".equals(fieldName)) {
fieldOptionsBuilder.phraseLimit(parser.intValue());
}
} else if (token == XContentParser.Token.START_OBJECT) {
if ("highlight_query".equals(fieldName) || "highlightQuery".equals(fieldName)) {
fieldOptionsBuilder.highlightQuery(context.queryParserService().parse(parser).query());
} else if ("options".equals(fieldName)) {
fieldOptionsBuilder.options(parser.map());
}
}
}
return fieldOptionsBuilder;
}
}
Expand Up @@ -35,7 +35,7 @@ public class SearchContextHighlight {

public SearchContextHighlight(Collection<Field> fields) {
assert fields != null;
this.fields = Maps.newHashMap();
this.fields = new LinkedHashMap<String, Field>(fields.size());
for (Field field : fields) {
this.fields.put(field.field, field);
}
Expand Down
Expand Up @@ -23,6 +23,7 @@
import org.elasticsearch.common.text.Text;

import java.util.List;
import java.util.Locale;
import java.util.Map;

/**
Expand All @@ -38,9 +39,24 @@ public String[] names() {
@Override
public HighlightField highlight(HighlighterContext highlighterContext) {
SearchContextHighlight.Field field = highlighterContext.field;
CacheEntry cacheEntry = (CacheEntry) highlighterContext.hitContext.cache().get("test-custom");
if (cacheEntry == null) {
cacheEntry = new CacheEntry();
highlighterContext.hitContext.cache().put("test-custom", cacheEntry);
cacheEntry.docId = highlighterContext.hitContext.docId();
cacheEntry.position = 1;
} else {
if (cacheEntry.docId == highlighterContext.hitContext.docId()) {
cacheEntry.position++;
} else {
cacheEntry.docId = highlighterContext.hitContext.docId();
cacheEntry.position = 1;
}
}

List<Text> responses = Lists.newArrayList();
responses.add(new StringText("standard response"));
responses.add(new StringText(String.format(Locale.ENGLISH, "standard response for %s at position %s", field.field(),
cacheEntry.position)));

if (field.fieldOptions().options() != null) {
for (Map.Entry<String, Object> entry : field.fieldOptions().options().entrySet()) {
Expand All @@ -50,4 +66,9 @@ public HighlightField highlight(HighlighterContext highlighterContext) {

return new HighlightField(highlighterContext.fieldName, responses.toArray(new Text[]{}));
}

private static class CacheEntry {
private int position;
private int docId;
}
}