Token-filter recipes
|
This section documents the current Apache Lucene 10.x line — Lucene 10 requires Java 21 — as published
at the Apache Lucene documentation and Javadoc, which is the reference these
pages are written and verified against. No specific patch version is pinned; examples target This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production, as Lucene iterates quickly. This section’s bibliography lists the reference material consulted while preparing these pages. |
Once the analysis pipeline and
CustomAnalyzer are in place, most
real work is choosing the right token filters and ordering them correctly. This page is a set of
recipes — each a self-contained CustomAnalyzer or Analyzer — for the filters in the
lucene-analysis-common module that come up again and again. Every class here has SPI-name and
factory forms; see
the
analysis-common package summary for the full catalogue.
Folding: lower-casing and accent removal
LowerCaseFilter normalises case; ASCIIFoldingFilter maps accented Latin-1/Latin-Extended
characters to their ASCII base (café → cafe), optionally keeping the original token with
preserveOriginal=true so an exact accented match still scores. Fold with the same chain at index
and query time.
// https://lucene.apache.org/core/10_0_0/analysis/common/org/apache/lucene/analysis/miscellaneous/ASCIIFoldingFilter.html
Analyzer folding = CustomAnalyzer.builder()
.withTokenizer("standard")
.addTokenFilter("lowercase")
.addTokenFilter("asciiFolding", "preserveOriginal", "false")
.build();
For non-Latin scripts reach for the ICU filters instead — see Pluggable language modules.
Stop words
StopFilter drops high-frequency, low-signal terms. Pass an explicit CharArraySet rather than
relying on a language default when you can, and remember that removing stop words changes phrase
positions — a slop-0 "to be or not to be" phrase cannot match an index that dropped every word in
it.
// https://lucene.apache.org/core/10_0_0/analysis/common/org/apache/lucene/analysis/core/StopFilter.html
CharArraySet stop = new CharArraySet(Arrays.asList("a", "an", "the", "of", "and"), true);
Analyzer withStop = CustomAnalyzer.builder()
.withTokenizer("standard")
.addTokenFilter("lowercase")
.addTokenFilter("stop", "words", "stopwords.txt", "ignoreCase", "true") // file on the resource path
.build();
// or programmatically: new StopFilter(tokenStream, stop)
Synonyms: SynonymGraphFilter + FlattenGraphFilter
SynonymGraphFilter emits a token graph so multi-word synonyms (ny <→ new york) keep correct
positions. A graph cannot be written to the index as-is, so the index analyzer must append
FlattenGraphFilter as the last filter; the query analyzer uses SynonymGraphFilter alone (the
query path consumes the graph directly).
// https://lucene.apache.org/core/10_0_0/analysis/common/org/apache/lucene/analysis/synonym/SynonymGraphFilter.html
SynonymMap.Builder b = new SynonymMap.Builder(true);
b.add(new CharsRef("ny"), new CharsRef("new york"), true);
b.add(new CharsRef("laptop"), new CharsRef("notebook"), true);
final SynonymMap synonyms = b.build();
Analyzer indexAnalyzer = new Analyzer() {
@Override
protected TokenStreamComponents createComponents(String field) {
Tokenizer src = new StandardTokenizer();
TokenStream tok = new LowerCaseFilter(src);
tok = new SynonymGraphFilter(tok, synonyms, true);
tok = new FlattenGraphFilter(tok); // index-time only
return new TokenStreamComponents(src, tok);
}
};
// Query analyzer: identical but WITHOUT FlattenGraphFilter.
Stemming and lemmatization
Four interchangeable reducers, roughly from most aggressive to least:
| Filter | Notes |
|---|---|
|
Classic Porter algorithm, English only, no options; fast and aggressive ( |
|
Less aggressive English stemmer that stays closer to real words ( |
|
Snowball (Porter2) with a |
|
Not a stemmer: a dictionary that pins listed words to a fixed stem and marks them keyword so a later stemmer leaves them alone. Place it before the real stemmer. |
// https://lucene.apache.org/core/10_0_0/analysis/common/org/apache/lucene/analysis/en/KStemFilter.html
// https://lucene.apache.org/core/10_0_0/analysis/common/org/apache/lucene/analysis/miscellaneous/StemmerOverrideFilter.html
StemmerOverrideFilter.Builder ob = new StemmerOverrideFilter.Builder();
ob.add("mice", "mouse");
ob.add("aging", "age");
final StemmerOverrideFilter.StemmerOverrideMap overrides = ob.build();
Analyzer english = new Analyzer() {
@Override
protected TokenStreamComponents createComponents(String field) {
Tokenizer src = new StandardTokenizer();
TokenStream tok = new LowerCaseFilter(src);
tok = new StemmerOverrideFilter(tok, overrides);
tok = new KStemFilter(tok); // swap for PorterStemFilter / SnowballFilter
return new TokenStreamComponents(src, tok);
}
};
Autocomplete and partial matching: n-grams
Two shapes:
-
EdgeNGramTokenFilter— prefix grams of each token (search→s, se, sea, sear, searc, search). Apply it in the index analyzer only; the query analyzer feeds the raw user prefix soseamatches theseagram. This is the standard "search-as-you-type" recipe. -
NGramTokenizer/NGramTokenFilter— interior grams too, for substring ("infix") matching.
Both multiply term volume: a 20-character field with minGramSize=1 produces up to ~20 terms per
token, so the term dictionary and postings grow several-fold. Bound minGramSize (2 or 3, not 1) and
maxGramSize, and prefer a dedicated field so the cost is not paid on every query.
// https://lucene.apache.org/core/10_0_0/analysis/common/org/apache/lucene/analysis/ngram/EdgeNGramTokenFilter.html
Analyzer autocompleteIndex = CustomAnalyzer.builder()
.withTokenizer("standard")
.addTokenFilter("lowercase")
.addTokenFilter("asciiFolding")
.addTokenFilter("edgeNGram", "minGramSize", "2", "maxGramSize", "20", "preserveOriginal", "true")
.build();
Analyzer autocompleteQuery = CustomAnalyzer.builder() // NO edgeNGram here
.withTokenizer("standard")
.addTokenFilter("lowercase")
.addTokenFilter("asciiFolding")
.build();
For a ranked suggester that is not a plain analyzer chain, see Highlighting, suggesters & more.
ShingleFilter: token n-grams for phrase-ish matching
ShingleFilter joins adjacent tokens into "shingles" (the quick fox → the quick, quick fox),
which lets a plain TermQuery approximate a phrase match and feeds bigram-boost strategies. Keep
outputUnigrams=true unless the field is used only for shingle matching.
// https://lucene.apache.org/core/10_0_0/analysis/common/org/apache/lucene/analysis/shingle/ShingleFilter.html
Analyzer shingles = CustomAnalyzer.builder()
.withTokenizer("standard")
.addTokenFilter("lowercase")
.addTokenFilter("shingle",
"minShingleSize", "2", "maxShingleSize", "2",
"outputUnigrams", "true", "tokenSeparator", " ")
.build();
WordDelimiterGraphFilter: splitting mixed tokens
Splits Wi-Fi, PowerShot, SD500 and iPhone10 on case changes, letter/number boundaries and
intra-word delimiters, and can re-concatenate the parts. Like the synonym filter it produces a graph,
so the index analyzer ends with FlattenGraphFilter. Run it before LowerCaseFilter when
splitOnCaseChange is on, or the case information is already gone.
// https://lucene.apache.org/core/10_0_0/analysis/common/org/apache/lucene/analysis/miscellaneous/WordDelimiterGraphFilter.html
Analyzer partNumbers = new Analyzer() {
@Override
protected TokenStreamComponents createComponents(String field) {
Tokenizer src = new WhitespaceTokenizer();
int flags = WordDelimiterGraphFilter.GENERATE_WORD_PARTS
| WordDelimiterGraphFilter.GENERATE_NUMBER_PARTS
| WordDelimiterGraphFilter.CATENATE_WORDS
| WordDelimiterGraphFilter.SPLIT_ON_CASE_CHANGE
| WordDelimiterGraphFilter.PRESERVE_ORIGINAL;
TokenStream tok = new WordDelimiterGraphFilter(src, flags, null);
tok = new LowerCaseFilter(tok);
tok = new FlattenGraphFilter(tok); // index-time only
return new TokenStreamComponents(src, tok);
}
};
DelimitedPayloadTokenFilter: per-term payloads
Reads a delimiter-suffixed weight off each token (fox|0.9) and stores it as a term
payload — a few bytes attached to each posting, later read back by a payload-aware query or
similarity for term-level weighting (importance tags, part-of-speech weights, per-occurrence scores).
// https://lucene.apache.org/core/10_0_0/analysis/common/org/apache/lucene/analysis/payloads/DelimitedPayloadTokenFilter.html
Analyzer payloads = new Analyzer() {
@Override
protected TokenStreamComponents createComponents(String field) {
Tokenizer src = new WhitespaceTokenizer(); // input: "quick|0.2 fox|0.9"
TokenStream tok = new LowerCaseFilter(src);
tok = new DelimitedPayloadTokenFilter(tok, '|', new FloatEncoder());
return new TokenStreamComponents(src, tok);
}
};
// Read them back with PostingsEnum.getPayload(), or score with
// org.apache.lucene.queries.payloads.PayloadScoreQuery.
Related pages
-
Analysis pipeline — char filters, tokenizers and the
TokenStreamcontract these recipes plug into. -
Built-in analyzers & CustomAnalyzer — the builder and SPI-name resolution used throughout.
-
Pluggable language modules — ICU folding and per-language stemming for non-English text.
-
Highlighting, suggesters & more — ranked autocomplete beyond an n-gram field.
-
Elasticsearch: text analysis — the same filter set exposed as JSON analyzer/filter definitions.