diff --git a/modules/mapper-extras/src/main/java/org/elasticsearch/index/mapper/extras/MatchOnlyTextFieldMapper.java b/modules/mapper-extras/src/main/java/org/elasticsearch/index/mapper/extras/MatchOnlyTextFieldMapper.java index 26187d0e8a4c8..d6922792d4cb6 100644 --- a/modules/mapper-extras/src/main/java/org/elasticsearch/index/mapper/extras/MatchOnlyTextFieldMapper.java +++ b/modules/mapper-extras/src/main/java/org/elasticsearch/index/mapper/extras/MatchOnlyTextFieldMapper.java @@ -47,6 +47,7 @@ import org.elasticsearch.index.mapper.BlockStoredFieldsReader; import org.elasticsearch.index.mapper.DocumentParserContext; import org.elasticsearch.index.mapper.FieldMapper; +import org.elasticsearch.index.mapper.KeywordFieldMapper; import org.elasticsearch.index.mapper.MappedFieldType; import org.elasticsearch.index.mapper.MapperBuilderContext; import org.elasticsearch.index.mapper.SourceValueFetcher; @@ -66,6 +67,7 @@ import java.io.IOException; import java.io.UncheckedIOException; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; @@ -246,6 +248,10 @@ private IOFunction, IOExcepti String parentField = searchExecutionContext.parentPath(name()); var parent = searchExecutionContext.lookup().fieldType(parentField); if (parent.isStored()) { + if (parent instanceof KeywordFieldMapper.KeywordFieldType keywordParent + && keywordParent.ignoreAbove() != Integer.MAX_VALUE) { + return storedFieldFetcher(parentField, keywordParent.originalName()); + } return storedFieldFetcher(parentField); } else if (parent.hasDocValues()) { var ifd = searchExecutionContext.getForField(parent, MappedFieldType.FielddataOperation.SEARCH); @@ -259,7 +265,11 @@ private IOFunction, IOExcepti if (kwd != null) { var fieldType = kwd.fieldType(); if (fieldType.isStored()) { - return storedFieldFetcher(fieldType.name()); + if (fieldType.ignoreAbove() != Integer.MAX_VALUE) { + return storedFieldFetcher(fieldType.name(), fieldType.originalName()); + } else { + return storedFieldFetcher(fieldType.name()); + } } else if (fieldType.hasDocValues()) { var ifd = searchExecutionContext.getForField(fieldType, MappedFieldType.FielddataOperation.SEARCH); return docValuesFieldFetcher(ifd); @@ -306,13 +316,17 @@ private static IOFunction, IO }; } - private static IOFunction, IOException>> storedFieldFetcher(String name) { - var loader = StoredFieldLoader.create(false, Set.of(name)); + private static IOFunction, IOException>> storedFieldFetcher(String... names) { + var loader = StoredFieldLoader.create(false, Set.of(names)); return context -> { var leafLoader = loader.getLoader(context, null); return docId -> { leafLoader.advanceTo(docId); - return leafLoader.storedFields().get(name); + var storedFields = leafLoader.storedFields(); + if (names.length == 1) { + return storedFields.get(names[0]); + } + return Arrays.stream(names).map(storedFields::get).filter(Objects::nonNull).flatMap(List::stream).toList(); }; }; } diff --git a/modules/mapper-extras/src/yamlRestTest/resources/rest-api-spec/test/match_only_text/10_basic.yml b/modules/mapper-extras/src/yamlRestTest/resources/rest-api-spec/test/match_only_text/10_basic.yml index 1d52038e29d45..deb0f1a496814 100644 --- a/modules/mapper-extras/src/yamlRestTest/resources/rest-api-spec/test/match_only_text/10_basic.yml +++ b/modules/mapper-extras/src/yamlRestTest/resources/rest-api-spec/test/match_only_text/10_basic.yml @@ -479,6 +479,49 @@ synthetic_source match_only_text as multi-field with stored keyword as parent: hits.hits.0._source.foo: "Apache Lucene powers Elasticsearch" --- +synthetic_source match_only_text as multi-field with ignored stored keyword as parent: + - requires: + cluster_features: [ "mapper.source.mode_from_index_setting" ] + reason: "Source mode configured through index setting" + + - do: + indices.create: + index: synthetic_source_test + body: + settings: + index: + mapping.source.mode: synthetic + mappings: + properties: + foo: + type: keyword + store: true + doc_values: false + ignore_above: 10 + fields: + text: + type: match_only_text + + - do: + index: + index: synthetic_source_test + id: "1" + refresh: true + body: + foo: "Apache Lucene powers Elasticsearch" + + - do: + search: + index: synthetic_source_test + body: + query: + match_phrase: + foo.text: apache lucene + + - match: { "hits.total.value": 1 } + - match: + hits.hits.0._source.foo: "Apache Lucene powers Elasticsearch" +--- synthetic_source match_only_text with multi-field: - requires: cluster_features: [ "mapper.source.mode_from_index_setting" ] @@ -561,3 +604,47 @@ synthetic_source match_only_text with stored multi-field: - match: { "hits.total.value": 1 } - match: hits.hits.0._source.foo: "Apache Lucene powers Elasticsearch" + +--- +synthetic_source match_only_text with ignored stored multi-field: + - requires: + cluster_features: [ "mapper.source.mode_from_index_setting" ] + reason: "Source mode configured through index setting" + + - do: + indices.create: + index: synthetic_source_test + body: + settings: + index: + mapping.source.mode: synthetic + mappings: + properties: + foo: + type: match_only_text + fields: + raw: + type: keyword + store: true + doc_values: false + ignore_above: 10 + + - do: + index: + index: synthetic_source_test + id: "1" + refresh: true + body: + foo: "Apache Lucene powers Elasticsearch" + + - do: + search: + index: synthetic_source_test + body: + query: + match_phrase: + foo: apache lucene + + - match: { "hits.total.value": 1 } + - match: + hits.hits.0._source.foo: "Apache Lucene powers Elasticsearch" diff --git a/server/src/main/java/org/elasticsearch/index/mapper/KeywordFieldMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/KeywordFieldMapper.java index 4d0549dd231d6..73be966b21626 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/KeywordFieldMapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/KeywordFieldMapper.java @@ -425,6 +425,7 @@ public static final class KeywordFieldType extends StringFieldType { private final FieldValues scriptValues; private final boolean isDimension; private final boolean isSyntheticSource; + private final String originalName; public KeywordFieldType( String name, @@ -450,6 +451,7 @@ public KeywordFieldType( this.scriptValues = builder.scriptValues(); this.isDimension = builder.dimension.getValue(); this.isSyntheticSource = isSyntheticSource; + this.originalName = isSyntheticSource ? name + "._original" : null; } public KeywordFieldType(String name, boolean isIndexed, boolean hasDocValues, Map meta) { @@ -461,6 +463,7 @@ public KeywordFieldType(String name, boolean isIndexed, boolean hasDocValues, Ma this.scriptValues = null; this.isDimension = false; this.isSyntheticSource = false; + this.originalName = null; } public KeywordFieldType(String name) { @@ -483,6 +486,7 @@ public KeywordFieldType(String name, FieldType fieldType) { this.scriptValues = null; this.isDimension = false; this.isSyntheticSource = false; + this.originalName = null; } public KeywordFieldType(String name, NamedAnalyzer analyzer) { @@ -494,6 +498,7 @@ public KeywordFieldType(String name, NamedAnalyzer analyzer) { this.scriptValues = null; this.isDimension = false; this.isSyntheticSource = false; + this.originalName = null; } @Override @@ -945,6 +950,15 @@ public Query automatonQuery( ) { return new AutomatonQueryWithDescription(new Term(name()), automatonSupplier.get(), description); } + + /** + * The name used to store "original" that have been ignored + * by {@link KeywordFieldType#ignoreAbove()} so that they can be rebuilt + * for synthetic source. + */ + public String originalName() { + return originalName; + } } private final boolean indexed; @@ -992,7 +1006,7 @@ private KeywordFieldMapper( this.ignoreAbove = builder.ignoreAbove.getValue(); this.offsetsFieldName = offsetsFieldName; this.indexSourceKeepMode = indexSourceKeepMode; - this.originalName = isSyntheticSource ? fullPath() + "._original" : null; + this.originalName = mappedFieldType.originalName(); } @Override @@ -1052,7 +1066,7 @@ private boolean indexValue(DocumentParserContext context, XContentString value) // Save a copy of the field so synthetic source can load it var utfBytes = value.bytes(); var bytesRef = new BytesRef(utfBytes.bytes(), utfBytes.offset(), utfBytes.length()); - context.doc().add(new StoredField(originalName(), bytesRef)); + context.doc().add(new StoredField(originalName, bytesRef)); } return false; } @@ -1155,15 +1169,6 @@ boolean hasNormalizer() { return normalizerName != null; } - /** - * The name used to store "original" that have been ignored - * by {@link KeywordFieldType#ignoreAbove()} so that they can be rebuilt - * for synthetic source. - */ - private String originalName() { - return originalName; - } - @Override protected SyntheticSourceSupport syntheticSourceSupport() { if (hasNormalizer()) { @@ -1212,7 +1217,7 @@ protected BytesRef preserve(BytesRef value) { } if (fieldType().ignoreAbove != Integer.MAX_VALUE) { - layers.add(new CompositeSyntheticFieldLoader.StoredFieldLayer(originalName()) { + layers.add(new CompositeSyntheticFieldLoader.StoredFieldLayer(originalName) { @Override protected void writeValue(Object value, XContentBuilder b) throws IOException { BytesRef ref = (BytesRef) value;