Migrating from v6 to v7
Lindera v7.0.0 turns the lindera crate into a facade: it re-exports the
morphological segmenter, now published as lindera-segmenter, and — through
the new analysis feature, on by default — the analysis chain of
lindera-analysis as lindera::analysis. One dependency line gives you the
Segmenter, the Tokenizer, and every filter. The release also renames
lindera-binding-core to lindera-binding and removes the deprecated
LINDERA_DICTIONARIES_PATH fallback, as announced in v5.0.0, corrects
the IPADIC schema, which had the conjugation_type and conjugation_form
names swapped, keeps dictionary entries whose surface is whitespace or
starts or ends with it, skips whitespace in the lattice as MeCab does
(SudachiDict excepted, as Sudachi keeps it),
resolves overlapping char.def lines as MeCab does, carries the context
across 、 and 。 within a line as MeCab does, returns N-best results
that each cover the whole input, lets unknown words start at every position
as MeCab does, creates them from each character's default category and
groups the characters that share a category, as MeCab does, keeps the
dashes and tildes of dictionary entries as written
instead of rewriting them, removing the normalize_details setting, outputs
the first CSV row among tied dictionary entries as MeCab does, chooses the
segmentation whose last word starts later when two cost the same, as MeCab
does, and has the lattice backtraces of lindera-dictionary return each
token's end offset. The bundled CC-CEDICT dictionary gets new costs, with
which unknown words no longer cost less than dictionary words, so that runs
of Latin letters, kana and digits stay one word. lindera train builds its lattices as MeCab's learner
does, with the seed words and the unknown words that the runtime creates,
and keeps every unk.def entry, so models trained with earlier versions
must be trained again, and lindera build rejects a dictionary whose
context IDs fall outside its connection matrix, as building a tokenizer
does for such a user dictionary. Dictionary directories built or downloaded with v6.2.0
or earlier no longer load and must be rebuilt or downloaded again
(dictionary format version 3). Building Lindera now takes Rust 1.89 or
newer.
This guide lists every breaking change and the one-line fixes for each.
Overview
| Change | Affects | What you do |
|---|---|---|
lindera is a facade: lindera::analysis re-exports lindera-analysis (feature analysis, on by default) | Rust users who depend on both lindera and lindera-analysis | Bump both to "7" in the same step, or drop lindera-analysis and import from lindera::analysis::… |
default-features = false now means "segmenter only": it turns off analysis (and, as before, mmap) | Rust users of lindera with default-features = false who want lindera::analysis | Add "analysis" to features (and keep "mmap" listed for memory-mapped loading) |
The former lindera crate is published as lindera-segmenter | Nobody directly | Nothing — lindera = "7" re-exports it |
The raw lindera-dictionary modules are reachable under lindera::dictionary | Direct lindera-dictionary users | Nothing — optionally drop the direct dependency and use lindera::dictionary::core::…, ::builder, ::viterbi, … |
The implicit features lindera-ipadic, lindera-ipadic-neologd, lindera-unidic, lindera-sudachidict, lindera-ko-dic, lindera-cc-cedict, and lindera-jieba are gone | Anyone enabling a dictionary crate by name in features = [...] | Use the embed-* features instead |
lindera-binding-core is renamed to lindera-binding | Rust users of the binding helper crate | Depend on lindera-binding and replace lindera_binding_core:: with lindera_binding:: |
LINDERA_DICTIONARIES_PATH is removed | Anyone still setting the deprecated build-cache variable | Set LINDERA_BUILD_DICTIONARY_CACHE_DIR; the old name is now ignored |
| Rust 1.89 or newer is required | Anyone who builds Lindera with Rust 1.88, including a source build of the Python or Ruby package | Update the toolchain (rustup update stable) |
| Dictionary format version 3: dictionary directories built or downloaded with v6.2.0 or earlier no longer load | Anyone who loads a dictionary from files (load_dictionary with a path, lindera tokenize --dict <dir>, the bindings' path and byte loaders, dictionaries saved in the WASM binding's OPFS storage); not the embedded dictionaries or user dictionaries | Rebuild with the 7.0.0 lindera build, or download again with the 7.0.0 lindera download or from the 7.0.0 release assets |
IPADIC and IPADIC-NEologd: conjugation_type and conjugation_form now name the right columns | Anyone who reads these two fields by name with IPADIC or IPADIC-NEologd (Token::get, Token::as_value, lindera tokenize -o json, binding schemas) | Expect the two values to trade places; rebuild or re-download dictionaries made with v6.2.0 or earlier |
| Dictionary entries with whitespace are kept | Text with U+3000 (IPADIC, IPADIC-NEologd, UniDic), text with spaces (SudachiDict), and a few entries that ended with whitespace | Expect U+3000 to be 記号,空白 / 空白 and SudachiDict to segment spaced text much closer to Sudachi; rebuild or re-download dictionaries made with v6.2.0 or earlier |
| Whitespace is skipped in the lattice: the words on either side of a space connect directly, as in MeCab | Anyone who segments text containing whitespace with keep_whitespace false (the default), most visibly with ko-dic; not with SudachiDict, which keeps whitespace in the lattice as Sudachi does | Expect MeCab's segmentation for such text; skip_whitespace(false) ("skip_whitespace": false, --disable-skip-whitespace) restores the v6 handling of whitespace, but not the other output changes in this guide |
Overlapping char.def lines: the last line decides | Text with Ð (U+00D0; every dictionary except ko-dic), 々 (U+3005) or 〇 (U+3007); lindera train users whose char.def has single-code-point or overlapping lines | Expect Ð to stay in the output as a letter; rebuild or re-download dictionaries made with v6.2.0 or earlier |
The context is carried across 、 and 。 within a line, as in MeCab | Anyone who segments Japanese text with 、 or 。 inside a line | Expect some words after 、 or 。 to be read differently, mostly as MeCab reads them, and the N-best costs of such lines to change; no setting restores the v6 behavior |
| N-best: every result covers the whole input | Anyone who asks for N-best results (segment_nbest, tokenize_nbest, lindera tokenize -N, the bindings' N-best methods) for input with more than one sentence | Expect the cheapest segmentations of the whole input from the second result on, and one result without tokens for an empty input; a cost threshold now applies to the whole input |
| Unknown words start at every position, as in MeCab | Anyone who segments text with runs of katakana, symbols or Latin letters in normal mode, most visibly katakana joined by ・ and Korean sentence-final punctuation such as ." | Expect such runs to be split where MeCab splits them, and katakana- and Latin-heavy text to take about 13% more instructions; no setting restores the v6 behavior |
| Unknown words come from each character's default category and group the characters that share a category, as in MeCab | Anyone who segments text where a character with several categories meets other characters; in the bundled dictionaries, a hanja numeral followed by hanja with ko-dic, and 〇 (U+3007) with ko-dic; Rust code that relies on the order of CharacterDefinition::lookup_categories | Expect such text to be segmented as MeCab segments it (ko-dic 三國史記 is one SH word, 〇〇〇 one SY word); no setting restores the v6 behavior |
| CC-CEDICT: unknown words cost 400, and the entries made only of Latin letters or only of digits 399 | Anyone who segments text with CC-CEDICT, most visibly entries that mix Latin letters or digits with Chinese characters, such as T恤; anyone who reads its N-best costs | Expect such entries to be one word, 〇 and entries such as PK to come with their pinyin and definition, Latin words, kana and numbers to stay one word as in v6, and the costs of paths to change; build a CC-CEDICT dictionary directory from the CC-CEDICT-MeCab-0.1.0-20261021 archive |
Jieba: 〇 is a numeral entry | Anyone who segments text with 〇 (U+3007) with Jieba | Expect each 〇 to be one numeral word (m, pinyin ling2) instead of part of an unknown word, as MeCab segments it with the same dictionary; build a Jieba dictionary directory from the mecab-jieba-0.1.3 archive |
| Jieba: costs retrained on UD Chinese GSD | Anyone who segments text with Jieba; users of simple Jieba user dictionaries | Expect Chinese text to be segmented as UD Chinese GSD segments it, finer than jieba (中华 / 人民 / 共和 / 国, 这 / 个; segmentation F1 on its test split 77.3 → 87.7), and simple user dictionary words to cost −32768; build a Jieba dictionary directory from the mecab-jieba-0.1.3 archive |
| IPADIC and IPADIC-NEologd: dashes and tildes in dictionary entries are kept as written, as in MeCab | Anyone who segments text with ― (U+2015), — (U+2014), ~ (U+FF5E) or 〜 (U+301C) with IPADIC or IPADIC-NEologd | Expect entries spelled with ― or ~ to match text spelled the same way, and text with — or 〜 to miss the 12 IPADIC and 1,085 IPADIC-NEologd entries it found only through the old rewrite (a mapping character filter can fold the spellings); rebuild or re-download dictionaries made with v6.2.0 or earlier |
| Entries with the same surface, context IDs and cost: the first CSV row is output, as in MeCab | Anyone who reads token details such as the reading or the base form with IPADIC, IPADIC-NEologd, UniDic or ko-dic, or with a user dictionary that has such entries; N-best users | Expect such words to get MeCab's details (IPADIC 狡い reads ズルイ, not コスイ), a user entry to still win a tie with a system entry, and the first N-best result to always be the 1-best; no setting restores the v6 behavior |
| Segmentations of equal cost: the one whose last word starts later is chosen, as in MeCab | Anyone who segments text: rarely with the Japanese and Korean dictionaries (9 of about 54,000 lines in the texts compared), but in about a quarter of the sentences with CC-CEDICT (1,267 of the 4,997 sentences of UD Chinese GSD; 3 with Jieba) | Expect such lines to be segmented as MeCab segments them (IPADIC 腸窒扶斯 is 腸 / 窒扶 / 斯, not 腸 / 窒 / 扶斯, and CC-CEDICT 電視台上班 is 電視 / 台上 / 班, not 電視 / 台 / 上班); no setting restores the v6 behavior |
lindera tokenize removes only the line terminator from each input line and writes a result for every line | CLI users whose input lines start or end with whitespace, or who read the wakati or N-best output of lines without tokens | Expect the offsets to index the input line, U+3000 at the start or end of a line to be a token (IPADIC 記号,空白), --keep-whitespace to output the spaces there, and an empty line or a line of spaces to give an empty wakati line and one N-best result; no setting restores the v6 behavior |
WASM: the TokenizerBuilder settings apply whichever way the dictionary is set | WASM users who combine setDictionaryInstance() with setKeepWhitespace() or filters, or setDictionary() with setUserDictionaryInstance() | Expect those settings and the user dictionary to take effect; a dictionary instance whose char.def has no SPACE category now needs setKeepWhitespace(true), as a dictionary set by URI does |
| PHP: an optional argument of the wrong type throws instead of selecting the default | PHP code that passes a value of another type to an optional parameter, such as a numeric string for $max_iter of Lindera\Trainer::train() or 1 for $unique of tokenizeNbest() | Pass the declared type, casting strings such as $argv values with (int); null or leaving the argument out still selects the default |
| PHP, Node.js and WASM: an integer argument outside the range of its setting throws instead of wrapping | Code that passes such a value, such as 100000 for the default word cost of Metadata (v6 stored -31072) or -1 for the number of N-best results (v6 listed every segmentation); a negative index for getFieldName or PHP's getDetail; in Node.js and WASM, a number that is not an integer, such as 1.5 or NaN | Pass an integer in the range; the error names the parameter and the range |
Bindings: a mode is a Mode, which holds the penalty of decompose mode as the Rust Mode does; set_mode / setMode and the Tokenizer constructors take a Mode instead of a mode name, and TokenizerBuilder::set_segmenter_mode keeps the penalty of a Mode::Decompose | Binding users who set a mode; code that uses a binding's Mode, Node.js's JsMode or WASM's numeric Mode; TypeScript code that names the Node.js type JsPenalty; Rust code that passes a custom penalty to set_segmenter_mode; binding authors who call CoreTokenizerBuilder::set_mode or the CoreTokenizer::from_segmenter functions | Pass Mode.normal() or Mode.decompose(penalty) (in Ruby Lindera::Mode.normal, in PHP Lindera\Mode::normal()) instead of "normal" or "decompose", and turn a mode name read at run time into a Mode with Mode.from_str / Mode.fromStr; rename JsPenalty to Penalty; pass a lindera::mode::Mode to the lindera-binding functions |
lindera train builds its lattices as MeCab's learner does and keeps every unk.def entry; model.dat has the format version 2.0.0 | Users of lindera train and lindera export, and of the training functions of the Python, Node.js, Ruby and PHP bindings | Train every model again with 7.0.0, then export and build it again (a model.dat written by v6.2.0 or earlier is rejected); give every unk.def line at least five fields; remove comment lines from seed lexicons |
lindera build rejects context IDs outside matrix.def | Anyone who builds a system dictionary whose lex.csv or unk.def has a context ID beyond the size of its matrix.def (such a dictionary built and then panicked at tokenization); not dictionaries that MeCab's mecab-dict-index accepts. Also anyone whose user dictionary has such a context ID for the system dictionary it is used with: building the tokenizer now reports it, where tokenization used to panic | Fix the IDs or the matrix, or set skip_invalid_cost_or_id in metadata.json to drop such lex.csv rows; fix such IDs in the user dictionary |
lindera build and lindera train check char.def against unk.def | Anyone whose char.def has a range line that names a category not defined on an earlier line, or a category defined twice, or whose char.def and unk.def do not name the same categories (such a dictionary built, and then panicked, used the settings of another category, cut its output short or ignored unk.def lines); not dictionaries that MeCab's mecab-dict-index accepts | Define each category once, before the range lines that name it; give every category an unk.def line, and remove the unk.def lines of undefined categories |
lindera build gives a cell missing from matrix.def the cost 0, as MeCab does | Anyone who builds a system dictionary from a matrix.def that leaves cells out; not the bundled dictionaries or a matrix.def that lindera export writes, which list every cell | Expect such a cell to cost 0 instead of 32767; to keep a connection expensive, list its cell with a cost |
| The CLI writes library warnings to standard error | CLI users who read standard error, for example scripts that treat any output there as a failure; most visibly input with more than 32 KiB without a sentence delimiter, and lindera build of ko-dic or UniDic without --context-id-freq | Expect [WARN …] lines on standard error; set RUST_LOG=error to hide them. Standard output does not change |
The normalize_details setting is removed: Metadata::new and CoreMetadata::new take 10 arguments, Ruby's Metadata.new takes 8 | Code that creates or reads dictionary metadata: Rust (Metadata, lindera_binding::CoreMetadata, PrefixDictionaryBuilderOptions) and the Metadata classes of the Python, Ruby, PHP and Node.js bindings | Remove the argument, option or property; a metadata.json that still has the key loads, and the key is ignored |
Lattice::tokens_offset, tokens_offset_into, nbest_tokens_offset and NBestGenerator::next return (start, end, WordId) | Rust code that calls these lindera_dictionary functions directly (also as lindera::dictionary::viterbi::…, lindera::dictionary::Lattice and lindera::dictionary::nbest::…) | Destructure three fields and use the returned end instead of the next token's start |
Lattice::tokens_offset_into returns the BOS index of the best path (Option<usize>) | Rust code that uses the () value of tokens_offset_into | Nothing for a call written as a statement; otherwise ignore the new value |
UnknownDictionary::gen_unk_words, UnknownDictionary::compatible_unk_index, UnkWord and WordIdx are removed | Rust code that uses them from lindera_dictionary::dictionary::unknown_dictionary (also lindera::dictionary::core::unknown_dictionary) | Use Lattice::unknown_word_candidates, which lists the unknown words that the runtime creates |
lindera_trainer: SerializableModel::unk_category_names and unk_categories are replaced by unk_entries, and Trainer::max_grouping_len takes the runtime's meaning | Rust code that uses lindera-trainer (also lindera::dictionary::trainer) | Read unk_entries, the unk.def entries in file order; pass max_grouping_len the cap the dictionary will be used with |
The language bindings (Python, Node.js, Ruby, PHP, WASM) and the CLI keep
their package names, and their APIs apart from the mode and one setting: a
binding takes the mode as a Mode, which holds the penalty of decompose
mode as the Rust Mode does, instead of a mode name (see
Bindings: a mode is a Mode), and the
Metadata classes of the Python, Node.js, Ruby and PHP bindings no longer
take or expose normalize_details (see
The normalize_details setting is removed).
The PHP binding also rejects a value of the wrong type for an optional
argument, which v6 read as null (see
PHP: optional arguments reject a value of the wrong type),
and the PHP, Node.js and WASM bindings reject an integer argument outside
the range of its setting, which v6 wrapped (see
Bindings: integer arguments outside their range are rejected).
Their version number moves to 7.0.0. Like the Rust crates, they no longer
load a dictionary directory built or downloaded with v6.2.0 or earlier (see
Dictionary directories must be rebuilt).
A model file written by their training functions, or by lindera train,
with v6.2.0 or earlier has to be trained again (see
lindera train builds its lattices as MeCab's learner does).
There are fourteen output differences, all described below; the last two are
in the CLI only and in the WASM binding only. With IPADIC or
IPADIC-NEologd, the values reported under the names conjugation_type and
conjugation_form trade places. The dictionaries now contain the entries
whose surface is or starts or ends with whitespace, which changes the
segmentation of text that contains such whitespace. Text that contains
whitespace is segmented as MeCab segments it, except with SudachiDict, which
keeps whitespace in the lattice as Sudachi does. Ð, 々 and 〇 get the
character categories that MeCab gives them, which changes the segmentation
of text that contains them. Within a line, the words after 、 and 。
are read in the context of the words before them, which changes the
segmentation of some such text and the N-best costs of every such line. For
input with more than one sentence, the N-best results from the second one on
are the cheapest segmentations of the whole input, and an empty input gets
one N-best result without tokens. And in normal mode, an
unknown word can start inside a run of characters of one kind, as in MeCab,
which splits some runs of katakana, symbols and Latin letters. Unknown words
come from each character's default category, and a grouped unknown word
goes on while each character shares a category with the one before it, as
in MeCab, which makes a ko-dic hanja numeral and the hanja after it one
word and 〇 a symbol with ko-dic. With IPADIC
and IPADIC-NEologd, the entries spelled with ― or ~ are found under
that spelling, as in MeCab, which changes the segmentation of text that
contains ―, —, ~ or 〜. Among dictionary entries that share the
surface, the context IDs and the cost, the first CSV row is output instead
of the last, as in MeCab, which changes the reading, the base form or other
details of such words, and the first N-best result is always the 1-best.
When two segmentations cost exactly the same, the one whose last word starts
later is chosen, as in MeCab, which changes the segmentation of a few lines
with the Japanese and Korean dictionaries and Jieba and of about a quarter
of the sentences with CC-CEDICT.
With CC-CEDICT, unknown words no longer cost less than dictionary words,
which makes entries such as T恤 one word and gives 〇 and entries such
as PK their pinyin and definition. Jieba's costs are trained again on UD
Chinese GSD, which segments Chinese text as that treebank does and raises
segmentation F1 on its test split from 77.3 to 87.7, and 〇 is a numeral
entry, one word with its pinyin instead of part of an unknown word.
And lindera tokenize removes only the line terminator from each input line
instead of all the whitespace at its ends, which keeps U+3000 at the start
or end of a line and makes the offsets index the line, and it writes a
result for every line, also in the wakati format and with -N. And the
WASM binding's TokenizerBuilder applies all its settings and the user
dictionary whichever way the dictionary was set, where v6 ignored some of
them.
Otherwise, for the same input and dictionary, v7.0.0 produces the same
tokens with the same positional details as v6.2.0.
The lindera crate is now a facade
Up to v6 the Rust API was split between two crates that you had to depend on
separately: lindera (the segmenter and the dictionary loading API) and
lindera-analysis (character filters, token filters, and the Tokenizer).
In v7 the lindera crate is a thin facade over both:
lindera ──┬─▶ lindera-segmenter ──┬─▶ lindera-dictionary
│ ├─▶ dictionary crates [features: embed-*]
│ └─▶ lindera-trainer [feature: train]
└─▶ lindera-analysis [feature: analysis, default on] ──▶ lindera-segmenter
Its public tree keeps every lindera::… path of v6 and adds
lindera::analysis:
lindera
├── LinderaResult, get_version()
├── dictionary, error, mode, segmenter, space_penalty, token, worker (from lindera-segmenter, as in v6)
└── analysis [feature: analysis, default on] (lindera-analysis: character_filter, token_filter, tokenizer, worker)
One dependency instead of two
# v6
[dependencies]
lindera = "6"
lindera-analysis = "6"
# v7
[dependencies]
lindera = "7"
#![allow(unused)] fn main() { // v6 use lindera_analysis::tokenizer::Tokenizer; use lindera_analysis::token_filter::japanese_stop_tags::JapaneseStopTagsTokenFilter; // v7 use lindera::analysis::tokenizer::Tokenizer; use lindera::analysis::token_filter::japanese_stop_tags::JapaneseStopTagsTokenFilter; }
Keeping a direct lindera-analysis = "7" dependency also works:
lindera::analysis is that same crate, so
lindera_analysis::tokenizer::Tokenizer and
lindera::analysis::tokenizer::Tokenizer are one type and existing use
paths need not change.
Update lindera and lindera-analysis together
If you keep both dependencies, bump them in the same step. lindera-analysis
6.x depends on the lindera 6.x segmenter, so a manifest with the lindera
line moved to "7" while lindera-analysis stays at "6" pulls two
versions of the segmenter into the build:
# Two Segmenter types: lindera 7 (via lindera-segmenter) and lindera 6 (via lindera-analysis 6)
[dependencies]
lindera = "7"
lindera-analysis = "6"
lindera::segmenter::Segmenter is then the v7 type while
lindera_analysis::tokenizer::Tokenizer::new expects the v6 one, and the
call fails to compile with a type mismatch. Move both lines to "7" at once,
or drop lindera-analysis as shown above.
default-features = false now means segmenter only
The facade's default features are mmap and analysis. With
default-features = false you get the pure segmenter, and both are off:
lindera::analysis does not exist, and memory-mapped loading is no longer
the default for filesystem dictionaries (as in v6, where
default-features = false also turned mmap off). Re-enable what you need:
# Segmenter with memory-mapped loading (the v6 default build)
lindera = { version = "7", default-features = false, features = ["mmap"] }
# Segmenter and analysis chain, without memory-mapped loading as the default
lindera = { version = "7", default-features = false, features = ["analysis"] }
memmap2 itself stays linked either way through lindera-dictionary's
default features; the facade's mmap feature only selects the default for
use_mmap in load_dictionary and friends, and
load_dictionary_with_options(uri, true) works without it.
The segmenter is published as lindera-segmenter
The lindera crate of v6 and earlier continues as lindera-segmenter 7.0.0,
with the same modules (dictionary, error, mode, segmenter,
space_penalty, token, worker) and the same API. The facade re-exports
every one of them at its root, so every lindera::… path that compiled in v6
compiles in v7. The crate exists so that lindera-analysis has a segmenter
to build on without depending on the facade that re-exports it; nobody needs
to depend on lindera-segmenter directly, and application code should keep
depending on lindera.
lindera::dictionary exposes the raw dictionary crate
Code that reached into the building blocks of lindera-dictionary — the
dictionary data structures, the dictionary builder, the Viterbi lattice —
used to need a direct lindera-dictionary dependency. In v7 these modules
are re-exported inside lindera::dictionary, next to the high-level loading
API:
lindera::dictionary
├── Dictionary, Metadata, Schema, load_dictionary, load_user_dictionary, DictionaryKind, … (unchanged)
├── core lindera_dictionary::dictionary (prefix_dictionary, character_definition, connection_cost_matrix, unknown_dictionary, …)
├── builder, error, loader, mode, nbest, space_penalty, util, viterbi
├── embedded_dictionary!, include_bytes_aligned!
└── trainer lindera_trainer, with the `train` feature (unchanged)
All existing lindera::dictionary::* paths stay valid, and the re-exports
are the same items — lindera::dictionary::Dictionary and
lindera::dictionary::core::Dictionary are one type. A direct
lindera-dictionary dependency keeps working; it is simply no longer
required:
#![allow(unused)] fn main() { // v6: needs lindera-dictionary in Cargo.toml use lindera_dictionary::dictionary::prefix_dictionary::PrefixDictionary; use lindera_dictionary::viterbi::Lattice; // v7: reachable through the facade use lindera::dictionary::core::prefix_dictionary::PrefixDictionary; use lindera::dictionary::viterbi::Lattice; }
[!NOTE]
lindera::dictionary::coreshares its name with Rust'scorecrate. A glob import such asuse lindera::dictionary::*;brings that module into scope and shadows thecorecrate for the rest of the module, so paths likecore::fmt::Debugstop resolving to the standard library there. Import the items you need by name instead.
Implicit dictionary features removed
In v6 the optional dictionary dependencies of lindera doubled as Cargo
features: features = ["lindera-ipadic"] was accepted, and pulled in the
crate without embedding a dictionary. The facade has no dictionary
dependencies of its own, so these seven implicit features no longer exist:
lindera-ipadic, lindera-ipadic-neologd, lindera-unidic,
lindera-sudachidict, lindera-ko-dic, lindera-cc-cedict, and
lindera-jieba. Cargo rejects a manifest that names one of them. Use the
embed-* features, which have always been the documented way to embed a
dictionary:
# v6
[dependencies]
lindera = { version = "6", features = ["lindera-ipadic"] }
# v7
[dependencies]
lindera = { version = "7", features = ["embed-ipadic"] }
lindera-binding-core renamed to lindera-binding
This only matters if you build your own language binding on the shared
helper crate. lindera-binding 7.0.0 depends on the lindera facade (with
the analysis feature) instead of on lindera plus lindera-analysis; its
own API — CoreTokenizerBuilder, CoreTokenizer, TokenView, and the
argument, metadata, and schema helpers — is unchanged, except that
CoreMetadata no longer has normalize_details (see
The normalize_details setting is removed).
The published
lindera-binding-core releases (4.0.0 through 6.2.0) stay on crates.io as
they are and are not yanked, but receive no further versions.
# v6
[dependencies]
lindera-binding-core = "6"
# v7
[dependencies]
lindera-binding = "7"
#![allow(unused)] fn main() { // v6 use lindera_binding_core::tokenizer::CoreTokenizer; // v7 use lindera_binding::tokenizer::CoreTokenizer; }
LINDERA_DICTIONARIES_PATH removed
v5.0.0 renamed the build-time dictionary cache variable to
LINDERA_BUILD_DICTIONARY_CACHE_DIR and kept the old name,
LINDERA_DICTIONARIES_PATH, as a deprecated fallback with a build warning
(see Migration v4 to v5). v7.0.0 removes the
fallback. A build that sets only the old name no longer uses the cache — and
no longer warns about it: the dictionary crates download and build their
dictionary as if no cache were configured. Rename the variable wherever it is
set (shell profiles, CI configuration, container images):
# v6
export LINDERA_DICTIONARIES_PATH=/path/to/cache
# v7
export LINDERA_BUILD_DICTIONARY_CACHE_DIR=/path/to/cache
Rust 1.89 or newer is required
v7.0.0 declares rust-version = "1.89"; v6.2.0 declared 1.88. The build
scripts of the dictionary crates now lock the entry of
LINDERA_BUILD_DICTIONARY_CACHE_DIR they write with File::lock, which
Rust 1.89 stabilized, so that builds sharing the cache no longer remove each
other's files. With an older toolchain, cargo stops with a message that
names the required version. Update the toolchain:
rustup update stable
Dictionary directories must be rebuilt
v7.0.0 writes dictionaries in format version 3 and rejects a dictionary directory built or downloaded with v6.2.0 or earlier, which is at version 2, when it loads it:
Dictionary 'ipadic' has format version 2, but this build of Lindera reads format version 3. To fix this, rebuild it with `lindera build`, or download a matching prebuilt dictionary with `lindera download`.
Version 3 lists each code point's default category first in char_def.bin,
because unknown words now come from that category only (see
Unknown words come from the default category, as in MeCab).
A version 2 file has the same layout, so without the check it would load
and silently give some characters the wrong default category. The version
also covers the other changes in what the dictionary builder writes, which
a v6.2.0 dictionary does not have:
the IPADIC conjugation names,
the entries with whitespace,
the overlapping char.def lines
and the dash and tilde spellings.
| Dictionary | What you do |
|---|---|
Embedded (embed-* features) | Nothing: it is built when the crate is compiled |
Downloaded with lindera download | Download it again with the 7.0.0 CLI, which stores it under a directory of its own version |
Built with lindera build | Build it again with the 7.0.0 CLI |
| Downloaded from the release assets | Download the 7.0.0 asset |
| Saved in the WASM binding's OPFS storage | Remove it with removeDictionary() and download the 7.0.0 asset with downloadDictionary(); a hasDictionary() check alone keeps the old one |
User dictionaries (CSV and .bin) | Nothing: they have no char_def.bin and no format version |
IPADIC conjugation field names corrected
IPADIC stores 活用型, the conjugation type (for example 五段・カ行イ音便),
in column 8 and 活用形, the conjugation form (for example 連用タ接続), in
column 9. From v1.0.0 through v6.2.0, the lindera-ipadic and
lindera-ipadic-neologd schemas named the two columns the other way round,
so every lookup by name returned the other value. v7.0.0 corrects the names.
UniDic and SudachiDict were already correct.
For the token 書い in 書いた:
| Field name | v6 | v7 |
|---|---|---|
conjugation_type | 連用タ接続 | 五段・カ行イ音便 |
conjugation_form | 五段・カ行イ音便 | 連用タ接続 |
Only access by name changes: Token::get("conjugation_type") and
Token::get("conjugation_form"), the JSON from Token::as_value() and
lindera tokenize -o json, and the field names in the dictionary schema
that the bindings expose. Positional access — details, and the MeCab and
wakati output formats — is unchanged, because the values were always in the
right columns. If your code reads these fields by name, or swapped them back
as a workaround, update it.
The names come from the metadata.json stored in each built dictionary, not
from the library. The embedded dictionaries (embed-ipadic,
embed-ipadic-neologd) and the dictionaries that the 7.0.0 CLI fetches with
lindera download carry the corrected names. A dictionary directory built
or downloaded with v6.2.0 or earlier no longer loads (see
Dictionary directories must be rebuilt);
rebuild it with the v7.0.0 lindera-ipadic/metadata.json (or
lindera-ipadic-neologd/metadata.json), or download the 7.0.0 release asset.
If you build dictionaries with your own copy of the IPADIC metadata.json,
swap "conjugation_form" and "conjugation_type" in its
dictionary_schema.fields list.
Dictionary entries with whitespace are kept
Up to v6.2.0, the dictionary builder trimmed the surface of every lexicon entry. An entry whose surface is whitespace was dropped, and an entry that starts or ends with whitespace was stored under the trimmed surface, where a cheaper whitespace variant could take over the entry without whitespace. v7.0.0 reads the surface exactly as written, as MeCab and Sudachi do:
| Dictionary | Change |
|---|---|
| IPADIC, IPADIC-NEologd | U+3000 (ideographic space) is the dictionary entry 記号,空白 instead of an unknown 名詞,サ変接続, so the word after it is also read as in MeCab: in 東京 都, 都 is 名詞,一般 instead of 名詞,接尾. ルーマニア has the base form ルーマニア instead of ルーマニア followed by U+3000 |
| UniDic | U+3000 is the entry 空白 instead of an unknown 名詞,普通名詞,サ変可能 |
| SudachiDict | U+0020 uses the dictionary's half-width space entry (空白) instead of the SPACE unknown word, so text with spaces is segmented much closer to Sudachi (on 600 sentences with a space between every word, agreement with Sudachi went from 201 to 597). With keep_whitespace(true), each space is its own token. U+3000 is unchanged, because SudachiDict has no entry for it |
| ko-dic | 에듀 and 캘리 on their own are NNP (person names) instead of the NNG of their trailing-space variants |
| CC-CEDICT, Jieba | No change |
Entries that end with whitespace (55 in IPADIC-NEologd, 4 in ko-dic and 4 in
SudachiDict) now match only where the text has that whitespace, as in MeCab:
with IPADIC-NEologd, the entry GeForce GTX Titan X followed by a space
matches GeForce GTX Titan X です but no longer GeForce GTX Titan Xです.
The token ends with that whitespace, as in MeCab, whether whitespace stays
in the lattice (keep_whitespace(true), or skip_whitespace(false),
SudachiDict's default) or is skipped (the default for the other
dictionaries, see the next section). Skipping leaves out only the whitespace
after the entry: in GeForce GTX Titan X です, with two spaces, the token
ends with the first space. 50 of the 55 IPADIC-NEologd entries end with a
space, as do all those of ko-dic and SudachiDict; the other 5 end with
U+3000 or U+00A0, which are not SPACE characters and are never skipped.
The Decompose length penalty counts the entry's own whitespace, as it does
when whitespace stays in the lattice. With unique, N-best results that
differ only in that whitespace are no longer folded into one, such as
ko-dic's 에듀 followed by the space (NNG) and 에듀 (NNP) in
에듀 센터. A word right after such an entry pays no left-space penalty
(ko-dic), as in mecab-ko, whose penalty applies only after whitespace that
the word before does not end with: 에듀 이 reads as 에듀 followed by
the space and 이/VCP.
U+3000 is not in the SPACE character category, so it is a token even with
keep_whitespace(false), as in MeCab. To remove it, drop 記号,空白
(IPADIC) or 空白 (UniDic) with the japanese_stop_tags token filter, or
turn it into U+0020 with the unicode_normalize character filter (NFKC).
The change is in the dictionary builder. The embedded dictionaries and the
dictionaries that the 7.0.0 CLI fetches with lindera download include
these entries; a dictionary directory built or downloaded with v6.2.0 or
earlier no longer loads (see
Dictionary directories must be rebuilt).
Whitespace is skipped in the lattice
With keep_whitespace false (the default), v7.0.0 skips whitespace in the
Viterbi lattice, as MeCab does: the word after a space connects directly to
the word before it. v6 dropped whitespace from the output but kept it in the
lattice as the SPACE unknown word, so the words around a space connected to
that node instead of to each other. How much that changed depends on the
dictionary's connection costs for the node: in ko-dic they are all zero, so
every space cut the context between the words around it; in IPADIC and
UniDic they are not zero, and the node's costs stood in for the connection
between the words.
The output changes only for sentences that contain whitespace. That includes
a sentence that ends with the \n or \t it was split at, such as a line
that does not end with 。: its last word now connects to the end of the
sentence directly.
| Dictionary | Effect | Example |
|---|---|---|
| ko-dic | Spaced Korean text now follows mecab-ko | 2년 전 대회: 전 is NNG, not 저/NP + ㄴ/JX; 하고 있다: 있 is VX, not VV |
| IPADIC, IPADIC-NEologd, UniDic | A word after a half-width space connects to the word before it, as in MeCab | IPADIC Google が 新しい: が is a case particle, not a conjunction; 東京 都: 都 is a suffix, as in MeCab |
| SudachiDict | No change: its metadata.json sets skip_whitespace to false, because Sudachi keeps whitespace in the lattice and the costs assume it | — |
| CC-CEDICT | No change in the best path (the dictionary has no connection costs); N-best costs no longer include the whitespace nodes | — |
| Jieba | No change in the best path on the 247 sentences with a space of UD Chinese GSD and of the GSDSimp dev and test splits; N-best costs no longer include the whitespace nodes | — |
The default comes from the dictionary's metadata.json; a dictionary that
does not set skip_whitespace skips. A SudachiDict built by Lindera 6.x has
no such setting, but it no longer loads anyway; the rebuilt one has it.
Token surfaces and offsets never include the skipped whitespace; whitespace
that belongs to a dictionary entry, inside it or at its end, stays in that
entry's token, as in MeCab (see the previous section). Skipping does not
change the segmentation of text without whitespace. keep_whitespace(true)
keeps whitespace in the lattice, so its output is the same as in v6; beyond
the whitespace tokens, it can differ from the default v7 output, which skips
whitespace.
To get the v6 handling of whitespace back, turn skipping off; whitespace is
still dropped from the output. This does not undo the other output changes in
this guide, such as
Unknown words start at every position, as in MeCab.
Leaving skip_whitespace out of the configuration, or setting it to null,
keeps the dictionary's default:
#![allow(unused)] fn main() { // Segmenter let segmenter = Segmenter::new(Mode::Normal, dictionary, None).skip_whitespace(false); // SegmentWorker worker.set_skip_whitespace(false); }
# Configuration file / TokenizerBuilder::set_segmenter_skip_whitespace(false)
segmenter:
skip_whitespace: false
lindera tokenize --disable-skip-whitespace
The language bindings set it on TokenizerBuilder:
set_skip_whitespace(False) in Python, set_skip_whitespace(false) in Ruby,
and setSkipWhitespace(false) in Node.js, PHP and WASM. null (None in
Python, nil in Ruby) goes back to the dictionary's default.
Overlapping char.def lines are resolved as in MeCab
A dictionary's char.def can list the same code point on several lines. Up
to v6.2.0, the dictionary builder gave such a code point the categories of
every line that covers it. v7.0.0 uses only the last line, as MeCab,
vibrato and Kuromoji do. In the bundled dictionaries, three characters
change (ko-dic calls the kanji categories HANJA and HANJANUMERIC, and
CC-CEDICT and Jieba call them CHINESE and CHINESENUMERIC):
| Character | v6 categories | v7 categories | Effect |
|---|---|---|---|
Ð (U+00D0) | SPACE, ALPHA | ALPHA | Every dictionary except ko-dic maps U+00D0 to SPACE (a typo for U+000D inherited from mecab-ipadic) before the ALPHA range, so Ð was dropped as whitespace and split the word around it. GUÐMUNDUR さん now gives GUÐMUNDUR and さん instead of GU, MUNDUR and さん |
々 (U+3005) | KANJI, SYMBOL | SYMBOL | Dictionary words such as 人々 and 佐々木 are unchanged. After a kanji that the dictionary does not know, 々 is a token of its own, as in MeCab: 龘々 gives 龘 and 々 instead of one unknown word. With ko-dic, 々 is tagged SY instead of SH |
〇 (U+3007) | KANJI, SYMBOL, KANJINUMERIC | SYMBOL, KANJINUMERIC | Runs of numerals can be grouped differently. With ko-dic, 二〇二六年 is one unknown word (SH) instead of 二, 〇, 二六 and 年 (see also Unknown words come from the default category, as in MeCab) |
The kanji numerals 一 to 九, 十, 百, 千, 万, 億 and 兆 are
also listed on two lines, but keep their categories.
lindera train now reads char.def with the dictionary builder. It used
to keep only the first category of each line, skip lines that name a single
code point (such as 0x0020 SPACE), and resolve overlaps by start position.
The %t feature template now gives the first category of the last line
that covers the character, which is MeCab's default type. The bundled
training files have neither kind of line, so the models trained from them
do not change.
A range line without a category is now an error in lindera build and
lindera train, as in MeCab; it would otherwise reset the earlier lines to
DEFAULT.
The change is in the dictionary builder. The embedded dictionaries and the
dictionaries that the 7.0.0 CLI fetches with lindera download have the new
categories; a dictionary directory built or downloaded with v6.2.0 or
earlier no longer loads (see
Dictionary directories must be rebuilt).
The context is carried across 、 and 。
The segmenter cuts the input into sentences at \n, \t, 。 and 、,
and after 32 KiB without any of them, to keep each lattice small. Up to v6,
every sentence started from the beginning-of-sentence context (BOS) and
ended with the connection to the end of the sentence (EOS), so the word
after 、 or 。 was read as if it began a text. MeCab does not cut
inside a line.
v7.0.0 carries the context across the cuts at 、 and 。 and across the
forced cuts: the next sentence starts from the words that end the previous
one, and EOS is paid only at a \n, a \t or the end of the input. The
sentences up to that point form a segment, and segments stay independent.
The best path of a segment, and its cost, are those of one lattice over the
whole segment, which is what MeCab gives for a line:
| Input (dictionary) | v6 | v7 (as MeCab) |
|---|---|---|
彼は、ああ言った (IPADIC) | ああ is an interjection (感動詞) | ああ is an adverb (副詞) |
だから、こんなに答える (IPADIC) | こんな (連体詞) and に | こんなに (副詞) |
選手が泳ぎ、さらにカヌーで進んだ (UniDic) | さらに is a conjunction (接続詞) | さらに is an adverb (副詞) |
言語としては、Javaのオブジェクト (UniDic) | J, a, v and a, each 記号,文字 | Java (名詞,普通名詞) |
On the sentences of UD Japanese GSD that contain 、, the change alters
50 lines with UniDic: 27 come closer to the gold segmentation and
part-of-speech tags, 7 move away (sign test p = 0.0008), and 16 are ties.
Of 505 paragraphs of Botchan, the number that Lindera segments and tags
exactly as MeCab does went from 352 to 438 with IPADIC and from 428 to 503
with UniDic. Text without 、, 。 and forced cuts, such as text cut only
at \n and \t, is not affected. Carrying the context costs the 1-best
segmentation about 1% more instructions.
The result can still differ from one lattice over the segment:
- No word spans a cut. A dictionary entry that contains
、or。never matches: MeCab reads UniDic's一、二塁as one noun, Lindera as一,、and二塁. An unknown word is not grouped across a cut, and a word that would span a forced cut is split there. - The left-space penalty (ko-dic) does not apply to a word that starts right at a forced cut, even when whitespace precedes the cut.
The N-best results follow the same model. Within a segment, they are
exactly the N best paths of one lattice over the segment: unique compares
the word boundaries of the whole segment, and the cost threshold is
measured from the first result. Across segments, the results are the
cheapest combinations of one path per segment (see the next section). The
first result is the 1-best segmentation. The costs of a line with 、 or
。 change, the first result's included, because they now include the
connections across the cuts and the EOS connection only once.
There is no setting to restore the v6 behavior.
N-best results cover the whole input
The N-best methods (Segmenter::segment_nbest, Tokenizer::tokenize_nbest,
their _with_lattice and worker forms, lindera tokenize -N and the
bindings' N-best methods) cut the input into sentences as the 1-best
segmentation does. Up to v6, they searched each sentence on its own, and
the k-th result joined the k-th path of every sentence. From the second
result on, that changed every sentence at once, so the results were not the
cheapest segmentations of the input and their costs could go down, and a
sentence with fewer than k paths was missing from the k-th result. v7.0.0
searches the input segment by segment (see the previous section) and
returns the N cheapest combinations of one path per segment, in ascending
order of total cost, and every result covers the whole input:
Input (IPADIC, -N 3) | v6 | v7 |
|---|---|---|
東京、です | 3079 東京 、 です; 23850 東 京 、 で す; 24249 東 京 、 で す | 3357 東京 、 です; 12059 東京 、 です (、 as 名詞,数); 13633 東京 、 で す |
。東京 | 13 。 東京; 30433 。 東 京; 13520 東 京 (no 。) | 852 。 東京; 13569 。 東 京; 13636 。 東 京 |
Each of these lines is one segment, so its v7 results are the three best
paths of one lattice over the line, the ones MeCab returns. The first result
is the 1-best segmentation, and the output for input with a single sentence
does not change. The cost threshold (cost_threshold,
--nbest-cost-threshold) is now measured over the whole input: a result is
kept when its total cost is within the threshold of the first result's. v6
applied the threshold to each sentence separately. With unique, the
results still have distinct word boundaries.
An empty input now gets one result without tokens, with the connection
cost from BOS to EOS, as in MeCab and as an input of only whitespace
already did; v6 gave no result, because an empty input has no sentence.
This applies to segment_nbest, tokenize_nbest, their worker forms, the
bindings' N-best methods and lindera tokenize -N, which now writes a
result for an empty line.
Unknown words start at every position, as in MeCab
A word that is not in the dictionary is read as an unknown word, built from
the character categories of the dictionary's char.def (katakana, Latin
letters, digits, symbols and so on). Most categories are set to group: a
run of characters that share a category, such as a run of katakana, becomes
one candidate, a grouped unknown word. Up to v6, normal mode created no unknown
words at the positions inside the last grouped unknown word, a shortcut
inherited from Kuromoji that MeCab does not have. After a dictionary word
that ends inside such a run, no unknown word could continue the path, so the
grouped unknown word that swallowed the dictionary word was often the only
way through. v7.0.0 creates unknown-word candidates at every position that a
path reaches, in every mode, under MeCab's condition: the category that
creates them is set to always create them (INVOKE in char.def), or no
dictionary word starts at that position. Decompose mode already did this, and its output does not
change.
| Input (dictionary) | v6 | v7 (as MeCab) |
|---|---|---|
それが「⁂⁂第一だ (IPADIC) | 「⁂⁂, one unknown noun that swallows the dictionary symbol 「 | 「 (記号,括弧開) and ⁂⁂ |
ジョン・レノンが歌う (IPADIC) | ジョン・レノン, one unknown noun | ジョン, ・ and レノン |
ホテル・コルテシアに泊まる (UniDic) | ホテル・コルテシア, one unknown noun | ホテル, ・ and コルテシア |
"좋아." (ko-dic) | 아 (EC) and ." (SY) | 아 (EF), . (SF) and " (SY) |
- Japanese: IPADIC, IPADIC-NEologd and UniDic put
・in the katakana range of theirchar.def. After a dictionary word and・, an unknown katakana word could not start, soジョン・レノンbecame one unknown word. Now the unknown word starts after them, as in MeCab. A run that starts with an unknown word, such asレノン・ジョン, stays one unknown word, in MeCab too. In the same way, a dictionary symbol is no longer merged into the unknown symbols after it. - Korean: a sentence-final
.",?",!"or.'was one symbol (SY). Now.,?and!are sentence-final punctuation (SF), as in MeCab, and the ending before them can change with it, as in"좋아.". - Chinese: Jieba was not affected with the
mecab-jieba-0.1.2dictionary (not measured with0.1.3, see Jieba: costs retrained on UD Chinese GSD), and CC-CEDICT keeps Latin words and runs of kana in one word, as in v6. With the costs of the CC-CEDICT dictionary of v6 (CC-CEDICT-MeCab-0.1.0-20200409), the change would split them, as MeCab does with that dictionary: every unknown word costs −3,200 whatever its length, so a path with more words costs less, andT(an entry, −400) followed by the unknown wordhe(−3,200) beats the unknown wordThe. The shortcut hid this. v7.0.0 builds CC-CEDICT with new costs (see CC-CEDICT: unknown words no longer cost less than dictionary words). - SudachiDict output can change in the same kinds of runs.
- N-best: the first result changes as the 1-best segmentation does, and
the other results can now include splits such as
GandoogleforGoogle(UniDic).
With dictionaries built from the same sources, the number of lines that
Lindera segments and tags exactly as MeCab does rises, on top of the other
changes in this guide, from 589 to 596 of 600 Japanese sentences and from
438 to 459 of 505 paragraphs of Botchan with IPADIC, from 590 to 596 and
from 441 to 459 with IPADIC-NEologd, and from 877 to 1,498 of 1,500 Korean
sentences with ko-dic (with the left-space penalty off, as MeCab has none).
No line that matched before stops matching, and UniDic is unchanged on these
texts. On UD Japanese GSD with UniDic, 64 sentences change: 53 come closer
to the gold segmentation and part-of-speech tags, 10 move away (sign test
p = 3.4 × 10⁻⁸), and 1 is a tie. The 10 all match MeCab's output; GSD keeps
some names joined by ・ and some numbers as one word.
Text with many runs of katakana or Latin letters costs more, because every
position inside a run now gets candidates, as in MeCab. Against the same
release without this change, counting the speedups made with it, the
instruction count rises by 13% on UD Japanese GSD (Wikipedia text; 14% to
17% for N-best), and falls by 0.8% on Botchan and on Korean and by 14% to
16% in Decompose mode. A line of 10,240 katakana without a delimiter costs
3 to 5 times as much in 1-best and 3 to 15 times in N-best, and the peak
memory of lindera tokenize -N 3 on it grows from 12 to 56 MiB with IPADIC
and from 18 to 127 MiB with UniDic.
There is no setting to restore the v6 behavior. unknown_word_ladder(false)
(--disable-unknown-word-ladder) still turns off the shorter unknown-word
candidates, but no longer reproduces the output of Lindera before v6.
Unknown words come from the default category, as in MeCab
A character can have more than one category in char.def: the kanji
numerals are KANJINUMERIC KANJI in IPADIC and UniDic,
HANJANUMERIC HANJA in ko-dic and CHINESENUMERIC CHINESE in CC-CEDICT
and Jieba, and 〇 (U+3007) is SYMBOL plus the numeral category in all
of them but Jieba (see
Jieba: 〇 is a numeral entry). The first category of the line that
decides a character's categories is its default category. Up to v6.2.0,
Lindera created unknown-word candidates for every category of a character,
and went on with a grouped unknown word only while the next character had
the same category at the same position of its category list. v7.0.0
follows MeCab:
- Unknown words come from the default category only, with its
INVOKE,GROUPandLENGTHsettings and itsunk.defentries. - A grouped unknown word goes on while each character shares a category with the one before it.
- The shorter candidates of the length ladder go on while each character shares a category with the first one.
| Input (dictionary) | v6 | v7 (as MeCab) |
|---|---|---|
三國史記 (ko-dic) | 三 (SH), 國史 (NNG) and 記 (NNG) | 三國史記 (SH) |
十人十色 (ko-dic) | 十 (SH), 人 (NNG), 十 (SH) and 色 (NNG) | 十人十色 (SH) |
二十歲 (ko-dic) | 二十 (SH) and 歲 (SH) | 二十歲 (SH) |
〇〇〇 (ko-dic) | 〇〇〇 (SH) | 〇〇〇 (SY) |
- Korean: many ko-dic numerals, such as
三and十, are not dictionary words, and their default categoryHANJANUMERICgroups, so a numeral and the hanja after it become oneSHword, as in MeCab. 〇: its default category isSYMBOL, which creates unknown words even where a dictionary word starts, so it is a symbol with ko-dic (SY). CC-CEDICT has〇as an entry, which beats such a group with the costs of the v7.0.0 dictionary, so二〇二六年stays二,〇,二,六and年(see CC-CEDICT: unknown words no longer cost less than dictionary words). With Jieba, whose group ran on through the numerals and the Chinese characters after them,〇is now a numeral entry (see Jieba:〇is a numeral entry).- Japanese: IPADIC, IPADIC-NEologd and UniDic have the kanji numerals
and
〇as dictionary words, and no line of the texts compared changes. SudachiDict was not compared.
With dictionaries built from the same sources, the number of lines that Lindera segments and tags exactly as MeCab does rises, on top of the other changes in this guide, from 3,913 to 3,917 of the 3,919 non-empty lines of Sangnoksu with ko-dic (with the left-space penalty off, as MeCab has none); every generated sentence that combines the characters with several categories with other characters now matches MeCab with IPADIC, IPADIC-NEologd, UniDic, ko-dic, CC-CEDICT and Jieba. No line that matched before stops matching. The instruction count falls by 0.3% to 4.8%.
CharacterDefinition::lookup_categories (in
lindera_dictionary::dictionary::character_definition) returns the default
category first, then the other categories in category id order. The 7.0.0
char_def.bin stores them in this order, which is why the dictionary format
version changes (see
Dictionary directories must be rebuilt).
There is no setting to restore the v6 behavior.
CC-CEDICT: unknown words no longer cost less than dictionary words
v7.0.0 builds the CC-CEDICT dictionary from CC-CEDICT-MeCab-0.1.0-20261021
instead of CC-CEDICT-MeCab-0.1.0-20200409
(#1128). The CC-CEDICT
data (2020-04-09), char.def and the other entries are the same, and two
kinds of cost change:
- Every unknown word costs 400 instead of −3,200.
- The 20 entries made only of Latin letters (such as
T,PK,TA,cosandTony) and the 2 made only of digits (88and996) cost 399, one less than an unknown word, instead of −400 to −3,200.
CC-CEDICT has no connection costs, so a path costs the sum of the costs of
its words, and with the old costs a path with more unknown words cost less.
Unknown words start at every position
would then split Latin words and runs of kana: T (−400) and the unknown
word he (−3,200) beat the unknown word The (−3,200). With the new
costs, more unknown words cost more, and an entry made only of Latin
letters or of digits wins only for exactly its own characters (399 against
400), never as the start of a longer run (399 + 400 against 400). Latin
words, runs of kana and numbers stay one word, as in v6, and MeCab
segments them the same way with the new dictionary.
What changes from v6 is that dictionary words now beat the unknown words that cover the same characters:
| Input | v6 | v7 |
|---|---|---|
他穿着一件T恤。 | T and 恤 | T恤 |
我们去卡拉OK唱歌。 | 卡拉 and OK | 卡拉OK |
请把文件存到U盘里。 | U and 盘 | U盘 |
二〇〇八年北京奧運會 | 二, 〇〇 (unknown), 八, 年, 北京 and 奧運會 | 二, 〇, 〇, 八, 年, 北京 and 奧運會 |
两个队PK了一场。 | PK, an unknown word | PK, the entry (P K, "(slang) to take on/to challenge/…") |
- An entry that mixes Latin letters or digits with Chinese characters, such
as
T恤,B超,AA制,卡拉OK,阿Q,3C,U盘orX光, used to lose to unknown words over its Latin letters or digits. 〇and the entries made only of Latin letters or of digits come with their pinyin and definition, where an unknown word over the same characters used to win.
Against v7.0.0 with the 0.1.0-20200409 dictionary, 167 of the 4,997
sentences of UD Chinese GSD change: 163 come closer to the gold
segmentation and 4 move away (sign test p = 3.4 × 10⁻⁴³). The 4 are
CC-CEDICT words that GSD writes as two words (X光, T恤 and K書) or
splits at · (喬治·華盛頓). The simplified UD Chinese GSDSimp gives the
same counts. Lindera and MeCab agree on the same 4,951 GSD sentences with
either dictionary (the other 46 differ where Lindera cuts the sentence after
。 or 、), and on every one of 8,064 generated sentences around the
characters with several categories with the new one. The instruction count
on GSD falls by 0.1%.
The costs of paths change with the costs of unknown words, so N-best costs
and the cost threshold of N-best results are on a different scale. Entries
of a user dictionary keep their own costs (−10,000 for the simple format,
the default_word_cost of the metadata), so they still cost less than an
unknown word.
A CC-CEDICT dictionary directory built from the 0.1.0-20200409 archive
keeps the old costs (and has to be rebuilt anyway, see
Dictionary directories must be rebuilt):
build it from the 0.1.0-20261021 archive, as lindera-cc-cedict and the
7.0.0 release assets do. There is no setting to restore the old costs.
Jieba: 〇 is a numeral entry
v7.0.0 builds the Jieba dictionary from mecab-jieba-0.1.3 instead of
mecab-jieba-0.1.1. Since 0.1.2
(#1162), it reads 〇
(U+3007 IDEOGRAPHIC NUMBER ZERO) as a numeral, like 一 to 兆; 0.1.3
trains the costs again (see
Jieba: costs retrained on UD Chinese GSD):
- In
char.def,〇isCHINESENUMERIC CHINESE, the categories of the other numerals, instead ofSYMBOL CHINESENUMERIC. 〇is an entry like零: part of speechm, pinyinling2and definitionzero.
With the 0.1.1 dictionary, 〇 was an unknown word. Since
unknown words come from the default category,
its SYMBOL group went on through the numerals, which are CHINESE too,
and the Chinese characters after them, as in MeCab: 二〇〇八年北京奧運會
was 二 and 〇〇八年北京奧運會 (w). An entry alone would not stop the
group: SYMBOL makes unknown words even where an entry starts, and with
the connection costs of 0.1.1 one unknown word over 〇〇八年北京奧運會 costs
less than the path through 北京奧運 and 會, because entering or leaving
their context ID costs 32,767. With the numeral categories, which make no
unknown word where an entry starts, and the entry, no unknown word starts
at 〇:
| Input | v6 | v7 |
|---|---|---|
二〇〇八年北京奧運會 | 二, 〇〇 (unknown), 八, 年, 北京奧運 and 會 | 二, 〇, 〇, 八, 年, 北京, 奧運 and 會 |
编号〇〇七的特工 | 编号, 〇〇 (unknown), 七, 的 and 特工 | 编号, 〇, 〇, 七, 的 and 特工 |
第二〇章 | 第二 and 〇章 (unknown) | 第二, 〇 and 章 |
With 0.1.2, only text with 〇 changed: none of the 4,997 sentences of UD
Chinese GSD has 〇. Lindera and MeCab segment the same way with the
0.1.3 dictionary: 28 hand-written lines, 2,160 generated lines around the
numerals and 〇, the 8,064 generated sentences around the characters with
several categories, and 4,951 of the 4,997 GSD sentences, as many as with
the older dictionaries. As with the other numerals, a character that starts
no entry and the 〇 after it can make one unknown word, as in MeCab:
両〇頻 is 両〇 and 頻.
A Jieba dictionary directory built from the 0.1.1 archive keeps 〇 as a
symbol (and has to be rebuilt anyway, see
Dictionary directories must be rebuilt):
build it from the 0.1.3 archive, as lindera-jieba and the 7.0.0 release
assets do.
Jieba: costs retrained on UD Chinese GSD
v7.0.0 builds the Jieba dictionary from mecab-jieba-0.1.3, whose word and
connection costs are trained again
(#1250). Up to 0.1.2,
every connection into or out of context ID 2, which 52 of the 55 parts of
speech shared (362,085 of the 584,430 entries, such as every v, nr and
ns), cost 32,767. Three defects added up: the training corpus never made
a gold word a dictionary word, the trainer of lindera 2.3.3 put no
dictionary words in its lattices, and its export wrote 32,767 for a
connection without a trained weight. The best path was the one with the
fewest such connections, close to longest match, and the word costs had
never been trained against each other. 0.1.3 is trained with the
trainer of v7.0.0 on the training splits of UD Chinese GSD and of the same
sentences in simplified script (GSDSimp), with each gold word that is in
the dictionary given the features of a dictionary entry. Every part of
speech has its own context ID (a 56 × 56 matrix), and no connection costs
32,767.
Jieba now segments text as UD Chinese GSD does, which is finer than jieba
and than 0.1.2. Both columns are v7.0.0; MeCab segments every line the
same way with the same dictionary:
| Input | 0.1.2 | 0.1.3 (as MeCab) |
|---|---|---|
北京是中华人民共和国的首都。 | 北京 / 是 / 中华人民共和国 / 的 / 首都 / 。 | 北京 / 是 / 中华 / 人民 / 共和 / 国 / 的 / 首都 / 。 |
可以进行中文形态学分析。 | 可以 / 进行 / 中文 / 形态 / 学分 / 析 / 。 | 可以 / 进行 / 中文 / 形态 / 学 / 分析 / 。 |
这个问题很重要。 | 这个 / 问题 / 很重 / 要 / 。 | 这 / 个 / 问题 / 很 / 重要 / 。 |
武汉市解除离汉离鄂通道管控措施 | 武汉市 / 解除 / 离 / 汉 / 离 / 鄂 / 通道 / 管控 / 措施 | 武汉 / 市 / 解除 / 离 / 汉 / 离 / 鄂 / 通道 / 管控 / 措施 |
GSD's gold segmentation has 中華 / 人民 / 共和 / 國 (54 times) and
這 / 個 (69 times, never 這個). Segmentation F1 over word spans, with
models trained on the training splits only:
| Dictionary | GSD dev | GSD test | GSDSimp dev | GSDSimp test |
|---|---|---|---|---|
0.1.2 | 77.09 | 77.31 | 77.07 | 77.70 |
0.1.3 | 88.84 | 87.71 | 89.59 | 88.75 |
MeCab 0.996 with the same dictionaries gives the same F1 within 0.09, and segments 4,951 of the 4,997 GSD sentences as Lindera does, with either dictionary. The parts of speech are jieba's, as before.
A simple user dictionary row (a surface, a part of speech and a pinyin)
costs the default_word_cost of metadata.json. The trained word costs go
down to −25,149, so lindera-jieba sets it to −32768 instead of 1625:
with 1625, or with the −10000 of the other dictionaries, the user word
羽田机场 lost to 羽田 and 机场. Of user words made of two adjacent GSD
words that are not in the dictionary, one per sentence of the test splits,
−32768 keeps 99.4% as one word, −10000 53% to 58% and 1625 4%. A detailed
row gives its own context IDs and cost: the IDs of 0.1.2 (0 to 6) name
other parts of speech in 0.1.3, so take them from the left-id.def and
right-id.def of the 0.1.3 archive.
A Jieba dictionary directory built from an older archive has to be rebuilt
from mecab-jieba-0.1.3, as lindera-jieba and the 7.0.0 release assets
are. There is no setting to restore the old costs.
Dash and tilde spellings are kept as written
Japanese text spells the dash and the tilde in two ways each: the dash as
― (U+2015, HORIZONTAL BAR) or — (U+2014, EM DASH), the tilde as ~
(U+FF5E, FULLWIDTH TILDE) or 〜 (U+301C, WAVE DASH). Up to v6.2.0, the
dictionary builder rewrote ― to — and ~ to 〜 in every entry of
IPADIC and IPADIC-NEologd, whose metadata.json turned the rewrite on with
normalize_details. It rewrote the surface, the spelling under which the
segmenter finds the entry in the text, and the detail fields, such as the
base form and the reading. The text to segment was not rewritten, so an
entry that the dictionary source spells with ― or ~ matched only text
that spelled it with — or 〜. The rewrite was a workaround for an old
EUC-JP conversion. The dictionary sources are UTF-8 now, and ― is the dash
that the common Shift_JIS and EUC-JP converters produce and that Aozora
Bunko texts use. v7.0.0 keeps every entry as written in the dictionary's CSV
files, as MeCab does:
| Input (dictionary) | v6 | v7 (as MeCab) |
|---|---|---|
CD―ROM (IPADIC) | CD, ― and ROM | CD―ROM (名詞,一般) |
―― (IPADIC) | An unknown noun (名詞,サ変接続) | 記号,一般 |
あいいれなぁ~い (IPADIC-NEologd) | あいいれなぁ, ~ and い | あいいれなぁ~い (形容詞,自立) |
CD―ROM (IPADIC-NEologd) | CD, ― and ROM | CD―ROM (名詞,一般) |
CD—ROM (IPADIC) | CD—ROM (名詞,一般) | CD, — and ROM |
—— (IPADIC) | 記号,一般 | An unknown noun (名詞,サ変接続) |
The detail fields keep the CSV spelling as well: the entry CD―ROM
gives the base form CD―ROM, where v6 gave CD—ROM.
With dictionaries built from the same sources, the number of lines that Lindera segments and tags exactly as MeCab does rises, on top of the other changes in this guide, from 459 to 503 of 505 paragraphs of Botchan (from 2,690 to 2,744 of its 2,746 sentences) and from 3,377 to 3,581 of 3,587 paragraphs of Kokoro and I Am a Cat, two other novels from Aozora Bunko. The figures are the same with IPADIC and IPADIC-NEologd, and no line that matched before stops matching.
Text that writes these characters as — or 〜 no longer finds the entries
that it found only through the rewrite: 12 in IPADIC, all spelled with ―,
and 1,085 in IPADIC-NEologd, most of them spelled with ~. In the table
above, CD—ROM is split and —— is an unknown noun, as in MeCab.
IPADIC-NEologd has most of its words with a tilde in both spellings, so text
with either tilde still finds them.
To have both spellings find the entries, map the text to the dictionary's
spelling with the mapping character filter. IPADIC spells all its entries
with a dash with ― and all those with a tilde with 〜, so map — to ―
and ~ to 〜:
lindera tokenize --dict ipadic --char-filter 'mapping:{"mapping":{"—":"―","~":"〜"}}'
# Configuration file
character_filters:
- kind: mapping
args:
mapping:
"—": "―"
"~": "〜"
With this filter, CD—ROM is one word and —— is 記号,一般 again,
and text with ~ also finds IPADIC's entries with 〜, which it did not
in v6 either. The token's surface is the mapped text, such as CD―ROM;
its byte offsets point into the original text. Mapping the tilde the other
way, 〜 to ~, would hide IPADIC's entries with 〜. With
IPADIC-NEologd, a mapping trades words: some of its words exist in only one
spelling, 129 only with ― and 85 only with —, 957 only with ~ and
1,813 only with 〜, so mapping one spelling to the other makes text find
the words of one and miss those of the other. NFKC normalization
(the unicode_normalize character filter) does not fold these characters:
it turns ~ into ~ and leaves ―, — and 〜 as they are.
User dictionaries were never rewritten and do not change. The change is in
the dictionary builder. The embedded dictionaries and the dictionaries that
the 7.0.0 CLI fetches with lindera download keep the spellings; a
dictionary directory built or downloaded with v6.2.0 or earlier no longer
loads (see
Dictionary directories must be rebuilt).
UniDic, ko-dic, CC-CEDICT, Jieba and SudachiDict never turned the rewrite
on.
The normalize_details setting is removed
normalize_details turned this rewrite on and did nothing else, so v7.0.0
removes it from the bundled metadata.json files and from the APIs that
create or expose dictionary metadata:
| API | Change |
|---|---|
| Rust | Metadata::new takes 10 arguments instead of 11 (normalize_details was the ninth), and the Metadata::normalize_details field and the PrefixDictionaryBuilderOptions::normalize_details method are gone. For binding authors, lindera_binding::CoreMetadata and CoreMetadata::new change the same way. See Rust API changes in lindera_dictionary |
| Python | The normalize_details keyword argument of Metadata, the property and the to_dict() key are gone; passing the keyword raises TypeError |
| Ruby | Metadata.new takes 8 positional arguments instead of 9, so passing 9 raises ArgumentError; the normalize_details reader and the to_h key are gone |
| PHP | The last argument of the Metadata constructor, normalize_details, the property and the toArray() key are gone; passing the argument raises Error (by name) or ArgumentCountError (as a ninth argument) |
| Node.js | The normalizeDetails option of Metadata, the property and the toObject() key are gone; TypeScript rejects the option, and at run time it is ignored |
| WASM | No change: its Metadata never exposed the setting |
A metadata.json that still contains normalize_details, such as one
written by v6, loads as before, whether it comes with a built dictionary,
is passed to lindera build or is read by a binding; the key is ignored
and is not written back.
Tied entries resolve to the first CSV row, as in MeCab
A dictionary can have several entries with the same surface, the same left and right context IDs and the same cost, which differ only in their details, such as the reading or the base form. Such entries are tied: a path costs the same with any of them, so the order in which the segmenter considers them decides which one it outputs. Up to v6.2.0, Lindera added the dictionary entries that start at a position in reverse order, and the search keeps the earlier of two candidates with equal cost, so the last CSV row of a tied group won. The reverse order came in with v2.1.0, apparently by accident. v7.0.0 outputs the first row, as MeCab does:
| Input (dictionary) | v6 | v7 (as MeCab) |
|---|---|---|
あいつは狡い (IPADIC) | 狡い reads コスイ | 狡い reads ズルイ |
IPADIC's Adj.csv has the two rows in this order:
狡い,19,19,4682,形容詞,自立,*,*,形容詞・アウオ段,基本形,狡い,ズルイ,ズルイ
狡い,19,19,4682,形容詞,自立,*,*,形容詞・アウオ段,基本形,狡い,コスイ,コスイ
Tied entries exist for 5,637 surfaces in IPADIC, 90,991 in IPADIC-NEologd,
3,324 in UniDic and 175 in ko-dic. In the Japanese dictionaries, the rows of
a tied group never differ in the part of speech, only in fields such as the
reading and the base form; in ko-dic, 5 groups also differ in the part of
speech. With each tied surface on its own line, the number of surfaces whose
full details equal those of MeCab 0.996 rises from 1,607 to 5,637 of 5,637
with IPADIC, from 524 to 3,324 of 3,324 with UniDic, and from 8,205 to
86,319 of 86,464 with IPADIC-NEologd (surfaces with whitespace or control
characters left out). The other 145 IPADIC-NEologd surfaces contain 、,
which no word spans (see
The context is carried across 、 and 。),
or belong to groups whose rows MeCab reads in another order (see below).
With ko-dic, all 175 agree when MeCab's dictionary is built from the CSV
files in name order.
On running text, the number of lines whose full details equal MeCab's rises,
on top of the other changes in this guide, from 7,989 to 8,096 of the 8,100
sentences of UD Japanese GSD and from 3,278 to 3,583 of the 3,587 paragraphs
of Kokoro and I Am a Cat with IPADIC, and from 3,394 to 3,571 of the
same 3,587 paragraphs with UniDic. No line is segmented differently. In all
the texts compared, 15 tokens change their part of speech, all to MeCab's.
Most of them are entries of the same surface with different context IDs and
costs whose paths happen to cost the same in that sentence, and the same
order decides between them: UniDic, for example, now reads 御前 as a
pronoun (代名詞) instead of a common noun (名詞,普通名詞,一般), as
MeCab does.
User dictionaries follow the same rule: among tied user entries, the first
row is now chosen instead of the last. A user entry that ties with a system
entry is still chosen over it, as in v6. This is the one intended difference
from MeCab, which chooses the system entry. Such a tie needs a user entry in
the detailed format that copies the context IDs and the cost of a system
entry, for example to give a word another reading. An entry in the simple
three-column format gets the cost and the context IDs of the dictionary's
metadata.json (default_word_cost, default_left_context_id and
default_right_context_id: -10000, 0 and 0 for IPADIC), so it
practically never ties with a system entry.
N-best search uses the same order. In addition, the first N-best result is
now always the 1-best segmentation, the output of segment; up to v6 it
could be another path when several paths had exactly the same cost. The
costs of the N-best results do not change: only results with the same cost
can change places, which can also change which of them is cut off at the
n-th result. N-best search takes about 3% to 4% more instructions
(-N 3); the speed of the 1-best segmentation does not change (within
0.2%).
With unique (--nbest-unique), every result is the cheapest path of its
word boundaries, and of tied entries it now holds the first CSV row in
every result, as the 1-best segmentation does. Up to v6, unique N-best went
through the paths in order of cost and kept the first path of each
segmentation, which could hold either row of a tied pair, and it went
through every path of a segmentation
before it reached the next one: a short line whose words have many entries
could take tens of seconds and gigabytes. The search now goes over word
boundaries, so it does not depend on the number of paths. Again, only
results with the same cost can change.
MeCab's mecab-dict-index reads the CSV files in the order the file system
lists them (readdir order), so for tied groups whose rows are in different
files (in IPADIC-NEologd and ko-dic), MeCab's choice depends on the file
system, while Lindera reads the files in name order.
The change is in the segmenter, not in the dictionary files. There is no setting to restore the v6 behavior.
Equal-cost segmentations keep the later-starting word, as in MeCab
Two segmentations can reach the same position with exactly the same cost while their last words start at different positions. MeCab keeps the one whose last word starts later: it puts every word at the head of the list of words that end where it ends, so the words that start later come first in that list, and of words of equal cost it keeps the first. Up to v6.2.0, Lindera kept the one whose last word starts earlier. v7.0.0 chooses as MeCab does:
| Input (dictionary) | v6 | v7 (as MeCab) |
|---|---|---|
腸窒扶斯 (IPADIC) | 腸 / 窒 / 扶斯 | 腸 / 窒扶 / 斯 |
にフリーホイールダイオードや (IPADIC) | フリーホイールダイオード (one unknown word) | フリー / ホイール / ダイオード |
家系譜 (UniDic) | 家 / 系譜 | 家系 / 譜 |
차나 마셔 (ko-dic) | 차나 / 마셔 | 차 / 나 / 마셔 |
電視台上班 (CC-CEDICT) | 電視 / 台 / 上班 | 電視 / 台上 / 班 |
二十二年 (Jieba) | 二 / 十二 / 年 | 二十 / 二 / 年 |
Both segmentations of 腸窒扶斯 cost 28,382. With the Japanese and Korean
dictionaries, such ties are rare. The texts compared are Botchan (as one
file, as paragraphs and as sentences), the 3,587 paragraphs of Kokoro and
I Am a Cat and the 8,100 sentences of UD Japanese GSD, each with IPADIC,
IPADIC-NEologd and UniDic, and the 7,759 lines of Sangnoksu with ko-dic:
about 54,000 lines in all. 9 of them change, all to MeCab's segmentation,
and none moves away from it. In Decompose mode, which MeCab does not have,
0 to 2 lines per text change in the same way.
With CC-CEDICT, ties are common, because its costs take few values.
CC-CEDICT has no connection costs, and an entry of len
characters costs max(-36000, -400 * len ** 1.5) (except the 22 entries
made only of Latin letters or only of digits, see
CC-CEDICT: unknown words no longer cost less than dictionary words),
so two segmentations into entries of the same lengths cost the same, such
as 電視 / 台 / 上班 and 電視 / 台上 / 班. Jieba's costs are trained
(see
Jieba: costs retrained on UD Chinese GSD),
so ties are rare with it; with the mecab-jieba-0.1.2 dictionary, whose
584,430 entries had 12 distinct costs and 4 context IDs, 1,186 sentences
changed. Compared with v7.0.0 changed to keep the earlier-starting word on
ties, about a quarter of the 4,997 sentences of UD Chinese GSD change with
CC-CEDICT:
| Dictionary | Sentences that change | Closer to / farther from the gold segmentation | Sentences segmented as MeCab, before → after |
|---|---|---|---|
| CC-CEDICT | 1,267 | 634 / 455 (sign test p = 6.5 × 10⁻⁸) | 3,694 → 4,951 |
| Jieba | 3 | 1 / 1 | 4,948 → 4,951 |
The other sentences that change come neither closer to the gold segmentation nor farther from it, and segmentation F1 rises from 77.94 to 78.30 with CC-CEDICT and stays 88.79 with Jieba. Every sentence that changes costs the same either way.
When whitespace is skipped (see Whitespace is skipped in the lattice), the word that ends later wins first, as in MeCab, which looks the next word up from every position a word ends at: an entry that ends with the space wins over a word that ends before it. One difference from MeCab remains in theory: after skipped whitespace, MeCab keeps a node for every position the previous word can end at, while Lindera keeps one, so two words that start right after the same whitespace and tie through previous words that end at different positions can be chosen differently. This needs an entry that ends with whitespace and an exact tie, and it does not occur in the texts compared.
Among words that start at the same position, the order of
Tied entries resolve to the first CSV row, as in MeCab
still applies, and the first N-best result is still the 1-best
segmentation. When the context is carried across 、 or 。, a tie
between the words that end the previous sentence resolves the same way, as
in one lattice over the line: the word that starts later wins (see
The context is carried across 、 and 。).
The 1-best segmentation takes 0.6% to 0.9% more instructions with IPADIC,
IPADIC-NEologd and UniDic, with no measurable change in time, and about 2%
more instructions and 2% to 5% more time with ko-dic. N-best search
(-N 3) takes about 1% more instructions.
The change is in the segmenter, not in the dictionary files. There is no setting to restore the v6 behavior.
lindera tokenize removes only the line terminator and writes every line
lindera tokenize reads its input line by line. Up to v6.2.0, it removed
all the whitespace at both ends of each line before tokenizing it: spaces,
tabs, U+3000, U+00A0 and the other Unicode whitespace (str::trim). The
library, the language bindings and MeCab do not. v7.0.0 removes only the
line terminator (\n, or \r\n) and tokenizes the rest of the line as the
library does:
| Input line (IPADIC) | v6 | v7 (as MeCab) |
|---|---|---|
Two spaces, 東京, a space | 東京 at bytes 0-6 | 東京 at bytes 2-8 |
U+3000, 東京 | 東京 at 0-6 | U+3000 (記号,空白) at 0-3, 東京 at 3-9 |
東京, U+3000 | 東京 at 0-6 | 東京 at 0-6, U+3000 (記号,空白) at 6-9 |
U+00A0, 東京, U+00A0 | 東京 at 0-6 | U+00A0 (unknown, 記号,一般) at 0-2, 東京 at 2-8, U+00A0 at 8-10 |
- Spaces and tabs are
SPACEcharacters, which the segmenter skips and drops from the output, so they still give no tokens; only the byte offsets of the tokens after them change, and now index the input line, as the library's offsets do. - U+3000 at the start or end of a line is a token, as in the middle of a
line, with IPADIC, IPADIC-NEologd (
記号,空白) and UniDic (空白) (see Dictionary entries with whitespace are kept). In the 538 lines of Botchan as one file, where 202 lines start with an indent of U+3000, 206 such tokens appear and no other token changes. Text without whitespace at the ends of its lines, such as the 8,100 sentences of UD Japanese GSD and the 3,587 paragraphs of Kokoro and I Am a Cat, gives the same output as before. - With
--keep-whitespace, the spaces at the start and end of a line are tokens too: two spaces,東京and a space give three tokens. - With
-N, a line of spaces or tabs and an empty line give one result without tokens (NBEST 1andEOS), as in MeCab, instead of no result (see N-best results cover the whole input for the empty input). The costs of a line with U+3000 at an end include that token. - The wakati format writes a line for every input line, or for every
result with
-N: a line without tokens, such as an empty line or a line of spaces, gives an empty line, as in MeCab. v6 wrote nothing for it, so the output had fewer lines than the input (Botchan as one file: 505 output lines for its 538 input lines, 33 of them empty). - The
\rof a line that ends with\r\nis removed, and only that one\r. MeCab removes only the\nand outputs the\ras an unknown word; Lindera removes it so that text with Windows line endings does not end every line with a\rtoken.
The library and the language bindings never trimmed their input, so they do
not change, except for the N-best result of an empty input. The change is
in the CLI, not in the dictionary files. No option restores the v6 behavior; to get the v6 offsets and tokens,
remove the whitespace at the ends of each line before passing the text to
lindera tokenize.
WASM: the builder settings apply whichever way the dictionary is set
Up to v6.2.0, TokenizerBuilder.build() in the WASM binding ignored part of
the builder's configuration, depending on how the dictionary was set:
| Dictionary set with | Ignored up to v6.2.0 |
|---|---|
setDictionaryInstance(), such as a dictionary loaded from OPFS with loadDictionaryFromBytes() | setKeepWhitespace(), appendCharacterFilter() and appendTokenFilter() |
setDictionary(), such as an embedded:// dictionary | The user dictionary set with setUserDictionaryInstance() |
v7.0.0 applies every setting and the user dictionary either way, as the
Tokenizer API page describes. For example,
with IPADIC loaded from bytes, setKeepWhitespace(true) and the
unicode_normalize (NFKC) and lowercase filters turn Lindera 東京
into lindera, a space and 東京; v6 gave Lindera and 東京.
With a dictionary instance, build() now also checks what it checks for a
dictionary set by URI: unless whitespace is kept, the dictionary's
char.def must define the SPACE category. Every bundled dictionary does;
a custom dictionary without it now needs setKeepWhitespace(true).
The other bindings, the CLI and the library do not change, and neither do the dictionary files. No option restores the v6 behavior; to get it, leave out the settings that v6 ignored.
PHP: optional arguments reject a value of the wrong type
Up to v6.2.0, the PHP binding read a value of the wrong type for an
optional parameter as null, so the call silently used the default, with
or without strict_types. For example,
$tokenizer->tokenizeNbest('東京都', 2, 'yes') returned results that were
not deduplicated, and Lindera\Trainer::train(..., max_iter: $argv[1])
trained for the default 100 iterations whatever the command line said. A
required parameter already rejected such a value.
v7.0.0 rejects it for the optional parameters too: those of the Penalty,
Metadata, FieldDefinition and Tokenizer constructors, of
Mode::decompose(), Tokenizer::tokenizeNbest(), Trainer::train(),
Trainer::export() and Dictionary::buildUser(). The call throws a TypeError,
Invalid value given for argument followed by the parameter's name. A
required parameter now throws the same TypeError for such a value; v6
threw an Exception with the same message. null and leaving the argument
out still select the default. $lambda of Trainer::train() now takes an
int as well as a float, as a PHP float parameter does; v6 read an
int there as null too.
Pass each argument its declared type, and cast values that arrive as
strings, such as (int) $argv[1] (see
Argument types). Code that catches such a
value with catch (Exception $e) must catch TypeError instead: a
TypeError is an Error, not an Exception. The parameters declared
mixed, the other bindings, the CLI and the library do not change. No
option restores the v6 behavior.
Bindings: integer arguments outside their range are rejected
Up to v6.2.0, the PHP, Node.js and WASM bindings converted an integer
argument to the type of its setting by wrapping, so a value outside the
range silently became another value. The PHP binding took every int as a
64-bit integer and cast it to the narrower type. In Node.js and WASM, the
conversion from a JS number to a 32-bit integer (ECMAScript's ToUint32 or
ToInt32) wrapped it before the binding saw it, and a WASM BigInt was taken
modulo 2^64.
| Call | v6.2.0 | v7.0.0 |
|---|---|---|
PHP new Lindera\Metadata(default_word_cost: 100000) | cost -31072 | ValueError |
Node.js new Metadata({ defaultLeftContextId: 2 ** 32 + 5 }) | context ID 5 | Error with the code InvalidArg |
Node.js schema.getFieldName(2 ** 32) | "surface", the first field | null |
WASM new Penalty(-1) | threshold 4294967295 | a thrown string |
tokenizeNbest('東京', -1) in PHP, Node.js and WASM | all 33 segmentations | the binding's error |
v7.0.0 rejects such a value. The PHP binding throws a ValueError, the
Node.js binding an Error with the code InvalidArg, and the WASM binding a
string, as for its other errors. The message names the parameter and the
range, such as
default_word_cost must be an integer between -32768 and 32767, got 100000.
The parameters are the integer settings of Metadata (the default word cost
and context IDs) and Penalty, the index of FieldDefinition,
getFieldName and PHP's Token::getDetail, the number of N-best results
and, in Node.js and WASM, the N-best cost threshold, and the iteration and
thread counts of the training functions. The checks apply to the
constructors and to the property setters (Node.js Metadata, WASM Penalty
and FieldDefinition).
With them, some values that v6 accepted now throw:
- A negative index, for which v6 returned
null(PHP, Node.js) orundefined(WASM). An index past the end still returnsnullorundefined. - In Node.js and WASM, a number that is not an integer, such as
1.5,NaNorInfinity, which v6 truncated or turned into 0. - In WASM, a BigInt cost threshold outside the 64-bit range, and a cost threshold that is not a BigInt.
Pass an integer in the range. The TypeScript declarations of the Node.js
and WASM bindings do not change. The Python and Ruby bindings already rejected
these values, with OverflowError and RangeError, and still do. The
RangeError of the Ruby binding now has the message above, which names the
parameter and the range, where v6 named only the Rust type
(fixnum too big to convert into `i16` ); this covers the N-best cost
threshold too, for an Integer outside the 64-bit range. The
OverflowError of a Python property setter now names the property in its
note (while processing 'default_word_cost' instead of 'cost'). No option
restores the v6 behavior.
Bindings: a mode is a Mode
Up to v6.2.0, every binding exported a Mode, but no function or
constructor took one: set_mode / setMode and the Tokenizer
constructors took a mode name, and decompose mode always used the default
penalty. Node.js exported a string enum JsMode, whose values 'Normal'
and 'Decompose' setMode rejected, and WASM a numeric enum. A custom
penalty reached the segmenter only through a configuration file loaded with
from_file / fromFile. In Rust, TokenizerBuilder::set_segmenter_mode
lost the penalty of a Mode::Decompose the same way, because it wrote only
the name of the mode into the configuration.
In v7.0.0, the Mode of each binding is a copy of the Rust
lindera::mode::Mode: normal mode, or decompose mode with a Penalty.
set_mode / setMode and the Tokenizer constructors take a Mode, as
set_segmenter_mode and Segmenter::new do in Rust, and no longer take a
mode name. Leaving the mode of a constructor out, or passing null /
None / nil, still means normal mode, and set_segmenter_mode keeps the
penalty:
| Binding | v6 | v7 |
|---|---|---|
| Python | builder.set_mode("decompose") | builder.set_mode(Mode.decompose(penalty)) |
| Ruby | builder.set_mode('decompose') | builder.set_mode(Lindera::Mode.decompose(penalty)) |
| PHP | $builder->setMode('decompose') | $builder->setMode(Lindera\Mode::decompose($penalty)) |
| Node.js | builder.setMode("decompose") | builder.setMode(Mode.decompose(penalty)) |
| WASM | builder.setMode("decompose") | builder.setMode(Mode.decompose(penalty)) |
The Tokenizer constructors take the same Mode as their second argument,
for example Tokenizer(dictionary, Mode.normal()) in Python. The penalty
is optional: Mode.decompose() uses the default one. With IPADIC and both
length penalties 0, decompose mode segments 関西国際空港限定トートバッグ
as 関西国際空港 / 限定 / トートバッグ instead of
関西 / 国際 / 空港 / 限定 / トートバッグ.
A mode name read at run time, such as a setting, becomes a Mode with
Mode.from_str(name) (Mode.fromStr in PHP, Node.js and WASM). It parses
the name as the Rust Mode does: "normal" or "decompose", case
sensitive, the latter with the default penalty; another name throws the
error that set_mode threw for it in v6. A mode name, or any other value
that is not a Mode, passed to set_mode / setMode or a constructor now
throws a type error: TypeError in Python, Ruby and PHP, an InvalidArg
error in Node.js and expected instance of Mode in WASM.
The other changes:
- The
Modeclasses: aModehas no constructor;Mode.normal(),Mode.decompose()andMode.from_str()create one. It has aname, apenalty(a copy, or none for normal mode), the normal and decompose checks, and equality of the name and the penalty (==, orequals()in Node.js and WASM). Python'sMode(mode_str)and its attributesMode.Normal/Mode.Decompose, Ruby'sLindera::Mode.new, PHP'snew Lindera\Mode(), Node.js'sJsModeand WASM'sMode.Normal/Mode.Decomposeare gone. The Python and Ruby constructors accepted"Normal"and"Decompose", and PHP's any case;from_strdoes not. A PythonModeis not hashable, as the RustModeis notHash. - Node.js: the TypeScript type
JsPenaltyis renamedPenalty, and its fields become optional; a field left out keeps its default. TypeScript code that namesJsPenaltyusesPenalty. The package exports no value for the type, so JavaScript code does not change. - Rust:
TokenizerBuilder::set_segmenter_modewith aMode::Decomposewhose penalty is not the default now applies that penalty, so the output changes. A mode parsed from"decompose", such as the one the CLI's--modegives, carries the default penalty and is not affected. - Binding authors:
CoreTokenizerBuilder::set_mode,CoreTokenizer::from_segmenterandCoreTokenizer::from_segmenter_with_space_penaltytake alindera::mode::Modeinstead of the&strthat v6.2.0'slindera-binding-coretook.set_modeno longer returns aResult.lindera_binding::parse_modeparses a mode name, as the bindings'from_strdo.
lindera train builds its lattices as MeCab's learner does
Up to v6.2.0, lindera train built training lattices that differ from
those of MeCab's learner:
- The seed lexicon was not in the lattice, so a gold token competed only with unknown words, never with the other seed words.
- The unknown-word candidates ignored
char.def. A fixed classifier (ASCII digit, ASCII letter, kanji, katakana, hiragana, other) gave the class of the first character, every candidate was one to three characters long, and the class number served as theunk.defentry, so only anunk.defwith one entry per class, in the orderDEFAULT,HIRAGANA,KATAKANA,KANJI,ALPHA,NUMERIC, worked. - Each
char.defcategory got one label, with the feature string of its lastunk.defline, andlindera exportwrote oneunk.defline per category. mecab-ipadic'sunk.defhas 40 lines for 11 categories; they became 11 lines, and the unknownALPHA,HIRAGANAandKATAKANAwords all became感動詞, the feature of the last line of each of these categories. - A gold token that was not a seed word got an unknown-word label through
a check written for IPADIC features (
名詞,一般and the like), or a label without features. Anunk.defline with fewer than five fields was silently skipped. - When regularization left no bigram weight for some context IDs,
lindera exportwrote amatrix.defsmaller than the IDs of the entries (0 0when no weight was left at all), and the built dictionary panicked at tokenization.model_info.connection_matrix_sizeinmetadata.jsonwas a square of the larger ID. lindera exportwrote32767(i16::MAX) tomatrix.deffor a connection without a trained weight, whose bigram features never met in training or whose weights regularization zeroed. Training gives such a connection the cost 0, and so does MeCab'smecab-dict-gen, so the built dictionary avoided transitions that the model had learned to be neutral. In a five-fold cross-validation with mecab-ipadic's files and the default L1 regularization, writing 0 raised the boundary F1 from 0.9552 to 0.9572 in every fold (#1168).model_info.training_iterationsinmetadata.jsonwas the--max-iterationsvalue, not the number of iterations that ran.- The export functions of the Python, Node.js, Ruby and PHP bindings wrote
lex.csv,matrix.def,unk.def,metadata.jsonand a fixedchar.defwith the categoriesDEFAULT,HIRAGANA,KATAKANA,KANJI,ALPHAandNUMERIC, not thechar.defthe model was trained with, and nofeature.def,rewrite.def,left-id.deforright-id.def. - The seed lexicon was read line by line and split on
,, and a line that starts with#was skipped as a comment. mecab-ko-dic's quoted comma surface","became the surface", andlindera exportwrote it unquoted, solindera buildread the rest oflex.csvas one field and dropped every row after it without an error. The#rows of UniDic, SudachiDict and IPADIC NEologd were not trained. - A feature was split on
,for the rewrite rules, the feature templates, unknown-word matching and the context IDs of user lexicon entries. A quoted column with a comma in it became several columns and shifted the columns after it: in 19,955 rows of mecab-jieba's training seed, whose quoted English definition is followed by four more columns,%F[6]to%F[9]were pieces of the definition. - A
%F[n],%L[n]or%R[n]without?that read past the last column of a feature read*there, so the template still gave a feature, such asU09:*, which every feature with fewer columns shared. MeCab's learner skips the template. Theunk.deffeatures of mecab-jieba have five columns, and itsfeature.defreads%F[6]to%F[9], so its 11 unknown-word labels had four features,U06:*toU09:*, that no seed row had. Model::write_dictionarygave an entry of the user lexicon thatModel::read_user_lexiconreads whose context IDs and cost are0,0,0the context IDs of another user lexicon entry: the one at the index of the seed row whose feature is the most similar to the entry's, or else the first one. An entry whose feature had no column in common with any seed row got the index of its first character's category inchar.defas both IDs, which is not a context ID.Model::write_dictionarygave every such entry with0,0,0the same cost,tocost(0.8, cost_factor), whatever its feature.Model::read_user_lexiconread an empty context ID or cost, and a value that is not a number, as0, and a negative context ID wrapped around to 16 bits (-1became65535); the parameters were filled in only when all three were0. So an entry with an empty cost and given context IDs got the cost0, and one with empty context IDs and a given cost got the BOS/EOS IDs0,0.
v7.0.0 trains as MeCab's learner does (see How training works):
- The lattice has the seed words that start at each position, and the
unknown words that the runtime creates there, with the same code: those
of the character's default category, with its
INVOKE,GROUPandLENGTHsettings, for everyunk.defentry of the category. - Every
unk.defline is an entry and a CRF label of its own, with the ID of its category as%t, andlindera exportwrites every line, in file order. Comment lines and empty lines are skipped, and a line with fewer than five fields is an error. - A gold token gets the label of the first seed row with the same span and
the same whole feature string; else that of the first
unk.defentry of an unknown-word candidate with the same span whose feature columns other than*all equal the gold token's; else a virtual label, which is trained and not exported, as MeCab's learner adds a virtual node.lindera trainprints the counts in aLattice statistics:line. matrix.defcovers every context ID, plus 0 for BOS/EOS, andmodel_info.connection_matrix_sizegives its size, rows by columns.- A connection without a trained weight costs 0 in
matrix.def, as in training and in MeCab. model_info.training_iterationsis the number of L-BFGS iterations that ran:--max-iterationswhen that stopped training, fewer when it converged earlier.- The export functions of the Python, Node.js, Ruby and PHP bindings write
the files
lindera exportwrites, with the same code: thechar.def,feature.defandrewrite.defthe model was trained with, andleft-id.defandright-id.deftoo. model.dathas the format version 2.0.0.lindera exportand the bindings' export functions reject amodel.datof any other version, including everymodel.datwritten by v6.2.0 or earlier, with an error that ends in "re-runlindera trainto regenerate it".- The seed lexicon is read as CSV, as
lindera buildreadslex.csv(see Seed Lexicon): a field may be quoted, a line that starts with#is a row, and the feature columns are kept as written. A quoted field that spans lines is an error.lindera exportquotes a surface that contains,,"or a line break. The user lexicon thatModel::read_user_lexiconreads is read the same way. - A feature is split into columns as MeCab's learner splits it (see
Seed Lexicon):
a quoted column is one column, spaces and tabs before a column are
skipped, and the text from a closing quote to the next
,is dropped. The rewrite rules join the columns they rewrite with,, quoting a column that contains,or", as MeCab does. - A template that reads past the last column of a feature gives no feature
for it, with or without
?, as in MeCab's learner (see Feature Template). In the training files of mecab-ipadic, mecab-ipadic-neologd, unidic-mecab, mecab-ko-dic, mecab-jieba andresources/training/, only theunk.deflines of mecab-jieba have features that a template reads past the last column of, so only a model trained on mecab-jieba's files changes. Model::write_dictionaryfills in the parameters of a user lexicon entry separately, asmecab-dict-indexdoes: an empty cost is computed, and when either context ID is empty or negative, both are inferred.0,0,0still fills in all three. A context ID or cost that is neither empty nor a number in its range is an error that names the entry.- A computed cost is the one MeCab's
calcCostcomputes: the sum of the trained weights of the entry's unigram features, converted as the costs oflex.csvare, so an entry whose feature and first character's category are those of a seed row gets that row's cost. - Inferred context IDs are those that
lex.csvgives the seed row whose feature is the most similar to the entry's. An entry whose feature has no column in common with any seed row is an error, and nothing is written, asmecab-dict-indexfails for a user dictionary entry whose feature has no context ID.
Training takes longer, because every lattice now has the seed words. It is
slower than MeCab's learner, and it can stop at --max-iterations
before it converges
(#1167). The files in
resources/training/ now define a SPACE category in char.def and a
SPACE line in unk.def, so a dictionary trained from them tokenizes
without --keep-whitespace, which it needed in v6
(#1166).
What you do: train every model again with the 7.0.0 lindera train or the
bindings' training functions, then export it and build the dictionary
again. Give every unk.def line at least five fields. An unk.def with
several entries per category, such as mecab-ipadic's, is now trained and
exported as it is. Remove the comment lines from your seed lexicons: a line
that starts with # is now a row. Give the context IDs of a user lexicon
entry whose feature has no column in common with any seed row. Write each
context ID and cost of a user lexicon entry as a number, or leave it empty
to have it filled in. There is no setting to restore the v6 behavior.
lindera build rejects context IDs outside the connection matrix
Up to v6.2.0, lindera build accepted a lex.csv row or an unk.def line
whose context IDs fall outside matrix.def, and the built dictionary
panicked when such a word was looked up during tokenization. A matrix.def
row whose ID exceeded its axis was accepted too, and its cost landed in
another cell. v7.0.0 rejects all of these, as MeCab's mecab-dict-index
does: in every lex.csv row and unk.def line, the right context ID must
be below the first value of the matrix.def header, and the left context
ID below the second. The error names the file, the line, the ID and the
matrix size:
Error: LinderaError { kind: Content, source: ./dict-source/lex.csv: line 2: right context id 2 is out of range for the connection matrix (matrix.def size 2 2; right context ids must be below 2) }
- A dictionary whose
metadata.jsonsetsskip_invalid_cost_or_iddrops such alex.csvrow instead, with a warning, which the CLI writes to standard error (see The CLI writes library warnings to standard error).unk.deflines are always checked. - An
unk.defline with fewer than four fields, such as a comment or an empty line, is an error instead of a panic, also inDictionaryBuilder::build_unknown_dictionaryandparse_unkoflindera_dictionary. The error names the line, and so does the error for a context ID or a cost that is not a number, which did not name it before. - A user dictionary is checked when it is combined with the system
dictionary:
Segmenter::try_new, the configuration path,lindera tokenize --user-dict, and theTokenizerconstructors and builders of the language bindings return an error that names the first entry outside the matrix (#1171; see Context IDs).Segmenter::newdoes not check, as before.lindera build --userchecks the rows already when the new--system-dictoption names the system dictionary (DictionaryBuilder::with_system_matrixin the Rust API), and names the first row outside the matrix by its CSV line and surface (#1182). A.csvuser dictionary that the configuration path,lindera tokenize --user-dictor the bindings'TokenizerBuilderloads by path, or that the newload_user_dictionary_forof the Rust API loads, is checked the same way as it is built for the system dictionary, and its error names the CSV line and surface too (#1187). The language bindings have such a loader for a loaded system dictionary too:load_user_dictionary_forin Python and Ruby,loadUserDictionaryForin Node.js,Lindera\Dictionary::loadUserForin PHP andloadUserDictionaryFromBytesForin WebAssembly (#1193). The loaders that take metadata are unchanged. The user dictionary builders of these bindings have such a variant as well, which checks the rows against a loaded system dictionary as it builds the CSV:build_user_dictionary_forin Python and Ruby,buildUserDictionaryForin Node.js andLindera\Dictionary::buildUserForin PHP (#1188). The builders that take metadata are unchanged too: theiroutput_diris, as before, the path of the file they write, not a directory. - The errors for a user dictionary row whose cost or context ID is not a
number, from
lindera build --useror from loading a.csvuser dictionary, name the row's CSV line (at line 1). Before, they counted the rows in the order of their surfaces (at row 2for a row on line 1 whose surface sorts second).
Dictionaries that mecab-dict-index builds pass, since it checks the same
bounds. A dictionary exported by lindera export of v6.2.0 or earlier can
fail, since its matrix.def could be smaller than its context IDs; train
and export it again with 7.0.0 (see the previous section). For another
dictionary that fails, fix the IDs in lex.csv or unk.def or the size in
matrix.def.
lindera build and lindera train check char.def against unk.def
Up to v6.2.0, lindera build and lindera train accepted a char.def and
an unk.def that MeCab's mecab-dict-index rejects
(#1195):
- A range line of
char.defthat names a category no line defines. The built dictionary panicked on a character of the category, and so did training when the corpus had one. - A range line before the definition of its category, or a category
defined twice. The categories defined after it got the
INVOKE,GROUPandLENGTHsettings of another category, which changed the unknown words. - A
char.defcategory without anunk.defline. A character of it that no word covers got no unknown word, so tokenization stopped its output before it, without an error, and a training lattice had no path through it. - An
unk.defline whose categorychar.defdoes not define. The line was never used, andlindera exportwrote it back.
v7.0.0 rejects all of these, as mecab-dict-index does. The error names
the file, the category and, except for a category without an unk.def
line, the line. For example, with 0x3005 KANJI_ITERATION on line 8 of
char.def, whose category no line defines:
Error: LinderaError { kind: Content, source: ./dict-source/char.def: line 8: category KANJI_ITERATION is not defined; a range line can only name a category defined on an earlier line }
lindera train and the bindings' training functions check their input in
the same way before they train, and number the unk.def lines as in the
file, comment and empty lines included.
Every bundled dictionary passes, and so does a dictionary that
mecab-dict-index builds. For another dictionary that fails, define each
category once, before the range lines that name it, give every category at
least one unk.def line, and remove the unk.def lines of categories that
char.def does not define.
lindera build gives a cell missing from matrix.def the cost 0
Up to v6.2.0, lindera build gave a cell that matrix.def does not list
the cost 32767 (i16::MAX), which made the connection very expensive.
MeCab's mecab-dict-index gives such a cell the cost 0, and v7.0.0 does the
same (#1211; see
Cells missing from matrix.def).
The matrix.def of every bundled dictionary lists every cell, and so does
the matrix.def that lindera export writes, so neither changes. A
dictionary built from a matrix.def that leaves cells out can segment
differently, since a connection through such a cell now costs 0. To keep a
connection expensive, list its cell with a cost.
The CLI writes library warnings to standard error
Up to v6.2.0, the lindera CLI installed no logger, so the warnings that
the Lindera libraries report through the log crate were dropped
(#1107). v7.0.0 writes
them to standard error, warnings and errors by default. Standard output does
not change. For example:
lindera tokenizewarns when it cuts a sentence because more than 32 KiB of text have no sentence delimiter.lindera buildwarns when it drops alex.csvrow because ofskip_invalid_cost_or_id, and when the dictionary'smetadata.jsonsetsconnection_id_mapping(ko-dic, UniDic) and--context-id-freqis not given. Pass thecontext_id_freq.txtof the dictionary crate, as the build examples do.
[WARN lindera_segmenter::segmenter] no sentence delimiter (\n, \t, 。, 、) found within 32768 bytes from offset 0; forcing a sentence boundary to bound lattice size (see https://github.com/lindera/lindera/issues/871)
The RUST_LOG environment variable selects the messages: RUST_LOG=error
hides the warnings, and RUST_LOG=debug adds debugging messages (see
Warnings and log messages).
lindera build and lindera train no longer warn about an unk.def line
whose left and right context IDs differ, which mecab-dict-index accepts:
every line of mecab-ko-dic's unk.def is such a line. Rust programs that
install a logger no longer get this warning either.
Rust API changes in lindera_dictionary
These affect only code that calls the lattice backtraces directly, that
creates dictionary metadata with Metadata::new, that reads the
character categories of a dictionary, that generates unknown words with
UnknownDictionary, or that builds a dictionary from a char.def and an
unk.def that mecab-dict-index rejects (see
the previous section):
lindera_dictionary::viterbi::Lattice and
lindera_dictionary::nbest::NBestGenerator, also reachable as
lindera::dictionary::viterbi::…, lindera::dictionary::Lattice and
lindera::dictionary::nbest::…, and
lindera_dictionary::dictionary::metadata::Metadata, also reachable as
lindera::dictionary::Metadata, and
lindera_dictionary::dictionary::character_definition::CharacterDefinition, and
lindera_dictionary::dictionary::unknown_dictionary, also reachable as
lindera::dictionary::core::unknown_dictionary, and
lindera_dictionary::builder, also reachable as
lindera::dictionary::builder.
The Segmenter, Tokenizer,
SegmentWorker and AnalysisWorker APIs and the CLI are unaffected; the
language bindings change only in their Metadata class (see
The normalize_details setting is removed).
| v6 | v7 |
|---|---|
Lattice::tokens_offset() -> Vec<(usize, WordId)> | Lattice::tokens_offset() -> Vec<TokenOffset> |
Lattice::tokens_offset_into(&mut Vec<(usize, WordId)>) | Lattice::tokens_offset_into(&mut Vec<TokenOffset>) -> Option<usize> |
Lattice::nbest_tokens_offset(n, unique, cost_threshold) -> Vec<(Vec<(usize, WordId)>, i64)> | Lattice::nbest_tokens_offset(n, unique, cost_threshold) -> Vec<NBestPath> |
NBestGenerator::next() -> Option<(Vec<(usize, WordId)>, i64)> | NBestGenerator::next() -> Option<NBestPath> |
| — | New type aliases in viterbi: TokenOffset = (usize, usize, WordId) and NBestPath = (Vec<TokenOffset>, i64) |
| — | New in viterbi: BosContext, LatticeOptions::bos, LatticeExit, Lattice::exits_into and Lattice::exit_tokens_offset_into |
| — | New in nbest: NBestGenerator::from_exit and NBestGenerator::next_with_bos |
| — | New in nbest: UniqueNBestGenerator |
Metadata::new(name, encoding, simple_word_cost, default_left_context_id, default_right_context_id, default_field_value, flexible_csv, skip_invalid_cost_or_id, normalize_details, schema, userdic_schema) | Metadata::new(name, encoding, simple_word_cost, default_left_context_id, default_right_context_id, default_field_value, flexible_csv, skip_invalid_cost_or_id, schema, userdic_schema) |
Metadata::normalize_details | Removed |
PrefixDictionaryBuilderOptions::normalize_details(value) | Removed |
| — | New public field Metadata::skip_whitespace: Option<bool> (also lindera_binding::CoreMetadata::skip_whitespace) |
CharacterDefinition::lookup_categories(c) returns the categories of every line that covers c | Returns the categories of the last line that covers c, its default category first and the others in category id order |
DICTIONARY_FORMAT_VERSION is 2 | 3 |
UnknownDictionary::gen_unk_words(sentence, start_pos, has_matched, max_grouping_len, callback) | Removed |
UnknownDictionary::compatible_unk_index(sentence, start, end, feature) -> Option<WordIdx> | Removed |
unknown_dictionary::UnkWord and unknown_dictionary::WordIdx | Removed |
| — | New in viterbi: Lattice::unknown_word_candidates and UnknownWordCandidate |
CharacterDefinitionBuilder::build_from_str and build accept a range line that names a category not defined on an earlier line, and a category defined twice | Return an error that names the line and the category |
DictionaryBuilder::build_dictionary and build_unknown_dictionary accept an unk.def line whose category char.def does not define, and a char.def category without an unk.def line | Return an error that names the file and the category |
| — | New in unknown_dictionary: check_unknown_word_categories, the check of unk.def against the char.def categories that the builder and the trainer run |
Each token is now (start, end, word_id) instead of (start, word_id),
with byte offsets within the sentence. With whitespace skipped
(LatticeOptions::skip_whitespace, new in v7), a token does not always end
where the next one starts: skipped whitespace can lie in between, and only
the lattice knows how much of it belongs to the entry. The returned end leaves
the skipped whitespace out and keeps the whitespace an entry ends with (see
Dictionary entries with whitespace are kept).
With whitespace kept in the lattice, as set_text and set_text_nbest do,
the end is the next token's start, or the end of the sentence for the last
token, as before. nbest_tokens_offset with unique now compares the
(start, end) pairs of the paths instead of their starts.
#![allow(unused)] fn main() { use lindera::dictionary::viterbi::TokenOffset; // v6: a token ends where the next one starts let offsets = lattice.tokens_offset(); for (i, &(start, word_id)) in offsets.iter().enumerate() { let end = offsets.get(i + 1).map_or(sentence.len(), |&(next, _)| next); handle(&sentence[start..end], word_id); } // v7: the end is returned let offsets: Vec<TokenOffset> = lattice.tokens_offset(); for &(start, end, word_id) in &offsets { handle(&sentence[start..end], word_id); } }
The paths of nbest_tokens_offset and NBestGenerator::next hold the same
triples.
tokens_offset_into also returns the BOS index of the best path: the index
in LatticeOptions::bos of the BOS edge it starts from, Some(0) with the
default single BOS edge, and None when the lattice holds no complete path.
A call written as a statement compiles unchanged; where a () value is
expected, such as the body of a closure passed to for_each, add a ;.
The new items are what the segmenter uses to carry the context across
、 and 。 (see
The context is carried across 、 and 。).
They change nothing for existing code:
BosContextandLatticeOptions::bos: a sentence can start from several BOS edges, each with a right context ID and a cost. The default, an empty slice, is the single BOS edge of v6.LatticeExit,Lattice::exits_intoandLattice::exit_tokens_offset_into: for each right context ID of the words that end the sentence, the best path to such a word without the EOS connection, and its tokens.NBestGenerator::from_exitandNBestGenerator::next_with_bos: the paths that end with the right context ID of an exit, without EOS, in ascending order of cost, and each path together with the BOS index it starts from.nextreturns the paths ofnext_with_boswithout the BOS index.
UniqueNBestGenerator has the methods of NBestGenerator (new,
from_exit, next and next_with_bos) and yields the cheapest path of
each pair of word boundaries and BOS edge, in ascending order of cost; it is
what nbest_tokens_offset with unique and the segmenter use.
nbest_tokens_offset is meant for the default single BOS edge: with
several, it does not say which one a path starts from. Use
NBestGenerator::next_with_bos there.
Metadata::new loses its ninth argument, normalize_details: delete it
from the call. Metadata no longer has the field, and
PrefixDictionaryBuilderOptions (in lindera_dictionary::builder::prefix_dictionary)
no longer has the method, because the builder keeps every entry as written
(see
Dash and tilde spellings are kept as written).
Metadata still reads a metadata.json that contains the key, and ignores
it. For binding authors, lindera_binding::CoreMetadata loses the field the
same way, and CoreMetadata::new takes 10 arguments instead of 11.
Metadata gains the public field skip_whitespace: Option<bool>, the
dictionary's default for whitespace skipping (see
Whitespace is skipped in the lattice),
and so does lindera_binding::CoreMetadata. Neither struct is
#[non_exhaustive], so a struct literal that lists every field without
.. no longer compiles: add skip_whitespace: None, or end the literal
with ..Default::default(). Metadata::new and CoreMetadata::new set it
to None, which skips whitespace.
CharacterDefinition::lookup_categories keeps its signature. Its first
category is the one that creates unknown words (see
Unknown words come from the default category, as in MeCab);
code that only checks whether a character has a category is unaffected.
DICTIONARY_FORMAT_VERSION, the version that the builder writes and the
loaders accept, is now 3 (see
Dictionary directories must be rebuilt).
UnknownDictionary::gen_unk_words and UnknownDictionary::compatible_unk_index
are removed, together with the UnkWord and WordIdx types they used.
Only lindera train called them, and they ignored char.def (see
lindera train builds its lattices as MeCab's learner does).
To list the unknown words that the runtime creates, call
Lattice::unknown_word_candidates(dict, char_definitions, unknown_dictionary, text, max_grouping_len, out).
It fills out with an UnknownWordCandidate { start_char, end_char, word_id }
for every candidate of every start position of text, with character
positions and the unk.def entry ID, as set_text_with_options creates
them with the default options and the given max_grouping_len, also at
positions that no path reaches. The tokenization path does not change.
Rust API changes in lindera_trainer
These affect only Rust code that uses lindera-trainer directly or through
lindera::dictionary::trainer (the train feature) (see
lindera train builds its lattices as MeCab's learner does):
| v6 | v7 |
|---|---|
SerializableModel::unk_category_names: Vec<String> and SerializableModel::unk_categories: BTreeMap<String, String> | SerializableModel::unk_entries: Vec<(String, String)>, the (category, feature string) pairs of unk.def in file order |
ModelMetadata::version is "1.0.0", and Model::read_model does not check it | "2.0.0"; Model::read_model rejects a model of any other version |
| — | New in model: MODEL_FORMAT_VERSION |
| — | New: SerializableModel::export, which writes the files of lindera export to a directory |
TrainerConfig::dict() holds an empty lexicon and a placeholder unknown-word dictionary | Holds the seed lexicon (system_lexicon()) and the unk.def entries (unk_handler()) |
TrainerConfig::from_readers skips an unk.def line with fewer than five fields | Returns an error |
TrainerConfig::from_readers accepts a char.def and an unk.def that mecab-dict-index rejects | Returns an error that names the file, the category and, where there is one, the line (see lindera build and lindera train check char.def against unk.def) |
Trainer::max_grouping_len(len) bounds the unknown-word candidates, which never exceed three characters | Caps unknown-word grouping with the runtime's meaning, in characters beyond the first; None, the default, leaves it unbounded |
Model::num_labels() and ModelMetadata::label_count count one unknown-word label per char.def category | Count one per unk.def entry; the virtual labels are not counted |
Model::write_left_id_def / write_right_id_def write the IDs of the bigram feature strings, another ID space from the context IDs of lex.csv and matrix.def | Write the context IDs, as SerializableModel::write_left_id_def / write_right_id_def write them for the model that Model::write_model writes |
ModelMetadata::iterations (and model_info.training_iterations) is the max_iter setting | The number of L-BFGS iterations that ran |
Model::write_connection_costs and SerializableModel::write_connection_costs write i16::MAX for a connection without a trained weight | Write 0, the cost the connection has in training and in MeCab |
A model is now written with MODEL_FORMAT_VERSION as its metadata.version,
and Model::read_model checks it, in both the rkyv and the legacy JSON
format, so a model written by v6.2.0 or earlier has to be trained again.
Code that reads unk_category_names or unk_categories reads
unk_entries instead: one pair per unk.def line, several per category
included, in the order of the exported unk.def. If you set
Trainer::max_grouping_len, pass the cap that the dictionary will be used
with (max_grouping_len of the segmenter, --max-grouping-len of the CLI).
Who does not need to act
- Users of the language bindings and the CLI: the Python, Node.js, Ruby,
PHP, and WASM packages and
lindera-clikeep their package names, and their APIs apart from thenormalize_detailssetting ofMetadata(see Thenormalize_detailssetting is removed) and, in PHP, optional arguments of the wrong type (see PHP: optional arguments reject a value of the wrong type), and, in PHP, Node.js and WASM, integer arguments outside their range (see Bindings: integer arguments outside their range are rejected), and the mode, which they take as aModeinstead of a name, and the Node.js TypeScript typeJsPenalty, nowPenalty(see Bindings: a mode is aMode). The restructuring is internal to the Rust crates. A dictionary directory built or downloaded with v6.2.0 or earlier has to be rebuilt or downloaded again (see Dictionary directories must be rebuilt); the embedded dictionaries need nothing. A model file trained with v6.2.0 or earlier has to be trained again (seelindera trainbuilds its lattices as MeCab's learner does). The output changes are the IPADIC conjugation fix, which matters only if you read those two fields by name, the whitespace entries, which matter only for text with U+3000 (IPADIC, IPADIC-NEologd, UniDic) or with spaces (SudachiDict), the segmentation of text that contains whitespace, thechar.deffix, which matters only for text withÐ,々or〇, the context carried across、and。, which matters only for text with、or。inside a line, the N-best fix, which matters only for N-best results for input with more than one sentence or for an empty input, the unknown words that start at every position, which matter for runs of katakana, symbols or Latin letters in normal mode, the unknown words made from the default category, which matter for ko-dic hanja numerals followed by hanja and for〇with ko-dic, the new costs of CC-CEDICT, which matter for its entries that mix Latin letters or digits with Chinese characters and for its N-best costs, the〇entry of Jieba, which matters only for text with〇, the retrained costs of Jieba, which change the segmentation of most Chinese text with Jieba and the cost of simple Jieba user dictionary words, the dash and tilde spellings, which matter only for text with―,—,~or〜with IPADIC or IPADIC-NEologd, the choice among tied entries, which matters only if you read token details such as the reading or the base form, or the order of N-best results with the same cost, the choice between segmentations of equal cost, which changes the segmentation of a few lines with the Japanese and Korean dictionaries and Jieba and of about a quarter of the sentences with CC-CEDICT, in the CLI only, the whitespace at the ends of an input line and the output of lines without tokens, which matter only for lines that start or end with whitespace and for the wakati and N-best output of empty lines and lines of spaces, and the warnings on standard error, which matter only for scripts that read it, and, in the WASM binding only, the builder settings that v6 ignored for one way of setting the dictionary. - Projects staying on
lindera = "6": the 6.x releases oflinderaandlindera-analysisremain on crates.io and keep working together. Nothing changes until you bump the major version. - Users of the segmenter API only: code that uses
Segmenter,load_dictionary,Mode, and the otherlindera::…items of v6 compiles unchanged withlindera = "7", unless it calls the lattice backtraces oflindera::dictionary::Latticeor creates metadata withlindera::dictionary::Metadata::new(see Rust API changes inlindera_dictionary). Dictionary directories loaded by path still have to be rebuilt. The default build now also compileslindera-analysisand its dependencies (kanaria, regex, serde_yaml_ng, unicode-blocks, unicode-normalization, unicode-segmentation); to keep the v6 dependency tree, setdefault-features = false, features = ["mmap"].
Upgrade checklist
Rust crate users:
- Bump
linderato"7"and, in the same commit, either bumplindera-analysisto"7"or remove it and import fromlindera::analysis::…. - Optionally replace
lindera_analysis::withlindera::analysis::inusepaths — both name the same types. - If you set
default-features = false, add"analysis"(for theTokenizerand filters) and"mmap"(for memory-mapped loading) tofeaturesas needed. - Replace
features = ["lindera-<dictionary>"]withfeatures = ["embed-<dictionary>"]. - Optionally drop a direct
lindera-dictionarydependency in favor oflindera::dictionary::core,::builder,::viterbi, and the other re-exported modules. - If you call
Lattice::tokens_offset,tokens_offset_into,nbest_tokens_offsetorNBestGenerator::next, destructure(start, end, word_id)and use the returned end instead of the next token's start. - If you use the
()value ofLattice::tokens_offset_into, for example as the body of a closure, add a;: it now returns the BOS index of the best path. - If you call
Metadata::new, delete its ninth argument,normalize_details, and stop using theMetadata::normalize_detailsfield andPrefixDictionaryBuilderOptions::normalize_details. - If you build
Metadatawith a struct literal that lists every field, addskip_whitespace: Noneor end it with..Default::default(). - If you read
CharacterDefinition::lookup_categories, expect the default category first and the others in category id order. - If you call
UnknownDictionary::gen_unk_wordsorUnknownDictionary::compatible_unk_index, or useUnkWordorWordIdx, list the unknown words withLattice::unknown_word_candidatesinstead. - If you read
SerializableModel::unk_category_namesorunk_categories, readunk_entriesinstead, and if you setTrainer::max_grouping_len, pass it the cap that the dictionary will be used with. - If you pass
TokenizerBuilder::set_segmenter_modeaMode::Decomposewhose penalty is not the default, expect that penalty to apply.
Binding authors:
- Depend on
lindera-bindinginstead oflindera-binding-core, and replacelindera_binding_core::withlindera_binding::. - Delete the
normalize_detailsargument ofCoreMetadata::new, which now takes 10 arguments, and stop using theCoreMetadata::normalize_detailsfield. - If you build
CoreMetadatawith a struct literal that lists every field, addskip_whitespace: Noneor end it with..Default::default(). - Pass a
lindera::mode::Modeinstead of a mode name toCoreTokenizerBuilder::set_mode, which no longer returns aResult, and toCoreTokenizer::from_segmenterandCoreTokenizer::from_segmenter_with_space_penalty; parse a name withlindera_binding::parse_mode.
Build environments:
- Use Rust 1.89 or newer.
- Rename
LINDERA_DICTIONARIES_PATHtoLINDERA_BUILD_DICTIONARY_CACHE_DIRin shell profiles, CI configuration, and container images.
Everyone who loads a dictionary from files:
- Rebuild with
lindera build, or download again, every dictionary directory made with v6.2.0 or earlier, including those saved in the WASM binding's OPFS storage: v7.0.0 rejects them (format version 3). The embedded dictionaries and user dictionaries need nothing.
Users of IPADIC or IPADIC-NEologd:
- If you read
conjugation_typeorconjugation_formby name (including the CLI's JSON output), expect the two values to trade places. - If you build dictionaries with your own copy of the IPADIC
metadata.json, swap the two names in it.
Users of IPADIC, IPADIC-NEologd, UniDic or SudachiDict with text that contains U+3000 or spaces:
- Expect U+3000 to be
記号,空白(IPADIC) or空白(UniDic) tokens, and SudachiDict to segment spaced text much closer to Sudachi. Drop U+3000 withjapanese_stop_tagsor NFKC normalization if you do not want it.
Everyone who segments text that contains whitespace:
- Expect MeCab's segmentation for such text (most visible with ko-dic; not
with SudachiDict, which keeps whitespace in the lattice as Sudachi does). If
you need the v6 handling of whitespace, set
skip_whitespace(false),"skip_whitespace": false, or--disable-skip-whitespace; the other output changes in this guide still apply. - Expect the token of a dictionary entry that ends with a space (in IPADIC-NEologd, ko-dic and SudachiDict) to include that space, as in MeCab.
Users of any bundled dictionary with text that contains Ð, 々 or 〇:
- Expect
Ðto stay in the output, and々after an unknown kanji to be a token of its own.
Users of lindera train:
- Give every range line of your
char.defat least one category. - If your
char.defhas single-code-point or overlapping lines, retrain: the%tfeature now follows those lines as MeCab does. - Train every model again with the 7.0.0
lindera train(or the bindings' training functions), then export it and build the dictionary again:lindera exportrejects amodel.datwritten by v6.2.0 or earlier, and the training lattices, the unknown-word labels and the exportedunk.defandmatrix.defhave changed. - Give every line of your
unk.defat least five fields: the category, the two context IDs, the cost and at least one feature column. - If your
char.defandunk.defwere copied fromresources/training/and have noSPACEcategory, add it as those files now do, so that the trained dictionary tokenizes without--keep-whitespace(#1166). - If training reports a category of
char.deforunk.def, define each category once, before the range lines that name it, give every category anunk.defline, and remove theunk.deflines of undefined categories (#1195).
Everyone who builds a system dictionary with lindera build:
- If the build reports a context ID out of range for the connection
matrix, fix the IDs in
lex.csvorunk.defor the size inmatrix.def, or setskip_invalid_cost_or_idto drop suchlex.csvrows. Such a dictionary used to build and then panic at tokenization. A dictionary exported by v6.2.0 or earlier is best trained and exported again. - If the build reports a category of
char.deforunk.def, define each category once, before the range lines that name it, give every category anunk.defline, and remove theunk.deflines of undefined categories (#1195).
Everyone who uses a user dictionary with detailed entries:
- If building the tokenizer reports a user dictionary entry whose context
ID is out of range for the connection matrix, fix the IDs in the user
dictionary CSV for the system dictionary you use, and build the
.binagain if you use one. Such a user dictionary used to panic at tokenization when the entry matched the input.
Users of Japanese text with 、 or 。 inside a line:
- Expect some words after
、or。to be read in the context of the words before them, mostly as MeCab reads them. No setting restores the v6 output.
Users of N-best results:
- Expect every result to cover the whole input, and the results from the second one on to change for input with more than one sentence.
- For a line with
、or。, expect every cost to change, the first result's included: the costs are those of one lattice over the line. - If you set a cost threshold, it now applies to the total cost of the whole input, not to each sentence.
- Expect an empty input to give one result without tokens, as an input of only whitespace does, instead of no result.
- Expect the first result to be the 1-best segmentation also when several paths have exactly the same cost; results with the same cost can change places.
Users of normal mode with text that has runs of katakana, symbols or Latin letters:
- Expect such runs to be split where MeCab splits them: katakana joined by
・(IPADIC, IPADIC-NEologd, UniDic), unknown symbols right after a dictionary symbol, and Korean sentence-final punctuation such as.". No setting restores the v6 output, andunknown_word_ladder(false)no longer gives the output of Lindera before v6. - Expect katakana- and Latin-heavy text to take about 13% more instructions, and very long katakana runs in N-best to take much more time and memory.
Users of ko-dic with text that has hanja numerals or 〇:
- Expect a ko-dic hanja numeral and the hanja after it to be one unknown
word (
三國史記is oneSHword), and〇to be a symbol (SY), as in MeCab. No setting restores the v6 output.
Users of CC-CEDICT:
- Expect entries that mix Latin letters or digits with Chinese characters,
such as
T恤and卡拉OK, to be one word, and〇and entries such asPKto come with their pinyin and definition. Latin words, runs of kana and numbers stay one word, as in v6. - Expect the costs of paths, and so N-best costs, to change: unknown words cost 400 instead of −3,200.
- Build a CC-CEDICT dictionary directory from the
CC-CEDICT-MeCab-0.1.0-20261021archive.
Users of Jieba with text that has 〇:
- Expect each
〇to be one numeral word (m, pinyinling2) instead of part of an unknown word, as MeCab segments it with the same dictionary. - Build a Jieba dictionary directory from the
mecab-jieba-0.1.3archive.
Users of Jieba:
- Expect Chinese text to be segmented as UD Chinese GSD segments it, finer
than jieba (
中华 / 人民 / 共和 / 国,这 / 个), and as MeCab segments it with the same dictionary. - Expect simple user dictionary words to cost −32768. Take the context IDs
of detailed user dictionary rows from the
left-id.defandright-id.defofmecab-jieba-0.1.3. - Build a Jieba dictionary directory from the
mecab-jieba-0.1.3archive.
Users of IPADIC or IPADIC-NEologd with text that contains ―, —, ~ or
〜:
- Expect entries spelled with
―or~to match text spelled the same way, as in MeCab, and text with—or〜to miss the 12 IPADIC and 1,085 IPADIC-NEologd entries that it found only through the old rewrite. With IPADIC, map—to―and~to〜with themappingcharacter filter if both spellings should find the entries.
Users who read token details such as the reading or the base form:
- Expect words with tied entries to get the details of the first CSV row,
as in MeCab: with IPADIC,
狡いreadsズルイ, notコスイ. No setting restores the v6 output. - In a user dictionary, expect the first of tied rows to be chosen. A user entry still wins a tie with a system entry, unlike in MeCab.
Everyone who compares the segmentation with v6 or with MeCab:
- Expect the places where two segmentations cost exactly the same, few with
the Japanese and Korean dictionaries and Jieba but about a quarter of the
sentences with CC-CEDICT, to be segmented as MeCab segments them, with the
last word that starts later (IPADIC
腸窒扶斯is腸 / 窒扶 / 斯, CC-CEDICT電視台上班is電視 / 台上 / 班). No setting restores the v6 output.
Language bindings and CLI:
- Remove
normalize_detailswhere you createMetadata: the Python keyword argument, the ninth positional argument of Ruby'sMetadata.new, the last argument of PHP'sMetadataconstructor, and thenormalizeDetailsoption in Node.js. Stop reading the property and the key of the same name into_dict(),to_h,toArray()andtoObject(). - With
lindera tokenize, expect the offsets of a line that starts with whitespace to index the input line, U+3000 at the start or end of a line to be a token (IPADIC, IPADIC-NEologd, UniDic),--keep-whitespaceto output the spaces there,-Nto give one result for an empty line or a line of spaces, and the wakati format to write an empty line for a line without tokens. To get the v6 offsets and tokens, remove the whitespace at the ends of each line first. - With the WASM
TokenizerBuilder, expectsetKeepWhitespace()and the appended filters to apply to a dictionary set withsetDictionaryInstance(), and a user dictionary set withsetUserDictionaryInstance()to apply to a dictionary set withsetDictionary(). - With PHP, pass each argument its declared type: a value of the wrong
type for an optional parameter, which v6 read as
null, now throws anException. Cast strings such as$argvvalues with(int)or(float). - With PHP, Node.js and WASM, pass integer arguments in the range of their setting: a value outside it, which v6 wrapped, now throws, as do a negative index and, in Node.js and WASM, a number that is not an integer.
- Pass a
Modetoset_mode/setModeand to theTokenizerconstructor instead of"normal"or"decompose":Mode.normal()orMode.decompose()(RubyLindera::Mode.normal, PHPLindera\Mode::normal()), with aPenaltyto customize decompose mode, orMode.from_str(name)(Mode.fromStrin PHP, Node.js and WASM) for a name read at run time. Replace uses of the oldModetypes, such as Python'sMode("normal"), Node.js'sJsModeand WASM'sMode.Normal, in the same way. - With Node.js and TypeScript, rename the type
JsPenaltytoPenalty. - With the CLI, expect warnings on standard error, for example for a
sentence cut after 32 KiB without a delimiter; set
RUST_LOG=errorto hide them. - Nothing else to do beyond taking the 7.0.0 release, apart from the
dictionary rebuild, the dictionary, whitespace,
、and。, N-best, unknown-word, default-category, dash and tilde, tied-entry and equal-cost items above, thelindera trainandlindera builditems, and thelindera tokenize, WASM, PHP, integer-argument,Mode,JsPenaltyand CLI warning items.