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 04ace5ccc4157..050574272b8c8 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; @@ -252,6 +254,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); @@ -265,7 +271,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); @@ -312,13 +322,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 cf731cc5cbc65..594b27f029901 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/KeywordFieldMapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/KeywordFieldMapper.java @@ -513,6 +513,7 @@ public static final class KeywordFieldType extends StringFieldType { private final IndexMode indexMode; private final IndexSortConfig indexSortConfig; private final boolean hasDocValuesSkipper; + private final String originalName; public KeywordFieldType( String name, @@ -541,6 +542,7 @@ public KeywordFieldType( this.indexMode = builder.indexMode; this.indexSortConfig = builder.indexSortConfig; this.hasDocValuesSkipper = DocValuesSkipIndexType.NONE.equals(fieldType.docValuesSkipIndexType()) == false; + this.originalName = isSyntheticSource ? name + "._original" : null; } public KeywordFieldType(String name, boolean isIndexed, boolean hasDocValues, Map meta) { @@ -555,6 +557,7 @@ public KeywordFieldType(String name, boolean isIndexed, boolean hasDocValues, Ma this.indexMode = IndexMode.STANDARD; this.indexSortConfig = null; this.hasDocValuesSkipper = false; + this.originalName = null; } public KeywordFieldType(String name) { @@ -580,6 +583,7 @@ public KeywordFieldType(String name, FieldType fieldType) { this.indexMode = IndexMode.STANDARD; this.indexSortConfig = null; this.hasDocValuesSkipper = DocValuesSkipIndexType.NONE.equals(fieldType.docValuesSkipIndexType()) == false; + this.originalName = null; } public KeywordFieldType(String name, NamedAnalyzer analyzer) { @@ -594,6 +598,7 @@ public KeywordFieldType(String name, NamedAnalyzer analyzer) { this.indexMode = IndexMode.STANDARD; this.indexSortConfig = null; this.hasDocValuesSkipper = false; + this.originalName = null; } @Override @@ -1057,6 +1062,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; @@ -1109,7 +1123,7 @@ private KeywordFieldMapper( this.useDocValuesSkipper = useDocValuesSkipper; this.offsetsFieldName = offsetsFieldName; this.indexSourceKeepMode = indexSourceKeepMode; - this.originalName = isSyntheticSource ? fullPath() + "._original" : null; + this.originalName = mappedFieldType.originalName(); } @Override @@ -1169,7 +1183,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; } @@ -1280,15 +1294,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()) { @@ -1337,7 +1342,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;