blob: 593c3d00fb7903bb159445e631237488d4999dbd [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF 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.apache.lucene.analysis.custom;
import static org.apache.lucene.analysis.AnalysisSPILoader.newFactoryClassInstance;
import java.io.IOException;
import java.io.Reader;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Function;
import java.util.function.Predicate;
import org.apache.lucene.analysis.AbstractAnalysisFactory;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.CharFilterFactory;
import org.apache.lucene.analysis.TokenFilterFactory;
import org.apache.lucene.analysis.TokenStream;
import org.apache.lucene.analysis.Tokenizer;
import org.apache.lucene.analysis.TokenizerFactory;
import org.apache.lucene.analysis.miscellaneous.ConditionalTokenFilter;
import org.apache.lucene.analysis.miscellaneous.ConditionalTokenFilterFactory;
import org.apache.lucene.analysis.tokenattributes.CharTermAttribute;
import org.apache.lucene.analysis.util.FilesystemResourceLoader;
import org.apache.lucene.util.ClasspathResourceLoader;
import org.apache.lucene.util.ResourceLoader;
import org.apache.lucene.util.ResourceLoaderAware;
import org.apache.lucene.util.SetOnce;
import org.apache.lucene.util.Version;
/**
* A general-purpose Analyzer that can be created with a builder-style API. Under the hood it uses
* the factory classes {@link TokenizerFactory}, {@link TokenFilterFactory}, and {@link
* CharFilterFactory}.
*
* <p>You can create an instance of this Analyzer using the builder by passing the SPI names (as
* defined by {@link java.util.ServiceLoader} interface) to it:
*
* <pre class="prettyprint">
* Analyzer ana = CustomAnalyzer.builder(Paths.get(&quot;/path/to/config/dir&quot;))
* .withTokenizer(StandardTokenizerFactory.NAME)
* .addTokenFilter(LowerCaseFilterFactory.NAME)
* .addTokenFilter(StopFilterFactory.NAME, &quot;ignoreCase&quot;, &quot;false&quot;, &quot;words&quot;, &quot;stopwords.txt&quot;, &quot;format&quot;, &quot;wordset&quot;)
* .build();
* </pre>
*
* The parameters passed to components are also used by Apache Solr and are documented on their
* corresponding factory classes. Refer to documentation of subclasses of {@link TokenizerFactory},
* {@link TokenFilterFactory}, and {@link CharFilterFactory}.
*
* <p>This is the same as the above:
*
* <pre class="prettyprint">
* Analyzer ana = CustomAnalyzer.builder(Paths.get(&quot;/path/to/config/dir&quot;))
* .withTokenizer(&quot;standard&quot;)
* .addTokenFilter(&quot;lowercase&quot;)
* .addTokenFilter(&quot;stop&quot;, &quot;ignoreCase&quot;, &quot;false&quot;, &quot;words&quot;, &quot;stopwords.txt&quot;, &quot;format&quot;, &quot;wordset&quot;)
* .build();
* </pre>
*
* <p>The list of names to be used for components can be looked up through: {@link
* TokenizerFactory#availableTokenizers()}, {@link TokenFilterFactory#availableTokenFilters()}, and
* {@link CharFilterFactory#availableCharFilters()}.
*
* <p>You can create conditional branches in the analyzer by using {@link Builder#when(String,
* String...)} and {@link Builder#whenTerm(Predicate)}:
*
* <pre class="prettyprint">
* Analyzer ana = CustomAnalyzer.builder()
* .withTokenizer(&quot;standard&quot;)
* .addTokenFilter(&quot;lowercase&quot;)
* .whenTerm(t -&gt; t.length() &gt; 10)
* .addTokenFilter(&quot;reversestring&quot;)
* .endwhen()
* .build();
* </pre>
*
* @since 5.0.0
*/
public final class CustomAnalyzer extends Analyzer {
/**
* Returns a builder for custom analyzers that loads all resources from Lucene's classloader. All
* path names given must be absolute with package prefixes.
*/
public static Builder builder() {
return builder(new ClasspathResourceLoader(CustomAnalyzer.class.getClassLoader()));
}
/**
* Returns a builder for custom analyzers that loads all resources from the given file system base
* directory. Place, e.g., stop word files there. Files that are not in the given directory are
* loaded from Lucene's classloader.
*/
public static Builder builder(Path configDir) {
return builder(new FilesystemResourceLoader(configDir, CustomAnalyzer.class.getClassLoader()));
}
/**
* Returns a builder for custom analyzers that loads all resources using the given {@link
* ResourceLoader}.
*/
public static Builder builder(ResourceLoader loader) {
return new Builder(loader);
}
private final CharFilterFactory[] charFilters;
private final TokenizerFactory tokenizer;
private final TokenFilterFactory[] tokenFilters;
private final Integer posIncGap, offsetGap;
CustomAnalyzer(
Version defaultMatchVersion,
CharFilterFactory[] charFilters,
TokenizerFactory tokenizer,
TokenFilterFactory[] tokenFilters,
Integer posIncGap,
Integer offsetGap) {
this.charFilters = charFilters;
this.tokenizer = tokenizer;
this.tokenFilters = tokenFilters;
this.posIncGap = posIncGap;
this.offsetGap = offsetGap;
if (defaultMatchVersion != null) {
setVersion(defaultMatchVersion);
}
}
@Override
protected Reader initReader(String fieldName, Reader reader) {
for (final CharFilterFactory charFilter : charFilters) {
reader = charFilter.create(reader);
}
return reader;
}
@Override
protected Reader initReaderForNormalization(String fieldName, Reader reader) {
for (CharFilterFactory charFilter : charFilters) {
reader = charFilter.normalize(reader);
}
return reader;
}
@Override
protected TokenStreamComponents createComponents(String fieldName) {
final Tokenizer tk = tokenizer.create(attributeFactory(fieldName));
TokenStream ts = tk;
for (final TokenFilterFactory filter : tokenFilters) {
ts = filter.create(ts);
}
return new TokenStreamComponents(tk, ts);
}
@Override
protected TokenStream normalize(String fieldName, TokenStream in) {
TokenStream result = in;
for (TokenFilterFactory filter : tokenFilters) {
result = filter.normalize(result);
}
return result;
}
@Override
public int getPositionIncrementGap(String fieldName) {
// use default from Analyzer base class if null
return (posIncGap == null) ? super.getPositionIncrementGap(fieldName) : posIncGap.intValue();
}
@Override
public int getOffsetGap(String fieldName) {
// use default from Analyzer base class if null
return (offsetGap == null) ? super.getOffsetGap(fieldName) : offsetGap.intValue();
}
/** Returns the list of char filters that are used in this analyzer. */
public List<CharFilterFactory> getCharFilterFactories() {
return Collections.unmodifiableList(Arrays.asList(charFilters));
}
/** Returns the tokenizer that is used in this analyzer. */
public TokenizerFactory getTokenizerFactory() {
return tokenizer;
}
/** Returns the list of token filters that are used in this analyzer. */
public List<TokenFilterFactory> getTokenFilterFactories() {
return Collections.unmodifiableList(Arrays.asList(tokenFilters));
}
@Override
public String toString() {
final StringBuilder sb = new StringBuilder(getClass().getSimpleName()).append('(');
for (final CharFilterFactory filter : charFilters) {
sb.append(filter).append(',');
}
sb.append(tokenizer);
for (final TokenFilterFactory filter : tokenFilters) {
sb.append(',').append(filter);
}
return sb.append(')').toString();
}
/**
* Builder for {@link CustomAnalyzer}.
*
* @see CustomAnalyzer#builder()
* @see CustomAnalyzer#builder(Path)
* @see CustomAnalyzer#builder(ResourceLoader)
*/
public static final class Builder {
private final ResourceLoader loader;
private final SetOnce<Version> defaultMatchVersion = new SetOnce<>();
private final List<CharFilterFactory> charFilters = new ArrayList<>();
private final SetOnce<TokenizerFactory> tokenizer = new SetOnce<>();
private final List<TokenFilterFactory> tokenFilters = new ArrayList<>();
private final SetOnce<Integer> posIncGap = new SetOnce<>();
private final SetOnce<Integer> offsetGap = new SetOnce<>();
private boolean componentsAdded = false;
Builder(ResourceLoader loader) {
this.loader = loader;
}
/**
* This match version is passed as default to all tokenizers or filters. It is used unless you
* pass the parameter {code luceneMatchVersion} explicitly. It defaults to undefined, so the
* underlying factory will (in most cases) use {@link Version#LATEST}.
*/
public Builder withDefaultMatchVersion(Version version) {
Objects.requireNonNull(version, "version may not be null");
if (componentsAdded) {
throw new IllegalStateException(
"You may only set the default match version before adding tokenizers, "
+ "token filters, or char filters.");
}
this.defaultMatchVersion.set(version);
return this;
}
/**
* Sets the position increment gap of the analyzer. The default is defined in the analyzer base
* class.
*
* @see Analyzer#getPositionIncrementGap(String)
*/
public Builder withPositionIncrementGap(int posIncGap) {
if (posIncGap < 0) {
throw new IllegalArgumentException("posIncGap must be >= 0");
}
this.posIncGap.set(posIncGap);
return this;
}
/**
* Sets the offset gap of the analyzer. The default is defined in the analyzer base class.
*
* @see Analyzer#getOffsetGap(String)
*/
public Builder withOffsetGap(int offsetGap) {
if (offsetGap < 0) {
throw new IllegalArgumentException("offsetGap must be >= 0");
}
this.offsetGap.set(offsetGap);
return this;
}
/**
* Uses the given tokenizer.
*
* @param factory class that is used to create the tokenizer.
* @param params a list of factory string params as key/value pairs. The number of parameters
* must be an even number, as they are pairs.
*/
public Builder withTokenizer(Class<? extends TokenizerFactory> factory, String... params)
throws IOException {
return withTokenizer(factory, paramsToMap(params));
}
/**
* Uses the given tokenizer.
*
* @param factory class that is used to create the tokenizer.
* @param params the map of parameters to be passed to factory. The map must be modifiable.
*/
public Builder withTokenizer(
Class<? extends TokenizerFactory> factory, Map<String, String> params) throws IOException {
Objects.requireNonNull(factory, "Tokenizer factory may not be null");
tokenizer.set(
applyResourceLoader(newFactoryClassInstance(factory, applyDefaultParams(params))));
componentsAdded = true;
return this;
}
/**
* Uses the given tokenizer.
*
* @param name is used to look up the factory with {@link TokenizerFactory#forName(String,
* Map)}. The list of possible names can be looked up with {@link
* TokenizerFactory#availableTokenizers()}.
* @param params a list of factory string params as key/value pairs. The number of parameters
* must be an even number, as they are pairs.
*/
public Builder withTokenizer(String name, String... params) throws IOException {
return withTokenizer(name, paramsToMap(params));
}
/**
* Uses the given tokenizer.
*
* @param name is used to look up the factory with {@link TokenizerFactory#forName(String,
* Map)}. The list of possible names can be looked up with {@link
* TokenizerFactory#availableTokenizers()}.
* @param params the map of parameters to be passed to factory. The map must be modifiable.
*/
public Builder withTokenizer(String name, Map<String, String> params) throws IOException {
Objects.requireNonNull(name, "Tokenizer name may not be null");
tokenizer.set(
applyResourceLoader(TokenizerFactory.forName(name, applyDefaultParams(params))));
componentsAdded = true;
return this;
}
/**
* Adds the given token filter.
*
* @param factory class that is used to create the token filter.
* @param params a list of factory string params as key/value pairs. The number of parameters
* must be an even number, as they are pairs.
*/
public Builder addTokenFilter(Class<? extends TokenFilterFactory> factory, String... params)
throws IOException {
return addTokenFilter(factory, paramsToMap(params));
}
/**
* Adds the given token filter.
*
* @param factory class that is used to create the token filter.
* @param params the map of parameters to be passed to factory. The map must be modifiable.
*/
public Builder addTokenFilter(
Class<? extends TokenFilterFactory> factory, Map<String, String> params)
throws IOException {
Objects.requireNonNull(factory, "TokenFilter name may not be null");
tokenFilters.add(
applyResourceLoader(newFactoryClassInstance(factory, applyDefaultParams(params))));
componentsAdded = true;
return this;
}
/**
* Adds the given token filter.
*
* @param name is used to look up the factory with {@link TokenFilterFactory#forName(String,
* Map)}. The list of possible names can be looked up with {@link
* TokenFilterFactory#availableTokenFilters()}.
* @param params a list of factory string params as key/value pairs. The number of parameters
* must be an even number, as they are pairs.
*/
public Builder addTokenFilter(String name, String... params) throws IOException {
return addTokenFilter(name, paramsToMap(params));
}
/**
* Adds the given token filter.
*
* @param name is used to look up the factory with {@link TokenFilterFactory#forName(String,
* Map)}. The list of possible names can be looked up with {@link
* TokenFilterFactory#availableTokenFilters()}.
* @param params the map of parameters to be passed to factory. The map must be modifiable.
*/
public Builder addTokenFilter(String name, Map<String, String> params) throws IOException {
Objects.requireNonNull(name, "TokenFilter name may not be null");
tokenFilters.add(
applyResourceLoader(TokenFilterFactory.forName(name, applyDefaultParams(params))));
componentsAdded = true;
return this;
}
private Builder addTokenFilter(TokenFilterFactory factory) {
Objects.requireNonNull(factory, "TokenFilterFactory may not be null");
tokenFilters.add(factory);
componentsAdded = true;
return this;
}
/**
* Adds the given char filter.
*
* @param factory class that is used to create the char filter.
* @param params a list of factory string params as key/value pairs. The number of parameters
* must be an even number, as they are pairs.
*/
public Builder addCharFilter(Class<? extends CharFilterFactory> factory, String... params)
throws IOException {
return addCharFilter(factory, paramsToMap(params));
}
/**
* Adds the given char filter.
*
* @param factory class that is used to create the char filter.
* @param params the map of parameters to be passed to factory. The map must be modifiable.
*/
public Builder addCharFilter(
Class<? extends CharFilterFactory> factory, Map<String, String> params) throws IOException {
Objects.requireNonNull(factory, "CharFilter name may not be null");
charFilters.add(
applyResourceLoader(newFactoryClassInstance(factory, applyDefaultParams(params))));
componentsAdded = true;
return this;
}
/**
* Adds the given char filter.
*
* @param name is used to look up the factory with {@link CharFilterFactory#forName(String,
* Map)}. The list of possible names can be looked up with {@link
* CharFilterFactory#availableCharFilters()}.
* @param params a list of factory string params as key/value pairs. The number of parameters
* must be an even number, as they are pairs.
*/
public Builder addCharFilter(String name, String... params) throws IOException {
return addCharFilter(name, paramsToMap(params));
}
/**
* Adds the given char filter.
*
* @param name is used to look up the factory with {@link CharFilterFactory#forName(String,
* Map)}. The list of possible names can be looked up with {@link
* CharFilterFactory#availableCharFilters()}.
* @param params the map of parameters to be passed to factory. The map must be modifiable.
*/
public Builder addCharFilter(String name, Map<String, String> params) throws IOException {
Objects.requireNonNull(name, "CharFilter name may not be null");
charFilters.add(
applyResourceLoader(CharFilterFactory.forName(name, applyDefaultParams(params))));
componentsAdded = true;
return this;
}
/**
* Add a {@link ConditionalTokenFilterFactory} to the analysis chain
*
* <p>TokenFilters added by subsequent calls to {@link ConditionBuilder#addTokenFilter(String,
* String...)} and related functions will only be used if the current token matches the
* condition. Consumers must call {@link ConditionBuilder#endwhen()} to return to the normal
* tokenfilter chain once conditional filters have been added
*
* @param name is used to look up the factory with {@link TokenFilterFactory#forName(String,
* Map)}
* @param params the parameters to be passed to the factory
*/
public ConditionBuilder when(String name, String... params) throws IOException {
return when(name, paramsToMap(params));
}
/**
* Add a {@link ConditionalTokenFilterFactory} to the analysis chain
*
* <p>TokenFilters added by subsequent calls to {@link ConditionBuilder#addTokenFilter(String,
* String...)} and related functions will only be used if the current token matches the
* condition. Consumers must call {@link ConditionBuilder#endwhen()} to return to the normal
* tokenfilter chain once conditional filters have been added
*
* @param name is used to look up the factory with {@link TokenFilterFactory#forName(String,
* Map)}
* @param params the parameters to be passed to the factory. The map must be modifiable
*/
@SuppressWarnings("unchecked")
public ConditionBuilder when(String name, Map<String, String> params) throws IOException {
Class<? extends TokenFilterFactory> clazz = TokenFilterFactory.lookupClass(name);
if (ConditionalTokenFilterFactory.class.isAssignableFrom(clazz) == false) {
throw new IllegalArgumentException(
"TokenFilterFactory " + name + " is not a ConditionalTokenFilterFactory");
}
return when((Class<? extends ConditionalTokenFilterFactory>) clazz, params);
}
/**
* Add a {@link ConditionalTokenFilterFactory} to the analysis chain
*
* <p>TokenFilters added by subsequent calls to {@link ConditionBuilder#addTokenFilter(String,
* String...)} and related functions will only be used if the current token matches the
* condition. Consumers must call {@link ConditionBuilder#endwhen()} to return to the normal
* tokenfilter chain once conditional filters have been added
*
* @param factory class that is used to create the ConditionalTokenFilter
* @param params the parameters to be passed to the factory
*/
public ConditionBuilder when(
Class<? extends ConditionalTokenFilterFactory> factory, String... params)
throws IOException {
return when(factory, paramsToMap(params));
}
/**
* Add a {@link ConditionalTokenFilterFactory} to the analysis chain
*
* <p>TokenFilters added by subsequent calls to {@link ConditionBuilder#addTokenFilter(String,
* String...)} and related functions will only be used if the current token matches the
* condition. Consumers must call {@link ConditionBuilder#endwhen()} to return to the normal
* tokenfilter chain once conditional filters have been added
*
* @param factory class that is used to create the ConditionalTokenFilter
* @param params the parameters to be passed to the factory. The map must be modifiable
*/
public ConditionBuilder when(
Class<? extends ConditionalTokenFilterFactory> factory, Map<String, String> params)
throws IOException {
return when(newFactoryClassInstance(factory, applyDefaultParams(params)));
}
/**
* Add a {@link ConditionalTokenFilterFactory} to the analysis chain
*
* <p>TokenFilters added by subsequent calls to {@link ConditionBuilder#addTokenFilter(String,
* String...)} and related functions will only be used if the current token matches the
* condition. Consumers must call {@link ConditionBuilder#endwhen()} to return to the normal
* tokenfilter chain once conditional filters have been added
*/
public ConditionBuilder when(ConditionalTokenFilterFactory factory) {
return new ConditionBuilder(factory, this);
}
/**
* Apply subsequent token filters if the current token's term matches a predicate
*
* <p>This is the equivalent of:
*
* <pre>
* when(new ConditionalTokenFilterFactory(Collections.emptyMap()) {
* {@code @}Override
* protected ConditionalTokenFilter create(TokenStream input, Function&lt;TokenStream, TokenStream&gt; inner) {
* return new ConditionalTokenFilter(input, inner) {
* CharTermAttribute termAtt = addAttribute(CharTermAttribute.class);
* {@code @}Override
* protected boolean shouldFilter() {
* return predicate.test(termAtt);
* }
* };
* }
* });
* </pre>
*/
public ConditionBuilder whenTerm(Predicate<CharSequence> predicate) {
return new ConditionBuilder(
new ConditionalTokenFilterFactory(Collections.emptyMap()) {
@Override
protected ConditionalTokenFilter create(
TokenStream input, Function<TokenStream, TokenStream> inner) {
return new ConditionalTokenFilter(input, inner) {
CharTermAttribute termAtt = addAttribute(CharTermAttribute.class);
@Override
protected boolean shouldFilter() {
return predicate.test(termAtt);
}
};
}
},
this);
}
/** Builds the analyzer. */
public CustomAnalyzer build() {
if (tokenizer.get() == null) {
throw new IllegalStateException("You have to set at least a tokenizer.");
}
return new CustomAnalyzer(
defaultMatchVersion.get(),
charFilters.toArray(new CharFilterFactory[charFilters.size()]),
tokenizer.get(),
tokenFilters.toArray(new TokenFilterFactory[tokenFilters.size()]),
posIncGap.get(),
offsetGap.get());
}
private Map<String, String> applyDefaultParams(Map<String, String> map) {
if (defaultMatchVersion.get() != null
&& !map.containsKey(AbstractAnalysisFactory.LUCENE_MATCH_VERSION_PARAM)) {
map.put(
AbstractAnalysisFactory.LUCENE_MATCH_VERSION_PARAM,
defaultMatchVersion.get().toString());
}
return map;
}
private Map<String, String> paramsToMap(String... params) {
if (params.length % 2 != 0) {
throw new IllegalArgumentException(
"Key-value pairs expected, so the number of params must be even.");
}
final Map<String, String> map = new HashMap<>();
for (int i = 0; i < params.length; i += 2) {
Objects.requireNonNull(params[i], "Key of param may not be null.");
map.put(params[i], params[i + 1]);
}
return map;
}
<T> T applyResourceLoader(T factory) throws IOException {
if (factory instanceof ResourceLoaderAware) {
((ResourceLoaderAware) factory).inform(loader);
}
return factory;
}
}
/** Factory class for a {@link ConditionalTokenFilter} */
public static class ConditionBuilder {
private final List<TokenFilterFactory> innerFilters = new ArrayList<>();
private final ConditionalTokenFilterFactory factory;
private final Builder parent;
private ConditionBuilder(ConditionalTokenFilterFactory factory, Builder parent) {
this.factory = factory;
this.parent = parent;
}
/**
* Adds the given token filter.
*
* @param name is used to look up the factory with {@link TokenFilterFactory#forName(String,
* Map)}. The list of possible names can be looked up with {@link
* TokenFilterFactory#availableTokenFilters()}.
* @param params the map of parameters to be passed to factory. The map must be modifiable.
*/
public ConditionBuilder addTokenFilter(String name, Map<String, String> params)
throws IOException {
innerFilters.add(TokenFilterFactory.forName(name, parent.applyDefaultParams(params)));
return this;
}
/**
* Adds the given token filter.
*
* @param name is used to look up the factory with {@link TokenFilterFactory#forName(String,
* Map)}. The list of possible names can be looked up with {@link
* TokenFilterFactory#availableTokenFilters()}.
* @param params the map of parameters to be passed to factory. The map must be modifiable.
*/
public ConditionBuilder addTokenFilter(String name, String... params) throws IOException {
return addTokenFilter(name, parent.paramsToMap(params));
}
/**
* Adds the given token filter.
*
* @param factory class that is used to create the token filter.
* @param params the map of parameters to be passed to factory. The map must be modifiable.
*/
public ConditionBuilder addTokenFilter(
Class<? extends TokenFilterFactory> factory, Map<String, String> params)
throws IOException {
innerFilters.add(newFactoryClassInstance(factory, parent.applyDefaultParams(params)));
return this;
}
/**
* Adds the given token filter.
*
* @param factory class that is used to create the token filter.
* @param params the map of parameters to be passed to factory. The map must be modifiable.
*/
public ConditionBuilder addTokenFilter(
Class<? extends TokenFilterFactory> factory, String... params) throws IOException {
return addTokenFilter(factory, parent.paramsToMap(params));
}
/** Close the branch and return to the main analysis chain */
public Builder endwhen() throws IOException {
factory.setInnerFilters(innerFilters);
parent.applyResourceLoader(factory);
parent.addTokenFilter(factory);
return parent;
}
}
}