diff --git a/rest-api-spec/src/main/resources/rest-api-spec/test/suggest/50_completion_with_multi_fields.yml b/rest-api-spec/src/main/resources/rest-api-spec/test/suggest/50_completion_with_multi_fields.yml new file mode 100644 index 0000000000000..42207a073fb1a --- /dev/null +++ b/rest-api-spec/src/main/resources/rest-api-spec/test/suggest/50_completion_with_multi_fields.yml @@ -0,0 +1,299 @@ + +--- +"Search by suggestion and by keyword sub-field should work": + + - skip: + version: " - 6.99.99" + reason: "Search by suggestion with multi-fields was introduced 7.0.0" + + - do: + indices.create: + index: completion_with_sub_keyword + body: + mappings: + test: + "properties": + "suggest_1": + "type" : "completion" + "fields": + "text_raw": + "type" : "keyword" + + - do: + index: + index: completion_with_sub_keyword + type: test + id: 1 + body: + suggest_1: "bar" + + - do: + index: + index: completion_with_sub_keyword + type: test + id: 2 + body: + suggest_1: "baz" + + - do: + indices.refresh: {} + + - do: + search: + index: completion_with_sub_keyword + body: + suggest: + result: + text: "b" + completion: + field: suggest_1 + + - length: { suggest.result: 1 } + - length: { suggest.result.0.options: 2 } + + + - do: + search: + index: completion_with_sub_keyword + body: + query: { term: { suggest_1.text_raw: "bar" }} + + - match: { hits.total: 1 } + + + +--- +"Search by suggestion on sub field should work": + + - skip: + version: " - 6.99.99" + reason: "Search by suggestion with multi-fields was introduced 7.0.0" + + - do: + indices.create: + index: completion_with_sub_completion + body: + mappings: + test: + "properties": + "suggest_1": + "type": "completion" + "fields": + "suggest_2": + "type": "completion" + + - do: + index: + index: completion_with_sub_completion + type: test + id: 1 + body: + suggest_1: "bar" + + - do: + index: + index: completion_with_sub_completion + type: test + id: 2 + body: + suggest_1: "baz" + + - do: + indices.refresh: {} + + - do: + search: + index: completion_with_sub_completion + body: + suggest: + result: + text: "b" + completion: + field: suggest_1.suggest_2 + + - length: { suggest.result: 1 } + - length: { suggest.result.0.options: 2 } + +--- +"Search by suggestion on sub field with context should work": + + - skip: + version: " - 6.99.99" + reason: "Search by suggestion with multi-fields was introduced 7.0.0" + + - do: + indices.create: + index: completion_with_context + body: + mappings: + test: + "properties": + "suggest_1": + "type": "completion" + "contexts": + - + "name": "color" + "type": "category" + "fields": + "suggest_2": + "type": "completion" + "contexts": + - + "name": "color" + "type": "category" + + + - do: + index: + index: completion_with_context + type: test + id: 1 + body: + suggest_1: + input: "foo red" + contexts: + color: "red" + + - do: + index: + index: completion_with_context + type: test + id: 2 + body: + suggest_1: + input: "foo blue" + contexts: + color: "blue" + + - do: + indices.refresh: {} + + - do: + search: + index: completion_with_context + body: + suggest: + result: + prefix: "foo" + completion: + field: suggest_1.suggest_2 + contexts: + color: "red" + + - length: { suggest.result: 1 } + - length: { suggest.result.0.options: 1 } + - match: { suggest.result.0.options.0.text: "foo red" } + + +--- +"Search by suggestion on sub field with weight should work": + + - skip: + version: " - 6.99.99" + reason: "Search by suggestion with multi-fields was introduced 7.0.0" + + - do: + indices.create: + index: completion_with_weight + body: + mappings: + test: + "properties": + "suggest_1": + "type": "completion" + "fields": + "suggest_2": + "type": "completion" + + - do: + index: + index: completion_with_weight + type: test + id: 1 + body: + suggest_1: + input: "bar" + weight: 2 + + - do: + index: + index: completion_with_weight + type: test + id: 2 + body: + suggest_1: + input: "baz" + weight: 3 + + - do: + indices.refresh: {} + + - do: + search: + index: completion_with_weight + body: + suggest: + result: + text: "b" + completion: + field: suggest_1.suggest_2 + + - length: { suggest.result: 1 } + - length: { suggest.result.0.options: 2 } + - match: { suggest.result.0.options.0.text: "baz" } + - match: { suggest.result.0.options.1.text: "bar" } + +--- +"Search by suggestion on geofield-hash on sub field should work": + + - skip: + version: " - 6.99.99" + reason: "Search by suggestion with multi-fields was introduced 7.0.0" + + - do: + indices.create: + index: geofield_with_completion + body: + mappings: + test: + "properties": + "geofield": + "type": "geo_point" + "fields": + "suggest_1": + "type": "completion" + + - do: + index: + index: geofield_with_completion + type: test + id: 1 + body: + geofield: "hgjhrwysvqw7" + #41.12,-72.34,12 + + - do: + index: + index: geofield_with_completion + type: test + id: 1 + body: + geofield: "hgm4psywmkn7" + #41.12,-71.34,12 + + - do: + indices.refresh: {} + + - do: + search: + index: geofield_with_completion + body: + suggest: + result: + prefix: "hgm" + completion: + field: geofield.suggest_1 + + + - length: { suggest.result: 1 } + - length: { suggest.result.0.options: 1 } diff --git a/server/src/main/java/org/elasticsearch/index/mapper/CompletionFieldMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/CompletionFieldMapper.java index db04e64b164df..0635cdd066139 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/CompletionFieldMapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/CompletionFieldMapper.java @@ -436,8 +436,9 @@ public void parse(ParseContext context) throws IOException { Token token = parser.currentToken(); Map inputMap = new HashMap<>(1); - // ignore null values - if (token == Token.VALUE_NULL) { + if (context.externalValueSet()) { + inputMap = getInputMapFromExternalValue(context); + } else if (token == Token.VALUE_NULL) { // ignore null values return; } else if (token == Token.START_ARRAY) { while ((token = parser.nextToken()) != Token.END_ARRAY) { @@ -471,12 +472,33 @@ public void parse(ParseContext context) throws IOException { context.doc().add(new SuggestField(fieldType().name(), input, metaData.weight)); } } + List fields = new ArrayList<>(1); createFieldNamesField(context, fields); for (IndexableField field : fields) { context.doc().add(field); } - multiFields.parse(this, context); + + for (CompletionInputMetaData metaData: inputMap.values()) { + ParseContext externalValueContext = context.createExternalValueContext(metaData); + multiFields.parse(this, externalValueContext); + } + } + + private Map getInputMapFromExternalValue(ParseContext context) { + Map inputMap; + if (isExternalValueOfClass(context, CompletionInputMetaData.class)) { + CompletionInputMetaData inputAndMeta = (CompletionInputMetaData) context.externalValue(); + inputMap = Collections.singletonMap(inputAndMeta.input, inputAndMeta); + } else { + String fieldName = context.externalValue().toString(); + inputMap = Collections.singletonMap(fieldName, new CompletionInputMetaData(fieldName, Collections.emptyMap(), 1)); + } + return inputMap; + } + + private boolean isExternalValueOfClass(ParseContext context, Class clazz) { + return context.externalValue().getClass().equals(clazz); } /** @@ -487,7 +509,7 @@ public void parse(ParseContext context) throws IOException { private void parse(ParseContext parseContext, Token token, XContentParser parser, Map inputMap) throws IOException { String currentFieldName = null; if (token == Token.VALUE_STRING) { - inputMap.put(parser.text(), new CompletionInputMetaData(Collections.>emptyMap(), 1)); + inputMap.put(parser.text(), new CompletionInputMetaData(parser.text(), Collections.emptyMap(), 1)); } else if (token == Token.START_OBJECT) { Set inputs = new HashSet<>(); int weight = 1; @@ -561,7 +583,7 @@ private void parse(ParseContext parseContext, Token token, XContentParser parser } for (String input : inputs) { if (inputMap.containsKey(input) == false || inputMap.get(input).weight < weight) { - inputMap.put(input, new CompletionInputMetaData(contextsMap, weight)); + inputMap.put(input, new CompletionInputMetaData(input, contextsMap, weight)); } } } else { @@ -570,13 +592,20 @@ private void parse(ParseContext parseContext, Token token, XContentParser parser } static class CompletionInputMetaData { + public final String input; public final Map> contexts; public final int weight; - CompletionInputMetaData(Map> contexts, int weight) { + CompletionInputMetaData(String input, Map> contexts, int weight) { + this.input = input; this.contexts = contexts; this.weight = weight; } + + @Override + public String toString() { + return input; + } } @Override diff --git a/server/src/test/java/org/elasticsearch/index/mapper/CompletionFieldMapperTests.java b/server/src/test/java/org/elasticsearch/index/mapper/CompletionFieldMapperTests.java index e3739eed3362d..cc09ae16c0578 100644 --- a/server/src/test/java/org/elasticsearch/index/mapper/CompletionFieldMapperTests.java +++ b/server/src/test/java/org/elasticsearch/index/mapper/CompletionFieldMapperTests.java @@ -18,9 +18,11 @@ */ package org.elasticsearch.index.mapper; +import org.apache.lucene.document.SortedSetDocValuesField; import org.apache.lucene.index.IndexableField; import org.apache.lucene.search.Query; import org.apache.lucene.search.suggest.document.CompletionAnalyzer; +import org.apache.lucene.search.suggest.document.ContextSuggestField; import org.apache.lucene.search.suggest.document.FuzzyCompletionQuery; import org.apache.lucene.search.suggest.document.PrefixCompletionQuery; import org.apache.lucene.search.suggest.document.RegexCompletionQuery; @@ -42,11 +44,18 @@ import org.elasticsearch.index.IndexService; import org.elasticsearch.index.analysis.NamedAnalyzer; import org.elasticsearch.test.ESSingleNodeTestCase; +import org.hamcrest.FeatureMatcher; +import org.hamcrest.Matcher; +import org.hamcrest.Matchers; +import org.hamcrest.core.CombinableMatcher; import java.io.IOException; import java.util.Map; +import java.util.function.Function; import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder; +import static org.hamcrest.Matchers.arrayContainingInAnyOrder; +import static org.hamcrest.Matchers.arrayWithSize; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.instanceOf; @@ -182,6 +191,328 @@ public void testParsingFailure() throws Exception { assertEquals("failed to parse [completion]: expected text or object, but got VALUE_NUMBER", e.getCause().getMessage()); } + public void testKeywordWithSubCompletionAndContext() throws Exception { + String mapping = Strings.toString(jsonBuilder().startObject().startObject("type1") + .startObject("properties") + .startObject("keywordfield") + .field("type", "keyword") + .startObject("fields") + .startObject("subsuggest") + .field("type", "completion") + .startArray("contexts") + .startObject() + .field("name","place_type") + .field("type","category") + .field("path","cat") + .endObject() + .endArray() + .endObject() + .endObject() + .endObject().endObject() + .endObject().endObject() + ); + + DocumentMapper defaultMapper = createIndex("test").mapperService().documentMapperParser().parse("type1", new CompressedXContent(mapping)); + + ParsedDocument parsedDocument = defaultMapper.parse(SourceToParse.source("test", "type1", "1", BytesReference + .bytes(XContentFactory.jsonBuilder() + .startObject() + .array("keywordfield", "key1", "key2", "key3") + .endObject()), + XContentType.JSON)); + + ParseContext.Document indexableFields = parsedDocument.rootDoc(); + + assertThat(indexableFields.getFields("keywordfield"), arrayContainingInAnyOrder( + keywordField("key1"), + sortedSetDocValuesField("key1"), + keywordField("key2"), + sortedSetDocValuesField("key2"), + keywordField("key3"), + sortedSetDocValuesField("key3") + )); + assertThat(indexableFields.getFields("keywordfield.subsuggest"), arrayContainingInAnyOrder( + contextSuggestField("key1"), + contextSuggestField("key2"), + contextSuggestField("key3") + )); + } + + public void testCompletionWithContextAndSubCompletion() throws Exception { + String mapping = Strings.toString(jsonBuilder().startObject().startObject("type1") + .startObject("properties") + .startObject("suggest") + .field("type", "completion") + .startArray("contexts") + .startObject() + .field("name","place_type") + .field("type","category") + .field("path","cat") + .endObject() + .endArray() + .startObject("fields") + .startObject("subsuggest") + .field("type", "completion") + .startArray("contexts") + .startObject() + .field("name","place_type") + .field("type","category") + .field("path","cat") + .endObject() + .endArray() + .endObject() + .endObject() + .endObject().endObject() + .endObject().endObject() + ); + + DocumentMapper defaultMapper = createIndex("test").mapperService().documentMapperParser().parse("type1", new CompressedXContent(mapping)); + + ParsedDocument parsedDocument = defaultMapper.parse(SourceToParse.source("test", "type1", "1", BytesReference + .bytes(XContentFactory.jsonBuilder() + .startObject() + .startObject("suggest") + .array("input","timmy","starbucks") + .startObject("contexts") + .array("place_type","cafe","food") + .endObject() + .field("weight", 3) + .endObject() + .endObject()), + XContentType.JSON)); + + ParseContext.Document indexableFields = parsedDocument.rootDoc(); + assertThat(indexableFields.getFields("suggest"), arrayContainingInAnyOrder( + contextSuggestField("timmy"), + contextSuggestField("starbucks") + )); + assertThat(indexableFields.getFields("suggest.subsuggest"), arrayContainingInAnyOrder( + contextSuggestField("timmy"), + contextSuggestField("starbucks") + )); + //unable to assert about context, covered in a REST test + } + + public void testCompletionWithContextAndSubCompletionIndexByPath() throws Exception { + String mapping = Strings.toString(jsonBuilder().startObject().startObject("type1") + .startObject("properties") + .startObject("suggest") + .field("type", "completion") + .startArray("contexts") + .startObject() + .field("name","place_type") + .field("type","category") + .field("path","cat") + .endObject() + .endArray() + .startObject("fields") + .startObject("subsuggest") + .field("type", "completion") + .startArray("contexts") + .startObject() + .field("name","place_type") + .field("type","category") + .field("path","cat") + .endObject() + .endArray() + .endObject() + .endObject() + .endObject().endObject() + .endObject().endObject() + ); + + DocumentMapper defaultMapper = createIndex("test").mapperService().documentMapperParser().parse("type1", new CompressedXContent(mapping)); + + ParsedDocument parsedDocument = defaultMapper.parse(SourceToParse.source("test", "type1", "1", BytesReference + .bytes(XContentFactory.jsonBuilder() + .startObject() + .array("suggest", "timmy","starbucks") + .array("cat","cafe","food") + .endObject()), + XContentType.JSON)); + + ParseContext.Document indexableFields = parsedDocument.rootDoc(); + assertThat(indexableFields.getFields("suggest"), arrayContainingInAnyOrder( + contextSuggestField("timmy"), + contextSuggestField("starbucks") + )); + assertThat(indexableFields.getFields("suggest.subsuggest"), arrayContainingInAnyOrder( + contextSuggestField("timmy"), + contextSuggestField("starbucks") + )); + //unable to assert about context, covered in a REST test + } + + + public void testKeywordWithSubCompletionAndStringInsert() throws Exception { + String mapping = Strings.toString(jsonBuilder().startObject().startObject("type1") + .startObject("properties").startObject("geofield") + .field("type", "geo_point") + .startObject("fields") + .startObject("analyzed") + .field("type", "completion") + .endObject() + .endObject() + .endObject().endObject() + .endObject().endObject() + ); + + DocumentMapper defaultMapper = createIndex("test").mapperService().documentMapperParser().parse("type1", new CompressedXContent(mapping)); + + ParsedDocument parsedDocument = defaultMapper.parse(SourceToParse.source("test", "type1", "1", BytesReference + .bytes(XContentFactory.jsonBuilder() + .startObject() + .field("geofield", "drm3btev3e86")//"41.12,-71.34" + .endObject()), + XContentType.JSON)); + + ParseContext.Document indexableFields = parsedDocument.rootDoc(); + assertThat(indexableFields.getFields("geofield"), arrayWithSize(2)); + assertThat(indexableFields.getFields("geofield.analyzed"), arrayContainingInAnyOrder( + suggestField("drm3btev3e86") + )); + //unable to assert about geofield content, covered in a REST test + } + + public void testCompletionTypeWithSubCompletionFieldAndStringInsert() throws Exception { + String mapping = Strings.toString(jsonBuilder().startObject().startObject("type1") + .startObject("properties").startObject("suggest") + .field("type", "completion") + .startObject("fields") + .startObject("subsuggest") + .field("type", "completion") + .endObject() + .endObject() + .endObject().endObject() + .endObject().endObject() + ); + + DocumentMapper defaultMapper = createIndex("test").mapperService().documentMapperParser().parse("type1", new CompressedXContent(mapping)); + + ParsedDocument parsedDocument = defaultMapper.parse(SourceToParse.source("test", "type1", "1", BytesReference + .bytes(XContentFactory.jsonBuilder() + .startObject() + .field("suggest", "suggestion") + .endObject()), + XContentType.JSON)); + + ParseContext.Document indexableFields = parsedDocument.rootDoc(); + assertThat(indexableFields.getFields("suggest"), arrayContainingInAnyOrder( + suggestField("suggestion") + )); + assertThat(indexableFields.getFields("suggest.subsuggest"), arrayContainingInAnyOrder( + suggestField("suggestion") + )); + } + + public void testCompletionTypeWithSubCompletionFieldAndObjectInsert() throws Exception { + String mapping = Strings.toString(jsonBuilder().startObject().startObject("type1") + .startObject("properties").startObject("completion") + .field("type", "completion") + .startObject("fields") + .startObject("analyzed") + .field("type", "completion") + .endObject() + .endObject() + .endObject().endObject() + .endObject().endObject() + ); + + DocumentMapper defaultMapper = createIndex("test").mapperService().documentMapperParser().parse("type1", new CompressedXContent(mapping)); + + ParsedDocument parsedDocument = defaultMapper.parse(SourceToParse.source("test", "type1", "1", BytesReference + .bytes(XContentFactory.jsonBuilder() + .startObject() + .startObject("completion") + .array("input","New York", "NY") + .field("weight",34) + .endObject() + .endObject()), + XContentType.JSON)); + + ParseContext.Document indexableFields = parsedDocument.rootDoc(); + assertThat(indexableFields.getFields("completion"), arrayContainingInAnyOrder( + suggestField("New York"), + suggestField("NY") + )); + assertThat(indexableFields.getFields("completion.analyzed"), arrayContainingInAnyOrder( + suggestField("New York"), + suggestField("NY") + )); + //unable to assert about weight, covered in a REST test + } + + public void testCompletionTypeWithSubKeywordFieldAndObjectInsert() throws Exception { + String mapping = Strings.toString(jsonBuilder().startObject().startObject("type1") + .startObject("properties").startObject("completion") + .field("type", "completion") + .startObject("fields") + .startObject("analyzed") + .field("type", "keyword") + .endObject() + .endObject() + .endObject().endObject() + .endObject().endObject() + ); + + DocumentMapper defaultMapper = createIndex("test").mapperService().documentMapperParser().parse("type1", new CompressedXContent(mapping)); + + ParsedDocument parsedDocument = defaultMapper.parse(SourceToParse.source("test", "type1", "1", BytesReference + .bytes(XContentFactory.jsonBuilder() + .startObject() + .startObject("completion") + .array("input","New York", "NY") + .field("weight",34) + .endObject() + .endObject()), + XContentType.JSON)); + + ParseContext.Document indexableFields = parsedDocument.rootDoc(); + assertThat(indexableFields.getFields("completion"), arrayContainingInAnyOrder( + suggestField("New York"), + suggestField("NY") + )); + assertThat(indexableFields.getFields("completion.analyzed"), arrayContainingInAnyOrder( + keywordField("New York"), + sortedSetDocValuesField("New York"), + keywordField("NY"), + sortedSetDocValuesField("NY") + )); + //unable to assert about weight, covered in a REST test + } + + public void testCompletionTypeWithSubKeywordFieldAndStringInsert() throws Exception { + String mapping = Strings.toString(jsonBuilder().startObject().startObject("type1") + .startObject("properties").startObject("completion") + .field("type", "completion") + .startObject("fields") + .startObject("analyzed") + .field("type", "keyword") + .endObject() + .endObject() + .endObject().endObject() + .endObject().endObject() + ); + + DocumentMapper defaultMapper = createIndex("test").mapperService().documentMapperParser().parse("type1", new CompressedXContent(mapping)); + + ParsedDocument parsedDocument = defaultMapper.parse(SourceToParse.source("test", "type1", "1", BytesReference + .bytes(XContentFactory.jsonBuilder() + .startObject() + .field("completion", "suggestion") + .endObject()), + XContentType.JSON)); + + ParseContext.Document indexableFields = parsedDocument.rootDoc(); + assertThat(indexableFields.getFields("completion"), arrayContainingInAnyOrder( + suggestField("suggestion") + )); + assertThat(indexableFields.getFields("completion.analyzed"), arrayContainingInAnyOrder( + keywordField("suggestion"), + sortedSetDocValuesField("suggestion") + )); + } + public void testParsingMultiValued() throws Exception { String mapping = Strings.toString(jsonBuilder().startObject().startObject("type1") .startObject("properties").startObject("completion") @@ -199,7 +530,10 @@ public void testParsingMultiValued() throws Exception { .endObject()), XContentType.JSON)); IndexableField[] fields = parsedDocument.rootDoc().getFields(fieldMapper.name()); - assertSuggestFields(fields, 2); + assertThat(fields, arrayContainingInAnyOrder( + suggestField("suggestion1"), + suggestField("suggestion2") + )); } public void testParsingWithWeight() throws Exception { @@ -222,7 +556,9 @@ public void testParsingWithWeight() throws Exception { .endObject()), XContentType.JSON)); IndexableField[] fields = parsedDocument.rootDoc().getFields(fieldMapper.name()); - assertSuggestFields(fields, 1); + assertThat(fields, arrayContainingInAnyOrder( + suggestField("suggestion") + )); } public void testParsingMultiValueWithWeight() throws Exception { @@ -245,7 +581,11 @@ public void testParsingMultiValueWithWeight() throws Exception { .endObject()), XContentType.JSON)); IndexableField[] fields = parsedDocument.rootDoc().getFields(fieldMapper.name()); - assertSuggestFields(fields, 3); + assertThat(fields, arrayContainingInAnyOrder( + suggestField("suggestion1"), + suggestField("suggestion2"), + suggestField("suggestion3") + )); } public void testParsingWithGeoFieldAlias() throws Exception { @@ -318,7 +658,11 @@ public void testParsingFull() throws Exception { .endObject()), XContentType.JSON)); IndexableField[] fields = parsedDocument.rootDoc().getFields(fieldMapper.name()); - assertSuggestFields(fields, 3); + assertThat(fields, arrayContainingInAnyOrder( + suggestField("suggestion1"), + suggestField("suggestion2"), + suggestField("suggestion3") + )); } public void testParsingMixed() throws Exception { @@ -351,7 +695,14 @@ public void testParsingMixed() throws Exception { .endObject()), XContentType.JSON)); IndexableField[] fields = parsedDocument.rootDoc().getFields(fieldMapper.name()); - assertSuggestFields(fields, 6); + assertThat(fields, arrayContainingInAnyOrder( + suggestField("suggestion1"), + suggestField("suggestion2"), + suggestField("suggestion3"), + suggestField("suggestion4"), + suggestField("suggestion5"), + suggestField("suggestion6") + )); } public void testNonContextEnabledParsingWithContexts() throws Exception { @@ -508,9 +859,13 @@ public void testRegexQueryType() throws Exception { } private static void assertSuggestFields(IndexableField[] fields, int expected) { + assertFieldsOfType(fields, SuggestField.class, expected); + } + + private static void assertFieldsOfType(IndexableField[] fields, Class clazz, int expected) { int actualFieldCount = 0; for (IndexableField field : fields) { - if (field instanceof SuggestField) { + if (clazz.isInstance(field)) { actualFieldCount++; } } @@ -529,4 +884,33 @@ public void testEmptyName() throws IOException { ); assertThat(e.getMessage(), containsString("name cannot be empty string")); } + + private Matcher suggestField(String value) { + return Matchers.allOf(hasProperty(IndexableField::stringValue, equalTo(value)), + Matchers.instanceOf(SuggestField.class)); + } + + private Matcher contextSuggestField(String value) { + return Matchers.allOf(hasProperty(IndexableField::stringValue, equalTo(value)), + Matchers.instanceOf(ContextSuggestField.class)); + } + + private CombinableMatcher sortedSetDocValuesField(String value) { + return Matchers.both(hasProperty(IndexableField::binaryValue, equalTo(new BytesRef(value)))) + .and(Matchers.instanceOf(SortedSetDocValuesField.class)); + } + + private CombinableMatcher keywordField(String value) { + return Matchers.both(hasProperty(IndexableField::binaryValue, equalTo(new BytesRef(value)))) + .and(hasProperty(IndexableField::fieldType, Matchers.instanceOf(KeywordFieldMapper.KeywordFieldType.class))); + } + + private Matcher hasProperty(Function property, Matcher valueMatcher) { + return new FeatureMatcher(valueMatcher, "object with", property.toString()) { + @Override + protected V featureValueOf(T actual) { + return property.apply(actual); + } + }; + } }