Natural Language Processing

The smile-nlp module covers the classical text-processing pipeline: Unicode normalization, paragraph / sentence / word splitting, stemming, part-of-speech tagging, dictionaries, collocation extraction, relevance ranking, word embeddings, and lightweight concept taxonomies.

This guide is for engineered NLP features and retrieval — not large language models. For LLM serving and generation APIs, see Large Language Models. Trainable sequence labelers (HMM / CRF) live on Sequence Labeling; classic bag-of-words classifiers are on Classification.

Module Contents

Sub-package / ClassDescription
smile.nlp (core)Text, Corpus, SimpleCorpus, Bigram, NGram, Word2Vec
smile.util.StringsUnicode text normalization (Strings.normalize)
smile.nlp.tokenizerParagraph, sentence, and word splitting
smile.nlp.stemmerPorter and Lancaster word stemmers
smile.nlp.posPenn Treebank POS tagging (pretrained HMM)
smile.nlp.dictionaryStop words, punctuations, English dictionary
smile.nlp.relevanceTF-IDF and BM25 relevance ranking
smile.nlp.taxonomyConcept taxonomy and taxonomic distance

Quick Start

A typical preprocessing path is normalize → paragraphs → sentences → tokens → POS tags → stems. Filter on PennTreebankPOS.open when you only want content words. Use one PorterStemmer per thread; the default HMM tagger is safe to share.

import smile.util.Strings;
import smile.nlp.tokenizer.*;
import smile.nlp.stemmer.PorterStemmer;
import smile.nlp.pos.*;

String raw = "Machine learning is a subfield of AI. Deep learning uses neural nets.";
String text = Strings.normalize(raw);

var paragraphs = SimpleParagraphSplitter.getInstance();
var sentences = SimpleSentenceSplitter.getInstance();
var tokenizer = new SimpleTokenizer();
var tagger = HMMPOSTagger.getDefault();
var stemmer = new PorterStemmer();

for (String para : paragraphs.split(text)) {
    for (String sent : sentences.split(para)) {
        String[] tokens = tokenizer.split(sent);
        PennTreebankPOS[] tags = tagger.tag(tokens);
        for (int i = 0; i < tokens.length; i++) {
            if (tags[i].open) {
                System.out.printf("%-16s %-6s %s%n",
                    tokens[i], tags[i], stemmer.stem(tokens[i].toLowerCase()));
            }
        }
    }
}

Machine          NN     machin
learning         NN     learn
...
      

Core Types

The root smile.nlp package holds the document model, collocation records, an in-memory corpus, and word-embedding loaders.

Text

Text is a lightweight immutable record with optional title and body. Factory methods are Text.of(content) and Text.of(title, content).

import smile.nlp.Text;

Text doc = Text.of("Machine learning is a subfield of AI.");
Text titled = Text.of("Overview", "Machine learning is a subfield of AI.");
System.out.println(doc.content());
System.out.println(titled.title());

Machine learning is a subfield of AI.
Overview
      

Bigram and NGram

Bigram and NGram are records for word pairs and n-word phrases, optionally carrying raw frequency and association scores. They are the return types of collocation extractors.

Java
import smile.nlp.Bigram;
import smile.nlp.NGram;

Bigram b = new Bigram("machine", "learning");
NGram n = new NGram(new String[]{"deep", "learning"});
System.out.println(b);
System.out.println(n);

Bigram{w1='machine', w2='learning', ...}
NGram{words=[deep, learning], ...}
      

SimpleCorpus

Corpus is the indexed-document interface. SimpleCorpus is the built-in in-memory implementation: it tokenizes on insert and keeps term / bigram tables for collocations and relevance search. Scala and Kotlin helpers build a corpus from a sequence of strings.

import smile.nlp.SimpleCorpus;

SimpleCorpus corpus = new SimpleCorpus();
corpus.add(corpus.doc("SMILE is a fast machine learning library."));
corpus.add(corpus.doc("Deep learning uses neural networks."));
System.out.println(corpus.docCount());
System.out.println(corpus.count("learning"));

2
2
      

Word2Vec

Word2Vec loads pretrained embeddings in Word2vec binary format (Word2Vec.of) or GloVe text format (Word2Vec.glove). Look up vectors with apply or lookup, and compare tokens with cosine similarity.

Java
import java.nio.file.Path;
import smile.nlp.Word2Vec;

