diff --git a/docs/reference/mapping/types/text.asciidoc b/docs/reference/mapping/types/text.asciidoc index 2a6e2f3ef8ae8..9ffed270aa54c 100644 --- a/docs/reference/mapping/types/text.asciidoc +++ b/docs/reference/mapping/types/text.asciidoc @@ -174,6 +174,8 @@ a <> sub-field that supports synthetic `_source` or if the `text` field sets `store` to `true`. Either way, it may not have <>. +`store` will be set to `true` by default if synthetic source is enabled and there is no suitable <> sub-field. + If using a sub-`keyword` field then the values are sorted in the same way as a `keyword` field's values are sorted. By default, that means sorted with duplicates removed. So: diff --git a/server/src/main/java/org/elasticsearch/index/mapper/FieldMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/FieldMapper.java index 71fd9edd49903..d49cf415071ff 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/FieldMapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/FieldMapper.java @@ -678,6 +678,10 @@ public void setValue(T value) { this.value = value; } + public boolean isSet() { + return isSet; + } + public boolean isConfigured() { return isSet && Objects.equals(value, getDefaultValue()) == false; } diff --git a/server/src/main/java/org/elasticsearch/index/mapper/TextFieldMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/TextFieldMapper.java index faa840dacc732..daadc93cf3aee 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/TextFieldMapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/TextFieldMapper.java @@ -387,13 +387,9 @@ private static KeywordFieldMapper.KeywordFieldType syntheticSourceDelegate(Field if (fieldType.stored()) { return null; } - for (Mapper sub : multiFields) { - if (sub.typeName().equals(KeywordFieldMapper.CONTENT_TYPE)) { - KeywordFieldMapper kwd = (KeywordFieldMapper) sub; - if (kwd.hasNormalizer() == false && (kwd.fieldType().hasDocValues() || kwd.fieldType().isStored())) { - return kwd.fieldType(); - } - } + var kwd = getKeywordFieldMapperForSyntheticSource(multiFields); + if (kwd != null) { + return kwd.fieldType(); } return null; } @@ -460,6 +456,20 @@ private SubFieldInfo buildPhraseInfo(FieldType fieldType, TextFieldType parent) @Override public TextFieldMapper build(MapperBuilderContext context) { MultiFields multiFields = multiFieldsBuilder.build(this, context); + + // If synthetic source is used we need to either store this field + // to recreate the source or use keyword multi-fields for that. + // So if there are no suitable multi-fields we will default to + // storing the field without requiring users to explicitly set 'store'. + // + // If 'store' parameter was explicitly provided we'll let it pass and + // fail in TextFieldMapper#syntheticFieldLoader later if needed. + if (store.isSet() == false + && context.isSourceSynthetic() + && TextFieldMapper.getKeywordFieldMapperForSyntheticSource(multiFields) == null) { + store.setValue(true); + } + FieldType fieldType = TextParams.buildFieldType( index, store, @@ -1454,15 +1464,12 @@ protected void write(XContentBuilder b, Object value) throws IOException { } }; } - for (Mapper sub : this) { - if (sub.typeName().equals(KeywordFieldMapper.CONTENT_TYPE)) { - KeywordFieldMapper kwd = (KeywordFieldMapper) sub; - if (kwd.hasNormalizer() == false && (kwd.fieldType().hasDocValues() || kwd.fieldType().isStored())) { - return kwd.syntheticFieldLoader(simpleName()); - } - } + var kwd = getKeywordFieldMapperForSyntheticSource(this); + if (kwd != null) { + return kwd.syntheticFieldLoader(simpleName()); } + throw new IllegalArgumentException( String.format( Locale.ROOT, @@ -1473,4 +1480,17 @@ protected void write(XContentBuilder b, Object value) throws IOException { ) ); } + + private static KeywordFieldMapper getKeywordFieldMapperForSyntheticSource(Iterable multiFields) { + for (Mapper sub : multiFields) { + if (sub.typeName().equals(KeywordFieldMapper.CONTENT_TYPE)) { + KeywordFieldMapper kwd = (KeywordFieldMapper) sub; + if (kwd.hasNormalizer() == false && (kwd.fieldType().hasDocValues() || kwd.fieldType().isStored())) { + return kwd; + } + } + } + + return null; + } } diff --git a/server/src/test/java/org/elasticsearch/index/mapper/SourceLoaderTests.java b/server/src/test/java/org/elasticsearch/index/mapper/SourceLoaderTests.java index aa30efb7dbc51..939dd0613fb6e 100644 --- a/server/src/test/java/org/elasticsearch/index/mapper/SourceLoaderTests.java +++ b/server/src/test/java/org/elasticsearch/index/mapper/SourceLoaderTests.java @@ -36,7 +36,9 @@ public void testEmptyObject() throws IOException { public void testUnsupported() throws IOException { Exception e = expectThrows( IllegalArgumentException.class, - () -> createDocumentMapper(syntheticSourceMapping(b -> b.startObject("txt").field("type", "text").endObject())) + () -> createDocumentMapper( + syntheticSourceMapping(b -> b.startObject("txt").field("type", "text").field("store", "false").endObject()) + ) ); assertThat( e.getMessage(), diff --git a/server/src/test/java/org/elasticsearch/index/mapper/TextFieldMapperTests.java b/server/src/test/java/org/elasticsearch/index/mapper/TextFieldMapperTests.java index f92867d1ce461..5c78b66404c7d 100644 --- a/server/src/test/java/org/elasticsearch/index/mapper/TextFieldMapperTests.java +++ b/server/src/test/java/org/elasticsearch/index/mapper/TextFieldMapperTests.java @@ -1119,6 +1119,7 @@ protected boolean supportsIgnoreMalformed() { protected SyntheticSourceSupport syntheticSourceSupport(boolean ignoreMalformed) { assumeFalse("ignore_malformed not supported", ignoreMalformed); boolean storeTextField = randomBoolean(); + boolean explicitlyStoreTextField = randomBoolean(); boolean storedKeywordField = storeTextField || randomBoolean(); boolean indexText = randomBoolean(); Integer ignoreAbove = randomBoolean() ? null : between(10, 100); @@ -1138,7 +1139,10 @@ public SyntheticSourceExample example(int maxValues) { delegate.expectedForSyntheticSource(), delegate.expectedForBlockLoader(), b -> { - b.field("type", "text").field("store", true); + b.field("type", "text"); + if (explicitlyStoreTextField) { + b.field("store", true); + } if (indexText == false) { b.field("index", false); } @@ -1174,30 +1178,42 @@ public List invalidExample() throws IOException { "field [field] of type [text] doesn't support synthetic source unless it is stored or" + " has a sub-field of type [keyword] with doc values or stored and without a normalizer" ); - return List.of( - new SyntheticSourceInvalidExample(err, TextFieldMapperTests.this::minimalMapping), - new SyntheticSourceInvalidExample(err, b -> { - b.field("type", "text"); - b.startObject("fields"); - { - b.startObject("l"); - b.field("type", "long"); - b.endObject(); - } + return List.of(new SyntheticSourceInvalidExample(err, b -> { + b.field("type", "text"); + b.field("store", "false"); + }), new SyntheticSourceInvalidExample(err, b -> { + b.field("type", "text"); + b.field("store", "false"); + b.startObject("fields"); + { + b.startObject("l"); + b.field("type", "long"); b.endObject(); - }), - new SyntheticSourceInvalidExample(err, b -> { - b.field("type", "text"); - b.startObject("fields"); - { - b.startObject("kwd"); - b.field("type", "keyword"); - b.field("normalizer", "lowercase"); - b.endObject(); - } + } + b.endObject(); + }), new SyntheticSourceInvalidExample(err, b -> { + b.field("type", "text"); + b.field("store", "false"); + b.startObject("fields"); + { + b.startObject("kwd"); + b.field("type", "keyword"); + b.field("normalizer", "lowercase"); b.endObject(); - }) - ); + } + b.endObject(); + }), new SyntheticSourceInvalidExample(err, b -> { + b.field("type", "text"); + b.field("store", "false"); + b.startObject("fields"); + { + b.startObject("kwd"); + b.field("type", "keyword"); + b.field("doc_values", "false"); + b.endObject(); + } + b.endObject(); + })); } }; }