diff --git a/server/src/main/java/org/elasticsearch/index/mapper/DocumentMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/DocumentMapper.java index 3ec0f54435526..03445ea0e0d53 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/DocumentMapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/DocumentMapper.java @@ -138,7 +138,8 @@ public DocumentMapper(MapperService mapperService, Mapping mapping) { newFieldMappers.add(metadataMapper); } } - MapperUtils.collect(this.mapping.root, newObjectMappers, newFieldMappers); + MapperUtils.collect(this.mapping.root, + newObjectMappers, newFieldMappers, new ArrayList<>()); final IndexAnalyzers indexAnalyzers = mapperService.getIndexAnalyzers(); this.fieldMappers = new DocumentFieldMappers(newFieldMappers, diff --git a/server/src/main/java/org/elasticsearch/index/mapper/DocumentParser.java b/server/src/main/java/org/elasticsearch/index/mapper/DocumentParser.java index 61ff4a4ff3d0f..fa975ac2b4b67 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/DocumentParser.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/DocumentParser.java @@ -459,13 +459,18 @@ private static ParseContext nestedContext(ParseContext context, ObjectMapper map private static void parseObjectOrField(ParseContext context, Mapper mapper) throws IOException { if (mapper instanceof ObjectMapper) { parseObjectOrNested(context, (ObjectMapper) mapper); - } else { - FieldMapper fieldMapper = (FieldMapper)mapper; + } else if (mapper instanceof FieldMapper) { + FieldMapper fieldMapper = (FieldMapper) mapper; Mapper update = fieldMapper.parse(context); if (update != null) { context.addDynamicMapper(update); } parseCopyFields(context, fieldMapper.copyTo().copyToFields()); + } else if (mapper instanceof FieldAliasMapper) { + throw new IllegalArgumentException("Cannot write to a field alias [" + mapper.name() + "]."); + } else { + throw new IllegalStateException("The provided mapper [" + mapper.name() + "] has an unrecognized type [" + + mapper.getClass().getSimpleName() + "]."); } } diff --git a/server/src/main/java/org/elasticsearch/index/mapper/FieldAliasMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/FieldAliasMapper.java new file mode 100644 index 0000000000000..80e2e16df6220 --- /dev/null +++ b/server/src/main/java/org/elasticsearch/index/mapper/FieldAliasMapper.java @@ -0,0 +1,127 @@ +/* + * 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.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.support.XContentMapValues; + +import java.io.IOException; +import java.util.Collections; +import java.util.Iterator; +import java.util.Map; + +/** + * A mapper for field aliases. + * + * A field alias has no concrete field mappings of its own, but instead points to another field by + * its path. Once defined, an alias can be used in place of the concrete field name in search requests. + */ +public final class FieldAliasMapper extends Mapper { + public static final String CONTENT_TYPE = "alias"; + + public static class Names { + public static final String PATH = "path"; + } + + private final String name; + private final String path; + + public FieldAliasMapper(String simpleName, + String name, + String path) { + super(simpleName); + this.name = name; + this.path = path; + } + + @Override + public String name() { + return name; + } + + public String path() { + return path; + } + + @Override + public Mapper merge(Mapper mergeWith) { + if (!(mergeWith instanceof FieldAliasMapper)) { + throw new IllegalArgumentException("Cannot merge a field alias mapping [" + + name() + "] with a mapping that is not for a field alias."); + } + return mergeWith; + } + + @Override + public Mapper updateFieldType(Map fullNameToFieldType) { + return this; + } + + @Override + public Iterator iterator() { + return Collections.emptyIterator(); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + return builder.startObject(simpleName()) + .field("type", CONTENT_TYPE) + .field(Names.PATH, path) + .endObject(); + } + + public static class TypeParser implements Mapper.TypeParser { + @Override + public Mapper.Builder parse(String name, Map node, ParserContext parserContext) + throws MapperParsingException { + FieldAliasMapper.Builder builder = new FieldAliasMapper.Builder(name); + Object pathField = node.remove(Names.PATH); + String path = XContentMapValues.nodeStringValue(pathField, null); + if (path == null) { + throw new MapperParsingException("The [path] property must be specified for field [" + name + "]."); + } + return builder.path(path); + } + } + + public static class Builder extends Mapper.Builder { + private String name; + private String path; + + protected Builder(String name) { + super(name); + this.name = name; + } + + public String name() { + return this.name; + } + + public Builder path(String path) { + this.path = path; + return this; + } + + public FieldAliasMapper build(BuilderContext context) { + String fullName = context.path().pathAsText(name); + return new FieldAliasMapper(name, fullName, path); + } + } +} diff --git a/server/src/main/java/org/elasticsearch/index/mapper/FieldTypeLookup.java b/server/src/main/java/org/elasticsearch/index/mapper/FieldTypeLookup.java index 069468ddb7a25..a7d0177224d45 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/FieldTypeLookup.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/FieldTypeLookup.java @@ -35,30 +35,36 @@ */ class FieldTypeLookup implements Iterable { - /** Full field name to field type */ final CopyOnWriteHashMap fullNameToFieldType; + private final CopyOnWriteHashMap aliasToConcreteName; - /** Create a new empty instance. */ FieldTypeLookup() { fullNameToFieldType = new CopyOnWriteHashMap<>(); + aliasToConcreteName = new CopyOnWriteHashMap<>(); } - private FieldTypeLookup(CopyOnWriteHashMap fullName) { - this.fullNameToFieldType = fullName; + private FieldTypeLookup(CopyOnWriteHashMap fullNameToFieldType, + CopyOnWriteHashMap aliasToConcreteName) { + this.fullNameToFieldType = fullNameToFieldType; + this.aliasToConcreteName = aliasToConcreteName; } /** * Return a new instance that contains the union of this instance and the field types - * from the provided fields. If a field already exists, the field type will be updated - * to use the new mappers field type. + * from the provided mappers. If a field already exists, its field type will be updated + * to use the new type from the given field mapper. Similarly if an alias already + * exists, it will be updated to reference the field type from the new mapper. */ - public FieldTypeLookup copyAndAddAll(String type, Collection fieldMappers) { + public FieldTypeLookup copyAndAddAll(String type, + Collection fieldMappers, + Collection fieldAliasMappers) { Objects.requireNonNull(type, "type must not be null"); if (MapperService.DEFAULT_MAPPING.equals(type)) { throw new IllegalArgumentException("Default mappings should not be added to the lookup"); } CopyOnWriteHashMap fullName = this.fullNameToFieldType; + CopyOnWriteHashMap aliases = this.aliasToConcreteName; for (FieldMapper fieldMapper : fieldMappers) { MappedFieldType fieldType = fieldMapper.fieldType(); @@ -75,7 +81,14 @@ public FieldTypeLookup copyAndAddAll(String type, Collection fieldM } } } - return new FieldTypeLookup(fullName); + + for (FieldAliasMapper fieldAliasMapper : fieldAliasMappers) { + String aliasName = fieldAliasMapper.name(); + String fieldName = fieldAliasMapper.path(); + aliases = aliases.copyAndPut(aliasName, fieldName); + } + + return new FieldTypeLookup(fullName, aliases); } /** @@ -92,7 +105,8 @@ private void checkCompatibility(MappedFieldType existingFieldType, MappedFieldTy /** Returns the field for the given field */ public MappedFieldType get(String field) { - return fullNameToFieldType.get(field); + String resolvedField = aliasToConcreteName.getOrDefault(field, field); + return fullNameToFieldType.get(resolvedField); } /** @@ -105,6 +119,11 @@ public Collection simpleMatchToFullName(String pattern) { fields.add(fieldType.name()); } } + for (String aliasName : aliasToConcreteName.keySet()) { + if (Regex.simpleMatch(pattern, aliasName)) { + fields.add(aliasName); + } + } return fields; } diff --git a/server/src/main/java/org/elasticsearch/index/mapper/MapperService.java b/server/src/main/java/org/elasticsearch/index/mapper/MapperService.java index 8988238d9277e..b99cae2e3b6d7 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/MapperService.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/MapperService.java @@ -21,7 +21,6 @@ import com.carrotsearch.hppc.ObjectHashSet; import com.carrotsearch.hppc.cursors.ObjectCursor; - import org.apache.logging.log4j.message.ParameterizedMessage; import org.apache.lucene.analysis.Analyzer; import org.apache.lucene.analysis.DelegatingAnalyzerWrapper; @@ -395,15 +394,16 @@ private synchronized Map internalMerge(@Nullable Documen // check basic sanity of the new mapping List objectMappers = new ArrayList<>(); List fieldMappers = new ArrayList<>(); + List fieldAliasMappers = new ArrayList<>(); Collections.addAll(fieldMappers, newMapper.mapping().metadataMappers); - MapperUtils.collect(newMapper.mapping().root(), objectMappers, fieldMappers); + MapperUtils.collect(newMapper.mapping().root(), objectMappers, fieldMappers, fieldAliasMappers); checkFieldUniqueness(newMapper.type(), objectMappers, fieldMappers, fullPathObjectMappers, fieldTypes); checkObjectsCompatibility(objectMappers, fullPathObjectMappers); checkPartitionedIndexConstraints(newMapper); // update lookup data-structures // this will in particular make sure that the merged fields are compatible with other types - fieldTypes = fieldTypes.copyAndAddAll(newMapper.type(), fieldMappers); + fieldTypes = fieldTypes.copyAndAddAll(newMapper.type(), fieldMappers, fieldAliasMappers); for (ObjectMapper objectMapper : objectMappers) { if (fullPathObjectMappers == this.fullPathObjectMappers) { @@ -482,7 +482,7 @@ private boolean assertMappersShareSameFieldType() { if (mapper != null) { List fieldMappers = new ArrayList<>(); Collections.addAll(fieldMappers, mapper.mapping().metadataMappers); - MapperUtils.collect(mapper.root(), new ArrayList<>(), fieldMappers); + MapperUtils.collect(mapper.root(), new ArrayList<>(), fieldMappers, new ArrayList<>()); for (FieldMapper fieldMapper : fieldMappers) { assert fieldMapper.fieldType() == fieldTypes.get(fieldMapper.name()) : fieldMapper.name(); } diff --git a/server/src/main/java/org/elasticsearch/index/mapper/MapperUtils.java b/server/src/main/java/org/elasticsearch/index/mapper/MapperUtils.java index ad57d72b345ab..70da6b73f312e 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/MapperUtils.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/MapperUtils.java @@ -24,17 +24,28 @@ enum MapperUtils { ; - /** Split mapper and its descendants into object and field mappers. */ - public static void collect(Mapper mapper, Collection objectMappers, Collection fieldMappers) { + /** + * Splits the provided mapper and its descendants into object, field, and field alias mappers. + */ + public static void collect(Mapper mapper, Collection objectMappers, + Collection fieldMappers, + Collection fieldAliasMappers) { if (mapper instanceof RootObjectMapper) { // root mapper isn't really an object mapper } else if (mapper instanceof ObjectMapper) { objectMappers.add((ObjectMapper)mapper); } else if (mapper instanceof FieldMapper) { fieldMappers.add((FieldMapper)mapper); + } else if (mapper instanceof FieldAliasMapper) { + fieldAliasMappers.add((FieldAliasMapper) mapper); + } else { + throw new IllegalStateException("Unrecognized mapper type [" + + mapper.getClass().getSimpleName() + "]."); } + + for (Mapper child : mapper) { - collect(child, objectMappers, fieldMappers); + collect(child, objectMappers, fieldMappers, fieldAliasMappers); } } } diff --git a/server/src/main/java/org/elasticsearch/index/search/QueryParserHelper.java b/server/src/main/java/org/elasticsearch/index/search/QueryParserHelper.java index b3751afbc9c5d..4c1958b203ec0 100644 --- a/server/src/main/java/org/elasticsearch/index/search/QueryParserHelper.java +++ b/server/src/main/java/org/elasticsearch/index/search/QueryParserHelper.java @@ -25,6 +25,7 @@ import org.elasticsearch.index.mapper.FieldMapper; import org.elasticsearch.index.mapper.IpFieldMapper; import org.elasticsearch.index.mapper.KeywordFieldMapper; +import org.elasticsearch.index.mapper.MappedFieldType; import org.elasticsearch.index.mapper.MapperService; import org.elasticsearch.index.mapper.MetadataFieldMapper; import org.elasticsearch.index.mapper.NumberFieldMapper; @@ -167,23 +168,27 @@ public static Map resolveMappingField(QueryShardContext context, if (fieldSuffix != null && context.fieldMapper(fieldName + fieldSuffix) != null) { fieldName = fieldName + fieldSuffix; } - FieldMapper mapper = getFieldMapper(context.getMapperService(), fieldName); - if (mapper == null) { - // Unmapped fields are not ignored - fields.put(fieldOrPattern, weight); - continue; - } - if (acceptMetadataField == false && mapper instanceof MetadataFieldMapper) { - // Ignore metadata fields + + MappedFieldType fieldType = context.getMapperService().fullName(fieldName); + if (fieldType == null) { + // Note that we don't ignore unmapped fields. + fields.put(fieldName, weight); continue; } + // Ignore fields that are not in the allowed mapper types. Some // types do not support term queries, and thus we cannot generate // a special query for them. - String mappingType = mapper.fieldType().typeName(); + String mappingType = fieldType.typeName(); if (acceptAllTypes == false && ALLOWED_QUERY_MAPPER_TYPES.contains(mappingType) == false) { continue; } + + // Ignore metadata fields. + FieldMapper mapper = getFieldMapper(context.getMapperService(), fieldName); + if (acceptMetadataField == false && mapper instanceof MetadataFieldMapper) { + continue; + } fields.put(fieldName, weight); } checkForTooManyFields(fields); diff --git a/server/src/main/java/org/elasticsearch/indices/IndicesModule.java b/server/src/main/java/org/elasticsearch/indices/IndicesModule.java index 2d41491e3a746..a1038853c0670 100644 --- a/server/src/main/java/org/elasticsearch/indices/IndicesModule.java +++ b/server/src/main/java/org/elasticsearch/indices/IndicesModule.java @@ -36,6 +36,7 @@ import org.elasticsearch.index.mapper.BooleanFieldMapper; import org.elasticsearch.index.mapper.CompletionFieldMapper; import org.elasticsearch.index.mapper.DateFieldMapper; +import org.elasticsearch.index.mapper.FieldAliasMapper; import org.elasticsearch.index.mapper.FieldNamesFieldMapper; import org.elasticsearch.index.mapper.GeoPointFieldMapper; import org.elasticsearch.index.mapper.GeoShapeFieldMapper; @@ -129,7 +130,9 @@ private Map getMappers(List mapperPlugi mappers.put(ObjectMapper.CONTENT_TYPE, new ObjectMapper.TypeParser()); mappers.put(ObjectMapper.NESTED_CONTENT_TYPE, new ObjectMapper.TypeParser()); mappers.put(CompletionFieldMapper.CONTENT_TYPE, new CompletionFieldMapper.TypeParser()); + mappers.put(FieldAliasMapper.CONTENT_TYPE, new FieldAliasMapper.TypeParser()); mappers.put(GeoPointFieldMapper.CONTENT_TYPE, new GeoPointFieldMapper.TypeParser()); + if (ShapesAvailability.JTS_AVAILABLE && ShapesAvailability.SPATIAL4J_AVAILABLE) { mappers.put(GeoShapeFieldMapper.CONTENT_TYPE, new GeoShapeFieldMapper.TypeParser()); } diff --git a/server/src/main/java/org/elasticsearch/search/fetch/subphase/highlight/HighlightPhase.java b/server/src/main/java/org/elasticsearch/search/fetch/subphase/highlight/HighlightPhase.java index e5ff7abc68b34..11e46061d6786 100644 --- a/server/src/main/java/org/elasticsearch/search/fetch/subphase/highlight/HighlightPhase.java +++ b/server/src/main/java/org/elasticsearch/search/fetch/subphase/highlight/HighlightPhase.java @@ -100,7 +100,7 @@ public void hitExecute(SearchContext context, HitContext hitContext) { if (highlightQuery == null) { highlightQuery = context.parsedQuery().query(); } - HighlighterContext highlighterContext = new HighlighterContext(fieldName, + HighlighterContext highlighterContext = new HighlighterContext(fieldType.name(), field, fieldType, context, hitContext, highlightQuery); if ((highlighter.canHighlight(fieldType) == false) && fieldNameContainsWildcards) { @@ -109,7 +109,11 @@ public void hitExecute(SearchContext context, HitContext hitContext) { } HighlightField highlightField = highlighter.highlight(highlighterContext); if (highlightField != null) { - highlightFields.put(highlightField.name(), highlightField); + // Note that we make sure to use the original field name in the response. This is because the + // original field could be an alias, and highlighter implementations may instead reference the + // concrete field it points to. + highlightFields.put(fieldName, + new HighlightField(fieldName, highlightField.fragments())); } } } diff --git a/server/src/main/java/org/elasticsearch/search/suggest/SuggestionBuilder.java b/server/src/main/java/org/elasticsearch/search/suggest/SuggestionBuilder.java index dcdc669539f53..9199615868a13 100644 --- a/server/src/main/java/org/elasticsearch/search/suggest/SuggestionBuilder.java +++ b/server/src/main/java/org/elasticsearch/search/suggest/SuggestionBuilder.java @@ -27,7 +27,6 @@ import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.lucene.BytesRefs; -import org.elasticsearch.common.xcontent.ToXContent.Params; import org.elasticsearch.common.xcontent.ToXContentFragment; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentParser; @@ -321,7 +320,7 @@ protected void populateCommonFields(MapperService mapperService, SuggestionSearc suggestionContext.setAnalyzer(luceneAnalyzer); } - suggestionContext.setField(field); + suggestionContext.setField(fieldType.name()); if (size != null) { suggestionContext.setSize(size); diff --git a/server/src/test/java/org/elasticsearch/index/mapper/DocumentParserTests.java b/server/src/test/java/org/elasticsearch/index/mapper/DocumentParserTests.java index 11f69c738e949..3156bc96e12d8 100644 --- a/server/src/test/java/org/elasticsearch/index/mapper/DocumentParserTests.java +++ b/server/src/test/java/org/elasticsearch/index/mapper/DocumentParserTests.java @@ -1389,4 +1389,33 @@ public void testBlankFieldNames() throws Exception { client().prepareIndex("idx", "type").setSource(bytes2, XContentType.JSON).get()); assertThat(ExceptionsHelper.detailedMessage(err), containsString("field name cannot be an empty string")); } + + public void testFieldAlias() throws Exception { + String mapping = Strings.toString(XContentFactory.jsonBuilder() + .startObject() + .startObject("type") + .startObject("properties") + .startObject("alias-field") + .field("type", "alias") + .field("path", "concrete-field") + .endObject() + .startObject("concrete-field") + .field("type", "keyword") + .endObject() + .endObject() + .endObject() + .endObject()); + + DocumentMapperParser mapperParser = createIndex("test").mapperService().documentMapperParser(); + DocumentMapper mapper = mapperParser.parse("type", new CompressedXContent(mapping)); + + BytesReference bytes = BytesReference.bytes(XContentFactory.jsonBuilder() + .startObject() + .field("alias-field", "value") + .endObject()); + MapperParsingException exception = expectThrows(MapperParsingException.class, + () -> mapper.parse(SourceToParse.source("test", "type", "1", bytes, XContentType.JSON))); + + assertEquals("Cannot write to a field alias [alias-field].", exception.getCause().getMessage()); + } } diff --git a/server/src/test/java/org/elasticsearch/index/mapper/FieldAliasMapperTests.java b/server/src/test/java/org/elasticsearch/index/mapper/FieldAliasMapperTests.java new file mode 100644 index 0000000000000..889bcfa81cde5 --- /dev/null +++ b/server/src/test/java/org/elasticsearch/index/mapper/FieldAliasMapperTests.java @@ -0,0 +1,164 @@ +/* + * 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.common.Strings; +import org.elasticsearch.common.compress.CompressedXContent; +import org.elasticsearch.common.xcontent.XContentFactory; +import org.elasticsearch.index.IndexService; +import org.elasticsearch.index.mapper.MapperService.MergeReason; +import org.elasticsearch.test.ESSingleNodeTestCase; +import org.junit.Before; + +import java.io.IOException; + +public class FieldAliasMapperTests extends ESSingleNodeTestCase { + private MapperService mapperService; + private DocumentMapperParser parser; + + @Before + public void setup() { + IndexService indexService = createIndex("test"); + mapperService = indexService.mapperService(); + parser = mapperService.documentMapperParser(); + } + + public void testParsing() throws IOException { + String mapping = Strings.toString(XContentFactory.jsonBuilder() + .startObject() + .startObject("type") + .startObject("properties") + .startObject("alias-field") + .field("type", "alias") + .field("path", "concrete-field") + .endObject() + .startObject("concrete-field") + .field("type", "keyword") + .endObject() + .endObject() + .endObject() + .endObject()); + DocumentMapper mapper = parser.parse("type", new CompressedXContent(mapping)); + assertEquals(mapping, mapper.mappingSource().toString()); + } + + public void testParsingWithMissingPath() throws IOException { + String mapping = Strings.toString(XContentFactory.jsonBuilder() + .startObject() + .startObject("type") + .startObject("properties") + .startObject("alias-field") + .field("type", "alias") + .endObject() + .endObject() + .endObject() + .endObject()); + MapperParsingException exception = expectThrows(MapperParsingException.class, + () -> parser.parse("type", new CompressedXContent(mapping))); + assertEquals("The [path] property must be specified for field [alias-field].", exception.getMessage()); + } + + public void testParsingWithExtraArgument() throws IOException { + String mapping = Strings.toString(XContentFactory.jsonBuilder() + .startObject() + .startObject("type") + .startObject("properties") + .startObject("alias-field") + .field("type", "alias") + .field("path", "concrete-field") + .field("extra-field", "extra-value") + .endObject() + .endObject() + .endObject() + .endObject()); + MapperParsingException exception = expectThrows(MapperParsingException.class, + () -> parser.parse("type", new CompressedXContent(mapping))); + assertEquals("Mapping definition for [alias-field] has unsupported parameters: [extra-field : extra-value]", + exception.getMessage()); + } + + public void testMerge() throws IOException { + String mapping = Strings.toString(XContentFactory.jsonBuilder().startObject() + .startObject("type") + .startObject("properties") + .startObject("first-field") + .field("type", "keyword") + .endObject() + .startObject("alias-field") + .field("type", "alias") + .field("path", "first-field") + .endObject() + .endObject() + .endObject() + .endObject()); + mapperService.merge("type", new CompressedXContent(mapping), MergeReason.MAPPING_UPDATE); + + MappedFieldType firstFieldType = mapperService.fullName("alias-field"); + assertEquals("first-field", firstFieldType.name()); + assertTrue(firstFieldType instanceof KeywordFieldMapper.KeywordFieldType); + + String newMapping = Strings.toString(XContentFactory.jsonBuilder().startObject() + .startObject("type") + .startObject("properties") + .startObject("second-field") + .field("type", "text") + .endObject() + .startObject("alias-field") + .field("type", "alias") + .field("path", "second-field") + .endObject() + .endObject() + .endObject() + .endObject()); + mapperService.merge("type", new CompressedXContent(newMapping), MergeReason.MAPPING_UPDATE); + + MappedFieldType secondFieldType = mapperService.fullName("alias-field"); + assertEquals("second-field", secondFieldType.name()); + assertTrue(secondFieldType instanceof TextFieldMapper.TextFieldType); + } + + public void testMergeFailure() throws IOException { + String mapping = Strings.toString(XContentFactory.jsonBuilder().startObject() + .startObject("type") + .startObject("properties") + .startObject("alias-field") + .field("type", "alias") + .field("path", "concrete-field") + .endObject() + .endObject() + .endObject() + .endObject()); + mapperService.merge("type", new CompressedXContent(mapping), MergeReason.MAPPING_UPDATE); + + String newMapping = Strings.toString(XContentFactory.jsonBuilder().startObject() + .startObject("type") + .startObject("properties") + .startObject("alias-field") + .field("type", "keyword") + .endObject() + .endObject() + .endObject() + .endObject()); + IllegalArgumentException exception = expectThrows(IllegalArgumentException.class, + () -> mapperService.merge("type", new CompressedXContent(newMapping), MergeReason.MAPPING_UPDATE)); + assertEquals("Cannot merge a field alias mapping [alias-field] with a mapping that is not for a field alias.", + exception.getMessage()); + } +} diff --git a/server/src/test/java/org/elasticsearch/index/mapper/FieldTypeLookupTests.java b/server/src/test/java/org/elasticsearch/index/mapper/FieldTypeLookupTests.java index 4f1b908cae84e..8a49169e09e79 100644 --- a/server/src/test/java/org/elasticsearch/index/mapper/FieldTypeLookupTests.java +++ b/server/src/test/java/org/elasticsearch/index/mapper/FieldTypeLookupTests.java @@ -28,10 +28,11 @@ import java.util.Arrays; import java.util.Collection; -import java.util.Collections; import java.util.Iterator; import java.util.List; +import static java.util.Collections.emptyList; + public class FieldTypeLookupTests extends ESTestCase { public void testEmpty() { @@ -48,7 +49,7 @@ public void testEmpty() { public void testDefaultMapping() { FieldTypeLookup lookup = new FieldTypeLookup(); try { - lookup.copyAndAddAll(MapperService.DEFAULT_MAPPING, Collections.emptyList()); + lookup.copyAndAddAll(MapperService.DEFAULT_MAPPING, emptyList(), emptyList()); fail(); } catch (IllegalArgumentException expected) { assertEquals("Default mappings should not be added to the lookup", expected.getMessage()); @@ -58,7 +59,7 @@ public void testDefaultMapping() { public void testAddNewField() { FieldTypeLookup lookup = new FieldTypeLookup(); MockFieldMapper f = new MockFieldMapper("foo"); - FieldTypeLookup lookup2 = lookup.copyAndAddAll("type", newList(f)); + FieldTypeLookup lookup2 = lookup.copyAndAddAll("type", newList(f), emptyList()); assertNull(lookup.get("foo")); assertNull(lookup.get("bar")); assertEquals(f.fieldType(), lookup2.get("foo")); @@ -70,8 +71,8 @@ public void testAddExistingField() { MockFieldMapper f = new MockFieldMapper("foo"); MockFieldMapper f2 = new MockFieldMapper("foo"); FieldTypeLookup lookup = new FieldTypeLookup(); - lookup = lookup.copyAndAddAll("type1", newList(f)); - FieldTypeLookup lookup2 = lookup.copyAndAddAll("type2", newList(f2)); + lookup = lookup.copyAndAddAll("type1", newList(f), emptyList()); + FieldTypeLookup lookup2 = lookup.copyAndAddAll("type2", newList(f2), emptyList()); assertEquals(1, size(lookup2.iterator())); assertSame(f.fieldType(), lookup2.get("foo")); @@ -81,13 +82,13 @@ public void testAddExistingField() { public void testCheckCompatibilityMismatchedTypes() { FieldMapper f1 = new MockFieldMapper("foo"); FieldTypeLookup lookup = new FieldTypeLookup(); - lookup = lookup.copyAndAddAll("type", newList(f1)); + lookup = lookup.copyAndAddAll("type", newList(f1), emptyList()); OtherFakeFieldType ft2 = new OtherFakeFieldType(); ft2.setName("foo"); FieldMapper f2 = new MockFieldMapper("foo", ft2); try { - lookup.copyAndAddAll("type2", newList(f2)); + lookup.copyAndAddAll("type2", newList(f2), emptyList()); fail("expected type mismatch"); } catch (IllegalArgumentException e) { assertTrue(e.getMessage().contains("cannot be changed from type [faketype] to [otherfaketype]")); @@ -97,41 +98,114 @@ public void testCheckCompatibilityMismatchedTypes() { public void testCheckCompatibilityConflict() { FieldMapper f1 = new MockFieldMapper("foo"); FieldTypeLookup lookup = new FieldTypeLookup(); - lookup = lookup.copyAndAddAll("type", newList(f1)); + lookup = lookup.copyAndAddAll("type", newList(f1), emptyList()); MappedFieldType ft2 = new MockFieldMapper.FakeFieldType(); ft2.setName("foo"); ft2.setBoost(2.0f); FieldMapper f2 = new MockFieldMapper("foo", ft2); - lookup.copyAndAddAll("type", newList(f2)); // boost is updateable, so ok since we are implicitly updating all types - lookup.copyAndAddAll("type2", newList(f2)); // boost is updateable, so ok if forcing + lookup.copyAndAddAll("type", newList(f2), emptyList()); // boost is updateable, so ok since we are implicitly updating all types + lookup.copyAndAddAll("type2", newList(f2), emptyList()); // boost is updateable, so ok if forcing // now with a non changeable setting MappedFieldType ft3 = new MockFieldMapper.FakeFieldType(); ft3.setName("foo"); ft3.setStored(true); FieldMapper f3 = new MockFieldMapper("foo", ft3); try { - lookup.copyAndAddAll("type2", newList(f3)); + lookup.copyAndAddAll("type2", newList(f3), emptyList()); fail("expected conflict"); } catch (IllegalArgumentException e) { assertTrue(e.getMessage().contains("has different [store] values")); } } - public void testSimpleMatchFullNames() { - MockFieldMapper f1 = new MockFieldMapper("foo"); - MockFieldMapper f2 = new MockFieldMapper("bar"); + public void testAddFieldAlias() { + MockFieldMapper field = new MockFieldMapper("foo"); + FieldAliasMapper alias = new FieldAliasMapper("alias", "alias", "foo"); + + FieldTypeLookup lookup = new FieldTypeLookup(); + lookup = lookup.copyAndAddAll("type", newList(field), newList(alias)); + + MappedFieldType aliasType = lookup.get("alias"); + assertEquals(field.fieldType(), aliasType); + } + + public void testUpdateFieldAlias() { + // Add an alias 'alias' to the concrete field 'foo'. + MockFieldMapper.FakeFieldType fieldType1 = new MockFieldMapper.FakeFieldType(); + MockFieldMapper field1 = new MockFieldMapper("foo", fieldType1); + FieldAliasMapper alias1 = new FieldAliasMapper("alias", "alias", "foo"); + + FieldTypeLookup lookup = new FieldTypeLookup(); + lookup = lookup.copyAndAddAll("type", newList(field1), newList(alias1)); + + // Check that the alias refers to 'foo'. + MappedFieldType aliasType1 = lookup.get("alias"); + assertEquals(fieldType1, aliasType1); + + // Update the alias to refer to a new concrete field 'bar'. + MockFieldMapper.FakeFieldType fieldType2 = new MockFieldMapper.FakeFieldType(); + fieldType2.setStored(!fieldType1.stored()); + MockFieldMapper field2 = new MockFieldMapper("bar", fieldType2); + + FieldAliasMapper alias2 = new FieldAliasMapper("alias", "alias", "bar"); + lookup = lookup.copyAndAddAll("type", newList(field2), newList(alias2)); + + // Check that the alias now refers to 'bar'. + MappedFieldType aliasType2 = lookup.get("alias"); + assertEquals(fieldType2, aliasType2); + } + + public void testUpdateConcreteFieldWithAlias() { + // Add an alias 'alias' to the concrete field 'foo'. + FieldAliasMapper alias1 = new FieldAliasMapper("alias", "alias", "foo"); + MockFieldMapper.FakeFieldType fieldType1 = new MockFieldMapper.FakeFieldType(); + fieldType1.setBoost(1.0f); + MockFieldMapper field1 = new MockFieldMapper("foo", fieldType1); + FieldTypeLookup lookup = new FieldTypeLookup(); - lookup = lookup.copyAndAddAll("type", newList(f1, f2)); + lookup = lookup.copyAndAddAll("type", newList(field1), newList(alias1)); + + // Check that the alias maps to this field type. + MappedFieldType aliasType1 = lookup.get("alias"); + assertEquals(fieldType1, aliasType1); + + // Update the boost for field 'foo'. + MockFieldMapper.FakeFieldType fieldType2 = new MockFieldMapper.FakeFieldType(); + fieldType2.setBoost(2.0f); + MockFieldMapper field2 = new MockFieldMapper("foo", fieldType2); + lookup = lookup.copyAndAddAll("type", newList(field2), emptyList()); + + // Check that the alias maps to the new field type. + MappedFieldType aliasType2 = lookup.get("alias"); + assertEquals(fieldType2, aliasType2); + } + + public void testSimpleMatchToFullName() { + MockFieldMapper field1 = new MockFieldMapper("foo"); + MockFieldMapper field2 = new MockFieldMapper("bar"); + + FieldAliasMapper alias1 = new FieldAliasMapper("food", "food", "path"); + FieldAliasMapper alias2 = new FieldAliasMapper("barometer", "barometer", "other-path"); + + FieldTypeLookup lookup = new FieldTypeLookup(); + lookup = lookup.copyAndAddAll("type", + newList(field1, field2), + newList(alias1, alias2)); + Collection names = lookup.simpleMatchToFullName("b*"); + assertFalse(names.contains("foo")); + assertFalse(names.contains("food")); + assertTrue(names.contains("bar")); + assertTrue(names.contains("barometer")); } public void testIteratorImmutable() { MockFieldMapper f1 = new MockFieldMapper("foo"); FieldTypeLookup lookup = new FieldTypeLookup(); - lookup = lookup.copyAndAddAll("type", newList(f1)); + lookup = lookup.copyAndAddAll("type", newList(f1), emptyList()); try { Iterator itr = lookup.iterator(); @@ -144,7 +218,11 @@ public void testIteratorImmutable() { } } - static List newList(FieldMapper... mapper) { + private static List newList(FieldMapper... mapper) { + return Arrays.asList(mapper); + } + + private static List newList(FieldAliasMapper... mapper) { return Arrays.asList(mapper); } diff --git a/server/src/test/java/org/elasticsearch/search/aggregations/bucket/RangeIT.java b/server/src/test/java/org/elasticsearch/search/aggregations/bucket/RangeIT.java index c2a2405098dab..edc29b0d2c574 100644 --- a/server/src/test/java/org/elasticsearch/search/aggregations/bucket/RangeIT.java +++ b/server/src/test/java/org/elasticsearch/search/aggregations/bucket/RangeIT.java @@ -116,6 +116,21 @@ public void setupSuiteScopeCluster() throws Exception { .field(SINGLE_VALUED_FIELD_NAME, i * 2 - 1) .endObject())); } + + // Create two indices and add the field 'route_length_miles' as an alias in + // one, and a concrete field in the other. + prepareCreate("old_index") + .addMapping("_doc", "distance", "type=double", "route_length_miles", "type=alias,path=distance") + .get(); + prepareCreate("new_index") + .addMapping("_doc", "route_length_miles", "type=double") + .get(); + + builders.add(client().prepareIndex("old_index", "_doc").setSource("distance", 42.0)); + builders.add(client().prepareIndex("old_index", "_doc").setSource("distance", 50.5)); + builders.add(client().prepareIndex("new_index", "_doc").setSource("route_length_miles", 100.2)); + builders.add(client().prepareIndex("new_index", "_doc").setSource(Collections.emptyMap())); + indexRandom(true, builders); ensureSearchable(); } @@ -972,4 +987,72 @@ public void testDontCacheScripts() throws Exception { assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() .getMissCount(), equalTo(1L)); } + + public void testFieldAlias() { + SearchResponse response = client().prepareSearch("old_index", "new_index") + .addAggregation(range("range") + .field("route_length_miles") + .addUnboundedTo(50.0) + .addRange(50.0, 150.0) + .addUnboundedFrom(150.0)) + .execute().actionGet(); + + assertSearchResponse(response); + + Range range = response.getAggregations().get("range"); + assertThat(range, notNullValue()); + assertThat(range.getName(), equalTo("range")); + List buckets = range.getBuckets(); + assertThat(buckets.size(), equalTo(3)); + + Range.Bucket bucket = buckets.get(0); + assertThat(bucket, notNullValue()); + assertThat(bucket.getKey(), equalTo("*-50.0")); + assertThat(bucket.getDocCount(), equalTo(1L)); + + bucket = buckets.get(1); + assertThat(bucket, notNullValue()); + assertThat(bucket.getKey(), equalTo("50.0-150.0")); + assertThat(bucket.getDocCount(), equalTo(2L)); + + bucket = buckets.get(2); + assertThat(bucket, notNullValue()); + assertThat(bucket.getKey(), equalTo("150.0-*")); + assertThat(bucket.getDocCount(), equalTo(0L)); + } + + + public void testFieldAliasWithMissingValue() { + SearchResponse response = client().prepareSearch("old_index", "new_index") + .addAggregation(range("range") + .field("route_length_miles") + .missing(0.0) + .addUnboundedTo(50.0) + .addRange(50.0, 150.0) + .addUnboundedFrom(150.0)) + .execute().actionGet(); + + assertSearchResponse(response); + + Range range = response.getAggregations().get("range"); + assertThat(range, notNullValue()); + assertThat(range.getName(), equalTo("range")); + List buckets = range.getBuckets(); + assertThat(buckets.size(), equalTo(3)); + + Range.Bucket bucket = buckets.get(0); + assertThat(bucket, notNullValue()); + assertThat(bucket.getKey(), equalTo("*-50.0")); + assertThat(bucket.getDocCount(), equalTo(2L)); + + bucket = buckets.get(1); + assertThat(bucket, notNullValue()); + assertThat(bucket.getKey(), equalTo("50.0-150.0")); + assertThat(bucket.getDocCount(), equalTo(2L)); + + bucket = buckets.get(2); + assertThat(bucket, notNullValue()); + assertThat(bucket.getKey(), equalTo("150.0-*")); + assertThat(bucket.getDocCount(), equalTo(0L)); + } } diff --git a/server/src/test/java/org/elasticsearch/search/aggregations/metrics/SumIT.java b/server/src/test/java/org/elasticsearch/search/aggregations/metrics/SumIT.java index 1b4ae466bb3bc..b3a5df4dbfc07 100644 --- a/server/src/test/java/org/elasticsearch/search/aggregations/metrics/SumIT.java +++ b/server/src/test/java/org/elasticsearch/search/aggregations/metrics/SumIT.java @@ -18,12 +18,7 @@ */ package org.elasticsearch.search.aggregations.metrics; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - +import org.elasticsearch.action.index.IndexRequestBuilder; import org.elasticsearch.action.search.SearchResponse; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.plugins.Plugin; @@ -36,6 +31,14 @@ import org.elasticsearch.search.aggregations.bucket.histogram.Histogram; import org.elasticsearch.search.aggregations.bucket.terms.Terms; import org.elasticsearch.search.aggregations.metrics.sum.Sum; +import org.hamcrest.core.IsNull; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import static org.elasticsearch.index.query.QueryBuilders.matchAllQuery; import static org.elasticsearch.index.query.QueryBuilders.termQuery; @@ -61,6 +64,33 @@ protected Collection> nodePlugins() { return Collections.singleton(MetricAggScriptPlugin.class); } + @Override + public void setupSuiteScopeCluster() throws Exception { + super.setupSuiteScopeCluster(); + + // Create two indices and add the field 'route_length_miles' as an alias in + // one, and a concrete field in the other. + prepareCreate("old_index") + .addMapping("_doc", + "transit_mode", "type=keyword", + "distance", "type=double", + "route_length_miles", "type=alias,path=distance") + .get(); + prepareCreate("new_index") + .addMapping("_doc", + "transit_mode", "type=keyword", + "route_length_miles", "type=double") + .get(); + + List builders = new ArrayList<>(); + builders.add(client().prepareIndex("old_index", "_doc").setSource("transit_mode", "train", "distance", 42.0)); + builders.add(client().prepareIndex("old_index", "_doc").setSource("transit_mode", "bus", "distance", 50.5)); + builders.add(client().prepareIndex("new_index", "_doc").setSource("transit_mode", "train", "route_length_miles", 100.2)); + + indexRandom(true, builders); + ensureSearchable(); + } + @Override public void testEmptyAggregation() throws Exception { @@ -382,4 +412,54 @@ public void testDontCacheScripts() throws Exception { assertThat(client().admin().indices().prepareStats("cache_test_idx").setRequestCache(true).get().getTotal().getRequestCache() .getMissCount(), equalTo(1L)); } + + public void testFieldAlias() { + SearchResponse response = client().prepareSearch("old_index", "new_index") + .addAggregation(sum("sum") + .field("route_length_miles")) + .execute().actionGet(); + + assertSearchResponse(response); + + Sum sum = response.getAggregations().get("sum"); + assertThat(sum, IsNull.notNullValue()); + assertThat(sum.getName(), equalTo("sum")); + assertThat(sum.getValue(), equalTo(192.7)); + } + + public void testFieldAliasInSubAggregation() { + SearchResponse response = client().prepareSearch("old_index", "new_index") + .addAggregation(terms("terms") + .field("transit_mode") + .subAggregation(sum("sum") + .field("route_length_miles"))) + .execute().actionGet(); + + assertSearchResponse(response); + + Terms terms = response.getAggregations().get("terms"); + assertThat(terms, notNullValue()); + assertThat(terms.getName(), equalTo("terms")); + + List buckets = terms.getBuckets(); + assertThat(buckets.size(), equalTo(2)); + + Terms.Bucket bucket = buckets.get(0); + assertThat(bucket, notNullValue()); + assertThat(bucket.getKey(), equalTo("train")); + assertThat(bucket.getDocCount(), equalTo(2L)); + + Sum sum = bucket.getAggregations().get("sum"); + assertThat(sum, notNullValue()); + assertThat(sum.getValue(), equalTo(142.2)); + + bucket = buckets.get(1); + assertThat(bucket, notNullValue()); + assertThat(bucket.getKey(), equalTo("bus")); + assertThat(bucket.getDocCount(), equalTo(1L)); + + sum = bucket.getAggregations().get("sum"); + assertThat(sum, notNullValue()); + assertThat(sum.getValue(), equalTo(50.5)); + } } diff --git a/server/src/test/java/org/elasticsearch/search/fetch/subphase/highlight/HighlighterSearchIT.java b/server/src/test/java/org/elasticsearch/search/fetch/subphase/highlight/HighlighterSearchIT.java index 35c5a19cc2e8c..c8bc5c3aa76df 100644 --- a/server/src/test/java/org/elasticsearch/search/fetch/subphase/highlight/HighlighterSearchIT.java +++ b/server/src/test/java/org/elasticsearch/search/fetch/subphase/highlight/HighlighterSearchIT.java @@ -171,6 +171,106 @@ public void testHighlightingWithWildcardName() throws IOException { } } + public void testFieldAlias() throws IOException { + XContentBuilder mappings = jsonBuilder() + .startObject() + .startObject("type") + .startObject("properties") + .startObject("text") + .field("type", "text") + .field("store", true) + .field("term_vector", "with_positions_offsets") + .endObject() + .startObject("alias") + .field("type", "alias") + .field("path", "text") + .endObject() + .endObject() + .endObject() + .endObject(); + assertAcked(prepareCreate("test").addMapping("type", mappings)); + + client().prepareIndex("test", "type", "1").setSource("text", "foo").get(); + refresh(); + + for (String type : ALL_TYPES) { + HighlightBuilder builder = new HighlightBuilder() + .field(new Field("alias").highlighterType(type)) + .requireFieldMatch(randomBoolean()); + SearchResponse search = client().prepareSearch() + .setQuery(matchQuery("alias", "foo")) + .highlighter(builder) + .get(); + assertHighlight(search, 0, "alias", 0, equalTo("foo")); + } + } + + public void testFieldAliasWithSourceLookup() throws IOException { + XContentBuilder mappings = jsonBuilder() + .startObject() + .startObject("type") + .startObject("properties") + .startObject("text") + .field("type", "text") + .field("analyzer", "whitespace") + .field("store", false) + .field("term_vector", "with_positions_offsets") + .endObject() + .startObject("alias") + .field("type", "alias") + .field("path", "text") + .endObject() + .endObject() + .endObject() + .endObject(); + assertAcked(prepareCreate("test").addMapping("type", mappings)); + + client().prepareIndex("test", "type", "1").setSource("text", "foo bar").get(); + refresh(); + + for (String type : ALL_TYPES) { + HighlightBuilder builder = new HighlightBuilder() + .field(new Field("alias").highlighterType(type)) + .requireFieldMatch(randomBoolean()); + SearchResponse search = client().prepareSearch() + .setQuery(matchQuery("alias", "bar")) + .highlighter(builder) + .get(); + assertHighlight(search, 0, "alias", 0, equalTo("foo bar")); + } + } + + public void testFieldAliasWithWildcardField() throws IOException { + XContentBuilder mappings = jsonBuilder() + .startObject() + .startObject("type") + .startObject("properties") + .startObject("keyword") + .field("type", "keyword") + .endObject() + .startObject("alias") + .field("type", "alias") + .field("path", "keyword") + .endObject() + .endObject() + .endObject() + .endObject(); + assertAcked(prepareCreate("test").addMapping("type", mappings)); + + client().prepareIndex("test", "type", "1").setSource("keyword", "foo").get(); + refresh(); + + HighlightBuilder builder = new HighlightBuilder() + .field(new Field("al*")) + .requireFieldMatch(false); + SearchResponse search = client().prepareSearch() + .setQuery(matchQuery("alias", "foo")) + .highlighter(builder) + .get(); + assertHighlight(search, 0, "alias", 0, equalTo("foo")); + } + + public void testHighlightingWhenFieldsAreNotStoredThereIsNoSource() throws IOException { XContentBuilder mappings = jsonBuilder(); mappings.startObject(); diff --git a/server/src/test/java/org/elasticsearch/search/fieldcaps/FieldCapabilitiesIT.java b/server/src/test/java/org/elasticsearch/search/fieldcaps/FieldCapabilitiesIT.java new file mode 100644 index 0000000000000..90b1437c93c06 --- /dev/null +++ b/server/src/test/java/org/elasticsearch/search/fieldcaps/FieldCapabilitiesIT.java @@ -0,0 +1,110 @@ +/* + * 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.search.fieldcaps; + +import org.elasticsearch.action.fieldcaps.FieldCapabilities; +import org.elasticsearch.action.fieldcaps.FieldCapabilitiesResponse; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentFactory; +import org.elasticsearch.test.ESIntegTestCase; +import org.junit.Before; + +import java.util.Map; + +import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertAcked; + +public class FieldCapabilitiesIT extends ESIntegTestCase { + + @Before + public void setUp() throws Exception { + super.setUp(); + + XContentBuilder oldIndexMapping = XContentFactory.jsonBuilder() + .startObject() + .startObject("_doc") + .startObject("properties") + .startObject("distance") + .field("type", "double") + .endObject() + .startObject("route_length_miles") + .field("type", "alias") + .field("path", "distance") + .endObject() + .endObject() + .endObject() + .endObject(); + assertAcked(prepareCreate("old_index").addMapping("_doc", oldIndexMapping)); + + XContentBuilder newIndexMapping = XContentFactory.jsonBuilder() + .startObject() + .startObject("_doc") + .startObject("properties") + .startObject("distance") + .field("type", "text") + .endObject() + .startObject("route_length_miles") + .field("type", "double") + .endObject() + .endObject() + .endObject() + .endObject(); + assertAcked(prepareCreate("new_index").addMapping("_doc", newIndexMapping)); + } + + public void testFieldAlias() { + FieldCapabilitiesResponse response = client().prepareFieldCaps().setFields("distance", "route_length_miles") + .execute().actionGet(); + + // Ensure the response has entries for both requested fields. + assertTrue(response.get().containsKey("distance")); + assertTrue(response.get().containsKey("route_length_miles")); + + // Check the capabilities for the 'distance' field. + Map distance = response.getField("distance"); + assertEquals(2, distance.size()); + + assertTrue(distance.containsKey("double")); + assertEquals( + new FieldCapabilities("distance", "double", true, true, new String[] {"old_index"}, null, null), + distance.get("double")); + + assertTrue(distance.containsKey("text")); + assertEquals( + new FieldCapabilities("distance", "text", true, false, new String[] {"new_index"}, null, null), + distance.get("text")); + + // Check the capabilities for the 'route_length_miles' alias. + Map routeLength = response.getField("route_length_miles"); + assertEquals(1, routeLength.size()); + + assertTrue(routeLength.containsKey("double")); + assertEquals( + new FieldCapabilities("route_length_miles", "double", true, true), + routeLength.get("double")); + } + + public void testFieldAliasWithWildcardField() { + FieldCapabilitiesResponse response = client().prepareFieldCaps().setFields("route*") + .execute().actionGet(); + + assertEquals(1, response.get().size()); + assertTrue(response.get().containsKey("route_length_miles")); + } +} diff --git a/server/src/test/java/org/elasticsearch/search/fields/SearchFieldsIT.java b/server/src/test/java/org/elasticsearch/search/fields/SearchFieldsIT.java index ab5387b6e3f48..ec704f755ae7f 100644 --- a/server/src/test/java/org/elasticsearch/search/fields/SearchFieldsIT.java +++ b/server/src/test/java/org/elasticsearch/search/fields/SearchFieldsIT.java @@ -30,6 +30,7 @@ import org.elasticsearch.common.document.DocumentField; import org.elasticsearch.common.joda.Joda; import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentFactory; import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.common.xcontent.support.XContentMapValues; @@ -49,6 +50,8 @@ import org.joda.time.DateTime; import org.joda.time.DateTimeZone; import org.joda.time.ReadableDateTime; +import org.joda.time.format.DateTimeFormat; +import org.joda.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.Arrays; @@ -912,6 +915,71 @@ public void testScriptFields() throws Exception { } } + public void testDocValueFieldsWithFieldAlias() throws Exception { + XContentBuilder mapping = XContentFactory.jsonBuilder() + .startObject() + .startObject("type") + .startObject("_source") + .field("enabled", false) + .endObject() + .startObject("properties") + .startObject("text_field") + .field("type", "text") + .field("fielddata", true) + .endObject() + .startObject("date_field") + .field("type", "date") + .field("format", "yyyy-MM-dd") + .endObject() + .startObject("text_field_alias") + .field("type", "alias") + .field("path", "text_field") + .endObject() + .startObject("date_field_alias") + .field("type", "alias") + .field("path", "date_field") + .endObject() + .endObject() + .endObject() + .endObject(); + assertAcked(prepareCreate("test").addMapping("type", mapping)); + ensureGreen("test"); + + DateTime date = new DateTime(1990, 12, 29, 0, 0, DateTimeZone.UTC); + DateTimeFormatter formatter = DateTimeFormat.forPattern("yyyy-MM-dd"); + + index("test", "type", "1", "text_field", "foo", "date_field", formatter.print(date)); + refresh("test"); + + SearchRequestBuilder builder = client().prepareSearch().setQuery(matchAllQuery()) + .addDocValueField("text_field_alias") + .addDocValueField("date_field_alias", "use_field_mapping") + .addDocValueField("date_field"); + SearchResponse searchResponse = builder.execute().actionGet(); + + assertNoFailures(searchResponse); + assertHitCount(searchResponse, 1); + SearchHit hit = searchResponse.getHits().getAt(0); + + Map fields = hit.getFields(); + assertThat(fields.keySet(), equalTo(newHashSet("text_field_alias", "date_field_alias", "date_field"))); + + DocumentField textFieldAlias = fields.get("text_field_alias"); + assertThat(textFieldAlias.getName(), equalTo("text_field_alias")); + assertThat(textFieldAlias.getValue(), equalTo("foo")); + + DocumentField dateFieldAlias = fields.get("date_field_alias"); + assertThat(dateFieldAlias.getName(), equalTo("date_field_alias")); + assertThat(dateFieldAlias.getValue(), + equalTo("1990-12-29")); + + DocumentField dateField = fields.get("date_field"); + assertThat(dateField.getName(), equalTo("date_field")); + + ReadableDateTime fetchedDate = dateField.getValue(); + assertThat(fetchedDate, equalTo(date)); + } + public void testLoadMetadata() throws Exception { assertAcked(prepareCreate("test")); diff --git a/server/src/test/java/org/elasticsearch/search/query/ExistsIT.java b/server/src/test/java/org/elasticsearch/search/query/ExistsIT.java index 6e4a1b7d618ea..45910044d499f 100644 --- a/server/src/test/java/org/elasticsearch/search/query/ExistsIT.java +++ b/server/src/test/java/org/elasticsearch/search/query/ExistsIT.java @@ -24,6 +24,7 @@ import org.elasticsearch.action.search.SearchResponse; import org.elasticsearch.common.Strings; import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentFactory; import org.elasticsearch.common.xcontent.json.JsonXContent; import org.elasticsearch.index.query.QueryBuilders; import org.elasticsearch.search.SearchHit; @@ -141,4 +142,89 @@ public void testExists() throws Exception { } } } + + public void testFieldAlias() throws Exception { + XContentBuilder mapping = XContentFactory.jsonBuilder() + .startObject() + .startObject("type") + .startObject("properties") + .startObject("bar") + .field("type", "long") + .endObject() + .startObject("foo") + .field("type", "object") + .startObject("properties") + .startObject("bar") + .field("type", "double") + .endObject() + .endObject() + .endObject() + .startObject("foo-bar") + .field("type", "alias") + .field("path", "foo.bar") + .endObject() + .endObject() + .endObject() + .endObject(); + assertAcked(prepareCreate("idx").addMapping("type", mapping)); + ensureGreen("idx"); + + List indexRequests = new ArrayList<>(); + indexRequests.add(client().prepareIndex("idx", "type").setSource(emptyMap())); + indexRequests.add(client().prepareIndex("idx", "type").setSource(emptyMap())); + indexRequests.add(client().prepareIndex("idx", "type").setSource("bar", 3)); + indexRequests.add(client().prepareIndex("idx", "type").setSource("foo", singletonMap("bar", 2.718))); + indexRequests.add(client().prepareIndex("idx", "type").setSource("foo", singletonMap("bar", 6.283))); + indexRandom(true, false, indexRequests); + + Map expected = new LinkedHashMap<>(); + expected.put("foo.bar", 2); + expected.put("foo-bar", 2); + expected.put("foo*", 2); + expected.put("*bar", 3); + + for (Map.Entry entry : expected.entrySet()) { + String fieldName = entry.getKey(); + int expectedCount = entry.getValue(); + + SearchResponse response = client().prepareSearch("idx") + .setQuery(QueryBuilders.existsQuery(fieldName)) + .get(); + assertSearchResponse(response); + assertHitCount(response, expectedCount); + } + } + + public void testFieldAliasWithNoDocValues() throws Exception { + XContentBuilder mapping = XContentFactory.jsonBuilder() + .startObject() + .startObject("type") + .startObject("properties") + .startObject("foo") + .field("type", "long") + .field("doc_values", false) + .endObject() + .startObject("foo-alias") + .field("type", "alias") + .field("path", "foo") + .endObject() + .endObject() + .endObject() + .endObject(); + assertAcked(prepareCreate("idx").addMapping("type", mapping)); + ensureGreen("idx"); + + List indexRequests = new ArrayList<>(); + indexRequests.add(client().prepareIndex("idx", "type").setSource(emptyMap())); + indexRequests.add(client().prepareIndex("idx", "type").setSource(emptyMap())); + indexRequests.add(client().prepareIndex("idx", "type").setSource("foo", 3)); + indexRequests.add(client().prepareIndex("idx", "type").setSource("foo", 43)); + indexRandom(true, false, indexRequests); + + SearchResponse response = client().prepareSearch("idx") + .setQuery(QueryBuilders.existsQuery("foo-alias")) + .get(); + assertSearchResponse(response); + assertHitCount(response, 2); + } } diff --git a/server/src/test/java/org/elasticsearch/search/query/QueryStringIT.java b/server/src/test/java/org/elasticsearch/search/query/QueryStringIT.java index 7145f9db2db23..5caab8c9dfec6 100644 --- a/server/src/test/java/org/elasticsearch/search/query/QueryStringIT.java +++ b/server/src/test/java/org/elasticsearch/search/query/QueryStringIT.java @@ -51,6 +51,7 @@ import static org.elasticsearch.test.StreamsUtils.copyToStringFromClasspath; import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertAcked; import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertHitCount; +import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertNoFailures; import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertSearchHits; import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.containsString; @@ -376,6 +377,70 @@ public void testLimitOnExpandedFields() throws Exception { containsString("field expansion matches too many fields, limit: 1024, got: 1025")); } + public void testFieldAlias() throws Exception { + List indexRequests = new ArrayList<>(); + indexRequests.add(client().prepareIndex("test", "_doc", "1").setSource("f3", "text", "f2", "one")); + indexRequests.add(client().prepareIndex("test", "_doc", "2").setSource("f3", "value", "f2", "two")); + indexRequests.add(client().prepareIndex("test", "_doc", "3").setSource("f3", "another value", "f2", "three")); + indexRandom(true, false, indexRequests); + + SearchResponse response = client().prepareSearch("test") + .setQuery(queryStringQuery("value").field("f3_alias")) + .execute().actionGet(); + + assertNoFailures(response); + assertHitCount(response, 2); + assertHits(response.getHits(), "2", "3"); + } + + public void testFieldAliasWithEmbeddedFieldNames() throws Exception { + List indexRequests = new ArrayList<>(); + indexRequests.add(client().prepareIndex("test", "_doc", "1").setSource("f3", "text", "f2", "one")); + indexRequests.add(client().prepareIndex("test", "_doc", "2").setSource("f3", "value", "f2", "two")); + indexRequests.add(client().prepareIndex("test", "_doc", "3").setSource("f3", "another value", "f2", "three")); + indexRandom(true, false, indexRequests); + + SearchResponse response = client().prepareSearch("test") + .setQuery(queryStringQuery("f3_alias:value AND f2:three")) + .execute().actionGet(); + + assertNoFailures(response); + assertHitCount(response, 1); + assertHits(response.getHits(), "3"); + } + + public void testFieldAliasWithWildcardField() throws Exception { + List indexRequests = new ArrayList<>(); + indexRequests.add(client().prepareIndex("test", "_doc", "1").setSource("f3", "text", "f2", "one")); + indexRequests.add(client().prepareIndex("test", "_doc", "2").setSource("f3", "value", "f2", "two")); + indexRequests.add(client().prepareIndex("test", "_doc", "3").setSource("f3", "another value", "f2", "three")); + indexRandom(true, false, indexRequests); + + SearchResponse response = client().prepareSearch("test") + .setQuery(queryStringQuery("value").field("f3_*")) + .execute().actionGet(); + + assertNoFailures(response); + assertHitCount(response, 2); + assertHits(response.getHits(), "2", "3"); + } + + public void testFieldAliasOnDisallowedFieldType() throws Exception { + List indexRequests = new ArrayList<>(); + indexRequests.add(client().prepareIndex("test", "_doc", "1").setSource("f3", "text", "f2", "one")); + indexRandom(true, false, indexRequests); + + // The wildcard field matches aliases for both a text and boolean field. + // By default, the boolean field should be ignored when building the query. + SearchResponse response = client().prepareSearch("test") + .setQuery(queryStringQuery("text").field("f*_alias")) + .execute().actionGet(); + + assertNoFailures(response); + assertHitCount(response, 1); + assertHits(response.getHits(), "1"); + } + private void assertHits(SearchHits hits, String... ids) { assertThat(hits.getTotalHits(), equalTo((long) ids.length)); Set hitIds = new HashSet<>(); diff --git a/server/src/test/java/org/elasticsearch/search/query/SimpleQueryStringIT.java b/server/src/test/java/org/elasticsearch/search/query/SimpleQueryStringIT.java index d7db62b61438c..5176c327ac78e 100644 --- a/server/src/test/java/org/elasticsearch/search/query/SimpleQueryStringIT.java +++ b/server/src/test/java/org/elasticsearch/search/query/SimpleQueryStringIT.java @@ -58,6 +58,7 @@ import static java.util.Collections.singletonList; import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder; import static org.elasticsearch.index.query.QueryBuilders.boolQuery; +import static org.elasticsearch.index.query.QueryBuilders.queryStringQuery; import static org.elasticsearch.index.query.QueryBuilders.simpleQueryStringQuery; import static org.elasticsearch.index.query.QueryBuilders.termQuery; import static org.elasticsearch.test.StreamsUtils.copyToStringFromClasspath; @@ -585,6 +586,67 @@ public void testLimitOnExpandedFields() throws Exception { containsString("field expansion matches too many fields, limit: 1024, got: 1025")); } + public void testFieldAlias() throws Exception { + String indexBody = copyToStringFromClasspath("/org/elasticsearch/search/query/all-query-index.json"); + assertAcked(prepareCreate("test").setSource(indexBody, XContentType.JSON)); + ensureGreen("test"); + + List indexRequests = new ArrayList<>(); + indexRequests.add(client().prepareIndex("test", "_doc", "1").setSource("f3", "text", "f2", "one")); + indexRequests.add(client().prepareIndex("test", "_doc", "2").setSource("f3", "value", "f2", "two")); + indexRequests.add(client().prepareIndex("test", "_doc", "3").setSource("f3", "another value", "f2", "three")); + indexRandom(true, false, indexRequests); + + SearchResponse response = client().prepareSearch("test") + .setQuery(simpleQueryStringQuery("value").field("f3_alias")) + .execute().actionGet(); + + assertNoFailures(response); + assertHitCount(response, 2); + assertHits(response.getHits(), "2", "3"); + } + + public void testFieldAliasWithWildcardField() throws Exception { + String indexBody = copyToStringFromClasspath("/org/elasticsearch/search/query/all-query-index.json"); + assertAcked(prepareCreate("test").setSource(indexBody, XContentType.JSON)); + ensureGreen("test"); + + List indexRequests = new ArrayList<>(); + indexRequests.add(client().prepareIndex("test", "_doc", "1").setSource("f3", "text", "f2", "one")); + indexRequests.add(client().prepareIndex("test", "_doc", "2").setSource("f3", "value", "f2", "two")); + indexRequests.add(client().prepareIndex("test", "_doc", "3").setSource("f3", "another value", "f2", "three")); + indexRandom(true, false, indexRequests); + + SearchResponse response = client().prepareSearch("test") + .setQuery(simpleQueryStringQuery("value").field("f3_*")) + .execute().actionGet(); + + assertNoFailures(response); + assertHitCount(response, 2); + assertHits(response.getHits(), "2", "3"); + } + + + public void testFieldAliasOnDisallowedFieldType() throws Exception { + String indexBody = copyToStringFromClasspath("/org/elasticsearch/search/query/all-query-index.json"); + assertAcked(prepareCreate("test").setSource(indexBody, XContentType.JSON)); + ensureGreen("test"); + + List indexRequests = new ArrayList<>(); + indexRequests.add(client().prepareIndex("test", "_doc", "1").setSource("f3", "text", "f2", "one")); + indexRandom(true, false, indexRequests); + + // The wildcard field matches aliases for both a text and boolean field. + // By default, the boolean field should be ignored when building the query. + SearchResponse response = client().prepareSearch("test") + .setQuery(queryStringQuery("text").field("f*_alias")) + .execute().actionGet(); + + assertNoFailures(response); + assertHitCount(response, 1); + assertHits(response.getHits(), "1"); + } + private void assertHits(SearchHits hits, String... ids) { assertThat(hits.getTotalHits(), equalTo((long) ids.length)); Set hitIds = new HashSet<>(); diff --git a/server/src/test/java/org/elasticsearch/search/sort/FieldSortIT.java b/server/src/test/java/org/elasticsearch/search/sort/FieldSortIT.java index 3af370326f5e4..ff0196aacdf16 100644 --- a/server/src/test/java/org/elasticsearch/search/sort/FieldSortIT.java +++ b/server/src/test/java/org/elasticsearch/search/sort/FieldSortIT.java @@ -40,6 +40,7 @@ import org.elasticsearch.script.Script; import org.elasticsearch.script.ScriptType; import org.elasticsearch.search.SearchHit; +import org.elasticsearch.search.SearchHits; import org.elasticsearch.test.ESIntegTestCase; import org.elasticsearch.test.InternalSettingsPlugin; import org.hamcrest.Matchers; @@ -1572,4 +1573,60 @@ public void testScriptFieldSort() throws Exception { } } } + + public void testFieldAlias() throws Exception { + // Create two indices and add the field 'route_length_miles' as an alias in + // one, and a concrete field in the other. + assertAcked(prepareCreate("old_index") + .addMapping("_doc", "distance", "type=double", "route_length_miles", "type=alias,path=distance")); + assertAcked(prepareCreate("new_index") + .addMapping("_doc", "route_length_miles", "type=double")); + ensureGreen("old_index", "new_index"); + + List builders = new ArrayList<>(); + builders.add(client().prepareIndex("old_index", "_doc").setSource("distance", 42.0)); + builders.add(client().prepareIndex("old_index", "_doc").setSource("distance", 50.5)); + builders.add(client().prepareIndex("new_index", "_doc").setSource("route_length_miles", 100.2)); + indexRandom(true, true, builders); + + SearchResponse response = client().prepareSearch() + .setQuery(matchAllQuery()) + .setSize(builders.size()) + .addSort(SortBuilders.fieldSort("route_length_miles")) + .execute().actionGet(); + SearchHits hits = response.getHits(); + + assertEquals(3, hits.getHits().length); + assertEquals(42.0, hits.getAt(0).getSortValues()[0]); + assertEquals(50.5, hits.getAt(1).getSortValues()[0]); + assertEquals(100.2, hits.getAt(2).getSortValues()[0]); + } + + public void testFieldAliasesWithMissingValues() throws Exception { + // Create two indices and add the field 'route_length_miles' as an alias in + // one, and a concrete field in the other. + assertAcked(prepareCreate("old_index") + .addMapping("_doc", "distance", "type=double", "route_length_miles", "type=alias,path=distance")); + assertAcked(prepareCreate("new_index") + .addMapping("_doc", "route_length_miles", "type=double")); + ensureGreen("old_index", "new_index"); + + List builders = new ArrayList<>(); + builders.add(client().prepareIndex("old_index", "_doc").setSource("distance", 42.0)); + builders.add(client().prepareIndex("old_index", "_doc").setSource(Collections.emptyMap())); + builders.add(client().prepareIndex("new_index", "_doc").setSource("route_length_miles", 100.2)); + indexRandom(true, true, builders); + + SearchResponse response = client().prepareSearch() + .setQuery(matchAllQuery()) + .setSize(builders.size()) + .addSort(SortBuilders.fieldSort("route_length_miles").missing(120.3)) + .execute().actionGet(); + SearchHits hits = response.getHits(); + + assertEquals(3, hits.getHits().length); + assertEquals(42.0, hits.getAt(0).getSortValues()[0]); + assertEquals(100.2, hits.getAt(1).getSortValues()[0]); + assertEquals(120.3, hits.getAt(2).getSortValues()[0]); + } } diff --git a/server/src/test/java/org/elasticsearch/search/suggest/AbstractSuggestionBuilderTestCase.java b/server/src/test/java/org/elasticsearch/search/suggest/AbstractSuggestionBuilderTestCase.java index eb31f19ad4e83..c753bf5abec15 100644 --- a/server/src/test/java/org/elasticsearch/search/suggest/AbstractSuggestionBuilderTestCase.java +++ b/server/src/test/java/org/elasticsearch/search/suggest/AbstractSuggestionBuilderTestCase.java @@ -159,7 +159,7 @@ public void testBuild() throws IOException { indexSettings); MapperService mapperService = mock(MapperService.class); ScriptService scriptService = mock(ScriptService.class); - MappedFieldType fieldType = mockFieldType(); + MappedFieldType fieldType = mockFieldType(suggestionBuilder.field()); boolean fieldTypeSearchAnalyzerSet = randomBoolean(); if (fieldTypeSearchAnalyzerSet) { NamedAnalyzer searchAnalyzer = new NamedAnalyzer("fieldSearchAnalyzer", AnalyzerScope.INDEX, new SimpleAnalyzer()); @@ -210,8 +210,10 @@ public void testBuild() throws IOException { */ protected abstract void assertSuggestionContext(SB builder, SuggestionContext context) throws IOException; - protected MappedFieldType mockFieldType() { - return mock(MappedFieldType.class); + protected MappedFieldType mockFieldType(String fieldName) { + MappedFieldType fieldType = mock(MappedFieldType.class); + when(fieldType.name()).thenReturn(fieldName); + return fieldType; } /** diff --git a/server/src/test/java/org/elasticsearch/search/suggest/CompletionSuggestSearchIT.java b/server/src/test/java/org/elasticsearch/search/suggest/CompletionSuggestSearchIT.java index 0717e1be2121e..23fb51be930ba 100644 --- a/server/src/test/java/org/elasticsearch/search/suggest/CompletionSuggestSearchIT.java +++ b/server/src/test/java/org/elasticsearch/search/suggest/CompletionSuggestSearchIT.java @@ -19,7 +19,6 @@ package org.elasticsearch.search.suggest; import com.carrotsearch.randomizedtesting.generators.RandomStrings; - import org.apache.lucene.analysis.TokenStreamToAutomaton; import org.apache.lucene.search.suggest.document.ContextSuggestField; import org.apache.lucene.util.LuceneTestCase.SuppressCodecs; @@ -36,6 +35,7 @@ import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.unit.Fuzziness; import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentFactory; import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.index.mapper.MapperParsingException; import org.elasticsearch.plugins.Plugin; @@ -1161,6 +1161,32 @@ public void testMultiDocSuggestions() throws Exception { assertSuggestions("foo", prefix, "suggester10", "suggester9", "suggester8", "suggester7", "suggester6"); } + public void testSuggestWithFieldAlias() throws Exception { + XContentBuilder mapping = XContentFactory.jsonBuilder() + .startObject() + .startObject(TYPE) + .startObject("properties") + .startObject(FIELD) + .field("type", "completion") + .endObject() + .startObject("alias") + .field("type", "alias") + .field("path", FIELD) + .endObject() + .endObject() + .endObject() + .endObject(); + assertAcked(prepareCreate(INDEX).addMapping(TYPE, mapping)); + + List builders = new ArrayList<>(); + builders.add(client().prepareIndex(INDEX, TYPE).setSource(FIELD, "apple")); + builders.add(client().prepareIndex(INDEX, TYPE).setSource(FIELD, "mango")); + builders.add(client().prepareIndex(INDEX, TYPE).setSource(FIELD, "papaya")); + indexRandom(true, false, builders); + + CompletionSuggestionBuilder suggestionBuilder = SuggestBuilders.completionSuggestion("alias").text("app"); + assertSuggestions("suggestion", suggestionBuilder, "apple"); + } public static boolean isReservedChar(char c) { switch (c) { diff --git a/server/src/test/java/org/elasticsearch/search/suggest/SuggestSearchIT.java b/server/src/test/java/org/elasticsearch/search/suggest/SuggestSearchIT.java index 677cc4163ccf7..aaeaadd4c9f83 100644 --- a/server/src/test/java/org/elasticsearch/search/suggest/SuggestSearchIT.java +++ b/server/src/test/java/org/elasticsearch/search/suggest/SuggestSearchIT.java @@ -979,6 +979,35 @@ public void testSuggestWithManyCandidates() throws InterruptedException, Executi // assertThat(total, lessThan(1000L)); // Takes many seconds without fix - just for debugging } + public void testSuggestWithFieldAlias() throws Exception { + XContentBuilder mapping = XContentFactory.jsonBuilder() + .startObject() + .startObject("type") + .startObject("properties") + .startObject("text") + .field("type", "keyword") + .endObject() + .startObject("alias") + .field("type", "alias") + .field("path", "text") + .endObject() + .endObject() + .endObject() + .endObject(); + assertAcked(prepareCreate("test").addMapping("type", mapping)); + + List builders = new ArrayList<>(); + builders.add(client().prepareIndex("test", "type").setSource("text", "apple")); + builders.add(client().prepareIndex("test", "type").setSource("text", "mango")); + builders.add(client().prepareIndex("test", "type").setSource("text", "papaya")); + indexRandom(true, false, builders); + + TermSuggestionBuilder termSuggest = termSuggestion("alias").text("appple"); + + Suggest searchSuggest = searchSuggest("suggestion", termSuggest); + assertSuggestion(searchSuggest, 0, "suggestion", "apple"); + } + @Override protected Collection> nodePlugins() { return Collections.singleton(DummyTemplatePlugin.class); diff --git a/server/src/test/java/org/elasticsearch/search/suggest/completion/CompletionSuggesterBuilderTests.java b/server/src/test/java/org/elasticsearch/search/suggest/completion/CompletionSuggesterBuilderTests.java index 862916890e1bb..3e6935ecc64f5 100644 --- a/server/src/test/java/org/elasticsearch/search/suggest/completion/CompletionSuggesterBuilderTests.java +++ b/server/src/test/java/org/elasticsearch/search/suggest/completion/CompletionSuggesterBuilderTests.java @@ -164,8 +164,9 @@ protected void mutateSpecificParameters(CompletionSuggestionBuilder builder) thr } @Override - protected MappedFieldType mockFieldType() { + protected MappedFieldType mockFieldType(String fieldName) { CompletionFieldType completionFieldType = new CompletionFieldType(); + completionFieldType.setName(fieldName); completionFieldType.setContextMappings(new ContextMappings(contextMappings)); return completionFieldType; } diff --git a/server/src/test/resources/org/elasticsearch/search/query/all-query-index.json b/server/src/test/resources/org/elasticsearch/search/query/all-query-index.json index 72c9b54f6e3da..abdc11928229f 100644 --- a/server/src/test/resources/org/elasticsearch/search/query/all-query-index.json +++ b/server/src/test/resources/org/elasticsearch/search/query/all-query-index.json @@ -11,6 +11,10 @@ "f1": {"type": "text"}, "f2": {"type": "keyword"}, "f3": {"type": "text"}, + "f3_alias": { + "type": "alias", + "path": "f3" + }, "f4": { "type": "text", "index_options": "docs" @@ -42,6 +46,10 @@ "format": "yyyy/MM/dd||epoch_millis" }, "f_bool": {"type": "boolean"}, + "f_bool_alias": { + "type": "alias", + "path": "f_bool" + }, "f_byte": {"type": "byte"}, "f_short": {"type": "short"}, "f_int": {"type": "integer"},