From ec421974b960854609f4f0d0b131a88f9f177a6e Mon Sep 17 00:00:00 2001 From: Nilabh Sagar Date: Thu, 13 Apr 2017 12:13:29 +0530 Subject: [PATCH] Allow different data types for category in Context suggester (#23491) The "category" in context suggester could be String, Number or Boolean. However with the changes in version 5 this is failing and only accepting String. This will have problem for existing users of Elasticsearch if they choose to migrate to higher version; as their existing Mapping and query will fail as mentioned in a bug #22358 This PR fixes the above mentioned issue and allows user to migrate seamlessly. Closes #22358 --- .../index/mapper/CompletionFieldMapper.java | 6 +- .../context/CategoryContextMapping.java | 13 +- .../context/CategoryQueryContext.java | 15 +- .../completion/context/ContextMapping.java | 7 +- .../CategoryContextMappingTests.java | 385 +++++++++++++++++- 5 files changed, 408 insertions(+), 18 deletions(-) diff --git a/core/src/main/java/org/elasticsearch/index/mapper/CompletionFieldMapper.java b/core/src/main/java/org/elasticsearch/index/mapper/CompletionFieldMapper.java index 3855489efe3..1ab84eda639 100644 --- a/core/src/main/java/org/elasticsearch/index/mapper/CompletionFieldMapper.java +++ b/core/src/main/java/org/elasticsearch/index/mapper/CompletionFieldMapper.java @@ -528,14 +528,10 @@ public class CompletionFieldMapper extends FieldMapper implements ArrayValueMapp if (currentToken == XContentParser.Token.FIELD_NAME) { fieldName = parser.currentName(); contextMapping = contextMappings.get(fieldName); - } else if (currentToken == XContentParser.Token.VALUE_STRING - || currentToken == XContentParser.Token.START_ARRAY - || currentToken == XContentParser.Token.START_OBJECT) { + } else { assert fieldName != null; assert !contextsMap.containsKey(fieldName); contextsMap.put(fieldName, contextMapping.parseContext(parseContext, parser)); - } else { - throw new IllegalArgumentException("contexts must be an object or an array , but was [" + currentToken + "]"); } } } else { diff --git a/core/src/main/java/org/elasticsearch/search/suggest/completion/context/CategoryContextMapping.java b/core/src/main/java/org/elasticsearch/search/suggest/completion/context/CategoryContextMapping.java index 150b7bf4f90..38e31ec92a4 100644 --- a/core/src/main/java/org/elasticsearch/search/suggest/completion/context/CategoryContextMapping.java +++ b/core/src/main/java/org/elasticsearch/search/suggest/completion/context/CategoryContextMapping.java @@ -107,21 +107,24 @@ public class CategoryContextMapping extends ContextMapping * */ @Override - public Set parseContext(ParseContext parseContext, XContentParser parser) throws IOException, ElasticsearchParseException { + public Set parseContext(ParseContext parseContext, XContentParser parser) + throws IOException, ElasticsearchParseException { final Set contexts = new HashSet<>(); Token token = parser.currentToken(); - if (token == Token.VALUE_STRING) { + if (token == Token.VALUE_STRING || token == Token.VALUE_NUMBER || token == Token.VALUE_BOOLEAN) { contexts.add(parser.text()); } else if (token == Token.START_ARRAY) { while ((token = parser.nextToken()) != Token.END_ARRAY) { - if (token == Token.VALUE_STRING) { + if (token == Token.VALUE_STRING || token == Token.VALUE_NUMBER || token == Token.VALUE_BOOLEAN) { contexts.add(parser.text()); } else { - throw new ElasticsearchParseException("context array must have string values"); + throw new ElasticsearchParseException( + "context array must have string, number or boolean values, but was [" + token + "]"); } } } else { - throw new ElasticsearchParseException("contexts must be a string or a list of strings"); + throw new ElasticsearchParseException( + "contexts must be a string, number or boolean or a list of string, number or boolean, but was [" + token + "]"); } return contexts; } diff --git a/core/src/main/java/org/elasticsearch/search/suggest/completion/context/CategoryQueryContext.java b/core/src/main/java/org/elasticsearch/search/suggest/completion/context/CategoryQueryContext.java index 59f59075bd3..51b740a3529 100644 --- a/core/src/main/java/org/elasticsearch/search/suggest/completion/context/CategoryQueryContext.java +++ b/core/src/main/java/org/elasticsearch/search/suggest/completion/context/CategoryQueryContext.java @@ -21,6 +21,7 @@ package org.elasticsearch.search.suggest.completion.context; import org.elasticsearch.ElasticsearchParseException; import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.ParsingException; import org.elasticsearch.common.xcontent.ObjectParser; import org.elasticsearch.common.xcontent.ToXContent; import org.elasticsearch.common.xcontent.XContentBuilder; @@ -98,7 +99,8 @@ public final class CategoryQueryContext implements ToXContent { private static ObjectParser CATEGORY_PARSER = new ObjectParser<>(NAME, null); static { - CATEGORY_PARSER.declareString(Builder::setCategory, new ParseField(CONTEXT_VALUE)); + CATEGORY_PARSER.declareField(Builder::setCategory, XContentParser::text, new ParseField(CONTEXT_VALUE), + ObjectParser.ValueType.VALUE); CATEGORY_PARSER.declareInt(Builder::setBoost, new ParseField(CONTEXT_BOOST)); CATEGORY_PARSER.declareBoolean(Builder::setPrefix, new ParseField(CONTEXT_PREFIX)); } @@ -108,11 +110,16 @@ public final class CategoryQueryContext implements ToXContent { XContentParser.Token token = parser.currentToken(); Builder builder = builder(); if (token == XContentParser.Token.START_OBJECT) { - CATEGORY_PARSER.parse(parser, builder, null); - } else if (token == XContentParser.Token.VALUE_STRING) { + try { + CATEGORY_PARSER.parse(parser, builder, null); + } catch(ParsingException e) { + throw new ElasticsearchParseException("category context must be a string, number or boolean"); + } + } else if (token == XContentParser.Token.VALUE_STRING || token == XContentParser.Token.VALUE_BOOLEAN + || token == XContentParser.Token.VALUE_NUMBER) { builder.setCategory(parser.text()); } else { - throw new ElasticsearchParseException("category context must be an object or string"); + throw new ElasticsearchParseException("category context must be an object, string, number or boolean"); } return builder.build(); } diff --git a/core/src/main/java/org/elasticsearch/search/suggest/completion/context/ContextMapping.java b/core/src/main/java/org/elasticsearch/search/suggest/completion/context/ContextMapping.java index f41273662a4..273138bbb79 100644 --- a/core/src/main/java/org/elasticsearch/search/suggest/completion/context/ContextMapping.java +++ b/core/src/main/java/org/elasticsearch/search/suggest/completion/context/ContextMapping.java @@ -109,13 +109,14 @@ public abstract class ContextMapping implements ToXContent List queryContexts = new ArrayList<>(); XContentParser parser = context.parser(); Token token = parser.nextToken(); - if (token == Token.START_OBJECT || token == Token.VALUE_STRING) { - queryContexts.add(fromXContent(context)); - } else if (token == Token.START_ARRAY) { + if (token == Token.START_ARRAY) { while (parser.nextToken() != Token.END_ARRAY) { queryContexts.add(fromXContent(context)); } + } else { + queryContexts.add(fromXContent(context)); } + return toInternalQueryContexts(queryContexts); } diff --git a/core/src/test/java/org/elasticsearch/search/suggest/completion/CategoryContextMappingTests.java b/core/src/test/java/org/elasticsearch/search/suggest/completion/CategoryContextMappingTests.java index 15c05a56226..8b4b01c8de8 100644 --- a/core/src/test/java/org/elasticsearch/search/suggest/completion/CategoryContextMappingTests.java +++ b/core/src/test/java/org/elasticsearch/search/suggest/completion/CategoryContextMappingTests.java @@ -23,6 +23,7 @@ import org.apache.lucene.document.Field; import org.apache.lucene.document.StringField; import org.apache.lucene.index.IndexableField; import org.apache.lucene.search.suggest.document.ContextSuggestField; +import org.elasticsearch.ElasticsearchParseException; import org.elasticsearch.common.compress.CompressedXContent; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentParser; @@ -31,6 +32,7 @@ import org.elasticsearch.common.xcontent.json.JsonXContent; import org.elasticsearch.index.mapper.DocumentMapper; import org.elasticsearch.index.mapper.FieldMapper; import org.elasticsearch.index.mapper.MappedFieldType; +import org.elasticsearch.index.mapper.MapperParsingException; import org.elasticsearch.index.mapper.ParseContext; import org.elasticsearch.index.mapper.ParsedDocument; import org.elasticsearch.index.mapper.SourceToParse; @@ -120,6 +122,103 @@ public class CategoryContextMappingTests extends ESSingleNodeTestCase { IndexableField[] fields = parsedDocument.rootDoc().getFields(completionFieldType.name()); assertContextSuggestFields(fields, 3); } + + public void testIndexingWithSimpleNumberContexts() throws Exception { + String mapping = jsonBuilder().startObject().startObject("type1") + .startObject("properties").startObject("completion") + .field("type", "completion") + .startArray("contexts") + .startObject() + .field("name", "ctx") + .field("type", "category") + .endObject() + .endArray() + .endObject().endObject() + .endObject().endObject().string(); + + DocumentMapper defaultMapper = createIndex("test").mapperService().documentMapperParser().parse("type1", new CompressedXContent(mapping)); + FieldMapper fieldMapper = defaultMapper.mappers().getMapper("completion"); + MappedFieldType completionFieldType = fieldMapper.fieldType(); + ParsedDocument parsedDocument = defaultMapper.parse("test", "type1", "1", jsonBuilder() + .startObject() + .startArray("completion") + .startObject() + .array("input", "suggestion5", "suggestion6", "suggestion7") + .startObject("contexts") + .field("ctx", 100) + .endObject() + .field("weight", 5) + .endObject() + .endArray() + .endObject() + .bytes()); + IndexableField[] fields = parsedDocument.rootDoc().getFields(completionFieldType.name()); + assertContextSuggestFields(fields, 3); + } + + public void testIndexingWithSimpleBooleanContexts() throws Exception { + String mapping = jsonBuilder().startObject().startObject("type1") + .startObject("properties").startObject("completion") + .field("type", "completion") + .startArray("contexts") + .startObject() + .field("name", "ctx") + .field("type", "category") + .endObject() + .endArray() + .endObject().endObject() + .endObject().endObject().string(); + + DocumentMapper defaultMapper = createIndex("test").mapperService().documentMapperParser().parse("type1", new CompressedXContent(mapping)); + FieldMapper fieldMapper = defaultMapper.mappers().getMapper("completion"); + MappedFieldType completionFieldType = fieldMapper.fieldType(); + ParsedDocument parsedDocument = defaultMapper.parse("test", "type1", "1", jsonBuilder() + .startObject() + .startArray("completion") + .startObject() + .array("input", "suggestion5", "suggestion6", "suggestion7") + .startObject("contexts") + .field("ctx", true) + .endObject() + .field("weight", 5) + .endObject() + .endArray() + .endObject() + .bytes()); + IndexableField[] fields = parsedDocument.rootDoc().getFields(completionFieldType.name()); + assertContextSuggestFields(fields, 3); + } + + public void testIndexingWithSimpleNULLContexts() throws Exception { + String mapping = jsonBuilder().startObject().startObject("type1") + .startObject("properties").startObject("completion") + .field("type", "completion") + .startArray("contexts") + .startObject() + .field("name", "ctx") + .field("type", "category") + .endObject() + .endArray() + .endObject().endObject() + .endObject().endObject().string(); + + DocumentMapper defaultMapper = createIndex("test").mapperService().documentMapperParser().parse("type1", new CompressedXContent(mapping)); + XContentBuilder builder = jsonBuilder() + .startObject() + .startArray("completion") + .startObject() + .array("input", "suggestion5", "suggestion6", "suggestion7") + .startObject("contexts") + .nullField("ctx") + .endObject() + .field("weight", 5) + .endObject() + .endArray() + .endObject(); + + Exception e = expectThrows(MapperParsingException.class, () -> defaultMapper.parse("test", "type1", "1", builder.bytes())); + assertEquals("contexts must be a string, number or boolean or a list of string, number or boolean, but was [VALUE_NULL]", e.getCause().getMessage()); + } public void testIndexingWithContextList() throws Exception { String mapping = jsonBuilder().startObject().startObject("type1") @@ -152,6 +251,66 @@ public class CategoryContextMappingTests extends ESSingleNodeTestCase { IndexableField[] fields = parsedDocument.rootDoc().getFields(completionFieldType.name()); assertContextSuggestFields(fields, 3); } + + public void testIndexingWithMixedTypeContextList() throws Exception { + String mapping = jsonBuilder().startObject().startObject("type1") + .startObject("properties").startObject("completion") + .field("type", "completion") + .startArray("contexts") + .startObject() + .field("name", "ctx") + .field("type", "category") + .endObject() + .endArray() + .endObject().endObject() + .endObject().endObject().string(); + + DocumentMapper defaultMapper = createIndex("test").mapperService().documentMapperParser().parse("type1", new CompressedXContent(mapping)); + FieldMapper fieldMapper = defaultMapper.mappers().getMapper("completion"); + MappedFieldType completionFieldType = fieldMapper.fieldType(); + ParsedDocument parsedDocument = defaultMapper.parse("test", "type1", "1", jsonBuilder() + .startObject() + .startObject("completion") + .array("input", "suggestion5", "suggestion6", "suggestion7") + .startObject("contexts") + .array("ctx", "ctx1", true, 100) + .endObject() + .field("weight", 5) + .endObject() + .endObject() + .bytes()); + IndexableField[] fields = parsedDocument.rootDoc().getFields(completionFieldType.name()); + assertContextSuggestFields(fields, 3); + } + + public void testIndexingWithMixedTypeContextListHavingNULL() throws Exception { + String mapping = jsonBuilder().startObject().startObject("type1") + .startObject("properties").startObject("completion") + .field("type", "completion") + .startArray("contexts") + .startObject() + .field("name", "ctx") + .field("type", "category") + .endObject() + .endArray() + .endObject().endObject() + .endObject().endObject().string(); + + DocumentMapper defaultMapper = createIndex("test").mapperService().documentMapperParser().parse("type1", new CompressedXContent(mapping)); + XContentBuilder builder = jsonBuilder() + .startObject() + .startObject("completion") + .array("input", "suggestion5", "suggestion6", "suggestion7") + .startObject("contexts") + .array("ctx", "ctx1", true, 100, null) + .endObject() + .field("weight", 5) + .endObject() + .endObject(); + + Exception e = expectThrows(MapperParsingException.class, () -> defaultMapper.parse("test", "type1", "1", builder.bytes())); + assertEquals("context array must have string, number or boolean values, but was [VALUE_NULL]", e.getCause().getMessage()); + } public void testIndexingWithMultipleContexts() throws Exception { String mapping = jsonBuilder().startObject().startObject("type1") @@ -202,6 +361,37 @@ public class CategoryContextMappingTests extends ESSingleNodeTestCase { assertThat(internalQueryContexts.get(0).boost, equalTo(1)); assertThat(internalQueryContexts.get(0).isPrefix, equalTo(false)); } + + public void testBooleanQueryContextParsingBasic() throws Exception { + XContentBuilder builder = jsonBuilder().value(true); + XContentParser parser = createParser(JsonXContent.jsonXContent, builder.bytes()); + CategoryContextMapping mapping = ContextBuilder.category("cat").build(); + List internalQueryContexts = mapping.parseQueryContext(createParseContext(parser)); + assertThat(internalQueryContexts.size(), equalTo(1)); + assertThat(internalQueryContexts.get(0).context, equalTo("true")); + assertThat(internalQueryContexts.get(0).boost, equalTo(1)); + assertThat(internalQueryContexts.get(0).isPrefix, equalTo(false)); + } + + public void testNumberQueryContextParsingBasic() throws Exception { + XContentBuilder builder = jsonBuilder().value(10); + XContentParser parser = createParser(JsonXContent.jsonXContent, builder.bytes()); + CategoryContextMapping mapping = ContextBuilder.category("cat").build(); + List internalQueryContexts = mapping.parseQueryContext(createParseContext(parser)); + assertThat(internalQueryContexts.size(), equalTo(1)); + assertThat(internalQueryContexts.get(0).context, equalTo("10")); + assertThat(internalQueryContexts.get(0).boost, equalTo(1)); + assertThat(internalQueryContexts.get(0).isPrefix, equalTo(false)); + } + + public void testNULLQueryContextParsingBasic() throws Exception { + XContentBuilder builder = jsonBuilder().nullValue(); + XContentParser parser = createParser(JsonXContent.jsonXContent, builder.bytes()); + CategoryContextMapping mapping = ContextBuilder.category("cat").build(); + + Exception e = expectThrows(ElasticsearchParseException.class, () -> mapping.parseQueryContext(createParseContext(parser))); + assertEquals("category context must be an object, string, number or boolean", e.getMessage()); + } public void testQueryContextParsingArray() throws Exception { XContentBuilder builder = jsonBuilder().startArray() @@ -219,6 +409,46 @@ public class CategoryContextMappingTests extends ESSingleNodeTestCase { assertThat(internalQueryContexts.get(1).boost, equalTo(1)); assertThat(internalQueryContexts.get(1).isPrefix, equalTo(false)); } + + public void testQueryContextParsingMixedTypeValuesArray() throws Exception { + XContentBuilder builder = jsonBuilder().startArray() + .value("context1") + .value("context2") + .value(true) + .value(10) + .endArray(); + XContentParser parser = createParser(JsonXContent.jsonXContent, builder.bytes()); + CategoryContextMapping mapping = ContextBuilder.category("cat").build(); + List internalQueryContexts = mapping.parseQueryContext(createParseContext(parser)); + assertThat(internalQueryContexts.size(), equalTo(4)); + assertThat(internalQueryContexts.get(0).context, equalTo("context1")); + assertThat(internalQueryContexts.get(0).boost, equalTo(1)); + assertThat(internalQueryContexts.get(0).isPrefix, equalTo(false)); + assertThat(internalQueryContexts.get(1).context, equalTo("context2")); + assertThat(internalQueryContexts.get(1).boost, equalTo(1)); + assertThat(internalQueryContexts.get(1).isPrefix, equalTo(false)); + assertThat(internalQueryContexts.get(2).context, equalTo("true")); + assertThat(internalQueryContexts.get(2).boost, equalTo(1)); + assertThat(internalQueryContexts.get(2).isPrefix, equalTo(false)); + assertThat(internalQueryContexts.get(3).context, equalTo("10")); + assertThat(internalQueryContexts.get(3).boost, equalTo(1)); + assertThat(internalQueryContexts.get(3).isPrefix, equalTo(false)); + } + + public void testQueryContextParsingMixedTypeValuesArrayHavingNULL() throws Exception { + XContentBuilder builder = jsonBuilder().startArray() + .value("context1") + .value("context2") + .value(true) + .value(10) + .nullValue() + .endArray(); + XContentParser parser = createParser(JsonXContent.jsonXContent, builder.bytes()); + CategoryContextMapping mapping = ContextBuilder.category("cat").build(); + + Exception e = expectThrows(ElasticsearchParseException.class, () -> mapping.parseQueryContext(createParseContext(parser))); + assertEquals("category context must be an object, string, number or boolean", e.getMessage()); + } public void testQueryContextParsingObject() throws Exception { XContentBuilder builder = jsonBuilder().startObject() @@ -235,7 +465,49 @@ public class CategoryContextMappingTests extends ESSingleNodeTestCase { assertThat(internalQueryContexts.get(0).isPrefix, equalTo(true)); } + public void testQueryContextParsingObjectHavingBoolean() throws Exception { + XContentBuilder builder = jsonBuilder().startObject() + .field("context", false) + .field("boost", 10) + .field("prefix", true) + .endObject(); + XContentParser parser = createParser(JsonXContent.jsonXContent, builder.bytes()); + CategoryContextMapping mapping = ContextBuilder.category("cat").build(); + List internalQueryContexts = mapping.parseQueryContext(createParseContext(parser)); + assertThat(internalQueryContexts.size(), equalTo(1)); + assertThat(internalQueryContexts.get(0).context, equalTo("false")); + assertThat(internalQueryContexts.get(0).boost, equalTo(10)); + assertThat(internalQueryContexts.get(0).isPrefix, equalTo(true)); + } + public void testQueryContextParsingObjectHavingNumber() throws Exception { + XContentBuilder builder = jsonBuilder().startObject() + .field("context", 333) + .field("boost", 10) + .field("prefix", true) + .endObject(); + XContentParser parser = createParser(JsonXContent.jsonXContent, builder.bytes()); + CategoryContextMapping mapping = ContextBuilder.category("cat").build(); + List internalQueryContexts = mapping.parseQueryContext(createParseContext(parser)); + assertThat(internalQueryContexts.size(), equalTo(1)); + assertThat(internalQueryContexts.get(0).context, equalTo("333")); + assertThat(internalQueryContexts.get(0).boost, equalTo(10)); + assertThat(internalQueryContexts.get(0).isPrefix, equalTo(true)); + } + + public void testQueryContextParsingObjectHavingNULL() throws Exception { + XContentBuilder builder = jsonBuilder().startObject() + .nullField("context") + .field("boost", 10) + .field("prefix", true) + .endObject(); + XContentParser parser = createParser(JsonXContent.jsonXContent, builder.bytes()); + CategoryContextMapping mapping = ContextBuilder.category("cat").build(); + + Exception e = expectThrows(ElasticsearchParseException.class, () -> mapping.parseQueryContext(createParseContext(parser))); + assertEquals("category context must be a string, number or boolean", e.getMessage()); + } + public void testQueryContextParsingObjectArray() throws Exception { XContentBuilder builder = jsonBuilder().startArray() .startObject() @@ -260,6 +532,82 @@ public class CategoryContextMappingTests extends ESSingleNodeTestCase { assertThat(internalQueryContexts.get(1).boost, equalTo(3)); assertThat(internalQueryContexts.get(1).isPrefix, equalTo(false)); } + + public void testQueryContextParsingMixedTypeObjectArray() throws Exception { + XContentBuilder builder = jsonBuilder().startArray() + .startObject() + .field("context", "context1") + .field("boost", 2) + .field("prefix", true) + .endObject() + .startObject() + .field("context", "context2") + .field("boost", 3) + .field("prefix", false) + .endObject() + .startObject() + .field("context", true) + .field("boost", 3) + .field("prefix", false) + .endObject() + .startObject() + .field("context", 333) + .field("boost", 3) + .field("prefix", false) + .endObject() + .endArray(); + XContentParser parser = createParser(JsonXContent.jsonXContent, builder.bytes()); + CategoryContextMapping mapping = ContextBuilder.category("cat").build(); + List internalQueryContexts = mapping.parseQueryContext(createParseContext(parser)); + assertThat(internalQueryContexts.size(), equalTo(4)); + assertThat(internalQueryContexts.get(0).context, equalTo("context1")); + assertThat(internalQueryContexts.get(0).boost, equalTo(2)); + assertThat(internalQueryContexts.get(0).isPrefix, equalTo(true)); + assertThat(internalQueryContexts.get(1).context, equalTo("context2")); + assertThat(internalQueryContexts.get(1).boost, equalTo(3)); + assertThat(internalQueryContexts.get(1).isPrefix, equalTo(false)); + assertThat(internalQueryContexts.get(2).context, equalTo("true")); + assertThat(internalQueryContexts.get(2).boost, equalTo(3)); + assertThat(internalQueryContexts.get(2).isPrefix, equalTo(false)); + assertThat(internalQueryContexts.get(3).context, equalTo("333")); + assertThat(internalQueryContexts.get(3).boost, equalTo(3)); + assertThat(internalQueryContexts.get(3).isPrefix, equalTo(false)); + } + + public void testQueryContextParsingMixedTypeObjectArrayHavingNULL() throws Exception { + XContentBuilder builder = jsonBuilder().startArray() + .startObject() + .field("context", "context1") + .field("boost", 2) + .field("prefix", true) + .endObject() + .startObject() + .field("context", "context2") + .field("boost", 3) + .field("prefix", false) + .endObject() + .startObject() + .field("context", true) + .field("boost", 3) + .field("prefix", false) + .endObject() + .startObject() + .field("context", 333) + .field("boost", 3) + .field("prefix", false) + .endObject() + .startObject() + .nullField("context") + .field("boost", 3) + .field("prefix", false) + .endObject() + .endArray(); + XContentParser parser = createParser(JsonXContent.jsonXContent, builder.bytes()); + CategoryContextMapping mapping = ContextBuilder.category("cat").build(); + + Exception e = expectThrows(ElasticsearchParseException.class, () -> mapping.parseQueryContext(createParseContext(parser))); + assertEquals("category context must be a string, number or boolean", e.getMessage()); + } private static QueryParseContext createParseContext(XContentParser parser) { return new QueryParseContext(parser); @@ -273,17 +621,52 @@ public class CategoryContextMappingTests extends ESSingleNodeTestCase { .field("prefix", true) .endObject() .value("context2") + .value(false) + .startObject() + .field("context", 333) + .field("boost", 2) + .field("prefix", true) + .endObject() .endArray(); XContentParser parser = createParser(JsonXContent.jsonXContent, builder.bytes()); CategoryContextMapping mapping = ContextBuilder.category("cat").build(); List internalQueryContexts = mapping.parseQueryContext(createParseContext(parser)); - assertThat(internalQueryContexts.size(), equalTo(2)); + assertThat(internalQueryContexts.size(), equalTo(4)); assertThat(internalQueryContexts.get(0).context, equalTo("context1")); assertThat(internalQueryContexts.get(0).boost, equalTo(2)); assertThat(internalQueryContexts.get(0).isPrefix, equalTo(true)); assertThat(internalQueryContexts.get(1).context, equalTo("context2")); assertThat(internalQueryContexts.get(1).boost, equalTo(1)); assertThat(internalQueryContexts.get(1).isPrefix, equalTo(false)); + assertThat(internalQueryContexts.get(2).context, equalTo("false")); + assertThat(internalQueryContexts.get(2).boost, equalTo(1)); + assertThat(internalQueryContexts.get(2).isPrefix, equalTo(false)); + assertThat(internalQueryContexts.get(3).context, equalTo("333")); + assertThat(internalQueryContexts.get(3).boost, equalTo(2)); + assertThat(internalQueryContexts.get(3).isPrefix, equalTo(true)); + } + + public void testQueryContextParsingMixedHavingNULL() throws Exception { + XContentBuilder builder = jsonBuilder().startArray() + .startObject() + .field("context", "context1") + .field("boost", 2) + .field("prefix", true) + .endObject() + .value("context2") + .value(false) + .startObject() + .field("context", 333) + .field("boost", 2) + .field("prefix", true) + .endObject() + .nullValue() + .endArray(); + XContentParser parser = createParser(JsonXContent.jsonXContent, builder.bytes()); + CategoryContextMapping mapping = ContextBuilder.category("cat").build(); + + Exception e = expectThrows(ElasticsearchParseException.class, () -> mapping.parseQueryContext(createParseContext(parser))); + assertEquals("category context must be an object, string, number or boolean", e.getMessage()); } public void testParsingContextFromDocument() throws Exception {