diff --git a/server/src/main/java/org/elasticsearch/index/analysis/AnalysisMode.java b/server/src/main/java/org/elasticsearch/index/analysis/AnalysisMode.java new file mode 100644 index 0000000000000..ea9e1e0c6aa7f --- /dev/null +++ b/server/src/main/java/org/elasticsearch/index/analysis/AnalysisMode.java @@ -0,0 +1,82 @@ +/* + * 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.analysis; + +/** + * Enum representing the mode in which token filters and analyzers are allowed to operate. + * While most token filters are allowed both in index and search time analyzers, some are + * restricted to be used only at index time, others at search time. + */ +public enum AnalysisMode { + + /** + * AnalysisMode representing analysis components that can be used only at index time + */ + INDEX_TIME("index time") { + @Override + public AnalysisMode merge(AnalysisMode other) { + if (other == AnalysisMode.SEARCH_TIME) { + throw new IllegalStateException("Cannot merge SEARCH_TIME and INDEX_TIME analysis mode."); + } + return AnalysisMode.INDEX_TIME; + } + }, + /** + * AnalysisMode representing analysis components that can be used only at search time + */ + SEARCH_TIME("search time") { + @Override + public AnalysisMode merge(AnalysisMode other) { + if (other == AnalysisMode.INDEX_TIME) { + throw new IllegalStateException("Cannot merge SEARCH_TIME and INDEX_TIME analysis mode."); + } + return AnalysisMode.SEARCH_TIME; + } + }, + /** + * AnalysisMode representing analysis components that can be used both at index and search time + */ + ALL("all") { + @Override + public AnalysisMode merge(AnalysisMode other) { + return other; + } + }; + + private String readableName; + + AnalysisMode(String name) { + this.readableName = name; + } + + public String getReadableName() { + return this.readableName; + } + + /** + * Returns a mode that is compatible with both this mode and the other mode, that is: + * + */ + abstract AnalysisMode merge(AnalysisMode other); +} diff --git a/server/src/main/java/org/elasticsearch/index/analysis/AnalysisRegistry.java b/server/src/main/java/org/elasticsearch/index/analysis/AnalysisRegistry.java index 165256940bb81..eff594d1d1152 100644 --- a/server/src/main/java/org/elasticsearch/index/analysis/AnalysisRegistry.java +++ b/server/src/main/java/org/elasticsearch/index/analysis/AnalysisRegistry.java @@ -20,11 +20,11 @@ import org.apache.lucene.analysis.Analyzer; import org.apache.lucene.analysis.core.WhitespaceTokenizer; -import org.elasticsearch.Version; -import org.elasticsearch.core.internal.io.IOUtils; import org.elasticsearch.ElasticsearchException; +import org.elasticsearch.Version; import org.elasticsearch.cluster.metadata.IndexMetaData; import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.core.internal.io.IOUtils; import org.elasticsearch.env.Environment; import org.elasticsearch.index.Index; import org.elasticsearch.index.IndexSettings; @@ -456,6 +456,7 @@ public IndexAnalyzers build(IndexSettings indexSettings, if (defaultAnalyzer == null) { throw new IllegalArgumentException("no default analyzer configured"); } + defaultAnalyzer.checkAllowedInMode(AnalysisMode.ALL); if (analyzers.containsKey("default_index")) { throw new IllegalArgumentException("setting [index.analysis.analyzer.default_index] is not supported anymore, use " + "[index.analysis.analyzer.default] instead for index [" + index.getName() + "]"); diff --git a/server/src/main/java/org/elasticsearch/index/analysis/CustomAnalyzer.java b/server/src/main/java/org/elasticsearch/index/analysis/CustomAnalyzer.java index 87a540312b411..a41ee33564400 100644 --- a/server/src/main/java/org/elasticsearch/index/analysis/CustomAnalyzer.java +++ b/server/src/main/java/org/elasticsearch/index/analysis/CustomAnalyzer.java @@ -36,6 +36,7 @@ public final class CustomAnalyzer extends Analyzer { private final int positionIncrementGap; private final int offsetGap; + private final AnalysisMode analysisMode; public CustomAnalyzer(String tokenizerName, TokenizerFactory tokenizerFactory, CharFilterFactory[] charFilters, TokenFilterFactory[] tokenFilters) { @@ -50,6 +51,12 @@ public CustomAnalyzer(String tokenizerName, TokenizerFactory tokenizerFactory, C this.tokenFilters = tokenFilters; this.positionIncrementGap = positionIncrementGap; this.offsetGap = offsetGap; + // merge and transfer token filter analysis modes with analyzer + AnalysisMode mode = AnalysisMode.ALL; + for (TokenFilterFactory f : tokenFilters) { + mode = mode.merge(f.getAnalysisMode()); + } + this.analysisMode = mode; } /** @@ -84,6 +91,10 @@ public int getOffsetGap(String field) { return this.offsetGap; } + public AnalysisMode getAnalysisMode() { + return this.analysisMode; + } + @Override protected TokenStreamComponents createComponents(String fieldName) { Tokenizer tokenizer = tokenizerFactory.create(); diff --git a/server/src/main/java/org/elasticsearch/index/analysis/NamedAnalyzer.java b/server/src/main/java/org/elasticsearch/index/analysis/NamedAnalyzer.java index 416967e94f5f0..4831d88f3aa1f 100644 --- a/server/src/main/java/org/elasticsearch/index/analysis/NamedAnalyzer.java +++ b/server/src/main/java/org/elasticsearch/index/analysis/NamedAnalyzer.java @@ -21,7 +21,10 @@ import org.apache.lucene.analysis.Analyzer; import org.apache.lucene.analysis.DelegatingAnalyzerWrapper; +import org.elasticsearch.index.mapper.MapperException; +import java.util.ArrayList; +import java.util.List; import java.util.Objects; /** @@ -34,6 +37,7 @@ public class NamedAnalyzer extends DelegatingAnalyzerWrapper { private final AnalyzerScope scope; private final Analyzer analyzer; private final int positionIncrementGap; + private final AnalysisMode analysisMode; public NamedAnalyzer(NamedAnalyzer analyzer, int positionIncrementGap) { this(analyzer.name(), analyzer.scope(), analyzer.analyzer(), positionIncrementGap); @@ -43,12 +47,17 @@ public NamedAnalyzer(String name, AnalyzerScope scope, Analyzer analyzer) { this(name, scope, analyzer, Integer.MIN_VALUE); } - public NamedAnalyzer(String name, AnalyzerScope scope, Analyzer analyzer, int positionIncrementGap) { + NamedAnalyzer(String name, AnalyzerScope scope, Analyzer analyzer, int positionIncrementGap) { super(ERROR_STRATEGY); this.name = name; this.scope = scope; this.analyzer = analyzer; this.positionIncrementGap = positionIncrementGap; + if (analyzer instanceof org.elasticsearch.index.analysis.CustomAnalyzer) { + this.analysisMode = ((org.elasticsearch.index.analysis.CustomAnalyzer) analyzer).getAnalysisMode(); + } else { + this.analysisMode = AnalysisMode.ALL; + } } /** @@ -65,6 +74,13 @@ public AnalyzerScope scope() { return this.scope; } + /** + * Returns whether this analyzer can be updated + */ + public AnalysisMode getAnalysisMode() { + return this.analysisMode; + } + /** * The actual analyzer. */ @@ -85,9 +101,37 @@ public int getPositionIncrementGap(String fieldName) { return super.getPositionIncrementGap(fieldName); } + /** + * Checks the wrapped analyzer for the provided restricted {@link AnalysisMode} and throws + * an error if the analyzer is not allowed to run in that mode. The error contains more detailed information about + * the offending filters that caused the analyzer to not be allowed in this mode. + */ + public void checkAllowedInMode(AnalysisMode mode) { + Objects.requireNonNull(mode); + if (this.getAnalysisMode() == AnalysisMode.ALL) { + return; // everything allowed if this analyzer is in ALL mode + } + if (this.getAnalysisMode() != mode) { + if (analyzer instanceof CustomAnalyzer) { + TokenFilterFactory[] tokenFilters = ((CustomAnalyzer) analyzer).tokenFilters(); + List offendingFilters = new ArrayList<>(); + for (TokenFilterFactory tokenFilter : tokenFilters) { + if (tokenFilter.getAnalysisMode() != mode) { + offendingFilters.add(tokenFilter.name()); + } + } + throw new MapperException("analyzer [" + name + "] contains filters " + offendingFilters + + " that are not allowed to run in " + mode.getReadableName() + " mode."); + } else { + throw new MapperException( + "analyzer [" + name + "] contains components that are not allowed to run in " + mode.getReadableName() + " mode."); + } + } + } + @Override public String toString() { - return "analyzer name[" + name + "], analyzer [" + analyzer + "]"; + return "analyzer name[" + name + "], analyzer [" + analyzer + "], analysisMode [" + analysisMode + "]"; } /** It is an error if this is ever used, it means we screwed up! */ diff --git a/server/src/main/java/org/elasticsearch/index/analysis/TokenFilterFactory.java b/server/src/main/java/org/elasticsearch/index/analysis/TokenFilterFactory.java index b7ed6fd9e9e24..3dbd571288575 100644 --- a/server/src/main/java/org/elasticsearch/index/analysis/TokenFilterFactory.java +++ b/server/src/main/java/org/elasticsearch/index/analysis/TokenFilterFactory.java @@ -74,6 +74,15 @@ default TokenFilterFactory getSynonymFilter() { return this; } + /** + * Get the {@link AnalysisMode} this filter is allowed to be used in. The default is + * {@link AnalysisMode#ALL}. Instances need to override this method to define their + * own restrictions. + */ + default AnalysisMode getAnalysisMode() { + return AnalysisMode.ALL; + } + /** * A TokenFilterFactory that does no filtering to its TokenStream */ diff --git a/server/src/main/java/org/elasticsearch/index/mapper/TypeParsers.java b/server/src/main/java/org/elasticsearch/index/mapper/TypeParsers.java index 8cf66009ea140..77d7be62fc1b9 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/TypeParsers.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/TypeParsers.java @@ -23,6 +23,7 @@ import org.elasticsearch.ElasticsearchParseException; import org.elasticsearch.common.time.DateFormatter; import org.elasticsearch.common.xcontent.support.XContentMapValues; +import org.elasticsearch.index.analysis.AnalysisMode; import org.elasticsearch.index.analysis.NamedAnalyzer; import org.elasticsearch.index.similarity.SimilarityProvider; @@ -80,6 +81,7 @@ private static void parseAnalyzersAndTermVectors(FieldMapper.Builder builder, St if (analyzer == null) { throw new MapperParsingException("analyzer [" + propNode.toString() + "] not found for field [" + name + "]"); } + analyzer.checkAllowedInMode(AnalysisMode.SEARCH_TIME); searchAnalyzer = analyzer; iterator.remove(); } else if (propName.equals("search_quote_analyzer")) { @@ -87,11 +89,29 @@ private static void parseAnalyzersAndTermVectors(FieldMapper.Builder builder, St if (analyzer == null) { throw new MapperParsingException("analyzer [" + propNode.toString() + "] not found for field [" + name + "]"); } + analyzer.checkAllowedInMode(AnalysisMode.SEARCH_TIME); searchQuoteAnalyzer = analyzer; iterator.remove(); } } + // check analyzers are allowed to work in the respective AnalysisMode + { + if (indexAnalyzer != null) { + if (searchAnalyzer == null) { + indexAnalyzer.checkAllowedInMode(AnalysisMode.ALL); + } else { + indexAnalyzer.checkAllowedInMode(AnalysisMode.INDEX_TIME); + } + } + if (searchAnalyzer != null) { + searchAnalyzer.checkAllowedInMode(AnalysisMode.SEARCH_TIME); + } + if (searchQuoteAnalyzer != null) { + searchQuoteAnalyzer.checkAllowedInMode(AnalysisMode.SEARCH_TIME); + } + } + if (indexAnalyzer == null && searchAnalyzer != null) { throw new MapperParsingException("analyzer on field [" + name + "] must be set when search_analyzer is set"); } diff --git a/server/src/test/java/org/elasticsearch/index/analysis/AnalysisRegistryTests.java b/server/src/test/java/org/elasticsearch/index/analysis/AnalysisRegistryTests.java index 04dc98deb7bf5..b836a5d0372b8 100644 --- a/server/src/test/java/org/elasticsearch/index/analysis/AnalysisRegistryTests.java +++ b/server/src/test/java/org/elasticsearch/index/analysis/AnalysisRegistryTests.java @@ -20,6 +20,8 @@ package org.elasticsearch.index.analysis; import com.carrotsearch.randomizedtesting.generators.RandomPicks; + +import org.apache.lucene.analysis.Analyzer; import org.apache.lucene.analysis.MockTokenFilter; import org.apache.lucene.analysis.TokenStream; import org.apache.lucene.analysis.en.EnglishAnalyzer; @@ -31,6 +33,7 @@ import org.elasticsearch.env.Environment; import org.elasticsearch.env.TestEnvironment; import org.elasticsearch.index.IndexSettings; +import org.elasticsearch.index.mapper.MapperException; import org.elasticsearch.indices.analysis.AnalysisModule; import org.elasticsearch.indices.analysis.AnalysisModule.AnalysisProvider; import org.elasticsearch.indices.analysis.PreBuiltAnalyzers; @@ -102,6 +105,29 @@ public void testOverrideDefaultAnalyzer() throws IOException { assertThat(indexAnalyzers.getDefaultSearchQuoteAnalyzer().analyzer(), instanceOf(EnglishAnalyzer.class)); } + public void testOverrideDefaultAnalyzerWithoutAnalysisModeAll() throws IOException { + Version version = VersionUtils.randomVersion(random()); + Settings settings = Settings.builder().put(IndexMetaData.SETTING_VERSION_CREATED, version).build(); + TokenFilterFactory tokenFilter = new AbstractTokenFilterFactory(IndexSettingsModule.newIndexSettings("index", settings), + "my_filter", Settings.EMPTY) { + @Override + public AnalysisMode getAnalysisMode() { + return randomFrom(AnalysisMode.SEARCH_TIME, AnalysisMode.INDEX_TIME); + } + + @Override + public TokenStream create(TokenStream tokenStream) { + return null; + } + }; + Analyzer analyzer = new CustomAnalyzer("tokenizerName", null, new CharFilterFactory[0], new TokenFilterFactory[] { tokenFilter }); + MapperException ex = expectThrows(MapperException.class, + () -> emptyRegistry.build(IndexSettingsModule.newIndexSettings("index", settings), + singletonMap("default", new PreBuiltAnalyzerProvider("my_analyzer", AnalyzerScope.INDEX, analyzer)), emptyMap(), + emptyMap(), emptyMap(), emptyMap())); + assertEquals("analyzer [my_analyzer] contains filters [my_filter] that are not allowed to run in all mode.", ex.getMessage()); + } + public void testOverrideDefaultIndexAnalyzerIsUnsupported() { Version version = VersionUtils.randomVersionBetween(random(), Version.V_6_0_0_alpha1, Version.CURRENT); Settings settings = Settings.builder().put(IndexMetaData.SETTING_VERSION_CREATED, version).build(); diff --git a/server/src/test/java/org/elasticsearch/index/analysis/NamedAnalyzerTests.java b/server/src/test/java/org/elasticsearch/index/analysis/NamedAnalyzerTests.java new file mode 100644 index 0000000000000..e0f4a37c57fa2 --- /dev/null +++ b/server/src/test/java/org/elasticsearch/index/analysis/NamedAnalyzerTests.java @@ -0,0 +1,79 @@ +/* + * 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.analysis; + +import org.apache.lucene.analysis.Analyzer; +import org.apache.lucene.analysis.TokenStream; +import org.elasticsearch.index.mapper.MapperException; +import org.elasticsearch.test.ESTestCase; + +public class NamedAnalyzerTests extends ESTestCase { + + public void testCheckAllowedInMode() { + try (NamedAnalyzer testAnalyzer = new NamedAnalyzer("my_analyzer", AnalyzerScope.INDEX, + createAnalyzerWithMode("my_analyzer", AnalysisMode.INDEX_TIME), Integer.MIN_VALUE)) { + testAnalyzer.checkAllowedInMode(AnalysisMode.INDEX_TIME); + MapperException ex = expectThrows(MapperException.class, () -> testAnalyzer.checkAllowedInMode(AnalysisMode.SEARCH_TIME)); + assertEquals("analyzer [my_analyzer] contains filters [my_analyzer] that are not allowed to run in search time mode.", + ex.getMessage()); + ex = expectThrows(MapperException.class, () -> testAnalyzer.checkAllowedInMode(AnalysisMode.ALL)); + assertEquals("analyzer [my_analyzer] contains filters [my_analyzer] that are not allowed to run in all mode.", ex.getMessage()); + } + + try (NamedAnalyzer testAnalyzer = new NamedAnalyzer("my_analyzer", AnalyzerScope.INDEX, + createAnalyzerWithMode("my_analyzer", AnalysisMode.SEARCH_TIME), Integer.MIN_VALUE)) { + testAnalyzer.checkAllowedInMode(AnalysisMode.SEARCH_TIME); + MapperException ex = expectThrows(MapperException.class, () -> testAnalyzer.checkAllowedInMode(AnalysisMode.INDEX_TIME)); + assertEquals("analyzer [my_analyzer] contains filters [my_analyzer] that are not allowed to run in index time mode.", + ex.getMessage()); + ex = expectThrows(MapperException.class, () -> testAnalyzer.checkAllowedInMode(AnalysisMode.ALL)); + assertEquals("analyzer [my_analyzer] contains filters [my_analyzer] that are not allowed to run in all mode.", ex.getMessage()); + } + + try (NamedAnalyzer testAnalyzer = new NamedAnalyzer("my_analyzer", AnalyzerScope.INDEX, + createAnalyzerWithMode("my_analyzer", AnalysisMode.ALL), Integer.MIN_VALUE)) { + testAnalyzer.checkAllowedInMode(AnalysisMode.ALL); + testAnalyzer.checkAllowedInMode(AnalysisMode.INDEX_TIME); + testAnalyzer.checkAllowedInMode(AnalysisMode.SEARCH_TIME); + } + } + + private Analyzer createAnalyzerWithMode(String name, AnalysisMode mode) { + TokenFilterFactory tokenFilter = new TokenFilterFactory() { + + @Override + public String name() { + return name; + } + + @Override + public TokenStream create(TokenStream tokenStream) { + return null; + } + + @Override + public AnalysisMode getAnalysisMode() { + return mode; + } + }; + return new CustomAnalyzer("tokenizerName", null, new CharFilterFactory[0], + new TokenFilterFactory[] { tokenFilter }); + } +} diff --git a/server/src/test/java/org/elasticsearch/index/mapper/TypeParsersTests.java b/server/src/test/java/org/elasticsearch/index/mapper/TypeParsersTests.java new file mode 100644 index 0000000000000..a2966053ae7dc --- /dev/null +++ b/server/src/test/java/org/elasticsearch/index/mapper/TypeParsersTests.java @@ -0,0 +1,169 @@ +/* + * 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.apache.lucene.analysis.Analyzer; +import org.apache.lucene.analysis.TokenStream; +import org.apache.lucene.analysis.standard.StandardAnalyzer; +import org.elasticsearch.Version; +import org.elasticsearch.cluster.metadata.IndexMetaData; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.index.IndexSettings; +import org.elasticsearch.index.analysis.AbstractTokenFilterFactory; +import org.elasticsearch.index.analysis.AnalysisMode; +import org.elasticsearch.index.analysis.AnalyzerScope; +import org.elasticsearch.index.analysis.CharFilterFactory; +import org.elasticsearch.index.analysis.CustomAnalyzer; +import org.elasticsearch.index.analysis.IndexAnalyzers; +import org.elasticsearch.index.analysis.NamedAnalyzer; +import org.elasticsearch.index.analysis.TokenFilterFactory; +import org.elasticsearch.test.ESTestCase; + +import java.util.HashMap; +import java.util.Map; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class TypeParsersTests extends ESTestCase { + + private static final IndexMetaData EMPTY_INDEX_METADATA = IndexMetaData.builder("") + .settings(Settings.builder().put(IndexMetaData.SETTING_VERSION_CREATED, Version.CURRENT)) + .numberOfShards(1).numberOfReplicas(0).build(); + private static final IndexSettings indexSettings = new IndexSettings(EMPTY_INDEX_METADATA, Settings.EMPTY); + + public void testParseTextFieldCheckAnalyzerAnalysisMode() { + TextFieldMapper.Builder builder = new TextFieldMapper.Builder("textField"); + Map fieldNode = new HashMap(); + fieldNode.put("analyzer", "my_analyzer"); + Mapper.TypeParser.ParserContext parserContext = mock(Mapper.TypeParser.ParserContext.class); + + // check AnalysisMode.ALL works + Map analyzers = new HashMap<>(); + analyzers.put("my_analyzer", + new NamedAnalyzer("my_named_analyzer", AnalyzerScope.INDEX, createAnalyzerWithMode("my_analyzer", AnalysisMode.ALL))); + + IndexAnalyzers indexAnalyzers = new IndexAnalyzers(indexSettings, + new NamedAnalyzer("default", AnalyzerScope.INDEX, null), null, null, analyzers, null, null); + when(parserContext.getIndexAnalyzers()).thenReturn(indexAnalyzers); + TypeParsers.parseTextField(builder, "name", new HashMap<>(fieldNode), parserContext); + + // check that "analyzer" set to something that only supports AnalysisMode.SEARCH_TIME or AnalysisMode.INDEX_TIME is blocked + AnalysisMode mode = randomFrom(AnalysisMode.SEARCH_TIME, AnalysisMode.INDEX_TIME); + analyzers = new HashMap<>(); + analyzers.put("my_analyzer", new NamedAnalyzer("my_named_analyzer", AnalyzerScope.INDEX, + createAnalyzerWithMode("my_analyzer", mode))); + indexAnalyzers = new IndexAnalyzers(indexSettings, new NamedAnalyzer("default", AnalyzerScope.INDEX, null), null, null, analyzers, + null, null); + when(parserContext.getIndexAnalyzers()).thenReturn(indexAnalyzers); + MapperException ex = expectThrows(MapperException.class, + () -> TypeParsers.parseTextField(builder, "name", new HashMap<>(fieldNode), parserContext)); + assertEquals("analyzer [my_named_analyzer] contains filters [my_analyzer] that are not allowed to run in all mode.", + ex.getMessage()); + } + + public void testParseTextFieldCheckSearchAnalyzerAnalysisMode() { + TextFieldMapper.Builder builder = new TextFieldMapper.Builder("textField"); + for (String settingToTest : new String[] { "search_analyzer", "search_quote_analyzer" }) { + Map fieldNode = new HashMap(); + fieldNode.put(settingToTest, "my_analyzer"); + fieldNode.put("analyzer", "standard"); + if (settingToTest.equals("search_quote_analyzer")) { + fieldNode.put("search_analyzer", "standard"); + } + Mapper.TypeParser.ParserContext parserContext = mock(Mapper.TypeParser.ParserContext.class); + + // check AnalysisMode.ALL and AnalysisMode.SEARCH_TIME works + Map analyzers = new HashMap<>(); + AnalysisMode mode = randomFrom(AnalysisMode.ALL, AnalysisMode.SEARCH_TIME); + analyzers.put("my_analyzer", + new NamedAnalyzer("my_named_analyzer", AnalyzerScope.INDEX, createAnalyzerWithMode("my_analyzer", mode))); + analyzers.put("standard", new NamedAnalyzer("standard", AnalyzerScope.INDEX, new StandardAnalyzer())); + + IndexAnalyzers indexAnalyzers = new IndexAnalyzers(indexSettings, new NamedAnalyzer("default", AnalyzerScope.INDEX, null), null, + null, analyzers, null, null); + when(parserContext.getIndexAnalyzers()).thenReturn(indexAnalyzers); + TypeParsers.parseTextField(builder, "name", new HashMap<>(fieldNode), parserContext); + + // check that "analyzer" set to AnalysisMode.INDEX_TIME is blocked + mode = AnalysisMode.INDEX_TIME; + analyzers = new HashMap<>(); + analyzers.put("my_analyzer", + new NamedAnalyzer("my_named_analyzer", AnalyzerScope.INDEX, createAnalyzerWithMode("my_analyzer", mode))); + analyzers.put("standard", new NamedAnalyzer("standard", AnalyzerScope.INDEX, new StandardAnalyzer())); + indexAnalyzers = new IndexAnalyzers(indexSettings, new NamedAnalyzer("default", AnalyzerScope.INDEX, null), null, null, + analyzers, null, null); + when(parserContext.getIndexAnalyzers()).thenReturn(indexAnalyzers); + MapperException ex = expectThrows(MapperException.class, + () -> TypeParsers.parseTextField(builder, "name", new HashMap<>(fieldNode), parserContext)); + assertEquals("analyzer [my_named_analyzer] contains filters [my_analyzer] that are not allowed to run in search time mode.", + ex.getMessage()); + } + } + + public void testParseTextFieldCheckAnalyzerWithSearchAnalyzerAnalysisMode() { + TextFieldMapper.Builder builder = new TextFieldMapper.Builder("textField"); + Map fieldNode = new HashMap(); + fieldNode.put("analyzer", "my_analyzer"); + Mapper.TypeParser.ParserContext parserContext = mock(Mapper.TypeParser.ParserContext.class); + + // check that "analyzer" set to AnalysisMode.INDEX_TIME is blocked if there is no search analyzer + AnalysisMode mode = AnalysisMode.INDEX_TIME; + Map analyzers = new HashMap<>(); + analyzers.put("my_analyzer", + new NamedAnalyzer("my_named_analyzer", AnalyzerScope.INDEX, createAnalyzerWithMode("my_analyzer", mode))); + IndexAnalyzers indexAnalyzers = new IndexAnalyzers(indexSettings, new NamedAnalyzer("default", AnalyzerScope.INDEX, null), null, + null, analyzers, null, null); + when(parserContext.getIndexAnalyzers()).thenReturn(indexAnalyzers); + MapperException ex = expectThrows(MapperException.class, + () -> TypeParsers.parseTextField(builder, "name", new HashMap<>(fieldNode), parserContext)); + assertEquals("analyzer [my_named_analyzer] contains filters [my_analyzer] that are not allowed to run in all mode.", + ex.getMessage()); + + // check AnalysisMode.INDEX_TIME is okay if search analyzer is also set + fieldNode.put("search_analyzer", "standard"); + analyzers = new HashMap<>(); + mode = randomFrom(AnalysisMode.ALL, AnalysisMode.INDEX_TIME); + analyzers.put("my_analyzer", + new NamedAnalyzer("my_named_analyzer", AnalyzerScope.INDEX, createAnalyzerWithMode("my_analyzer", mode))); + analyzers.put("standard", new NamedAnalyzer("standard", AnalyzerScope.INDEX, new StandardAnalyzer())); + + indexAnalyzers = new IndexAnalyzers(indexSettings, new NamedAnalyzer("default", AnalyzerScope.INDEX, null), null, null, analyzers, + null, null); + when(parserContext.getIndexAnalyzers()).thenReturn(indexAnalyzers); + TypeParsers.parseTextField(builder, "name", new HashMap<>(fieldNode), parserContext); + } + + private Analyzer createAnalyzerWithMode(String name, AnalysisMode mode) { + TokenFilterFactory tokenFilter = new AbstractTokenFilterFactory(indexSettings, name, Settings.EMPTY) { + @Override + public AnalysisMode getAnalysisMode() { + return mode; + } + + @Override + public TokenStream create(TokenStream tokenStream) { + return null; + } + }; + return new CustomAnalyzer("tokenizerName", null, new CharFilterFactory[0], + new TokenFilterFactory[] { tokenFilter }); + } +}