// Word2vec binary or GloVe text
Word2Vec w2v = Word2Vec.of(Path.of("GoogleNews-vectors-negative300.bin"));
// Word2Vec glove = Word2Vec.glove(Path.of("glove.6B.100d.txt"));

float[] vec = w2v.apply("king");
w2v.lookup("king").ifPresent(v -> System.out.println(v.length));
w2v.similarity("king", "queen").ifPresent(System.out::println);
System.out.println(w2v.size() + " x " + w2v.dimension());

300
≈ 0.65
(vocabulary size) x 300
      

Normalizer

Strings.normalize cleans heterogeneous Unicode before tokenization. Always normalize text from the web, PDFs, or user input to avoid spurious tokens from encoding variants.

StepDetail
NFKCCanonical decomposition followed by composition
WhitespaceStrip, trim, and compress whitespace
Control charsRemove Cc and Cf categories
DashesEm-dash, en-dash, etc. → -
Double quotesTypographic quotes → "
Single quotesTypographic apostrophes → '
import smile.util.Strings;

String clean = Strings.normalize("\u201CHello,\u201D   world\u2019s\u2026");
System.out.println(clean);

"Hello," world's...
      

Tokenizers

The tokenizer package splits text at three levels. All splitter interfaces extend Function<String, String[]>, so they compose in streams. Prefer the Simple* and PennTreebank singletons for English; BreakIterator* variants are locale-aware but not thread-safe.

ClassLevelNotes
SimpleParagraphSplitterParagraphBlank lines; singleton
SimpleSentenceSplitterSentenceEnglish heuristics + abbreviations; singleton
BreakIteratorSentenceSplitterSentenceLocale-aware; not thread-safe
SimpleTokenizerWordExpands contractions; thread-safe
PennTreebankTokenizerWordPenn Treebank conventions; singleton
BreakIteratorTokenizerWordLocale-aware; not thread-safe

Scala and Kotlin enrich strings with sentences and words(filter). The filter may be none, default, comprehensive, google, mysql, or a comma-separated custom stop list.

import smile.nlp.tokenizer.*;

String text = "Dr. Smith lives in the U.S. He works at NASA.";
String[] sents = SimpleSentenceSplitter.getInstance().split(text);
Tokenizer tok = new SimpleTokenizer();
for (String s : sents) {
    System.out.println(String.join(" | ", tok.split(s)));
}

Dr. | Smith | lives | in | the | U.S. | .
He | works | at | NASA | .
      

Choosing an Implementation

Word tokenizer scenarioRecommended
General English textSimpleTokenizer
Penn Treebank / pretrained NLP modelsPennTreebankTokenizer
Non-English or multilingualBreakIteratorTokenizer
Sentence splitter scenarioRecommended
English text (production)SimpleSentenceSplitter
Multilingual / locale-sensitiveBreakIteratorSentenceSplitter
Paragraph splitter scenarioRecommended
Blank-line paragraph boundariesSimpleParagraphSplitter

Thread Safety

ClassThread-safe?Notes
SimpleTokenizerYesStateless after construction
PennTreebankTokenizerYesStateless singleton
BreakIteratorTokenizerNoUse one instance per thread (ThreadLocal)
SimpleSentenceSplitterYesStateless singleton
BreakIteratorSentenceSplitterNoUse one instance per thread
SimpleParagraphSplitterYesStateless singleton

Penn Treebank vs SimpleTokenizer

Package-private EnglishAbbreviations backs the English splitters: SimpleSentenceSplitter and both word tokenizers consult it so titles and Latin abbreviations (Dr., etc., U.S., …) are not treated as sentence or token boundaries. Prefer PennTreebankTokenizer.getInstance() when feeding HMMPOSTagger (trained on Treebank conventions); SimpleTokenizer is the general-purpose default.

import java.util.Arrays;
import smile.nlp.tokenizer.*;

String s = "They couldn't have known.";
Tokenizer simple = new SimpleTokenizer();
Tokenizer ptb = PennTreebankTokenizer.getInstance();
System.out.println(Arrays.toString(simple.split(s)));
System.out.println(Arrays.toString(ptb.split(s)));

