Language analysis: per-language stemming, lemmatization & detection
|
This section documents the current Solr line (10.0; 9.10.x the maintained 9.x branch), written and verified against the Apache Solr Reference Guide. No specific patch version is pinned. Some capabilities (the Solr Operator on Kubernetes, the package-manager ecosystem, Learning To Rank model training, and expert plugin development) are linked, not documented in depth. This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. This section’s bibliography lists the reference material consulted while preparing these pages. |
Solr’s default analysis chains (see Text analysis: analyzers, tokenizers & token filters) are tuned for
English-like whitespace-and-punctuation text. Real multilingual content needs more: a German
compound word, an Arabic word written with or without diacritics, and a Japanese sentence with no
spaces at all each need a different tokenizer and a different notion of "the same word." This page
covers Solr’s per-language field types and analyzer chains, the stemming-vs-lemmatization trade-off
(including Hunspell dictionary stemming and OpenNLP lemmatization), ICU folding, phonetic matching,
and the langid update processor that can detect a document’s language and route it to the right
field automatically. See
Language Analysis
for the full per-language reference this page draws from.
Strategies for multilingual content
Before picking filters, decide how languages map onto the schema. Three approaches are common, and they are not mutually exclusive within one deployment:
| Strategy | How it works and when to use it |
|---|---|
Field-per-language |
One field per language per logical attribute ( |
Core-per-language (or collection-per-language) |
A separate core or collection per language, each with its own schema and configset, so per-language tuning (stemmers, stopwords, synonyms, caches) is fully isolated and one language’s reindex or schema change cannot affect another’s. Cross-language search then means querying multiple collections and merging results (or fronting them with an aggregator), which costs more operationally. See Collections API, configsets & replica placement and SolrCloud architecture. |
Multiple languages in one field |
A single field with a generic, language-agnostic chain (Unicode tokenization plus ICU folding and normalization, no language-specific stemming) that degrades gracefully across languages. Simplest to operate, but no field gets language-specific stemming, so recall and ranking quality drop relative to a tuned per-language field. Reasonable when the language mix is unknown per document or too broad to enumerate. |
A langid update processor (below) can automate the field-per-language approach by detecting each
document’s language at index time and writing (or renaming) into the matching field, so the client
does not have to know the language in advance.
Language-specific field types and analyzer chains
Each language ships as a pre-built fieldType in the example managed-schema (text_en, text_de,
text_fr, text_ar, text_ja, text_zh, …), each combining a tokenizer with the token filters
appropriate for that language’s morphology. Declare or inspect one through the Schema API like any
other field type — see Schema & fields for the managed schema and the
Schema API in general.
# Inspect the built-in English field type and its analyzer chain.
curl "http://localhost:8983/solr/books/schema/fieldtypes/text_en?wt=json"
# https://solr.apache.org/guide/solr/latest/indexing-guide/language-analysis.html
A representative German field type — a language with productive compounding and case-sensitive capitalization rules — layers a decompounder ahead of a language-specific stemmer:
{
"add-field-type": {
"name": "text_de_tuned",
"class": "solr.TextField",
"positionIncrementGap": "100",
"analyzer": {
"tokenizer": { "class": "solr.StandardTokenizerFactory" },
"filters": [
{ "class": "solr.LowerCaseFilterFactory" },
{ "class": "solr.GermanNormalizationFilterFactory" },
{ "class": "solr.SnowballPorterFilterFactory", "language": "German2" }
]
}
}
}
Post that with the Schema API’s add-field-type command against /solr/<collection>/schema, the
same endpoint used for any managed-schema change. A handful of languages need more than a tokenizer
and a stemmer: Japanese uses solr.JapaneseTokenizerFactory (Kuromoji, with a mode of normal,
search, or extended) followed by JapaneseBaseFormFilterFactory and
JapanesePartOfSpeechStopFilterFactory; Korean uses solr.KoreanTokenizerFactory (Nori); Chinese
uses solr.HMMChineseTokenizerFactory for simplified-Chinese segmentation, or
solr.CJKBigramFilterFactory as a segmenter-free fallback across Chinese, Japanese and Korean. Two
filters guard stemming from over-eager rewrites regardless of language:
solr.KeywordMarkerFilterFactory (with a protected word list) exempts specific terms from every
downstream stemmer, and solr.KeywordRepeatFilterFactory emits both the original and the stemmed
token at the same position — pair it with solr.RemoveDuplicatesTokenFilterFactory so an unstemmed
exact match and a stemmed fuzzy match can both be found from the same field.
Stemming vs. lemmatization
Stemming strips a word down to a root by rule, without knowing the word’s part of speech or
whether the result is a real word ("organization" → "organ" is a classic over-stem).
Lemmatization instead maps a word to its dictionary base form (a lemma) using either a
dictionary or a trained model that is usually part-of-speech aware, so "better" can correctly
resolve to "good" rather than being left untouched. Solr offers three tiers, in increasing order of
accuracy and cost:
Algorithmic stemming: Snowball
solr.SnowballPorterFilterFactory runs a Snowball/Porter-family algorithm for one language
parameter ("English", "French", "German2", "Spanish", "Russian", …). It is fast and needs
no external dictionary, at the cost of occasional over- or under-stemming.
<!-- Field type definition in the managed schema, applied at both index and query time. -->
<fieldType name="text_es" class="solr.TextField">
<analyzer>
<tokenizer class="solr.StandardTokenizerFactory"/>
<filter class="solr.LowerCaseFilterFactory"/>
<filter class="solr.SnowballPorterFilterFactory" language="Spanish"/>
</analyzer>
</fieldType>
<!-- https://solr.apache.org/guide/solr/latest/indexing-guide/language-analysis.html -->
Hunspell dictionary stemming
solr.HunspellStemFilterFactory stems against a real Hunspell .dic/.aff dictionary pair (the
same format used by many spell-checkers and word processors), which is markedly more accurate than
an algorithmic stemmer for languages with irregular morphology, at the cost of loading the dictionary
into memory and being slower per token. Dictionaries live under the collection’s conf/ directory
alongside the configset:
<filter class="solr.HunspellStemFilterFactory"
dictionary="hunspell/en_US.dic"
affix="hunspell/en_US.aff"
ignoreCase="true"/>
<!-- https://solr.apache.org/guide/solr/latest/indexing-guide/language-analysis.html -->
OpenNLP lemmatization
For true lemmatization, Solr’s OpenNLP integration chains solr.OpenNLPTokenizerFactory with
solr.OpenNLPPOSFilterFactory (part-of-speech tagging, needed because a lemma often depends on
whether a word is a noun or a verb) and solr.OpenNLPLemmatizerFilterFactory, which can run either a
dictionary-based or a statistical-model-based lemmatizer. Each stage needs its own pretrained OpenNLP
model file, referenced by path in the analyzer definition:
<analyzer>
<tokenizer class="solr.OpenNLPTokenizerFactory"
sentenceModel="opennlp/en-sent.bin"
tokenizerModel="opennlp/en-tokenizer.bin"/>
<filter class="solr.OpenNLPPOSFilterFactory" posTaggerModel="opennlp/en-pos-maxent.bin"/>
<filter class="solr.OpenNLPLemmatizerFilterFactory"
dictionary="opennlp/en-lemmatizer.dict"
lemmatizerModel="opennlp/en-lemmatizer.bin"/>
</analyzer>
<!-- https://solr.apache.org/guide/solr/latest/indexing-guide/language-analysis.html -->
Use the Analysis API to compare all three on the same input before committing to one in a schema — this is the closest Solr equivalent to Elasticsearch’s _analyze endpoint:
curl -G "http://localhost:8983/solr/books/analysis/field" \
--data-urlencode "analysis.fieldtype=text_en" \
--data-urlencode "analysis.fieldvalue=The runners were running quickly" \
--data-urlencode "wt=json"
# Returns the token stream produced by each stage of the text_en analyzer chain.
ICU folding and normalization
asciifolding-style accent stripping only works for Latin scripts. solr.ICUFoldingFilterFactory
(shipped in the analysis-extras module) applies Unicode NFKC normalization, case folding, and
accent removal in one pass, and does so correctly across non-Latin scripts as well — solr.ASCIIFoldingFilterFactory folds café to cafe but has no useful behavior for, say, Cyrillic
or Greek input. solr.ICUNormalizer2FilterFactory applies just the Unicode normalization step
without folding case or accents, and solr.ICUTransformFilterFactory runs an arbitrary ICU
transliteration rule (for example Cyrillic-Latin or Any-Latin; NFC) for script-to-script
conversion. For CJK text specifically, solr.CJKWidthFilterFactory normalizes fullwidth and
halfwidth character variants that would otherwise index as distinct tokens.
<filter class="solr.ICUFoldingFilterFactory"/>
<!-- https://solr.apache.org/guide/solr/latest/indexing-guide/language-analysis.html -->
Phonetic matching
Phonetic filters index a word alongside a code representing how it sounds, so misspellings and
transliteration variants of the same name can still match. solr.PhoneticFilterFactory wraps several
general-purpose encoders selected with the encoder parameter: DoubleMetaphone, Metaphone,
Soundex, RefinedSoundex, Caverphone2, ColognePhonetic, and Nysiis. Two more specialized
factories exist for names: solr.BeiderMorseFilterFactory (Beider-Morse Phonetic Matching, tuned
across roughly a dozen languages for personal and place names, with fewer false hits than plain
Soundex) and solr.DaitchMokotoffSoundexFilterFactory (a Soundex refinement especially accurate for
Slavic and Yiddish surnames, which can emit multiple codes per token). See
Phonetic Matching
for the full comparison and each encoder’s trade-offs.
Because phonetic encoding is lossy, it is normally indexed into a separate field via copyField
rather than replacing the exact-text field, so exact and phonetic queries both remain possible:
<fieldType name="phonetic_dm" class="solr.TextField">
<analyzer>
<tokenizer class="solr.StandardTokenizerFactory"/>
<filter class="solr.DoubleMetaphoneFilterFactory" inject="false"/>
</analyzer>
</fieldType>
<field name="name" type="text_general" indexed="true" stored="true"/>
<field name="name_phonetic" type="phonetic_dm" indexed="true" stored="false"/>
<copyField source="name" dest="name_phonetic"/>
<!-- https://solr.apache.org/guide/solr/latest/indexing-guide/phonetic-matching.html -->
# "Smyth" and "Smith" both encode to the same Double Metaphone code and match.
curl -G "http://localhost:8983/solr/books/select" \
--data-urlencode "q=name_phonetic:Smyth"
inject="false" replaces the original token with the phonetic code instead of adding it as a second
token at the same position; leave it at the default true when the phonetic field should also match
exact spellings. See Spell checking, suggestions & similar documents for the complementary,
edit-distance-based approach to fuzzy name and term matching.
Automatic language identification with the langid processor
Rather than requiring the client to tag every document with its language, an update request
processor can detect it during indexing and act on the result. Two implementations ship with Solr:
LangDetectLangIdUpdateProcessorFactory (statistical, based on the language-detection library) and
OpenNLPLangDetectUpdateProcessorFactory (uses a pretrained OpenNLP language-detection model). Both
are configured the same way in solrconfig.xml’s update request processor chain — `langid.fl names
the source field(s) to inspect, and langid.langField is where the detected code is written:
<updateRequestProcessorChain name="langid">
<processor class="solr.LangDetectLangIdUpdateProcessorFactory">
<str name="langid.fl">title,body</str>
<str name="langid.langField">language_s</str>
<str name="langid.fallback">en</str>
<bool name="langid.map.lcmap">true</bool>
</processor>
<processor class="solr.LogUpdateProcessorFactory"/>
<processor class="solr.RunUpdateProcessorFactory"/>
</updateRequestProcessorChain>
<!-- https://solr.apache.org/guide/solr/latest/indexing-guide/language-detection.html -->
Point an update request at that chain with update.chain, and the detected language lands in
language_s on every indexed document:
curl "http://localhost:8983/solr/books/update?update.chain=langid&commit=true" \
-H 'Content-Type: application/json' \
-d '[ { "id": "42", "title": "Le Petit Prince", "body": "Il etait une fois..." } ]'
curl -G "http://localhost:8983/solr/books/select" --data-urlencode "q=language_s:fr"
langid can go a step further and drive the field-per-language strategy directly: with
langid.map=true, langid.map.fl naming the fields to rewrite, and langid.map.pattern /
langid.map.replace giving a rename rule, the processor can rewrite title to title_fr in place
based on the detected language, so the document lands straight in the language-specific field defined
above without any client-side branching. See
Language Detection
for the full parameter reference, including langid.langsField for multivalued detection and
langid.whitelist to constrain the candidate languages.
Where this fits
Once text is broken into the right per-language terms, Query parsers and Query basics & parameters cover searching across one or several of these fields, and Relevance & scoring covers weighting them against each other. For the non-language-specific half of the analysis chain — tokenizers, character filters, and general-purpose token filters — see Text analysis: analyzers, tokenizers & token filters, and for how a field’s type and analyzer are declared in the first place, see Schema & fields and Field types.