-
Notifications
You must be signed in to change notification settings - Fork 0
Text 0.4.0 stemming
Lodestar.Text 0.4.0. This page is frozen at that release. Read the current documentation for what
mainsays now. A link to a decision or a migration page followsmain, and leaves the archive.
ran, running and runs are three spellings of one idea, and an index that stores them as
three terms cannot match a query that uses the fourth. A stemmer cuts each word down to a shared
key so they collide on purpose.
Lodestar.Text.Stemming holds seven stemmers, one static class each, all with the same single
method: a string in, a string out.
flowchart TD
A["What language is the text?"] --> B["English"]
A --> C["French, German, Italian,<br/>Spanish, Portuguese"]
B --> D{"Matching an existing<br/>Porter index?"}
D -->|"no — this is a new index"| E["EnglishSnowballStemmer"]
D -->|"yes"| F["PorterStemmer"]
C --> G["the Snowball stemmer<br/>for that language"]
Language picks the stemmer, and for six of the seven that is the whole decision. A stemmer is
built from one language's suffix rules and has nothing sensible to say about another's:
GermanSnowballStemmer.Stem applied to French returns
something, and that something is noise.
English has two, and they are the same algorithm a generation apart.
PorterStemmer.Stem is Martin Porter's 1980 original;
EnglishSnowballStemmer.Stem is his own later revision,
published as Snowball and universally called Porter2.
Porter2 is the one to reach for. It is Porter's own later revision, so where the two disagree it is the original being corrected. Over the 86 words both are pinned on, they disagree six times, and Porter2 trims less in five of the six:
| word | Porter | Porter2 |
|---|---|---|
ties |
ti |
tie |
fairly |
fairli |
fair |
communism |
commun |
communism |
generalization |
gener |
general |
formative |
form |
format |
homologou |
homolog |
homologou |
generalization is the clearest of them: Porter trims all the way to gener, a fragment short
enough that several unrelated words reach it. Porter2 stops at general, which still names
something.
The reason to choose the original anyway is compatibility, not quality. An index built by Porter has to be queried by Porter, and a corpus already stemmed one way cannot be searched the other. That is the whole of the case for it.
using Lodestar.Text.Stemming;
string old = PorterStemmer.Stem("generalization"); // => gener
string current = EnglishSnowballStemmer.Stem("generalization"); // => generalA stem is a key, not a word. musico, musica, musicos and musicas all stem to music,
and Spanish cantar stems to cant — which is not Spanish. The output is meant to be compared
against other output, never shown to a reader.
This is what separates stemming from lemmatisation, which returns the dictionary form and needs a
dictionary to do it. Nothing here carries one: these are rule engines, small and fast, and they
are wrong on irregular words by construction. Lodestar ships no lemmatiser.
The consequence for a search index is that both sides must be stemmed by the same stemmer — the documents when they are indexed, the query when it arrives. Stem one and not the other and the keys never meet.
-
Input is lowercased first. The algorithms are defined on lowercase, so
Runningandrunninggive the same stem, and the result is always lowercase. -
A null word is refused, with
ArgumentNullException. An empty string is not: it comes back empty. - Each is a static class with no state, so all seven are safe to call from any number of threads at once.
-
Each is checked word for word against nltk, and the corpora are in
tests/oracles. Where a stem looks wrong, it is wrong in the same way the reference is.
| Type | What it is |
|---|---|
EnglishSnowballStemmer |
English Porter2 — the one to use for new English text. |
FrenchSnowballStemmer |
French Snowball. |
GermanSnowballStemmer |
German Snowball. |
ItalianSnowballStemmer |
Italian Snowball. |
PorterStemmer |
English Porter (1980), for compatibility with an existing index. |
PortugueseSnowballStemmer |
Portuguese Snowball. |
SpanishSnowballStemmer |
Spanish Snowball. |
- Python → C# equivalence — the nltk call each of these replaces.
- From string to vector — where a stemmer sits in a pipeline.
-
decisions/0008— the one place a stemmer here follows nltk over the published algorithm.
- 0001-target-framework
- 0002-unicode-comparison-unit
- 0003-provenance-and-licensing
- 0004-levenshtein-myers-backlog
- 0005-hamming-jellyfish-divergence
- 0006-ratcliff-autojunk
- 0007-metaphone-scope
- 0008-italian-enza-nltk-divergence
- 0009-sample-consumes-a-local-feed
- 0010-stop-word-list-provenance
- 0011-persistence-format
- 0012-per-package-versioning
- 0013-sentencepiece-parity-scope
- 0014-precompiled-normalizer
- 0015-sonar-rules-in-the-build
- 0016-metrics-package-placement
- 0017-bpe-parity-scope
- 0018-multiclass-roc-auc-parallelism-is-opt-in
- 0019-the-net-analysers-run-in-the-build-too
- 0020-normalize-is-a-projection-not-a-parameter
- 0021-multioutput-is-a-method-not-an-enum
- 0022-added-token-matching-flags
- 0023-byte-level-decode-substitutes
- 0024-weighted-median-averages-within-scikit-learns-epsilon
- 0025-quickselect-replaces-a-full-sort-for-the-median
- 0026-r2-and-explainedvariance-split-their-undefined-cases-differently
- 0027-r2-and-explainedvariance-vectorize-only-a-single-output
- 0028-log1p-is-kahans-identity-not-math-log-1-plus-x
- 0029-balanced-accuracy-adjusted-is-left-to-ieee-754-at-the-edge
- 0030-cohen-kappa-keeps-scikit-learns-expected-matrix-orientation
- 0031-nosamplecorrect-mirrors-numpys-float64-upcast
- 0032-fbeta-substitutes-tp-predicted-and-support-algebraically
- 0033-compensated-sum-is-neumaiers-variant
- 0034-dropout-is-refused-for-want-of-a-user
- 0035-a-null-pre-split-is-removed-with-invert-not-isolated
- 0036-a-member-may-ship-without-an-oracle-if-it-says-so
- 0037-the-guards-run-before-the-commit
- 0038-the-gate-confronts-an-exception-tag-with-the-page-that-documents-it
- 0039-mutual-information-returns-zero-on-an-empty-input
- 0040-a-curve-is-a-sealed-class-per-curve
- 0041-one-sample-file-per-public-class
- 0042-phonetic-encoders-refuse-a-null-word
- 0043-the-equality-table-is-sized-to-the-pattern
- 0044-compression-belongs-to-the-caller
- 0045-a-console-call-carries-its-reason-on-the-line
- 0046-check-adr-immutable-runs-in-ci-only
- 0047-one-gate-per-kernel-not-one-per-alphabet
- 0048-the-gate-depends-on-the-kernel-and-the-alphabet
- 0049-two-gates-per-kernel-tested-where-the-width-is-known
- 0050-the-sentencepiece-bpe-lineage-stays-a-bpe-model
- benchmark_latest
- decisions
- equivalence
- matplotlib
- migration
- nightly_run
- numpy
- pandas
- performance
- pytorch
- seaborn
- sklearn
- statsmodels