Rishe (ریشه)
Fast Persian NLP and morphology, built in Rust.
Rishe (root in Persian) turns raw Persian text into clean, structured data in one call: normalized text, tokens, lemmas, sentences, ezafe markers, and a formal rewrite of colloquial writing. It also ships the morphology utilities that usually live in a separate library: stemming, verb conjugation, inflection, token splitting, spell correction, and stopword removal.
The core is Rust. You get it as a pip install with prebuilt wheels, no model
downloads, no runtime dependencies, and the same engine as a Rust crate.
=
=
# ['آن', 'دارد', 'تو', 'خانه', 'کتاب', 'میخواند', '،', 'چیست', '؟']
# ['آن', 'داشت', 'تو', 'خانه', 'کتاب', 'خواند', '،', 'چیست', '؟']
# 'آن دارد تو خانه کتاب میخواند، چیست؟'
Why Rishe
- Normalization about 80× faster than Hazm and Shekar: 1.2 µs per sentence against 96 µs and 104 µs.
- Best tokenization and lemmatization in this comparison. Token F1 of 1.0000 on UD-Seraji, 0.9989 on UD-PerDT and 0.9028 on PEYMA; lemma accuracy of 0.9209 and 0.9316, ahead of Hazm (0.9048 / 0.9009) and Shekar (0.81 / 0.82).
- Morphology in the same core. Stemming, conjugation, inflection, token splitting, spell correction and stopwords — no second library, no extra runtime. On the UD lemma proxy, stemming is 89.2% / 89.5% exact against Hazm (66.4% / 64.4%) and Shekar (76.1% / 72.0%).
- Spell correction that is fast enough to stay in the loop. 80.8% top-1 and 96.2% top-5 on synthetic 1-edit typos, at 0.16 ms per word; Shekar's statistical checker is 60.8% / 77.6% at 94 ms per word on the same set.
- Embeddings without a Python runtime. A 2.3 GB fastText
.vecloads in 5.9 s (Hazm/gensim: 176 s) with bit-identical vectors; sentence pooling is 2.5× faster, nearest neighbours match Hazm's top-10, and batch calls release the GIL. - Millions of tokens per second. 1.0M tokens/s on one thread;
pipe()runs batches in parallel with the GIL released and reaches 6.5M tokens/s on 12 threads. - Everything in one call. Normalization, informal → formal conversion,
tokenization, lemmatization, sentence splitting and ezafe detection from one
Pipelineobject, in about 16 µs per sentence. - Informal Persian handled. Colloquial forms (
اون,خونه,داره,میخوام,چیه) are rewritten to their formal spelling, and fused clitics such asبهشandازشare split intoبه+اوandاز+او. - Nothing to set up. Compiled-in lexicons (190k nouns, 1.2k verb stems, word frequencies, stopwords), no model downloads, no Python dependencies, no network access.
- Wheels for every major platform. Linux (glibc and musl, x86_64 and aarch64), macOS (Intel and Apple Silicon) and Windows, for Python 3.9+ (abi3). The same core is available as a Rust crate.
- Thread-safe. One
Pipelinecan be shared across threads.
Benchmarks
Measured against Hazm and Shekar on the full UD Persian-Seraji, UD Persian-PerDT and PEYMA test sets. Every library is built once and reused.
Core speed (per sentence, single thread)
| Rishe | Hazm | Shekar | |
|---|---|---|---|
| Normalize | 1.2 µs | 96 µs | 104 µs |
| Tokenize | 4.1 µs | 5.7 µs | 4.4 µs |
| Full pipeline | 16.3 µs | — | — |
Core accuracy
| Task | Dataset | Rishe | Compared with |
|---|---|---|---|
| Token F1 | UD-Seraji | 1.0000 | Shekar 0.9992, Hazm 0.9810 |
| Token F1 | UD-PerDT | 0.9989 | Shekar 0.9976, Hazm 0.9609 |
| Token F1 | PEYMA | 0.9028 | Shekar 0.9018, Hazm 0.8901 |
| Lemma accuracy | UD-Seraji | 0.9209 | Hazm 0.9048, Shekar 0.8058 |
| Lemma accuracy | UD-PerDT | 0.9316 | Hazm 0.9009, Shekar 0.8170 |
New feature highlights
| Feature | Metric | Rishe | Hazm | Shekar |
|---|---|---|---|---|
| Stemming | exact UD-lemma proxy (Seraji / PerDT) | 89.2% / 89.5% | 66.4% / 64.4% | 76.1% / 72.0% |
| Conjugation | hand-written spot-check (29 forms) | 29/29 | 28/29 | 29/29 |
| Conjugation | ms/verb | 0.010 | 0.152 | 0.080 |
| Inflection | comparative / superlative / plural | 100% | N/A | 100% |
| Token splitting | ms/token | 0.0006 | 12.05 | N/A |
| Spell correction | top-1 / top-5 on 1-edit typos | 80.8% / 96.2% | N/A | 60.8% / 77.6% |
| Stopwords | list size | 389 | 389 | 301 |
Static embeddings (same 2.3 GB fastText .vec: 884,790 words × 300 dims;
Hazm loads it through gensim 4.4.0)
| Rishe | Hazm/gensim | |
|---|---|---|
| Load | 5.9 s | 176 s |
| Word vectors | identical (max component Δ 0.0) | — |
| Sentence embedding, mean pooling (UD-Seraji) | 0.015 ms | 0.037 ms |
| Word → Python list | 5.9 µs | 5.1 µs |
| Top-10 nearest neighbours | 100% overlap | — |
Rishe loads the same model 30× faster and pools sentences 2.5× faster, with bit-identical word vectors. Word lookup is at parity: both paths build a Python list, which dominates the cost.
The normalizer operators are 1.5–6× faster than Shekar's per operator (the
exception is ya, 0.7×) with exact output agreement of 100% on 8 of 11
operators and 99.04% on verbal spacing. normalize_stages is opt-in; the default
normalize path is unchanged.
The full core tables, batch scaling and method are in docs/BENCHMARKS.md. The feature tables, proxy metrics and caveats are in docs/FEATURE_BENCHMARKS.md.
Reproduce:
Install
or, from Rust (Cargo.toml): rishe = "0.4".
The base install has no Python dependencies and no model downloads. Optional models are pulled explicitly and nothing touches the network at runtime.
Usage
= # unify digits, strip diacritics, fix ZWNJ
=
, ,
, ,
= # parallel batch -> list[ProcessedDoc]
# 'میکنم ۲۰۲۶'
# raw tokenization, no normalization
# normalize + tokenize
# sentence splitting only
# 'رفت' (past-stem lemmas, UD style)
# ('رفت', 'رو') (past + present stem)
pipe(text) also rewrites colloquial words to formal ones, so its tokens can
differ from the surface text; word_tokenize tokenizes without rewriting.
tokenize is the fast raw path used for the tokenization benchmarks.
Morphology and utilities
# 'کتاب'
# ['میروم', 'میروی', 'میرود']
# 'بزرگتر'
# 'بزرگترین'
# 'کتابها'
# 'خانهام'
# ['به', 'او']
# ['سلام', 'اسلام', ...]
# 'سلام بر شما'
# [('سلام', 0, 4), ('دنیا', 5, 9)] (char offsets)
# drop Persian stopwords
# choose pipeline stages
Normalizer operators
normalize_stages applies the P1 normalizer operators in the order you list them:
alphabet, arabic_unicode, ya, ya_joda, digits, punctuation,
repeated_letters, orthographic_spacing, punctuation_spacing, word_spacing,
verbal_spacing, and spacing (all four spacing operators).
# 'بسم الله الرحمن الرحیم ۱۲۳۴۵۶'
# 'سلام (دنیا)! میروم'
word_spacing and verbal_spacing validate against the compiled lexicons, so they
only join forms the engine knows (کتاب ها → کتابها, رفته ام → رفتهام).
Unknown stage names raise ValueError.
Masking
mask_stages removes or masks URLs, emails, mentions, hashtags, emoji, HTML
tags, non-Persian letters, stopwords, offensive words, digits, diacritics and
punctuation. Stage names are url, email, mention, hashtag, emoji,
html, non_persian, stopwords, offensive, digits, diacritics and
punctuation.
# 'سلام به <URL> یا <EMAIL> بنویس.'
# '[MASK] کتاب [MASK]'
Defaults are <URL> for URLs, <EMAIL> for emails, a space for mentions,
hashtags and HTML tags, and removal for everything else. Pass mask_token to
override all stages. Masking keeps the spaces left behind; run
normalize_stages(..., ["spacing"]) afterwards to collapse them. Unknown stage
names raise ValueError.
Spell correction uses a 190k-word frequency lexicon and edit-distance 1; typos with two or more edits are left unchanged.
Utilities
# 'یک هزار و چهارصد و چهار'
# 'سه ممیز چهارده'
# ['این', 'یک', 'متن', 'این یک', 'یک متن']
# 'پرونده را با رایانه باز کن'
# [('فایل', ['پرونده'], 0, 4)]
# 'الف', 'ب', 'پ'
persianize uses a built-in curated loanword map; persianize_suggest returns
the alternatives and character spans.
Augmentation
keyboard_noise, ocr_noise and whitespace_noise simulate typing, OCR and
spacing errors. Pass seed= for reproducible output.
Keywords and transliteration
# 'хона'
# 'خانه'
Transliteration is a rule-based script mapping; it preserves the consonant skeleton and is not a phonetic translation.
Tokenizer options
# ['رفته_است']
# ['EMAIL']
Options: join_verb_parts, join_abbreviations, separate_emoji,
replace_links, replace_ids, replace_emails, replace_numbers,
replace_hashtags. All are off by default, so plain word_tokenize is
unchanged.
Embeddings
Static fastText/word2vec vectors load from a .vec file into the Rust core and
stay there: lookups and sentence pooling run without Python objects, numpy or
gensim, and the model is shared read-only across threads. Downloads are
explicit and checksum-verified:
=
, # vector size, vocabulary size
# list[float] or None
# cosine or None
# mean of in-vocab token vectors
# parallel, GIL released
# [(word, cosine), ...]
Out-of-vocabulary tokens are skipped in sentence vectors; vector() and
similarity() return None. RISHE_HOME overrides the cache root, and
rishe.hub.pull(url, sha256=...) pulls a custom model. Only .vec text files
are supported: fastText .bin and GloVe files are not.
Corpora
ConlluReader parses UD/CoNLL-U, PeymaReader parses the PEYMA test format,
and PlainTextReader reads one sentence per line. Other corpus-specific formats
can be added as needed.
CLI
|
Convenience classes
Module-level helpers have object-style counterparts when you want a long-lived instance instead of building one per call:
=
# 'میکنم'
=
# 'رفت#رو' (past#present)
# ['رفته_است']
Normalizer, WordTokenizer, SentenceTokenizer, Lemmatizer, Stemmer and
TokenSplitter are all available this way, plus the module-level
word_tokenize and sent_tokenize. They take no lexicon paths: Rishe compiles
its lexicons in.
Rust
use Pipeline;
let pipe = create;
let doc = pipe.process;
println!;
println!;
Scope
Rishe is a text-preprocessing and morphology engine: normalization, tokenization, sentence splitting, lemmatization, stemming, verb conjugation, inflection, token splitting, spell correction, stopword removal, ezafe detection, informal → formal conversion and static word/sentence embeddings.
It does not yet do POS tagging, chunking, NER, dependency parsing, contextual embeddings or text classification. Static word/sentence embeddings are available as an opt-in model file; the base stays zero-dependency and fast. Today, use Rishe as the preprocessing step in front of a tagger or parser.
Building from source
scripts/extract_lexicons.py regenerates data/*.tsv and
data/word_freq.tsv from Hazm's data files; build.rs compiles them into FSTs.
License
Dual-licensed under Apache-2.0 or MIT, at your option. The bundled lexicons, stopwords and word frequencies are derived from Hazm data (MIT); see NOTICE.md.