/*
* 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.index.mapper;
import org.elasticsearch.ExceptionsHelper;
import org.elasticsearch.common.compress.CompressedXContent;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.xcontent.XContentFactory;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.index.IndexService;
import org.elasticsearch.index.mapper.KeywordFieldMapper.KeywordFieldType;
import org.elasticsearch.index.mapper.MapperService.MergeReason;
import org.elasticsearch.index.mapper.NumberFieldMapper.NumberFieldType;
import org.elasticsearch.test.ESSingleNodeTestCase;
import org.hamcrest.Matchers;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.function.Function;
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.startsWith;
public class MapperServiceTests extends ESSingleNodeTestCase {
public void testTypeNameStartsWithIllegalDot() {
String index = "test-index";
String type = ".test-type";
String field = "field";
IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> {
client().admin().indices().prepareCreate(index)
.addMapping(type, field, "type=text")
.execute().actionGet();
});
assertTrue(e.getMessage(), e.getMessage().contains("mapping type name [.test-type] must not start with a '.'"));
}
public void testTypeNameTooLong() {
String index = "text-index";
String field = "field";
String type = new String(new char[256]).replace("\0", "a");
MapperException e = expectThrows(MapperException.class, () -> {
client().admin().indices().prepareCreate(index)
.addMapping(type, field, "type=text")
.execute().actionGet();
});
assertTrue(e.getMessage(), e.getMessage().contains("mapping type name [" + type + "] is too long; limit is length 255 but was [256]"));
}
public void testTypes() throws Exception {
IndexService indexService1 = createIndex("index1", Settings.builder().put("index.mapping.single_type", false).build());
MapperService mapperService = indexService1.mapperService();
assertEquals(Collections.emptySet(), mapperService.types());
mapperService.merge("type1", new CompressedXContent("{\"type1\":{}}"), MapperService.MergeReason.MAPPING_UPDATE, false);
assertNull(mapperService.documentMapper(MapperService.DEFAULT_MAPPING));
assertEquals(Collections.singleton("type1"), mapperService.types());
mapperService.merge(MapperService.DEFAULT_MAPPING, new CompressedXContent("{\"_default_\":{}}"), MapperService.MergeReason.MAPPING_UPDATE, false);
assertNotNull(mapperService.documentMapper(MapperService.DEFAULT_MAPPING));
assertEquals(Collections.singleton("type1"), mapperService.types());
mapperService.merge("type2", new CompressedXContent("{\"type2\":{}}"), MapperService.MergeReason.MAPPING_UPDATE, false);
assertNotNull(mapperService.documentMapper(MapperService.DEFAULT_MAPPING));
assertEquals(new HashSet<>(Arrays.asList("type1", "type2")), mapperService.types());
}
public void testIndexIntoDefaultMapping() throws Throwable {
// 1. test implicit index creation
ExecutionException e = expectThrows(ExecutionException.class, () -> {
client().prepareIndex("index1", MapperService.DEFAULT_MAPPING, "1").setSource("{}", XContentType.JSON).execute().get();
});
Throwable throwable = ExceptionsHelper.unwrapCause(e.getCause());
if (throwable instanceof IllegalArgumentException) {
assertEquals("It is forbidden to index into the default mapping [_default_]", throwable.getMessage());
} else {
throw e;
}
// 2. already existing index
IndexService indexService = createIndex("index2");
e = expectThrows(ExecutionException.class, () -> {
client().prepareIndex("index1", MapperService.DEFAULT_MAPPING, "2").setSource().execute().get();
});
throwable = ExceptionsHelper.unwrapCause(e.getCause());
if (throwable instanceof IllegalArgumentException) {
assertEquals("It is forbidden to index into the default mapping [_default_]", throwable.getMessage());
} else {
throw e;
}
assertFalse(indexService.mapperService().hasMapping(MapperService.DEFAULT_MAPPING));
}
public void testTotalFieldsExceedsLimit() throws Throwable {
Function<String, String> mapping = type -> {
try {
return XContentFactory.jsonBuilder().startObject().startObject(type).startObject("properties")
.startObject("field1").field("type", "keyword")
.endObject().endObject().endObject().endObject().string();
} catch (IOException e) {
throw new UncheckedIOException(e);
}
};
createIndex("test1").mapperService().merge("type", new CompressedXContent(mapping.apply("type")), MergeReason.MAPPING_UPDATE, false);
//set total number of fields to 1 to trigger an exception
IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> {
createIndex("test2", Settings.builder().put(MapperService.INDEX_MAPPING_TOTAL_FIELDS_LIMIT_SETTING.getKey(), 1).build())
.mapperService().merge("type", new CompressedXContent(mapping.apply("type")), MergeReason.MAPPING_UPDATE, false);
});
assertTrue(e.getMessage(), e.getMessage().contains("Limit of total fields [1] in index [test2] has been exceeded"));
}
public void testMappingDepthExceedsLimit() throws Throwable {
CompressedXContent simpleMapping = new CompressedXContent(XContentFactory.jsonBuilder().startObject()
.startObject("properties")
.startObject("field")
.field("type", "text")
.endObject()
.endObject().endObject().bytes());
IndexService indexService1 = createIndex("test1", Settings.builder().put(MapperService.INDEX_MAPPING_DEPTH_LIMIT_SETTING.getKey(), 1).build());
// no exception
indexService1.mapperService().merge("type", simpleMapping, MergeReason.MAPPING_UPDATE, false);
CompressedXContent objectMapping = new CompressedXContent(XContentFactory.jsonBuilder().startObject()
.startObject("properties")
.startObject("object1")
.field("type", "object")
.endObject()
.endObject().endObject().bytes());
IndexService indexService2 = createIndex("test2");
// no exception
indexService2.mapperService().merge("type", objectMapping, MergeReason.MAPPING_UPDATE, false);
IllegalArgumentException e = expectThrows(IllegalArgumentException.class,
() -> indexService1.mapperService().merge("type2", objectMapping, MergeReason.MAPPING_UPDATE, false));
assertThat(e.getMessage(), containsString("Limit of mapping depth [1] in index [test1] has been exceeded"));
}
public void testUnmappedFieldType() {
MapperService mapperService = createIndex("index").mapperService();
assertThat(mapperService.unmappedFieldType("keyword"), instanceOf(KeywordFieldType.class));
assertThat(mapperService.unmappedFieldType("long"), instanceOf(NumberFieldType.class));
// back compat
assertThat(mapperService.unmappedFieldType("string"), instanceOf(KeywordFieldType.class));
assertWarnings("[unmapped_type:string] should be replaced with [unmapped_type:keyword]");
}
public void testMergeWithMap() throws Throwable {
IndexService indexService1 = createIndex("index1");
MapperService mapperService = indexService1.mapperService();
Map<String, Map<String, Object>> mappings = new HashMap<>();
mappings.put(MapperService.DEFAULT_MAPPING, MapperService.parseMapping(xContentRegistry(), "{}"));
MapperException e = expectThrows(MapperParsingException.class,
() -> mapperService.merge(mappings, MergeReason.MAPPING_UPDATE, false));
assertThat(e.getMessage(), startsWith("Failed to parse mapping [" + MapperService.DEFAULT_MAPPING + "]: "));
mappings.clear();
mappings.put("type1", MapperService.parseMapping(xContentRegistry(), "{}"));
e = expectThrows( MapperParsingException.class,
() -> mapperService.merge(mappings, MergeReason.MAPPING_UPDATE, false));
assertThat(e.getMessage(), startsWith("Failed to parse mapping [type1]: "));
}
public void testMergeParentTypesSame() {
// Verifies that a merge (absent a DocumentMapper change)
// doesn't change the parentTypes reference.
// The collection was being rewrapped with each merge
// in v5.2 resulting in eventual StackOverflowErrors.
// https://github.com/elastic/elasticsearch/issues/23604
IndexService indexService1 = createIndex("index1");
MapperService mapperService = indexService1.mapperService();
Set<String> parentTypes = mapperService.getParentTypes();
Map<String, Map<String, Object>> mappings = new HashMap<>();
mapperService.merge(mappings, MergeReason.MAPPING_UPDATE, false);
assertSame(parentTypes, mapperService.getParentTypes());
}
public void testOtherDocumentMappersOnlyUpdatedWhenChangingFieldType() throws IOException {
IndexService indexService = createIndex("test", Settings.builder().put("index.mapping.single_type", false).build());
CompressedXContent simpleMapping = new CompressedXContent(XContentFactory.jsonBuilder().startObject()
.startObject("properties")
.startObject("field")
.field("type", "text")
.endObject()
.endObject().endObject().bytes());
indexService.mapperService().merge("type1", simpleMapping, MergeReason.MAPPING_UPDATE, true);
DocumentMapper documentMapper = indexService.mapperService().documentMapper("type1");
indexService.mapperService().merge("type2", simpleMapping, MergeReason.MAPPING_UPDATE, true);
assertSame(indexService.mapperService().documentMapper("type1"), documentMapper);
CompressedXContent normsDisabledMapping = new CompressedXContent(XContentFactory.jsonBuilder().startObject()
.startObject("properties")
.startObject("field")
.field("type", "text")
.field("norms", false)
.endObject()
.endObject().endObject().bytes());
indexService.mapperService().merge("type3", normsDisabledMapping, MergeReason.MAPPING_UPDATE, true);
assertNotSame(indexService.mapperService().documentMapper("type1"), documentMapper);
}
public void testAllEnabled() throws Exception {
IndexService indexService = createIndex("test");
assertFalse(indexService.mapperService().allEnabled());
CompressedXContent enabledAll = new CompressedXContent(XContentFactory.jsonBuilder().startObject()
.startObject("_all")
.field("enabled", true)
.endObject().endObject().bytes());
CompressedXContent disabledAll = new CompressedXContent(XContentFactory.jsonBuilder().startObject()
.startObject("_all")
.field("enabled", false)
.endObject().endObject().bytes());
Exception e = expectThrows(MapperParsingException.class,
() -> indexService.mapperService().merge(MapperService.DEFAULT_MAPPING, enabledAll,
MergeReason.MAPPING_UPDATE, random().nextBoolean()));
assertThat(e.getMessage(), containsString("[_all] is disabled in 6.0"));
}
public void testPartitionedConstraints() {
// partitioned index must have routing
IllegalArgumentException noRoutingException = expectThrows(IllegalArgumentException.class, () -> {
client().admin().indices().prepareCreate("test-index")
.addMapping("type", "{\"type\":{}}", XContentType.JSON)
.setSettings(Settings.builder()
.put("index.number_of_shards", 4)
.put("index.routing_partition_size", 2))
.execute().actionGet();
});
assertTrue(noRoutingException.getMessage(), noRoutingException.getMessage().contains("must have routing"));
// partitioned index cannot have parent/child relationships
IllegalArgumentException parentException = expectThrows(IllegalArgumentException.class, () -> {
client().admin().indices().prepareCreate("test-index")
.addMapping("parent", "{\"parent\":{\"_routing\":{\"required\":true}}}", XContentType.JSON)
.addMapping("child", "{\"child\": {\"_routing\":{\"required\":true}, \"_parent\": {\"type\": \"parent\"}}}",
XContentType.JSON)
.setSettings(Settings.builder()
.put("index.number_of_shards", 4)
.put("index.routing_partition_size", 2))
.execute().actionGet();
});
assertTrue(parentException.getMessage(), parentException.getMessage().contains("cannot have a _parent field"));
// valid partitioned index
assertTrue(client().admin().indices().prepareCreate("test-index")
.addMapping("type", "{\"type\":{\"_routing\":{\"required\":true}}}", XContentType.JSON)
.setSettings(Settings.builder()
.put("index.number_of_shards", 4)
.put("index.routing_partition_size", 2))
.execute().actionGet().isAcknowledged());
}
public void testIndexSortWithNestedFields() throws IOException {
Settings settings = Settings.builder()
.put("index.sort.field", "foo")
.build();
IllegalArgumentException invalidNestedException = expectThrows(IllegalArgumentException.class,
() -> createIndex("test", settings, "t", "nested_field", "type=nested", "foo", "type=keyword"));
assertThat(invalidNestedException.getMessage(),
containsString("cannot have nested fields when index sort is activated"));
IndexService indexService = createIndex("test", settings, "t", "foo", "type=keyword");
CompressedXContent nestedFieldMapping = new CompressedXContent(XContentFactory.jsonBuilder().startObject()
.startObject("properties")
.startObject("nested_field")
.field("type", "nested")
.endObject()
.endObject().endObject().bytes());
invalidNestedException = expectThrows(IllegalArgumentException.class,
() -> indexService.mapperService().merge("t", nestedFieldMapping,
MergeReason.MAPPING_UPDATE, true));
assertThat(invalidNestedException.getMessage(),
containsString("cannot have nested fields when index sort is activated"));
}
public void testForbidMultipleTypes() throws IOException {
String mapping = XContentFactory.jsonBuilder().startObject().startObject("type").endObject().endObject().string();
MapperService mapperService = createIndex("test").mapperService();
mapperService.merge("type", new CompressedXContent(mapping), MergeReason.MAPPING_UPDATE, randomBoolean());
String mapping2 = XContentFactory.jsonBuilder().startObject().startObject("type2").endObject().endObject().string();
IllegalArgumentException e = expectThrows(IllegalArgumentException.class,
() -> mapperService.merge("type2", new CompressedXContent(mapping2), MergeReason.MAPPING_UPDATE, randomBoolean()));
assertThat(e.getMessage(), Matchers.startsWith("Rejecting mapping update to [test] as the final mapping would have more than 1 type: "));
}
}