Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/changelog/134582.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
pr: 134582
summary: Fixed match only text block loader not working when a keyword multi field
is present
area: Mapping
type: bug
issues: []
Original file line number Diff line number Diff line change
Expand Up @@ -133,27 +133,28 @@ protected Parameter<?>[] getParameters() {
return new Parameter<?>[] { meta };
}

private MatchOnlyTextFieldType buildFieldType(MapperBuilderContext context) {
private MatchOnlyTextFieldType buildFieldType(MapperBuilderContext context, MultiFields multiFields) {
NamedAnalyzer searchAnalyzer = analyzers.getSearchAnalyzer();
NamedAnalyzer searchQuoteAnalyzer = analyzers.getSearchQuoteAnalyzer();
NamedAnalyzer indexAnalyzer = analyzers.getIndexAnalyzer();
TextSearchInfo tsi = new TextSearchInfo(Defaults.FIELD_TYPE, null, searchAnalyzer, searchQuoteAnalyzer);
MatchOnlyTextFieldType ft = new MatchOnlyTextFieldType(
return new MatchOnlyTextFieldType(
context.buildFullName(leafName()),
tsi,
indexAnalyzer,
context.isSourceSynthetic(),
meta.getValue(),
withinMultiField,
multiFieldsBuilder.hasSyntheticSourceCompatibleKeywordField(),
storedFieldInBinaryFormat
storedFieldInBinaryFormat,
// match only text fields are not stored by definition
TextFieldMapper.SyntheticSourceHelper.syntheticSourceDelegate(false, multiFields)
);
return ft;
}

@Override
public MatchOnlyTextFieldMapper build(MapperBuilderContext context) {
MatchOnlyTextFieldType tft = buildFieldType(context);
BuilderParams builderParams = builderParams(this, context);
MatchOnlyTextFieldType tft = buildFieldType(context, builderParams.multiFields());
final boolean storeSource;
if (multiFieldsNotStoredByDefaultIndexVersionCheck(indexCreatedVersion)) {
storeSource = context.isSourceSynthetic()
Expand All @@ -164,6 +165,7 @@ public MatchOnlyTextFieldMapper build(MapperBuilderContext context) {
}
return new MatchOnlyTextFieldMapper(leafName(), Defaults.FIELD_TYPE, tft, builderParams(this, context), storeSource, this);
}

}

private static boolean isSyntheticSourceStoredFieldInBinaryFormat(IndexVersion indexCreatedVersion) {
Expand Down Expand Up @@ -191,7 +193,6 @@ public static class MatchOnlyTextFieldType extends StringFieldType {
private final String originalName;

private final boolean withinMultiField;
private final boolean hasCompatibleMultiFields;
private final boolean storedFieldInBinaryFormat;

public MatchOnlyTextFieldType(
Expand All @@ -201,15 +202,14 @@ public MatchOnlyTextFieldType(
boolean isSyntheticSource,
Map<String, String> meta,
boolean withinMultiField,
boolean hasCompatibleMultiFields,
boolean storedFieldInBinaryFormat
boolean storedFieldInBinaryFormat,
KeywordFieldMapper.KeywordFieldType syntheticSourceDelegate
) {
super(name, true, false, false, tsi, meta);
this.indexAnalyzer = Objects.requireNonNull(indexAnalyzer);
this.textFieldType = new TextFieldType(name, isSyntheticSource);
this.textFieldType = new TextFieldType(name, isSyntheticSource, syntheticSourceDelegate);
this.originalName = isSyntheticSource ? name + "._original" : null;
this.withinMultiField = withinMultiField;
this.hasCompatibleMultiFields = hasCompatibleMultiFields;
this.storedFieldInBinaryFormat = storedFieldInBinaryFormat;
}

Expand All @@ -222,7 +222,7 @@ public MatchOnlyTextFieldType(String name) {
Collections.emptyMap(),
false,
false,
false
null
);
}

Expand Down Expand Up @@ -270,26 +270,23 @@ private IOFunction<LeafReaderContext, CheckedIntFunction<List<Object>, IOExcepti
} else {
assert false : "parent field should either be stored or have doc values";
}
} else if (searchExecutionContext.isSourceSynthetic() && hasCompatibleMultiFields) {
var mapper = (MatchOnlyTextFieldMapper) searchExecutionContext.getMappingLookup().getMapper(name());
var kwd = TextFieldMapper.SyntheticSourceHelper.getKeywordFieldMapperForSyntheticSource(mapper);
} else if (searchExecutionContext.isSourceSynthetic() && textFieldType.syntheticSourceDelegate() != null) {
var kwd = textFieldType.syntheticSourceDelegate();

if (kwd != null) {
var fieldType = kwd.fieldType();

if (fieldType.ignoreAbove().isSet()) {
if (fieldType.isStored()) {
return storedFieldFetcher(fieldType.name(), fieldType.originalName());
} else if (fieldType.hasDocValues()) {
var ifd = searchExecutionContext.getForField(fieldType, MappedFieldType.FielddataOperation.SEARCH);
return combineFieldFetchers(docValuesFieldFetcher(ifd), storedFieldFetcher(fieldType.originalName()));
if (kwd.ignoreAbove().isSet()) {
if (kwd.isStored()) {
return storedFieldFetcher(kwd.name(), kwd.originalName());
} else if (kwd.hasDocValues()) {
var ifd = searchExecutionContext.getForField(kwd, MappedFieldType.FielddataOperation.SEARCH);
return combineFieldFetchers(docValuesFieldFetcher(ifd), storedFieldFetcher(kwd.originalName()));
}
}

if (fieldType.isStored()) {
return storedFieldFetcher(fieldType.name());
} else if (fieldType.hasDocValues()) {
var ifd = searchExecutionContext.getForField(fieldType, MappedFieldType.FielddataOperation.SEARCH);
if (kwd.isStored()) {
return storedFieldFetcher(kwd.name());
} else if (kwd.hasDocValues()) {
var ifd = searchExecutionContext.getForField(kwd, MappedFieldType.FielddataOperation.SEARCH);
return docValuesFieldFetcher(ifd);
} else {
assert false : "multi field should either be stored or have doc values";
Expand Down Expand Up @@ -512,7 +509,7 @@ public Query phrasePrefixQuery(TokenStream stream, int slop, int maxExpansions,
return toQuery(query, queryShardContext);
}

private static class BytesFromMixedStringsBytesRefBlockLoader extends BlockStoredFieldsReader.StoredFieldsBlockLoader {
static class BytesFromMixedStringsBytesRefBlockLoader extends BlockStoredFieldsReader.StoredFieldsBlockLoader {
BytesFromMixedStringsBytesRefBlockLoader(String field) {
super(field);
}
Expand Down Expand Up @@ -543,12 +540,27 @@ protected BytesRef toBytesRef(Object v) {
@Override
public BlockLoader blockLoader(BlockLoaderContext blContext) {
if (textFieldType.isSyntheticSource()) {
if (storedFieldInBinaryFormat) {
return new BlockStoredFieldsReader.BytesFromBytesRefsBlockLoader(storedFieldNameForSyntheticSource());
} else {
return new BytesFromMixedStringsBytesRefBlockLoader(storedFieldNameForSyntheticSource());
// if there is no synthetic source delegate, then this match only text field would've created StoredFields for us to use
if (textFieldType.syntheticSourceDelegate() == null) {
if (storedFieldInBinaryFormat) {
return new BlockStoredFieldsReader.BytesFromBytesRefsBlockLoader(storedFieldNameForSyntheticSource());
} else {
return new BytesFromMixedStringsBytesRefBlockLoader(storedFieldNameForSyntheticSource());
}
}

// otherwise, delegate block loading to the synthetic source delegate if possible
if (textFieldType.canUseSyntheticSourceDelegateForLoading()) {
return new BlockLoader.Delegating(textFieldType.syntheticSourceDelegate().blockLoader(blContext)) {
@Override
protected String delegatingTo() {
return textFieldType.syntheticSourceDelegate().name();
}
};
}
}

// fallback to _source (synthetic or not)
SourceValueFetcher fetcher = SourceValueFetcher.toString(blContext.sourcePaths(name()));
// MatchOnlyText never has norms, so we have to use the field names field
BlockSourceReader.LeafIteratorLookup lookup = BlockSourceReader.lookupFromFieldNames(blContext.fieldNames(), name());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
package org.elasticsearch.index.mapper.extras;

import org.apache.lucene.analysis.TokenStream;
import org.apache.lucene.document.FieldType;
import org.apache.lucene.index.Term;
import org.apache.lucene.queries.intervals.Intervals;
import org.apache.lucene.queries.intervals.IntervalsSource;
Expand All @@ -27,20 +28,38 @@
import org.apache.lucene.tests.analysis.Token;
import org.apache.lucene.util.BytesRef;
import org.elasticsearch.ElasticsearchException;
import org.elasticsearch.cluster.metadata.IndexMetadata;
import org.elasticsearch.common.lucene.BytesRefs;
import org.elasticsearch.common.lucene.Lucene;
import org.elasticsearch.common.lucene.search.AutomatonQueries;
import org.elasticsearch.common.lucene.search.MultiPhrasePrefixQuery;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.unit.Fuzziness;
import org.elasticsearch.index.IndexMode;
import org.elasticsearch.index.IndexSettings;
import org.elasticsearch.index.IndexVersion;
import org.elasticsearch.index.analysis.NamedAnalyzer;
import org.elasticsearch.index.mapper.BlockLoader;
import org.elasticsearch.index.mapper.FieldNamesFieldMapper;
import org.elasticsearch.index.mapper.FieldTypeTestCase;
import org.elasticsearch.index.mapper.KeywordFieldMapper;
import org.elasticsearch.index.mapper.MappedFieldType;
import org.elasticsearch.index.mapper.MappingParserContext;
import org.elasticsearch.index.mapper.TextFieldMapper;
import org.elasticsearch.index.mapper.TextSearchInfo;
import org.elasticsearch.index.mapper.extras.MatchOnlyTextFieldMapper.MatchOnlyTextFieldType;
import org.elasticsearch.script.ScriptCompiler;
import org.hamcrest.Matchers;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;

public class MatchOnlyTextFieldTypeTests extends FieldTypeTestCase {

public void testTermQuery() {
Expand Down Expand Up @@ -205,4 +224,149 @@ public void testRangeIntervals() {
((SourceIntervalsSource) rangeIntervals).getIntervalsSource()
);
}

public void test_block_loader_uses_stored_fields_for_loading_when_synthetic_source_delegate_is_absent() {
// given
MatchOnlyTextFieldMapper.MatchOnlyTextFieldType ft = new MatchOnlyTextFieldMapper.MatchOnlyTextFieldType(
"parent",
new TextSearchInfo(TextFieldMapper.Defaults.FIELD_TYPE, null, Lucene.STANDARD_ANALYZER, Lucene.STANDARD_ANALYZER),
mock(NamedAnalyzer.class),
true,
Collections.emptyMap(),
false,
false,
null
);

// when
BlockLoader blockLoader = ft.blockLoader(mock(MappedFieldType.BlockLoaderContext.class));

// then
// verify that we delegate block loading to the synthetic source delegate
assertThat(blockLoader, Matchers.instanceOf(MatchOnlyTextFieldType.BytesFromMixedStringsBytesRefBlockLoader.class));
}

public void test_block_loader_uses_synthetic_source_delegate_when_ignore_above_is_not_set() {
// given
KeywordFieldMapper.KeywordFieldType syntheticSourceDelegate = new KeywordFieldMapper.KeywordFieldType(
"child",
true,
true,
Collections.emptyMap()
);

MatchOnlyTextFieldMapper.MatchOnlyTextFieldType ft = new MatchOnlyTextFieldMapper.MatchOnlyTextFieldType(
"parent",
new TextSearchInfo(TextFieldMapper.Defaults.FIELD_TYPE, null, Lucene.STANDARD_ANALYZER, Lucene.STANDARD_ANALYZER),
mock(NamedAnalyzer.class),
true,
Collections.emptyMap(),
false,
false,
syntheticSourceDelegate
);

// when
BlockLoader blockLoader = ft.blockLoader(mock(MappedFieldType.BlockLoaderContext.class));

// then
// verify that we delegate block loading to the synthetic source delegate
assertThat(blockLoader, Matchers.instanceOf(BlockLoader.Delegating.class));
}

public void test_block_loader_does_not_use_synthetic_source_delegate_when_ignore_above_is_set() {
// given
Settings settings = Settings.builder()
.put(IndexMetadata.SETTING_VERSION_CREATED, IndexVersion.current())
.put(IndexSettings.MODE.getKey(), IndexMode.STANDARD)
.put(IndexMetadata.SETTING_NUMBER_OF_SHARDS, 1)
.put(IndexMetadata.SETTING_NUMBER_OF_REPLICAS, 1)
.build();
IndexSettings indexSettings = new IndexSettings(IndexMetadata.builder("index").settings(settings).build(), settings);
MappingParserContext mappingParserContext = mock(MappingParserContext.class);
doReturn(settings).when(mappingParserContext).getSettings();
doReturn(indexSettings).when(mappingParserContext).getIndexSettings();
doReturn(mock(ScriptCompiler.class)).when(mappingParserContext).scriptCompiler();

KeywordFieldMapper.Builder builder = new KeywordFieldMapper.Builder("child", mappingParserContext);
builder.ignoreAbove(123);

KeywordFieldMapper.KeywordFieldType syntheticSourceDelegate = new KeywordFieldMapper.KeywordFieldType(
"child",
mock(FieldType.class),
mock(NamedAnalyzer.class),
mock(NamedAnalyzer.class),
mock(NamedAnalyzer.class),
builder,
true
);

MatchOnlyTextFieldMapper.MatchOnlyTextFieldType ft = new MatchOnlyTextFieldMapper.MatchOnlyTextFieldType(
"parent",
new TextSearchInfo(TextFieldMapper.Defaults.FIELD_TYPE, null, Lucene.STANDARD_ANALYZER, Lucene.STANDARD_ANALYZER),
mock(NamedAnalyzer.class),
true,
Collections.emptyMap(),
false,
false,
syntheticSourceDelegate
);

// when
MappedFieldType.BlockLoaderContext blContext = mock(MappedFieldType.BlockLoaderContext.class);
doReturn(FieldNamesFieldMapper.FieldNamesFieldType.get(false)).when(blContext).fieldNames();
BlockLoader blockLoader = ft.blockLoader(blContext);

// then
// verify that we don't delegate anything
assertThat(blockLoader, Matchers.not(Matchers.instanceOf(BlockLoader.Delegating.class)));
}

public void test_block_loader_does_not_use_synthetic_source_delegate_when_ignore_above_is_set_at_index_level() {
// given
Settings settings = Settings.builder()
.put(IndexMetadata.SETTING_VERSION_CREATED, IndexVersion.current())
.put(IndexSettings.MODE.getKey(), IndexMode.STANDARD)
.put(IndexMetadata.SETTING_NUMBER_OF_SHARDS, 1)
.put(IndexMetadata.SETTING_NUMBER_OF_REPLICAS, 1)
.put(IndexSettings.IGNORE_ABOVE_SETTING.getKey(), 123)
.build();
IndexSettings indexSettings = new IndexSettings(IndexMetadata.builder("index").settings(settings).build(), settings);
MappingParserContext mappingParserContext = mock(MappingParserContext.class);
doReturn(settings).when(mappingParserContext).getSettings();
doReturn(indexSettings).when(mappingParserContext).getIndexSettings();
doReturn(mock(ScriptCompiler.class)).when(mappingParserContext).scriptCompiler();

KeywordFieldMapper.Builder builder = new KeywordFieldMapper.Builder("child", mappingParserContext);

KeywordFieldMapper.KeywordFieldType syntheticSourceDelegate = new KeywordFieldMapper.KeywordFieldType(
"child",
mock(FieldType.class),
mock(NamedAnalyzer.class),
mock(NamedAnalyzer.class),
mock(NamedAnalyzer.class),
builder,
true
);

MatchOnlyTextFieldMapper.MatchOnlyTextFieldType ft = new MatchOnlyTextFieldMapper.MatchOnlyTextFieldType(
"parent",
new TextSearchInfo(TextFieldMapper.Defaults.FIELD_TYPE, null, Lucene.STANDARD_ANALYZER, Lucene.STANDARD_ANALYZER),
mock(NamedAnalyzer.class),
true,
Collections.emptyMap(),
false,
false,
syntheticSourceDelegate
);

// when
MappedFieldType.BlockLoaderContext blContext = mock(MappedFieldType.BlockLoaderContext.class);
doReturn(FieldNamesFieldMapper.FieldNamesFieldType.get(false)).when(blContext).fieldNames();
BlockLoader blockLoader = ft.blockLoader(blContext);

// then
// verify that we don't delegate anything
assertThat(blockLoader, Matchers.not(Matchers.instanceOf(BlockLoader.Delegating.class)));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ private AnnotatedTextFieldType buildFieldType(FieldType fieldType, MapperBuilder
store.getValue(),
tsi,
context.isSourceSynthetic(),
TextFieldMapper.SyntheticSourceHelper.syntheticSourceDelegate(fieldType, multiFields),
TextFieldMapper.SyntheticSourceHelper.syntheticSourceDelegate(fieldType.stored(), multiFields),
meta.getValue()
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ public class MapperFeatures implements FeatureSpecification {
public static final NodeFeature SORT_FIELDS_CHECK_FOR_NESTED_OBJECT_FIX = new NodeFeature("mapper.nested.sorting_fields_check_fix");
public static final NodeFeature DYNAMIC_HANDLING_IN_COPY_TO = new NodeFeature("mapper.copy_to.dynamic_handling");
public static final NodeFeature DOC_VALUES_SKIPPER = new NodeFeature("mapper.doc_values_skipper");
public static final NodeFeature MATCH_ONLY_TEXT_BLOCK_LOADER_FIX = new NodeFeature("mapper.match_only_text_block_loader_fix");

static final NodeFeature UKNOWN_FIELD_MAPPING_UPDATE_ERROR_MESSAGE = new NodeFeature(
"mapper.unknown_field_mapping_update_error_message"
);
Expand Down Expand Up @@ -80,7 +82,8 @@ public Set<NodeFeature> getTestFeatures() {
SEARCH_LOAD_PER_SHARD,
SPARSE_VECTOR_INDEX_OPTIONS_FEATURE,
PATTERNED_TEXT,
MULTI_FIELD_UNICODE_OPTIMISATION_FIX
MULTI_FIELD_UNICODE_OPTIMISATION_FIX,
MATCH_ONLY_TEXT_BLOCK_LOADER_FIX
);
}
}
Loading
Loading