[They, couldn't, have, known, .]
[They, could, n't, have, known, .]
      

Stemmers

Both stemmers implement Stemmer extends Function<String,String> for stream use. Porter is moderate and not thread-safe (one instance per thread). Lancaster is aggressive and shareable; pass true to also strip prefixes.

ClassAlgorithmAggressivenessThread-safe?
PorterStemmerPorter 1980ModerateNo (per-thread)
LancasterStemmerPaice/Husk 1990HighYes
import smile.nlp.stemmer.*;

PorterStemmer porter = new PorterStemmer();
LancasterStemmer lancaster = new LancasterStemmer();
System.out.println(porter.stem("generalization"));
System.out.println(lancaster.stem("presumably"));

gener
presum
      

Porter vs Lancaster

CriterionPorterLancaster
Stem readabilityOften recognizableOften fragments
Conflation strengthModerateHigh
Typical useIR, topic models, searchAggressive dedup / small vocabularies
Thread safetyOne instance per threadSafe to share after construction

Start with PorterStemmer. Switch to LancasterStemmer when vocabulary size is the bottleneck. Porter does not lower-case input; Lancaster lower-cases and strips non-letters automatically.

stripPluralParticiple

PorterStemmer.stripPluralParticiple applies only the plural / participle step (Porter step 1a/1b-style), useful when you want light normalization without full stemming. Short words such as was, has, his, and this are preserved.

import smile.nlp.stemmer.PorterStemmer;

PorterStemmer porter = new PorterStemmer();
System.out.println(porter.stripPluralParticiple("cats"));
System.out.println(porter.stripPluralParticiple("running"));
System.out.println(porter.stripPluralParticiple("was"));
System.out.println(porter.stem("generalization"));

cat
run
was
gener
      

Lancaster Prefix Stripping

Pass true to strip scientific prefixes (kilo, micro, milli, intra, ultra, mega, nano, pico, pseudo). You can also supply a custom Lancaster rule file via new LancasterStemmer(InputStream) or new LancasterStemmer(InputStream, boolean).

import smile.nlp.stemmer.LancasterStemmer;

LancasterStemmer lancaster = new LancasterStemmer(true);
System.out.println(lancaster.stem("microprocessor"));
System.out.println(lancaster.stem("kilowatt"));
System.out.println(lancaster.stem("pseudoscience"));

process
wat
scy
      

Part-of-Speech Tagging

HMMPOSTagger.getDefault() loads a pretrained first-order HMM over the Penn Treebank tag set. PennTreebankPOS.open marks content (open-class) tags. Package-private RegexPOSTagger is the OOV pre-classifier used inside the HMM for numbers, URLs, and e-mails. To train general HMM / CRF sequence models, see Sequence Labeling.

ClassRole
PennTreebankPOS45-tag enum; open distinguishes content vs function words
HMMPOSTaggerPretrained HMM tagger; serializable
RegexPOSTaggerPackage-private OOV rules (numbers, URLs, e-mails)
EnglishPOSLexicon~200k word lexicon (Moby + WordNet)
import smile.nlp.pos.*;

HMMPOSTagger tagger = HMMPOSTagger.getDefault();
String[] tokens = {"The", "cat", "sat", "on", "the", "mat", "."};
PennTreebankPOS[] tags = tagger.tag(tokens);
for (int i = 0; i < tokens.length; i++) {
    System.out.printf("%-12s %s%n", tokens[i], tags[i]);
}

The          DT
cat          NN
sat          VBD
on           IN
the          DT
mat          NN
.            .
      

Open vs Closed Class

Open-class tags (nouns, verbs, adjectives, adverbs, …) have open == true and are the usual candidates for bag-of-words and keyword features. Closed-class tags (determiners, prepositions, pronouns, …) have open == false. PennTreebankPOS.getValue(String) parses corpus tag strings, including punctuation symbols mapped to named constants (e.g. "." → SENT).

Java
import smile.nlp.pos.PennTreebankPOS;

PennTreebankPOS nn = PennTreebankPOS.getValue("NN");
PennTreebankPOS dot = PennTreebankPOS.getValue(".");
System.out.println(nn + " open=" + nn.open);
System.out.println(dot + " open=" + dot.open);
System.out.println(PennTreebankPOS.DT.open);

NN open=true
. open=false
false
      

RegexPOSTagger (internal OOV rules)

RegexPOSTagger is package-private and used by HMMPOSTagger for out-of-vocabulary tokens. Its tag(String) returns an Optional<PennTreebankPOS>: cardinal / comma-formatted numbers → CD; phone numbers, URLs, and e-mails → NN; empty when no pattern matches.

EnglishPOSLexicon

EnglishPOSLexicon.get(word) looks up ~200k English words (Moby + WordNet) and returns an Optional of candidate Penn tags in priority order.

Java
import java.util.Arrays;
import smile.nlp.pos.EnglishPOSLexicon;

EnglishPOSLexicon.get("run").ifPresent(tags -> {
    System.out.println("primary=" + tags[0]);
    System.out.println(Arrays.toString(tags));
});
System.out.println(EnglishPOSLexicon.get("xyzzy").isEmpty());

primary=VB
[VB, VB, VB, NN]
true
      

Custom HMM POS Model

Train a domain tagger with HMMPOSTagger.fit on aligned token / tag arrays. On the bundled WSJ Treebank data, 10-fold CV error is about 5 %. Package-private HMMPOSTagger.read loads *.POS files (word/TAG lines) when you train from inside the module; otherwise build the arrays yourself. For general HMM / CRF training APIs, see Sequence Labeling.

Java
import smile.nlp.pos.*;

// sentences: String[][] tokens; labels: PennTreebankPOS[][]
// HMMPOSTagger.read(dir, sentences, labels); // package-private helper
String[][] x = {
    {"The", "cat", "sat", "."},
    {"Dogs", "bark", "."}
};
PennTreebankPOS[][] y = {
    {PennTreebankPOS.DT, PennTreebankPOS.NN, PennTreebankPOS.VBD, PennTreebankPOS.SENT},
    {PennTreebankPOS.NNS, PennTreebankPOS.VBP, PennTreebankPOS.SENT}
};
HMMPOSTagger custom = HMMPOSTagger.fit(x, y);
System.out.println(java.util.Arrays.toString(custom.tag(x[0])));

[DT, NN, VBD, .]
      

Dictionaries

Ready-made English word lists live in smile.nlp.dictionary. Use them to filter tokens before bag-of-words or keyword extraction.

EnglishStopWords

ConstantCoverage
EnglishStopWords.DEFAULTStandard list (~500 words)
EnglishStopWords.COMPREHENSIVEExtended list (thousands)
EnglishStopWords.GOOGLEGoogle stop-word list
EnglishStopWords.MYSQLMySQL FullText stop-word list
import smile.nlp.dictionary.EnglishStopWords;

EnglishStopWords stop = EnglishStopWords.DEFAULT;
System.out.println(stop.contains("the"));
System.out.println(stop.contains("machine"));

true
false
      

EnglishPunctuations

import smile.nlp.dictionary.EnglishPunctuations;

EnglishPunctuations punct = EnglishPunctuations.getInstance();
System.out.println(punct.contains("."));
System.out.println(punct.contains("NLP"));

true
false
      

EnglishDictionary

EnglishDictionary is an enum with a membership test over a loaded word list. Use the CONCISE constant — there is no static EnglishDictionary.contains.

import smile.nlp.dictionary.EnglishDictionary;

System.out.println(EnglishDictionary.CONCISE.contains("serendipity"));
System.out.println(EnglishDictionary.CONCISE.contains("xyzzy"));

true
false
      

Collocations

Collocations are statistically significant word co-occurrences. SimpleCorpus exposes two bigram overloads ranked by log-likelihood ratio (−2 log λ, χ² with 1 df): bigrams(k, minFreq) returns the top-k pairs, and bigrams(p, minFreq) returns all pairs whose p-value is below p (0 < p < 1). minFrequency is exclusive (count > minFrequency). NGram.apriori mines frequent phrases of length up to maxN from already-tokenized sentences.

import smile.nlp.SimpleCorpus;

SimpleCorpus corpus = new SimpleCorpus();
corpus.add(corpus.doc("Machine learning is part of artificial intelligence."));
corpus.add(corpus.doc("Deep learning is a type of machine learning."));
corpus.add(corpus.doc("Neural networks power deep learning systems."));

System.out.println("--- top-k ---");
corpus.bigrams(5, 1).forEach(b ->
    System.out.println(b.w1() + " " + b.w2() + "  score=" + b.score()));

System.out.println("--- p-value ---");
corpus.bigrams(0.01, 1).forEach(b ->
    System.out.println(b.w1() + " " + b.w2() + "  score=" + b.score()));

--- top-k ---
machine learning  score=...
--- p-value ---
...
      

Parameter Tuning

ParameterGuidance
minFrequencyExclusive lower bound. Tiny corpora: 1–2; 1k–10k tokens: 2–5; 10k–100k: 5–10; larger: 10–50
k (top-k)Fixed-size feature / display lists; start around 20
p0.05 lenient; 0.01 / 0.001 stricter; 0.0001 very strict

For n-grams longer than two, tokenize sentences first, then call NGram.apriori(sentences, maxN, minFreq). Index result[n] holds the n-grams (sorted by descending count). Scala / Kotlin ngram helpers accept raw text and handle sentence splitting and stemming internally.

import java.util.List;
import smile.nlp.NGram;
import smile.nlp.tokenizer.*;

String[] docs = {
    "Machine learning is part of artificial intelligence.",
    "Deep learning is a type of machine learning."
};
var splitter = SimpleSentenceSplitter.getInstance();
var tokenizer = new SimpleTokenizer();
List<String[]> sentences = java.util.Arrays.stream(docs)
    .flatMap(d -> java.util.Arrays.stream(splitter.split(d)))
    .map(tokenizer::split)
    .toList();

NGram[][] result = NGram.apriori(sentences, 3, 2);
for (NGram g : result[2]) {
    System.out.println(String.join(" ", g.words()) + "  count=" + g.count());
}

machin learn  count=...
      

Keywords

Text.keywords(maxNum) extracts top phrases from a single document via word co-occurrence statistics (Matsuo & Ishizuka). Scala and Kotlin expose the same algorithm as string extensions.

import smile.nlp.Text;

String body = "Machine learning is a field of artificial intelligence. "
    + "Deep learning is a subfield of machine learning that uses neural networks. "
    + "Neural networks learn hierarchical representations from data.";
Text.of(body).keywords(5).forEach(k ->
    System.out.println(String.join(" ", k.words()) + "  count=" + k.count()));

machin learn  count=...
deep learn  count=...
      

Relevance Ranking

Plug a RelevanceRanker into Corpus.search to rank documents for a query string or token array. TFIDF is classic weighted bag-of-words; BM25 is Okapi BM25+ for ad-hoc retrieval. Default new BM25() uses k1=1.2, b=0.75, delta=1.0; tune with new BM25(k1, b, delta) (set delta=0 for classic BM25 without the BM25+ floor).

CriterionTF-IDFBM25
SimplicityStraightforward formulaSlightly more parameters
Long documentsBias toward long docsLength-normalized by b
TF saturationNone (linear in tf)Logarithmic via k1
TunableOne parameter (a)Three (k1, b, delta)
Recommended forPrototypes, small corporaProduction search, larger corpora
import smile.nlp.Text;
import smile.nlp.SimpleCorpus;
import smile.nlp.relevance.*;

SimpleCorpus corpus = new SimpleCorpus();
corpus.add(corpus.doc(Text.of("Intro", "SMILE is a fast machine learning library.")));
corpus.add(corpus.doc(Text.of("DL", "Deep learning uses neural networks.")));
corpus.add(corpus.doc(Text.of("IR", "BM25 ranks documents for information retrieval.")));

RelevanceRanker bm25 = new BM25();
corpus.search(bm25, new String[]{"machine", "learning"})
    .forEachRemaining(r ->
        System.out.printf("%.4f  %s%n", r.score(), r.text().title()));

0.xxxx  Intro
...
      

TF-IDF Side by Side

Same corpus, same query — swap the ranker to compare scores. Start with new BM25() for production; use TFIDF when you want a simpler baseline.

Java
import smile.nlp.Text;
import smile.nlp.SimpleCorpus;
import smile.nlp.relevance.*;

SimpleCorpus corpus = new SimpleCorpus();
corpus.add(corpus.doc(Text.of("Intro", "SMILE is a fast machine learning library.")));
corpus.add(corpus.doc(Text.of("DL", "Deep learning uses neural networks.")));
corpus.add(corpus.doc(Text.of("IR", "BM25 ranks documents for information retrieval.")));

String[] q = {"machine", "learning"};
System.out.println("BM25:");
corpus.search(new BM25(), q).forEachRemaining(r ->
    System.out.printf("  %.4f  %s%n", r.score(), r.text().title()));
System.out.println("TF-IDF:");
corpus.search(new TFIDF(), q).forEachRemaining(r ->
    System.out.printf("  %.4f  %s%n", r.score(), r.text().title()));

BM25:
  0.xxxx  Intro
  ...
TF-IDF:
  0.xxxx  Intro
  ...
      

Taxonomy

A Taxonomy is an ordered IS-A tree of Concept nodes. Each concept carries a synonym set via keywords(). TaxonomicDistance provides edge-counting distance d(), normalized distance, and three similarity measures (Wu–Palmer, Leacock–Chodorow, Lin). d() counts edges through the LCA — it is not Wu–Palmer; call wuPalmer separately for that similarity.

Parsing with Taxonomy.of

Taxonomy.of(String) builds a tree from indented text: 4 spaces (or one tab) per level, comma-separated synonyms on a line, # comments allowed. The first non-blank line is the root.

Java
import smile.nlp.taxonomy.*;

Taxonomy taxonomy = Taxonomy.of("""
    animal
        mammal, warm-blooded
            dog, canine
            cat, feline
        reptile
            snake
    """);
System.out.println(taxonomy);
System.out.println(taxonomy.getConcept("dog") == taxonomy.getConcept("canine"));
System.out.println(taxonomy.getConcept("dog").keywords());

[animal]
├── [mammal, warm-blooded]
│   ├── [canine, dog]
│   └── [cat, feline]
└── [reptile]
    └── [snake]
true
[canine, dog]
      

Depth, LCA, and Ancestors

Java
import smile.nlp.taxonomy.*;

Taxonomy t = Taxonomy.of("""
    animal
        mammal, warm-blooded
            dog, canine
            cat, feline
        reptile
            snake
    """);
System.out.println("depth(dog)=" + t.depth("dog"));
System.out.println("LCA(dog,snake)=" + t.lowestCommonAncestor("dog", "snake"));
System.out.println("isAncestor(mammal,dog)=" + t.isAncestor("mammal", "dog"));
System.out.println("height=" + t.height() + " size=" + t.size());

depth(dog)=2
LCA(dog,snake)=Concept [animal]
isAncestor(mammal,dog)=true
height=2 size=9
      

TaxonomicDistance Measures

MethodKindNotes
dDistanceEdge count via LCA: depth(a)+depth(b)−2·depth(LCA)
normalizedDistanceDistanceRaw distance / diameter ∈ [0, 1]
wuPalmerSimilarity2·depth(LCA)/(depth(a)+depth(b))
leacockChodorowSimilarityPath length vs taxonomy height, normalized to [0, 1]
linSimilarityDepth-proxy information content; no external corpus
Java
import smile.nlp.taxonomy.*;

Taxonomy t = Taxonomy.of("""
    animal
        mammal, warm-blooded
            dog, canine
            cat, feline
        reptile
            snake
    """);
TaxonomicDistance td = new TaxonomicDistance(t);
System.out.printf("d(dog,cat)=%.1f%n", td.d("dog", "cat"));
System.out.printf("normalized=%.3f%n", td.normalizedDistance("dog", "cat"));
System.out.printf("wuPalmer=%.3f%n", td.wuPalmer("dog", "cat"));
System.out.printf("leacockChodorow=%.3f%n", td.leacockChodorow("dog", "cat"));
System.out.printf("wuPalmer(dog,snake)=%.3f%n", td.wuPalmer("dog", "snake"));
System.out.printf("lin(dog,cat)=%.3f%n", td.lin("dog", "cat"));

d(dog,cat)=2.0
normalized=0.500
wuPalmer=0.500
leacockChodorow=0.500
wuPalmer(dog,snake)=0.000
lin(dog,cat)=0.000
      

Package Map

smile.nlp
├── Text, Corpus, SimpleCorpus          – document model & in-memory corpus
├── Bigram, NGram                        – collocation data records
├── Word2Vec                             – word embeddings (Word2vec / GloVe)
│
├── tokenizer/    Paragraph / Sentence / Word splitters
├── stemmer/      PorterStemmer, LancasterStemmer
├── pos/          HMMPOSTagger, PennTreebankPOS, RegexPOSTagger
├── dictionary/   EnglishStopWords, EnglishPunctuations, EnglishDictionary
├── relevance/    TFIDF, BM25
└── taxonomy/     Concept, Taxonomy, TaxonomicDistance