Lindera

License: MIT Crates.io

A morphological analysis library in Rust. Lindera is forked from kuromoji-rs and aims to provide easy installation and concise APIs for tokenizing text in multiple languages.

Key Features

FeatureDescription
Morphological AnalysisViterbi-based segmentation and part-of-speech tagging
Multi-language SupportJapanese (IPADIC, IPADIC NEologd, UniDic), Korean (ko-dic), Chinese (CC-CEDICT, Jieba)
Dictionary SystemPre-built dictionaries, user dictionaries, and custom dictionary training
Text Processing PipelineComposable character filters and token filters for flexible text normalization
CRF TrainingTrain custom CRF models for dictionary cost estimation
Python BindingsUse Lindera from Python via PyO3
WebAssemblyRun Lindera in the browser via wasm-bindgen
Pure RustNo C/C++ dependencies; works on any platform Rust supports

Tokenization Flow

graph LR
    subgraph Your Application
        T["Text"]
    end
    subgraph Lindera
        CF["Character Filters"]
        SEG["Segmenter\n(Dictionary + Viterbi)"]
        TF["Token Filters"]
    end
    T --> CF --> SEG --> TF --> R["Tokens"]

Document Map

SectionDescription
Getting StartedInstallation, quick start, and examples
DictionariesAvailable dictionaries and how to use them
ConfigurationYAML-based tokenizer configuration
User DictionaryBuilding and using custom user dictionaries
FiltersCharacter filters and token filters reference
CRF TrainingTraining custom dictionary cost models
CLICommand-line interface reference
ArchitectureCrate structure and design overview
API ReferenceRust API documentation
ContributingHow to contribute to Lindera

Performance

Lindera tokenizes Japanese text (IPADIC) at roughly 10-20 MB/s single-threaded, depending on hardware and whether the Viterbi lattice is reused across calls. Run make bench to reproduce the benchmark suite (lindera/benches/) on your own machine; see #875 for detailed methodology and comparison numbers.

Quick Example

use lindera::dictionary::load_dictionary;
use lindera::mode::Mode;
use lindera::segmenter::Segmenter;
use lindera::analysis::tokenizer::Tokenizer;
use lindera::LinderaResult;

fn main() -> LinderaResult<()> {
    let dictionary = load_dictionary("embedded://ipadic")?;
    let segmenter = Segmenter::new(Mode::Normal, dictionary, None);
    let tokenizer = Tokenizer::new(segmenter);

    let text = "関西国際空港限定トートバッグ";
    let mut tokens = tokenizer.tokenize(text)?;
    println!("text:\t{}", text);
    for token in tokens.iter_mut() {
        let details = token.details().join(",");
        println!("token:\t{}\t{}", token.surface.as_ref(), details);
    }

    Ok(())
}

Run the example:

cargo run -p lindera-analysis --features=embed-ipadic --example=tokenize

Output:

text:   関西国際空港限定トートバッグ
token:  関西国際空港    名詞,固有名詞,組織,*,*,*,関西国際空港,カンサイコクサイクウコウ,カンサイコクサイクーコー
token:  限定    名詞,サ変接続,*,*,*,*,限定,ゲンテイ,ゲンテイ
token:  トートバッグ    名詞,一般,*,*,*,*,*,*,*

License

Lindera is released under the MIT License.

Architecture

Lindera is organized as a Cargo workspace comprising multiple crates. Each crate has a focused responsibility, from low-level CRF computation to high-level CLI and language bindings.

Crate Dependency Graph

graph TB
    CRF["lindera-crf\n(CRF Engine)"]
    DICT["lindera-dictionary\n(Dictionary Base)"]
    TRAINER["lindera-trainer\n(CRF Training)"]
    IPADIC["lindera-ipadic"]
    UNIDIC["lindera-unidic"]
    SUDACHIDICT["lindera-sudachidict"]
    KODIC["lindera-ko-dic"]
    CCCEDICT["lindera-cc-cedict"]
    JIEBA["lindera-jieba"]
    NEOLOGD["lindera-ipadic-neologd"]
    SEGMENTER["lindera-segmenter\n(Segmenter)"]
    ANALYSIS["lindera-analysis\n(Analysis Chain)"]
    LIB["lindera\n(Facade)"]
    CLI["lindera-cli\n(CLI)"]
    BINDING["lindera-binding"]
    PY["lindera-python"]
    NODEJS["lindera-nodejs"]
    RUBY["lindera-ruby"]
    PHP["lindera-php"]
    WASM["lindera-wasm"]

    CRF --> TRAINER
    DICT --> TRAINER
    TRAINER -.->|"train feature"| SEGMENTER
    DICT --> IPADIC
    DICT --> UNIDIC
    DICT --> SUDACHIDICT
    DICT --> KODIC
    DICT --> CCCEDICT
    DICT --> JIEBA
    DICT --> NEOLOGD
    DICT --> SEGMENTER
    IPADIC --> SEGMENTER
    UNIDIC --> SEGMENTER
    SUDACHIDICT --> SEGMENTER
    KODIC --> SEGMENTER
    CCCEDICT --> SEGMENTER
    JIEBA --> SEGMENTER
    NEOLOGD --> SEGMENTER
    SEGMENTER --> ANALYSIS
    SEGMENTER --> LIB
    ANALYSIS -.->|"analysis feature"| LIB
    LIB --> CLI
    LIB --> BINDING
    BINDING --> PY
    BINDING --> NODEJS
    BINDING --> RUBY
    BINDING --> PHP
    BINDING --> WASM

Crate Overview

CrateTypeDescription
lindera-crfCorePure Rust CRF (Conditional Random Field) implementation. Supports no_std. Uses rkyv for serialization.
lindera-dictionaryCoreDictionary base library. Provides dictionary loading and building.
lindera-trainerCoreCRF-based dictionary training pipeline. Builds on lindera-crf and lindera-dictionary; consumed directly or via the lindera facade's train feature.
lindera-segmenterCorePure morphological segmenter (the lindera crate of v6 and earlier). Integrates the dictionary crates and provides the Segmenter API.
lindera-analysisCoreLucene-style analysis chain on top of lindera-segmenter: character filters, token filters, and the Tokenizer that composes them around a Segmenter. Reachable as lindera::analysis.
linderaCoreFacade crate and the one dependency Rust users add: re-exports lindera-segmenter and, with the default analysis feature, lindera-analysis as lindera::analysis.
lindera-cliApplicationCommand-line interface for tokenization, dictionary building, and CRF training.
lindera-bindingCoreFFI-independent helpers shared by the five language bindings below; depends on the lindera facade.
lindera-ipadicDictionaryJapanese dictionary based on IPADIC.
lindera-ipadic-neologdDictionaryJapanese dictionary based on IPADIC NEologd (includes neologisms).
lindera-unidicDictionaryJapanese dictionary based on UniDic.
lindera-sudachidictDictionaryJapanese dictionary based on SudachiDict.
lindera-ko-dicDictionaryKorean dictionary based on ko-dic.
lindera-cc-cedictDictionaryChinese dictionary based on CC-CEDICT.
lindera-jiebaDictionaryChinese dictionary based on Jieba.
lindera-pythonBindingPython bindings via PyO3.
lindera-nodejsBindingNode.js bindings via NAPI-RS.
lindera-rubyBindingRuby bindings via Magnus + rb-sys.
lindera-phpBindingPHP bindings via ext-php-rs.
lindera-wasmBindingWebAssembly bindings via wasm-bindgen.

Tokenization Pipeline

Lindera processes text through a multi-stage pipeline:

Input Text
  |
  v
Character Filters    -- Normalize characters (e.g., Unicode normalization, mapping)
  |
  v
Segmenter            -- Segment text into tokens using a dictionary and the Viterbi algorithm
  |
  v
Token Filters        -- Transform tokens (e.g., POS filtering, stop words, stemming)
  |
  v
Output Tokens

The Segmenter is the core component. It builds a lattice of candidate tokens from the dictionary, then applies the Viterbi algorithm to find the lowest-cost path, producing the most likely segmentation. Dictionary lookups walk a char-wise double-array trie (built with crawdad) in place over the serialized bytes of dict.trie, rather than deserializing into an owned structure. For repeated tokenization, Segmenter::new_worker() returns a SegmentWorker that reuses lattice and scratch-buffer allocations across calls, with an automatic shrink policy to bound retained memory.

Feature Flags

FeatureDescriptionDefault
analysisRe-export of lindera-analysis as lindera::analysis (character filters, token filters, Tokenizer)Enabled (off with default-features = false)
mmapMemory-mapped file support for filesystem-based dictionary loading (opt-in via --mmap/use_mmap; the word-list files, the connection-cost matrix, and the trie all stay lazily paged)Enabled
trainCRF-based dictionary training functionality (depends on lindera-crf)CLI + Python/Node.js/Ruby/PHP bindings (default); opt-in for the lindera library crate; unavailable in lindera-wasm
embed-ipadicEmbed the IPADIC dictionary into the binaryDisabled
embed-ipadic-neologdEmbed the IPADIC NEologd dictionary into the binaryDisabled
embed-unidicEmbed the UniDic dictionary into the binaryDisabled
embed-sudachidictEmbed the SudachiDict dictionary into the binaryDisabled
embed-ko-dicEmbed the ko-dic dictionary into the binaryDisabled
embed-cc-cedictEmbed the CC-CEDICT dictionary into the binaryDisabled
embed-jiebaEmbed the Jieba dictionary into the binaryDisabled
embed-cjkEmbed IPADIC + ko-dic + Jieba dictionariesDisabled
embed-cjk2Embed UniDic + ko-dic + Jieba dictionariesDisabled
embed-cjk3Embed IPADIC NEologd + ko-dic + Jieba dictionariesDisabled
embed-cjk4Embed SudachiDict + ko-dic + Jieba dictionariesDisabled

Learn More

Getting Started

This section will guide you through installing Lindera and running your first morphological analysis.

  • Installation -- Add Lindera to your project and configure environment variables
  • Quick Start -- Tokenize your first text in just a few lines of code
  • Examples -- Explore example programs for common use cases

Installation

Requirements

Rust 1.89 or newer. The workspace declares this as rust-version, so an older toolchain reports it as a cargo error rather than failing to compile.

Adding the Dependency

Put the following in Cargo.toml:

[dependencies]
lindera = "7"

[!NOTE] This single dependency also provides the analysis chain (character filters, token filters, and the Tokenizer) as lindera::analysis; use default-features = false if you only need the segmenter. Upgrading from v6? See Migration v6 to v7 for details.

Dictionary Setup

Lindera requires a pre-built dictionary at runtime. Download a dictionary from GitHub Releases and specify its path when loading:

#![allow(unused)]
fn main() {
let dictionary = load_dictionary("/path/to/ipadic")?;
}

[!TIP] If you want to embed a dictionary directly into the binary (advanced usage), enable the corresponding embed-* feature flag and load it using the embedded:// scheme:

#![allow(unused)]
fn main() {
// Cargo.toml: lindera = { version = "7", features = ["embed-ipadic"] }
let dictionary = load_dictionary("embedded://ipadic")?;
}

See Feature Flags for details.

Environment Variables

LINDERA_BUILD_DICTIONARY_CACHE_DIR

The LINDERA_BUILD_DICTIONARY_CACHE_DIR environment variable designates a build-time cache directory for the embedded-dictionary build pipeline. It is read only by the dictionary crates' build scripts and has no effect at runtime.

When set, each build stores two kinds of files under $LINDERA_BUILD_DICTIONARY_CACHE_DIR/<version>-fmt<format>/ (where <version> is the dictionary crate version and <format> is the dictionary format version):

  • the downloaded distribution archive (validated with MD5; invalid files are re-downloaded)
  • the built binary dictionary that gets embedded into the crate, with a build_inputs.json that records what it was built from

The format version is part of the path so that a cache written by a build with a different on-disk dictionary layout is a miss rather than a stale hit. A cached dictionary is reused only when all of the following match the current build, and is rebuilt otherwise:

  • the dictionary format version and the contents of the crate's metadata.json
  • the lindera-dictionary crate that builds it: its version and a digest of its source, so that any change to the dictionary builder, an uncommitted one included, rebuilds the dictionary
  • the source archive (file name and MD5) and the directory the build reads in it
  • the context-id frequency histogram the build reads (context_id_freq.txt, or the file named by LINDERA_CTX_FREQ_FILE) when connection_id_mapping is enabled

A dictionary left behind by an interrupted build, or cached by a release from before build_inputs.json, is rebuilt. The versions of the builder's dependencies are not compared: after upgrading a dependency of lindera-dictionary without a format version change, clear the cache if the built dictionaries may differ. Directories from older format versions are not deleted automatically; remove them by hand if the cache grows.

cargo build -vv shows, for each dictionary crate, whether the cached dictionary was used and why or why not, on lines that start with lindera dictionary cache:.

This enables:

  • Offline builds: once cached, subsequent builds need no network access
  • Faster builds: download and dictionary build are skipped when valid cached files exist
  • Reproducible builds: consistent dictionary versions across builds

Usage:

export LINDERA_BUILD_DICTIONARY_CACHE_DIR=/path/to/cache
cargo build --features=embed-ipadic

Notes:

  • The directory is managed automatically and is safe to delete; contents are re-downloaded and rebuilt as needed
  • Version subdirectories accumulate across upgrades and are not garbage-collected; old ones can be removed freely
  • Setting this variable causes dictionary crates to download and build their dictionaries even when no embed-* feature is enabled (useful for pre-populating the cache). Such a build embeds nothing, so it keeps a dictionary already in the cache without checking it; a build with the embed-* feature checks it and rebuilds it if needed
  • Use one cache directory per checkout. Two checkouts with different builder code that share a directory rebuild each other's dictionaries, and a crate whose build script does not rerun can embed the dictionary the other checkout built. In this repository, a relative path resolves under the workspace root, which gives each checkout its own cache
  • Builds that share the directory, such as cargo build alongside rust-analyzer with its own target directory, write each dictionary one at a time: a build that finds another build writing the same dictionary waits for it, and then uses the dictionary it left (cargo build -vv shows lindera dictionary cache: waiting for another build). The lock is an empty <dictionary>.lock file next to the dictionary, held while a build writes it. It works between builds on one machine, and on a network file system only as far as the file system supports file locks. Where the file cannot be locked, the build prints a warning and goes on without the lock, as builds before 7.0.0 did, so two such builds can still remove each other's files
  • Builds with DOCS_RS set ignore this variable (see DOCS_RS below)

LINDERA_CONFIG_FILE

The LINDERA_CONFIG_FILE environment variable names a YAML configuration file for the tokenizer, which TokenizerBuilder::new() reads. This allows you to configure tokenizer behavior without modifying Rust code.

export LINDERA_CONFIG_FILE=./resources/config/lindera.yml

LINDERA_CONFIG_PATH, the name of this variable up to v6, still works until v8.0.0, with a deprecation warning on standard error. When both are set, LINDERA_CONFIG_FILE is used.

See the Configuration section for details on the configuration format.

DOCS_RS

The DOCS_RS environment variable is automatically set by docs.rs when building documentation. When this variable is detected, Lindera creates dummy dictionary files instead of downloading actual dictionary data, allowing documentation to be built without network access or large file downloads.

Such a build ignores LINDERA_BUILD_DICTIONARY_CACHE_DIR: the dummy dictionaries are built in the build script's output directory, never in the cache, so a later build without DOCS_RS does not embed them.

This is primarily used internally by docs.rs and typically doesn't need to be set by users.

Quick Start

This example covers the basic usage of Lindera.

It will:

  • Create a segmenter in normal mode
  • Segment the input text
  • Output the tokens

This example uses the embed-ipadic feature, which downloads the IPADIC dictionary and embeds it into the binary automatically at build time — no manual dictionary download is required.

use std::borrow::Cow;

use lindera::dictionary::load_dictionary;
use lindera::mode::Mode;
use lindera::segmenter::Segmenter;
use lindera::LinderaResult;

fn main() -> LinderaResult<()> {
    let dictionary = load_dictionary("embedded://ipadic")?;
    let segmenter = Segmenter::new(Mode::Normal, dictionary, None);

    let text = "関西国際空港限定トートバッグ";
    let mut tokens = segmenter.segment(Cow::Borrowed(text))?;
    println!("text:\t{}", text);
    for token in tokens.iter_mut() {
        let details = token.details().join(",");
        println!("token:\t{}\t{}", token.surface.as_ref(), details);
    }

    Ok(())
}

The above example can be run as follows:

% cargo run --features embed-ipadic --example=segment

[!TIP] If you prefer not to embed the dictionary into the binary, download a pre-built IPADIC dictionary from GitHub Releases, extract it to a local directory (e.g., /path/to/ipadic), and call load_dictionary("/path/to/ipadic") instead — no embed-ipadic feature needed in that case. See Feature Flags for details.

You can see the result as follows:

text:   関西国際空港限定トートバッグ
token:  関西国際空港    名詞,固有名詞,組織,*,*,*,関西国際空港,カンサイコクサイクウコウ,カンサイコクサイクーコー
token:  限定    名詞,サ変接続,*,*,*,*,限定,ゲンテイ,ゲンテイ
token:  トートバッグ    名詞,一般,*,*,*,*,*,*,*

[!NOTE] Character filters, token filters, and the Tokenizer API are available as lindera::analysis (the analysis feature, enabled by default), so lindera = "7" already covers the whole analysis chain — no extra dependency is needed. With default-features = false you get the segmenter alone. See Lindera Analysis for details, and Migration v6 to v7 if you are upgrading from v6.

Examples

Lindera includes several example programs that demonstrate common use cases. The source code is available in the examples directory on GitHub (the tokenize* examples are in lindera-analysis/examples).

The tokenize* examples use the Tokenizer and filter APIs from lindera::analysis. They live in the lindera-analysis crate, hence -p lindera-analysis below.

All examples below are run with the embed-ipadic feature enabled, which downloads the IPADIC dictionary and embeds it into the binary automatically at build time — no manual dictionary download is required.

Available Examples

segment

Basic morphological segmentation with the Segmenter API — it needs only the segmenter, so it also works with default-features = false.

cargo run -p lindera --features=embed-ipadic --example=segment

tokenize

Basic tokenization using an external IPADIC dictionary. Segments input text and prints each token with its part-of-speech details.

cargo run -p lindera-analysis --features=embed-ipadic --example=tokenize

tokenize_with_user_dict

Tokenization with a user dictionary. Shows how to supplement the dictionary with custom entries for domain-specific terms.

cargo run -p lindera-analysis --features=embed-ipadic --example=tokenize_with_user_dict

tokenize_with_filters

Tokenization with character filters and token filters. Demonstrates the text processing pipeline, including Unicode normalization, part-of-speech filtering, and other transformations.

cargo run -p lindera-analysis --features=embed-ipadic --example=tokenize_with_filters

tokenize_with_config

Tokenization using a YAML configuration file. Shows how to configure the tokenizer declaratively instead of programmatically.

cargo run -p lindera-analysis --features=embed-ipadic --example=tokenize_with_config

Core Concepts

This section explains the fundamental concepts behind Lindera's morphological analysis system.

Morphological Analysis

What is morphological analysis?

Morphological analysis is the process of breaking down text into its smallest meaningful units (morphemes) and identifying their grammatical properties. For languages like Japanese, Chinese, and Korean -- where words are not separated by spaces -- morphological analysis is an essential first step for natural language processing tasks such as search indexing, text classification, and machine translation.

How Lindera works

Lindera is a dictionary-based morphological analyzer. It uses a pre-compiled system dictionary containing known words along with their costs, and applies the Viterbi algorithm to find the optimal segmentation of input text.

The analysis process works as follows:

  1. Sentence splitting: Before building the lattice, Lindera splits the input text into sentences at delimiter characters (\n, \t, 。, 、) and processes one sentence at a time. If no delimiter appears within roughly 32 KiB of a sentence's start, Lindera forces a sentence boundary there anyway and logs a warning, to bound the lattice's memory and CPU cost. Up to the next \n or \t, each sentence continues from the words that end the previous one, so the result is, with few exceptions, the one a single lattice over that text gives (see Sentence Splitting). The forced cut can affect tokenization at the artificial cut point for pathological delimiter-free input, such as minified text or base64-encoded blobs.
  2. Lattice construction: Lindera scans each sentence and looks up all possible words in the dictionary at every position, building a directed acyclic graph (lattice) of candidate segmentations.
  3. Cost assignment: Each candidate word has an associated word cost (from the dictionary), and each pair of adjacent words has a connection cost (from the connection cost matrix).
  4. Optimal path search: The Viterbi algorithm finds the path through the lattice with the minimum total cost, producing the best segmentation.

Key terminology

TermDescription
Surface formThe actual text as it appears in the input (e.g., "食べ").
Part-of-speech (POS)The grammatical category of a word (e.g., noun, verb, particle). Lindera dictionaries provide hierarchical POS tags with up to four levels of subcategories.
ReadingThe pronunciation of a word, typically in Katakana for Japanese dictionaries.
Base formThe uninflected (dictionary) form of a word (e.g., "食べる" for the surface "食べ").
ConjugationInflection information for words that conjugate, consisting of a conjugation type and a conjugation form.

Cost-based segmentation

The Viterbi algorithm selects the segmentation path with the minimum total cost. The total cost of a path is the sum of:

  • Word costs: Each word in the dictionary has an associated cost. Lower cost means the word is more likely to appear. Common words tend to have lower costs, while rare words have higher costs.
  • Connection costs: The cost of connecting two adjacent words, determined by the right context ID of the left word and the left context ID of the right word.

The algorithm computes:

Total cost = sum of word costs + sum of connection costs

By minimizing this total cost, Lindera finds the most natural segmentation of the input text.

Connection cost matrix

The connection cost matrix stores the cost of transitioning from one word to another. It is a two-dimensional matrix indexed by:

  • The right context ID of the preceding word
  • The left context ID of the following word

These context IDs encode grammatical information about word boundaries. For example, the connection cost between a noun and a particle is typically low (natural sequence), while the connection cost between two verbs in base form might be high (unnatural sequence).

The connection cost matrix is compiled into binary format as part of the dictionary build process and is loaded at runtime for efficient lookup.

Dictionaries

Lindera supports various dictionaries for Japanese, Korean, and Chinese morphological analysis. Each dictionary is provided as a separate crate.

DictionaryLanguageCrateDescription
IPADICJapaneselindera-ipadicThe most common dictionary for Japanese
IPADIC NEologdJapaneselindera-ipadic-neologdIPADIC with neologisms (new words)
UniDicJapaneselindera-unidicUniform word unit definitions
SudachiDictJapaneselindera-sudachidictActively maintained vocabulary, updated several times a year upstream
ko-dicKoreanlindera-ko-dicKorean morphological analysis
CC-CEDICTChineselindera-cc-cedictChinese-English dictionary
JiebaChineselindera-jiebaJieba-based Chinese dictionary

Obtaining Dictionaries

Pre-built dictionaries are available for download from GitHub Releases. Download the dictionary archive for your target language and extract it to a local directory.

#![allow(unused)]
fn main() {
// Load an external dictionary from a local path
let dictionary = load_dictionary("/path/to/ipadic")?;
}

[!TIP] If you need a self-contained binary without external dictionary files, you can embed dictionaries using the embed-* feature flags and load them using the embedded:// scheme:

#![allow(unused)]
fn main() {
let dictionary = load_dictionary("embedded://ipadic")?;
}

See Feature Flags for details.

See each dictionary crate's documentation for format details, build instructions, and usage examples.

Tokenization

Lindera provides multiple tokenization modes and supports N-Best analysis for enumerating alternative segmentation candidates.

Tokenization modes

Normal mode

Normal mode performs standard tokenization based on dictionary entries. Compound words that exist as single entries in the dictionary are kept as-is.

Example -- tokenizing "関西国際空港限定トートバッグ" in Normal mode:

関西国際空港 | 限定 | トートバッグ

The compound noun "関西国際空港" (Kansai International Airport) is preserved as a single token because it exists as one entry in the dictionary.

Decompose mode

Decompose mode further breaks down compound nouns into their constituent parts, even when the compound exists as a dictionary entry.

Example -- tokenizing "関西国際空港限定トートバッグ" in Decompose mode:

関西 | 国際 | 空港 | 限定 | トートバッグ

The compound "関西国際空港" is decomposed into "関西", "国際", and "空港".

Selecting a mode

In Rust, specify the mode when creating a Segmenter:

#![allow(unused)]
fn main() {
use lindera::mode::Mode;
use lindera::segmenter::Segmenter;
use lindera::dictionary::load_dictionary;

let dictionary = load_dictionary("embedded://ipadic")?;

// Normal mode
let segmenter = Segmenter::new(Mode::Normal, dictionary, None);

// Decompose mode
let segmenter = Segmenter::new(Mode::Decompose(Default::default()), dictionary, None);
}

With the CLI, use the --mode flag:

echo "関西国際空港限定トートバッグ" | lindera tokenize --dict embedded://ipadic --mode normal
echo "関西国際空港限定トートバッグ" | lindera tokenize --dict embedded://ipadic --mode decompose

[!NOTE] The embedded:// scheme requires lindera-cli to be built with the corresponding embed-* feature (e.g. cargo install lindera-cli --features=embed-ipadic). A default build of lindera-cli does not enable any embed-* feature, and passing --dict embedded://ipadic without it fails with Invalid dictionary scheme: embedded.

N-Best tokenization

N-Best tokenization enumerates the top N tokenization candidates ordered by total path cost (lower cost = better segmentation). This is useful when the best result is ambiguous, or when you want to explore alternative interpretations of the input text.

Algorithm

N-Best tokenization is based on the Forward-DP Backward-A* algorithm, which is compatible with MeCab's N-Best implementation. The forward pass computes optimal costs using dynamic programming, and the backward pass uses A* search to enumerate paths in order of increasing total cost.

As in 1-best tokenization, the input is first split into sentences, and the sentences up to each \n or \t form a segment (see Sentence Splitting). Each segment is searched on its own: within it, the context is carried across 、 and 。, so its paths are the N best paths of one lattice over the segment. Every candidate segments the whole input: it takes one path in each segment, and its cost is the sum of those paths' costs. The candidates are the N cheapest of these combinations.

With unique, the backward search runs over word boundaries instead of paths: a segmentation has one path per choice of dictionary entry for each of its words, which can be millions for a short line, and the search takes each segmentation once, with its cheapest path, without going through the others.

Parameters

The tokenize_nbest method accepts the following parameters:

ParameterTypeDescription
text&strThe text to tokenize.
nusizeNumber of N-best results to return.
uniqueboolWhen true, deduplicates results that produce the same word boundary positions: each result is the cheapest path of its word boundaries.
cost_thresholdOption<i64>When Some(threshold), only returns candidates with cost within best_cost + threshold, where best_cost is the cost of the first candidate (both are totals over the whole input).

Rust API example

use lindera::dictionary::load_dictionary;
use lindera::mode::Mode;
use lindera::segmenter::Segmenter;
use lindera::analysis::tokenizer::Tokenizer;
use lindera::LinderaResult;

fn main() -> LinderaResult<()> {
    let dictionary = load_dictionary("embedded://ipadic")?;
    let segmenter = Segmenter::new(Mode::Normal, dictionary, None);
    let tokenizer = Tokenizer::new(segmenter);

    let text = "すもももももももものうち";

    // Get top 3 tokenization results
    let results = tokenizer.tokenize_nbest(text, 3, false, None)?;

    for (rank, (tokens, cost)) in results.iter().enumerate() {
        println!("--- NBEST {} (cost={}) ---", rank + 1, cost);
        for token in tokens {
            let details = token.details().join(",");
            println!("{}\t{}", token.surface.as_ref(), details);
        }
    }

    Ok(())
}

Output:

--- NBEST 1 (cost=21245) ---
すもも  名詞,一般,*,*,*,*,すもも,スモモ,スモモ
も      助詞,係助詞,*,*,*,*,も,モ,モ
もも    名詞,一般,*,*,*,*,もも,モモ,モモ
も      助詞,係助詞,*,*,*,*,も,モ,モ
もも    名詞,一般,*,*,*,*,もも,モモ,モモ
の      助詞,連体化,*,*,*,*,の,ノ,ノ
うち    名詞,非自立,副詞可能,*,*,*,うち,ウチ,ウチ
--- NBEST 2 (cost=24541) ---
...

CLI example

echo "すもももももももものうち" | lindera tokenize --dict embedded://ipadic -N 3

Lattice reuse

For repeated tokenization, you can reuse a Lattice to reduce memory allocations:

#![allow(unused)]
fn main() {
use lindera::dictionary::Lattice;

let mut lattice = Lattice::default();
let results = tokenizer.tokenize_nbest_with_lattice(text, &mut lattice, 3, false, None)?;
}

At the segmenter level, SegmentWorker (created via Segmenter::new_worker) packages this pattern into a session object that owns the lattice and scratch buffers, and additionally bounds retained memory with an automatic shrink policy. See the Segmenter page for details.

User Dictionary

A user dictionary is a supplementary dictionary that allows you to register custom words alongside the system dictionary. This is useful for domain-specific terms, brand names, proper nouns, or any words that are not in the default system dictionary.

CSV format

The simplest user dictionary format is a CSV file with three columns:

<surface>,<part_of_speech>,<reading>

Example CSV content

東京スカイツリー,カスタム名詞,トウキョウスカイツリー
東武スカイツリーライン,カスタム名詞,トウブスカイツリーライン
とうきょうスカイツリー駅,カスタム名詞,トウキョウスカイツリーエキ

The surface (the first column) is used exactly as written: whitespace at its start or end is part of the word, and a surface may consist of whitespace only (IPADIC registers U+3000 this way). Only an empty surface is skipped. The same applies to the lexicon CSV files of a system dictionary built with lindera build.

An entry that ends with whitespace keeps it in its token, as in MeCab, even when whitespace is skipped in the lattice (the default for every bundled dictionary except SudachiDict, see Whitespace Handling). With whitespace skipped, however, an entry that starts with whitespace of the dictionary's SPACE category, or consists of such whitespace only, never matches, because no word starts on skipped whitespace. U+3000 is not in the SPACE category, so IPADIC's U+3000 entry still matches.

Each dictionary type (IPADIC, UniDic, ko-dic, etc.) also supports a detailed CSV format with full control over context IDs, costs, and all feature fields. See the Dictionaries section for the detailed format of each dictionary type.

Tied entries

Two entries tie when they have the same surface, the same left and right context IDs and the same cost: every path costs the same with either of them. A user entry that ties with a system entry is chosen over it, so a detailed entry that copies the surface, the context IDs and the cost of a system entry, for example to give the word another reading, takes its place in the output. MeCab chooses the system entry instead. Among tied user entries, the first row is chosen, as in MeCab (before v7, the last row was; see Migrating from v6 to v7). Simple entries get the cost and the context IDs from the dictionary's metadata.json (default_word_cost, default_left_context_id and default_right_context_id: -10000, 0 and 0 for IPADIC), so they practically never tie with a system entry.

Context IDs

The left and right context IDs of a detailed entry must address a cell of the connection matrix of the system dictionary it is used with: the left context ID must be below the second value of that dictionary's matrix.def header, and the right context ID below the first, the bounds lindera build applies to the system dictionary itself. IPADIC's matrix is 1316 x 1316, for example.

A user dictionary built from CSV together with its system dictionary is checked row by row as it is built, and the first row outside the matrix fails with an error that names its CSV line and surface:

  • lindera build --user when --system-dict names the system dictionary (DictionaryBuilder::with_system_matrix in the Rust API; see Context ID check)
  • a .csv user dictionary that the configuration names (Segmenter::from_config, TokenizerBuilder), that lindera tokenize --user-dict names, or that the TokenizerBuilder of the Python, Node.js, Ruby and PHP bindings is given by path
  • the loaders that load a user dictionary for a loaded system dictionary: load_user_dictionary_for in the Rust API, Python and Ruby, loadUserDictionaryFor in Node.js, Lindera\Dictionary::loadUserFor in PHP, and loadUserDictionaryFromBytesFor in WebAssembly
  • the builders that build a user dictionary for a loaded system dictionary: build_user_dictionary_for in Python and Ruby, buildUserDictionaryFor in Node.js, and Lindera\Dictionary::buildUserFor in PHP

For example, with a userdic.csv whose first row lies inside IPADIC's matrix and whose second row has the left context ID 60000:

$ echo 東京スカイツリーへ行く | lindera tokenize -d embedded://ipadic -u userdic.csv
Error: LinderaError { kind: Args, source: LinderaError(kind=Parse, source=failed to build tokenizer: LinderaError(kind=Content, source=user dictionary line 2 (surface: '東京スカイツリー'): left context id 60000 is out of range for the connection matrix (matrix.def size 1316 1316; left context ids must be below 1316)))

Otherwise the user dictionary is built without the system dictionary: a .bin that lindera build --user wrote without --system-dict or that a builder of the bindings wrote with metadata only (build_user_dictionary in Python and Ruby, buildUserDictionary in Node.js, Lindera\Dictionary::buildUser in PHP), or a user dictionary loaded with metadata only (load_user_dictionary in the Rust API, Python and Ruby, loadUserDictionary in Node.js, Lindera\Dictionary::loadUser in PHP, loadUserDictionaryFromBytes in WebAssembly). Its IDs are checked when the two dictionaries are combined: Segmenter::try_new, the configuration path, lindera tokenize --user-dict, and the Tokenizer constructors and builders of the language bindings return an error for the first entry outside the matrix. The error names the entry by its word id, which counts the rows from 0 in the order of their surfaces, and by its detail fields. The .bin built from the userdic.csv above gives:

$ echo 東京スカイツリーへ行く | lindera tokenize -d embedded://ipadic -u user_dict/userdic.bin
Error: LinderaError { kind: Args, source: LinderaError(kind=Parse, source=failed to build tokenizer: LinderaError(kind=Content, source=user dictionary entry 1 (名詞,固有名詞,一般,*,*,*,東京スカイツリー,トウキョウスカイツリー,トウキョウスカイツリー): left context id 60000 is out of range for the connection matrix (matrix.def size 1316 1316; left context ids must be below 1316)))

Segmenter::new does not check the IDs: an entry outside the matrix then makes segmentation panic when the entry matches the input. Before v7.0.0, every path behaved this way. Simple entries are checked too; they get the context IDs that the metadata.json they are built with sets (default_left_context_id and default_right_context_id, 0 in every bundled dictionary).

Rust API example

use std::fs::File;
use std::path::PathBuf;

use lindera::dictionary::{Metadata, load_dictionary, load_user_dictionary};
use lindera::error::LinderaErrorKind;
use lindera::mode::Mode;
use lindera::segmenter::Segmenter;
use lindera::analysis::tokenizer::Tokenizer;
use lindera::LinderaResult;

fn main() -> LinderaResult<()> {
    let user_dict_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
        .join("../resources")
        .join("user_dict")
        .join("ipadic_simple_userdic.csv");

    let metadata_file = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
        .join("../lindera-ipadic")
        .join("metadata.json");
    let metadata: Metadata = serde_json::from_reader(
        File::open(metadata_file)
            .map_err(|err| LinderaErrorKind::Io.with_error(anyhow::anyhow!(err)))
            .unwrap(),
    )
    .map_err(|err| LinderaErrorKind::Io.with_error(anyhow::anyhow!(err)))
    .unwrap();

    let dictionary = load_dictionary("embedded://ipadic")?;
    let user_dictionary = load_user_dictionary(user_dict_path.to_str().unwrap(), &metadata)?;
    // `try_new` checks the user dictionary's context IDs against the
    // dictionary's connection matrix (see "Context IDs" above).
    let segmenter = Segmenter::try_new(
        Mode::Normal,
        dictionary,
        Some(user_dictionary), // Using the loaded user dictionary
    )?;

    // Create a tokenizer.
    let tokenizer = Tokenizer::new(segmenter);

    // Tokenize a text.
    let text = "東京スカイツリーの最寄り駅はとうきょうスカイツリー駅です";
    let mut tokens = tokenizer.tokenize(text)?;

    // Print the text and tokens.
    println!("text:\t{}", text);
    for token in tokens.iter_mut() {
        let details = token.details().join(",");
        println!("token:\t{}\t{}", token.surface.as_ref(), details);
    }

    Ok(())
}

Output:

text:   東京スカイツリーの最寄り駅はとうきょうスカイツリー駅です
token:  東京スカイツリー        カスタム名詞,*,*,*,*,*,*,トウキョウスカイツリー,*
token:  の      助詞,連体化,*,*,*,*,の,ノ,ノ
token:  最寄り駅        名詞,一般,*,*,*,*,最寄り駅,モヨリエキ,モヨリエキ
token:  は      助詞,係助詞,*,*,*,*,は,ハ,ワ
token:  とうきょうスカイツリー駅        カスタム名詞,*,*,*,*,*,*,トウキョウスカイツリーエキ,*
token:  です    助動詞,*,*,*,特殊・デス,基本形,です,デス,デス

Note that the base_form field (the 7th CSV/detail field) is * rather than the surface form. The simple three-column user-dictionary schema only supplies surface, part_of_speech, and reading — every other field, including base_form, is filled with the dictionary's metadata.default_field_value (* for IPADIC).

Building a user dictionary with CLI

You can build a user dictionary from CSV to binary format using the CLI:

lindera build --src <user_dictionary.csv> --dest <dest_dir> --metadata <metadata.json> --user [--system-dict <system_dictionary>]

With --system-dict, the build checks the context IDs against the system dictionary the user dictionary is for (see Context IDs).

Binary vs CSV user dictionary

  • CSV format: Loaded and parsed at runtime. Convenient for development and small dictionaries.
  • Binary format: Pre-compiled for faster loading. Recommended for production use with large user dictionaries.

Both formats can be specified when creating a Segmenter. The binary format skips the CSV parsing step, resulting in faster startup times.

Filters

Lindera's analysis pipeline (the Tokenizer in lindera::analysis, provided by the lindera-analysis crate) has two extension points around the segmenter: character filters, which transform the raw input text before tokenization, and token filters, which transform the list of tokens after tokenization.

Input Text
  --> Character Filters (preprocessing)
  --> Tokenization
  --> Token Filters (postprocessing)
  --> Output Tokens

Character filters

Character filters preprocess the input text before it reaches the segmenter. They're typically used to normalize text so that tokenization is more consistent -- for example, converting full-width characters to half-width, canonicalizing Unicode representations, or expanding Japanese iteration marks into their repeated form. Because a character filter can change the length of the text, Lindera tracks every transformation it makes and corrects each token's byte offsets back to positions in the original, unfiltered text.

Token filters

Token filters postprocess the list of tokens produced by the segmenter. They're typically used to normalize or reduce the token list for search and analysis -- for example, replacing a token with its base (dictionary) form, converting a token's surface text between hiragana and katakana, removing tokens by part-of-speech tag, or removing stop words.

Configuring filters

Every filter -- character or token -- is identified by a kind string and configured with a JSON args object of filter-specific parameters. Filters run in the order they're added, and there are two ways to add them:

  • YAML configuration file: list filters under the character_filters and token_filters keys.

    character_filters:
      - kind: unicode_normalize
        args:
          kind: nfkc
    
    token_filters:
      - kind: japanese_base_form
    
  • Rust API: append filters to a Tokenizer, in order, with append_character_filter and append_token_filter.

    #![allow(unused)]
    fn main() {
    // Character filters run first, transforming the raw input text;
    // token filters run last, transforming the resulting token list.
    tokenizer
        .append_character_filter(BoxCharacterFilter::from(unicode_normalize_char_filter))
        .append_token_filter(BoxTokenFilter::from(japanese_base_form_filter));
    }

Available filters

Lindera ships 4 character filters and 21 token filters, covering Japanese, Korean, and general-purpose text normalization:

CategoryFilters
Character filtersunicode_normalize, japanese_iteration_mark, mapping, regex
Token filters -- normalizationjapanese_base_form, japanese_reading_form, korean_reading_form, japanese_kana, japanese_katakana_stem, japanese_number, korean_number, mapping, remove_diacritical_mark, lowercase, uppercase
Token filters -- tag-based filteringjapanese_keep_tags, japanese_stop_tags, korean_keep_tags, korean_stop_tags
Token filters -- word-based filteringkeep_words, stop_words
Token filters -- structural transformationjapanese_compound_word, korean_compound_word, korean_decompound, length

See the Filters reference for what each filter does, its parameters, and runnable YAML and Rust API examples.

Lindera CRF

Lindera CRF is a pure Rust implementation of Conditional Random Fields (CRFs), forked from rucrf. It provides a trainer and an estimator for CRFs with support for lattice structures.

Key Features

  • Lattices with variable length edges
  • L1, L2, and Elastic Net regularization
  • Multi-threaded training
  • Zero-copy deserialization with rkyv
  • no_std support via --no-default-features --features alloc

Contents

Changes from rucrf

  • Serialization backend: Switched from bincode to rkyv for zero-copy deserialization
  • Elastic Net regularization: Added Regularization::ElasticNet combining L1 and L2 penalties
  • Default regularization: Trainer::new regularizes with L2 and DEFAULT_LAMBDA (1.0), the objective of MeCab's learner with its default -c 1.0; rucrf's default is L1 with 0.1
  • Rust 2024 edition: Updated to Rust 2024 edition
  • Dependency updates: Updated argmin, argmin-math, hashbrown, etc.

Architecture

Module Structure

lindera-crf/src/
├── lib.rs                # Public API re-exports
├── bigram_table.rs       # Bigram weight indices of the training lattices
├── feature.rs            # FeatureSet, FeatureProvider
├── lattice.rs            # Edge, Node, Lattice
├── model.rs              # RawModel, MergedModel, Model trait
├── trainer.rs            # Trainer, Regularization, StopReason, TrainingStats
├── errors.rs             # Error types
├── forward_backward.rs   # Forward-backward algorithm
├── math.rs               # Mathematical utilities (logsumexp)
├── optimizers.rs         # Declares the optimizers module
├── optimizers/
│   └── lbfgs.rs          # L-BFGS optimization
└── utils.rs              # Utility traits

Key Components

FeatureProvider / FeatureSet

Manage per-label feature sets. Each FeatureSet holds unigram features and left/right bigram features for a given label (feature IDs only, no weights). FeatureProvider maps label IDs to FeatureSet instances. Weights are held separately, on RawModel (weights, unigram_weight_indices, bigram_weight_indices).

The bigram feature IDs are held in Arc<[Option<NonZeroU32>]>, so labels with the same bigram features can share one list: FeatureSet::with_shared_bigrams takes the lists as Arcs, while FeatureSet::new copies the slices it is given. When training drops the unused features, it filters each shared list once, and the labels that shared the list share the filtered one. lindera-trainer shares the lists among the labels whose rewritten left and right features are the same (#1263).

Lattice / Edge / Node

Lattice structure with variable-length edges for sequence labeling. Edge represents a candidate span with a label, while Node aggregates edges at a given position. The Lattice is constructed from input data and used by the model to find the best path.

Trainer

Trains a CRF model using L-BFGS optimization (OWL-QN when the regularization has an L1 part) with configurable regularization. As in MeCab's learner and libLBFGS, the first line search starts at a step of 1 / |d|, d being the first search direction, and every later one at a step of 1. The trainer accepts labeled lattice examples, computes the loss and its gradient via the forward-backward algorithm, and iteratively updates model weights. Trainer::train returns the trained RawModel; Trainer::train_with_stats also returns a TrainingStats with:

  • iterations: the number of iterations that ran, which is the maximum set with Trainer::max_iter when that stopped training, and fewer when the optimizer stopped earlier.
  • evaluations: the number of evaluations of the loss and its gradient, each one forward-backward pass over every lattice, line-search trials included.
  • stop_reason: why the optimizer stopped, a StopReason.

The optimizer stops at the first of these:

  • StopReason::Converged: the relative change of the objective, |previous - current| / |previous|, stayed below the tolerance set with Trainer::eta for 3 iterations in a row, or the optimizer stopped near the optimum, where the norm of the gradient (the pseudo-gradient for OWL-QN) is under 1e-5 times its norm at the initial weights, or vanishes. The default tolerance, DEFAULT_ETA (5e-5), is the default eta of MeCab's learner, which applies the same test to every evaluation, line-search trials included, where Lindera counts iterations. Trainer::eta(0.0) turns the test off. Near the optimum the objective changes by less than its rounding, and whether an iteration then leaves it unchanged or a line search fails depends on the platform; either is Converged there.
  • StopReason::NoProgress: an iteration left the objective unchanged far from the optimum, where the norm of the gradient was still at least 1e-5 times its norm at the initial weights. When this happens after a few iterations, the model is barely trained.
  • StopReason::MaxIterations: the cap set with Trainer::max_iter.
  • StopReason::LineSearchFailed: the line search of an iteration failed far from the optimum, where the norm of the gradient at the best weights of the run was still at least 1e-5 times its norm at the initial weights. The weights are the best ones of the run. A search direction with a component that is not finite, which the optimizer builds from a step that left the gradient unchanged, fails the line search too.

Regularization

Configurable regularization strategies:

  • L1: Sparse models via L1 penalty
  • L2: Smooth models via L2 penalty
  • ElasticNet: Combines L1 and L2 with a configurable l1_ratio

Trainer::new uses L2 with DEFAULT_LAMBDA (1.0): the objective of MeCab's learner with its default -c 1.0. MeCab adds ‖w‖² / (2C), so the coefficient is 1 / C in general.

Model (trait)

Interface for searching the best path through a lattice. Two implementations are provided:

  • RawModel: Stores weights in a flat vector indexed by feature ID
  • MergedModel: Optimized for inference; merges feature weights into a compact representation serializable with rkyv, using MergedFeatureSet as its per-label element type

MergedFeatureSet holds the pre-summed unigram weight for a label, along with its left_id/right_id bigram connection IDs into MergedModel's bigram weight matrix.

RawModel::unigram_weight computes that sum for a list of unigram feature IDs, and merge calls it for every label. A feature without a weight adds nothing, so it also weighs the features of an entry that was not a label in training: lindera-trainer uses it for the cost of a user lexicon entry.

Forward-backward Algorithm

Computes alpha (forward) and beta (backward) values over the lattice. Used during training to calculate the loss and the expected feature counts. One pass over a lattice yields both its loss and its share of the gradient. The optimizer keeps the result for the last parameter vector, because argmin's L-BFGS asks for the cost and the gradient of the same point in two calls.

Before the optimizer starts, a bigram table records, for every pair of edges that meet in the lattices, the weight indices of their bigram features, in the order the evaluation adds them; labels with the same bigram feature IDs share the entries. The evaluations read the indices from the table instead of looking each bigram feature up in a map, and add the same weights in the same order, so the trained weights do not change.

Feature Flags

FeatureDescriptionDefault
allocAlloc support for no_stdNo*
stdStandard library support (implies alloc)No*
trainTraining functionality (L-BFGS, multi-threading, logging)Yes

* Not listed directly in default, but transitively enabled by train (which is on by default and enables std, which in turn enables alloc) -- so alloc/std are active in a default build.

API Reference

The API reference is available. Please see following URL:

Lindera Dictionary

Lindera Dictionary is the base library for morphological analysis dictionaries. It provides dictionary loading, building, and Viterbi-based segmentation. CRF-based dictionary training lives in the separate lindera-trainer crate. Through the lindera facade its modules are available under lindera::dictionary — lindera::dictionary::core (the dictionary data structures), lindera::dictionary::builder, lindera::dictionary::viterbi, and so on — so a direct lindera-dictionary dependency is rarely needed.

Key Features

  • Dictionary loading from filesystem or embedded data
  • Dictionary building from MeCab-format CSV source files
  • Viterbi algorithm for optimal segmentation
  • N-best path generation (Forward-DP Backward-A*)
  • Memory-mapped file support

Contents

Architecture

Module Structure

lindera-dictionary/src/
├── lib.rs               # Public API
├── dictionary.rs        # Dictionary, UserDictionary
├── builder.rs           # DictionaryBuilder
├── loader.rs            # DictionaryLoader trait, FSDictionaryLoader
├── viterbi.rs           # Lattice, Edge, Viterbi segmentation
├── nbest.rs             # NBestGenerator, UniqueNBestGenerator (Forward-DP Backward-A*)
├── mode.rs              # Mode (Normal/Decompose), Penalty
├── error.rs             # LinderaError, LinderaErrorKind
├── assets.rs            # Download and file management
├── macros.rs            # embedded_dictionary! macro shared by dictionary crates
├── dictionary/
│   ├── character_definition.rs    # Character type definitions
│   ├── connection_cost_matrix.rs  # Connection cost matrix
│   ├── context_id_map.rs          # ContextIdMap: connection-cost context-ID remap
│   ├── prefix_dictionary.rs       # Prefix dictionary (char-wise double-array trie)
│   ├── unknown_dictionary.rs      # Unknown word handling
│   ├── metadata.rs                # Dictionary metadata
│   └── schema.rs                  # Schema definitions

Key Components

Dictionary / UserDictionary

Main data structures holding the compiled dictionary data. A Dictionary contains the character definitions, connection cost matrix, prefix dictionary, and unknown word dictionary. The prefix dictionary is a char-wise double-array trie, built with crawdad at build time and walked in place over its serialized bytes at runtime — loading it is an O(1) header check, not a deserialization pass. UserDictionary allows users to add custom vocabulary on top of the system dictionary; unlike the system dictionary, it still uses a daachorse automaton inside its rkyv-archived .bin files.

DictionaryBuilder

Fluent API for building dictionaries from source CSV files. It compiles MeCab-format dictionary sources into the binary format used at runtime. Its four build stages (metadata, unknown dictionary, prefix dictionary, connection cost matrix) run concurrently on scoped threads on non-wasm targets, falling back to a sequential build on wasm (which has no OS threads); the concurrent path has higher peak memory, since all four stages' working sets are held at once.

DictionaryLoader / FSDictionaryLoader

DictionaryLoader is a trait for loading compiled dictionaries. FSDictionaryLoader is the filesystem-based implementation that reads dictionary files from a directory, with optional memory-mapped file support.

Embedded Dictionary Macro

The embedded_dictionary! macro (lindera-dictionary/src/macros.rs, #[macro_export]) generates the boilerplate each dictionary crate needs to bake its compiled dictionary into the binary: a load() function that reads the dictionary components via include_bytes! and a loader struct implementing DictionaryLoader. Every dictionary crate's embedded.rs (lindera-ipadic, lindera-ipadic-neologd, lindera-unidic, lindera-ko-dic, lindera-cc-cedict, lindera-jieba) invokes this macro instead of duplicating the loading logic.

Viterbi (Lattice, Edge)

Builds a lattice of candidate tokens from the input text and finds the optimal segmentation path using the Viterbi algorithm. Each Edge in the lattice represents a candidate token with associated costs (word cost + connection cost).

NBestGenerator

Generates N-best segmentation paths using the Forward-DP Backward-A* algorithm. This enables applications to consider alternative segmentations beyond the single best path.

UniqueNBestGenerator

Generates the N-best distinct segmentations: for each sequence of word boundaries, its cheapest path, in ascending order of cost. Unique N-best (unique, --nbest-unique) uses it. A segmentation has one path per choice of dictionary entry for each of its words, which can be millions of paths for a short line, so the search runs backward over suffixes of word boundaries instead of paths: one search node holds every candidate of a span, and its cost is exact, so each segmentation comes out once and the work grows with the number of results and the length of the line, not with the number of paths. The first result is the 1-best path, and of identical entries (same surface, context ids and cost) every result holds the first CSV row, as the 1-best path does.

Mode

Controls tokenization behavior:

  • Normal: Standard tokenization using the optimal Viterbi path
  • Decompose: Further splits compound nouns based on configurable Penalty thresholds

Dictionary Format Version

A built dictionary directory records the on-disk layout it was written in, as format_version in metadata.json. DictionaryBuilder::build_metadata stamps the value from DICTIONARY_FORMAT_VERSION (lindera-dictionary/src/dictionary/metadata.rs) — the builder is the only thing that can state truthfully what it just wrote, so a value in the source metadata.json is ignored. Loading a dictionary directory checks the recorded version and fails with an actionable message if it does not match.

The check matters because most artifacts have no header of their own: matrix.mtx, dict.vals and dict.words are raw arrays, so a dictionary from an older layout that happens to be a plausible length decodes into garbage rather than failing. A source metadata.json — the hand-written kind checked into each dictionary crate — describes build inputs, carries no format_version, and is never format-checked.

Bump DICTIONARY_FORMAT_VERSION on any change to the bytes of a built artifact. That includes upgrading a dependency whose serialized form is written verbatim (crawdad for dict.trie, rkyv for char_def.bin and unk.bin, daachorse inside user dictionary .bin files), which is easy to overlook because no line of this crate changes. The build cache is keyed on the format version for exactly that reason.

The current format version is 3. Version 2 (v6.0.0) replaced the system prefix dictionary's byte-wise daachorse Aho-Corasick automaton (dict.da) with a char-wise double-array trie (dict.trie) plus a u32 prefix-sum index (dict.valsidx). Version 3 (v7.0.0) lists each code point's default category first in its char_def.bin row, followed by its other categories in category id order, because unknown words come from the default category only, as in MeCab; a version 2 row would silently give the wrong default category. Version 3 also covers the earlier 7.0.0 changes to what the builder writes (see Migrating from v6 to v7). Dictionaries at version 2 or earlier are rejected at load time with an actionable error. User dictionary .bin files were not affected by that change — but they are written by daachorse, so a daachorse upgrade does invalidate them, which is exactly why it belongs in the list above.

Context ID Remapping

Dictionary metadata (lindera-dictionary/src/dictionary/metadata.rs) carries a connection_id_mapping: bool flag and an optional context_id_map: Option<ContextIdMap>. When a dictionary crate (e.g. lindera-unidic) enables connection_id_mapping, DictionaryBuilder relabels the connection matrix's left/right context IDs by access frequency at build time, so that frequently-used connection-cost cells cluster together for better cache locality; DictionaryBuilder::with_context_id_freq attaches an optional bundled frequency histogram used to rank the IDs, and the remap itself is computed in lindera-dictionary/src/builder/context_id_remap.rs. ContextIdMap (lindera-dictionary/src/dictionary/context_id_map.rs) holds the resulting left/right permutation and is persisted into the built metadata.json so the same mapping can be reapplied later. Because user dictionaries are always compiled in the original, un-remapped ID space, UserDictionary::remap_context_ids (lindera-dictionary/src/dictionary.rs) uses the persisted ContextIdMap to relabel a user dictionary's context IDs into the same space as the system dictionary it is attached to. The remap is a bijective relabeling, so it does not change tokenization output, only lookup locality.

Training

The CRF-based dictionary training pipeline lives in the separate lindera-trainer crate, which builds on this crate's runtime types. See the training pipeline documentation for details.

Feature Flags

FeatureDescriptionDefault
mmapMemory-mapped file support for filesystem-based dictionary loading (the word-list files, the connection-cost matrix, and the trie all stay lazily paged; nothing is fully materialized at load)Yes
build_rsHTTP download for dictionary sourcesNo
ctxfreqExperimental: instruments connection-matrix access-frequency profiling, used to build the context-ID frequency remapNo

API Reference

The API reference is available. Please see following URL:

Lindera Trainer

Lindera Trainer implements the CRF-based dictionary training pipeline (lindera train), turning an annotated corpus and a seed lexicon into a trained model that can be exported to MeCab-format dictionary source files and compiled into a binary dictionary. It builds on the runtime types of lindera-dictionary and the CRF core of lindera-crf. The crate can be used directly, or through the lindera facade's train feature, which re-exports it as lindera::dictionary::trainer.

Key Features

  • CRF-based weight learning via lindera-crf, with L1, L2, and Elastic Net regularization
  • MeCab-compatible feature template parsing (feature.def: %F[n], %L[n], %R[n], %w, %u, %l, %r, and their optional ? variants)
  • MeCab-compatible 3-section feature rewriting (rewrite.def: unigram / left / right rewrite rules)
  • Dictionary-format agnostic: works with any lexicon whose columns follow surface,left_id,right_id,cost,feature... (IPADIC, UniDic, ko-dic, CC-CEDICT, etc.)
  • Training lattices built as MeCab's learner builds them: the seed words at every position, and the unknown words that the runtime creates from char.def and unk.def, with one CRF label per unk.def entry
  • Connection cost matrix and MeCab-compatible cost conversion (tocost) derived from learned CRF weights
  • Zero-copy rkyv binary model serialization with a format version, with legacy JSON fallback on read
  • Direct export of Lindera/MeCab dictionary source files (lex.csv, matrix.def, unk.def, char.def, feature.def, rewrite.def, left-id.def, right-id.def)

Contents

Architecture

Module Structure

lindera-trainer/src/
├── lib.rs                # Trainer: CRF training orchestration; public API re-exports
├── config.rs             # TrainerConfig: parses seed lexicon, char.def, unk.def, feature.def, rewrite.def
├── corpus.rs              # Corpus, Example, Word: training data representation
├── feature_extractor.rs  # FeatureExtractor: feature template parsing and feature ID management
├── feature_rewriter.rs   # DictionaryRewriter: MeCab-compatible 3-section rewrite
└── model.rs               # Model, SerializableModel: trained model storage, serialization, dictionary output

Key Components

TrainerConfig

Parses the five training input files -- seed lexicon (lex.csv), character definition (char.def), unknown-word definition (unk.def), feature template (feature.def), and rewrite rules (rewrite.def) -- into the configuration consumed by Trainer. TrainerConfig::from_readers (or from_paths, its file-path convenience wrapper) extracts the surface/feature vocabulary from the seed lexicon, builds a FeatureExtractor from the parsed feature templates, builds a DictionaryRewriter from the rewrite rules, and assembles an in-memory lindera_dictionary::dictionary::Dictionary from the seed lexicon, the character definition and the unknown-word definition. char.def is parsed by the dictionary builder (CharacterDefinitionBuilder::build_from_str), so training sees the same character categories as the built dictionary; the default category of each character, which the %t feature uses, comes from the builder's build_default_category_table. The Dictionary (dict()) holds what the training lattices are built from: its prefix_dictionary (system_lexicon()) is the seed lexicon, a prefix dictionary over the seed surfaces whose word ID is the row index, and its unknown_dictionary (unk_handler()) holds the unk.def entries, read with the dictionary builder's parse_unk, one entry per line in file order. Their costs and context IDs are not used in training, and the Dictionary's connection matrix is a placeholder. TrainerConfig also keeps the seed feature strings and the unk.def entries as (category, feature string) pairs in file order, from which the labels and the exported unk.def are made. In unk.def, lines that start with # and empty lines are skipped, and a line with fewer than five fields, or whose context IDs or cost are not numbers, is an error that names the line. As mecab-dict-index and lindera build do, from_readers also rejects a char.def range line that names a category not defined on an earlier line, a category defined twice, an unk.def line whose category char.def does not define, and a char.def category without an unk.def line (lindera_dictionary::dictionary::unknown_dictionary::check_unknown_word_categories), so that such input fails before training rather than when the exported files are built. It also exposes surfaces() and features(), the surfaces and the feature strings of the seed rows in row order, get_features(), a user lexicon accessible via user_lexicon() / add_user_lexicon_entry() / load_user_lexicon_from_content(), and metadata().

Corpus / Example / Word

Represent annotated training data. Word pairs a surface form with its (comma-joined) feature string. Example is one training sentence, built from a Vec<Word> whose surfaces are concatenated to reconstruct the original sentence text. Corpus is a collection of Examples; Corpus::from_reader parses tab-separated surface<TAB>features lines, treating blank lines or literal EOS lines as sentence boundaries.

FeatureExtractor

Parses MeCab-compatible feature templates and manages the mapping from generated feature strings to interned, NonZeroU32 feature IDs. Supported template placeholders are %F[n] / %F?[n] (feature field at index n, the ? form skipped when the value is *), %t (character category), %w (surface form, unigram only), %u / %l / %r (full rewritten unigram/left/right feature string), and %L[n] / %L?[n] / %R[n] / %R?[n] for bigram left/right context fields. A template whose %F[n], %L[n] or %R[n] reads past the last column gives no feature, with or without ?, as in MeCab's learner. extract_unigram_feature_ids[_with_ctx], extract_left_feature_ids[_with_ctx], and extract_right_feature_ids[_with_ctx] apply the parsed templates to a feature vector (plus, for bigram extraction, an optional TemplateContext carrying surface/ufeature/lfeature/rfeature) and return the resulting feature IDs, creating new IDs on first use. The IDs live in three FeatureIdMaps, read through unigram_feature_ids(), left_feature_ids() and right_feature_ids(): the k-th string a map meets gets ID k, an ID is never reused (a feature that training removed and that comes again gets a new one), and the strings are kept back to back in one buffer with only the IDs in the hash table, since the unigram map of a full dictionary holds millions of strings (#1286).

The trainer takes the left and right feature IDs of a label from a cache keyed by the label's rewritten left and right features: the left and right templates read nothing else, so the pair decides the IDs. The first label with a pair makes them with extract_left_feature_ids_with_ctx and extract_right_feature_ids_with_ctx, so new IDs are minted in the order they would be without the cache, and the labels with the same pair share the lists (the 347,344 rows of mecab-ipadic have 1,329 pairs). Training forgets the cache once it removes the unused features. A template is expanded into a buffer that the extractor keeps, so a feature string is allocated only when it is new (#1263).

DictionaryRewriter

Implements MeCab's 3-section rewrite.def format: [unigram rewrite], [left rewrite], and [right rewrite] sections, each holding an ordered list of pattern<TAB>replacement rules built into a FeatureRewriter prefix trie (via the internal FeatureRewriterBuilder). DictionaryRewriter::from_reader parses all three sections (falling back to treating an unsectioned file as legacy right-rewrite-only content, for backward compatibility with older Lindera rewrite.def files). rewrite() applies all three rewriters to a feature string and returns (ufeature, lfeature, rfeature), passing a section through unchanged when no rule matches. As in MeCab, the feature and the pattern and replacement of each rule are split into columns with the rules of MeCab's tokenizeCSV (the crate-private feature_csv::split_feature), so a quoted column with , in it is one column, and rewrite() joins the rewritten columns with ,, quoting a column that contains , or ". The trainer splits the three results the same way for the feature templates, and has them written into buffers it reuses from one label to the next, so that the rewrite allocates only when a buffer grows (#1286). (An earlier rewrite_cached() memoized per full feature string, but real lexicon features are unique per row, so the cache never hit and was removed — #975.)

Trainer

Trainer::new creates the CRF labels: one per seed row, in row order (label row + 1, so that a match in the training lexicon, whose word ID is the row index, maps to its label), then one per unk.def entry, in file order, whose %t is the ID of the entry's category. Trainer::train builds one lindera_crf::Lattice per example, as MeCab's learner does:

  • The candidates at each position are the seed words that start there and the unknown words that the runtime creates there, listed by lindera_dictionary::viterbi::Lattice::unknown_word_candidates: those of the character's default category, with its INVOKE, GROUP and LENGTH settings, for every unk.def entry of the category.
  • Each 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.def entry, in file order, of an unknown-word candidate with the same span whose feature columns other than * all equal the gold token's (both split as rewrite() splits a feature); else a virtual label built from the gold feature string (%t 0, no %w, one per distinct feature string), as MeCab's learner adds a virtual node. Virtual labels follow the other labels, are trained, and are never exported.
  • Every other candidate becomes a negative edge.

Once the lattices are built, it prints a Lattice statistics: line with the number of gold tokens labelled each way (gold_lexicon, gold_unknown, gold_virtual), the number of virtual labels (virtual_labels), and the negative edges from the seed lexicon and from the unknown-word candidates (negative_lexicon, negative_unknown). When there are gold tokens and none is a seed word, or the gold tokens whose surface has seed rows but none with their feature string outnumber the seed words, it then writes a warning on stderr with eprintln!, not through the log crate, for which the bindings install no logger. The warning gives the first such gold token next to the feature strings of the first five seed rows with its surface. Trainer::max_grouping_len caps unknown-word grouping with the runtime's meaning (characters beyond the first; None, the default, leaves it unbounded); the other builder methods set the regularization (L2 with DEFAULT_LAMBDA, 1.0, by default; use_l2(false) selects L1), the iteration cap, the tolerance of the convergence test (Trainer::eta, default DEFAULT_ETA, 5e-5; see the lindera-crf architecture) and the thread count. After training it prints why the optimizer stopped, with the number of iterations and of evaluations of the loss and its gradient. When an iteration left the objective unchanged or the line search failed far from the optimum, it then writes a warning on stderr, with eprintln! as for the warning about seed words.

Model / SerializableModel

Model is the trained model produced by Trainer::train. It owns the trained lindera_crf::RawModel, the TrainerConfig used to train it, the extracted feature weights and labels, the feature strings of the virtual labels, and any user lexicon entries added afterward via read_user_lexicon. It converts learned CRF weights into MeCab-compatible integer costs (tocost(weight, cost_factor) = clamp(round(-weight * cost_factor), i16::MIN, i16::MAX), i.e. clamped to [-32768, 32767]), with the cost factor computed to make full use of the i16 range (calculate_cost_factor). Model can serialize itself (write_model) and export dictionary source files directly.

SerializableModel is the plain-data, rkyv-serializable counterpart returned by Model::read_model when loading a previously trained model back from a reader (as done by the lindera export CLI command). It carries the same trained information as Model -- feature weights, labels, POS info, connection cost matrix, the unk.def entries (unk_entries, (category, feature string) pairs in file order), preserved char.def/feature.def/rewrite.def content, cost factor, and left/right context ID maps -- as owned data, without a TrainerConfig, and exposes its own writer methods for producing the dictionary export files.

Model public methods

MethodDescription
read_user_lexicon<R: Read>(&mut self, rdr: R) -> Result<()>Loads a user-defined lexicon (same surface,left_id,right_id,cost,feature... CSV format as the seed lexicon) into the model so that write_dictionary can fill in the parameters its entries do not give. As in MeCab's mecab-dict-index, the cost and the connection IDs are filled in separately: an empty cost is computed, and when either connection ID is empty or negative, both are inferred. An entry whose parameters are 0,0,0 gets all three, as in Vibrato's trainer (mecab-dict-index keeps them). A connection ID or cost that is neither empty nor a number in its range is an error that names the entry, and no entry is added then; mecab-dict-index reads such a value as a number with atoi, but lindera build rejects it. Must be called before writing the dictionary if a user lexicon is needed.
write_model<W: Write>(&self, writer: &mut W) -> Result<()>Serializes the full trained model (feature weights, labels, POS info, connection matrix, unk.def entries, preserved definition file contents, cost factor, left/right ID maps) to rkyv binary format, with metadata.version set to model::MODEL_FORMAT_VERSION (3.0.0). The stored matrix size covers every context ID of the model, whether or not a weight survived for it. The output is byte-reproducible: the connection matrix is an ordered map and the unk.def entries a Vec, so the bytes depend only on the model's contents.
read_model<R: Read>(reader: R) -> Result<SerializableModel>Associated function. Deserializes a SerializableModel from data written by write_model. It tries rkyv first, and falls back to legacy JSON only when the payload actually begins a JSON object; anything else is reported as a format mismatch with the instruction to re-train. A model, in either format, whose metadata.version is not MODEL_FORMAT_VERSION is rejected with the same instruction, so every model written before 7.0.0 must be retrained. Backfills feature_sets from feature_weights for older models that predate that field.
write_dictionary<W1, W2, W3, W4>(&self, lexicon_wtr, connector_wtr, unk_handler_wtr, user_lexicon_wtr) -> Result<()>Convenience method that writes the lexicon, connection cost matrix, unknown-word dictionary, and user lexicon in one call. The user lexicon is written from the entries loaded by read_user_lexicon, in input order. An entry without connection IDs gets those that lex.csv gives the seed row whose feature is the most similar to its own. An entry without a cost gets the one MeCab's calcCost computes: the sum of the trained weights of its unigram features, converted with tocost and the cost factor of lex.csv. Its features are made as a seed row's are, from the rewritten feature, the default category of its first character (%t) and its surface (%w, which MeCab does not accept in a unigram template), so an entry whose feature and first character's category are those of a seed row gets the cost lex.csv gives that row; a feature the model has no weight for adds nothing. A parameter the entry gives is written as it is. The most similar row is the one with the highest score, where each column equal to the entry's at the same position scores more the earlier it is, and the first one on a tie. An entry without connection IDs whose feature has no column in common with any seed row is an error, as in MeCab's mecab-dict-index, and nothing is written then.
write_lexicon<W: Write>(&self, writer: &mut W) -> Result<()>Writes lex.csv-style entries (surface,left_id,right_id,cost,features...) for every seed vocabulary entry, using the connection IDs and costs from the merged CRF model.
write_connection_costs<W: Write>(&self, writer: &mut W) -> Result<()>Writes a dense matrix.def-style connection cost matrix over every (right_id, left_id) pair of the merged model's context IDs (including 0 for BOS/EOS). A pair gets its trained weight converted with tocost; a pair without a trained weight, because its bigram features never met in training or regularization zeroed their weights, gets 0, the cost the transition has in training and in MeCab's mecab-dict-gen. It is the matrix that SerializableModel::write_connection_costs writes for the model that write_model writes.
write_unknown_dictionary<W: Write>(&self, writer: &mut W) -> Result<()>Writes one unk.def line for each unk.def entry the model was trained with, in file order, with the entry's category name and feature string and its learned connection IDs and cost.
get_unknown_word_cost(&self, category: usize) -> i32Returns the input cost of a char.def category index, read from unk.def's cost column rather than learned; for a category with several entries, the cost of its last line. Defaults to 2000 if the category has no configured cost.
num_features(&self) -> usizeNumber of feature weights held by the trained model.
num_labels(&self) -> usizeNumber of exported labels in the trained model: the seed rows plus the unk.def entries. The virtual labels are not counted.
raw_model(&self) -> &lindera_crf::RawModelAccess to the underlying lindera-crf raw model, for advanced or lower-level operations.
write_bigram_details<L: Write, R: Write, C: Write>(&self, left_wtr, right_wtr, cost_wtr) -> Result<()>Writes three diagnostic files describing bigram features and costs (left feature list, right feature list, and one line per raw bigram feature pair with BOS/EOS for id 0 and names from the feature extractor). All three files are byte-reproducible for the same model.
evaluate(&self, test_lattices: &[lindera_crf::Lattice]) -> f64Returns the mean absolute value of the raw model's feature weights as a simple evaluation score. The test_lattices argument is currently unused; it does not yet score the model against held-out data.
write_dictionary_buffers(&self, lexicon, connector, unk_handler, user_lexicon: &mut Vec<u8>) -> Result<()>Serializes labels, feature weights, user-entry count, and surfaces into four raw rkyv byte buffers -- a lower-level alternative to the CSV-oriented write_dictionary.
write_left_id_def<W: Write>(&self, writer: &mut W) -> Result<()>Writes left-id.def: 0 BOS/EOS, then each left context ID of the merged model -- the IDs that write_dictionary writes to lex.csv, unk.def and matrix.def -- with the left feature of the first label with that ID. It is the file that SerializableModel::write_left_id_def writes for the model that write_model writes: both use one mapping.
write_right_id_def<W: Write>(&self, writer: &mut W) -> Result<()>Writes right-id.def: 0 BOS/EOS, then each right context ID of the merged model with the right feature of the first label with that ID, as write_left_id_def does for the left IDs. It is the file that SerializableModel::write_right_id_def writes for the model that write_model writes.

SerializableModel public methods

Available on the value returned by Model::read_model, for re-exporting dictionary source files from a previously trained and serialized model (used by the lindera export CLI command and the export functions of the Python, Node.js, Ruby and PHP bindings):

MethodDescription
export(&self, output_dir: &Path, base_metadata: Option<&Path>) -> Result<Vec<PathBuf>>Writes the dictionary source files to output_dir, which it creates when it does not exist, with the methods below: lex.csv, matrix.def, unk.def, char.def, feature.def, rewrite.def, left-id.def and right-id.def, and metadata.json (update_metadata_json) when base_metadata is given. Returns the paths written, in that order. lindera export and the bindings' export functions call it, so they write the same files. metadata.json is made first, so a base metadata.json that cannot be read fails with no file written; an error names the file.
write_lexicon<W: Write>(&self, writer: &mut W) -> Result<()>Writes lex.csv-style entries from the stored feature_sets, labels, and pos_info, skipping the unk.def entry labels that follow the seed rows.
write_connection_costs<W: Write>(&self, writer: &mut W) -> Result<()>Writes the dense matrix.def-style connection cost matrix from the stored connection_matrix, max_left_id, and max_right_id: max_right_id + 1 rows by max_left_id + 1 columns, which cover every context ID of the model even when no bigram weight survived training. A cell missing from connection_matrix, a connection without a trained weight, gets 0, as in Model::write_connection_costs.
write_unknown_dictionary<W: Write>(&self, writer: &mut W) -> Result<()>Writes one unk.def line for each stored entry of unk_entries, in file order, with its category name, learned connection IDs and cost, and feature string.
write_char_def<W: Write>(&self, writer: &mut W) -> Result<()>Writes the char.def content preserved verbatim from the original training input.
write_feature_def<W: Write>(&self, writer: &mut W) -> Result<()>Writes the feature.def content preserved verbatim from the original training input.
write_rewrite_def<W: Write>(&self, writer: &mut W) -> Result<()>Writes the rewrite.def content preserved verbatim from the original training input.
write_left_id_def<W: Write>(&self, writer: &mut W) -> Result<()>Writes left-id.def from the stored left_id_map, with 0 BOS/EOS as the first line.
write_right_id_def<W: Write>(&self, writer: &mut W) -> Result<()>Writes right-id.def from the stored right_id_map, with 0 BOS/EOS as the first line.
update_metadata_json<W: Write>(&self, base_metadata_path: &Path, writer: &mut W) -> Result<()>Reads a base metadata.json, updates it with training-derived values (default_word_cost estimated from the median feature weight, plus a model_info section with feature/label counts, context ID ranges, the matrix.def size as connection_matrix_size ({rows}x{columns}), and training metadata), and writes the result. The output carries no timestamp and is byte-reproducible for the same model.

ModelMetadata and FeatureSetInfo

ModelMetadata records summary information about a training run: version (the model format version, MODEL_FORMAT_VERSION), regularization (the cost coefficient used), regularization_type (the RegularizationType used, exported as model_info.regularization_type), elastic_net_l1_ratio (the L1 ratio, None unless the type is Elastic Net; exported for Elastic Net only), iterations (the number of L-BFGS iterations that ran, at most the maximum set with Trainer::max_iter; exported as model_info.training_iterations), feature_count, and label_count. It is embedded in SerializableModel::metadata.

FeatureSetInfo holds the per-label connection information extracted from the merged CRF model after training: left_id, right_id, and weight. SerializableModel::feature_sets stores one entry per CRF label: one for each entry of labels, in the same order, then one for each virtual label, which is not exported.

API Reference

The API reference is available. Please see following URL:

Lindera Library

The lindera crate is the facade of the Rust API: it re-exports the morphological segmenter from lindera-segmenter (lindera::segmenter, lindera::dictionary, lindera::mode, ...) and, with the default analysis feature, the analysis chain from lindera-analysis as lindera::analysis. lindera = "7" is the only dependency you need; default-features = false gives you the segmenter alone. This section covers segmentation, error handling, and API reference.

If you need the Tokenizer, character filters, or token filters (a Lucene-style analysis chain built on top of Segmenter), use lindera::analysis — see the Lindera Analysis chapter, including its Configuration and Filters pages.

Segmenter

The Segmenter is the core component that performs morphological analysis. It uses the Viterbi algorithm to find the optimal segmentation of input text based on a dictionary and cost model. Among dictionary entries that share the surface, the context IDs and the cost, and so tie, it chooses the first CSV row, as MeCab does, and a user-dictionary entry over a tied system entry (see Tied entries). Of two segmentations of equal cost that end at the same position, it chooses the one whose last word starts later, as MeCab does.

Creating a Segmenter

A Segmenter requires three components:

  • Mode - the tokenization strategy (Normal or Decompose)
  • Dictionary - a system dictionary for morphological analysis
  • UserDictionary (optional) - a supplementary dictionary for custom words
#![allow(unused)]
fn main() {
use lindera::dictionary::load_dictionary;
use lindera::mode::Mode;
use lindera::segmenter::Segmenter;

let dictionary = load_dictionary("embedded://ipadic")?;
let segmenter = Segmenter::new(Mode::Normal, dictionary, None);
}

Segmenter::new does not check a user dictionary against the system dictionary. Segmenter::try_new takes the same arguments and returns an error when an entry's context ID lies outside the system dictionary's connection matrix; Segmenter::new accepts such an entry, and segmentation then panics when the entry matches the input (see Context IDs). Building from a configuration checks too. load_user_dictionary_for loads a user dictionary for a loaded system dictionary and checks a .csv already as it builds it, so the error names the row's CSV line and surface; try_new names an entry of a .bin, or of a user dictionary loaded with load_user_dictionary and metadata only, by its word id.

#![allow(unused)]
fn main() {
use lindera::dictionary::{load_dictionary, load_user_dictionary_for};
use lindera::mode::Mode;
use lindera::segmenter::Segmenter;

let dictionary = load_dictionary("embedded://ipadic")?;
let user_dictionary = load_user_dictionary_for("userdic.csv", &dictionary)?;
let segmenter = Segmenter::try_new(Mode::Normal, dictionary, Some(user_dictionary))?;
}

Tokenization Modes

Mode::Normal

Standard tokenization based on the dictionary entries. Words are segmented faithfully according to what is registered in the dictionary.

#![allow(unused)]
fn main() {
use lindera::mode::Mode;

let mode = Mode::Normal;
}

Mode::Decompose

Decomposes compound nouns into their constituent parts. This mode applies a configurable penalty to long compound words, encouraging the segmenter to split them into shorter components.

For example, with Mode::Normal, the compound word "関西国際空港" in the sentence "関西国際空港限定トートバッグ" remains part of a single token, while with Mode::Decompose, it is split into "関西", "国際", and "空港".

#![allow(unused)]
fn main() {
use lindera::mode::Mode;

let mode = Mode::Decompose(Default::default());
}

Dictionary Loading

Lindera provides the load_dictionary function to load dictionaries from various sources.

Embedded Dictionaries

When built with the appropriate feature flag (e.g., embed-ipadic), dictionaries can be loaded directly from the binary:

#![allow(unused)]
fn main() {
use lindera::dictionary::load_dictionary;

let dictionary = load_dictionary("embedded://ipadic")?;
}

Available embedded dictionary URIs:

  • embedded://ipadic - IPADIC (Japanese)
  • embedded://ipadic-neologd - IPADIC NEologd (Japanese)
  • embedded://unidic - UniDic (Japanese)
  • embedded://ko-dic - ko-dic (Korean)
  • embedded://cc-cedict - CC-CEDICT (Chinese)
  • embedded://jieba - Jieba (Chinese)

External Dictionaries

Pre-built dictionary directories can be loaded from the filesystem:

#![allow(unused)]
fn main() {
use lindera::dictionary::load_dictionary;

let dictionary = load_dictionary("/path/to/dictionary")?;
}

Using with Tokenizer

The Segmenter is typically used through the Tokenizer, which adds support for character filters and token filters:

use lindera::dictionary::load_dictionary;
use lindera::mode::Mode;
use lindera::segmenter::Segmenter;
use lindera::analysis::tokenizer::Tokenizer;
use lindera::LinderaResult;

fn main() -> LinderaResult<()> {
    let dictionary = load_dictionary("embedded://ipadic")?;
    let segmenter = Segmenter::new(Mode::Normal, dictionary, None);
    let tokenizer = Tokenizer::new(segmenter);

    let text = "日本語の形態素解析を行うことができます。";
    let tokens = tokenizer.tokenize(text)?;

    for mut token in tokens {
        let details = token.details().join(",");
        println!("{}\t{}", token.surface.as_ref(), details);
    }

    Ok(())
}

Note the mut binding on token: Token::details takes &mut self, so iterating with a plain for token in tokens fails to compile (E0596: cannot borrow as mutable).

Building from Config

Segmenter::from_config builds a Segmenter from a SegmenterConfig (a serde_json::Value), the same configuration format used by Tokenizer/TokenizerBuilder (see Configuration) but scoped to just the segmenter: section:

#![allow(unused)]
fn main() {
use serde_json::json;
use lindera::segmenter::{Segmenter, SegmenterConfig};

let config: SegmenterConfig = json!({
    "mode": "normal",
    "dictionary": "embedded://ipadic",
    "keep_whitespace": false,
    "use_mmap": false
});
let segmenter = Segmenter::from_config(&config)?;
}

Note: use_mmap here is set to false only to illustrate the option explicitly; omitting it entirely gives the same default (true, see below).

Segmenter::from_config_with_dictionaries applies a configuration to dictionaries that are already loaded, such as ones loaded from bytes. A dictionary passed as Some is used instead of the one the dictionary key names, and the key may then be absent (use_mmap does not apply); a user dictionary passed as Some is used instead of the one the user_dictionary key names. For None, the dictionary is loaded from the configuration as from_config loads it, and a configured user dictionary is loaded for the system dictionary in use with load_user_dictionary_for: a .csv is built with its metadata and checked against its connection matrix. Every other key applies as with from_config:

#![allow(unused)]
fn main() {
use serde_json::json;
use lindera::dictionary::load_dictionary;
use lindera::segmenter::{Segmenter, SegmenterConfig};

let dictionary = load_dictionary("embedded://ipadic")?;
let config: SegmenterConfig = json!({
    "mode": "normal",
    "keep_whitespace": true
});
let segmenter = Segmenter::from_config_with_dictionaries(&config, Some(dictionary), None)?;
}

Tokenizer::from_config_with_dictionaries and TokenizerBuilder::build_with_dictionaries do the same for a tokenizer configuration, filters included (see Architecture).

Memory-Mapped Loading

For a filesystem-based (not embedded://) dictionary, use_mmap defaults to true whenever the mmap Cargo feature is compiled in (the default) — memory-mapped reads are used automatically. Set it to false to force eager, non-memory-mapped file reads instead. Every large component stays lazily paged this way: the word-list files, the connection-cost matrix, and the prefix-dictionary trie, which is walked in place over its serialized bytes. use_mmap is silently ignored for embedded:// dictionaries, since their data is already a static, zero-copy byte slice. Requires the mmap cargo feature (enabled by default).

Whitespace Handling

By default, whitespace is skipped in the lattice, as MeCab does: no word starts on a whitespace character, and the word after a run of whitespace connects directly to the word before it, with the connection cost the dictionary was trained with. Skipped whitespace is not output and is left out of token surfaces and offsets; whitespace that belongs to a dictionary entry, inside it (IPADIC-NEologd GeForce GTX, a user entry 해운대 해수욕장) or at its end (ko-dic 에듀 followed by a space), stays in that entry's token, as in MeCab. "Whitespace" is the dictionary's SPACE character category (char.def): U+0020, U+0009, U+000A, U+000B and U+000D for ko-dic, and U+0020, U+0009, U+000A and U+000B for IPADIC, IPADIC-NEologd, UniDic, SudachiDict, CC-CEDICT and Jieba. Those char.def files also map U+00D0 (Ð) to SPACE, a typo for U+000D, but a later 0x00C0..0x00FF ALPHA line takes precedence, so Ð is a letter, as in MeCab.

The default comes from the dictionary's metadata.json (skip_whitespace). SudachiDict sets it to false: Sudachi keeps whitespace in the lattice and outputs it as 空白 tokens, and SudachiDict's costs are tuned for that, so skipping would, for example, split the entry caramel man into two common nouns. With SudachiDict, whitespace therefore stays in the lattice by default, as a node of its own (a 空白 dictionary entry, such as the one for U+0020, or the SPACE unknown word), though it is still dropped from the output. The other bundled dictionaries leave the setting out, which means skipping, and so does a dictionary built before the setting existed (a SudachiDict built by Lindera 6.x included, until it is rebuilt).

Call keep_whitespace(true) on a Segmenter to output whitespace tokens. Whitespace then also stays in the lattice, as a node of its own (a whitespace dictionary entry, such as IPADIC's or UniDic's for U+3000, or the SPACE unknown word), so the other tokens can differ from those of the default output, which skips whitespace:

#![allow(unused)]
fn main() {
let segmenter = Segmenter::new(Mode::Normal, dictionary, None).keep_whitespace(true);
}

Before v7, Lindera kept whitespace in the lattice even when it dropped it from the output, so the words on either side of a space connected to the whitespace node instead of to each other. How much that mattered depends on the dictionary's connection costs for the SPACE unknown word. In ko-dic its row and column of the connection matrix are all zero, so every space reset the context and a word between spaces was chosen by its word cost alone: ko-dic read 전 in 2년 전 대회 as 저/NP + ㄴ/JX instead of the noun NNG. In IPADIC and UniDic the costs are not zero (IPADIC's SPACE unknown word shares its context with the full-width space entry 記号,空白), and IPADIC read が in Google が 新しい as a conjunction. CC-CEDICT has no connection costs at all, so its segmentation does not depend on this, although the path costs that N-best search reports do. Jieba's connection costs are trained (mecab-jieba 0.1.3), but on the 247 sentences with a space of UD Chinese GSD and of the GSDSimp dev and test splits, its segmentation is the same either way. skip_whitespace overrides the dictionary's default: skip_whitespace(false) keeps whitespace nodes in the lattice as before, while still dropping whitespace from the output, and skip_whitespace(true) turns skipping on for SudachiDict. In a SegmenterConfig the skip_whitespace key takes true or false, and leaving it out, or null, keeps the dictionary's default; on the CLI, --disable-skip-whitespace turns skipping off:

#![allow(unused)]
fn main() {
let segmenter = Segmenter::new(Mode::Normal, dictionary, None).skip_whitespace(false);
}

A \n or a \t still ends a segment (see Sentence Splitting), so the context is not carried across a newline or a tab. mecab-ko analyzes a line at a time and skips a tab inside it like any other whitespace.

Unknown-Word Grouping

A word that is not in the dictionary is read as an unknown word, built from the categories that the dictionary's char.def gives its characters (katakana, Latin letters, digits, symbols and so on). A character can have more than one category, such as a kanji numeral (KANJINUMERIC KANJI in IPADIC); the first category of the char.def line that decides its categories is its default category. At every position that a path through the lattice reaches, Lindera creates unknown-word candidates from the default category of the character there, if that category is set to always create them (INVOKE in char.def) or if no dictionary word starts at that position, as MeCab does. If the category is set to group (GROUP), one candidate is a grouped unknown word: the run of characters from that position on in which each character shares a category with the one before it, such as a run of katakana.

With ko-dic, the numeral 三 is HANJANUMERIC HANJA (default category HANJANUMERIC, which groups) and the other hanja are HANJA, so the grouped unknown word that starts at 三 runs through the hanja after it: 三國史記 is one unknown word (SH), as in MeCab. Before v7, Lindera created candidates for every category of a character and went on with a run only while the next character had the same category at the same position of its category list, so it returned 三 / 國史 / 記.

An unknown word can start at any such position, also inside a run that a grouped unknown word from an earlier position covers. IPADIC's char.def puts ・ in the katakana range, so in ジョン・レノンが歌う the first position has the grouped unknown word ジョン・レノン. The dictionary words ジョン and ・ reach the position of レ, where the unknown word レノン starts, and the result is ジョン, ・, レノン, が and 歌う, as in MeCab. Before v7, normal mode created no unknown words at the positions inside the last grouped unknown word, a shortcut inherited from Kuromoji, so it returned ジョン・レノン as one unknown word; Decompose mode never took this shortcut.

Unknown-word grouping is unbounded by default. max_grouping_len(Some(n)) caps it with MeCab's max-grouping-size semantics, counting characters beyond the first (MeCab defaults to 24):

#![allow(unused)]
fn main() {
let segmenter = Segmenter::new(Mode::Normal, dictionary, None).max_grouping_len(Some(24));
}

The cap is applied at each lattice position rather than to a run as a whole. When the run starting at a position is longer than the cap, the grouped candidate is not emitted there; the single-character candidate (plus the length-ladder candidates described below and any dictionary words) remains, and the remaining tail is grouped again once it fits. No unknown token is therefore longer than n + 1 characters, but an over-long run does not turn into single characters throughout. With IPADIC, the out-of-vocabulary katakana run ヷヸヹヺヷ and max_grouping_len(Some(2)) segment as ヷ / ヸ / ヹヺヷ when the length ladder is off: the first two positions see runs of 5 and 4 characters (over the cap) and keep only the single-character candidate, while the 3-character tail fits the cap and groups. With the ladder on (the default), the 2-character ladder candidate wins at the first position and the output is ヷヸ / ヹヺヷ.

Some(0) never groups a run of two or more characters. The max_grouping_len config key and the CLI flag --max-grouping-len treat 0 as unbounded instead.

Independently of grouping, Lindera also generates a MeCab/Vibrato-inspired "length ladder" of shorter unknown-word candidates by default, so the Viterbi search can pick whichever length scores lowest. The candidates are one character long and longer, up to the char.def LENGTH field of the default category, over the characters that share a category with the first one; as in MeCab, each character is compared with the first one, not with the one before it. Call unknown_word_ladder(false) to disable it: a position then gets only the grouped or the single-character candidate, as before v6. The output still differs from that of Lindera before v6 wherever later changes apply, such as unknown words that start inside a grouped run (see Migrating from v6 to v7):

#![allow(unused)]
fn main() {
let segmenter = Segmenter::new(Mode::Normal, dictionary, None).unknown_word_ladder(false);
}

Left-Space Penalty (Korean)

MeCab-based Korean analyzers (mecab-ko with mecab-ko-dic, Lucene's nori) add a cost to a candidate that starts right after whitespace when its part-of-speech tag is one that attaches to the preceding word without a space: particles (J*), endings (E*), the copula (VCP) and derivational suffixes (XS*). Without it, 검색 이 잘 된다 reads 이 as the subject particle JKS rather than the interjection IC. Lindera applies the same penalty by default for dictionaries that ship the rules in their metadata (ko-dic does); other dictionaries are unaffected. Opt out with Segmenter::space_penalty(None), "space_penalty": false in the config, or --disable-space-penalty on the CLI. Together with skip_whitespace(false), this turns off both changes since v6.0 in how Korean is read around spaces (this penalty and whitespace skipping), but not the other output changes since then, such as the split of punctuation runs like ." (see Migrating from v6 to v7).

SpacePenaltyConfig is a list of rules, each pairing first part-of-speech tags with a cost. A candidate is matched by the part of its tag before the first + (for ko-dic Inflect rows, the first_part_of_speech column); the first matching rule wins and unlisted tags cost nothing. mecab-ko-dic's dicrc rules translate to:

#![allow(unused)]
fn main() {
use lindera::space_penalty::{SpacePenaltyConfig, SpacePenaltyRule};

let rules = SpacePenaltyConfig::new(vec![
    SpacePenaltyRule::new(["EC", "EF", "EP", "ETM", "ETN", "VCP", "XSA", "XSN", "XSV"], 3000),
    SpacePenaltyRule::new(["JC", "JKB", "JKC", "JKG", "JKO", "JKQ", "JKS", "JKV", "JX"], 6000),
]);
let segmenter = Segmenter::new(Mode::Normal, dictionary, None).space_penalty(Some(rules))?;
}

"Whitespace" means a character of the dictionary's SPACE category (char.def), the same set that is skipped in the lattice and that keep_whitespace filters on; a dictionary without that category falls back to Unicode White_Space. The penalty applies in both modes and in N-best search, to system, user and unknown-word entries alike. space_penalty builds a per-word-id lookup once (a few tens of milliseconds for ko-dic), and returns an error when the dictionary schema has neither a part_of_speech_tag nor a part_of_speech field.

A dictionary can ship its default rules in metadata.json under space_penalty; ko-dic does, with exactly the rules above, and Segmenter::new applies them automatically. space_penalty_from_dictionary() re-enables them after an opt-out and returns an error for a dictionary that ships none. Shipped rules that cannot be applied, because the schema has no part-of-speech field, only log a warning in Segmenter::new:

#![allow(unused)]
fn main() {
let segmenter = Segmenter::new(Mode::Normal, dictionary, None).space_penalty_from_dictionary()?;
}

Only a ko-dic built by Lindera 6.1.0 or later carries these rules. A ko-dic downloaded from the v6.0.0 release ships none, so with it the penalty silently stays off, and space_penalty_from_dictionary() (or "space_penalty": true) returns an error until the dictionary is rebuilt. Explicit rules (space_penalty, --space-penalty-rules) work with any ko-dic.

In a SegmenterConfig, the space_penalty key takes an object for explicit rules, false for off, and true for the dictionary's rules (an error when it ships none). Leaving the key out, or null, keeps the default: the dictionary's rules when it ships any.

{
  "dictionary": "embedded://ko-dic",
  "space_penalty": {
    "rules": [
      { "pos": ["EC", "EF", "EP", "ETM", "ETN", "VCP", "XSA", "XSN", "XSV"], "cost": 3000 },
      { "pos": ["JC", "JKB", "JKC", "JKG", "JKO", "JKQ", "JKS", "JKV", "JX"], "cost": 6000 }
    ]
  }
}

A candidate pays the penalty when whitespace that is not part of the word before it separates the two, which is mecab-ko's test (it penalizes a node that skipped whitespace, rlength > length). So a word after leading whitespace pays it, and a word right after an entry whose surface ends with the whitespace does not: ko-dic has four such entries (내셔날, 에듀, 캘리 and 콜라비, each followed by a space), and 에듀 이 reads as that entry and 이/VCP, as in mecab-ko, while 에듀 이, with a second space the entry does not end with, reads 이/NR. The test is the same whether whitespace is skipped in the lattice (the default, see Whitespace Handling) or kept as nodes, where a word after a whitespace node pays the penalty. With whitespace skipped, the connection costs on their own already rule out some readings the penalty targets (시 in 서울 시 에서 reads as NNG without it), but not all of them (이 in 검색 이 잘 된다).

N-Best Segmentation

segment_nbest returns the top-n segmentations ordered by total path cost, each paired with its cost. Set unique to deduplicate results that share the same word boundaries but differ only in POS tags (each result is the cheapest path of its word boundaries, and the search does not go through the others, so it stays fast when the words of a line have many entries), and cost_threshold to discard paths whose cost exceeds best_cost + threshold:

#![allow(unused)]
fn main() {
let results = segmenter.segment_nbest(Cow::Borrowed("すもももももももものうち"), 3, false, None)?;
for (tokens, cost) in results {
    println!("cost={cost}");
    for token in tokens {
        println!("  {}", token.surface.as_ref());
    }
}
}

Every result segments the whole input. The input is split into segments as described in Sentence Splitting, and each segment is searched on its own. Within a segment, the context is carried across 、, 。 and forced cuts, so the paths of a segment are the n best paths of one lattice over the whole segment, with the exceptions listed there; unique compares the word boundaries of the whole segment. A result takes one path in each segment, its cost is the sum of those paths' costs, and the results are the n cheapest of these combinations. So a result can differ from the best one in a single segment. best_cost is the cost of the first result, so cost_threshold applies to the whole input. The first result is always the output of segment. An input without words gives one result without tokens, with the connection cost from BOS to EOS, as in MeCab: an input of skipped whitespace and, since v7, an empty input (which gave no result before).

segment_nbest_with_lattice is the same operation but lets you pass in a reusable Lattice buffer to avoid reallocating one per call.

Sentence Splitting

Before building the lattice, Lindera splits the input into sentences at delimiter characters (\n, \t, 。, 、) and builds one Viterbi lattice per sentence. If no delimiter appears within roughly 32 KiB of a sentence's start, Lindera forces a sentence boundary there anyway and logs a warning. The cuts bound the size — and therefore the memory and CPU cost — of each lattice.

A \n, a \t or the end of the input ends a segment, the run of sentences up to it. Segments are independent of each other. Within a segment, the cuts at 、 and 。 and the forced cuts carry the context: the next sentence starts from the words that end the previous one (from their right context IDs, each with the cost of the best path to it) rather than from the beginning of a sentence (BOS), and the connection to the end of the sentence (EOS) is paid only at the end of the segment. So the best path of a segment, and its cost, are those of one lattice over the whole segment, as MeCab, which does not cut inside a line, finds them. Before v7, every sentence started from BOS and ended with EOS, so the word after 、 lost its context: with IPADIC, ああ in 彼は、ああ言った was an interjection, and it is now an adverb, as in MeCab.

The result can still differ from one lattice over the segment:

  • No word spans a cut: a dictionary entry that contains 、 or 。, such as UniDic's 一、二塁, never matches, 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 (see Left-Space Penalty (Korean)) does not apply to a word that starts right at a forced cut, even when whitespace precedes the cut.
  • A sentence without a path, which only a dictionary that leaves some character without an unknown-word candidate gives, ends the segment before it: the best path up to the cut is kept as a path that ends there, the EOS connection included, and the sentence is segmented on its own. The first N-best result keeps the same path.

Ordinary text never reaches the forced cut, but for pathological delimiter-free input (e.g. minified text or base64-encoded blobs), it can affect tokenization at the artificial cut point.

N-best search uses the same segments; see N-Best Segmentation.

Reusable Worker

SegmentWorker is a reusable segmentation session that owns the Viterbi lattice and the scratch buffers of the segmentation, so repeated calls avoid the per-call allocations segment pays. Create one with new_worker (clones the segmenter) or into_worker (consumes it, avoiding a user-dictionary copy), then call segment/segment_nbest on it:

#![allow(unused)]
fn main() {
let mut worker = segmenter.new_worker();
for line in lines {
    let tokens = worker.segment(line)?;
    for token in &tokens {
        println!("{}", token.surface.as_ref());
    }
}
}

The returned tokens borrow the worker, so they must be consumed before the next call (the usual per-line loop above compiles as-is). set_mode, set_keep_whitespace, set_skip_whitespace, set_max_grouping_len, set_unknown_word_ladder, set_space_penalty and set_space_penalty_from_dictionary switch the configuration between calls. segmenter() returns a shared reference to the underlying segmenter; no &mut accessor is provided, since swapping the dictionary out from under the reused lattice would break the dictionary-lattice pairing the worker exists to guarantee.

A worker also bounds retained memory: one delimiter-free maximum-length sentence grows the lattice to several MB (roughly 18 MB for a 32 KiB ASCII sentence, 6 MB for a 32 KiB CJK one, whose character-indexed lattice has a third of the slots), and a plain Lattice keeps that forever. The worker automatically shrinks its lattice, and releases its scratch buffers, once a window of calls shows the capacity is oversized, and shrink_to(text_len_hint) forces a shrink immediately. reset() discards the internal buffers outright and replaces them with fresh ones; it is intended for recovery paths (e.g. after a panic poisoned a mutex holding the worker) where the buffers may hold an inconsistent intermediate state — the segmenter configuration itself is preserved.

The worker is permanently bound to the dictionary of the segmenter that created it; there is no way to swap dictionaries under a live worker, which rules out a class of lattice-reuse bugs by construction. For multi-threaded use, create one worker per thread from a shared Segmenter.

Error Handling

Lindera uses a structured error system based on anyhow and thiserror for ergonomic error handling throughout the library.

LinderaResult

The LinderaResult<T> type alias is the standard return type for fallible operations in Lindera:

#![allow(unused)]
fn main() {
pub type LinderaResult<T> = Result<T, LinderaError>;
}

LinderaError

LinderaError is the main error type, containing an error kind and a source error with full context:

#![allow(unused)]
fn main() {
pub struct LinderaError {
    pub kind: LinderaErrorKind,
    source: anyhow::Error,
}
}

The add_context method allows attaching additional context to an error:

#![allow(unused)]
fn main() {
let error = error.add_context("failed to load dictionary from /path/to/dict");
}

LinderaErrorKind

LinderaErrorKind is an enum that categorizes errors:

KindDescription
IoI/O errors (file read/write, network)
ParseParsing errors (invalid input format)
SerializeSerialization errors
DeserializeDeserialization errors
ContentInvalid content or data errors
ArgsInvalid argument errors
DecodeDecoding errors
NotFoundResource not found (e.g., dictionary file missing)
BuildDictionary build errors
DictionaryDictionary-related errors
ModeInvalid tokenization mode errors
FeatureDisabledAttempted to use a feature that is not enabled

Creating Errors

Use LinderaErrorKind::with_error to create an error from a kind and a source:

#![allow(unused)]
fn main() {
use lindera::error::LinderaErrorKind;

let error = LinderaErrorKind::Io.with_error(anyhow::anyhow!("file not found: config.yml"));
}

Using the ? Operator

Since Lindera functions return LinderaResult, the ? operator can propagate errors naturally:

#![allow(unused)]
fn main() {
use lindera::dictionary::load_dictionary;
use lindera::mode::Mode;
use lindera::segmenter::Segmenter;
use lindera::analysis::tokenizer::Tokenizer;
use lindera::LinderaResult;

fn analyze(text: &str) -> LinderaResult<Vec<String>> {
    let dictionary = load_dictionary("embedded://ipadic")?;
    let segmenter = Segmenter::new(Mode::Normal, dictionary, None);
    let tokenizer = Tokenizer::new(segmenter);

    let tokens = tokenizer.tokenize(text)?;
    Ok(tokens.iter().map(|t| t.surface.as_ref().to_string()).collect())
}
}

Error Handling Patterns

Matching on Error Kind

#![allow(unused)]
fn main() {
use lindera::dictionary::load_dictionary;
use lindera::error::LinderaErrorKind;

match load_dictionary("/path/to/dictionary") {
    Ok(dict) => { /* use dictionary */ }
    Err(e) if e.kind() == LinderaErrorKind::NotFound => {
        eprintln!("Dictionary not found: {}", e);
    }
    Err(e) if e.kind() == LinderaErrorKind::Io => {
        eprintln!("I/O error loading dictionary: {}", e);
    }
    Err(e) => {
        eprintln!("Unexpected error: {}", e);
    }
}
}

Converting from External Errors

#![allow(unused)]
fn main() {
use lindera::error::LinderaErrorKind;

let content = std::fs::read_to_string("config.yml")
    .map_err(|err| LinderaErrorKind::Io.with_error(anyhow::anyhow!(err)))?;
}

API Reference

The API reference is available. Please see following URL:

Lindera Analysis

Lindera Analysis layers a Lucene-style text analysis chain on top of the morphological segmenter provided by the lindera-segmenter crate. It is normally used through the lindera facade as lindera::analysis (enabled by the default analysis feature). It composes character filters, a Segmenter, and token filters into a single Tokenizer pipeline that can be built programmatically or loaded entirely from a YAML configuration file.

Key Features

  • Character filters that transform input text before segmentation, with automatic byte-offset correction back to the original text
  • Token filters that transform, merge, filter, or reorder the tokens produced by the segmenter
  • Tokenizer / TokenizerBuilder to assemble a full analysis pipeline, either in Rust code or from a YAML file (LINDERA_CONFIG_FILE)
  • Built-in filters covering Japanese, Korean, and general-purpose text normalization

Dependency

Through the facade (recommended):

[dependencies]
lindera = "7"
#![allow(unused)]
fn main() {
use lindera::analysis::tokenizer::Tokenizer;
}

As a direct dependency. It is the same crate, so both paths name the same types; keep lindera and lindera-analysis at the same major version (see Migration v6 to v7):

[dependencies]
lindera = "7"
lindera-analysis = "7"
#![allow(unused)]
fn main() {
use lindera_analysis::tokenizer::Tokenizer;
}

Contents

Configuration

Lindera is able to read YAML format configuration files. Specify the path to the following file in the environment variable LINDERA_CONFIG_FILE. You can use it easily without having to code the behavior of the tokenizer in Rust code.

segmenter:
  mode: "normal"
  dictionary: "embedded://ipadic"
  # user_dictionary: "./resources/user_dict/ipadic_simple_userdic.csv"
  # keep_whitespace: false
  # skip_whitespace: false # skip whitespace in the lattice, as MeCab does; on by default except for SudachiDict. false keeps whitespace nodes (pre-v7)
  # use_mmap: false # only meaningful for filesystem (non-embedded://) dictionaries
  # max_grouping_len: 24 # cap on unknown-word grouping; omit or 0 for unbounded
  # unknown_word_ladder: true # emit shorter unknown-word candidates (default: true)
  # space_penalty: false # left-space penalty (Korean); on by default for dictionaries that ship rules (ko-dic). false turns it off, an object gives explicit rules

character_filters:
  - kind: "unicode_normalize"
    args:
      kind: "nfkc"
  - kind: "japanese_iteration_mark"
    args:
      normalize_kanji: true
      normalize_kana: true
  - kind: mapping
    args:
       mapping:
         リンデラ: Lindera

token_filters:
  - kind: "japanese_compound_word"
    args:
      # Merge the numeral tokens only. A counter merged into the number token keeps japanese_number from converting it.
      tags:
        - "名詞,数"
      new_tag: "名詞,数"
  - kind: "japanese_number"
    args:
      tags:
        - "名詞,数"
  - kind: "japanese_stop_tags"
    args:
      tags:
        - "接続詞"
        - "助詞"
        - "助詞,格助詞"
        - "助詞,格助詞,一般"
        - "助詞,格助詞,引用"
        - "助詞,格助詞,連語"
        - "助詞,係助詞"
        - "助詞,副助詞"
        - "助詞,間投助詞"
        - "助詞,並立助詞"
        - "助詞,終助詞"
        - "助詞,副助詞/並立助詞/終助詞"
        - "助詞,連体化"
        - "助詞,副詞化"
        - "助詞,特殊"
        - "助動詞"
        - "記号"
        - "記号,一般"
        - "記号,読点"
        - "記号,句点"
        - "記号,空白"
        - "記号,括弧閉"
        - "その他,間投"
        - "フィラー"
        - "非言語音"
  - kind: "japanese_katakana_stem"
    args:
      min: 3
  - kind: "remove_diacritical_mark"
    args:
      japanese: false

Segmenter Options

KeyTypeDefaultDescription
modestring"normal"Segmentation mode: "normal" or "decompose"
dictionarystring(required)Dictionary URI, e.g. "embedded://ipadic"
user_dictionarystring(none)Path to a user dictionary
keep_whitespaceboolfalseEmit whitespace tokens. Whitespace then also stays in the lattice, so the other tokens can differ from those of the default output, which skips whitespace as MeCab does
skip_whitespacebool(the dictionary's: true, false for SudachiDict)With keep_whitespace false, skip whitespace in the lattice so the words on either side connect directly, as in MeCab. false keeps whitespace in the lattice as a node of its own (a whitespace dictionary entry or the SPACE unknown word), as before v7; with ko-dic, whose connection costs for that node are all zero, every space then resets the connection context. Omitted or null keeps the default from the dictionary's metadata.json. See Segmenter
use_mmapboolon when the mmap feature is compiled in (the default)Memory-map the dictionary; only meaningful for filesystem (non-embedded://) dictionaries
max_grouping_leninteger(unbounded)Cap on unknown-word grouping, in characters beyond the first, matching MeCab's max-grouping-size (MeCab defaults to 24). Applied at each position: a run longer than the cap is not grouped there, the single-character candidate (plus the length ladder and dictionary words) remains, and the remaining tail is grouped again once it fits, so no unknown token exceeds cap+1 characters. Omitting the key, or setting 0, leaves grouping unbounded
unknown_word_ladderbooltrueAlso emit the shorter unknown-word candidates up to the LENGTH field in char.def of the character's default category, as MeCab and Vibrato do. false leaves them out, as before v6; other changes since then, such as unknown words that start inside a grouped run, still apply. See Segmenter
space_penaltybool or object(the dictionary's rules, if any)Left-space penalty (mecab-ko's left-space-penalty-factor): a candidate that starts right after whitespace and whose first part-of-speech tag is listed gets the cost added. Omitted or null keeps the rules the dictionary ships in its metadata.json (ko-dic ships mecab-ko-dic's, so the penalty is on by default for ko-dic; the other bundled dictionaries ship none); false turns it off; true requires the dictionary's rules (an error when it ships none); an object {"rules": [{"pos": [...], "cost": n}, ...]} gives explicit rules. See Segmenter
% export LINDERA_CONFIG_FILE=./resources/config/lindera.yml
use std::path::PathBuf;

use lindera::analysis::tokenizer::TokenizerBuilder;
use lindera::LinderaResult;

fn main() -> LinderaResult<()> {
    // Load tokenizer configuration from file
    let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
        .join("../resources")
        .join("config")
        .join("lindera.yml");

    let builder = TokenizerBuilder::from_file(&path)?;

    let tokenizer = builder.build()?;

    let text = "Linderaは形態素解析エンジンです。ユーザー辞書も利用可能です。".to_string();
    println!("text: {text}");

    let tokens = tokenizer.tokenize(&text)?;

    for token in tokens {
        println!(
            "token: {:?}, start: {:?}, end: {:?}, details: {:?}",
            token.surface, token.byte_start, token.byte_end, token.details
        );
    }

    Ok(())
}

Filters

Character filters and token filters are the two pre/post-processing stages of the lindera-analysis Tokenizer pipeline.

  • Character filters transform the input text before segmentation. Byte offsets are corrected automatically so that the resulting tokens still report positions relative to the original, unfiltered text.
  • Token filters transform the list of tokens produced by the segmenter after segmentation.

Both kinds of filters are configured the same way: a kind string (also used with the CLI's --character-filter / --token-filter flags in the form kind:{"json": "args"}) and a JSON args object with filter-specific parameters.

Character Filters

Character filters are configured under the character_filters key of the YAML configuration file. Each entry is applied in order, and each filter's output feeds into the next.

unicode_normalize

Normalizes the input text using one of the four standard Unicode normalization forms.

Parameters:

ArgumentTypeRequiredDescription
kindstringYesOne of nfc, nfd, nfkc, or nfkd

Example:

{
  "kind": "unicode_normalize",
  "args": {
    "kind": "nfkc"
  }
}

japanese_iteration_mark

Normalizes Japanese iteration marks (々, ゝ, ゞ, ヽ, ヾ) by replacing each mark with the character it repeats, adding or removing the voiced sound mark (dakuten) as needed for the hiragana/katakana variants.

Parameters:

ArgumentTypeRequiredDefaultDescription
normalize_kanjiboolNofalseNormalize the kanji iteration mark 々
normalize_kanaboolNofalseNormalize the hiragana/katakana iteration marks ゝ, ゞ, ヽ, ヾ

Example:

{
  "kind": "japanese_iteration_mark",
  "args": {
    "normalize_kanji": true,
    "normalize_kana": true
  }
}

mapping (character filter)

Replaces occurrences of the keys in mapping with their corresponding values, using longest-match search over the input text (built with an Aho-Corasick automaton).

Parameters:

ArgumentTypeRequiredDescription
mappingobject (string to string)YesSubstrings to replace, mapped to their replacements

Example:

{
  "kind": "mapping",
  "args": {
    "mapping": {
      "リンデラ": "Lindera"
    }
  }
}

regex

Replaces every match of a regular expression in the input text with a literal replacement string. Capture groups are not interpolated into the replacement.

Parameters:

ArgumentTypeRequiredDescription
patternstringYesA regular expression (using the regex crate's syntax)
replacementstringYesThe literal string that replaces every match of pattern

Example:

{
  "kind": "regex",
  "args": {
    "pattern": "\\s{2,}",
    "replacement": " "
  }
}

Token Filters

Token filters are configured under the token_filters key of the YAML configuration file. Each filter is applied in order to the token list produced by the segmenter.

japanese_base_form

Replaces the token's surface text with its base (dictionary) form, as registered in the base_form or orthographic_base_form field of the dictionary. Acts as a lemmatizer for verbs and adjectives. Tokens produced by unknown-word processing (token.word_id.is_unknown()) are left unchanged.

This filter takes no configuration parameters.

Example:

{
  "kind": "japanese_base_form"
}

japanese_compound_word

Merges consecutive tokens whose part-of-speech tag matches one of tags into a single compound token.

The merged token's details carry new_tag at the part-of-speech position resolved from the dictionary schema; any leading non-part-of-speech field (SudachiDict's display_surface) and every remaining field are set to *.

Parameters:

ArgumentTypeRequiredDescription
tagsarray<string>YesPart-of-speech tags (up to 4 comma-separated levels) that mark tokens eligible for merging
new_tagstringNoPart-of-speech tag assigned to the merged token. When omitted, the merged token is tagged 複合語

Example:

{
  "kind": "japanese_compound_word",
  "args": {
    "tags": [
      "名詞,数",
      "名詞,接尾,助数詞"
    ],
    "new_tag": "名詞,数"
  }
}

japanese_kana

Converts token text between hiragana and katakana.

Parameters:

ArgumentTypeRequiredDescription
kindstringYes"hiragana" converts katakana to hiragana; "katakana" converts hiragana to katakana

Example:

{
  "kind": "japanese_kana",
  "args": {
    "kind": "hiragana"
  }
}

japanese_katakana_stem

Removes a trailing prolonged sound mark (ー, U+30FC) from katakana tokens, but only when the token is longer than min characters.

Parameters:

ArgumentTypeRequiredDescription
minpositive integerYesMinimum katakana token length (in characters) required before the trailing prolonged sound mark is stemmed

Example:

{
  "kind": "japanese_katakana_stem",
  "args": {
    "min": 3
  }
}

japanese_keep_tags

Keeps only tokens whose part-of-speech tag matches one of tags, removing all others.

Parameters:

ArgumentTypeRequiredDescription
tagsarray<string>YesPart-of-speech tags (up to 4 comma-separated levels) to keep

Example:

{
  "kind": "japanese_keep_tags",
  "args": {
    "tags": [
      "名詞,一般",
      "名詞,固有名詞,一般"
    ]
  }
}

japanese_number

Converts Japanese numeral representations (kanji numerals, formal/legal kanji numerals, and fullwidth digits) in the token's surface text to Arabic numerals.

A token is converted only when every one of its characters takes part in a numeral, the rule Lucene's JapaneseNumberFilter applies. An ordinary word that merely begins with a numeral character is therefore returned unchanged: 一部 stays 一部 rather than becoming 1部.

When this filter follows japanese_compound_word, merge only the 名詞,数 tokens. A counter (名詞,接尾,助数詞) merged into the number token makes the merged token something other than a numeral, so 一万円 is left alone instead of being converted.

Parameters:

ArgumentTypeRequiredDescription
tagsarray<string> or nullNoPart-of-speech tags (up to 4 comma-separated levels) to restrict the conversion to, matched the same way as japanese_keep_tags. When omitted or null, every token is converted

Example:

{
  "kind": "japanese_number",
  "args": {
    "tags": [
      "名詞,数"
    ]
  }
}

japanese_reading_form

Replaces the token's surface text with its reading, in katakana, as registered in the dictionary's reading field. Tokens produced by unknown-word processing (token.word_id.is_unknown()) are left unchanged.

This filter takes no configuration parameters.

Example:

{
  "kind": "japanese_reading_form"
}

japanese_stop_tags

Removes tokens whose part-of-speech tag matches one of tags.

Parameters:

ArgumentTypeRequiredDescription
tagsarray<string>YesPart-of-speech tags (up to 4 comma-separated levels) to remove

Example:

{
  "kind": "japanese_stop_tags",
  "args": {
    "tags": [
      "助詞,格助詞,一般",
      "助詞,係助詞",
      "助詞,連体化",
      "助動詞"
    ]
  }
}

Tags are normalized to exactly four comma-separated levels (missing levels are padded with *) and compared for exact equality against the four part-of-speech details of each token, starting at the field named part_of_speech in the dictionary schema (or at the first detail when the schema has no such field). A bare 助詞 therefore never matches IPADIC particle tokens — they always carry a subcategory such as 助詞,係助詞 — while a bare 助動詞 does match, because auxiliary verbs have no subcategory (助動詞,*,*,*). The same matching rule applies to japanese_keep_tags, japanese_compound_word, and the tags option of japanese_number. Because the position is resolved through the schema, the rule also holds for dictionaries whose schema has leading non-part-of-speech columns, such as SudachiDict's display_surface.

keep_words

Keeps only tokens whose surface text exactly matches one of words.

Parameters:

ArgumentTypeRequiredDescription
wordsarray<string>YesSurface forms to keep

Example:

{
  "kind": "keep_words",
  "args": {
    "words": [
      "すもも",
      "もも"
    ]
  }
}

korean_compound_word

Merges consecutive Korean tokens whose first part-of-speech tag matches one of tags into a single compound token. The merged token keeps the first token's byte_start and position, takes the last token's byte_end, and its position_length is the number of tokens merged. A matching token whose neighbours do not match is left as it is.

ko-dic tokenizes a Sino-Korean numeral into one token per morpheme, so this filter is what turns 이/NR 천/NR 이/NR 십/NR 육/NR back into one token before korean_number converts it:

InputTokens from ko-dicAfter korean_compound_word (tags: ["SN", "NR"], new_tag: "NR")After korean_number
이천이십육년이/NR 천/NR 이/NR 십/NR 육/NR 년/NNBC이천이십육/NR 년/NNBC2026 년
10만10/SN 만/NR10만/NR100000
2천262/SN 천/NR 26/SN2천26/NR2026

Parameters:

ArgumentTypeRequiredDescription
tagsarray<string>YesPart-of-speech tags (the single ko-dic tag, such as NR or SN) that mark tokens eligible for merging
new_tagstringNoPart-of-speech tag assigned to the merged token; its other details are *. When omitted, the merged token is tagged 복합어, which is not a ko-dic tag, so pass a tag such as NR when a following korean_number or korean_keep_tags should pick the merged token up

Example:

{
  "kind": "korean_compound_word",
  "args": {
    "tags": [
      "SN",
      "NR"
    ],
    "new_tag": "NR"
  }
}

With the default settings, ko-dic tags 만 in 만 원 and 십만 원 as the numeral NR, so 십만 원 becomes 십만/NR 원/NNBC. With whitespace skipping turned off (skip_whitespace: false), the word after 만 connects to the whitespace node instead, and ko-dic tags 만 there as the particle JX (만/JX 원/NNG), which stays outside ["SN", "NR"].

korean_decompound

Splits ko-dic compound, inflected and pre-analyzed tokens into the morphemes their expression field lists, reproducing Lucene nori's DecompoundMode. This is the inverse of korean_compound_word, which merges neighbouring tokens. Lindera's Mode::Decompose applies a length penalty tuned for Japanese kanji and does not read these fields, so 무궁화 stays whole in every mode without this filter.

InputTokens from ko-dicmode: "discard"mode: "mixed"
무궁화꽃이무궁화/NNG 꽃/NNG 이/JKS무궁 화 꽃 이무궁화 무궁 화 꽃 이
가곡역가곡역/NNP가곡 역가곡역 가곡 역

Fragment offsets follow nori: only Compound fragments get their own byte range, each covering the text it spells. Inflect and Preanalysis fragments share the whole token's span, because an inflected form's fragments need not be substrings of it (갔 decomposes to 가 + 았).

A Compound token is carved only when its fragments spell its surface exactly; otherwise it shares its span too. That covers the 12 bundled ko-dic entries whose fragments are not the surface's own text (안지랑역 = 안지 + 역, 고춧값 = 고추 + 값), where nori's offsets point some fragments at text they do not spell, as well as a user-dictionary entry such as 프린터3D = 프린터 + 쓰리디 and a surface an earlier filter has rewritten. For every other bundled entry the offsets are nori's, and every offset is safe to slice the text with.

In discard the fragments take consecutive positions starting at the compound's own; in mixed the compound keeps that position with a position_length spanning its fragments, which are then emitted from the same position onward. Tokens after the expansion shift by the positions it added, and only by those, so a gap left by an earlier filter such as korean_stop_tags survives.

Only tokens spanning a single position are expanded, so the compound a mixed pass keeps is not split again. Put the filter in a chain once all the same: a single-fragment entry kept by mixed still spans one position. When combining it with korean_compound_word, place the merge first; merging never joins tokens that overlap in byte range, so the reverse order does not corrupt anything, but merging first lets it see the segmenter's own tokens.

A fragment takes its own part-of-speech tag and semantic class and blanks the fields that describe the compound (type, expression, first_part_of_speech, last_part_of_speech, reading, presence_absence). Blanking the reading matches nori, whose DecompoundToken.getReading() returns null; korean_reading_form skips *, so a fragment keeps its own surface.

Applied to a dictionary whose schema has no type/expression fields, the filter warns once per configured filter and passes every token through unchanged.

Parameters:

ArgumentTypeRequiredDescription
modestringNo"discard" (default, as in nori) replaces the compound with its fragments; "mixed" keeps the compound and emits the fragments as well
typesarray<string>NoThe type values to decompound, case-sensitive. Defaults to ["Compound", "Inflect", "Preanalysis"]; an empty list or any other value is rejected

Example:

{
  "kind": "korean_decompound",
  "args": {
    "mode": "discard",
    "types": [
      "Compound",
      "Inflect",
      "Preanalysis"
    ]
  }
}

korean_keep_tags

Keeps only Korean tokens whose first part-of-speech tag matches one of tags.

Parameters:

ArgumentTypeRequiredDescription
tagsarray<string>YesPart-of-speech tags to keep

Example:

{
  "kind": "korean_keep_tags",
  "args": {
    "tags": [
      "NNG"
    ]
  }
}

korean_number

Converts Sino-Korean numeral representations (Hangul numerals, Hanja numerals, and fullwidth digits) in the token's surface text to Arabic numerals. Native Korean numerals (하나, 둘, 열, 스물, ...) have no positional structure and are left unchanged.

Each token is converted on its own. ko-dic tokenizes a Sino-Korean numeral into one token per morpheme, so the tokens of a single number do not add up to one Arabic numeral:

InputTokens from ko-dicAfter korean_number
이천이십육년이/NR 천/NR 이/NR 십/NR 육/NR 년/NNBC2 1000 2 10 6 년
10만10/SN 만/NR10 10000

Put korean_compound_word in front of this filter to merge those tokens first, the way japanese_compound_word does for IPADIC. Its new_tag must be one of this filter's tags for the merged token to be converted:

[
  {"kind": "korean_compound_word", "args": {"tags": ["SN", "NR"], "new_tag": "NR"}},
  {"kind": "korean_number", "args": {"tags": ["SN", "NR"]}}
]

With that chain 이천이십육년 becomes 2026 년, 10만 becomes 100000 and 2천26 becomes 2026. A single token that holds the whole numeral, such as the Hanja 二千二十六/SH, is converted in full by this filter alone.

Parameters:

ArgumentTypeRequiredDescription
tagsarray<string> or nullNoPart-of-speech tags to restrict the conversion to. Omitted, it defaults to ["SN", "NR"]; null converts every token

A token is converted only when every one of its characters is a numeral, the rule Lucene's KoreanNumberFilter applies. That is what keeps native Korean numerals intact: 일곱 ("seven") starts with the Sino-Korean 일, and a character-by-character conversion would give 1곱.

Hangul numerals are also homographs of very common morphemes (이 is a subject particle, 만 an auxiliary particle), and those are single characters that the all-numeral rule cannot separate from a numeral. That is why the default is restricted to the numeral tags.

Add SH to convert Hanja numerals as well. The coverage is partial in two ways: ko-dic tags whole Hanja words as NNG (參加, 萬歲 are untouched), while a single Hanja character it cannot attach to a word is SH on its own, so 十字架 becomes 10 字 架 and 百貨店 becomes 100 貨 店.

Example:

{
  "kind": "korean_number",
  "args": {
    "tags": [
      "SN",
      "NR"
    ]
  }
}

korean_reading_form

Replaces the token's surface text with its reading, as registered in the dictionary's reading field. Tokens produced by unknown-word processing (token.word_id.is_unknown()) are left unchanged.

This filter takes no configuration parameters.

Example:

{
  "kind": "korean_reading_form"
}

korean_stop_tags

Removes Korean tokens whose first part-of-speech tag matches one of tags.

Parameters:

ArgumentTypeRequiredDescription
tagsarray<string>YesPart-of-speech tags to remove

Example:

{
  "kind": "korean_stop_tags",
  "args": {
    "tags": [
      "EP",
      "EF",
      "JKG"
    ]
  }
}

length

Keeps only tokens whose surface text length (in characters) falls within [min, max].

Parameters:

ArgumentTypeRequiredDescription
minunsigned integerNoMinimum character length (inclusive)
maxunsigned integerNoMaximum character length (inclusive)

Example:

{
  "kind": "length",
  "args": {
    "min": 2,
    "max": 3
  }
}

lowercase

Converts token surface text to lowercase.

This filter takes no configuration parameters.

Example:

{
  "kind": "lowercase"
}

mapping (token filter)

Replaces occurrences of the keys in mapping with their corresponding values in each token's surface text, using longest-match search (built with an Aho-Corasick automaton). This is the token-level counterpart of the mapping character filter.

Parameters:

ArgumentTypeRequiredDescription
mappingobject (string to string)YesSubstrings to replace, mapped to their replacements

Example:

{
  "kind": "mapping",
  "args": {
    "mapping": {
      "籠": "篭"
    }
  }
}

remove_diacritical_mark

Removes diacritical (combining) marks from token surface text, re-applying the text's original Unicode normalization form afterward.

Parameters:

ArgumentTypeRequiredDefaultDescription
japaneseboolNofalseAlso remove Japanese (han-)dakuten combining marks (e.g. from decomposed voiced/semi-voiced kana)

Example:

{
  "kind": "remove_diacritical_mark",
  "args": {
    "japanese": false
  }
}

stop_words

Removes tokens whose surface text exactly matches one of words.

Parameters:

ArgumentTypeRequiredDescription
wordsarray<string>YesSurface forms to remove

Example:

{
  "kind": "stop_words",
  "args": {
    "words": [
      "も",
      "の"
    ]
  }
}

uppercase

Converts token surface text to uppercase.

This filter takes no configuration parameters.

Example:

{
  "kind": "uppercase"
}

YAML Configuration

Character filters and token filters are configured together with the segmenter in a single YAML file. See Configuration for the full file format; the relevant excerpt looks like this:

character_filters:
  - kind: "unicode_normalize"
    args:
      kind: "nfkc"
  - kind: "japanese_iteration_mark"
    args:
      normalize_kanji: true
      normalize_kana: true

token_filters:
  - kind: "japanese_stop_tags"
    args:
      tags:
        - "助詞,格助詞,一般"
        - "助詞,係助詞"
        - "助詞,連体化"
        - "助動詞"
  - kind: "japanese_katakana_stem"
    args:
      min: 3
  - kind: "lowercase"
  - kind: "length"
    args:
      min: 2

Rust API

Character filters and token filters can also be created and applied programmatically:

use lindera::dictionary::load_dictionary;
use lindera::mode::Mode;
use lindera::segmenter::Segmenter;
use lindera::analysis::character_filter::BoxCharacterFilter;
use lindera::analysis::character_filter::unicode_normalize::{
    UnicodeNormalizeCharacterFilter, UnicodeNormalizeKind,
};
use lindera::analysis::token_filter::BoxTokenFilter;
use lindera::analysis::token_filter::japanese_stop_tags::JapaneseStopTagsTokenFilter;
use lindera::analysis::token_filter::japanese_katakana_stem::JapaneseKatakanaStemTokenFilter;
use lindera::analysis::tokenizer::Tokenizer;
use lindera::LinderaResult;

fn main() -> LinderaResult<()> {
    let dictionary = load_dictionary("embedded://ipadic")?;
    let segmenter = Segmenter::new(Mode::Normal, dictionary, None);

    let mut tokenizer = Tokenizer::new(segmenter);

    // Add a character filter
    let normalize_filter = UnicodeNormalizeCharacterFilter::new(UnicodeNormalizeKind::NFKC);
    tokenizer.append_character_filter(BoxCharacterFilter::from(normalize_filter));

    // Add token filters
    let stop_tags_filter = JapaneseStopTagsTokenFilter::new(
        vec![
            "助詞,格助詞,一般".to_string(),
            "助詞,係助詞".to_string(),
            "助詞,連体化".to_string(),
            "助動詞".to_string(),
        ]
        .into_iter()
        .collect(),
    );
    tokenizer.append_token_filter(BoxTokenFilter::from(stop_tags_filter));

    let katakana_stem_filter =
        JapaneseKatakanaStemTokenFilter::new(std::num::NonZeroUsize::new(3).unwrap());
    tokenizer.append_token_filter(BoxTokenFilter::from(katakana_stem_filter));

    // Tokenize with filters applied
    let tokens = tokenizer.tokenize("Linderaは形態素解析エンジンです。")?;

    for token in tokens {
        println!(
            "token: {:?}, details: {:?}",
            token.surface, token.details
        );
    }

    Ok(())
}

The append_character_filter and append_token_filter methods add filters in order. Character filters are applied sequentially to the text before segmentation; token filters are applied sequentially to the token list after segmentation.

Architecture

Module Structure

lindera-analysis/src/
├── lib.rs                          # Public API re-exports, CLI flag parsing helper
├── character_filter.rs             # CharacterFilter trait, OffsetMapping, CharacterFilterLoader
├── character_filter/
│   ├── unicode_normalize.rs        # Unicode normalization (NFC/NFD/NFKC/NFKD)
│   ├── japanese_iteration_mark.rs  # Japanese iteration mark normalization
│   ├── mapping.rs                  # Mapping-based text replacement
│   └── regex.rs                    # Regex-based text replacement
├── token_filter.rs                 # TokenFilter trait, TokenFilterLoader
├── token_filter/
│   ├── compound.rs                 # Shared compound-word merge helper (private)
│   ├── japanese_base_form.rs
│   ├── japanese_compound_word.rs
│   ├── japanese_kana.rs
│   ├── japanese_katakana_stem.rs
│   ├── japanese_keep_tags.rs
│   ├── japanese_number.rs
│   ├── japanese_reading_form.rs
│   ├── japanese_stop_tags.rs
│   ├── keep_words.rs
│   ├── korean_compound_word.rs
│   ├── korean_decompound.rs
│   ├── korean_keep_tags.rs
│   ├── korean_number.rs
│   ├── korean_reading_form.rs
│   ├── korean_stop_tags.rs
│   ├── length.rs
│   ├── lowercase.rs
│   ├── mapping.rs
│   ├── numeral.rs                  # Shared numeral conversion engine (private)
│   ├── remove_diacritical_mark.rs
│   ├── stop_words.rs
│   ├── tags.rs                     # Shared keep/stop-tag filtering helpers (private)
│   └── uppercase.rs
└── tokenizer.rs                     # Tokenizer, TokenizerBuilder

Key Components

CharacterFilter

A trait for filters that preprocess text before segmentation. Each implementation provides a name() and an apply(&self, text: &mut String) -> LinderaResult<OffsetMapping> method that rewrites text in place and returns an OffsetMapping describing every transformation it performed.

The OffsetMapping (built from a list of Transformation records) lets the Tokenizer translate token byte offsets computed against the filtered text back to byte offsets in the original input, even after multiple filters have run in sequence. BoxCharacterFilter wraps any CharacterFilter implementation as a boxed, cloneable trait object, and CharacterFilterLoader builds one from a kind string plus a serde_json::Value of arguments (used both by YAML configuration loading and by CLI flag parsing).

TokenFilter

A trait for filters that post-process the tokens produced by the segmenter. Each implementation provides a name() and an apply(&self, tokens: &mut Vec<Token<'_>>) -> LinderaResult<()> method that modifies, merges, reorders, or removes tokens in place. BoxTokenFilter wraps any TokenFilter implementation as a boxed, cloneable trait object, and TokenFilterLoader builds one from a kind string plus a serde_json::Value of arguments, mirroring CharacterFilterLoader.

Tokenizer / TokenizerBuilder

Tokenizer composes character filters, a lindera::segmenter::Segmenter, and token filters into a single analysis pipeline. Calling tokenize runs the character filters over the input text, segments the filtered text, applies the token filters to the resulting tokens, and finally corrects each token's byte offsets back to the original text via the recorded OffsetMappings.

TokenizerBuilder assembles a Tokenizer from a TokenizerConfig (a serde_json::Value), which can be constructed programmatically, loaded from a YAML file (via TokenizerBuilder::from_file, or automatically from the LINDERA_CONFIG_FILE environment variable via TokenizerBuilder::new), or built up incrementally with set_segmenter_mode, set_segmenter_dictionary, append_character_filter, and append_token_filter. See Configuration for the YAML file format and Filters for the full filter reference.

TokenizerBuilder::build loads the dictionaries the configuration names. TokenizerBuilder::build_with_dictionaries (and Tokenizer::from_config_with_dictionaries) takes already-loaded dictionaries instead, such as ones loaded from bytes, and applies every other setting of the configuration to them, filters included; see Building from Config for how the given dictionaries replace the configured ones. The WASM binding's TokenizerBuilder builds its tokenizers this way, with the dictionaries set by setDictionaryInstance() and setUserDictionaryInstance().

AnalysisWorker

AnalysisWorker (created with Tokenizer::new_worker or Tokenizer::into_worker) is a reusable session over the full analysis chain. It owns every per-call buffer — the Viterbi lattice and backtrace scratch (via SegmentWorker), the normalized-text buffer the character filters operate on, and the offset-mapping scratch — so repeated tokenize calls avoid the per-call allocations Tokenizer::tokenize pays. When character filters are configured, token surfaces borrow the worker's buffer instead of being copied into per-token Strings. Returned tokens borrow the worker and must be consumed before the next call; for multi-threaded use, create one worker per thread (or guard one with a Mutex, as lindera-binding does). See the Segmenter page for the underlying SegmentWorker and its automatic memory-shrink policy.

AnalysisWorker's public methods:

  • tokenize(&mut self, text: &str) — tokenizes text through the full analysis chain, reusing the worker's internal buffers; produces exactly the same tokens as Tokenizer::tokenize.
  • tokenize_nbest(&mut self, text: &str, n, unique, cost_threshold) — tokenizes text and returns the top-N results with costs, reusing the worker's internal buffers; produces exactly the same results as Tokenizer::tokenize_nbest.
  • set_mode(&mut self, mode: Mode) — sets the segmentation mode for subsequent calls.
  • set_keep_whitespace(&mut self, keep: bool) — sets whether whitespace tokens are kept in the output for subsequent calls; kept whitespace also stays in the lattice, so the other tokens can differ.
  • set_skip_whitespace(&mut self, skip: bool) — sets whether whitespace is skipped in the lattice for subsequent calls when whitespace tokens are not kept (by default the dictionary's setting).
  • set_max_grouping_len(&mut self, max_grouping_len: Option<usize>), set_unknown_word_ladder(&mut self, unknown_word_ladder: bool) — set the unknown-word grouping cap and the length ladder for subsequent calls.
  • set_space_penalty(&mut self, space_penalty: Option<SpacePenaltyConfig>), set_space_penalty_from_dictionary(&mut self) — set the left-space penalty rules for subsequent calls, or restore the dictionary's.
  • shrink_to(&mut self, text_len_hint: usize) — immediately shrinks the worker's internal buffers to what an input of text_len_hint bytes needs.
  • reset(&mut self) — discards all internal buffers, replacing them with fresh ones; intended for recovery paths (e.g. after a panic poisoned a mutex holding the worker), with configuration (dictionary, filters, mode) preserved.

Feature Flags

FeatureDescriptionDefault
embed-ipadicEmbed the IPADIC dictionary in the binary (forwards to lindera/embed-ipadic)No
embed-ipadic-neologdEmbed the IPADIC-NEologd dictionary in the binary (forwards to lindera/embed-ipadic-neologd)No
embed-unidicEmbed the UniDic dictionary in the binary (forwards to lindera/embed-unidic)No
embed-sudachidictEmbed the SudachiDict dictionary in the binary (forwards to lindera/embed-sudachidict)No
embed-ko-dicEmbed the ko-dic dictionary in the binary (forwards to lindera/embed-ko-dic)No
embed-cc-cedictEmbed the CC-CEDICT dictionary in the binary (forwards to lindera/embed-cc-cedict)No
embed-jiebaEmbed the Jieba dictionary in the binary (forwards to lindera/embed-jieba)No

API Reference

The API reference is available. Please see following URL:

Lindera CLI

A morphological analysis command-line interface for Lindera.

  • Installation - Install or build the CLI
  • Commands - Command reference for list, tokenize, build, download, train, and export
  • Tutorial - Step-by-step guide to get started

Installation

Install via Cargo

You can install the binary via cargo:

% cargo install lindera-cli

Download from GitHub Releases

Alternatively, you can download a pre-built binary from the release page:

Obtaining Dictionaries

Lindera does not bundle dictionaries with the binary. The easiest way to obtain one is the download subcommand, which fetches the pre-built dictionary matching the CLI version and installs it under the OS-standard application data directory:

% lindera download ipadic

After downloading, the dictionary can be referenced by name:

% echo "関西国際空港限定トートバッグ" | lindera tokenize --dict ipadic

See Commands for the available dictionary names and storage locations.

Alternatively, you can download a pre-built dictionary manually from the GitHub Releases page:

# Example: download and extract the IPADIC dictionary
% curl -LO https://github.com/lindera/lindera/releases/download/<version>/lindera-ipadic-<version>.zip
% unzip lindera-ipadic-<version>.zip -d /path/to/ipadic

Then specify the extracted dictionary path when using the CLI (note that the archive contains a lindera-ipadic directory):

% echo "関西国際空港限定トートバッグ" | lindera tokenize --dict /path/to/ipadic/lindera-ipadic

Build from Source

Build without dictionaries (default)

Build a binary containing only the tokenizer and trainer without embedded dictionaries:

% cargo build --release

Build with all features

% cargo build --release --all-features

Build with Embedded Dictionaries (Advanced)

For advanced users who want to embed dictionaries directly into the binary, use the embed-* feature flags. This eliminates the need for external dictionary files at runtime but increases the binary size.

IPADIC (Japanese dictionary)

% cargo build --release --features=embed-ipadic

IPADIC NEologd (Japanese dictionary)

% cargo build --release --features=embed-ipadic-neologd

UniDic (Japanese dictionary)

% cargo build --release --features=embed-unidic

SudachiDict (Japanese dictionary)

% cargo build --release --features=embed-sudachidict

ko-dic (Korean dictionary)

% cargo build --release --features=embed-ko-dic

CC-CEDICT (Chinese dictionary)

% cargo build --release --features=embed-cc-cedict

Jieba (Chinese dictionary)

% cargo build --release --features=embed-jieba

[!TIP] After building with an embed-* feature flag, use the embedded:// scheme to load the embedded dictionary:

% echo "関西国際空港限定トートバッグ" | lindera tokenize --dict embedded://ipadic

See Feature Flags for details.

Commands

The Lindera CLI provides six main commands:

  • list - List the morphological analysis dictionaries and their status
  • tokenize - Perform morphological analysis on text
  • build - Build a dictionary from source CSV files
  • download - Download a pre-built dictionary from the GitHub releases page
  • train - Train a CRF model from annotated corpus data
  • export - Export a trained model to dictionary format

Warnings and log messages

Every command writes its results to standard output and the warnings of the Lindera libraries to standard error. For example, when tokenize reads more than 32 KiB of text without a sentence delimiter, it cuts the sentence there (see Sentence Splitting) and warns:

% head -c 40000 /dev/zero | tr '\0' a | lindera tokenize --dict embedded://ipadic -o wakati > /dev/null
[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, in the syntax of env_logger:

  • Not set: warnings and errors.
  • RUST_LOG=error: errors only, which hides the warnings. RUST_LOG=off writes no log messages at all.
  • RUST_LOG=debug: also debugging messages, such as the files lindera build reads and writes.

The setting does not change standard output, and the error of a command that fails is written whatever the setting. The messages are colored when standard error is a terminal; RUST_LOG_STYLE=never turns the colors off.

list

List all known morphological analysis dictionaries and show, for each one, whether it is embedded in the binary (via the embed-* feature flags) and whether a pre-built copy has been downloaded locally with lindera download.

List parameters

This command takes no arguments. The LINDERA_DATA_DIR environment variable overrides the application data directory searched for downloaded dictionaries, exactly as it does for lindera download.

List usage

% lindera list
NAME            EMBEDDED  DOWNLOADED  PATH
ipadic          yes       yes         /home/user/.local/share/lindera/dictionaries/5.1.0/lindera-ipadic
ipadic-neologd  no        no          -
unidic          no        incomplete  /home/user/.local/share/lindera/dictionaries/5.1.0/lindera-unidic
sudachidict     no        no          -
ko-dic          no        no          -
cc-cedict       no        no          -
jieba           no        no          -

The output contains one dictionary per line:

  • EMBEDDED is yes when the dictionary was embedded at build time (e.g. --features=embed-ipadic).
  • DOWNLOADED is yes when a complete dictionary installed by lindera download exists for the running CLI version, and incomplete when the installation directory exists but required files are missing (re-download it with lindera download <name> --force).
  • PATH shows the installation directory when it exists, and - otherwise.

tokenize

Perform morphological analysis (tokenization) on Japanese, Chinese, or Korean text using various dictionaries.

Parameters

  • --dict / -d: Dictionary path, URI, or downloaded dictionary name (required)
    • File path: /path/to/dictionary
    • Embedded: embedded://ipadic, embedded://unidic, etc.
    • Downloaded dictionary name: ipadic, unidic, etc. — resolved to the dictionary installed by lindera download. An existing filesystem path with the same name takes precedence.
  • --output / -o: Output format (default: mecab)
    • mecab: MeCab-compatible format with part-of-speech info
    • wakati: Space-separated tokens only, one line per input line (an empty line for a line without tokens)
    • json: Detailed JSON format with all token information
  • --user-dict / -u: User dictionary path (optional). An entry whose context ID lies outside the system dictionary's connection matrix is an error (see Context IDs).
  • --mode / -m: Tokenization mode (default: normal)
    • normal: Standard tokenization
    • decompose: Decompose compound words
  • --char-filter / -c: Character filter configuration (JSON)
  • --token-filter / -t: Token filter configuration (JSON)
  • --keep-whitespace: Keep whitespace tokens in the output. Whitespace then also stays in the lattice, so the other tokens can differ from those of the default output, in which whitespace is dropped and, unless the dictionary's metadata.json turns it off (SudachiDict does), skipped in the lattice as MeCab does
  • --disable-skip-whitespace: Keep whitespace in the lattice as a node of its own (a whitespace dictionary entry or the SPACE unknown word) instead of skipping it, while still dropping it from the output (the behavior before v7). With ko-dic, whose connection costs for that node are all zero, every space then resets the connection context. Skipping is on by default for every dictionary whose metadata.json does not turn it off (SudachiDict does). No effect with --keep-whitespace. See the Segmenter documentation
  • --max-grouping-len: Cap on unknown-word grouping, in characters beyond the first (MeCab's max-grouping-size; MeCab defaults to 24). The cap is applied at each position: a run (characters that each share a char.def category with the one before, as in MeCab) longer than the cap is not grouped there, the single-character candidate (plus the length ladder and dictionary words) remains, and the remaining tail is grouped again once it fits, so no unknown token exceeds cap+1 characters. 0 or omitted: unbounded. See the Segmenter documentation for an example
  • --disable-unknown-word-ladder: Disable the MeCab/Vibrato-inspired unknown-word length ladder (char.def's LENGTH field). Enabled by default. Disabling it leaves out these candidates, as before v6; other changes since then, such as unknown words that start inside a grouped run, still apply. See the Segmenter documentation
  • --disable-space-penalty: Disable the left-space penalty (mecab-ko's left-space-penalty-factor) that a dictionary shipping rules in its metadata.json applies by default (ko-dic ships mecab-ko-dic's). A candidate that starts right after whitespace and whose first part-of-speech tag is listed gets the cost added; disabling it together with --disable-skip-whitespace turns off both changes since v6.0 in how Korean is read around spaces (this penalty and whitespace skipping), but not the other output changes since then, such as the split of punctuation runs like .". See the Segmenter documentation
  • --space-penalty-rules: Use explicit left-space penalty rules as JSON instead of the dictionary's, e.g. '{"rules":[{"pos":["JKS","JX"],"cost":6000}]}'. Mutually exclusive with --disable-space-penalty
  • --mmap: Use memory-mapped file loading for the dictionary directory's word list. Ignored for embedded:// dictionaries and when the mmap feature is disabled. Rebuilding or truncating the dictionary directory while a process holds it mapped can cause a SIGBUS on the next lookup.
  • --nbest / -N: Number of N-best results to return (default: 1). When set to 2 or more, N-best output is enabled.
  • --nbest-unique: Deduplicate N-best results by removing paths that produce the same segmentation: each result is the cheapest path of its word boundaries.
  • --nbest-cost-threshold: Maximum cost difference from the best result. Only results with cost within best_cost + threshold are returned; both costs are totals over the whole input line.
  • Input file: Optional file path (default: stdin). The input is read line by line, and each line is tokenized on its own. Only the line terminator (\n, or \r\n) is removed: whitespace at the start or end of a line is handled as elsewhere in the line (spaces and tabs, which are SPACE characters, are skipped and dropped from the output; U+3000 is a token with IPADIC, IPADIC-NEologd and UniDic), and the byte offsets in the JSON output index the line. Unlike MeCab, which outputs the \r of a CRLF line as an unknown word, Lindera removes it

Basic usage

# Tokenize text using a dictionary directory
echo "日本語の形態素解析を行うことができます。" | lindera tokenize \
  --dict /path/to/dictionary

# Tokenize text using embedded dictionary
echo "日本語の形態素解析を行うことができます。" | lindera tokenize \
  --dict embedded://ipadic

# Tokenize with different output format
echo "日本語の形態素解析を行うことができます。" | lindera tokenize \
  --dict embedded://ipadic \
  --output json

# Tokenize text from file
lindera tokenize \
  --dict /path/to/dictionary \
  --output wakati \
  input.txt

Examples with external dictionaries

Tokenize with external IPADIC (Japanese dictionary)

% echo "日本語の形態素解析を行うことができます。" | lindera tokenize \
  --dict /tmp/lindera-ipadic-2.7.0-20250920
日本語  名詞,一般,*,*,*,*,日本語,ニホンゴ,ニホンゴ
の      助詞,連体化,*,*,*,*,の,ノ,ノ
形態素  名詞,一般,*,*,*,*,形態素,ケイタイソ,ケイタイソ
解析    名詞,サ変接続,*,*,*,*,解析,カイセキ,カイセキ
を      助詞,格助詞,一般,*,*,*,を,ヲ,ヲ
行う    動詞,自立,*,*,五段・ワ行促音便,基本形,行う,オコナウ,オコナウ
こと    名詞,非自立,一般,*,*,*,こと,コト,コト
が      助詞,格助詞,一般,*,*,*,が,ガ,ガ
でき    動詞,自立,*,*,一段,連用形,できる,デキ,デキ
ます    助動詞,*,*,*,特殊・マス,基本形,ます,マス,マス
。      記号,句点,*,*,*,*,。,。,。
EOS

Tokenize with external IPADIC Neologd (Japanese dictionary)

% echo "日本語の形態素解析を行うことができます。" | lindera tokenize \
  --dict /tmp/lindera-ipadic-neologd-0.0.7-20200820
日本語  名詞,一般,*,*,*,*,日本語,ニホンゴ,ニホンゴ
の      助詞,連体化,*,*,*,*,の,ノ,ノ
形態素解析      名詞,固有名詞,一般,*,*,*,形態素解析,ケイタイソカイセキ,ケイタイソカイセキ
を      助詞,格助詞,一般,*,*,*,を,ヲ,ヲ
行う    動詞,自立,*,*,五段・ワ行促音便,基本形,行う,オコナウ,オコナウ
こと    名詞,非自立,一般,*,*,*,こと,コト,コト
が      助詞,格助詞,一般,*,*,*,が,ガ,ガ
でき    動詞,自立,*,*,一段,連用形,できる,デキ,デキ
ます    助動詞,*,*,*,特殊・マス,基本形,ます,マス,マス
。      記号,句点,*,*,*,*,。,。,。
EOS

Tokenize with external UniDic (Japanese dictionary)

% echo "日本語の形態素解析を行うことができます。" | lindera tokenize \
  --dict /tmp/lindera-unidic-2.1.2
日本    名詞,固有名詞,地名,国,*,*,ニッポン,日本,日本,ニッポン,日本,ニッポン,固,*,*,*,*
語      名詞,普通名詞,一般,*,*,*,ゴ,語,語,ゴ,語,ゴ,漢,*,*,*,*
の      助詞,格助詞,*,*,*,*,ノ,の,の,ノ,の,ノ,和,*,*,*,*
形態    名詞,普通名詞,一般,*,*,*,ケイタイ,形態,形態,ケータイ,形態,ケータイ,漢,*,*,*,*
素      接尾辞,名詞的,一般,*,*,*,ソ,素,素,ソ,素,ソ,漢,*,*,*,*
解析    名詞,普通名詞,サ変可能,*,*,*,カイセキ,解析,解析,カイセキ,解析,カイセキ,漢,*,*,*,*
を      助詞,格助詞,*,*,*,*,ヲ,を,を,オ,を,オ,和,*,*,*,*
行う    動詞,一般,*,*,五段-ワア行,連体形-一般,オコナウ,行う,行う,オコナウ,行う,オコナウ,和,*,*,*,*
こと    名詞,普通名詞,一般,*,*,*,コト,事,こと,コト,こと,コト,和,コ濁,基本形,*,*
が      助詞,格助詞,*,*,*,*,ガ,が,が,ガ,が,ガ,和,*,*,*,*
でき    動詞,非自立可能,*,*,上一段-カ行,連用形-一般,デキル,出来る,でき,デキ,できる,デキル,和,*,*,*,*
ます    助動詞,*,*,*,助動詞-マス,終止形-一般,マス,ます,ます,マス,ます,マス,和,*,*,*,*
。      補助記号,句点,*,*,*,*,,。,。,,。,,記号,*,*,*,*
EOS

Tokenize with external ko-dic (Korean dictionary)

% echo "한국어의형태해석을실시할수있습니다." | lindera tokenize \
  --dict /tmp/lindera-ko-dic-2.1.1-20180720
한국어  NNG,*,F,한국어,Compound,*,*,한국/NNG/*+어/NNG/*
의      JKG,*,F,의,*,*,*,*
형태    NNG,*,F,형태,*,*,*,*
해석    NNG,행위,T,해석,*,*,*,*
을      JKO,*,T,을,*,*,*,*
실시    NNG,행위,F,실시,*,*,*,*
할      XSV+ETM,*,T,할,Inflect,XSV,ETM,하/XSV/*+ᆯ/ETM/*
수      NNB,*,F,수,*,*,*,*
있      VV,*,T,있,*,*,*,*
습니다  EF,*,F,습니다,*,*,*,*
.       SF,*,*,*,*,*,*,*
EOS

Tokenize with external CC-CEDICT (Chinese dictionary)

% echo "可以进行中文形态学分析。" | lindera tokenize \
  --dict /tmp/lindera-cc-cedict-0.1.0-20261021
可以    *,*,*,*,ke3 yi3,可以,可以,can/may/possible/able to/not bad/pretty good/
进行    *,*,*,*,jin4 xing2,進行,进行,to advance/to conduct/underway/in progress/to do/to carry out/to carry on/to execute/
中文    *,*,*,*,Zhong1 wen2,中文,中文,Chinese language/
形态学  *,*,*,*,xing2 tai4 xue2,形態學,形态学,morphology (in biology or linguistics)/
分析    *,*,*,*,fen1 xi1,分析,分析,to analyze/analysis/CL:個|个[ge4]/
。      *,*,*,*,*,*,*,*
EOS

Tokenize with external Jieba (Chinese dictionary)

% echo "可以进行中文形态学分析。" | lindera tokenize \
  --dict /tmp/lindera-jieba-0.1.3

Examples with embedded dictionaries

Lindera can include dictionaries directly in the binary when built with specific feature flags. This allows tokenization without external dictionary files.

Tokenize with embedded IPADIC (Japanese dictionary)

% echo "日本語の形態素解析を行うことができます。" | lindera tokenize \
  --dict embedded://ipadic
日本語  名詞,一般,*,*,*,*,日本語,ニホンゴ,ニホンゴ
の      助詞,連体化,*,*,*,*,の,ノ,ノ
形態素  名詞,一般,*,*,*,*,形態素,ケイタイソ,ケイタイソ
解析    名詞,サ変接続,*,*,*,*,解析,カイセキ,カイセキ
を      助詞,格助詞,一般,*,*,*,を,ヲ,ヲ
行う    動詞,自立,*,*,五段・ワ行促音便,基本形,行う,オコナウ,オコナウ
こと    名詞,非自立,一般,*,*,*,こと,コト,コト
が      助詞,格助詞,一般,*,*,*,が,ガ,ガ
でき    動詞,自立,*,*,一段,連用形,できる,デキ,デキ
ます    助動詞,*,*,*,特殊・マス,基本形,ます,マス,マス
。      記号,句点,*,*,*,*,。,。,。
EOS

NOTE: To include IPADIC dictionary in the binary, you must build with the --features=embed-ipadic option.

Tokenize with embedded UniDic (Japanese dictionary)

% echo "日本語の形態素解析を行うことができます。" | lindera tokenize \
  --dict embedded://unidic
日本    名詞,固有名詞,地名,国,*,*,ニッポン,日本,日本,ニッポン,日本,ニッポン,固,*,*,*,*
語      名詞,普通名詞,一般,*,*,*,ゴ,語,語,ゴ,語,ゴ,漢,*,*,*,*
の      助詞,格助詞,*,*,*,*,ノ,の,の,ノ,の,ノ,和,*,*,*,*
形態    名詞,普通名詞,一般,*,*,*,ケイタイ,形態,形態,ケータイ,形態,ケータイ,漢,*,*,*,*
素      接尾辞,名詞的,一般,*,*,*,ソ,素,素,ソ,素,ソ,漢,*,*,*,*
解析    名詞,普通名詞,サ変可能,*,*,*,カイセキ,解析,解析,カイセキ,解析,カイセキ,漢,*,*,*,*
を      助詞,格助詞,*,*,*,*,ヲ,を,を,オ,を,オ,和,*,*,*,*
行う    動詞,一般,*,*,五段-ワア行,連体形-一般,オコナウ,行う,行う,オコナウ,行う,オコナウ,和,*,*,*,*
こと    名詞,普通名詞,一般,*,*,*,コト,事,こと,コト,こと,コト,和,コ濁,基本形,*,*
が      助詞,格助詞,*,*,*,*,ガ,が,が,ガ,が,ガ,和,*,*,*,*
でき    動詞,非自立可能,*,*,上一段-カ行,連用形-一般,デキル,出来る,でき,デキ,できる,デキル,和,*,*,*,*
ます    助動詞,*,*,*,助動詞-マス,終止形-一般,マス,ます,ます,マス,ます,マス,和,*,*,*,*
。      補助記号,句点,*,*,*,*,,。,。,,。,,記号,*,*,*,*
EOS

NOTE: To include UniDic dictionary in the binary, you must build with the --features=embed-unidic option.

Tokenize with embedded IPADIC NEologd (Japanese dictionary)

% echo "日本語の形態素解析を行うことができます。" | lindera tokenize \
  --dict embedded://ipadic-neologd
日本語  名詞,一般,*,*,*,*,日本語,ニホンゴ,ニホンゴ
の      助詞,連体化,*,*,*,*,の,ノ,ノ
形態素解析      名詞,固有名詞,一般,*,*,*,形態素解析,ケイタイソカイセキ,ケイタイソカイセキ
を      助詞,格助詞,一般,*,*,*,を,ヲ,ヲ
行う    動詞,自立,*,*,五段・ワ行促音便,基本形,行う,オコナウ,オコナウ
こと    名詞,非自立,一般,*,*,*,こと,コト,コト
が      助詞,格助詞,一般,*,*,*,が,ガ,ガ
でき    動詞,自立,*,*,一段,連用形,できる,デキ,デキ
ます    助動詞,*,*,*,特殊・マス,基本形,ます,マス,マス
。      記号,句点,*,*,*,*,。,。,。
EOS

NOTE: To include UniDic dictionary in the binary, you must build with the --features=embed-ipadic-neologd option.

Tokenize with embedded ko-dic (Korean dictionary)

% echo "한국어의형태해석을실시할수있습니다." | lindera tokenize \
  --dict embedded://ko-dic
한국어  NNG,*,F,한국어,Compound,*,*,한국/NNG/*+어/NNG/*
의      JKG,*,F,의,*,*,*,*
형태    NNG,*,F,형태,*,*,*,*
해석    NNG,행위,T,해석,*,*,*,*
을      JKO,*,T,을,*,*,*,*
실시    NNG,행위,F,실시,*,*,*,*
할      XSV+ETM,*,T,할,Inflect,XSV,ETM,하/XSV/*+ᆯ/ETM/*
수      NNB,*,F,수,*,*,*,*
있      VV,*,T,있,*,*,*,*
습니다  EF,*,F,습니다,*,*,*,*
.       SF,*,*,*,*,*,*,*
EOS

NOTE: To include ko-dic dictionary in the binary, you must build with the --features=embed-ko-dic option.

Tokenize with embedded CC-CEDICT (Chinese dictionary)

% echo "可以进行中文形态学分析。" | lindera tokenize \
  --dict embedded://cc-cedict
可以    *,*,*,*,ke3 yi3,可以,可以,can/may/possible/able to/not bad/pretty good/
进行    *,*,*,*,jin4 xing2,進行,进行,to advance/to conduct/underway/in progress/to do/to carry out/to carry on/to execute/
中文    *,*,*,*,Zhong1 wen2,中文,中文,Chinese language/
形态学  *,*,*,*,xing2 tai4 xue2,形態學,形态学,morphology (in biology or linguistics)/
分析    *,*,*,*,fen1 xi1,分析,分析,to analyze/analysis/CL:個|个[ge4]/
。      *,*,*,*,*,*,*,*
EOS

NOTE: To include CC-CEDICT dictionary in the binary, you must build with the --features=embed-cc-cedict option.

Tokenize with embedded Jieba (Chinese dictionary)

% echo "可以进行中文形态学分析。" | lindera tokenize \
  --dict embedded://jieba

NOTE: To include Jieba dictionary in the binary, you must build with the --features=embed-jieba option.

User dictionary examples

Lindera supports user dictionaries to add custom words alongside system dictionaries. User dictionaries can be in CSV or binary format.

Use user dictionary (CSV format)

% echo "東京スカイツリーの最寄り駅はとうきょうスカイツリー駅です" | lindera tokenize \
  --dict embedded://ipadic \
  --user-dict ./resources/user_dict/ipadic_simple_userdic.csv
東京スカイツリー        カスタム名詞,*,*,*,*,*,東京スカイツリー,トウキョウスカイツリー,*
の      助詞,連体化,*,*,*,*,の,ノ,ノ
最寄り駅        名詞,一般,*,*,*,*,最寄り駅,モヨリエキ,モヨリエキ
は      助詞,係助詞,*,*,*,*,は,ハ,ワ
とうきょうスカイツリー駅        カスタム名詞,*,*,*,*,*,とうきょうスカイツリー駅,トウキョウスカイツリーエキ,*
です    助動詞,*,*,*,特殊・デス,基本形,です,デス,デス
EOS

Use user dictionary (Binary format)

% echo "東京スカイツリーの最寄り駅はとうきょうスカイツリー駅です" | lindera tokenize \
  --dict /tmp/lindera-ipadic-2.7.0-20250920 \
  --user-dict ./resources/user_dict/ipadic_simple_userdic.bin
東京スカイツリー        カスタム名詞,*,*,*,*,*,東京スカイツリー,トウキョウスカイツリー,*
の      助詞,連体化,*,*,*,*,の,ノ,ノ
最寄り駅        名詞,一般,*,*,*,*,最寄り駅,モヨリエキ,モヨリエキ
は      助詞,係助詞,*,*,*,*,は,ハ,ワ
とうきょうスカイツリー駅        カスタム名詞,*,*,*,*,*,とうきょうスカイツリー駅,トウキョウスカイツリーエキ,*
です    助動詞,*,*,*,特殊・デス,基本形,です,デス,デス
EOS

Tokenization modes

Lindera provides two tokenization modes: normal and decompose.

Normal mode (default)

Tokenizes faithfully based on words registered in the dictionary:

% echo "関西国際空港限定トートバッグ" | lindera tokenize \
  --dict embedded://ipadic \
  --mode normal
関西国際空港    名詞,固有名詞,組織,*,*,*,関西国際空港,カンサイコクサイクウコウ,カンサイコクサイクーコー
限定    名詞,サ変接続,*,*,*,*,限定,ゲンテイ,ゲンテイ
トートバッグ    名詞,一般,*,*,*,*,*,*,*
EOS

Decompose mode

Tokenizes compound noun words additionally:

% echo "関西国際空港限定トートバッグ" | lindera tokenize \
  --dict embedded://ipadic \
  --mode decompose
関西    名詞,固有名詞,地域,一般,*,*,関西,カンサイ,カンサイ
国際    名詞,一般,*,*,*,*,国際,コクサイ,コクサイ
空港    名詞,一般,*,*,*,*,空港,クウコウ,クーコー
限定    名詞,サ変接続,*,*,*,*,限定,ゲンテイ,ゲンテイ
トートバッグ    名詞,一般,*,*,*,*,*,*,*
EOS

Output formats

Lindera provides three output formats: mecab, wakati and json.

MeCab format (default)

Outputs results in MeCab-compatible format with part-of-speech information:

% echo "お待ちしております。" | lindera tokenize \
  --dict embedded://ipadic \
  --output mecab
お待ち  名詞,サ変接続,*,*,*,*,お待ち,オマチ,オマチ
し  動詞,自立,*,*,サ変・スル,連用形,する,シ,シ
て  助詞,接続助詞,*,*,*,*,て,テ,テ
おり  動詞,非自立,*,*,五段・ラ行,連用形,おる,オリ,オリ
ます  助動詞,*,*,*,特殊・マス,基本形,ます,マス,マス
。  記号,句点,*,*,*,*,。,。,。
EOS

Wakati format

Outputs only the token text separated by spaces, one line per input line (or per result with -N). A line without tokens, such as an empty line or a line of spaces, gives an empty line, as in MeCab:

% echo "お待ちしております。" | lindera tokenize \
  --dict embedded://ipadic \
  --output wakati
お待ち し て おり ます 。

JSON format

Outputs detailed token information in JSON format:

% echo "お待ちしております。" | lindera tokenize \
  --dict embedded://ipadic \
  --output json
[
  {
    "base_form": "お待ち",
    "byte_end": 9,
    "byte_start": 0,
    "conjugation_form": "*",
    "conjugation_type": "*",
    "part_of_speech": "名詞",
    "part_of_speech_subcategory_1": "サ変接続",
    "part_of_speech_subcategory_2": "*",
    "part_of_speech_subcategory_3": "*",
    "pronunciation": "オマチ",
    "reading": "オマチ",
    "surface": "お待ち",
    "word_id": 14698
  },
  ...
]

N-Best tokenization

Lindera supports N-Best tokenization, which returns the top N tokenization candidates ordered by cost (lower cost = better). This is based on the Forward-DP Backward-A* algorithm, compatible with MeCab's N-Best implementation.

A line is cut into sentences at \t, 。 and 、, but the context is carried across 、 and 。, so the candidates for a line without a tab are the N best segmentations of the whole line (for the few exceptions, see Sentence Splitting). A tab ends a segment: a line with tabs is searched segment by segment, and its candidates are the N cheapest combinations of one path per segment. Every candidate segments the whole line.

Basic N-Best example

% echo "すもももももももものうち" | lindera tokenize \
  --dict embedded://ipadic \
  -N 3
NBEST 1 (cost=21245)
すもも  名詞,一般,*,*,*,*,すもも,スモモ,スモモ
も      助詞,係助詞,*,*,*,*,も,モ,モ
もも    名詞,一般,*,*,*,*,もも,モモ,モモ
も      助詞,係助詞,*,*,*,*,も,モ,モ
もも    名詞,一般,*,*,*,*,もも,モモ,モモ
の      助詞,連体化,*,*,*,*,の,ノ,ノ
うち    名詞,非自立,副詞可能,*,*,*,うち,ウチ,ウチ
EOS
NBEST 2 (cost=24541)
すもも  名詞,一般,*,*,*,*,すもも,スモモ,スモモ
もも    名詞,一般,*,*,*,*,もも,モモ,モモ
も      助詞,係助詞,*,*,*,*,も,モ,モ
もも    名詞,一般,*,*,*,*,もも,モモ,モモ
も      助詞,係助詞,*,*,*,*,も,モ,モ
の      助詞,連体化,*,*,*,*,の,ノ,ノ
うち    名詞,非自立,副詞可能,*,*,*,うち,ウチ,ウチ
EOS
NBEST 3 (cost=24541)
すもも  名詞,一般,*,*,*,*,すもも,スモモ,スモモ
も      助詞,係助詞,*,*,*,*,も,モ,モ
もも    名詞,一般,*,*,*,*,もも,モモ,モモ
もも    名詞,一般,*,*,*,*,もも,モモ,モモ
も      助詞,係助詞,*,*,*,*,も,モ,モ
の      助詞,連体化,*,*,*,*,の,ノ,ノ
うち    名詞,非自立,副詞可能,*,*,*,うち,ウチ,ウチ
EOS

Results 2 and 3 are different segmentations with the same cost (24541).

N-Best with unique results

When the same segmentation appears in multiple paths (differing only in the dictionary entries of their words, such as their parts of speech), use --nbest-unique to deduplicate. Each result is then the cheapest path of its word boundaries, and the search does not go through the other paths, which can be millions for a short line:

% echo "営業部長谷川です" | lindera tokenize \
  --dict embedded://ipadic \
  -N 5 --nbest-unique -o wakati
NBEST 1 (cost=15760)
営業 部長 谷川 です
NBEST 2 (cost=17747)
営業 部 長谷川 です
NBEST 3 (cost=18365)
営業 部 長谷 川 です
NBEST 4 (cost=21125)
営業 部 長 谷川 です
NBEST 5 (cost=22259)
営業 部長 谷 川 です

N-Best with cost threshold

Use --nbest-cost-threshold to limit results to paths within a certain cost range of the best path:

% echo "営業部長谷川です" | lindera tokenize \
  --dict embedded://ipadic \
  -N 10 --nbest-unique --nbest-cost-threshold 5000 -o wakati
NBEST 1 (cost=15760)
営業 部長 谷川 です
NBEST 2 (cost=17747)
営業 部 長谷川 です
NBEST 3 (cost=18365)
営業 部 長谷 川 です

Only 3 results are returned: the 4th result of the example above costs 21125, which exceeds 15760 + 5000 = 20760.

Advanced tokenization with filters

Lindera provides an analytical framework that combines character filters, tokenizers, and token filters for advanced text processing. Filters are configured using JSON.

% echo "すもももももももものうち" | lindera tokenize \
  --dict embedded://ipadic \
  --char-filter 'unicode_normalize:{"kind":"nfkc"}' \
  --token-filter 'japanese_keep_tags:{"tags":["名詞,一般"]}'
すもも  名詞,一般,*,*,*,*,すもも,スモモ,スモモ
もも    名詞,一般,*,*,*,*,もも,モモ,モモ
もも    名詞,一般,*,*,*,*,もも,モモ,モモ
EOS

build

Build (compile) a morphological analysis dictionary from source CSV files for use with Lindera.

Build parameters

  • --src / -s: Source directory containing dictionary CSV files (or single CSV file for user dictionary)
  • --dest / -d: Destination directory for compiled dictionary output
  • --metadata / -m: Metadata configuration file (metadata.json) that defines dictionary structure
  • --user / -u: Build user dictionary instead of system dictionary (optional flag)
  • --system-dict: The system dictionary the user dictionary is for, as a directory path, URI, or downloaded dictionary name, the forms tokenize --dict accepts (optional; only with --user). The build then checks every row's context IDs against its connection matrix (see Context ID check)
  • --context-id-freq / -f: Context-ID access-frequency file used to reorder connection-cost IDs (optional; only meaningful when the dictionary's metadata.json sets connection_id_mapping: true)

Dictionary types

System dictionary

A full morphological analysis dictionary containing:

  • Lexicon entries (word definitions)
  • Connection cost matrix
  • Unknown word handling rules
  • Character type definitions

User dictionary

A supplementary dictionary for custom words that works alongside a system dictionary.

Context ID check

When it builds a system dictionary, lindera build checks that every context ID lies inside the connection matrix, 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. A dictionary that breaks this fails to build, with an error that names the file, the line, the ID and the matrix size. For example, with a 2 x 2 matrix.def and a lex.csv whose second row has the right context ID 2:

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) }
  • If the dictionary's metadata.json sets skip_invalid_cost_or_id to true, such a lex.csv row is dropped instead, with a warning on standard error (see Warnings and log messages). An unk.def line is always checked.
  • A matrix.def row whose IDs fall outside its axes is an error too, and so is an unk.def line with fewer than four fields.
  • A user dictionary (--user) is checked only when --system-dict names the system dictionary it is for, as described below.

With --system-dict, lindera build --user checks every row of the user dictionary against the connection matrix of that system dictionary, with the same bounds. A simple row is checked with the default context IDs of metadata.json (default_left_context_id and default_right_context_id). The first row outside the matrix fails the build, with an error that names its CSV line and surface, and no .bin file is written. For example, against IPADIC's 1316 x 1316 matrix, with a userdic.csv whose second row has the left context ID 60000:

% lindera build --src ./userdic.csv --dest ./user_dict --metadata ./lindera-ipadic/metadata.json --user --system-dict embedded://ipadic
Error: LinderaError { kind: Content, source: user dictionary line 2 (surface: '東京スカイツリー'): left context id 60000 is out of range for the connection matrix (matrix.def size 1316 1316; left context ids must be below 1316) }

Without --system-dict, the build does not know the system dictionary, and the user dictionary builds. lindera tokenize --user-dict then checks it against the system dictionary and fails with an error that names the first entry outside the matrix by its word id (see Context IDs). A .csv given to lindera tokenize --user-dict is built for the system dictionary of --dict as it is loaded, so its error names the CSV line and surface, as with --system-dict.

Cells missing from matrix.def

A matrix.def does not have to list every cell. lindera build gives a cell that it leaves out the cost 0, as MeCab's mecab-dict-index does. Up to v6.2.0, such a cell cost 32767 (i16::MAX), which made the connection very expensive. For example, with the following matrix.def, the connections from the right context ID 1 to the left context IDs 0 and 1 cost 10 and 20, and the other seven cells cost 0:

3 3
1 0 10
1 1 20

The bundled dictionaries and the matrix.def that lindera export writes list every cell, so this rule does not change them.

char.def and unk.def check

When it builds a system dictionary, lindera build also checks char.def and unk.def against each other, as mecab-dict-index does:

  • A range line of char.def names only categories defined on an earlier line.
  • Each category is defined once.
  • Every unk.def line names a category that char.def defines.
  • Every char.def category has at least one unk.def line.

A dictionary that breaks one of these fails to build, with an error that names the file, the category and, except for a category without an unk.def line, the line. Such a dictionary used to build, and then lindera tokenize panicked on a character of an undefined category, gave some categories the settings of others, stopped its output before a character that no word or unknown word covers, or ignored the unk.def line. For example, with a char.def that defines the SPACE category and an unk.def that has no SPACE line:

Error: LinderaError { kind: Content, source: ./dict-source/unk.def: no line for category SPACE, which char.def defines; every char.def category needs at least one unk.def line }

lindera train checks its --char-def and --unk-def in the same way before it trains (see Training Pipeline).

Examples

Build IPADIC (Japanese dictionary)

# Download and extract IPADIC source files
% curl -L -o /tmp/mecab-ipadic-2.7.0-20250920.tar.gz "https://Lindera.dev/mecab-ipadic-2.7.0-20250920.tar.gz"
% tar zxvf /tmp/mecab-ipadic-2.7.0-20250920.tar.gz -C /tmp

# Build the dictionary
% lindera build \
  --src /tmp/mecab-ipadic-2.7.0-20250920 \
  --dest /tmp/lindera-ipadic-2.7.0-20250920 \
  --metadata ./lindera-ipadic/metadata.json

Build IPADIC NEologd (Japanese dictionary)

% curl -L -o /tmp/mecab-ipadic-neologd-0.0.7-20200820.tar.gz "https://lindera.dev/mecab-ipadic-neologd-0.0.7-20200820.tar.gz"
% tar zxvf /tmp/mecab-ipadic-neologd-0.0.7-20200820.tar.gz -C /tmp

% lindera build \
  --src /tmp/mecab-ipadic-neologd-0.0.7-20200820 \
  --dest /tmp/lindera-ipadic-neologd-0.0.7-20200820 \
  --metadata ./lindera-ipadic-neologd/metadata.json

Build UniDic (Japanese dictionary)

% curl -L -o /tmp/unidic-mecab-2.1.2.tar.gz "https://Lindera.dev/unidic-mecab-2.1.2.tar.gz"
% tar zxvf /tmp/unidic-mecab-2.1.2.tar.gz -C /tmp

% lindera build \
  --src /tmp/unidic-mecab-2.1.2 \
  --dest /tmp/lindera-unidic-2.1.2 \
  --metadata ./lindera-unidic/metadata.json \
  --context-id-freq ./lindera-unidic/context_id_freq.txt

Build CC-CEDICT (Chinese dictionary)

% curl -L -o /tmp/CC-CEDICT-MeCab-0.1.0-20261021.tar.gz "https://lindera.dev/CC-CEDICT-MeCab-0.1.0-20261021.tar.gz"
% tar zxvf /tmp/CC-CEDICT-MeCab-0.1.0-20261021.tar.gz -C /tmp

% lindera build \
  --src /tmp/CC-CEDICT-MeCab-0.1.0-20261021 \
  --dest /tmp/lindera-cc-cedict-0.1.0-20261021 \
  --metadata ./lindera-cc-cedict/metadata.json

Build Jieba (Chinese dictionary)

% curl -L -o /tmp/mecab-jieba-0.1.3.tar.gz "https://lindera.dev/mecab-jieba-0.1.3.tar.gz"
% tar zxvf /tmp/mecab-jieba-0.1.3.tar.gz -C /tmp

% lindera build \
  --src /tmp/mecab-jieba-0.1.3/dict-src \
  --dest /tmp/lindera-jieba-0.1.3 \
  --metadata ./lindera-jieba/metadata.json

Build ko-dic (Korean dictionary)

% curl -L -o /tmp/mecab-ko-dic-2.1.1-20180720.tar.gz "https://Lindera.dev/mecab-ko-dic-2.1.1-20180720.tar.gz"
% tar zxvf /tmp/mecab-ko-dic-2.1.1-20180720.tar.gz -C /tmp

% lindera build \
  --src /tmp/mecab-ko-dic-2.1.1-20180720 \
  --dest /tmp/lindera-ko-dic-2.1.1-20180720 \
  --metadata ./lindera-ko-dic/metadata.json \
  --context-id-freq ./lindera-ko-dic/context_id_freq.txt

Build user dictionaries

Build IPADIC user dictionary (Japanese)

For more details about user dictionary format please refer to the following URL:

% lindera build \
  --src ./resources/user_dict/ipadic_simple_userdic.csv \
  --dest ./resources/user_dict \
  --metadata ./lindera-ipadic/metadata.json \
  --user

Add --system-dict embedded://ipadic, or the path of a built IPADIC, to check the rows' context IDs against IPADIC's connection matrix (see Context ID check).

Build UniDic user dictionary (Japanese)

For more details about user dictionary format please refer to the following URL:

% lindera build \
  --src ./resources/user_dict/unidic_simple_userdic.csv \
  --dest ./resources/user_dict \
  --metadata ./lindera-unidic/metadata.json \
  --user

Build SudachiDict user dictionary (Japanese)

For more details about user dictionary format please refer to the following URL:

% lindera build \
  --src ./resources/user_dict/sudachidict_simple_userdic.csv \
  --dest ./resources/user_dict \
  --metadata ./lindera-sudachidict/metadata.json \
  --user

Build CC-CEDICT user dictionary (Chinese)

For more details about user dictionary format please refer to the following URL:

% lindera build \
  --src ./resources/user_dict/cc-cedict_simple_userdic.csv \
  --dest ./resources/user_dict \
  --metadata ./lindera-cc-cedict/metadata.json \
  --user

Build Jieba user dictionary (Chinese)

For more details about user dictionary format please refer to the following URL:

% lindera build \
  --src ./resources/user_dict/jieba_simple_userdic.csv \
  --dest ./resources/user_dict \
  --metadata ./lindera-jieba/metadata.json \
  --user

Build ko-dic user dictionary (Korean)

For more details about user dictionary format please refer to the following URL:

% lindera build \
  --src ./resources/user_dict/ko-dic_simple_userdic.csv \
  --dest ./resources/user_dict \
  --metadata ./lindera-ko-dic/metadata.json \
  --user

download

Download the pre-built dictionary archive matching the CLI version from the GitHub releases page and install it under the OS-standard application data directory.

Download parameters

  • Dictionary name (required): one of ipadic, ipadic-neologd, unidic, sudachidict, ko-dic, cc-cedict, jieba
  • --force: Re-download and replace an existing dictionary

Storage location

Dictionaries are installed under a versioned layout <data dir>/dictionaries/<version>/lindera-<name>/. The default base directory per OS:

OSDefault location
Linux~/.local/share/lindera
macOS~/Library/Application Support/lindera
Windows%LOCALAPPDATA%\lindera

Set the LINDERA_DATA_DIR environment variable to override the base directory. This variable is unrelated to the build-time source cache variable LINDERA_BUILD_DICTIONARY_CACHE_DIR.

Download usage

% lindera download ipadic
Downloading https://github.com/lindera/lindera/releases/download/v5.1.0/lindera-ipadic-5.1.0.zip
100% (15.1/15.1 MiB)
Downloaded dictionary 'ipadic'
/home/user/.local/share/lindera/dictionaries/5.1.0/lindera-ipadic

The installed path is printed to stdout (progress and notices go to stderr), so it can be captured in scripts:

% DICT_DIR=$(lindera download ipadic)

After downloading, the dictionary can be referenced by name in tokenize:

% echo "日本語の形態素解析を行うことができます。" | lindera tokenize --dict ipadic

Notes:

  • The archive version always matches the CLI version; running a development build whose version has no published release yet fails with HTTP 404.
  • Integrity is verified via the HTTPS transport and the ZIP archive's built-in per-file CRC32 checksums.
  • The largest dictionary (ipadic-neologd) is a ~305 MB download and requires roughly twice that amount of free disk space during extraction.

train

Train a new morphological analysis model from annotated corpus data. To use this feature, you must build with the train feature flag enabled. (The train feature flag is enabled by default.)

Train parameters

  • --seed / -s: Seed lexicon file (CSV format) to be weighted
  • --corpus / -c: Training corpus (annotated text)
  • --char-def / -C: Character definition file (char.def)
  • --unk-def / -u: Unknown word definition file (unk.def) to be weighted
  • --feature-def / -f: Feature definition file (feature.def)
  • --rewrite-def / -r: Rewrite rule definition file (rewrite.def)
  • --output / -o: Output model file
  • --lambda / -l: Regularization coefficient, finite and >= 0 (0 disables regularization); with l2, MeCab's learner's -c C is 1 / C (default: 1.0, MeCab's -c 1.0)
  • --regularization / -R: Regularization type: l1, l2, or elasticnet, in any case; elastic_net and elastic-net also select Elastic Net (default: l2)
  • --elastic-net-l1-ratio: L1 ratio for Elastic Net regularization (0.0-1.0), only used with --regularization elasticnet; 0 uses only L2, 1 uses only L1 (default: 0.5)
  • --max-iterations / -i: Maximum number of iterations for training, 1 or more (default: 100)
  • --eta / -e: Tolerance of the convergence test: training stops before --max-iterations once the relative change of the objective stays below this value for 3 iterations in a row; 0 disables the test (default: 0.00005, the default of MeCab's learner)
  • --max-threads / -t: Maximum number of threads, 1 or more (defaults to CPU core count; affects only speed, never the trained model)

Basic workflow

1. Prepare training files

Seed lexicon file (seed.csv):

The seed lexicon file contains initial dictionary entries used for training the CRF model. Each line represents a word entry with comma-separated fields:

  • Surface
  • Left context ID
  • Right context ID
  • Word cost
  • Part-of-speech tags (multiple fields)
  • Base form
  • Reading (katakana)
  • Pronunciation

Note: The exact field definitions differ between dictionary formats (IPADIC, UniDic, ko-dic, CC-CEDICT). Please refer to each dictionary's format specification for details.

外国,0,0,0,名詞,一般,*,*,*,*,外国,ガイコク,ガイコク
人,0,0,0,名詞,接尾,一般,*,*,*,人,ジン,ジン

The file is read as CSV, as lindera build reads lex.csv: a field may be quoted (",",0,0,0,...), and a line that starts with # is an entry, not a comment. See Seed Lexicon for the details.

Training corpus (corpus.txt):

The training corpus file contains annotated text data used to train the CRF model. Each line consists of:

  • A surface form (word) followed by a tab character
  • Comma-separated morphological features (part-of-speech tags, base form, reading, pronunciation)
  • Sentences are separated by "EOS" (End Of Sentence) markers
外国	名詞,一般,*,*,*,*,外国,ガイコク,ガイコク
人	名詞,接尾,一般,*,*,*,人,ジン,ジン
参政	名詞,サ変接続,*,*,*,*,参政,サンセイ,サンセイ
権	名詞,接尾,一般,*,*,*,権,ケン,ケン
EOS

For detailed information about file formats and advanced features, see Training Pipeline.

2. Train model

lindera train \
  --seed ./resources/training/seed.csv \
  --corpus ./resources/training/corpus.txt \
  --unk-def ./resources/training/unk.def \
  --char-def ./resources/training/char.def \
  --feature-def ./resources/training/feature.def \
  --rewrite-def ./resources/training/rewrite.def \
  --output /tmp/lindera/training/model.dat \
  --max-iterations 100

3. Training results

lindera train writes the model to the --output file. Once it has built the training lattices, it prints how the gold tokens of the corpus were labelled (see How training works). With the files in ./resources/training/:

Lattice statistics: gold_lexicon=14 gold_unknown=0 gold_virtual=0 virtual_labels=0 negative_lexicon=0 negative_unknown=17

When no gold token is a seed word, or fewer than half of the gold tokens whose surface has seed rows are, it then writes a warning on stderr, with the first gold token whose surface has seed rows but none with its feature string, next to those rows (see How training works).

Once training stops, it prints why, with the number of iterations and of evaluations of the loss and its gradient (see --eta above):

L-BFGS stopped after 8 iterations and 9 evaluations of the loss and its gradient: converged

When an iteration left the objective unchanged or the line search failed far from the optimum, it then writes a warning on stderr (see How training works).

The trained model contains:

  • Seed words: every seed lexicon row, with a learned weight and context IDs
  • Unknown words: every unk.def entry, in file order, with a learned weight and context IDs

Corpus words that are not in the seed lexicon are not added as words. They are learned through the unk.def entries, or through virtual labels, which are trained but not exported.

export

Export a trained model file to Lindera dictionary format files. This feature requires building with the train feature flag enabled.

Export parameters

  • --model / -m: Path to the trained model file (.dat format)
  • --output / -o: Directory to output the dictionary files
  • --metadata: Optional metadata.json file to update with trained model information
  • --cost-factor: Override cost factor for weight-to-cost conversion (default: value from trained model, typically 700)

Output files

The export command creates the following dictionary files in the output directory:

  • lex.csv: Lexicon file with learned weights (MeCab-compatible cost via tocost())
  • matrix.def: Dense connection cost matrix covering all (right_id, left_id) pairs of the model's context IDs, plus 0 for BOS/EOS. A pair without a trained weight costs 0, as in training and in MeCab
  • unk.def: Unknown word definitions, one line for each unk.def entry of the training input, in the same order
  • char.def: Character type definitions
  • feature.def: Feature template definitions (copied from trained model)
  • rewrite.def: Feature rewrite rules (copied from trained model)
  • left-id.def: Left context ID to feature string mapping
  • right-id.def: Right context ID to feature string mapping
  • metadata.json: Updated metadata file (if --metadata option is provided)

Model format version

lindera export reads a model of the current format version (3.0.0) only. A model.dat written by an earlier version, which includes every model.dat written before 7.0.0, is rejected, and has to be trained again with lindera train. For a model trained with the 6.1.0 CLI:

Error: LinderaError { kind: Deserialize, source: failed to deserialize model: failed without error information; enable debug assertions and the `alloc` feature in rancor for error information. The file is not a legacy JSON model either, so it was most likely written by a different version of lindera-trainer; re-run `lindera train` to regenerate it. }

Complete workflow example

1. Train model

lindera train \
  --seed ./resources/training/seed.csv \
  --corpus ./resources/training/corpus.txt \
  --unk-def ./resources/training/unk.def \
  --char-def ./resources/training/char.def \
  --feature-def ./resources/training/feature.def \
  --rewrite-def ./resources/training/rewrite.def \
  --output /tmp/lindera/training/model.dat \
  --max-iterations 100

2. Export to dictionary format

lindera export \
  --model /tmp/lindera/training/model.dat \
  --metadata ./resources/training/metadata.json \
  --output /tmp/lindera/training/dictionary

3. Build dictionary

lindera build \
  --src /tmp/lindera/training/dictionary \
  --dest /tmp/lindera/training/compiled_dictionary \
  --metadata /tmp/lindera/training/dictionary/metadata.json

4. Use trained dictionary

echo "これは外国人参政権です。" | lindera tokenize -d /tmp/lindera/training/compiled_dictionary
これ	名詞,代名詞,一般,*,*,*,これ,コレ,コレ
は	助詞,係助詞,*,*,*,*,は,ハ,ワ
外国	名詞,一般,*,*,*,*,外国,ガイコク,ガイコク
人	名詞,接尾,一般,*,*,*,人,ジン,ジン
参政	名詞,サ変接続,*,*,*,*,参政,サンセイ,サンセイ
権	名詞,接尾,一般,*,*,*,権,ケン,ケン
です	助動詞,*,*,*,特殊・デス,基本形,です,デス,デス
。	記号,句点,*,*,*,*,。,。,。
EOS

Metadata update feature

When the --metadata option is provided, the export command will:

  1. Read the base metadata.json file to preserve existing configuration
  2. Update specific fields with values from the trained model:
    • default_word_cost: Calculated from feature weight median
    • model_info: Training statistics including feature count, label count, matrix size (connection_matrix_size, the size of matrix.def as rows x columns), the number of training iterations that ran (training_iterations, at most --max-iterations), the regularization coefficient (regularization), its type (regularization_type: l1, l2 or elasticnet) and, for Elastic Net only, its L1 ratio (elastic_net_l1_ratio), and the model format version. No timestamp is written, so exporting the same model twice produces identical output
  3. Preserve existing settings such as dictionary name, character encoding, schema definitions, and other user-defined configuration

Tutorial

This tutorial walks you through the basic usage of the Lindera CLI, from installation to advanced text processing.

1. Install the CLI

Install Lindera CLI:

% cargo install lindera-cli

Verify the installation:

% lindera --help

2. Download a dictionary

Download the pre-built IPADIC dictionary from the GitHub releases page. It is installed under the OS-standard application data directory:

% lindera download ipadic

See Commands for the available dictionary names and storage locations.

3. Basic tokenization

Tokenize Japanese text using the downloaded IPADIC dictionary, referencing it by name:

% echo "東京は日本の首都です。" | lindera tokenize \
  --dict ipadic

Expected output:

東京    名詞,固有名詞,地域,一般,*,*,東京,トウキョウ,トーキョー
は      助詞,係助詞,*,*,*,*,は,ハ,ワ
日本    名詞,固有名詞,地域,国,*,*,日本,ニホン,ニホン
の      助詞,連体化,*,*,*,*,の,ノ,ノ
首都    名詞,一般,*,*,*,*,首都,シュト,シュト
です    助動詞,*,*,*,特殊・デス,基本形,です,デス,デス
。      記号,句点,*,*,*,*,。,。,。
EOS

4. Try different output formats

Wakati format (word segmentation only)

% echo "東京は日本の首都です。" | lindera tokenize \
  --dict ipadic \
  --output wakati

Expected output:

東京 は 日本 の 首都 です 。

JSON format (detailed information)

% echo "東京は日本の首都です。" | lindera tokenize \
  --dict ipadic \
  --output json

This produces a JSON array with detailed token information including byte offsets, part-of-speech tags, readings, and more.

5. Use decompose mode

Decompose mode splits compound nouns into their constituent parts:

% echo "関西国際空港限定トートバッグ" | lindera tokenize \
  --dict ipadic \
  --mode decompose

Expected output:

関西    名詞,固有名詞,地域,一般,*,*,関西,カンサイ,カンサイ
国際    名詞,一般,*,*,*,*,国際,コクサイ,コクサイ
空港    名詞,一般,*,*,*,*,空港,クウコウ,クーコー
限定    名詞,サ変接続,*,*,*,*,限定,ゲンテイ,ゲンテイ
トートバッグ    名詞,一般,*,*,*,*,*,*,*
EOS

Compare with normal mode, where "関西国際空港" remains as a single token.

6. Apply character and token filters

Use Unicode normalization and keep only common nouns:

% echo "Linderaは形態素解析エンジンです。" | lindera tokenize \
  --dict ipadic \
  --char-filter 'unicode_normalize:{"kind":"nfkc"}' \
  --token-filter 'japanese_keep_tags:{"tags":["名詞,一般","名詞,サ変接続","名詞,固有名詞,組織"]}'

Expected output:

Lindera 名詞,固有名詞,組織,*,*,*,*,*,*
形態素  名詞,一般,*,*,*,*,形態素,ケイタイソ,ケイタイソ
解析    名詞,サ変接続,*,*,*,*,解析,カイセキ,カイセキ
エンジン        名詞,一般,*,*,*,*,エンジン,エンジン,エンジン
EOS

The Unicode normalization converts full-width characters to half-width, and the token filter keeps only tokens matching the specified part-of-speech tags.

You can also combine multiple filters:

% echo "すもももももももものうち" | lindera tokenize \
  --dict ipadic \
  --token-filter 'japanese_stop_tags:{"tags":["助詞,格助詞,一般","助詞,係助詞","助詞,連体化","助動詞"]}'

7. Use user dictionary

Create a CSV file with custom word entries (e.g., my_dict.csv):

東京スカイツリー,カスタム名詞,トウキョウスカイツリー

Tokenize with the user dictionary:

% echo "東京スカイツリーの最寄り駅はとうきょうスカイツリー駅です" | lindera tokenize \
  --dict ipadic \
  --user-dict ./my_dict.csv

Without the user dictionary, "東京スカイツリー" would be split into multiple tokens. With the user dictionary, it is recognized as a single token.

For pre-built user dictionary examples, see:

% echo "東京スカイツリーの最寄り駅はとうきょうスカイツリー駅です" | lindera tokenize \
  --dict ipadic \
  --user-dict ./resources/user_dict/ipadic_simple_userdic.csv

Expected output:

東京スカイツリー        カスタム名詞,*,*,*,*,*,東京スカイツリー,トウキョウスカイツリー,*
の      助詞,連体化,*,*,*,*,の,ノ,ノ
最寄り駅        名詞,一般,*,*,*,*,最寄り駅,モヨリエキ,モヨリエキ
は      助詞,係助詞,*,*,*,*,は,ハ,ワ
とうきょうスカイツリー駅        カスタム名詞,*,*,*,*,*,とうきょうスカイツリー駅,トウキョウスカイツリーエキ,*
です    助動詞,*,*,*,特殊・デス,基本形,です,デス,デス
EOS

Lindera Python

Lindera Python provides Python bindings for the Lindera morphological analysis engine, built with PyO3. It brings Lindera's high-performance tokenization capabilities to the Python ecosystem with support for Python 3.10 and later.

Features

  • Multi-language support: Tokenize Japanese (IPADIC, IPADIC NEologd, UniDic), Korean (ko-dic), and Chinese (CC-CEDICT, Jieba) text
  • Text processing pipeline: Compose character filters and token filters for flexible preprocessing and postprocessing
  • CRF-based dictionary training: Train custom morphological analysis models from annotated corpora (requires train feature)
  • Multiple tokenization modes: Normal and decompose modes for different analysis granularity
  • N-best tokenization: Retrieve multiple tokenization candidates ranked by cost
  • User dictionaries: Extend system dictionaries with custom vocabulary

Documentation

Installation

Installing from PyPI

Pre-built wheels are available on PyPI:

pip install lindera

The wheels cover CPython 3.10 to 3.15 on Linux (x86_64 and aarch64, glibc), macOS (x86_64 and arm64), and Windows (x64 and arm64). On any other platform, such as musl-based Linux (Alpine), other CPU architectures, or free-threaded CPython, pip install lindera builds the package from the source distribution instead. That build needs the Rust toolchain 1.89 or later (install it via rustup) and a C linker such as cc, and takes several minutes.

[!NOTE] The PyPI package does not include dictionaries. See Obtaining Dictionaries below.

Obtaining Dictionaries

Lindera does not bundle dictionaries with the package. You need to obtain a pre-built dictionary separately.

Download from GitHub Releases

Pre-built dictionaries are available on the GitHub Releases page. Download and extract the dictionary archive to a local directory:

# Example: download and extract the IPADIC dictionary
curl -LO https://github.com/lindera/lindera/releases/download/<version>/lindera-ipadic-<version>.zip
unzip lindera-ipadic-<version>.zip -d /path/to/ipadic

Building from Source

If you need to build from source (e.g., to enable specific feature flags), the following prerequisites are required:

  • Python 3.10 or later (up to 3.15)
  • Rust toolchain -- Install via rustup
  • maturin -- Python package for building Rust-based Python extensions

Install maturin with pip:

pip install maturin

Development Build

Build and install lindera-python in development mode:

cd lindera-python
maturin develop

Or let the project Makefile create the virtual environment that maturin develop installs into: make setup-venv creates lindera-python/.venv with maturin and pytest installed:

make setup-venv
source lindera-python/.venv/bin/activate
cd lindera-python
maturin develop

Build with Training Support

The train feature enables CRF-based dictionary training functionality. It is enabled by default:

maturin develop --features train

Feature Flags

FeatureDescriptionDefault
trainCRF training functionalityEnabled
embed-ipadicEmbed Japanese dictionary (IPADIC) into the binaryDisabled
embed-unidicEmbed Japanese dictionary (UniDic) into the binaryDisabled
embed-sudachidictEmbed Japanese dictionary (SudachiDict) into the binaryDisabled
embed-ipadic-neologdEmbed Japanese dictionary (IPADIC NEologd) into the binaryDisabled
embed-ko-dicEmbed Korean dictionary (ko-dic) into the binaryDisabled
embed-cc-cedictEmbed Chinese dictionary (CC-CEDICT) into the binaryDisabled
embed-jiebaEmbed Chinese dictionary (Jieba) into the binaryDisabled
embed-cjkEmbed all CJK dictionaries (IPADIC, ko-dic, Jieba) into the binaryDisabled

Multiple features can be combined:

maturin develop --features "train,embed-ipadic,embed-ko-dic"

[!TIP] If you want to embed a dictionary directly into the binary (advanced usage), enable the corresponding embed-* feature flag and load it using the embedded:// scheme:

dictionary = load_dictionary("embedded://ipadic")

See Feature Flags for details.

Verifying the Installation

After installation, verify that lindera is available in Python:

import lindera

print(lindera.version())

Quick Start

This guide shows how to tokenize text using lindera-python.

Basic Tokenization

The recommended way to create a tokenizer is through TokenizerBuilder:

from lindera import Mode, TokenizerBuilder

builder = TokenizerBuilder()
builder.set_mode(Mode.normal())
builder.set_dictionary("/path/to/ipadic")
tokenizer = builder.build()

tokens = tokenizer.tokenize("関西国際空港限定トートバッグ")
for token in tokens:
    print(f"{token.surface}\t{','.join(token.details)}")

Note: Download a pre-built dictionary from GitHub Releases and specify the path to the extracted directory.

Expected output:

関西国際空港    名詞,固有名詞,組織,*,*,*,関西国際空港,カンサイコクサイクウコウ,カンサイコクサイクーコー
限定    名詞,サ変接続,*,*,*,*,限定,ゲンテイ,ゲンテイ
トートバッグ    名詞,一般,*,*,*,*,*,*,*

Method Chaining

TokenizerBuilder supports method chaining for concise configuration:

from lindera import Mode, TokenizerBuilder

tokenizer = (
    TokenizerBuilder()
    .set_mode(Mode.normal())
    .set_dictionary("/path/to/ipadic")
    .build()
)

tokens = tokenizer.tokenize("すもももももももものうち")
for token in tokens:
    print(f"{token.surface}\t{token.get_detail(0)}")

Accessing Token Properties

Each token exposes the following properties:

from lindera import TokenizerBuilder

tokenizer = TokenizerBuilder().set_dictionary("/path/to/ipadic").build()
tokens = tokenizer.tokenize("東京タワー")

for token in tokens:
    print(f"Surface: {token.surface}")
    print(f"Byte range: {token.byte_start}..{token.byte_end}")
    print(f"Position: {token.position}")
    print(f"Word ID: {token.word_id}")
    print(f"Unknown: {token.is_unknown}")
    print(f"Details: {token.details}")
    print()

N-best Tokenization

Retrieve multiple tokenization candidates ranked by cost:

from lindera import TokenizerBuilder

tokenizer = TokenizerBuilder().set_dictionary("/path/to/ipadic").build()
results = tokenizer.tokenize_nbest("すもももももももものうち", n=3)

for tokens, cost in results:
    surfaces = [t.surface for t in tokens]
    print(f"Cost {cost}: {' / '.join(surfaces)}")

Tokenizer API

TokenizerBuilder

TokenizerBuilder configures and constructs a Tokenizer instance using the builder pattern.

Constructors

TokenizerBuilder()

Creates a new builder with default configuration.

from lindera import TokenizerBuilder

builder = TokenizerBuilder()

TokenizerBuilder().from_file(file_path)

Loads configuration from a YAML file and returns a new builder. See lindera-python/resources/lindera.yml for a complete example covering segmenter, character_filters, and token_filters.

builder = TokenizerBuilder().from_file("lindera.yml")

Configuration Methods

All setter methods return self for method chaining.

set_mode(mode)

Sets the tokenization mode. mode is a Mode:

  • Mode.normal() -- Standard tokenization (default)
  • Mode.decompose(penalty=None) -- Decomposes compound words into smaller units, with the length penalties of a Penalty; None uses the default penalty

A value that is not a Mode, such as the string "normal", raises TypeError. To set a mode from its name, such as a configuration value, create it with Mode.from_str().

from lindera import Mode, Penalty

builder.set_mode(Mode.normal())
builder.set_mode(Mode.decompose(Penalty(kanji_penalty_length_penalty=0)))
builder.set_mode(Mode.from_str("decompose"))

set_dictionary(path)

Sets the system dictionary path or URI.

# Use an embedded dictionary
builder.set_dictionary("embedded://ipadic")

# Use an external dictionary
builder.set_dictionary("/path/to/dictionary")

set_user_dictionary(uri)

Sets the user dictionary URI.

builder.set_user_dictionary("/path/to/user_dictionary.csv")

set_keep_whitespace(keep)

Controls whether whitespace tokens appear in the output. Kept whitespace also stays in the lattice, so the other tokens can differ from those of the default output, which skips whitespace in the lattice as MeCab does. To keep whitespace in the lattice without outputting it, use set_skip_whitespace(False) instead.

builder.set_keep_whitespace(True)

set_skip_whitespace(skip)

Sets whether whitespace is skipped in the lattice. skip has the same meaning as segmenter.skip_whitespace in a configuration file:

  • True -- Skips whitespace, so the words on either side of it connect directly, as MeCab does.
  • False -- Keeps whitespace in the lattice as a node of its own (a whitespace dictionary entry or the SPACE unknown word), as before v7. Whitespace is still dropped from the output.
  • None -- The default of the dictionary's metadata.json: skipping, except with SudachiDict. Use it to undo an earlier call.

A value set here overrides the dictionary's default and a skip_whitespace loaded with from_file. It has no effect while whitespace tokens are kept (set_keep_whitespace(True)), because kept whitespace always stays in the lattice. Any other value raises TypeError.

With IPADIC, for example, Google が 新しい reads が as a particle (助詞) by default and as a conjunction (接続詞) with set_skip_whitespace(False). See Segmenter for details.

# Keep whitespace in the lattice, as before v7
builder.set_skip_whitespace(False)

set_space_penalty(value)

Sets the left-space penalty for Korean: a candidate that starts right after whitespace and whose part-of-speech tag normally attaches to the preceding word (particles, endings, ...) gets a cost added. value has the same meaning as segmenter.space_penalty in a configuration file:

  • None -- The default: the rules the dictionary ships in its metadata.json, if any (ko-dic ships mecab-ko-dic's rules; the other bundled dictionaries ship none). Use it to undo an earlier call.
  • False -- Turns the penalty off.
  • True -- Requires the dictionary's rules; build() raises ValueError for a dictionary that ships none.
  • A dict of the form {"rules": [{"pos": [...], "cost": n}, ...]} -- Applies these rules instead. A candidate whose first part-of-speech tag is listed in pos gets cost added; the first matching rule wins.

Any other value raises ValueError (a value with no JSON equivalent, such as an arbitrary object, raises TypeError, as in append_token_filter).

# Turn the penalty off
builder.set_space_penalty(False)

# Explicit rules
builder.set_space_penalty({
    "rules": [
        {"pos": ["EC", "EF", "EP", "ETM", "ETN", "VCP", "XSA", "XSN", "XSV"], "cost": 3000},
        {"pos": ["JC", "JKB", "JKC", "JKG", "JKO", "JKQ", "JKS", "JKV", "JX"], "cost": 6000},
    ]
})

[!NOTE] Since v6.1.0, ko-dic applies the left-space penalty by default, as mecab-ko does. For example, 검색 이 잘 된다 reads 이 as the interjection IC; without the penalty, as the subject particle JKS. Call builder.set_space_penalty(False) to turn it off. Only a ko-dic built by Lindera 6.1.0 or later ships the rules; with a ko-dic from the v6.0.0 release the penalty stays off and True fails until the dictionary is rebuilt. See Segmenter for details.

append_character_filter(kind, args=None)

Appends a character filter to the preprocessing pipeline.

builder.append_character_filter("unicode_normalize", {"kind": "nfkc"})

append_token_filter(kind, args=None)

Appends a token filter to the postprocessing pipeline.

builder.append_token_filter("lowercase", {})

The arguments of set_space_penalty, append_character_filter and append_token_filter may nest lists and dicts at most 128 levels deep. A deeper value, or one that contains itself, raises ValueError.

Build

build()

Builds and returns a Tokenizer with the configured settings.

tokenizer = builder.build()

Tokenizer

Tokenizer performs morphological analysis on text.

Creating a Tokenizer

Tokenizer(dictionary, mode=None, user_dictionary=None, space_penalty=None)

Creates a tokenizer directly from a loaded dictionary.

mode takes a Mode, as TokenizerBuilder.set_mode does; None (the default) is normal mode. Any other value, such as the string "normal", raises TypeError.

space_penalty takes the same values as TokenizerBuilder.set_space_penalty. None (the default) uses the left-space penalty rules the dictionary ships, so with ko-dic the penalty is on; pass False to turn it off. An invalid setting raises the same errors as set_space_penalty and build().

from lindera import Mode, Tokenizer, load_dictionary

dictionary = load_dictionary("embedded://ipadic")
tokenizer = Tokenizer(dictionary, mode=Mode.normal())

# ko-dic without the left-space penalty
ko_tokenizer = Tokenizer(load_dictionary("embedded://ko-dic"), space_penalty=False)

Tokenizer Methods

tokenize(text)

Tokenizes the input text and returns a list of Token objects.

tokens = tokenizer.tokenize("形態素解析")

Parameters:

NameTypeDescription
textstrText to tokenize

Returns: list[Token]

tokenize_surfaces(text)

Tokenizes the input text and returns only the token surfaces, as a list of strings. This is the fast path for wakati-style use: no Token objects are created and no morphological details are loaded, so it is significantly faster than tokenize when only the surface strings are needed. The result equals [t.surface for t in tokenizer.tokenize(text)].

surfaces = tokenizer.tokenize_surfaces("形態素解析")
# ["形態素", "解析"]

Parameters:

NameTypeDescription
textstrText to tokenize

Returns: list[str]

tokenize_nbest(text, n, unique=False, cost_threshold=None)

Returns the N-best tokenization results, each paired with its total path cost.

results = tokenizer.tokenize_nbest("すもももももももものうち", n=3)
for tokens, cost in results:
    print(cost, [t.surface for t in tokens])

Parameters:

NameTypeDescription
textstrText to tokenize
nintNumber of results to return
uniqueboolDeduplicate results (default: False)
cost_thresholdint or NoneMaximum cost difference from the best path (default: None)

Returns: list[tuple[list[Token], int]]

Mode

Mode is the tokenization mode that TokenizerBuilder.set_mode() and the Tokenizer constructor take. It is the counterpart of the Rust lindera::mode::Mode: normal mode, or decompose mode with its Penalty.

Creating a Mode

Mode has no constructor, so Mode() raises TypeError, and it has no Mode.Normal or Mode.Decompose attributes. Create a mode with one of the following static methods.

Mode.normal()

Returns normal mode: standard tokenization.

Mode.decompose(penalty=None)

Returns decompose mode, which adds the length penalties of penalty to the cost of long words, so that compound words are decomposed into smaller units. None (the default) uses the default penalty, Penalty(). A value that is not a Penalty raises TypeError.

Mode.from_str(name)

Returns the mode named name, as the Rust Mode parses a name: "normal" for normal mode, and "decompose" for decompose mode with the default penalty. Use it for a mode name read at run time, such as a configuration value. The name is case sensitive: any other name, such as "Normal", raises ValueError with the message Invalid mode. A value that is not a str raises TypeError.

from lindera import Mode, Penalty

normal = Mode.normal()
decompose = Mode.decompose()
whole_words = Mode.decompose(Penalty(kanji_penalty_length_penalty=0, other_penalty_length_penalty=0))
from_name = Mode.from_str("decompose")  # equal to Mode.decompose()

Mode Properties

Both properties are read-only:

PropertyTypeDescription
namestr"normal" or "decompose"
penaltyPenalty or NoneA copy of the penalty of decompose mode; None for normal mode

Methods

MethodReturnsDescription
__str__()strThe name: "normal" or "decompose"
__repr__()str"Mode.normal()", or Mode.decompose(...) with the penalty
__eq__(other)boolTrue if other is a Mode with the same name and penalty
is_normal()boolTrue for normal mode
is_decompose()boolTrue for decompose mode
mode = Mode.decompose()
mode.name              # 'decompose'
mode.penalty           # Penalty(kanji_threshold=2, kanji_penalty=3000, other_threshold=7, other_penalty=1700)
str(mode)              # 'decompose'
repr(mode)             # 'Mode.decompose(Penalty(kanji_threshold=2, kanji_penalty=3000, other_threshold=7, other_penalty=1700))'
mode.is_normal()       # False
mode.is_decompose()    # True
Mode.normal().penalty  # None
repr(Mode.normal())    # 'Mode.normal()'

Two modes are equal when their names and penalties are, as the Rust Mode compares them. A Mode is never equal to a value of another type, such as the string "normal":

Mode.from_str("decompose") == Mode.decompose()                               # True
Mode.decompose() == Mode.decompose(Penalty(kanji_penalty_length_penalty=0))  # False
Mode.normal() == "normal"                                                    # False

A Mode cannot be changed. Setting a property raises AttributeError, and penalty returns a copy, so changing the returned Penalty leaves the mode as it was; Mode.decompose() keeps a copy of the Penalty it is given as well. A Mode is not hashable, as the Rust Mode implements Eq but not Hash: hash(mode) raises TypeError, so a Mode cannot be a dict key or a set member.

mode = Mode.decompose()
penalty = mode.penalty
penalty.kanji_penalty_length_penalty = 0
mode.penalty.kanji_penalty_length_penalty  # 3000

Penalty

Penalty configures the length-based penalty thresholds used by decompose mode segmentation.

Creating a Penalty

Penalty(kanji_penalty_length_threshold=2, kanji_penalty_length_penalty=3000, other_penalty_length_threshold=7, other_penalty_length_penalty=1700)

All arguments are optional and default to the values shown above.

from lindera import Penalty

penalty = Penalty(
    kanji_penalty_length_threshold=2,
    kanji_penalty_length_penalty=3000,
    other_penalty_length_threshold=7,
    other_penalty_length_penalty=1700,
)

Penalty Properties

All four fields support both getting and setting:

PropertyTypeDefaultDescription
kanji_penalty_length_thresholdint2Kanji-only surface length above which the penalty applies
kanji_penalty_length_penaltyint3000Cost penalty added for kanji-only surfaces longer than the threshold
other_penalty_length_thresholdint7Surface length above which the penalty applies for non-kanji-only surfaces
other_penalty_length_penaltyint1700Cost penalty added for non-kanji-only surfaces longer than the threshold
penalty.kanji_penalty_length_threshold = 3
print(penalty.kanji_penalty_length_threshold)  # 3

Using a Penalty

Pass a Penalty to Mode.decompose(), and the mode to TokenizerBuilder.set_mode() or to the Tokenizer constructor. Without length penalties, decompose mode keeps long words whole:

from lindera import Mode, Penalty, Tokenizer, TokenizerBuilder, load_dictionary

text = "関西国際空港限定トートバッグ"
penalty = Penalty(kanji_penalty_length_penalty=0, other_penalty_length_penalty=0)

builder = TokenizerBuilder().set_dictionary("embedded://ipadic")
tokenizer = builder.set_mode(Mode.decompose()).build()
print([token.surface for token in tokenizer.tokenize(text)])
# ['関西', '国際', '空港', '限定', 'トートバッグ']

tokenizer = builder.set_mode(Mode.decompose(penalty)).build()
print([token.surface for token in tokenizer.tokenize(text)])
# ['関西国際空港', '限定', 'トートバッグ']

tokenizer = Tokenizer(load_dictionary("embedded://ipadic"), mode=Mode.decompose(penalty))
print([token.surface for token in tokenizer.tokenize(text)])
# ['関西国際空港', '限定', 'トートバッグ']

Normal mode has no penalty: Mode.normal() takes no arguments.

Token

Token represents a single morphological token.

Properties

PropertyTypeDescription
surfacestrSurface form of the token
byte_startintStart byte position in the original text
byte_endintEnd byte position in the original text
positionintToken position index
word_idintDictionary word ID
is_unknownboolTrue if the word is not in the dictionary
detailslist[str] or NoneMorphological details (part of speech, reading, etc.)

Token Methods

get_detail(index)

Returns the detail string at the specified index, or None if the index is out of range.

token = tokenizer.tokenize("東京")[0]
pos = token.get_detail(0)        # e.g., "名詞"
subpos = token.get_detail(1)     # e.g., "固有名詞"
reading = token.get_detail(7)    # e.g., "トウキョウ"

Parameters:

NameTypeDescription
indexintZero-based index into the details list

Returns: str or None

to_dict()

Returns the token as a plain dict, keeping each field's natural Python type, so it serializes without a custom encoder.

import json

data = tokenizer.tokenize("東京")[0].to_dict()
# {'surface': '東京', 'byte_start': 0, 'byte_end': 6, 'position': 0,
#  'word_id': 12345, 'is_unknown': False, 'details': [...]}

json.dumps([token.to_dict() for token in tokens])

Returns: dict with the keys surface, byte_start, byte_end, position, word_id, is_unknown, and details.

The structure of details depends on the dictionary:

  • IPADIC: [品詞, 品詞細分類1, 品詞細分類2, 品詞細分類3, 活用型, 活用形, 原形, 読み, 発音]
  • UniDic: Detailed morphological features following the UniDic specification
  • ko-dic / CC-CEDICT / Jieba: Dictionary-specific detail formats

Error Handling

Lindera Python functions raise standard Python exceptions rather than a custom exception type:

  • IOError (an alias of OSError) -- for I/O-related failures, such as a missing or unreadable file
  • ValueError -- for everything else, such as invalid configuration, parse errors, or tokenization failures
from lindera import load_dictionary

try:
    dictionary = load_dictionary("/path/that/does/not/exist")
except ValueError as e:
    print(f"Failed to load dictionary: {e}")

A LinderaError class is also registered as lindera.LinderaError, but no function in this crate currently raises it -- it can only be constructed and raised manually. Catch IOError/ValueError (or the general Exception) when handling errors from this library, not LinderaError.

Dictionary Management

Lindera Python provides functions for loading, building, and managing dictionaries used in morphological analysis.

Loading Dictionaries

System Dictionaries

Use load_dictionary(uri) to load a system dictionary. Download a pre-built dictionary from GitHub Releases and specify the path to the extracted directory:

from lindera import load_dictionary

dictionary = load_dictionary("/path/to/ipadic")

Embedded dictionaries (advanced) -- if you built with an embed-* feature flag, you can load an embedded dictionary:

dictionary = load_dictionary("embedded://ipadic")

A loaded Dictionary also exposes its own metadata:

print(dictionary.metadata_name())      # e.g. "ipadic"
print(dictionary.metadata_encoding())  # e.g. "UTF-8"
metadata = dictionary.metadata()       # the full Metadata object

A user dictionary must share the metadata (schema, encoding, etc.) of the system dictionary it augments. load_user_dictionary_for takes the system dictionary and uses its metadata, as in lindera-python/examples/tokenize_with_userdict.py (see User Dictionaries).

User Dictionaries

User dictionaries add custom vocabulary on top of a system dictionary. Load one for the system dictionary it is used with:

from lindera import load_dictionary, load_user_dictionary_for

dictionary = load_dictionary("/path/to/ipadic")
user_dict = load_user_dictionary_for("/path/to/user_dictionary.csv", dictionary)

A .csv is built with the system dictionary's metadata and checked against its connection matrix as it is built: a row whose context ID lies outside the matrix raises a ValueError that names its CSV line and surface (see Context IDs). A .bin is loaded as it was built.

load_user_dictionary(uri, metadata) loads a user dictionary with metadata only. A .csv loaded this way is checked when it is combined with the system dictionary, and the error names the entry by its word id:

from lindera import load_user_dictionary, Metadata

metadata = Metadata()
user_dict = load_user_dictionary("/path/to/user_dictionary.csv", metadata)

Pass the user dictionary when building a tokenizer:

from lindera import Mode, Tokenizer, load_dictionary, load_user_dictionary_for

dictionary = load_dictionary("/path/to/ipadic")
user_dict = load_user_dictionary_for("/path/to/user_dictionary.csv", dictionary)

tokenizer = Tokenizer(dictionary, mode=Mode.normal(), user_dictionary=user_dict)

Or via the builder:

from lindera import TokenizerBuilder

tokenizer = (
    TokenizerBuilder()
    .set_dictionary("/path/to/ipadic")
    .set_user_dictionary("/path/to/user_dictionary.csv")
    .build()
)

Building Dictionaries

System Dictionary

Build a system dictionary from source files:

from lindera import build_dictionary, Metadata

metadata = Metadata(name="custom", encoding="UTF-8")
build_dictionary("/path/to/input_dir", "/path/to/output_dir", metadata)

The input directory should contain the dictionary source files (CSV lexicon, matrix.def, etc.).

Metadata loaded with Metadata.from_json_file keeps the space_penalty rules of the metadata.json, and the built dictionary ships them. For ko-dic this means the left-space penalty stays on by default in the dictionary you build; turn it off per tokenizer with TokenizerBuilder.set_space_penalty(False) (see Tokenizer API). A Metadata created with the constructor carries no rules.

User Dictionary

Build a user dictionary from a CSV file for the system dictionary it is used with:

from lindera import build_user_dictionary_for, load_dictionary, load_user_dictionary_for

dictionary = load_dictionary("/path/to/ipadic")
build_user_dictionary_for("user_words.csv", "/path/to/user_dictionary.bin", dictionary)
user_dict = load_user_dictionary_for("/path/to/user_dictionary.bin", dictionary)

The CSV is built with the system dictionary's metadata and checked against its connection matrix: a row whose context ID lies outside the matrix raises a ValueError that names its CSV line and surface, and no file is written (see Context IDs). The second argument is the path of the file to write. End it with .bin, since the loaders choose how to load a user dictionary by its extension.

build_user_dictionary(kind, input_file, output_dir, metadata) builds a user dictionary with metadata only. Despite its name, output_dir is the path of the file to write too, not a directory. The rows are checked when the user dictionary is combined with the system dictionary, and the error names the entry by its word id:

from lindera import build_user_dictionary, Metadata

metadata = Metadata()
build_user_dictionary("ipadic", "user_words.csv", "/path/to/user_dictionary.bin", metadata)

The metadata parameter is optional. When omitted, default metadata values are used:

build_user_dictionary("ipadic", "user_words.csv", "/path/to/user_dictionary.bin")

Note: the first argument ("ipadic" above) is currently unused and reserved for future use -- it does not select or configure the build in any way. The build behavior is controlled entirely by metadata (in particular metadata.user_dictionary_schema). Any string may be passed today. To build for a system dictionary, use build_user_dictionary_for.

Metadata

The Metadata class configures dictionary parameters.

Creating Metadata

from lindera import Metadata

# Default metadata
metadata = Metadata()

# Custom metadata
metadata = Metadata(
    name="my_dictionary",
    encoding="UTF-8",
    default_word_cost=-10000,
)

Loading from JSON

metadata = Metadata.from_json_file("metadata.json")

Properties

PropertyTypeDefaultDescription
namestr"default"Dictionary name
encodingstr"UTF-8"Character encoding
default_word_costint-10000Default cost for unknown words
default_left_context_idint1288Default left context ID
default_right_context_idint1288Default right context ID
default_field_valuestr"*"Default value for missing fields
flexible_csvboolFalseAllow flexible CSV parsing
skip_invalid_cost_or_idboolFalseSkip entries with invalid cost or ID
dictionary_schemaSchemaIPADIC schemaSchema for the main dictionary
user_dictionary_schemaSchemaMinimal schemaSchema for user dictionaries

All properties support both getting and setting:

metadata = Metadata()
metadata.name = "custom_dict"
metadata.encoding = "EUC-JP"
print(metadata.name)  # "custom_dict"

to_dict()

Returns the metadata as a dict of strings: the properties above, with dictionary_schema_fields and user_dictionary_schema_fields (comma-separated field names) in place of the two schemas.

metadata = Metadata(name="test")
print(metadata.to_dict())

When the metadata carries left-space penalty rules, as ko-dic's does, the dict also has a space_penalty entry holding them as a JSON string; the key is absent otherwise:

metadata = Metadata.from_json_file("/path/to/ko-dic/metadata.json")
metadata.to_dict()["space_penalty"]
# '{"rules":[{"pos":["EC","EF",...],"cost":3000},{"pos":["JC","JKB",...],"cost":6000}]}'

Schema

Schema, FieldDefinition, and FieldType describe the field layout of a dictionary entry. A schema is used by Metadata.dictionary_schema and Metadata.user_dictionary_schema (see the table above).

FieldType

FieldType enumerates the category of a single field:

  • FieldType.Surface -- surface form (word text)
  • FieldType.LeftContextId -- left context ID
  • FieldType.RightContextId -- right context ID
  • FieldType.Cost -- word cost
  • FieldType.Custom -- any other, dictionary-specific field

FieldDefinition

FieldDefinition describes a single field within a schema.

FieldDefinition(index, name, field_type, description=None)

from lindera import FieldDefinition, FieldType

field = FieldDefinition(0, "surface", FieldType.Surface, "Surface form")

Properties (read-only):

PropertyTypeDescription
indexintZero-based position of the field within the schema
namestrField name
field_typeFieldTypeField type
descriptionstr or NoneOptional human-readable description

Creating a Schema

Schema holds an ordered list of field names and provides lookups between field name and index.

Schema(fields)

Creates a schema from a list of field names.

from lindera import Schema

schema = Schema([
    "surface",
    "left_context_id",
    "right_context_id",
    "cost",
    "major_pos",
    "reading",
])

Schema.create_default()

A static method that returns the built-in default schema: 13 fields matching the IPADIC-style layout (surface, left_context_id, right_context_id, cost, major_pos, pos_detail_1, pos_detail_2, pos_detail_3, conjugation_type, conjugation_form, base_form, reading, pronunciation).

schema = Schema.create_default()

Schema Methods and Properties

MemberReturnsDescription
fields (property)list[str]All field names, in order
field_count()intTotal number of fields
get_field_index(name)int or NoneIndex of the field named name
get_field_name(index)str or NoneField name at index
get_custom_fields()list[str]Field names after the four fixed fields (surface, left_context_id, right_context_id, cost)
get_field_by_name(name)FieldDefinition or NoneFull field definition for name
validate_record(record)NoneRaises ValueError if record does not match the schema
__len__()intSame as field_count()
schema = Schema.create_default()

schema.field_count()               # 13
schema.get_field_index("cost")     # 3
schema.get_field_name(0)           # "surface"
schema.get_custom_fields()         # ["major_pos", "pos_detail_1", ..., "pronunciation"]
len(schema)                        # 13

field = schema.get_field_by_name("surface")
print(field.index, field.name, field.field_type)  # 0 surface FieldType.Surface

schema.validate_record([
    "東京", "1288", "1288", "100",
    "名詞", "固有名詞", "地域", "一般", "*", "*",
    "東京", "トウキョウ", "トーキョー",
])

Text Processing Pipeline

Lindera Python supports a composable text processing pipeline that applies character filters before tokenization and token filters after tokenization. Filters are added to the TokenizerBuilder and executed in the order they are appended.

Input Text
  --> Character Filters (preprocessing)
  --> Tokenization
  --> Token Filters (postprocessing)
  --> Output Tokens

Character Filters

Character filters transform the input text before tokenization.

unicode_normalize

Applies Unicode normalization to the input text.

from lindera import TokenizerBuilder

tokenizer = (
    TokenizerBuilder()
    .set_dictionary("embedded://ipadic")
    .append_character_filter("unicode_normalize", {"kind": "nfkc"})
    .build()
)

Supported normalization forms: "nfc", "nfkc", "nfd", "nfkd".

mapping

Replaces characters or strings according to a mapping table.

tokenizer = (
    TokenizerBuilder()
    .set_dictionary("embedded://ipadic")
    .append_character_filter("mapping", {
        "mapping": {
            "\u30fc": "-",
            "\uff5e": "~",
        }
    })
    .build()
)

japanese_iteration_mark

Resolves Japanese iteration marks (odoriji) into their full forms.

tokenizer = (
    TokenizerBuilder()
    .set_dictionary("embedded://ipadic")
    .append_character_filter("japanese_iteration_mark", {
        "normalize_kanji": True,
        "normalize_kana": True,
    })
    .build()
)

Token Filters

Token filters transform or remove tokens after tokenization.

lowercase

Converts token surface forms to lowercase.

tokenizer = (
    TokenizerBuilder()
    .set_dictionary("embedded://ipadic")
    .append_token_filter("lowercase", {})
    .build()
)

japanese_base_form

Replaces inflected forms with their base (dictionary) form using the morphological details from the dictionary.

tokenizer = (
    TokenizerBuilder()
    .set_dictionary("embedded://ipadic")
    .append_token_filter("japanese_base_form", {})
    .build()
)

japanese_stop_tags

Removes tokens whose part-of-speech matches any of the specified tags.

tokenizer = (
    TokenizerBuilder()
    .set_dictionary("embedded://ipadic")
    .append_token_filter("japanese_stop_tags", {
        "tags": ["助詞,格助詞,一般", "助詞,係助詞", "助詞,連体化", "助動詞"],
    })
    .build()
)

Tags are normalized to exactly four comma-separated levels (missing levels are padded with *) and compared for exact equality against the four part-of-speech details of each token, starting at the field named part_of_speech in the dictionary schema (or at the first detail when the schema has no such field). A bare 助詞 therefore never matches IPADIC particle tokens — they always carry a subcategory such as 助詞,係助詞 — while a bare 助動詞 does match, because auxiliary verbs have no subcategory (助動詞,*,*,*).

japanese_keep_tags

Keeps only tokens whose part-of-speech matches one of the specified tags. All other tokens are removed. The following keeps only general nouns:

tokenizer = (
    TokenizerBuilder()
    .set_dictionary("embedded://ipadic")
    .append_token_filter("japanese_keep_tags", {
        "tags": ["名詞,一般"],
    })
    .build()
)

Complete Pipeline Example

The following example combines multiple character filters and token filters into a single pipeline:

from lindera import Mode, TokenizerBuilder

tokenizer = (
    TokenizerBuilder()
    .set_mode(Mode.normal())
    .set_dictionary("embedded://ipadic")
    # Preprocessing
    .append_character_filter("unicode_normalize", {"kind": "nfkc"})
    .append_character_filter("japanese_iteration_mark", {
        "normalize_kanji": True,
        "normalize_kana": True,
    })
    # Postprocessing
    .append_token_filter("japanese_base_form", {})
    .append_token_filter("japanese_stop_tags", {
        "tags": ["助詞,格助詞,一般", "助詞,係助詞", "助詞,連体化", "助動詞", "記号,句点", "記号,読点"],
    })
    .append_token_filter("lowercase", {})
    .build()
)

tokens = tokenizer.tokenize("Linderaは形態素解析を行うライブラリです。")
for token in tokens:
    print(f"{token.surface}\t{','.join(token.details)}")

In this pipeline:

  1. unicode_normalize converts full-width characters to half-width (NFKC normalization)
  2. japanese_iteration_mark resolves iteration marks
  3. japanese_base_form converts inflected tokens to base form
  4. japanese_stop_tags removes particles, auxiliary verbs, and punctuation
  5. lowercase normalizes alphabetic characters to lowercase

Training

Lindera Python supports training custom CRF-based morphological analysis models from annotated corpora. This functionality requires the train feature.

Prerequisites

Build lindera-python with the train feature enabled (enabled by default):

maturin develop --features train

Training a Model

Use lindera.train() to train a CRF model from a seed lexicon and annotated corpus:

import lindera

lindera.train(
    seed="resources/training/seed.csv",
    corpus="resources/training/corpus.txt",
    char_def="resources/training/char.def",
    unk_def="resources/training/unk.def",
    feature_def="resources/training/feature.def",
    rewrite_def="resources/training/rewrite.def",
    output="/tmp/model.dat",
    lambda_=1.0,
    max_iter=100,
    max_threads=4,
)

Training Parameters

ParameterTypeDefaultDescription
seedstrrequiredPath to the seed lexicon file (CSV format)
corpusstrrequiredPath to the annotated training corpus
char_defstrrequiredPath to the character definition file (char.def)
unk_defstrrequiredPath to the unknown word definition file (unk.def)
feature_defstrrequiredPath to the feature definition file (feature.def)
rewrite_defstrrequiredPath to the rewrite rule definition file (rewrite.def)
outputstrrequiredOutput path for the trained model file
lambda_float1.0Regularization coefficient, finite and >= 0; 0 disables regularization for every type; with L2, MeCab's -c C is 1 / C, so 1.0 is MeCab's -c 1.0
max_iterint100Maximum number of training iterations, 1 or more; training stops earlier once the objective converges (see eta)
max_threadsint or NoneNoneNumber of threads, 1 or more (None = auto-detect CPU cores)
regularizationstr"l2"Regularization type: "l1", "l2" or "elasticnet", in any case ("elastic_net" and "elastic-net" also select Elastic Net), as lindera train --regularization takes it
elastic_net_l1_ratiofloat0.5L1 ratio of Elastic Net regularization, between 0 and 1; 0 uses only L2, 1 uses only L1; used only with "elasticnet"
etafloat5e-5Tolerance of the convergence test, finite and >= 0: training stops before max_iter once the relative change of the objective stays below it for 3 iterations in a row; 0 disables the test

The settings are checked before any file is read: an unknown regularization, or a lambda_, elastic_net_l1_ratio or eta outside its range, raises a ValueError. To train with v6's default settings (the model still differs from v6's, since the first line search changed in #1290), pass regularization="l1" and lambda_=0.01, and eta=0 to stop only at max_iter.

Exporting a Trained Model

After training, export the model to dictionary source files using lindera.export():

import lindera

lindera.export(
    model="/tmp/model.dat",
    output="/tmp/dictionary_source",
    metadata="resources/training/metadata.json",
)

Export Parameters

ParameterTypeDefaultDescription
modelstrrequiredPath to the trained model file (.dat)
outputstrrequiredOutput directory for dictionary source files
metadatastr or NoneNonePath to a base metadata.json file

The export creates the following files in the output directory, the same files that lindera export creates:

  • lex.csv -- Lexicon entries with trained costs
  • matrix.def -- Connection cost matrix
  • unk.def -- Unknown word definitions
  • char.def -- Character category definitions (copied from the trained model)
  • feature.def -- Feature template definitions (copied from the trained model)
  • rewrite.def -- Feature rewrite rules (copied from the trained model)
  • left-id.def -- Left context ID to feature string mapping
  • right-id.def -- Right context ID to feature string mapping
  • metadata.json -- Updated metadata (when metadata parameter is provided)

The model file must come from this version's training. A model file written by an earlier version, which includes every model file written before 7.0.0, is rejected: lindera.export() fails with an error that asks you to re-run training. Train such a model again with lindera.train().

Complete Workflow

The full workflow for training and using a custom dictionary:

import lindera

# Step 1: Train the CRF model
lindera.train(
    seed="resources/training/seed.csv",
    corpus="resources/training/corpus.txt",
    char_def="resources/training/char.def",
    unk_def="resources/training/unk.def",
    feature_def="resources/training/feature.def",
    rewrite_def="resources/training/rewrite.def",
    output="/tmp/model.dat",
    lambda_=1.0,
    max_iter=100,
)

# Step 2: Export to dictionary source files
lindera.export(
    model="/tmp/model.dat",
    output="/tmp/dictionary_source",
    metadata="resources/training/metadata.json",
)

# Step 3: Build the dictionary from exported source files
metadata = lindera.Metadata.from_json_file("/tmp/dictionary_source/metadata.json")
lindera.build_dictionary("/tmp/dictionary_source", "/tmp/dictionary", metadata)

# Step 4: Use the trained dictionary
tokenizer = (
    lindera.TokenizerBuilder()
    .set_dictionary("/tmp/dictionary")
    .set_mode(lindera.Mode.normal())
    .build()
)

tokens = tokenizer.tokenize("形態素解析のテスト")
for token in tokens:
    print(f"{token.surface}\t{','.join(token.details)}")

Lindera Node.js

Lindera Node.js provides Node.js bindings for the Lindera morphological analysis engine, built with NAPI-RS. It brings Lindera's high-performance tokenization capabilities to the Node.js ecosystem, on the Node.js versions ^20.17.0 || ^22.13.0 || >= 23.5.0.

Features

  • Multi-language support: Tokenize Japanese (IPADIC, IPADIC NEologd, UniDic), Korean (ko-dic), and Chinese (CC-CEDICT, Jieba) text
  • Text processing pipeline: Compose character filters and token filters for flexible preprocessing and postprocessing
  • CRF-based dictionary training: Train custom morphological analysis models from annotated corpora (requires train feature)
  • Multiple tokenization modes: Normal and decompose modes for different analysis granularity
  • N-best tokenization: Retrieve multiple tokenization candidates ranked by cost
  • User dictionaries: Extend system dictionaries with custom vocabulary
  • TypeScript support: Full type definitions included out of the box

Documentation

Installation

Installing from npm

Pre-built packages will be available on npm:

npm install lindera

[!NOTE] The npm package does not include dictionaries. See Obtaining Dictionaries below. For browser/WASM usage, see lindera-wasm.

Building from Source

Prerequisites

  • Node.js ^20.17.0 || ^22.13.0 || >= 23.5.0 -- The engines range of lindera-nodejs/package.json (LTS versions recommended)
  • Rust toolchain -- Install via rustup

The NAPI-RS CLI (@napi-rs/cli), which builds the native addon, needs no global install: it is a devDependency of lindera-nodejs, which npm install installs (see Development Build).

Obtaining Dictionaries

Lindera does not bundle dictionaries with the package. You need to obtain a pre-built dictionary separately.

Download from GitHub Releases

Pre-built dictionaries are available on the GitHub Releases page. Download and extract the dictionary archive to a local directory:

# Example: download and extract the IPADIC dictionary
curl -LO https://github.com/lindera/lindera/releases/download/<version>/lindera-ipadic-<version>.zip
unzip lindera-ipadic-<version>.zip -d /path/to/ipadic

Development Build

Build lindera-nodejs in release mode. npm install also installs the NAPI-RS CLI (@napi-rs/cli) at the version package.json pins, which npm run build runs:

cd lindera-nodejs
npm install
npm run build

For a debug build, run npm run build:debug instead of npm run build.

Or use the project Makefile, which makes the same release build:

make build-lindera-nodejs

Build with Training Support

The train feature enables CRF-based dictionary training functionality. It is enabled by default:

npm run build -- --features train

Feature Flags

FeatureDescriptionDefault
trainCRF training functionalityEnabled
embed-ipadicEmbed Japanese dictionary (IPADIC) into the binaryDisabled
embed-unidicEmbed Japanese dictionary (UniDic) into the binaryDisabled
embed-sudachidictEmbed Japanese dictionary (SudachiDict) into the binaryDisabled
embed-ipadic-neologdEmbed Japanese dictionary (IPADIC NEologd) into the binaryDisabled
embed-ko-dicEmbed Korean dictionary (ko-dic) into the binaryDisabled
embed-cc-cedictEmbed Chinese dictionary (CC-CEDICT) into the binaryDisabled
embed-jiebaEmbed Chinese dictionary (Jieba) into the binaryDisabled
embed-cjkEmbed all CJK dictionaries (IPADIC, ko-dic, Jieba) into the binaryDisabled

Multiple features can be combined:

npm run build -- --features "train,embed-ipadic,embed-ko-dic"

[!TIP] If you want to embed a dictionary directly into the binary (advanced usage), enable the corresponding embed-* feature flag and load it using the embedded:// scheme:

const dictionary = loadDictionary("embedded://ipadic");

See Feature Flags for details.

Verifying the Installation

After installation, verify that lindera is available in Node.js:

const lindera = require("lindera");

console.log(lindera.version());

[!NOTE] The lindera npm package's exports map declares types, import, and require conditions, so the package loads from CommonJS (const { TokenizerBuilder } = require("lindera")) and ES modules (import { TokenizerBuilder } from "lindera") alike:

import { TokenizerBuilder } from "lindera";

console.log(typeof TokenizerBuilder); // "function"

Quick Start

This guide shows how to tokenize text using lindera-nodejs.

Basic Tokenization

The recommended way to create a tokenizer is through TokenizerBuilder:

const { Mode, TokenizerBuilder } = require("lindera");

const builder = new TokenizerBuilder();
builder.setMode(Mode.normal());
builder.setDictionary("/path/to/ipadic");
const tokenizer = builder.build();

const tokens = tokenizer.tokenize("関西国際空港限定トートバッグ");
for (const token of tokens) {
  console.log(`${token.surface}\t${token.details.join(",")}`);
}

Note: Download a pre-built dictionary from GitHub Releases and specify the path to the extracted directory.

Expected output:

関西国際空港    名詞,固有名詞,組織,*,*,*,関西国際空港,カンサイコクサイクウコウ,カンサイコクサイクーコー
限定    名詞,サ変接続,*,*,*,*,限定,ゲンテイ,ゲンテイ
トートバッグ    名詞,一般,*,*,*,*,*,*,*

Method Chaining

Every setter returns the builder itself (this), so the configuration can be chained:

const { Mode, TokenizerBuilder } = require("lindera");

const tokenizer = new TokenizerBuilder()
  .setMode(Mode.normal())
  .setDictionary("/path/to/ipadic")
  .build();

const tokens = tokenizer.tokenize("すもももももももものうち");
for (const token of tokens) {
  console.log(`${token.surface}\t${token.details[0]}`);
}

Calling the setters one statement at a time works just as well — both styles configure the same builder.

Accessing Token Properties

Each token exposes the following properties:

const { TokenizerBuilder } = require("lindera");

const builder = new TokenizerBuilder();
builder.setDictionary("/path/to/ipadic");
const tokenizer = builder.build();

const tokens = tokenizer.tokenize("東京タワー");
for (const token of tokens) {
  console.log(`Surface: ${token.surface}`);
  console.log(`Byte range: ${token.byteStart}..${token.byteEnd}`);
  console.log(`Position: ${token.position}`);
  console.log(`Word ID: ${token.wordId}`);
  console.log(`Unknown: ${token.isUnknown}`);
  console.log(`Details: ${token.details}`);
  console.log();
}

N-best Tokenization

Retrieve multiple tokenization candidates ranked by cost:

const { TokenizerBuilder } = require("lindera");

const builder = new TokenizerBuilder();
builder.setDictionary("/path/to/ipadic");
const tokenizer = builder.build();

const results = tokenizer.tokenizeNbest("すもももももももものうち", 3);
for (const { tokens, cost } of results) {
  const surfaces = tokens.map((t) => t.surface);
  console.log(`Cost ${cost}: ${surfaces.join(" / ")}`);
}

TypeScript

Lindera Node.js includes TypeScript type definitions, so all classes and functions are fully typed. The package's exports map serves both require and import, and the following sample compiles under moduleResolution: node16 with strict enabled:

import type { Token } from "lindera";
import { Mode, TokenizerBuilder } from "lindera";

const builder = new TokenizerBuilder();
builder.setMode(Mode.normal());
builder.setDictionary("/path/to/ipadic");
const tokenizer = builder.build();

const tokens: Token[] = tokenizer.tokenize("形態素解析");
for (const token of tokens) {
  console.log(`${token.surface}: ${token.details.join(",")}`);
}

Tokenizer API

TokenizerBuilder

TokenizerBuilder configures and constructs a Tokenizer instance using the builder pattern.

Constructors

new TokenizerBuilder()

Creates a new builder with default configuration.

const { TokenizerBuilder } = require("lindera");

const builder = new TokenizerBuilder();

new TokenizerBuilder().fromFile(filePath)

Loads configuration from a YAML file and returns a new builder.

const builder = new TokenizerBuilder().fromFile("lindera.yml");

See lindera-nodejs/resources/lindera.yml for a complete example configuration file, including dictionary, character filter, and token filter settings.

Configuration Methods

All setter methods return the builder itself (this), so they can be chained or called one statement at a time.

setMode(mode)

Sets the tokenization mode, a Mode:

  • Mode.normal() -- Standard tokenization (default)
  • Mode.decompose(penalty?) -- Decomposes compound words into smaller units, with the Penalty the mode holds

A value that is not a Mode, such as the mode name "normal", throws an InvalidArg error. A mode name read at run time, such as a setting, becomes a Mode with Mode.fromStr().

const { Mode } = require("lindera");

builder.setMode(Mode.normal());
builder.setMode(Mode.decompose({ kanjiPenaltyLengthPenalty: 0 }));
builder.setMode(Mode.fromStr("decompose"));

setDictionary(path)

Sets the system dictionary path or URI.

// Use an embedded dictionary
builder.setDictionary("embedded://ipadic");

// Use an external dictionary
builder.setDictionary("/path/to/dictionary");

setUserDictionary(uri)

Sets the user dictionary URI.

builder.setUserDictionary("/path/to/user_dictionary.csv");

setKeepWhitespace(keep)

Controls whether whitespace tokens appear in the output. Kept whitespace also stays in the lattice, so the other tokens can differ from those of the default output, which skips whitespace in the lattice as MeCab does. To keep whitespace in the lattice without outputting it, use setSkipWhitespace(false) instead.

builder.setKeepWhitespace(true);

setSkipWhitespace(skip)

Sets whether whitespace is skipped in the lattice. The value means the same as segmenter.skip_whitespace in a configuration file.

ValueEffect
trueSkips whitespace, so the words on either side of it connect directly, as MeCab does
falseKeeps whitespace in the lattice as a node of its own (a whitespace dictionary entry or the SPACE unknown word), as before v7; whitespace is still dropped from the output
null or undefinedDefault: the setting of the dictionary's metadata.json (skipping, except with SudachiDict)

A value set here overrides the dictionary's default and a skip_whitespace loaded with fromFile. It has no effect while whitespace tokens are kept (setKeepWhitespace(true)), because kept whitespace always stays in the lattice. The argument is required; any other value throws an Error.

With IPADIC, for example, Google が 新しい reads が as a particle (助詞) by default and as a conjunction (接続詞) with setSkipWhitespace(false). See Segmenter for details.

// Keep whitespace in the lattice, as before v7
builder.setSkipWhitespace(false);

// Back to the dictionary's default
builder.setSkipWhitespace(null);

setSpacePenalty(value)

Sets the left-space penalty for Korean (mecab-ko's left-space-penalty-factor): a candidate that starts right after whitespace and whose first part-of-speech tag is listed in a rule gets that rule's cost added. The value means the same as segmenter.space_penalty in a configuration file.

ValueEffect
null or undefinedDefault: the rules the dictionary ships in its metadata.json, if any
falseTurns the penalty off
trueRequires the dictionary's rules; build() throws if the dictionary ships none
{ rules: [{ pos: [...], cost: n }, ...] }Applies these rules instead of the dictionary's

Any other value throws an Error.

// Turn the penalty off
builder.setSpacePenalty(false);

// Explicit rules (these are the ones ko-dic ships)
builder.setSpacePenalty({
  rules: [
    { pos: ["EC", "EF", "EP", "ETM", "ETN", "VCP", "XSA", "XSN", "XSV"], cost: 3000 },
    { pos: ["JC", "JKB", "JKC", "JKG", "JKO", "JKQ", "JKS", "JKV", "JX"], cost: 6000 },
  ],
});

// Back to the dictionary's default
builder.setSpacePenalty(null);

[!NOTE] Since v6.1.0, ko-dic applies the left-space penalty by default, because its metadata.json ships mecab-ko-dic's rules. This changes the Korean output (for example, 이 in 검색 이 잘 된다 is the interjection IC with the penalty and the subject particle JKS without it). Call setSpacePenalty(false) to turn it off. For a Tokenizer created directly, pass false as the fourth argument: new Tokenizer(dictionary, Mode.normal(), null, false). A ko-dic downloaded from the v6.0.0 release ships no rules, so with it the penalty stays off and setSpacePenalty(true) makes build() throw. See Segmenter for details.

appendCharacterFilter(kind, args?)

Appends a character filter to the preprocessing pipeline.

builder.appendCharacterFilter("unicode_normalize", { kind: "nfkc" });

appendTokenFilter(kind, args?)

Appends a token filter to the postprocessing pipeline.

builder.appendTokenFilter("lowercase", {});

The arguments of setSpacePenalty, appendCharacterFilter and appendTokenFilter may nest arrays and objects at most 128 levels deep. A deeper value, or one that contains itself, throws an Error.

Build

build()

Builds and returns a Tokenizer with the configured settings.

const tokenizer = builder.build();

Tokenizer

Tokenizer performs morphological analysis on text.

Creating a Tokenizer

new Tokenizer(dictionary, mode?, userDictionary?, spacePenalty?)

Creates a tokenizer directly from a loaded dictionary.

mode takes a Mode, as setMode does, with the same errors. null or undefined (the default) is normal mode. For decompose mode with a custom penalty, pass Mode.decompose(penalty).

spacePenalty takes the same values as setSpacePenalty. null or undefined (the default) uses the left-space penalty rules the dictionary ships. An invalid setting throws, as setSpacePenalty and build() do.

const { Mode, Tokenizer, loadDictionary } = require("lindera");

const dictionary = loadDictionary("embedded://ipadic");
const tokenizer = new Tokenizer(dictionary, Mode.normal());

// ko-dic without the left-space penalty
const koTokenizer = new Tokenizer(loadDictionary("embedded://ko-dic"), Mode.normal(), null, false);

Tokenizer Methods

tokenize(text)

Tokenizes the input text and returns an array of plain token objects.

const tokens = tokenizer.tokenize("形態素解析");

Parameters:

NameTypeDescription
textstringText to tokenize

Returns: Token[]

Tokens are plain JavaScript objects, not class instances, so they are reclaimed by ordinary garbage collection and pass through JSON.stringify, structuredClone, and worker transfer without conversion. See Token.

tokenizeSurfaces(text)

Tokenizes the input text and returns only the token surfaces, as an array of strings. This is the fast path for wakati-style use: no token objects are built and no morphological details are loaded, so it is significantly faster than tokenize when only the surface strings are needed. The result equals tokenizer.tokenize(text).map((t) => t.surface).

const surfaces = tokenizer.tokenizeSurfaces("形態素解析");
// ["形態素", "解析"]

Parameters:

NameTypeDescription
textstringText to tokenize

Returns: string[]

tokenizeNbest(text, n, unique?, costThreshold?)

Returns the N-best tokenization results, each containing tokens and total path cost.

const results = tokenizer.tokenizeNbest("すもももももももものうち", 3);
for (const { tokens, cost } of results) {
  console.log(cost, tokens.map((t) => t.surface));
}

Parameters:

NameTypeDescription
textstringText to tokenize
nnumberNumber of results to return, an integer of 0 or more
uniquebooleanDeduplicate results (default: false)
costThresholdnumber | undefinedMaximum cost difference from the best path (default: undefined)

Returns: NbestResult[], where each NbestResult is { tokens: Token[], cost: number }

Token

Token is a plain object describing a single morphological token. It is a TypeScript interface, not a class: it has no methods and no prototype, and every field is read directly.

Properties

PropertyTypeDescription
surfacestringSurface form of the token
byteStartnumberStart byte position in the original text
byteEndnumberEnd byte position in the original text
positionnumberToken position index
wordIdnumberDictionary word ID
isUnknownbooleantrue if the word is not in the dictionary
detailsstring[]Morphological details (part of speech, reading, etc.)

Reading Details

Index details directly. Out-of-range indexes yield undefined.

const token = tokenizer.tokenize("東京")[0];
const pos = token.details[0];      // e.g., "名詞"
const subpos = token.details[1];   // e.g., "固有名詞"
const reading = token.details[7];  // e.g., "トウキョウ"

The structure of details depends on the dictionary:

  • IPADIC: [品詞, 品詞細分類1, 品詞細分類2, 品詞細分類3, 活用型, 活用形, 原形, 読み, 発音]
  • UniDic: Detailed morphological features following the UniDic specification
  • ko-dic / CC-CEDICT / Jieba: Dictionary-specific detail formats

Mode and Penalty

TokenizerBuilder.setMode() and the Tokenizer constructor take the mode as a Mode, which holds the penalty of decompose mode as a Penalty.

Mode

The tokenization mode: normal, or decompose with a Penalty. It is the counterpart of the Rust lindera::mode::Mode (see Segmenter), and setMode() and the Tokenizer constructor take it:

  • Normal mode -- standard dictionary-cost-based tokenization
  • Decompose mode -- penalty-based decomposition of compound words

Mode has no constructor, so new Mode() throws; the static factories below create a mode. A mode cannot be changed.

Mode.normal()

Creates normal mode.

const { Mode } = require("lindera");

const mode = Mode.normal();

Mode.decompose(penalty?)

Creates decompose mode with penalty, a Penalty object. null or undefined (the default) uses the default penalty, and a field left out keeps its default. A value that is not an object, or a field that is not an integer in its range, throws an InvalidArg error.

const decompose = Mode.decompose(); // the default penalty
const custom = Mode.decompose({ kanjiPenaltyLengthPenalty: 0 });

Mode.fromStr(name)

Creates a mode from its name, as the Rust Mode parses one: "normal" is normal mode, and "decompose" is decompose mode with the default penalty. The name is case sensitive; any other name, including "Normal" and "Decompose", throws an Error whose message contains Invalid mode. Use it for a mode name read at run time, such as a setting:

const mode = Mode.fromStr("decompose");
console.log(mode.equals(Mode.decompose()));
// true

Properties and Methods

MemberTypeDescription
namestringThe name of the mode, "normal" or "decompose" (a read-only property)
penaltyPenalty | nullThe penalty of decompose mode, as a copy with every field set, or null for normal mode (a read-only property). Changing the copy does not change the mode
isNormal()booleanWhether this is normal mode
isDecompose()booleanWhether this is decompose mode
equals(other)booleanWhether other has the same name and penalty. A value that is not a Mode throws an InvalidArg error
toString()stringThe name of the mode, as name

=== compares objects, not modes: each call of Mode.normal() returns a new object, so compare modes with equals.

const { Mode } = require("lindera");

const mode = Mode.decompose({ kanjiPenaltyLengthPenalty: 0 });
console.log(mode.name, mode.isNormal(), mode.isDecompose());
// decompose false true

// A copy with every field set: the field left out has its default
const penalty = mode.penalty;
console.log(penalty.kanjiPenaltyLengthPenalty, penalty.otherPenaltyLengthPenalty);
// 0 1700

// Changing the copy does not change the mode
penalty.otherPenaltyLengthPenalty = 0;
console.log(mode.penalty.otherPenaltyLengthPenalty);
// 1700

console.log(Mode.normal().penalty);
// null

console.log(Mode.decompose().equals(Mode.fromStr("decompose")));
// true
console.log(mode.equals(Mode.decompose()));
// false
console.log(`${Mode.normal()}`);
// normal

Penalty

A plain object (the TypeScript type Penalty) with the penalty parameters of decompose mode. Every field is optional, and a field left out keeps its default:

PropertyTypeDescription
kanjiPenaltyLengthThresholdnumberLength threshold for kanji sequences before a penalty is applied, an integer 0 or more (default: 2)
kanjiPenaltyLengthPenaltynumberPenalty value for long kanji sequences, a 32-bit signed integer (default: 3000)
otherPenaltyLengthThresholdnumberLength threshold for other character sequences before a penalty is applied, an integer 0 or more (default: 7)
otherPenaltyLengthPenaltynumberPenalty value for long other-character sequences, a 32-bit signed integer (default: 1700)

Pass it to Mode.decompose(), and the mode to setMode() or to the Tokenizer constructor. Without length penalties, decompose mode keeps long words whole, as normal mode does:

const { Mode, Tokenizer, TokenizerBuilder, loadDictionary } = require("lindera");

const text = "関西国際空港限定トートバッグ";
const penalty = { kanjiPenaltyLengthPenalty: 0, otherPenaltyLengthPenalty: 0 };
const surfaces = (tokenizer) => tokenizer.tokenize(text).map((token) => token.surface);

const builder = new TokenizerBuilder().setDictionary("embedded://ipadic");
console.log(surfaces(builder.setMode(Mode.normal()).build()));
// [ '関西国際空港', '限定', 'トートバッグ' ]

console.log(surfaces(builder.setMode(Mode.decompose()).build()));
// [ '関西', '国際', '空港', '限定', 'トートバッグ' ]

console.log(surfaces(builder.setMode(Mode.decompose(penalty)).build()));
// [ '関西国際空港', '限定', 'トートバッグ' ]

const tokenizer = new Tokenizer(loadDictionary("embedded://ipadic"), Mode.decompose(penalty));
console.log(surfaces(tokenizer));
// [ '関西国際空港', '限定', 'トートバッグ' ]

Dictionary Management

Lindera Node.js provides functions for loading, building, and managing dictionaries used in morphological analysis.

Loading Dictionaries

System Dictionaries

Use loadDictionary(uri) to load a system dictionary. Download a pre-built dictionary from GitHub Releases and specify the path to the extracted directory:

const { loadDictionary } = require("lindera");

const dictionary = loadDictionary("/path/to/ipadic");

Embedded dictionaries (advanced) -- if you built with an embed-* feature flag, you can load an embedded dictionary:

const dictionary = loadDictionary("embedded://ipadic");

Dictionary exposes a few read-only accessors for inspecting the loaded dictionary's metadata:

console.log(dictionary.metadataName());     // e.g. "ipadic"
console.log(dictionary.metadataEncoding()); // e.g. "UTF-8"

const metadata = dictionary.metadata(); // full Metadata object
console.log(metadata.defaultWordCost);

User Dictionaries

User dictionaries add custom vocabulary on top of a system dictionary. Load one for the system dictionary it is used with:

const { loadDictionary, loadUserDictionaryFor } = require("lindera");

const dictionary = loadDictionary("/path/to/ipadic");
const userDict = loadUserDictionaryFor("/path/to/user_dictionary.csv", dictionary);

A .csv is built with the system dictionary's metadata and checked against its connection matrix as it is built: a row whose context ID lies outside the matrix throws an error that names its CSV line and surface (see Context IDs). A .bin is loaded as it was built.

loadUserDictionary(uri, metadata) loads a user dictionary with metadata only. A .csv loaded this way is checked when it is combined with the system dictionary, and the error names the entry by its word id:

const { loadUserDictionary, Metadata } = require("lindera");

const metadata = new Metadata();
const userDict = loadUserDictionary("/path/to/user_dictionary.csv", metadata);

Pass the user dictionary when building a tokenizer:

const { Mode, Tokenizer, loadDictionary, loadUserDictionaryFor } = require("lindera");

const dictionary = loadDictionary("/path/to/ipadic");
const userDict = loadUserDictionaryFor("/path/to/user_dictionary.csv", dictionary);

const tokenizer = new Tokenizer(dictionary, Mode.normal(), userDict);

Or via the builder:

const { TokenizerBuilder } = require("lindera");

const builder = new TokenizerBuilder();
builder.setDictionary("/path/to/ipadic");
builder.setUserDictionary("/path/to/user_dictionary.csv");
const tokenizer = builder.build();

Building Dictionaries

System Dictionary

Build a system dictionary from source files:

const { buildDictionary, Metadata } = require("lindera");

const metadata = new Metadata({ name: "custom", encoding: "UTF-8" });
buildDictionary("/path/to/input_dir", "/path/to/output_dir", metadata);

The input directory should contain the dictionary source files (CSV lexicon, matrix.def, etc.).

To build a dictionary with the settings its metadata.json ships, load the file with Metadata.fromJsonFile() and pass the result. The file's space_penalty rules are kept, so a ko-dic built this way applies the left-space penalty by default (see setSpacePenalty). A Metadata created with new Metadata(...) carries no rules.

const metadata = Metadata.fromJsonFile("/path/to/lindera-ko-dic/metadata.json");
buildDictionary("/path/to/mecab-ko-dic", "/path/to/output_dir", metadata);

User Dictionary

Build a user dictionary from a CSV file for the system dictionary it is used with:

const { buildUserDictionaryFor, loadDictionary, loadUserDictionaryFor } = require("lindera");

const dictionary = loadDictionary("/path/to/ipadic");
buildUserDictionaryFor("user_words.csv", "/path/to/user_dictionary.bin", dictionary);
const userDict = loadUserDictionaryFor("/path/to/user_dictionary.bin", dictionary);

The CSV is built with the system dictionary's metadata and checked against its connection matrix: a row whose context ID lies outside the matrix throws an error that names its CSV line and surface, and no file is written (see Context IDs). The second argument is the path of the file to write. End it with .bin, since the loaders choose how to load a user dictionary by its extension.

buildUserDictionary(kind, inputFile, outputDir, metadata) builds a user dictionary with metadata only. Despite its name, outputDir is the path of the file to write too, not a directory. The rows are checked when the user dictionary is combined with the system dictionary, and the error names the entry by its word id:

const { buildUserDictionary, Metadata } = require("lindera");

const metadata = new Metadata();
buildUserDictionary("ipadic", "user_words.csv", "/path/to/user_dictionary.bin", metadata);

The metadata parameter is optional. When omitted, default metadata values are used:

buildUserDictionary("ipadic", "user_words.csv", "/path/to/user_dictionary.bin");

[!NOTE] The first argument (kind) is currently unused -- it is reserved for future use and has no effect on the build. Any string value can be passed for it today. To build for a system dictionary, use buildUserDictionaryFor.

Metadata

The Metadata class configures dictionary parameters.

Creating Metadata

const { Metadata } = require("lindera");

// Default metadata
const metadata = new Metadata();

// Custom metadata
const metadata = new Metadata({
  name: "my_dictionary",
  encoding: "UTF-8",
  defaultWordCost: -10000,
});

Loading from JSON

const metadata = Metadata.fromJsonFile("metadata.json");

Properties

PropertyTypeDefaultDescription
namestring"default"Dictionary name
encodingstring"UTF-8"Character encoding
defaultWordCostnumber-10000Default cost for unknown words, an integer from -32768 to 32767
defaultLeftContextIdnumber1288Default left context ID, an integer from 0 to 65535
defaultRightContextIdnumber1288Default right context ID, an integer from 0 to 65535
defaultFieldValuestring"*"Default value for missing fields
flexibleCsvbooleanfalseAllow flexible CSV parsing
skipInvalidCostOrIdbooleanfalseSkip entries with invalid cost or ID

[!NOTE] Schema information (dictionary/user-dictionary field layout) is not exposed on this binding's Metadata object -- there are no dictionarySchema / userDictionarySchema properties. Use the standalone Schema class instead.

All properties support both getting and setting:

const metadata = new Metadata();
metadata.name = "custom_dict";
metadata.encoding = "EUC-JP";
console.log(metadata.name); // "custom_dict"

toObject()

Returns a plain object representation of the metadata, with every value as a string:

const metadata = new Metadata({ name: "test" });
console.log(metadata.toObject());

When the metadata carries left-space penalty rules (for example, when it was loaded from ko-dic's metadata.json), the object also has a spacePenalty entry holding the rules as a JSON string. Otherwise the key is absent.

const koDic = Metadata.fromJsonFile("/path/to/lindera-ko-dic/metadata.json");
const { rules } = JSON.parse(koDic.toObject().spacePenalty);
console.log(rules[1].pos); // ["JC", "JKB", "JKC", "JKG", "JKO", "JKQ", "JKS", "JKV", "JX"]

Schema

The Schema class defines the field structure of dictionary entries.

Creating a Schema

const { Schema } = require("lindera");

// Default IPADIC-compatible schema
const schema = Schema.createDefault();

// Custom schema
const custom = new Schema(["surface", "left_id", "right_id", "cost", "pos", "reading"]);

Schema Methods

MethodReturnsDescription
getFieldIndex(name)number | nullGet field index by name
fieldCount()numberTotal number of fields
getFieldName(index)string | nullGet field name by index
getCustomFields()string[]Fields from index 4 onward (morphological features)
getAllFields()string[]All field names
getFieldByName(name)FieldDefinition | nullGet full field definition
validateRecord(record)voidValidate a CSV record against the schema
const schema = Schema.createDefault();

console.log(schema.fieldCount());           // 13 (IPADIC format)
console.log(schema.getFieldIndex("pos1"));  // e.g., 4
console.log(schema.getAllFields());          // ["surface", "left_id", ...]
console.log(schema.getCustomFields());      // Fields from index 4 onward

FieldDefinition

PropertyTypeDescription
indexnumberField position index
namestringField name
fieldTypeFieldTypeField type enum
descriptionstring | undefinedOptional description

FieldType

ValueDescription
FieldType.SurfaceWord surface text
FieldType.LeftContextIdLeft context ID
FieldType.RightContextIdRight context ID
FieldType.CostWord cost
FieldType.CustomMorphological feature field

Text Processing Pipeline

Lindera Node.js supports a composable text processing pipeline that applies character filters before tokenization and token filters after tokenization. Filters are added to the TokenizerBuilder and executed in the order they are appended.

Input Text
  --> Character Filters (preprocessing)
  --> Tokenization
  --> Token Filters (postprocessing)
  --> Output Tokens

[!NOTE] This page shows a few commonly used filters as examples -- it is not the complete list. See Filters for the full, authoritative catalogue of every character and token filter that lindera-analysis ships, including parameters and examples.

Character Filters

Character filters transform the input text before tokenization.

unicode_normalize

Applies Unicode normalization to the input text.

const { TokenizerBuilder } = require("lindera");

const builder = new TokenizerBuilder();
builder.setDictionary("embedded://ipadic");
builder.appendCharacterFilter("unicode_normalize", { kind: "nfkc" });
const tokenizer = builder.build();

Supported normalization forms: "nfc", "nfkc", "nfd", "nfkd".

mapping

Replaces characters or strings according to a mapping table.

const builder = new TokenizerBuilder();
builder.setDictionary("embedded://ipadic");
builder.appendCharacterFilter("mapping", {
  mapping: {
    "\u30fc": "-",
    "\uff5e": "~",
  },
});
const tokenizer = builder.build();

japanese_iteration_mark

Resolves Japanese iteration marks (odoriji) into their full forms.

const builder = new TokenizerBuilder();
builder.setDictionary("embedded://ipadic");
builder.appendCharacterFilter("japanese_iteration_mark", {
  normalize_kanji: true,
  normalize_kana: true,
});
const tokenizer = builder.build();

Token Filters

Token filters transform or remove tokens after tokenization.

lowercase

Converts token surface forms to lowercase.

const builder = new TokenizerBuilder();
builder.setDictionary("embedded://ipadic");
builder.appendTokenFilter("lowercase", {});
const tokenizer = builder.build();

japanese_base_form

Replaces inflected forms with their base (dictionary) form using the morphological details from the dictionary.

const builder = new TokenizerBuilder();
builder.setDictionary("embedded://ipadic");
builder.appendTokenFilter("japanese_base_form", {});
const tokenizer = builder.build();

japanese_stop_tags

Removes tokens whose part-of-speech matches any of the specified tags.

const builder = new TokenizerBuilder();
builder.setDictionary("embedded://ipadic");
builder.appendTokenFilter("japanese_stop_tags", {
  tags: ["助詞,格助詞,一般", "助詞,係助詞", "助詞,連体化", "助動詞"],
});
const tokenizer = builder.build();

[!NOTE] Tags are normalized to exactly four comma-separated levels (missing levels are padded with *) and compared for exact equality against the four part-of-speech details of each token, starting at the field named part_of_speech in the dictionary schema (or at the first detail when the schema has no such field). A bare 助詞 therefore never matches IPADIC particle tokens — they always carry a subcategory such as 助詞,係助詞 — while a bare 助動詞 does match, because auxiliary verbs have no subcategory (助動詞,*,*,*).

japanese_keep_tags

Keeps only tokens whose part-of-speech matches one of the specified tags. All other tokens are removed. The following keeps only general nouns:

const builder = new TokenizerBuilder();
builder.setDictionary("embedded://ipadic");
builder.appendTokenFilter("japanese_keep_tags", {
  tags: ["名詞,一般"],
});
const tokenizer = builder.build();

Complete Pipeline Example

The following example combines multiple character filters and token filters into a single pipeline:

const { Mode, TokenizerBuilder } = require("lindera");

const builder = new TokenizerBuilder();
builder.setMode(Mode.normal());
builder.setDictionary("embedded://ipadic");
// Preprocessing
builder.appendCharacterFilter("unicode_normalize", { kind: "nfkc" });
builder.appendCharacterFilter("japanese_iteration_mark", {
  normalize_kanji: true,
  normalize_kana: true,
});
// Postprocessing
builder.appendTokenFilter("japanese_base_form", {});
builder.appendTokenFilter("japanese_stop_tags", {
  tags: ["助詞,格助詞,一般", "助詞,係助詞", "助詞,連体化", "助動詞", "記号,句点", "記号,読点"],
});
builder.appendTokenFilter("lowercase", {});
const tokenizer = builder.build();

const tokens = tokenizer.tokenize("Linderaは形態素解析を行うライブラリです。");
for (const token of tokens) {
  console.log(`${token.surface}\t${token.details.join(",")}`);
}

In this pipeline:

  1. unicode_normalize converts full-width characters to half-width (NFKC normalization)
  2. japanese_iteration_mark resolves iteration marks
  3. japanese_base_form converts inflected tokens to base form
  4. japanese_stop_tags removes particles, auxiliary verbs, and punctuation
  5. lowercase normalizes alphabetic characters to lowercase

Training

Lindera Node.js supports training custom CRF-based morphological analysis models from annotated corpora. This functionality requires the train feature.

Prerequisites

Build lindera-nodejs with the train feature enabled (enabled by default):

npm run build -- --features train

Training a Model

[!NOTE] The file paths below (resources/training/*.csv, *.def) are illustrative placeholders — this repository does not ship those exact files. For a complete, runnable example that generates a seed lexicon, corpus, and definition files on the fly and then trains/exports/builds a real dictionary, see lindera-nodejs/examples/train_and_export.js.

Use train() to train a CRF model from a seed lexicon and annotated corpus:

const { train } = require("lindera");

train({
  seed: "resources/training/seed.csv",
  corpus: "resources/training/corpus.txt",
  charDef: "resources/training/char.def",
  unkDef: "resources/training/unk.def",
  featureDef: "resources/training/feature.def",
  rewriteDef: "resources/training/rewrite.def",
  output: "/tmp/model.dat",
  lambda: 1.0,
  maxIter: 100,
  maxThreads: 4,
});

Training Parameters

ParameterTypeDefaultDescription
seedstringrequiredPath to the seed lexicon file (CSV format)
corpusstringrequiredPath to the annotated training corpus
charDefstringrequiredPath to the character definition file (char.def)
unkDefstringrequiredPath to the unknown word definition file (unk.def)
featureDefstringrequiredPath to the feature definition file (feature.def)
rewriteDefstringrequiredPath to the rewrite rule definition file (rewrite.def)
outputstringrequiredOutput path for the trained model file
lambdanumber1.0Regularization coefficient, finite and >= 0; 0 disables regularization for every type; with L2, MeCab's -c C is 1 / C, so 1.0 is MeCab's -c 1.0
maxIternumber100Maximum number of training iterations, 1 or more; training stops earlier once the objective converges (see eta)
maxThreadsnumber | undefinedundefinedNumber of threads, 1 or more (undefined = auto-detect CPU cores)
regularizationstring"l2"Regularization type: "l1", "l2" or "elasticnet", in any case ("elastic_net" and "elastic-net" also select Elastic Net), as lindera train --regularization takes it
elasticNetL1Rationumber0.5L1 ratio of Elastic Net regularization, between 0 and 1; 0 uses only L2, 1 uses only L1; used only with "elasticnet"
etanumber5e-5Tolerance of the convergence test, finite and >= 0: training stops before maxIter once the relative change of the objective stays below it for 3 iterations in a row; 0 disables the test

The settings are checked before any file is read: an unknown regularization, or a lambda, elasticNetL1Ratio or eta outside its range, throws an InvalidArg error. To train with v6's default settings (the model still differs from v6's, since the first line search changed in #1290), pass regularization: "l1" and lambda: 0.01, and eta: 0 to stop only at maxIter.

Exporting a Trained Model

After training, export the model to dictionary source files using exportModel():

const { exportModel } = require("lindera");

exportModel({
  model: "/tmp/model.dat",
  output: "/tmp/dictionary_source",
  metadata: "resources/training/metadata.json",
});

Export Parameters

ParameterTypeDefaultDescription
modelstringrequiredPath to the trained model file (.dat)
outputstringrequiredOutput directory for dictionary source files
metadatastring | undefinedundefinedPath to a base metadata.json file

The export creates the following files in the output directory, the same files that lindera export creates:

  • lex.csv -- Lexicon entries with trained costs
  • matrix.def -- Connection cost matrix
  • unk.def -- Unknown word definitions
  • char.def -- Character category definitions (copied from the trained model)
  • feature.def -- Feature template definitions (copied from the trained model)
  • rewrite.def -- Feature rewrite rules (copied from the trained model)
  • left-id.def -- Left context ID to feature string mapping
  • right-id.def -- Right context ID to feature string mapping
  • metadata.json -- Updated metadata (when metadata parameter is provided)

The model file must come from this version's training. A model file written by an earlier version, which includes every model file written before 7.0.0, is rejected: exportModel() fails with an error that asks you to re-run training. Train such a model again with train().

Complete Workflow

The full workflow for training and using a custom dictionary:

const {
  train,
  exportModel,
  buildDictionary,
  Metadata,
  Mode,
  TokenizerBuilder,
} = require("lindera");

// Step 1: Train the CRF model
train({
  seed: "resources/training/seed.csv",
  corpus: "resources/training/corpus.txt",
  charDef: "resources/training/char.def",
  unkDef: "resources/training/unk.def",
  featureDef: "resources/training/feature.def",
  rewriteDef: "resources/training/rewrite.def",
  output: "/tmp/model.dat",
  lambda: 1.0,
  maxIter: 100,
});

// Step 2: Export to dictionary source files
exportModel({
  model: "/tmp/model.dat",
  output: "/tmp/dictionary_source",
  metadata: "resources/training/metadata.json",
});

// Step 3: Build the dictionary from exported source files
const metadata = Metadata.fromJsonFile("/tmp/dictionary_source/metadata.json");
buildDictionary("/tmp/dictionary_source", "/tmp/dictionary", metadata);

// Step 4: Use the trained dictionary
const builder = new TokenizerBuilder();
builder.setDictionary("/tmp/dictionary");
builder.setMode(Mode.normal());
const tokenizer = builder.build();

const tokens = tokenizer.tokenize("形態素解析のテスト");
for (const token of tokens) {
  console.log(`${token.surface}\t${token.details.join(",")}`);
}

Lindera Ruby

Lindera Ruby provides Ruby bindings for the Lindera morphological analysis engine, built with Magnus and rb-sys. It brings Lindera's high-performance tokenization capabilities to the Ruby ecosystem with support for Ruby 3.1 and later.

Features

  • Multi-language support: Tokenize Japanese (IPADIC, IPADIC NEologd, UniDic), Korean (ko-dic), and Chinese (CC-CEDICT, Jieba) text
  • Text processing pipeline: Compose character filters and token filters for flexible preprocessing and postprocessing
  • CRF-based dictionary training: Train custom morphological analysis models from annotated corpora (requires train feature)
  • Multiple tokenization modes: Normal and decompose modes for different analysis granularity
  • N-best tokenization: Retrieve multiple tokenization candidates ranked by cost
  • User dictionaries: Extend system dictionaries with custom vocabulary

Integer arguments

An integer parameter takes only the values its setting can hold. default_word_cost of Lindera::Metadata.new takes -32768 to 32767, the context IDs 0 to 65535, and the penalties of Lindera::Penalty.new a 32-bit integer. A count or an index takes 0 or more: the thresholds of Lindera::Penalty.new, index of Schema#get_field_name and Token#get_detail, and n of Tokenizer#tokenize_nbest. max_iter and max_threads of Lindera.train take 1 or more. cost_threshold of Tokenizer#tokenize_nbest takes a 64-bit integer. A value outside the range raises a RangeError that names the parameter and the range; an index past the end still returns nil:

require 'lindera'

begin
  Lindera::Metadata.new(nil, nil, 100_000, nil, nil, nil, nil, nil)
rescue RangeError => e
  puts "#{e.class}: #{e.message}"
  # RangeError: default_word_cost must be an integer between -32768 and 32767, got 100000
end

tokenizer = Lindera::Tokenizer.new(Lindera.load_dictionary('embedded://ipadic'))

begin
  tokenizer.tokenize_nbest('東京都', -1, nil, nil)
rescue RangeError => e
  puts "#{e.class}: #{e.message}"
  # RangeError: n must be an integer between 0 and 18446744073709551615, got -1
end

The float settings of Lindera.train are checked the same way, before any file is read: lambda and eta: take a finite number of 0 or more, and elastic_net_l1_ratio: a number between 0 and 1 (see Training).

Documentation

Installation

From v6.1.0, lindera-ruby installs from RubyGems as lindera. It is a source gem: gem install compiles the native extension from the bundled Rust source, which takes a few minutes.

Prerequisites

  • Ruby 3.1 or later, with the tools needed to build native gems (a C compiler and make)
  • Rust toolchain 1.89 or later -- Install via rustup
  • libclang -- libclang-dev on Debian/Ubuntu; the Xcode command line tools on macOS
  • Bundler -- only to build from source (gem install bundler)

Install from RubyGems

gem install lindera

Or add it to your Gemfile:

gem "lindera"

[!NOTE] Use lindera v6.1.0 or later. The lindera gems published before v6.1.0 fail to compile during installation; for those versions, build from source as described below.

The gem compiles against the crates.io releases of lindera and lindera-binding of the same version, which it pins exactly. It ships no Cargo.lock, so their dependencies, including the other lindera-* crates, resolve to the latest compatible releases at install time. It builds with the default features (train) and no embedded dictionary; see Obtaining Dictionaries. To enable other feature flags, set LINDERA_FEATURES when installing:

LINDERA_FEATURES="embed-ipadic" gem install lindera

Obtaining Dictionaries

Lindera does not bundle dictionaries with the package. You need to obtain a pre-built dictionary separately.

Download from GitHub Releases

Pre-built dictionaries are available on the GitHub Releases page. Download and extract the dictionary archive to a local directory:

# Example: download and extract the IPADIC dictionary
curl -LO https://github.com/lindera/lindera/releases/download/<version>/lindera-ipadic-<version>.zip
unzip lindera-ipadic-<version>.zip -d /path/to/ipadic

Development Build

Build the native extension of lindera-ruby in release mode. Rake puts it in lindera-ruby/lib/lindera/ and does not install the gem; Ruby run with bundle exec from lindera-ruby/ loads it from there:

cd lindera-ruby
bundle install
bundle exec rake compile

For a debug build, run bundle exec rake compile:dev instead of bundle exec rake compile.

Or use the project Makefile, which makes the same release build with the embed-ipadic and train features:

make build-lindera-ruby

Run make test-lindera-ruby to run both the Rust unit tests and the Ruby minitest suite.

Building the Gem

bundle exec rake build (or make package-lindera-ruby) writes the source gem to lindera-ruby/pkg/. It packages the crate as cargo package normalizes it, so the gem compiles outside this repository, against the crates.io releases of lindera and lindera-binding of the same version. Do not run gem build directly: the crate's own Cargo.toml only resolves inside the Cargo workspace.

make test-lindera-ruby-gem (requires Docker) builds the gem, installs it with gem install in a clean ruby container and tokenizes text with it. The test compiles the lindera crates from the checkout, so it also works for versions not yet released on crates.io.

Build with Training Support

The train feature enables CRF-based dictionary training functionality. It is enabled by default:

LINDERA_FEATURES="train" bundle exec rake compile

Feature Flags

Features are specified through the LINDERA_FEATURES environment variable as a comma-separated list, both for bundle exec rake compile and for gem install.

FeatureDescriptionDefault
trainCRF training functionalityEnabled
embed-ipadicEmbed Japanese dictionary (IPADIC) into the binaryDisabled
embed-unidicEmbed Japanese dictionary (UniDic) into the binaryDisabled
embed-sudachidictEmbed Japanese dictionary (SudachiDict) into the binaryDisabled
embed-ipadic-neologdEmbed Japanese dictionary (IPADIC NEologd) into the binaryDisabled
embed-ko-dicEmbed Korean dictionary (ko-dic) into the binaryDisabled
embed-cc-cedictEmbed Chinese dictionary (CC-CEDICT) into the binaryDisabled
embed-jiebaEmbed Chinese dictionary (Jieba) into the binaryDisabled
embed-cjkEmbed all CJK dictionaries (IPADIC, ko-dic, Jieba) into the binaryDisabled

Multiple features can be combined:

LINDERA_FEATURES="train,embed-ipadic,embed-ko-dic" bundle exec rake compile

[!TIP] If you want to embed a dictionary directly into the binary (advanced usage), enable the corresponding embed-* feature flag and load it using the embedded:// scheme:

dictionary = Lindera.load_dictionary("embedded://ipadic")

See Feature Flags for details.

Verifying the Installation

After installation, verify that lindera is available in Ruby:

require 'lindera'

puts Lindera.version

Quick Start

This guide shows how to tokenize text using lindera-ruby.

Basic Tokenization

The recommended way to create a tokenizer is through Lindera::TokenizerBuilder:

require 'lindera'

builder = Lindera::TokenizerBuilder.new
builder.set_mode(Lindera::Mode.normal)
builder.set_dictionary('/path/to/ipadic')
tokenizer = builder.build

tokens = tokenizer.tokenize('関西国際空港限定トートバッグ')
tokens.each do |token|
  puts "#{token.surface}\t#{token.details.join(',')}"
end

Note: Download a pre-built dictionary from GitHub Releases and specify the path to the extracted directory.

Expected output:

関西国際空港    名詞,固有名詞,組織,*,*,*,関西国際空港,カンサイコクサイクウコウ,カンサイコクサイクーコー
限定    名詞,サ変接続,*,*,*,*,限定,ゲンテイ,ゲンテイ
トートバッグ    名詞,一般,*,*,*,*,*,*,*

Sequential Configuration

TokenizerBuilder is configured through sequential method calls:

require 'lindera'

builder = Lindera::TokenizerBuilder.new
builder.set_mode(Lindera::Mode.normal)
builder.set_dictionary('/path/to/ipadic')
tokenizer = builder.build

tokens = tokenizer.tokenize('すもももももももものうち')
tokens.each do |token|
  puts "#{token.surface}\t#{token.get_detail(0)}"
end

Accessing Token Properties

Each token exposes the following properties:

require 'lindera'

builder = Lindera::TokenizerBuilder.new
builder.set_dictionary('/path/to/ipadic')
tokenizer = builder.build

tokens = tokenizer.tokenize('東京タワー')

tokens.each do |token|
  puts "Surface: #{token.surface}"
  puts "Byte range: #{token.byte_start}..#{token.byte_end}"
  puts "Position: #{token.position}"
  puts "Word ID: #{token.word_id}"
  puts "Unknown: #{token.unknown?}"
  puts "Details: #{token.details}"
  puts
end

N-best Tokenization

Retrieve multiple tokenization candidates ranked by cost:

require 'lindera'

builder = Lindera::TokenizerBuilder.new
builder.set_dictionary('/path/to/ipadic')
tokenizer = builder.build

results = tokenizer.tokenize_nbest('すもももももももものうち', 3, false, nil)

results.each do |tokens, cost|
  surfaces = tokens.map(&:surface)
  puts "Cost #{cost}: #{surfaces.join(' / ')}"
end

Tokenizer API

TokenizerBuilder

Lindera::TokenizerBuilder configures and constructs a Tokenizer instance using the builder pattern.

Constructors

Lindera::TokenizerBuilder.new

Creates a new builder with default configuration.

require 'lindera'

builder = Lindera::TokenizerBuilder.new

Lindera::TokenizerBuilder.from_file(file_path)

Loads configuration from a JSON file and returns a new builder. This is a class method, not chained off an existing instance.

builder = Lindera::TokenizerBuilder.from_file('config.json')

Configuration Methods

set_mode(mode)

Sets the tokenization mode. mode is a Lindera::Mode:

  • Lindera::Mode.normal -- Standard tokenization (default)
  • Lindera::Mode.decompose(penalty = nil) -- Decomposes compound words into smaller units, with the length penalties of a Lindera::Penalty (the default penalty for nil)

A value that is not a Lindera::Mode, such as a mode string ('normal'), a Symbol or nil, raises TypeError. To turn a mode name read at run time into a Mode, use Lindera::Mode.from_str.

builder.set_mode(Lindera::Mode.normal)
builder.set_mode(Lindera::Mode.decompose(Lindera::Penalty.new(nil, 0, nil, nil)))

set_dictionary(path)

Sets the system dictionary path or URI.

# Use an embedded dictionary
builder.set_dictionary('embedded://ipadic')

# Use an external dictionary
builder.set_dictionary('/path/to/dictionary')

set_user_dictionary(uri)

Sets the user dictionary URI.

builder.set_user_dictionary('/path/to/user_dictionary.csv')

set_keep_whitespace(keep)

Controls whether whitespace tokens appear in the output. Kept whitespace also stays in the lattice, so the other tokens can differ from those of the default output, which skips whitespace in the lattice as MeCab does. To keep whitespace in the lattice without outputting it, use set_skip_whitespace(false) instead.

builder.set_keep_whitespace(true)

set_skip_whitespace(skip)

Sets whether whitespace is skipped in the lattice. skip has the same meaning as segmenter.skip_whitespace in a configuration file:

  • true -- Skips whitespace, so the words on either side of it connect directly, as MeCab does.
  • false -- Keeps whitespace in the lattice as a node of its own (a whitespace dictionary entry or the SPACE unknown word), as before v7. Whitespace is still dropped from the output.
  • nil -- The default of the dictionary's metadata.json: skipping, except with SudachiDict. Use it to undo an earlier call.

A value set here overrides the dictionary's default and a skip_whitespace loaded with TokenizerBuilder.from_file. It has no effect while whitespace tokens are kept (set_keep_whitespace(true)), because kept whitespace always stays in the lattice. Other values count by Ruby truthiness, as in set_keep_whitespace: anything but false and nil turns skipping on.

With IPADIC, for example, Google が 新しい reads が as a particle (助詞) by default and as a conjunction (接続詞) with set_skip_whitespace(false). See Segmenter for details.

# Keep whitespace in the lattice, as before v7
builder.set_skip_whitespace(false)

set_space_penalty(value)

Sets the left-space penalty for Korean: a candidate that starts right after whitespace and whose part-of-speech tag normally attaches to the preceding word (particles, endings, ...) gets a cost added. value has the same meaning as segmenter.space_penalty in a configuration file:

  • nil -- The default: the rules the dictionary ships in its metadata.json, if any (ko-dic ships mecab-ko-dic's rules; the other bundled dictionaries ship none). Use it to undo an earlier call.
  • false -- Turns the penalty off.
  • true -- Requires the dictionary's rules; build raises RuntimeError for a dictionary that ships none.
  • A hash with string keys, { 'rules' => [{ 'pos' => [...], 'cost' => n }, ...] } -- Applies these rules instead. A candidate whose first part-of-speech tag is listed in pos gets cost added; the first matching rule wins.

A value that is not one of these forms, such as 1, 'false' or { 'rules' => 'JKS' }, raises RuntimeError. A value that cannot be converted raises TypeError: a Symbol (also as a hash key or an element, so write %w[JKS] rather than %i[JKS]), a non-finite Float (NaN or Infinity), an Integer outside the 64-bit range, or an object other than nil, true, false, an Integer, a Float, a String, an Array or a Hash.

# Turn the penalty off
builder.set_space_penalty(false)

# Explicit rules
builder.set_space_penalty({
  'rules' => [
    { 'pos' => %w[EC EF EP ETM ETN VCP XSA XSN XSV], 'cost' => 3000 },
    { 'pos' => %w[JC JKB JKC JKG JKO JKQ JKS JKV JX], 'cost' => 6000 }
  ]
})

[!NOTE] Since v6.1.0, ko-dic applies the left-space penalty by default, as mecab-ko does. For example, 검색 이 잘 된다 reads 이 as the interjection IC; without the penalty, as the subject particle JKS. Call builder.set_space_penalty(false) to turn it off. A tokenizer created directly with Lindera::Tokenizer.new(dictionary, ...) always uses the default, so use TokenizerBuilder to turn it off. Only a ko-dic built by Lindera 6.1.0 or later ships the rules; with a ko-dic from the v6.0.0 release the penalty stays off and true fails until the dictionary is rebuilt. See Segmenter for details.

append_character_filter(kind, args)

Appends a character filter to the preprocessing pipeline. The args parameter is a hash with string keys.

builder.append_character_filter('unicode_normalize', { 'kind' => 'nfkc' })

append_token_filter(kind, args)

Appends a token filter to the postprocessing pipeline. The args parameter is a hash with string keys, or nil if the filter requires no arguments.

builder.append_token_filter('lowercase', nil)

The arguments of set_space_penalty, append_character_filter and append_token_filter may nest arrays and hashes at most 128 levels deep. A deeper value, or one that contains itself, raises ArgumentError.

Build

build

Builds and returns a Tokenizer with the configured settings.

tokenizer = builder.build

Tokenizer

Lindera::Tokenizer performs morphological analysis on text.

Creating a Tokenizer

Lindera::Tokenizer.new(dictionary, mode = nil, user_dictionary = nil, space_penalty: nil)

Creates a tokenizer directly from a loaded dictionary. mode is a Lindera::Mode, as TokenizerBuilder#set_mode takes it; nil (the default) means normal mode, and a value that is not a Lindera::Mode, such as a mode string ('normal'), raises TypeError. user_dictionary is optional.

The space_penalty: keyword takes the same values as TokenizerBuilder#set_space_penalty. nil (the default) uses the left-space penalty rules the dictionary ships, so with ko-dic the penalty is on; pass false to turn it off. An invalid setting raises the same errors as set_space_penalty and build.

require 'lindera'

dictionary = Lindera.load_dictionary('embedded://ipadic')
tokenizer = Lindera::Tokenizer.new(dictionary)

# ko-dic without the left-space penalty
ko_tokenizer = Lindera::Tokenizer.new(Lindera.load_dictionary('embedded://ko-dic'), space_penalty: false)

With a user dictionary:

dictionary = Lindera.load_dictionary('embedded://ipadic')
user_dict = Lindera.load_user_dictionary_for('/path/to/user_dictionary.csv', dictionary)
tokenizer = Lindera::Tokenizer.new(dictionary, Lindera::Mode.normal, user_dict)

Tokenizer Methods

tokenize(text)

Tokenizes the input text and returns an array of Token objects.

tokens = tokenizer.tokenize('形態素解析')

Parameters:

NameTypeDescription
textStringText to tokenize

Returns: Array<Token>

tokenize_surfaces(text)

Tokenizes the input text and returns only the token surfaces, as an array of strings. This is the fast path for wakati-style use: no Token objects are created and no morphological details are loaded, so it is significantly faster than tokenize when only the surface strings are needed. The result equals tokenizer.tokenize(text).map(&:surface).

surfaces = tokenizer.tokenize_surfaces('形態素解析')
# ["形態素", "解析"]

Parameters:

NameTypeDescription
textStringText to tokenize

Returns: Array<String>

tokenize_nbest(text, n, unique, cost_threshold)

Returns the N-best tokenization results, each paired with its total path cost.

results = tokenizer.tokenize_nbest('すもももももももものうち', 3, false, nil)
results.each do |tokens, cost|
  puts "#{cost}: #{tokens.map(&:surface).inspect}"
end

Parameters:

NameTypeDescription
textStringText to tokenize
nIntegerNumber of results to return
uniqueBoolean or nilDeduplicate results (default: false)
cost_thresholdInteger or nilMaximum cost difference from the best path (default: nil)

Returns: Array<Array(Array<Token>, Integer)>

Mode

Lindera::Mode is the tokenization mode that TokenizerBuilder#set_mode and Tokenizer.new take. It is the Ruby counterpart of the Rust lindera::mode::Mode: normal mode, or decompose mode with the Lindera::Penalty it uses.

Creating a Mode

Lindera::Mode has no new (Lindera::Mode.new raises TypeError). The following class methods create a mode.

Lindera::Mode.normal

Returns normal mode.

Lindera::Mode.decompose(penalty = nil)

Returns decompose mode with penalty, a Lindera::Penalty. nil or no argument uses the default penalty. A value that is not a Lindera::Penalty raises TypeError.

Lindera::Mode.from_str(name)

Returns the mode named name, as the Rust Mode parses it: 'normal', or 'decompose' with the default penalty. The name is case sensitive; any other name, such as 'Normal', raises RuntimeError with an Invalid mode message. Use it for a mode name read at run time, such as from a setting.

require 'lindera'

normal = Lindera::Mode.normal
decompose = Lindera::Mode.decompose
no_length_penalty = Lindera::Mode.decompose(Lindera::Penalty.new(nil, 0, nil, 0))
from_setting = Lindera::Mode.from_str('decompose')

Mode Methods

MethodReturnsDescription
nameString"normal" or "decompose"
to_sStringSame as name
inspectStringThe name, followed by the penalty for decompose mode
penaltyPenalty or nilA copy of the penalty of decompose mode; nil for normal mode
normal?Booleantrue for normal mode
decompose?Booleantrue for decompose mode
==Booleantrue if the other value is a Mode with the same name and penalty

A mode cannot be changed: it has no setter methods, and penalty returns a copy.

== compares the name and the penalty, and returns false for a value of another type, such as the string 'normal'. eql? and hash stay by identity, as the Rust Mode is Eq but not Hash: two equal modes are different Hash keys, and uniq keeps both.

mode = Lindera::Mode.decompose(Lindera::Penalty.new(nil, 0, nil, 0))
mode.name                                   # "decompose"
mode.normal?                                # false
mode.decompose?                             # true
mode.penalty.kanji_penalty_length_penalty   # 0
mode.inspect
# "#<Lindera::Mode: decompose Penalty(kanji_threshold=2, kanji_penalty=0, other_threshold=7, other_penalty=0)>"

Lindera::Mode.normal.inspect   # "#<Lindera::Mode: normal>"
Lindera::Mode.normal.penalty   # nil

Lindera::Mode.decompose == Lindera::Mode.from_str('decompose')  # true
Lindera::Mode.decompose == mode                                 # false (the penalties differ)
Lindera::Mode.normal == 'normal'                                # false

Penalty

Lindera::Penalty configures the length-based penalty thresholds used by decompose mode segmentation. Pass it to Lindera::Mode.decompose.

Creating a Penalty

Lindera::Penalty.new(kanji_penalty_length_threshold, kanji_penalty_length_penalty, other_penalty_length_threshold, other_penalty_length_penalty)

All four positional arguments are required, but each may be nil to fall back to its default (shown below).

require 'lindera'

penalty = Lindera::Penalty.new(2, 3000, 7, 1700)
penalty = Lindera::Penalty.new(nil, nil, nil, nil)  # uses all defaults

Penalty Properties

All properties are read-only (there are no setter methods):

PropertyTypeDefaultDescription
kanji_penalty_length_thresholdInteger2Kanji-only surface length above which the penalty applies
kanji_penalty_length_penaltyInteger3000Cost penalty added for kanji-only surfaces longer than the threshold
other_penalty_length_thresholdInteger7Surface length above which the penalty applies for non-kanji-only surfaces
other_penalty_length_penaltyInteger1700Cost penalty added for non-kanji-only surfaces longer than the threshold
penalty = Lindera::Penalty.new(nil, nil, nil, nil)
penalty.kanji_penalty_length_threshold  # 2

Using a Penalty

Pass a Penalty to Lindera::Mode.decompose, and the mode to TokenizerBuilder#set_mode or Tokenizer.new. Without length penalties, decompose mode keeps long words whole:

require 'lindera'

text = '関西国際空港限定トートバッグ'
penalty = Lindera::Penalty.new(nil, 0, nil, 0)

builder = Lindera::TokenizerBuilder.new
builder.set_dictionary('embedded://ipadic')
builder.set_mode(Lindera::Mode.decompose)
p builder.build.tokenize(text).map(&:surface)
# => ["関西", "国際", "空港", "限定", "トートバッグ"]

builder.set_mode(Lindera::Mode.decompose(penalty))
p builder.build.tokenize(text).map(&:surface)
# => ["関西国際空港", "限定", "トートバッグ"]

dictionary = Lindera.load_dictionary('embedded://ipadic')
tokenizer = Lindera::Tokenizer.new(dictionary, Lindera::Mode.decompose(penalty))
p tokenizer.tokenize(text).map(&:surface)
# => ["関西国際空港", "限定", "トートバッグ"]

Token

Token represents a single morphological token.

Properties

PropertyTypeDescription
surfaceStringSurface form of the token
byte_startIntegerStart byte position in the original text
byte_endIntegerEnd byte position in the original text
positionIntegerToken position index
word_idIntegerDictionary word ID
detailsArray<String>Morphological details (part of speech, reading, etc.)

Additionally, the predicate method unknown? returns true if the word is not in the dictionary:

token.unknown?  # => false

Token Methods

get_detail(index)

Returns the detail string at the specified index, or nil if the index is past the last detail. A negative index raises RangeError (see Integer arguments).

token = tokenizer.tokenize('東京')[0]
pos = token.get_detail(0)        # e.g., "名詞"
subpos = token.get_detail(1)     # e.g., "固有名詞"
reading = token.get_detail(7)    # e.g., "トウキョウ"

Parameters:

NameTypeDescription
indexIntegerZero-based index into the details array

Returns: String or nil

to_h / to_hash

Returns the token as a plain Hash keyed by Symbols, keeping each field's natural Ruby type, so it serializes without a custom encoder. to_h and to_hash are the same method.

require 'json'

data = tokenizer.tokenize('東京')[0].to_h
# {surface: "東京", byte_start: 0, byte_end: 6, position: 0,
#  word_id: 12345, is_unknown: false, details: [...]}

JSON.generate(tokens.map(&:to_h))

Returns: Hash with the keys :surface, :byte_start, :byte_end, :position, :word_id, :is_unknown, and :details.

The structure of details depends on the dictionary:

  • IPADIC: [品詞, 品詞細分類1, 品詞細分類2, 品詞細分類3, 活用型, 活用形, 原形, 読み, 発音]
  • UniDic: Detailed morphological features following the UniDic specification
  • ko-dic / CC-CEDICT / Jieba: Dictionary-specific detail formats

Schema

Lindera::Schema holds an ordered list of field names and provides lookups between field name and index. It is used by Metadata#dictionary_schema and Metadata#user_dictionary_schema (see Dictionary Management).

Creating a Schema

Lindera::Schema.new(fields)

Creates a schema from an array of field names.

require 'lindera'

schema = Lindera::Schema.new(%w[
  surface
  left_context_id
  right_context_id
  cost
  major_pos
  reading
])

Lindera::Schema.create_default

Returns the built-in default schema: 13 fields matching the IPADIC-style layout (surface, left_context_id, right_context_id, cost, major_pos, pos_detail_1, pos_detail_2, pos_detail_3, conjugation_type, conjugation_form, base_form, reading, pronunciation).

schema = Lindera::Schema.create_default

Schema Methods

MethodReturnsDescription
fieldsArray<String>All field names, in order
get_all_fieldsArray<String>Same as fields
field_countIntegerTotal number of fields
get_field_index(name)Integer or nilIndex of the field named name
get_field_name(index)String or nilField name at index
get_custom_fieldsArray<String>Field names after the four fixed fields (surface, left_context_id, right_context_id, cost)
get_field_by_name(name)FieldDefinition or nilFull field definition for name
validate_record(record)nilRaises ArgumentError if record does not match the schema
to_sStringe.g. "Schema(fields=13)"
inspectStringFull field list
schema = Lindera::Schema.create_default

schema.field_count                  # 13
schema.get_field_index('cost')      # 3
schema.get_field_name(0)            # "surface"
schema.get_custom_fields            # ["major_pos", "pos_detail_1", ..., "pronunciation"]

field = schema.get_field_by_name('surface')
puts "#{field.index} #{field.name} #{field.field_type}"  # 0 surface surface

schema.validate_record([
  '東京', '1288', '1288', '100',
  '名詞', '固有名詞', '地域', '一般', '*', '*',
  '東京', 'トウキョウ', 'トーキョー'
])

FieldDefinition

Lindera::FieldDefinition describes a single field within a Schema. Instances are only obtained from Schema#get_field_by_name -- there is no public constructor (Lindera::FieldDefinition.new raises TypeError).

FieldDefinition Properties

PropertyTypeDescription
indexIntegerZero-based position of the field within the schema
nameStringField name
field_typeFieldTypeField type
descriptionString or nilOptional human-readable description
schema = Lindera::Schema.create_default
field = schema.get_field_by_name('surface')

field.index         # 0
field.name          # "surface"
field.field_type    # #<Lindera::FieldType: surface>
field.description    # nil (the default schema does not set descriptions)

FieldType

Lindera::FieldType enumerates the category of a single field. Like FieldDefinition, instances are only obtained from a Schema (via FieldDefinition#field_type) -- there is no public constructor.

to_s (and inspect) return one of:

  • "surface" -- surface form (word text)
  • "left_context_id" -- left context ID
  • "right_context_id" -- right context ID
  • "cost" -- word cost
  • "custom" -- any other, dictionary-specific field
field = Lindera::Schema.create_default.get_field_by_name('surface')
field.field_type.to_s  # "surface"

Dictionary Management

Lindera Ruby provides functions for loading, building, and managing dictionaries used in morphological analysis.

Loading Dictionaries

System Dictionaries

Use Lindera.load_dictionary(uri) to load a system dictionary. Download a pre-built dictionary from GitHub Releases and specify the path to the extracted directory:

require 'lindera'

dictionary = Lindera.load_dictionary('/path/to/ipadic')

Embedded dictionaries (advanced) -- if you built with an embed-* feature flag, you can load an embedded dictionary:

dictionary = Lindera.load_dictionary('embedded://ipadic')

User Dictionaries

User dictionaries add custom vocabulary on top of a system dictionary. Load one for the system dictionary it is used with:

require 'lindera'

dictionary = Lindera.load_dictionary('/path/to/ipadic')
user_dict = Lindera.load_user_dictionary_for('/path/to/user_dictionary.csv', dictionary)

A .csv is built with the system dictionary's metadata and checked against its connection matrix as it is built: a row whose context ID lies outside the matrix raises a RuntimeError that names its CSV line and surface (see Context IDs). A .bin is loaded as it was built.

Lindera.load_user_dictionary(uri, metadata) loads a user dictionary with metadata only. A .csv loaded this way is checked when it is combined with the system dictionary, and the error names the entry by its word id:

require 'lindera'

dictionary = Lindera.load_dictionary('/path/to/ipadic')
metadata = dictionary.metadata
user_dict = Lindera.load_user_dictionary('/path/to/user_dictionary.csv', metadata)

Pass the user dictionary when building a tokenizer:

require 'lindera'

dictionary = Lindera.load_dictionary('/path/to/ipadic')
user_dict = Lindera.load_user_dictionary_for('/path/to/user_dictionary.csv', dictionary)

tokenizer = Lindera::Tokenizer.new(dictionary, Lindera::Mode.normal, user_dict)

Or via the builder:

require 'lindera'

builder = Lindera::TokenizerBuilder.new
builder.set_dictionary('/path/to/ipadic')
builder.set_user_dictionary('/path/to/user_dictionary.csv')
tokenizer = builder.build

Building Dictionaries

System Dictionary

Build a system dictionary from source files:

require 'lindera'

metadata = Lindera::Metadata.from_json_file('metadata.json')
Lindera.build_dictionary('/path/to/input_dir', '/path/to/output_dir', metadata)

The input directory should contain the dictionary source files (CSV lexicon, matrix.def, etc.).

Metadata loaded with Lindera::Metadata.from_json_file keeps the space_penalty rules of the metadata.json, and the built dictionary ships them. For ko-dic this means the left-space penalty stays on by default in the dictionary you build; turn it off per tokenizer with TokenizerBuilder#set_space_penalty(false) (see Tokenizer API).

User Dictionary

Build a user dictionary from a CSV file for the system dictionary it is used with:

require 'lindera'

dictionary = Lindera.load_dictionary('/path/to/ipadic')
Lindera.build_user_dictionary_for('user_words.csv', '/path/to/user_dictionary.bin', dictionary)
user_dict = Lindera.load_user_dictionary_for('/path/to/user_dictionary.bin', dictionary)

The CSV is built with the system dictionary's metadata and checked against its connection matrix: a row whose context ID lies outside the matrix raises a RuntimeError that names its CSV line and surface, and no file is written (see Context IDs). The second argument is the path of the file to write. End it with .bin, since the loaders choose how to load a user dictionary by its extension.

Lindera.build_user_dictionary(kind, input_file, output_dir, metadata) builds a user dictionary with metadata only. Despite its name, output_dir is the path of the file to write too, not a directory. The rows are checked when the user dictionary is combined with the system dictionary, and the error names the entry by its word id:

require 'lindera'

metadata = Lindera::Metadata.from_json_file('metadata.json')
Lindera.build_user_dictionary('ipadic', 'user_words.csv', '/path/to/user_dictionary.bin', metadata)

The metadata parameter is optional. When omitted, default metadata values are used:

Lindera.build_user_dictionary('ipadic', 'user_words.csv', '/path/to/user_dictionary.bin', nil)

[!NOTE] The first argument (kind, 'ipadic' above) is currently unused -- it is reserved for future use and has no effect on the build. Any string value can be passed for it today. To build for a system dictionary, use Lindera.build_user_dictionary_for.

Metadata

The Lindera::Metadata class configures dictionary parameters.

Creating Metadata

require 'lindera'

# Create default metadata with standard settings
metadata = Lindera::Metadata.create_default

Lindera::Metadata.new takes all eight properties as required positional arguments (each may be nil to fall back to its default) -- use it only when you need to override specific values:

metadata = Lindera::Metadata.new(
  'my_dict', # name
  'UTF-8',   # encoding
  -10_000,   # default_word_cost
  1288,      # default_left_context_id
  1288,      # default_right_context_id
  '*',       # default_field_value
  false,     # flexible_csv
  false      # skip_invalid_cost_or_id
)

Loading from JSON

metadata = Lindera::Metadata.from_json_file('metadata.json')

Getting Metadata from a Dictionary

A loaded dictionary's metadata can be retrieved directly:

dictionary = Lindera.load_dictionary('/path/to/ipadic')
metadata = dictionary.metadata

Properties

PropertyTypeDefaultDescription
nameString"default"Dictionary name
encodingString"UTF-8"Character encoding
default_word_costInteger-10000Default cost for unknown words
default_left_context_idInteger1288Default left context ID
default_right_context_idInteger1288Default right context ID
default_field_valueString"*"Default value for missing fields
flexible_csvBooleanfalseAllow flexible CSV parsing
skip_invalid_cost_or_idBooleanfalseSkip entries with invalid cost or ID

to_h (and its alias to_hash) returns these properties as a Hash of strings, together with dictionary_schema_fields and user_dictionary_schema_fields (comma-separated field names). When the metadata carries left-space penalty rules, as ko-dic's does, the hash also has a space_penalty entry holding them as a JSON string:

metadata = Lindera::Metadata.from_json_file('/path/to/ko-dic/metadata.json')
metadata.to_h['space_penalty']
# '{"rules":[{"pos":["EC","EF",...],"cost":3000},{"pos":["JC","JKB",...],"cost":6000}]}'

Text Processing Pipeline

Lindera Ruby supports a composable text processing pipeline that applies character filters before tokenization and token filters after tokenization. Filters are added to the TokenizerBuilder and executed in the order they are appended.

Input Text
  --> Character Filters (preprocessing)
  --> Tokenization
  --> Token Filters (postprocessing)
  --> Output Tokens

[!NOTE] This page shows a few commonly used filters as examples -- it is not the complete list. See Filters for the full, authoritative catalogue of every character and token filter that lindera-analysis ships, including parameters and examples.

Character Filters

Character filters transform the input text before tokenization.

unicode_normalize

Applies Unicode normalization to the input text.

require 'lindera'

builder = Lindera::TokenizerBuilder.new
builder.set_dictionary('embedded://ipadic')
builder.append_character_filter('unicode_normalize', { 'kind' => 'nfkc' })
tokenizer = builder.build

Supported normalization forms: "nfc", "nfkc", "nfd", "nfkd".

mapping

Replaces characters or strings according to a mapping table.

builder = Lindera::TokenizerBuilder.new
builder.set_dictionary('embedded://ipadic')
builder.append_character_filter('mapping', {
  'mapping' => {
    "\u30fc" => '-',
    "\uff5e" => '~'
  }
})
tokenizer = builder.build

japanese_iteration_mark

Resolves Japanese iteration marks (odoriji) into their full forms.

builder = Lindera::TokenizerBuilder.new
builder.set_dictionary('embedded://ipadic')
builder.append_character_filter('japanese_iteration_mark', {
  'normalize_kanji' => true,
  'normalize_kana' => true
})
tokenizer = builder.build

Token Filters

Token filters transform or remove tokens after tokenization.

lowercase

Converts token surface forms to lowercase.

builder = Lindera::TokenizerBuilder.new
builder.set_dictionary('embedded://ipadic')
builder.append_token_filter('lowercase', nil)
tokenizer = builder.build

japanese_base_form

Replaces inflected forms with their base (dictionary) form using the morphological details from the dictionary.

builder = Lindera::TokenizerBuilder.new
builder.set_dictionary('embedded://ipadic')
builder.append_token_filter('japanese_base_form', nil)
tokenizer = builder.build

japanese_stop_tags

Removes tokens whose part-of-speech matches any of the specified tags.

builder = Lindera::TokenizerBuilder.new
builder.set_dictionary('embedded://ipadic')
builder.append_token_filter('japanese_stop_tags', {
  'tags' => ['助詞,格助詞,一般', '助詞,係助詞', '助詞,連体化', '助動詞']
})
tokenizer = builder.build

[!NOTE] Tags are normalized to exactly four comma-separated levels (missing levels are padded with *) and compared for exact equality against the four part-of-speech details of each token, starting at the field named part_of_speech in the dictionary schema (or at the first detail when the schema has no such field). A bare 助詞 therefore never matches IPADIC particle tokens — they always carry a subcategory such as 助詞,係助詞 — while a bare 助動詞 does match, because auxiliary verbs have no subcategory (助動詞,*,*,*).

japanese_keep_tags

Keeps only tokens whose part-of-speech matches one of the specified tags. All other tokens are removed. The following keeps only general nouns:

builder = Lindera::TokenizerBuilder.new
builder.set_dictionary('embedded://ipadic')
builder.append_token_filter('japanese_keep_tags', {
  'tags' => ['名詞,一般']
})
tokenizer = builder.build

japanese_katakana_stem

Removes trailing prolonged sound marks from katakana tokens that exceed a minimum length.

builder = Lindera::TokenizerBuilder.new
builder.set_dictionary('embedded://ipadic')
builder.append_token_filter('japanese_katakana_stem', { 'min' => 3 })
tokenizer = builder.build

Complete Pipeline Example

The following example combines multiple character filters and token filters into a single pipeline:

require 'lindera'

builder = Lindera::TokenizerBuilder.new
builder.set_mode(Lindera::Mode.normal)
builder.set_dictionary('embedded://ipadic')

# Preprocessing
builder.append_character_filter('unicode_normalize', { 'kind' => 'nfkc' })
builder.append_character_filter('japanese_iteration_mark', {
  'normalize_kanji' => true,
  'normalize_kana' => true
})

# Postprocessing
builder.append_token_filter('japanese_base_form', nil)
builder.append_token_filter('japanese_stop_tags', {
  'tags' => ['助詞,格助詞,一般', '助詞,係助詞', '助詞,連体化', '助動詞', '記号,句点', '記号,読点']
})
builder.append_token_filter('lowercase', nil)

tokenizer = builder.build

tokens = tokenizer.tokenize('Linderaは形態素解析を行うライブラリです。')
tokens.each do |token|
  puts "#{token.surface}\t#{token.details.join(',')}"
end

In this pipeline:

  1. unicode_normalize converts full-width characters to half-width (NFKC normalization)
  2. japanese_iteration_mark resolves iteration marks
  3. japanese_base_form converts inflected tokens to base form
  4. japanese_stop_tags removes particles, auxiliary verbs, and punctuation
  5. lowercase normalizes alphabetic characters to lowercase

Training

Lindera Ruby supports training custom CRF-based morphological analysis models from annotated corpora. This functionality requires the train feature.

Prerequisites

Build lindera-ruby with the train feature enabled:

LINDERA_FEATURES="embed-ipadic,train" bundle exec rake compile

Training a Model

Use Lindera.train to train a CRF model from a seed lexicon and annotated corpus:

require 'lindera'

Lindera.train(
  'resources/training/seed.csv',
  'resources/training/corpus.txt',
  'resources/training/char.def',
  'resources/training/unk.def',
  'resources/training/feature.def',
  'resources/training/rewrite.def',
  '/tmp/model.dat',
  1.0,   # lambda (regularization coefficient)
  100,   # max_iter
  nil    # max_threads (nil = auto-detect CPU cores)
)

Training Parameters

The first seven parameters are required positional arguments. lambda, max_iter and max_threads are optional positional arguments after them, and the regularization type, the Elastic Net ratio and eta are keyword arguments; nil, or leaving an argument out, selects its default:

PositionNameTypeDescription
1seedStringPath to the seed lexicon file (CSV format)
2corpusStringPath to the annotated training corpus
3char_defStringPath to the character definition file (char.def)
4unk_defStringPath to the unknown word definition file (unk.def)
5feature_defStringPath to the feature definition file (feature.def)
6rewrite_defStringPath to the rewrite rule definition file (rewrite.def)
7outputStringOutput path for the trained model file
8lambdaFloat or nilRegularization coefficient, finite and >= 0, default 1.0; 0 disables regularization for every type; with L2, MeCab's -c C is 1 / C, so 1.0 is MeCab's -c 1.0
9max_iterInteger or nilMaximum number of training iterations, 1 or more, default 100; training stops earlier once the objective converges (see eta:)
10max_threadsInteger or nilNumber of threads, 1 or more (nil = auto-detect CPU cores)
keywordregularization:String or nilRegularization type, default 'l2': 'l1', 'l2' or 'elasticnet', in any case ('elastic_net' and 'elastic-net' also select Elastic Net), as lindera train --regularization takes it
keywordelastic_net_l1_ratio:Float or nilL1 ratio of Elastic Net regularization, between 0 and 1, default 0.5; 0 uses only L2, 1 uses only L1; used only with 'elasticnet'
keywordeta:Float or nilTolerance of the convergence test, finite and >= 0, default 5e-5: training stops before max_iter once the relative change of the objective stays below it for 3 iterations in a row; 0 disables the test

The settings are checked before any file is read: an unknown regularization: raises an ArgumentError, and a lambda, elastic_net_l1_ratio: or eta: outside its range a RangeError. To train with v6's default settings (the model still differs from v6's, since the first line search changed in #1290), pass a lambda of 0.01 and regularization: 'l1', and eta: 0 to stop only at max_iter:

Lindera.train(*inputs, '/tmp/model.dat', 0.01, regularization: 'l1', eta: 0)

Exporting a Trained Model

After training, export the model to dictionary source files using Lindera.export:

require 'lindera'

Lindera.export(
  '/tmp/model.dat',
  '/tmp/dictionary_source',
  'resources/training/metadata.json'
)

Export Parameters

PositionNameTypeDescription
1modelStringPath to the trained model file (.dat)
2outputStringOutput directory for dictionary source files
3metadataString or nilPath to a base metadata.json file

The export creates the following files in the output directory, the same files that lindera export creates:

  • lex.csv -- Lexicon entries with trained costs
  • matrix.def -- Connection cost matrix
  • unk.def -- Unknown word definitions
  • char.def -- Character category definitions (copied from the trained model)
  • feature.def -- Feature template definitions (copied from the trained model)
  • rewrite.def -- Feature rewrite rules (copied from the trained model)
  • left-id.def -- Left context ID to feature string mapping
  • right-id.def -- Right context ID to feature string mapping
  • metadata.json -- Updated metadata (when metadata parameter is provided)

The model file must come from this version's training. A model file written by an earlier version, which includes every model file written before 7.0.0, is rejected: Lindera.export fails with an error that asks you to re-run training. Train such a model again with Lindera.train.

Complete Workflow

The full workflow for training and using a custom dictionary:

require 'lindera'

# Step 1: Train the CRF model
Lindera.train(
  'resources/training/seed.csv',
  'resources/training/corpus.txt',
  'resources/training/char.def',
  'resources/training/unk.def',
  'resources/training/feature.def',
  'resources/training/rewrite.def',
  '/tmp/model.dat',
  1.0,   # lambda
  100,   # max_iter
  nil    # max_threads
)

# Step 2: Export to dictionary source files
Lindera.export(
  '/tmp/model.dat',
  '/tmp/dictionary_source',
  'resources/training/metadata.json'
)

# Step 3: Build the dictionary from exported source files
metadata = Lindera::Metadata.from_json_file('/tmp/dictionary_source/metadata.json')
Lindera.build_dictionary('/tmp/dictionary_source', '/tmp/dictionary', metadata)

# Step 4: Use the trained dictionary
builder = Lindera::TokenizerBuilder.new
builder.set_dictionary('/tmp/dictionary')
builder.set_mode(Lindera::Mode.normal)
tokenizer = builder.build

tokens = tokenizer.tokenize('形態素解析のテスト')
tokens.each do |token|
  puts "#{token.surface}\t#{token.details.join(',')}"
end

Lindera PHP

Lindera PHP provides PHP bindings for the Lindera morphological analysis engine, built with ext-php-rs. It brings Lindera's high-performance tokenization capabilities to the PHP ecosystem with support for PHP 8.1 and later.

Features

  • Multi-language support: Tokenize Japanese (IPADIC, IPADIC NEologd, UniDic), Korean (ko-dic), and Chinese (CC-CEDICT, Jieba) text
  • Text processing pipeline: Compose character filters and token filters for flexible preprocessing and postprocessing
  • CRF-based dictionary training: Train custom morphological analysis models from annotated corpora (requires train feature)
  • Multiple tokenization modes: Normal and decompose modes for different analysis granularity
  • N-best tokenization: Retrieve multiple tokenization candidates ranked by cost
  • User dictionaries: Extend system dictionaries with custom vocabulary

Argument types

A parameter with a declared type takes a value of that type only, with or without declare(strict_types=1): an int parameter takes an int, not a numeric string or a float, and a ?Metadata parameter takes a Lindera\Metadata. A float parameter also takes an int. For an optional parameter, such as ?int $cost_threshold = null, null or leaving the argument out selects the default. Any other value throws a TypeError that names the parameter. Cast values that arrive as strings, such as command-line arguments, to the declared type:

<?php

$dictionary = Lindera\Dictionary::load('embedded://ipadic');
$tokenizer = new Lindera\Tokenizer($dictionary);

$threshold = '5000'; // for example, from $argv or a configuration file

try {
    $tokenizer->tokenizeNbest('東京都', 2, cost_threshold: $threshold);
} catch (TypeError $e) {
    echo get_class($e), ': ', $e->getMessage(), "\n";
    // TypeError: Invalid value given for argument `cost_threshold`.
}

// Cast it to the declared type
$results = $tokenizer->tokenizeNbest('東京都', 2, cost_threshold: (int) $threshold);

// null, or leaving the argument out, selects the default
$results = $tokenizer->tokenizeNbest('東京都', 2, cost_threshold: null);

The parameters declared mixed, such as $space_penalty of new Lindera\Tokenizer(...) and $args of the filter methods, are not covered by this rule: they take any PHP value and check it themselves.

An int parameter also takes only the values its setting can hold, which for most settings is a narrower range than PHP's 64-bit int. $default_word_cost of Lindera\Metadata takes -32768 to 32767, the context IDs 0 to 65535, and the penalties of Lindera\Penalty a 32-bit integer. A count or an index takes 0 or more: the thresholds of Lindera\Penalty, $index of new Lindera\FieldDefinition(...), Schema::getFieldName() and Token::getDetail(), and $n of tokenizeNbest(). $max_iter and $max_threads of Lindera\Trainer::train() take 1 or more. A value outside the range throws a ValueError that names the parameter and the range; an index past the end still returns null:

<?php

try {
    new Lindera\Metadata(default_word_cost: 100000);
} catch (ValueError $e) {
    echo get_class($e), ': ', $e->getMessage(), "\n";
    // ValueError: default_word_cost must be an integer between -32768 and 32767, got 100000
}

$dictionary = Lindera\Dictionary::load('embedded://ipadic');
$tokenizer = new Lindera\Tokenizer($dictionary);

try {
    $tokenizer->tokenizeNbest('東京都', -1);
} catch (ValueError $e) {
    echo get_class($e), ': ', $e->getMessage(), "\n";
    // ValueError: n must be an integer between 0 and 18446744073709551615, got -1
}

The float settings of Lindera\Trainer::train() are checked the same way, before any file is read: $lambda and $eta take a finite number of 0 or more, and $elastic_net_l1_ratio a number between 0 and 1 (see Training).

Documentation

Installation

lindera-php is a PHP extension. It is published on Packagist as lindera/lindera and installed with PIE, the PHP extension installer. The extension registers as lindera.

Prerequisites

  • PHP 8.1 or later on Linux or macOS. Windows is not supported.
  • PIE 1.4 or later and unzip -- see the PIE installation guide. Older PIE versions cannot read this package's metadata; run pie self-update.
  • Only when PIE falls back to building from source: a Rust toolchain (rustup), libclang (libclang-dev on Debian/Ubuntu; the Xcode command line tools or brew install llvm on macOS) and the usual PHP extension build tools (autoconf, libtool, make)

Install with PIE

pie install lindera/lindera

[!NOTE] lindera/lindera is available on Packagist from lindera v6.1.0. For earlier versions, build from source as described below.

PIE downloads a prebuilt lindera.so from the GitHub release when one exists for your PHP and platform, installs it into the extension directory and enables it; nothing is compiled and no Rust toolchain is needed. Prebuilt binaries cover:

  • PHP 8.1 to 8.5, NTS and ZTS
  • Linux x86_64 and arm64 with glibc 2.35 or newer (e.g. Ubuntu 22.04, Debian 12)
  • macOS on Apple silicon

For other platforms (Alpine and other musl systems, Intel Macs) the same command falls back to building from source against the PHP it finds. Pass --with-php-config=/path/to/php-config to target another PHP. Composer itself does not install php-ext packages, so composer require lindera/lindera is not an installation route.

[!WARNING] PIE does not check the glibc version. On a glibc system older than 2.35 (e.g. Ubuntu 20.04, Debian 11, RHEL 8) it still installs the prebuilt binary, which then fails to load. Build from source there instead: with PIE 1.5 or later, run pie install --suppress-download-url-method=pre-packaged-binary lindera/lindera, or build manually as described below.

The package does not embed any dictionary; see Obtaining Dictionaries.

Obtaining Dictionaries

Lindera does not bundle dictionaries with the package. You need to obtain a pre-built dictionary separately.

Download from GitHub Releases

Pre-built dictionaries are available on the GitHub Releases page. Download and extract the dictionary archive to a local directory:

# Example: download and extract the IPADIC dictionary
curl -LO https://github.com/lindera/lindera/releases/download/<version>/lindera-ipadic-<version>.zip
unzip lindera-ipadic-<version>.zip -d /path/to/ipadic

Building from Source

Build the lindera-php extension from the project root:

cargo build -p lindera-php

Or use the project Makefile:

make build-lindera-php

Build with Training Support

The train feature enables CRF-based dictionary training functionality:

cargo build -p lindera-php --features train

Feature Flags

FeatureDescriptionDefault
trainCRF training functionalityEnabled (default)
embed-ipadicEmbed Japanese dictionary (IPADIC) into the binaryDisabled
embed-unidicEmbed Japanese dictionary (UniDic) into the binaryDisabled
embed-sudachidictEmbed Japanese dictionary (SudachiDict) into the binaryDisabled
embed-ipadic-neologdEmbed Japanese dictionary (IPADIC NEologd) into the binaryDisabled
embed-ko-dicEmbed Korean dictionary (ko-dic) into the binaryDisabled
embed-cc-cedictEmbed Chinese dictionary (CC-CEDICT) into the binaryDisabled
embed-jiebaEmbed Chinese dictionary (Jieba) into the binaryDisabled
embed-cjkEmbed all CJK dictionaries (IPADIC, ko-dic, Jieba) into the binaryDisabled

Multiple features can be combined:

cargo build -p lindera-php --features "train,embed-ipadic,embed-ko-dic"

The prebuilt binaries and PIE's source build use the default features only (train, no embedded dictionary).

[!TIP] If you want to embed a dictionary directly into the binary (advanced usage), enable the corresponding embed-* feature flag and load it using the embedded:// scheme:

$dictionary = Lindera\Dictionary::load('embedded://ipadic');

See Feature Flags for details.

Loading a Self-Built Extension

Load the compiled shared library when running PHP:

php -d extension=target/debug/liblindera_php.so script.php

For release builds:

cargo build -p lindera-php --release
php -d extension=target/release/liblindera_php.so script.php

Alternatively, add the extension to your php.ini:

extension=/absolute/path/to/liblindera_php.so

On macOS, cargo produces liblindera_php.dylib instead. Whichever way it is loaded, the extension registers as lindera: php -m lists lindera, and extension_loaded('lindera') is the check to use.

Verifying the Installation

Verify that lindera is available in PHP:

# Installed with PIE
php -r "echo Lindera\Dictionary::version() . PHP_EOL;"

# Self-built
php -d extension=target/debug/liblindera_php.so -r "echo Lindera\Dictionary::version() . PHP_EOL;"

Stubs for Static Analysis

lindera-php/stubs/lindera.stubs.php declares every class of the extension for IDEs, PHPStan and Psalm. It is generated from the compiled extension and the test suite fails when it is out of date. Point your analyser at a copy of the file, and never include it at runtime -- the extension already declares the classes.

# phpstan.neon
parameters:
    stubFiles:
        - path/to/lindera.stubs.php

Quick Start

This guide shows how to tokenize text using lindera-php.

Basic Tokenization

Load a dictionary, create a tokenizer, and tokenize text:

<?php

// Load the dictionary
$dictionary = Lindera\Dictionary::load('/path/to/ipadic');

// Create a tokenizer
$tokenizer = new Lindera\Tokenizer($dictionary, Lindera\Mode::normal());

// Tokenize the text
$tokens = $tokenizer->tokenize('関西国際空港限定トートバッグ');
foreach ($tokens as $token) {
    echo $token->surface . "\t" . implode(',', $token->details) . "\n";
}

Note: Download a pre-built dictionary from GitHub Releases and specify the path to the extracted directory.

Expected output:

関西国際空港    名詞,固有名詞,組織,*,*,*,関西国際空港,カンサイコクサイクウコウ,カンサイコクサイクーコー
限定    名詞,サ変接続,*,*,*,*,限定,ゲンテイ,ゲンテイ
トートバッグ    名詞,一般,*,*,*,*,*,*,*

Using TokenizerBuilder

TokenizerBuilder gives you more flexible configuration options:

<?php

$builder = new Lindera\TokenizerBuilder();
$builder->setMode(Lindera\Mode::normal());
$builder->setDictionary('/path/to/ipadic');
$tokenizer = $builder->build();

$tokens = $tokenizer->tokenize('すもももももももものうち');
foreach ($tokens as $token) {
    echo $token->surface . "\t" . $token->getDetail(0) . "\n";
}

Accessing Token Properties

Each token exposes the following properties:

<?php

$builder = new Lindera\TokenizerBuilder();
$builder->setDictionary('/path/to/ipadic');
$tokenizer = $builder->build();
$tokens = $tokenizer->tokenize('東京タワー');

foreach ($tokens as $token) {
    echo "Surface: {$token->surface}\n";
    echo "Byte range: {$token->byte_start}..{$token->byte_end}\n";
    echo "Position: {$token->position}\n";
    echo "Word ID: {$token->word_id}\n";
    echo "Unknown: " . ($token->is_unknown ? 'true' : 'false') . "\n";
    echo "Details: " . implode(',', $token->details) . "\n";
    echo "\n";
}

N-best Tokenization

Retrieve multiple tokenization candidates ranked by cost:

<?php

$builder = new Lindera\TokenizerBuilder();
$builder->setDictionary('/path/to/ipadic');
$tokenizer = $builder->build();
$results = $tokenizer->tokenizeNbest('すもももももももものうち', 3);

foreach ($results as $result) {
    $surfaces = array_map(fn($t) => $t->surface, $result->tokens);
    echo "Cost {$result->cost}: " . implode(' / ', $surfaces) . "\n";
}

Tokenizer API

TokenizerBuilder

Lindera\TokenizerBuilder configures and constructs a Tokenizer instance using the builder pattern.

Constructors

new Lindera\TokenizerBuilder()

Creates a new builder with default configuration.

<?php

$builder = new Lindera\TokenizerBuilder();

$builder->fromFile($file_path)

Loads configuration from a JSON file.

<?php

$builder = new Lindera\TokenizerBuilder();
$builder->fromFile('config.json');

Configuration Methods

setMode($mode)

Sets the tokenization mode, a Lindera\Mode:

  • Lindera\Mode::normal() -- Standard tokenization (default)
  • Lindera\Mode::decompose($penalty) -- Decomposes compound words into smaller units, with a Lindera\Penalty or, without one, the default penalty

$mode takes only a Lindera\Mode: a string such as 'normal', null or an object of another class throws a TypeError. To select a mode by its name, pass Lindera\Mode::fromStr($name).

<?php

$builder->setMode(Lindera\Mode::normal());
$builder->setMode(Lindera\Mode::decompose(new Lindera\Penalty(kanji_penalty_length_penalty: 0)));

setDictionary($path)

Sets the system dictionary path or URI.

<?php

// Use an embedded dictionary
$builder->setDictionary('embedded://ipadic');

// Use an external dictionary
$builder->setDictionary('/path/to/dictionary');

setUserDictionary($uri)

Sets the user dictionary URI.

<?php

$builder->setUserDictionary('/path/to/user_dictionary.csv');

setKeepWhitespace($keep)

Controls whether whitespace tokens appear in the output. Kept whitespace also stays in the lattice, so the other tokens can differ from those of the default output, which skips whitespace in the lattice as MeCab does. To keep whitespace in the lattice without outputting it, use setSkipWhitespace(false) instead.

<?php

$builder->setKeepWhitespace(true);

setSkipWhitespace($skip)

Sets whether whitespace is skipped in the lattice. $skip has the same meaning as segmenter.skip_whitespace in a configuration file:

  • true -- Skips whitespace, so the words on either side of it connect directly, as MeCab does.
  • false -- Keeps whitespace in the lattice as a node of its own (a whitespace dictionary entry or the SPACE unknown word), as before v7. Whitespace is still dropped from the output.
  • null -- The default of the dictionary's metadata.json: skipping, except with SudachiDict. Use it to undo an earlier call.

A value set here overrides the dictionary's default and a skip_whitespace loaded with fromFile. It has no effect while whitespace tokens are kept (setKeepWhitespace(true)), because kept whitespace always stays in the lattice. The argument is required; a value of any other type throws TypeError.

With IPADIC, for example, Google が 新しい reads が as a particle (助詞) by default and as a conjunction (接続詞) with setSkipWhitespace(false). See Segmenter for details.

<?php

// Keep whitespace in the lattice, as before v7
$builder->setSkipWhitespace(false);

setSpacePenalty($value)

Sets the left-space penalty for Korean: a candidate that starts right after whitespace and whose part-of-speech tag normally attaches to the preceding word (particles, endings, ...) gets a cost added. $value has the same meaning as segmenter.space_penalty in a configuration file:

  • null -- The default: the rules the dictionary ships in its metadata.json, if any (ko-dic ships mecab-ko-dic's rules; the other bundled dictionaries ship none). Use it to undo an earlier call.
  • false -- Turns the penalty off.
  • true -- Requires the dictionary's rules; build() throws a ValueError for a dictionary that ships none.
  • An associative array, ['rules' => [['pos' => [...], 'cost' => n], ...]] -- Applies these rules instead. A candidate whose first part-of-speech tag is listed in pos gets cost added; the first matching rule wins.

rules and each pos must be lists, with keys 0, 1, 2, ... (apply array_values() after array_filter() or array_unique()), and cost must be an int in the 32-bit range; a float such as 6000.0 is rejected. Any other value throws a ValueError, including objects (a stdClass, or json_decode() without true) and NAN or INF.

<?php

$builder->setDictionary('embedded://ko-dic');

// Turn the penalty off
$builder->setSpacePenalty(false);

// Apply explicit rules
$builder->setSpacePenalty([
    'rules' => [
        ['pos' => ['EC', 'EF', 'EP', 'ETM', 'ETN', 'VCP', 'XSA', 'XSN', 'XSV'], 'cost' => 3000],
        ['pos' => ['JC', 'JKB', 'JKC', 'JKG', 'JKO', 'JKQ', 'JKS', 'JKV', 'JX'], 'cost' => 6000],
    ],
]);

// Back to the dictionary's default
$builder->setSpacePenalty(null);

[!NOTE] Since v6.1.0, ko-dic applies the left-space penalty by default, as mecab-ko does. For example, 검색 이 잘 된다 reads 이 as the interjection IC; without the penalty, as the subject particle JKS. Call $builder->setSpacePenalty(false) to turn it off; for a tokenizer created with new Lindera\Tokenizer(...), pass space_penalty: false. Only a ko-dic built by Lindera 6.1.0 or later ships the rules; with a ko-dic from the v6.0.0 release the penalty stays off and true fails until the dictionary is rebuilt. See Segmenter for details.

appendCharacterFilter($kind, $args)

Appends a character filter to the preprocessing pipeline.

<?php

$builder->appendCharacterFilter('unicode_normalize', ['kind' => 'nfkc']);

appendTokenFilter($kind, $args)

Appends a token filter to the postprocessing pipeline.

<?php

$builder->appendTokenFilter('lowercase');

The arguments of setSpacePenalty, appendCharacterFilter and appendTokenFilter may nest arrays at most 128 levels deep. A deeper array throws a ValueError.

Build

build()

Builds and returns a Tokenizer with the configured settings.

<?php

$tokenizer = $builder->build();

Tokenizer

Lindera\Tokenizer performs morphological analysis on text.

Creating a Tokenizer

new Lindera\Tokenizer($dictionary, $mode, $user_dictionary, $space_penalty)

Creates a tokenizer directly from a loaded dictionary. $mode, $user_dictionary and $space_penalty are optional and default to null.

$mode takes a Lindera\Mode, as setMode() does; null means normal mode. A value of another type, such as the string 'normal', throws a TypeError.

$space_penalty takes the same values as setSpacePenalty(). null uses the left-space penalty rules the dictionary ships (ko-dic ships them); pass false to turn the penalty off. Pass it by name to skip the other optional arguments. An invalid setting throws a ValueError, as setSpacePenalty() and build() do.

<?php

$dictionary = Lindera\Dictionary::load('embedded://ipadic');
$tokenizer = new Lindera\Tokenizer($dictionary, Lindera\Mode::normal());

// ko-dic without the left-space penalty
$koTokenizer = new Lindera\Tokenizer(Lindera\Dictionary::load('embedded://ko-dic'), space_penalty: false);

With a user dictionary:

<?php

$dictionary = Lindera\Dictionary::load('embedded://ipadic');
$userDict = Lindera\Dictionary::loadUserFor('/path/to/user_dictionary.csv', $dictionary);
$tokenizer = new Lindera\Tokenizer($dictionary, Lindera\Mode::normal(), $userDict);

Tokenizer Methods

tokenize($text)

Tokenizes the input text and returns an array of Token objects.

<?php

$tokens = $tokenizer->tokenize('形態素解析');

Parameters:

NameTypeDescription
$textstringText to tokenize

Returns: array<Token>

tokenizeSurfaces($text)

Tokenizes the input text and returns only the token surfaces, as an array of strings. This is the fast path for wakati-style use: no Token objects are created and no morphological details are loaded, so it is significantly faster than tokenize when only the surface strings are needed. The result equals array_map(fn ($t) => $t->surface, $tokenizer->tokenize($text)).

<?php

$surfaces = $tokenizer->tokenizeSurfaces('形態素解析');
// ["形態素", "解析"]

Parameters:

NameTypeDescription
$textstringText to tokenize

Returns: array<string>

tokenizeNbest($text, $n, $unique, $cost_threshold)

Returns the N-best tokenization results as an array of NbestResult objects.

<?php

$results = $tokenizer->tokenizeNbest('すもももももももものうち', 3);
foreach ($results as $result) {
    echo "Cost: {$result->cost}\n";
    foreach ($result->tokens as $token) {
        echo "  {$token->surface}\n";
    }
}

Parameters:

NameTypeDescription
$textstringText to tokenize
$nintNumber of results to return, 0 or more
$uniquebool|nullDeduplicate results (default: false)
$cost_thresholdint|nullMaximum cost difference from the best path (default: null)

Returns: array<NbestResult>

NbestResult

Lindera\NbestResult represents a single N-best tokenization result.

NbestResult Properties

PropertyTypeDescription
$tokensarray<Token>The tokens in this result
$costintThe total cost of this segmentation

Token

Lindera\Token represents a single morphological token.

Token Properties

PropertyTypeDescription
$surfacestringSurface form of the token
$byte_startintStart byte position in the original text
$byte_endintEnd byte position in the original text
$positionintToken position index
$word_idintDictionary word ID
$is_unknownbooltrue if the word is not in the dictionary
$detailsarray<string>Morphological details (part of speech, reading, etc.)

Token Methods

getDetail($index)

Returns the detail string at the specified index, or null if the token has no detail there. A negative index throws a ValueError.

<?php

$token = $tokenizer->tokenize('東京')[0];
$pos = $token->getDetail(0);        // e.g., "名詞"
$subpos = $token->getDetail(1);     // e.g., "固有名詞"
$reading = $token->getDetail(7);    // e.g., "トウキョウ"

Parameters:

NameTypeDescription
$indexintZero-based index into the details array

Returns: string|null

toArray()

Returns the token as an associative array, keeping each field's natural PHP type, so it serializes without a custom encoder.

<?php

$data = $tokenizer->tokenize('東京')[0]->toArray();
// ['surface' => '東京', 'byte_start' => 0, 'byte_end' => 6, 'position' => 0,
//  'word_id' => 12345, 'is_unknown' => false, 'details' => [...]]

echo json_encode(array_map(fn($t) => $t->toArray(), $tokens));

Returns: array with the keys surface, byte_start, byte_end, position, word_id, is_unknown, and details.

[!NOTE] Lindera\Token does not implement JsonSerializable, so passing a token object straight to json_encode does not produce these fields -- call toArray() first. The extension is built with ext-php-rs, whose #[php_class] cannot declare implemented interfaces yet (upstream ext-php-rs#326).

The structure of details depends on the dictionary:

  • IPADIC: [品詞, 品詞細分類1, 品詞細分類2, 品詞細分類3, 活用型, 活用形, 原形, 読み, 発音]
  • UniDic: Detailed morphological features following the UniDic specification
  • ko-dic / CC-CEDICT / Jieba: Dictionary-specific detail formats

Mode

Lindera\Mode is the tokenization mode: normal, or decompose with a Lindera\Penalty. It is the counterpart of the Rust lindera::mode::Mode, and setMode() and the Tokenizer constructor take it.

Creating a Mode

Lindera\Mode has no public constructor: new Lindera\Mode() throws an Exception (You cannot instantiate this class from PHP.). Create a mode with a static factory:

  • Lindera\Mode::normal() -- Normal mode.
  • Lindera\Mode::decompose($penalty) -- Decompose mode with $penalty, a Lindera\Penalty. null (the default) uses the default penalty; a value of another type throws a TypeError.
  • Lindera\Mode::fromStr($name) -- The mode named $name: 'normal', or 'decompose' with the default penalty. The name is case sensitive, as the Rust Mode parses it: any other name, such as 'Normal', throws a ValueError.
<?php

$normal = Lindera\Mode::normal();
$decompose = Lindera\Mode::decompose();  // the default penalty
$custom = Lindera\Mode::decompose(new Lindera\Penalty(kanji_penalty_length_penalty: 0));
$parsed = Lindera\Mode::fromStr('decompose');  // the default penalty

try {
    Lindera\Mode::fromStr('Normal');
} catch (ValueError $e) {
    echo get_class($e), ': ', $e->getMessage(), "\n";
    // ValueError: LinderaError(kind=Mode, source=Invalid mode: Normal)
}

Mode Properties

PropertyTypeDescription
$namestringThe mode name ("normal" or "decompose")
$penaltyPenalty|nullThe penalty of decompose mode; null for normal mode

A mode cannot be changed: assigning a property throws an Exception. $penalty returns a new copy each time it is read, so it is == to the Lindera\Penalty passed to decompose() but not ===.

Mode Methods

MethodReturn TypeDescription
isNormal()booltrue if the mode is normal
isDecompose()booltrue if the mode is decompose
__toString()stringThe mode name, as $name
<?php

$penalty = new Lindera\Penalty(kanji_penalty_length_penalty: 0);
$mode = Lindera\Mode::decompose($penalty);
echo $mode->name, ' ', $mode->penalty->kanji_penalty_length_penalty, "\n";
// decompose 0
var_dump($mode->penalty == $penalty, $mode->penalty === $penalty);
// bool(true)
// bool(false)
echo Lindera\Mode::normal(), "\n";
// normal
var_dump(Lindera\Mode::normal()->penalty);
// NULL

Comparing Modes

== compares the name and the penalty, as == of the Rust Mode does; === holds only for the same object. Because a mode converts to its name, == with a string compares the name alone: a decompose mode with any penalty is == to 'decompose'. An object of another class is never == to a mode.

<?php

$decompose = Lindera\Mode::decompose();
$custom = Lindera\Mode::decompose(new Lindera\Penalty(kanji_penalty_length_penalty: 0));

var_dump(Lindera\Mode::fromStr('decompose') == $decompose);
// bool(true)
var_dump($custom == $decompose);
// bool(false)
var_dump($custom == 'decompose');
// bool(true)
var_dump(Lindera\Mode::normal() === Lindera\Mode::normal());
// bool(false)

Penalty

Lindera\Penalty configures how aggressively decompose mode splits compound words, based on character type and length thresholds.

<?php

// All parameters are optional and default to the values used by decompose mode
$penalty = new Lindera\Penalty(
    kanji_penalty_length_threshold: 2,
    kanji_penalty_length_penalty: 3000,
    other_penalty_length_threshold: 7,
    other_penalty_length_penalty: 1700,
);

Penalty Properties

PropertyTypeDefaultDescription
$kanji_penalty_length_thresholdint2Length threshold for kanji sequences
$kanji_penalty_length_penaltyint3000Penalty applied to kanji sequences exceeding the threshold
$other_penalty_length_thresholdint7Length threshold for other character sequences
$other_penalty_length_penaltyint1700Penalty applied to other character sequences exceeding the threshold

Using a Penalty

Pass a Penalty to Lindera\Mode::decompose(), and the mode to setMode() or to the Tokenizer constructor. Without length penalties, decompose mode keeps long words whole:

<?php

$text = '関西国際空港限定トートバッグ';
$penalty = new Lindera\Penalty(kanji_penalty_length_penalty: 0, other_penalty_length_penalty: 0);
$surfaces = static fn (array $tokens): string => implode(' / ', array_map(static fn ($token) => $token->surface, $tokens));

$builder = new Lindera\TokenizerBuilder();
$builder->setDictionary('embedded://ipadic');
$builder->setMode(Lindera\Mode::decompose());
echo $surfaces($builder->build()->tokenize($text)), "\n";
// 関西 / 国際 / 空港 / 限定 / トートバッグ

$builder->setMode(Lindera\Mode::decompose($penalty));
echo $surfaces($builder->build()->tokenize($text)), "\n";
// 関西国際空港 / 限定 / トートバッグ

$tokenizer = new Lindera\Tokenizer(Lindera\Dictionary::load('embedded://ipadic'), Lindera\Mode::decompose($penalty));
echo $surfaces($tokenizer->tokenize($text)), "\n";
// 関西国際空港 / 限定 / トートバッグ

Dictionary Management

Lindera PHP provides static methods on the Lindera\Dictionary class for loading, building, and managing dictionaries used in morphological analysis.

Loading Dictionaries

System Dictionaries

Use Lindera\Dictionary::load($uri) to load a system dictionary. Download a pre-built dictionary from GitHub Releases and specify the path to the extracted directory:

<?php

$dictionary = Lindera\Dictionary::load('/path/to/ipadic');

Embedded dictionaries (advanced) -- if you built with an embed-* feature flag, you can load an embedded dictionary:

<?php

$dictionary = Lindera\Dictionary::load('embedded://ipadic');

User Dictionaries

User dictionaries add custom vocabulary on top of a system dictionary. Load one for the system dictionary it is used with:

<?php

$dictionary = Lindera\Dictionary::load('/path/to/ipadic');
$userDict = Lindera\Dictionary::loadUserFor('/path/to/user_dictionary.csv', $dictionary);

A .csv is built with the system dictionary's metadata and checked against its connection matrix as it is built: a row whose context ID lies outside the matrix throws a ValueError that names its CSV line and surface (see Context IDs). A .bin is loaded as it was built.

Lindera\Dictionary::loadUser($uri, $metadata) loads a user dictionary with metadata only. A .csv loaded this way is checked when it is combined with the system dictionary, and the error names the entry by its word id:

<?php

$dictionary = Lindera\Dictionary::load('/path/to/ipadic');
$metadata = $dictionary->metadata();
$userDict = Lindera\Dictionary::loadUser('/path/to/user_dictionary.csv', $metadata);

Pass the user dictionary when creating a tokenizer directly:

<?php

$dictionary = Lindera\Dictionary::load('/path/to/ipadic');
$userDict = Lindera\Dictionary::loadUserFor('/path/to/user_dictionary.csv', $dictionary);

$tokenizer = new Lindera\Tokenizer($dictionary, Lindera\Mode::normal(), $userDict);

Or via the builder:

<?php

$builder = new Lindera\TokenizerBuilder();
$builder->setDictionary('/path/to/ipadic');
$builder->setUserDictionary('/path/to/user_dictionary.csv');
$tokenizer = $builder->build();

Building Dictionaries

System Dictionary

Build a system dictionary from source files:

<?php

$metadata = Lindera\Metadata::fromJsonFile('/path/to/metadata.json');
Lindera\Dictionary::build('/path/to/input_dir', '/path/to/output_dir', $metadata);

The input directory should contain the dictionary source files (CSV lexicon, matrix.def, etc.).

Metadata loaded with Lindera\Metadata::fromJsonFile keeps the space_penalty rules of the metadata.json, and the built dictionary ships them. For ko-dic this means the left-space penalty stays on by default in the dictionary you build; turn it off per tokenizer with TokenizerBuilder::setSpacePenalty(false) (see Tokenizer API). Metadata created with the constructor carries no rules.

Here is an example that downloads and builds the IPADIC dictionary:

<?php

$url = 'https://lindera.dev/mecab-ipadic-2.7.0-20070801.tar.gz';
$filename = '/tmp/mecab-ipadic-2.7.0-20070801.tar.gz';

// Download and extract dictionary source
file_put_contents($filename, file_get_contents($url));
$phar = new PharData($filename);
$phar->extractTo('/tmp/', null, true);

// Load metadata and build
$metadata = Lindera\Metadata::fromJsonFile('resources/ipadic_metadata.json');
Lindera\Dictionary::build(
    '/tmp/mecab-ipadic-2.7.0-20070801',
    '/tmp/lindera-ipadic',
    $metadata
);

User Dictionary

Build a user dictionary from a CSV file for the system dictionary it is used with:

<?php

$dictionary = Lindera\Dictionary::load('/path/to/ipadic');
Lindera\Dictionary::buildUserFor('user_words.csv', '/path/to/user_dictionary.bin', $dictionary);
$userDict = Lindera\Dictionary::loadUserFor('/path/to/user_dictionary.bin', $dictionary);

The CSV is built with the system dictionary's metadata and checked against its connection matrix: a row whose context ID lies outside the matrix throws a ValueError that names its CSV line and surface, and no file is written (see Context IDs). The second argument is the path of the file to write. End it with .bin, since the loaders choose how to load a user dictionary by its extension.

Lindera\Dictionary::buildUser($kind, $input_file, $output_dir, $metadata) builds a user dictionary with metadata only. Despite its name, $output_dir is the path of the file to write too, not a directory. The rows are checked when the user dictionary is combined with the system dictionary, and the error names the entry by its word id:

<?php

$metadata = new Lindera\Metadata();
Lindera\Dictionary::buildUser('ipadic', 'user_words.csv', '/path/to/user_dictionary.bin', $metadata);

The first argument ($kind, 'ipadic' above) is currently unused and reserved for future use. To build for a system dictionary, use Lindera\Dictionary::buildUserFor.

Metadata

The Lindera\Metadata class configures dictionary parameters.

Creating Metadata

<?php

// Default metadata
$metadata = new Lindera\Metadata();

// Custom metadata
$metadata = new Lindera\Metadata(
    name: 'my_dictionary',
    encoding: 'UTF-8',
    default_word_cost: -10000,
);

// Create with all defaults explicitly
$metadata = Lindera\Metadata::createDefault();

Loading from JSON

<?php

$metadata = Lindera\Metadata::fromJsonFile('metadata.json');

Properties

PropertyTypeDefaultDescription
namestring"default"Dictionary name
encodingstring"UTF-8"Character encoding
default_word_costint-10000Default cost for unknown words
default_left_context_idint1288Default left context ID
default_right_context_idint1288Default right context ID
default_field_valuestring"*"Default value for missing fields
flexible_csvboolfalseAllow flexible CSV parsing
skip_invalid_cost_or_idboolfalseSkip entries with invalid cost or ID
dictionary_schema_fieldsarray<string>IPADIC schemaSchema fields for the main dictionary
user_dictionary_schema_fieldsarray<string>Minimal schemaSchema fields for user dictionaries

All properties are read-only via getter methods:

<?php

$metadata = new Lindera\Metadata(name: 'custom_dict', encoding: 'EUC-JP');
echo $metadata->name;      // "custom_dict"
echo $metadata->encoding;  // "EUC-JP"

toArray()

Returns an associative array representation of the metadata:

<?php

$metadata = new Lindera\Metadata(name: 'test');
print_r($metadata->toArray());

When the metadata carries left-space penalty rules (for example, loaded from ko-dic's metadata.json), the array also has a space_penalty entry holding them as a JSON string; the key is absent otherwise:

<?php

$metadata = Lindera\Metadata::fromJsonFile('/path/to/ko-dic/metadata.json');
$rules = json_decode($metadata->toArray()['space_penalty'], true);
// ['rules' => [['pos' => ['EC', 'EF', ...], 'cost' => 3000], ...]]

Dictionary Info

The Lindera\Dictionary object provides metadata accessors:

<?php

$dictionary = Lindera\Dictionary::load('/path/to/ipadic');
echo $dictionary->metadataName();      // Dictionary name
echo $dictionary->metadataEncoding();  // Dictionary encoding
$metadata = $dictionary->metadata();   // Full Metadata object

Version

Retrieve the Lindera library version:

<?php

echo Lindera\Dictionary::version();

Schema

The Lindera\Schema class defines the field layout of a dictionary.

Creating a Schema

<?php

// Default schema (IPADIC-compatible)
$schema = Lindera\Schema::createDefault();

// Custom schema
$schema = new Lindera\Schema(['surface', 'pos']);

Schema Methods

MethodReturn TypeDescription
fieldCount()intReturns the number of fields
getFieldIndex($name)intReturns the field's index (-1 if not found)
getFieldByName($name)FieldDefinition|nullReturns the field's definition
getCustomFields()array<string>Returns the custom field names
validateRecord($record)voidValidates that a record conforms to the schema

Schema Properties

PropertyTypeDescription
$fieldsarray<string>The field names

Text Processing Pipeline

Lindera PHP supports a composable text processing pipeline that applies character filters before tokenization and token filters after tokenization. Filters are added to the TokenizerBuilder and executed in the order they are appended.

Input Text
  --> Character Filters (preprocessing)
  --> Tokenization
  --> Token Filters (postprocessing)
  --> Output Tokens

[!NOTE] This page shows a few commonly used filters as examples -- it is not the complete list. See Filters for the full, authoritative catalogue of every character and token filter that lindera-analysis ships, including parameters and examples.

Character Filters

Character filters transform the input text before tokenization.

unicode_normalize

Applies Unicode normalization to the input text.

<?php

$builder = new Lindera\TokenizerBuilder();
$builder->setDictionary('embedded://ipadic');
$builder->appendCharacterFilter('unicode_normalize', ['kind' => 'nfkc']);
$tokenizer = $builder->build();

Supported normalization forms: "nfc", "nfkc", "nfd", "nfkd".

mapping

Replaces characters or strings according to a mapping table.

<?php

$builder = new Lindera\TokenizerBuilder();
$builder->setDictionary('embedded://ipadic');
$builder->appendCharacterFilter('mapping', [
    'mapping' => [
        'リンデラ' => 'lindera',
    ],
]);
$tokenizer = $builder->build();

japanese_iteration_mark

Resolves Japanese iteration marks (odoriji) into their full forms.

<?php

$builder = new Lindera\TokenizerBuilder();
$builder->setDictionary('embedded://ipadic');
$builder->appendCharacterFilter('japanese_iteration_mark', [
    'normalize_kanji' => 'true',
    'normalize_kana' => 'true',
]);
$tokenizer = $builder->build();

Token Filters

Token filters transform or remove tokens after tokenization.

lowercase

Converts token surface forms to lowercase.

<?php

$builder = new Lindera\TokenizerBuilder();
$builder->setDictionary('embedded://ipadic');
$builder->appendTokenFilter('lowercase');
$tokenizer = $builder->build();

japanese_base_form

Replaces inflected forms with their base (dictionary) form using the morphological details from the dictionary.

<?php

$builder = new Lindera\TokenizerBuilder();
$builder->setDictionary('embedded://ipadic');
$builder->appendTokenFilter('japanese_base_form', []);
$tokenizer = $builder->build();

japanese_katakana_stem

Removes the trailing long sound mark from katakana words to normalize spelling variants, stemming only words at least min characters long.

<?php

$builder = new Lindera\TokenizerBuilder();
$builder->setDictionary('embedded://ipadic');
$builder->appendTokenFilter('japanese_katakana_stem', ['min' => 3]);
$tokenizer = $builder->build();

japanese_stop_tags

Removes tokens whose part-of-speech matches any of the specified tags.

<?php

$builder = new Lindera\TokenizerBuilder();
$builder->setDictionary('embedded://ipadic');
$builder->appendTokenFilter('japanese_stop_tags', [
    'tags' => ['助詞,格助詞,一般', '助詞,係助詞', '助詞,連体化', '助動詞'],
]);
$tokenizer = $builder->build();

[!NOTE] Tags are normalized to exactly four comma-separated levels (missing levels are padded with *) and compared for exact equality against the four part-of-speech details of each token, starting at the field named part_of_speech in the dictionary schema (or at the first detail when the schema has no such field). A bare 助詞 therefore never matches IPADIC particle tokens — they always carry a subcategory such as 助詞,係助詞 — while a bare 助動詞 does match, because auxiliary verbs have no subcategory (助動詞,*,*,*).

japanese_keep_tags

Keeps only tokens whose part-of-speech matches one of the specified tags. All other tokens are removed. The following keeps only general nouns:

<?php

$builder = new Lindera\TokenizerBuilder();
$builder->setDictionary('embedded://ipadic');
$builder->appendTokenFilter('japanese_keep_tags', [
    'tags' => ['名詞,一般'],
]);
$tokenizer = $builder->build();

Complete Pipeline Example

The following example combines multiple character filters and token filters into a single pipeline:

<?php

$builder = new Lindera\TokenizerBuilder();

// Set mode and dictionary
$builder->setMode(Lindera\Mode::normal());
$builder->setDictionary('embedded://ipadic');

// Preprocessing
$builder->appendCharacterFilter('unicode_normalize', ['kind' => 'nfkc']);
$builder->appendCharacterFilter(
    'japanese_iteration_mark',
    ['normalize_kanji' => 'true', 'normalize_kana' => 'true']
);
$builder->appendCharacterFilter('mapping', ['mapping' => ['リンデラ' => 'lindera']]);

// Postprocessing
$builder->appendTokenFilter('japanese_katakana_stem', ['min' => 3]);
$builder->appendTokenFilter('japanese_stop_tags', [
    'tags' => [
        '接続詞',
        '助詞',
        '助詞,格助詞,一般',
        '助詞,格助詞,一般',
        '助詞,係助詞',
        '助詞,副助詞',
        '助詞,終助詞',
        '助詞,連体化',
        '助動詞',
        '記号',
        '記号,一般',
        '記号,読点',
        '記号,句点',
        '記号,空白',
    ],
]);
$builder->appendTokenFilter('lowercase');

// Build the tokenizer
$tokenizer = $builder->build();

// Tokenize
$text = 'Linderaは形態素解析エンジンです。';
$tokens = $tokenizer->tokenize($text);

foreach ($tokens as $token) {
    echo $token->surface . "\t" . implode(',', $token->details) . "\n";
}

In this pipeline:

  1. unicode_normalize converts full-width characters to half-width (NFKC normalization)
  2. japanese_iteration_mark resolves iteration marks
  3. mapping replaces the specified strings
  4. japanese_katakana_stem stems katakana words
  5. japanese_stop_tags removes particles, auxiliary verbs, and symbols
  6. lowercase normalizes alphabetic characters to lowercase

Training

Lindera PHP supports training custom CRF-based morphological analysis models from annotated corpora. This functionality requires the train feature.

Prerequisites

Build lindera-php with the train feature enabled (train is already enabled by default in lindera-php):

cargo build -p lindera-php --features train,embed-ipadic

Training a Model

Use Lindera\Trainer::train() to train a CRF model from a seed lexicon and annotated corpus:

<?php

Lindera\Trainer::train(
    seed: 'resources/training/seed.csv',
    corpus: 'resources/training/corpus.txt',
    char_def: 'resources/training/char.def',
    unk_def: 'resources/training/unk.def',
    feature_def: 'resources/training/feature.def',
    rewrite_def: 'resources/training/rewrite.def',
    output: '/tmp/model.dat',
    lambda: 1.0,
    max_iter: 100,
    max_threads: null,
);

Training Parameters

ParameterTypeDefaultDescription
$seedstringrequiredPath to the seed lexicon file (CSV format)
$corpusstringrequiredPath to the annotated training corpus
$char_defstringrequiredPath to the character definition file (char.def)
$unk_defstringrequiredPath to the unknown word definition file (unk.def)
$feature_defstringrequiredPath to the feature definition file (feature.def)
$rewrite_defstringrequiredPath to the rewrite rule definition file (rewrite.def)
$outputstringrequiredOutput path for the trained model file
$lambdafloat1.0Regularization coefficient, finite and >= 0; 0 disables regularization for every type; with L2, MeCab's -c C is 1 / C, so 1.0 is MeCab's -c 1.0
$max_iterint100Maximum number of training iterations, 1 or more; training stops earlier once the objective converges (see $eta)
$max_threadsint|nullnullNumber of threads, 1 or more (null = auto-detect CPU cores)
$regularizationstring|null'l2'Regularization type: 'l1', 'l2' or 'elasticnet', in any case ('elastic_net' and 'elastic-net' also select Elastic Net), as lindera train --regularization takes it
$elastic_net_l1_ratiofloat|null0.5L1 ratio of Elastic Net regularization, between 0 and 1; 0 uses only L2, 1 uses only L1; used only with 'elasticnet'
$etafloat|null5e-5Tolerance of the convergence test, finite and >= 0: training stops before $max_iter once the relative change of the objective stays below it for 3 iterations in a row; 0 disables the test

null selects the default of each optional parameter. The settings are checked before any file is read: an unknown $regularization, or a $lambda, $elastic_net_l1_ratio or $eta outside its range, throws a ValueError. To train with v6's default settings (the model still differs from v6's, since the first line search changed in #1290), pass regularization: 'l1' and lambda: 0.01, and eta: 0 to stop only at $max_iter.

Exporting a Trained Model

After training, export the model to dictionary source files using Lindera\Trainer::export():

<?php

Lindera\Trainer::export(
    model: '/tmp/model.dat',
    output: '/tmp/dictionary_source',
    metadata: 'resources/training/metadata.json',
);

Export Parameters

ParameterTypeDefaultDescription
$modelstringrequiredPath to the trained model file (.dat)
$outputstringrequiredOutput directory for dictionary source files
$metadatastring|nullnullPath to a base metadata.json file

The export creates the following files in the output directory, the same files that lindera export creates:

  • lex.csv -- Lexicon entries with trained costs
  • matrix.def -- Connection cost matrix
  • unk.def -- Unknown word definitions
  • char.def -- Character category definitions (copied from the trained model)
  • feature.def -- Feature template definitions (copied from the trained model)
  • rewrite.def -- Feature rewrite rules (copied from the trained model)
  • left-id.def -- Left context ID to feature string mapping
  • right-id.def -- Right context ID to feature string mapping
  • metadata.json -- Updated metadata (when $metadata parameter is provided)

The model file must come from this version's training. A model file written by an earlier version, which includes every model file written before 7.0.0, is rejected: Lindera\Trainer::export() fails with an error that asks you to re-run training. Train such a model again with Lindera\Trainer::train().

Complete Workflow

The full workflow for training and using a custom dictionary:

<?php

// Step 1: Train the CRF model
Lindera\Trainer::train(
    seed: 'resources/training/seed.csv',
    corpus: 'resources/training/corpus.txt',
    char_def: 'resources/training/char.def',
    unk_def: 'resources/training/unk.def',
    feature_def: 'resources/training/feature.def',
    rewrite_def: 'resources/training/rewrite.def',
    output: '/tmp/model.dat',
    lambda: 1.0,
    max_iter: 100,
);

// Step 2: Export to dictionary source files
Lindera\Trainer::export(
    model: '/tmp/model.dat',
    output: '/tmp/dictionary_source',
    metadata: 'resources/training/metadata.json',
);

// Step 3: Build the dictionary from exported source files
$metadata = Lindera\Metadata::fromJsonFile('/tmp/dictionary_source/metadata.json');
Lindera\Dictionary::build('/tmp/dictionary_source', '/tmp/dictionary', $metadata);

// Step 4: Use the trained dictionary
$builder = new Lindera\TokenizerBuilder();
$builder->setDictionary('/tmp/dictionary');
$builder->setMode(Lindera\Mode::normal());
$tokenizer = $builder->build();

$tokens = $tokenizer->tokenize('形態素解析のテスト');
foreach ($tokens as $token) {
    echo $token->surface . "\t" . implode(',', $token->details) . "\n";
}

Lindera WASM

Lindera WASM provides WebAssembly bindings for Lindera's morphological analysis engine, built with wasm-bindgen. It enables Japanese, Korean, and Chinese text tokenization directly in web browsers, Node.js, and bundler environments.

Distribution Formats

Lindera WASM supports multiple distribution formats via wasm-pack:

TargetUse CaseModule System
webBrowser ESMES Modules
bundlerWebpack, Vite, RollupES Modules (bundler-resolved)

Dictionary Packages

Each package embeds a specific dictionary for offline use:

Feature FlagDictionaryLanguage
(none)No embedded dictionary--
embed-ipadicIPADICJapanese
embed-unidicUniDicJapanese
embed-ko-dicko-dicKorean
embed-cc-cedictCC-CEDICTChinese
embed-jiebaJiebaChinese
embed-cjkIPADIC + ko-dic + JiebaCJK

Sections

Installation

Prerequisites

Obtaining Dictionaries

Lindera WASM does not bundle dictionaries by default. The recommended approach for browser environments is to download dictionaries at runtime using the OPFS (Origin Private File System) API.

Download from GitHub Releases

Pre-built dictionaries are available on the GitHub Releases page. In browser environments, use the OPFS helpers to download and cache dictionaries:

import { downloadDictionary, hasDictionary } from 'lindera-wasm/opfs';

if (!await hasDictionary("ipadic")) {
    await downloadDictionary(
        "https://github.com/lindera/lindera/releases/download/<version>/lindera-ipadic-<version>.zip",
        "ipadic",
    );
}

See OPFS Dictionary Storage for the full workflow.

Building with wasm-pack

Build the WASM package (the published lindera-wasm npm package is built with the web target):

wasm-pack build --target web

The output is written to the pkg/ directory inside the lindera-wasm crate. wasm-pack also supports other targets (e.g. --target nodejs) if you need a custom build, but the web-target build is what this documentation assumes.

Why the web Target (and Not bundler)?

wasm-pack's default --target is bundler, yet the published lindera-wasm package is deliberately built with --target web. The target names are somewhat misleading, so here is what each one actually produces:

  • --target bundler (the wasm-pack default) emits JavaScript that imports the .wasm file as an ES module. This relies on the WebAssembly ESM integration proposal, which is not yet a finalized web standard; in practice the only bundler that understands the output is Webpack with the asyncWebAssembly experiment enabled. The default dates from the era when Webpack was the dominant bundler -- despite the generic name, the output is effectively Webpack-specific. Vite, for example, needs extra plugins (vite-plugin-wasm plus top-level-await support) to consume it.
  • --target web emits standards-based code only: the .wasm file is resolved via new URL('lindera_wasm_bg.wasm', import.meta.url) and loaded with fetch + WebAssembly.instantiateStreaming inside an explicit async init function. This runs in browsers as native ES modules with no build step at all, and modern bundlers (Vite, Webpack 5, Rollup) recognize the new URL(..., import.meta.url) pattern and ship the .wasm file as an asset. The one cost is that you must call await __wbg_init() once before using the API.

In short, the bundler target is zero-config for Webpack only, while the web target runs everywhere at the cost of one explicit init call. Up to v5, Lindera published both variants (lindera-wasm-web and lindera-wasm-bundler); v6 consolidates on the single portable build. If you are migrating from lindera-wasm-bundler, see the v5 to v6 migration guide.

Available Feature Flags (Advanced)

For advanced users who want to embed dictionaries directly into the WASM binary, the following feature flags are available. This increases the binary size significantly but eliminates the need to download dictionaries at runtime.

FeatureDictionaryLanguage
embed-ipadicIPADICJapanese
embed-unidicUniDicJapanese
embed-sudachidictSudachiDictJapanese (note: ~570MB, generally impractical for WASM)
embed-ko-dicko-dicKorean
embed-cc-cedictCC-CEDICTChinese
embed-jiebaJiebaChinese
embed-cjkIPADIC + ko-dic + JiebaCJK (all)

You can combine multiple dictionaries by enabling multiple feature flags:

wasm-pack build --target web --features embed-ipadic,embed-ko-dic

NPM Package Naming Convention

The unscoped lindera-wasm-* prefix is the Lindera project's namespace. If you publish a custom build with embedded dictionaries to npm, use a name under your own scope so it cannot be mistaken for an official package:

@your-scope/lindera-wasm-{dict}

Examples:

  • @your-scope/lindera-wasm-ipadic
  • @your-scope/lindera-wasm-unidic
  • @your-scope/lindera-wasm-cjk

To set the package name before publishing, edit the name field in the generated pkg/package.json. Note that a package with an embedded dictionary redistributes that dictionary, so follow the dictionary's license terms (attribution and so on) as well.

[!NOTE] This project's own release workflow (.github/workflows/release.yml) only builds and publishes a single package to npm -- lindera-wasm, built with wasm-pack --target web and without any embed-* feature. Unscoped dictionary-suffixed names such as lindera-wasm-ipadic are reserved by the project (leftovers from pre-v3 releases, no longer updated) and must not be claimed by third parties; in this documentation they only illustrate what a local build with an embed-* feature (see Available Feature Flags above) would produce.

Installing from npm

The pre-built package is available on npm:

npm install lindera-wasm

Or with yarn:

yarn add lindera-wasm

Because the package is a web-target build, call the default-exported async init function once before using any API:

import __wbg_init, { TokenizerBuilder } from 'lindera-wasm';

await __wbg_init();

This applies both in browsers (native ES modules) and when the package is consumed through a bundler -- modern bundlers such as Vite or Webpack 5 (with the asyncWebAssembly experiment) handle the web-target build directly; see Browser Usage for bundler configuration.

[!NOTE] The npm package does not include dictionaries. Use the OPFS helpers to download dictionaries at runtime. See OPFS Dictionary Storage.

Quick Start

Web (Browser) -- OPFS Dictionary Loading

The recommended approach is to download dictionaries at runtime using the OPFS helpers:

import __wbg_init, { Mode, TokenizerBuilder, loadDictionaryFromBytes } from 'lindera-wasm';
import { downloadDictionary, loadDictionaryFiles, hasDictionary } from 'lindera-wasm/opfs';

async function main() {
    await __wbg_init();

    // Download dictionary if not cached
    if (!await hasDictionary("ipadic")) {
        await downloadDictionary(
            "https://github.com/lindera/lindera/releases/download/<version>/lindera-ipadic-<version>.zip",
            "ipadic",
        );
    }

    // Load dictionary from OPFS
    const files = await loadDictionaryFiles("ipadic");
    const dictionary = loadDictionaryFromBytes(
        files.metadata, files.dictTrie, files.dictValsIdx, files.dictVals,
        files.dictWordsIdx, files.dictWords, files.matrixMtx, files.charDef, files.unk,
    );

    // Build tokenizer
    const builder = new TokenizerBuilder();
    builder.setDictionaryInstance(dictionary);
    builder.setMode(Mode.normal());
    const tokenizer = builder.build();

    const tokens = tokenizer.tokenize("関西国際空港限定トートバッグ");
    tokens.forEach(token => {
        console.log(`${token.surface}\t${token.details.join(',')}`);
    });
}

main();

Note: Download a pre-built dictionary from GitHub Releases. See OPFS Dictionary Storage for the full workflow.

Expected output:

関西国際空港    名詞,固有名詞,組織,*,*,*,関西国際空港,カンサイコクサイクウコウ,カンサイコクサイクーコー
限定    名詞,サ変接続,*,*,*,*,限定,ゲンテイ,ゲンテイ
トートバッグ    名詞,一般,*,*,*,*,*,*,*

Using Embedded Dictionaries (Advanced)

If you built with an embed-* feature flag, you can use embedded dictionaries:

[!NOTE] lindera-wasm-ipadic is an illustrative package name here, not something published to npm. Only lindera-wasm is actually published; see NPM Package Naming Convention for how to build and name a package like this yourself.

import __wbg_init, { Mode, TokenizerBuilder } from 'lindera-wasm-ipadic';

async function main() {
    await __wbg_init();

    const builder = new TokenizerBuilder();
    builder.setDictionary("embedded://ipadic");
    builder.setMode(Mode.normal());
    const tokenizer = builder.build();

    const tokens = tokenizer.tokenize("関西国際空港限定トートバッグ");
    tokens.forEach(token => {
        console.log(`${token.surface}\t${token.details.join(',')}`);
    });
}

main();

Using Filters

You can add character filters and token filters to the tokenization pipeline:

import __wbg_init, { Mode, TokenizerBuilder, loadDictionaryFromBytes } from 'lindera-wasm';
import { loadDictionaryFiles } from 'lindera-wasm/opfs';

async function main() {
    await __wbg_init();

    // Assume dictionary is already cached in OPFS
    const files = await loadDictionaryFiles("ipadic");
    const dictionary = loadDictionaryFromBytes(
        files.metadata, files.dictTrie, files.dictValsIdx, files.dictVals,
        files.dictWordsIdx, files.dictWords, files.matrixMtx, files.charDef, files.unk,
    );

    const builder = new TokenizerBuilder();
    builder.setDictionaryInstance(dictionary);
    builder.setMode(Mode.normal());

    // Add Unicode NFKC normalization
    builder.appendCharacterFilter("unicode_normalize", { kind: "nfkc" });

    // Add a stop-tags filter to remove particles and auxiliary verbs
    builder.appendTokenFilter("japanese_stop_tags", {
        tags: ["助詞,格助詞,一般", "助詞,係助詞", "助詞,連体化", "助動詞"]
    });

    const tokenizer = builder.build();
    const tokens = tokenizer.tokenize("Linderaは形態素解析エンジンです");
    tokens.forEach(token => {
        console.log(`${token.surface}\t${token.details.join(',')}`);
    });
}

main();

N-Best Tokenization

Retrieve multiple tokenization candidates ranked by cost:

const results = tokenizer.tokenizeNbest("すもももももももものうち", 3);
results.forEach((result, rank) => {
    console.log(`--- NBEST ${rank + 1} (cost=${result.cost}) ---`);
    result.tokens.forEach(token => {
        console.log(`${token.surface}\t${token.details.join(',')}`);
    });
});

Tokenizer API

This page documents the JavaScript/TypeScript API exposed by lindera-wasm.

TokenizerBuilder

Builder class for creating a configured Tokenizer instance.

Constructor

const builder = new TokenizerBuilder();

Creates a new builder with default settings.

Methods

Every setter returns a builder handle that shares the same configuration, so the calls can be chained (builder.setMode(Mode.normal()).setDictionary(...)) or made one statement at a time. The returned handle is a new object, but it configures the same underlying builder.

setMode(mode)

Sets the tokenization mode. A builder that is not given one uses normal mode.

  • Parameters: mode (Mode) -- The Mode: Mode.normal(), or Mode.decompose() with the Penalty of decompose mode. The Mode stays usable, so the same one can be passed again.
  • Returns: TokenizerBuilder

A value that is not a Mode, such as a mode name, throws Error("expected instance of Mode"). To select a mode by its name, create it with Mode.fromStr().

builder.setMode(Mode.normal());
builder.setMode(Mode.decompose(new Penalty(undefined, 0)));
builder.setMode(Mode.fromStr("decompose"));

setDictionary(uri)

Sets the dictionary to use for tokenization. It replaces a dictionary set with setDictionaryInstance().

  • Parameters: uri (string) -- Dictionary URI (e.g., "embedded://ipadic")
  • Returns: TokenizerBuilder
builder.setDictionary("embedded://ipadic");

setDictionaryInstance(dictionary)

Sets a pre-loaded dictionary instance for tokenization. Use this when the dictionary has been loaded from bytes (e.g., via loadDictionaryFromBytes()) instead of from a URI. It replaces a dictionary set with setDictionary(): whichever of the two is called last sets the dictionary. Every other setting of the builder (the mode, setUserDictionaryInstance(), setKeepWhitespace(), setSkipWhitespace(), setSpacePenalty() and the appended filters) applies to it as to a dictionary set by URI.

  • Parameters: dictionary (Dictionary) -- A loaded dictionary object. It stays usable, so the same one can be passed to several tokenizers and builders.
  • Returns: TokenizerBuilder
import { loadDictionaryFromBytes } from 'lindera-wasm';
import { loadDictionaryFiles } from 'lindera-wasm/opfs';

const files = await loadDictionaryFiles("ipadic");
const dictionary = loadDictionaryFromBytes(
    files.metadata, files.dictTrie, files.dictValsIdx, files.dictVals,
    files.dictWordsIdx, files.dictWords, files.matrixMtx, files.charDef, files.unk,
);

builder.setDictionaryInstance(dictionary);

setUserDictionaryInstance(userDictionary)

Sets a pre-loaded user dictionary instance. Load one from bytes with loadUserDictionaryFromBytesFor() or loadUserDictionaryFromBytes() (CSV) or loadUserDictionaryBinFromBytes() (prebuilt .bin); URI-based user dictionaries are not available on WebAssembly. The user dictionary applies to a dictionary set with either setDictionary() or setDictionaryInstance().

  • Parameters: userDictionary (UserDictionary) -- A loaded user dictionary object. It stays usable, so the same one can be passed to several tokenizers and builders.
  • Returns: TokenizerBuilder

setKeepWhitespace(keep)

Sets whether whitespace tokens are preserved in the output. Kept whitespace also stays in the lattice, so the other tokens can differ from those of the default output, which skips whitespace in the lattice as MeCab does. To keep whitespace in the lattice without outputting it, use setSkipWhitespace(false) instead.

  • Parameters: keep (boolean) -- true to keep whitespace tokens
  • Returns: TokenizerBuilder
builder.setKeepWhitespace(true);

setSkipWhitespace(skip)

Sets whether whitespace is skipped in the lattice, with the same meaning as segmenter.skip_whitespace in a configuration file. true skips whitespace, so the words on either side of it connect directly, as MeCab does. false keeps whitespace in the lattice as a node of its own (a whitespace dictionary entry or the SPACE unknown word), as before v7; whitespace is still dropped from the output. null or undefined goes back to the default of the dictionary's metadata.json: skipping, except with SudachiDict.

A value set here overrides the dictionary's default, for a dictionary set with setDictionary() or setDictionaryInstance(). It has no effect while whitespace tokens are kept (setKeepWhitespace(true)), because kept whitespace always stays in the lattice. With IPADIC, for example, Google が 新しい reads が as a particle (助詞) by default and as a conjunction (接続詞) with setSkipWhitespace(false). See Segmenter for details.

  • Parameters: skip (boolean | null | undefined) -- The setting; null or undefined for the dictionary's default. The argument is required in the TypeScript declaration. Other values count by JavaScript truthiness.
  • Returns: TokenizerBuilder
// Keep whitespace in the lattice, as before v7
builder.setSkipWhitespace(false);

setSpacePenalty(value)

Sets the left-space penalty for Korean: a candidate that starts right after whitespace and whose part-of-speech tag normally attaches to the preceding word (particles, endings, ...) gets a cost added.

  • Parameters: value (boolean | object | null | undefined) -- The setting, with the same meaning as segmenter.space_penalty in a configuration file
  • Returns: TokenizerBuilder

value takes one of these forms; any other value throws an error string:

  • null or undefined -- The default: the rules the dictionary ships in its metadata.json, if any (ko-dic ships mecab-ko-dic's rules; the other bundled dictionaries ship none). Use it to undo an earlier call.
  • false -- Turns the penalty off.
  • true -- Requires the dictionary's rules; build() throws for a dictionary that ships none.
  • An object of the form { rules: [{ pos: [...], cost: n }, ...] } -- Applies these rules instead. A candidate whose first part-of-speech tag is listed in pos gets cost added; the first matching rule wins.

The setting applies both to a dictionary set with setDictionary() and to one set with setDictionaryInstance(), such as a ko-dic loaded from OPFS with loadDictionaryFromBytes(). The dictionary object itself is not changed. The Tokenizer constructor takes the same setting as its spacePenalty argument.

// Turn the penalty off
builder.setSpacePenalty(false);

// Explicit rules
builder.setSpacePenalty({
    rules: [
        { pos: ["EC", "EF", "EP", "ETM", "ETN", "VCP", "XSA", "XSN", "XSV"], cost: 3000 },
        { pos: ["JC", "JKB", "JKC", "JKG", "JKO", "JKQ", "JKS", "JKV", "JX"], cost: 6000 },
    ],
});

[!NOTE] Since v6.1.0, ko-dic applies the left-space penalty by default, as mecab-ko does. For example, 검색 이 잘 된다 reads 이 as the interjection IC; without the penalty, as the subject particle JKS. Call builder.setSpacePenalty(false) to turn it off. Only a ko-dic built by Lindera 6.1.0 or later ships the rules; with a ko-dic from the v6.0.0 release the penalty stays off and true fails until the dictionary is rebuilt. See Segmenter for details.

appendCharacterFilter(name, args)

Appends a character filter to the preprocessing pipeline.

  • Parameters:
    • name (string) -- Filter name (e.g., "unicode_normalize", "japanese_iteration_mark")
    • args (object, optional) -- Filter configuration
  • Returns: TokenizerBuilder
builder.appendCharacterFilter("unicode_normalize", { kind: "nfkc" });

appendTokenFilter(name, args)

Appends a token filter to the postprocessing pipeline.

  • Parameters:
    • name (string) -- Filter name (e.g., "japanese_stop_tags", "lowercase")
    • args (object, optional) -- Filter configuration
  • Returns: TokenizerBuilder
builder.appendTokenFilter("japanese_stop_tags", {
    tags: ["助詞,格助詞,一般", "助詞,係助詞", "助詞,連体化", "助動詞", "記号,句点", "記号,読点"]
});

The arguments of setSpacePenalty, appendCharacterFilter and appendTokenFilter may nest arrays, objects and Maps at most 128 levels deep. A deeper value, or one that contains itself, throws an error string.

build()

Builds and returns a configured Tokenizer instance. Every setting applies whether the dictionary was set with setDictionary() or with setDictionaryInstance(). The builder remains usable afterwards, so multiple tokenizers can be built from the same configuration.

  • Returns: Tokenizer
const tokenizer = builder.build();

Tokenizer

The main tokenizer class. Can be created via TokenizerBuilder.build() or directly via the constructor.

Tokenizer Constructor

const tokenizer = new Tokenizer(dictionary, mode, userDictionary, spacePenalty);
  • Parameters:
    • dictionary (Dictionary) -- A loaded dictionary object. It stays usable, so the same one can be passed to several tokenizers and builders.
    • mode (Mode, optional) -- The Mode, as setMode() takes it. null or undefined (the default) is normal mode. The Mode stays usable, so the same one can be passed to several tokenizers and builders. A value that is not a Mode throws Error("expected instance of Mode").
    • userDictionary (UserDictionary, optional) -- A loaded user dictionary, or null or undefined (the default) for none. It stays usable, as dictionary does. A value that is not a UserDictionary throws Error("expected instance of UserDictionary").
    • spacePenalty (boolean | object, optional) -- The left-space penalty, in the forms setSpacePenalty() accepts. null or undefined (the default) uses the rules the dictionary ships. An invalid setting throws an error string, as setSpacePenalty() and build() do.

Tokenizer Methods

tokenize(text)

Tokenizes the input text.

  • Parameters: text (string) -- Text to tokenize
  • Returns: Token[] -- Array of token objects
const tokens = tokenizer.tokenize("関西国際空港");

tokenizeSurfaces(text)

Tokenizes the input text and returns only the token surfaces. This is the fast path for wakati-style use: no token objects are created and no morphological details are loaded, so it is significantly faster than tokenize when only the surface strings are needed. The result equals tokenizer.tokenize(text).map((t) => t.surface). (Unrelated to Web Workers.)

  • Parameters: text (string) -- Text to tokenize
  • Returns: string[] -- Array of surface strings
const surfaces = tokenizer.tokenizeSurfaces("関西国際空港");
// ["関西国際空港"]

tokenizeNbest(text, n, unique?, costThreshold?)

Returns N-best tokenization results ordered by total path cost.

  • Parameters:
    • text (string) -- Text to tokenize
    • n (number) -- Number of results to return, an integer of 0 or more
    • unique (boolean, optional) -- Deduplicate results with identical segmentation (default: false)
    • costThreshold (bigint, optional) -- Only return paths within bestCost + threshold; it must fit in 64 bits
  • Returns: Array of { tokens: Token[], cost: number }
const results = tokenizer.tokenizeNbest("すもももももももものうち", 3);

// With a cost threshold -- note that it must be passed as a bigint literal
const resultsWithThreshold = tokenizer.tokenizeNbest("すもももももももものうち", 3, false, 100n);

Token

A single token produced by the tokenizer, as a plain JavaScript object.

Properties

PropertyTypeDescription
surfacestringSurface form of the token
byteStartnumberStart byte offset in the original text
byteEndnumberEnd byte offset in the original text
positionnumberPosition index of the token
wordIdnumberWord ID in the dictionary
isUnknownbooleanWhether the token is an unknown word
detailsstring[]Morphological detail fields

[!NOTE] Tokens are plain objects, not wasm_bindgen class instances. A class instance keeps its data on the Rust heap until the JavaScript side drops it, so a synchronous loop that tokenizes without yielding accumulates memory; nothing is allocated on the Rust side for a plain object, and the result passes through JSON.stringify, structuredClone, and worker transfer without conversion. Field names are camelCase, matching the lindera-nodejs binding.

Reading Details

Index details directly. Out-of-range indexes yield undefined.

const pos = token.details[0];     // e.g., "名詞"
const reading = token.details[7]; // e.g., "トウキョウ"

Serializing a Token

A token is already a plain object, so it serializes as-is.

console.log(JSON.stringify(token, null, 2));

Helper Functions

[!NOTE] The examples below import from lindera-wasm-ipadic, an illustrative package name for a local build with the embed-ipadic feature -- it is not published to npm. Only lindera-wasm is actually published; see NPM Package Naming Convention.

loadDictionary(uri)

Loads a dictionary from the specified URI.

  • Parameters: uri (string) -- Dictionary URI (e.g., "embedded://ipadic")
  • Returns: Dictionary
import { loadDictionary } from 'lindera-wasm-ipadic';

const dict = loadDictionary("embedded://ipadic");

loadUserDictionaryFromBytes(csv, metadata)

Builds a user dictionary from CSV bytes (UTF-8), obtained via fetch, a file input, or OPFS.

  • Parameters:
    • csv (Uint8Array) -- The user dictionary CSV content
    • metadata (Metadata) -- Metadata of the system dictionary the user dictionary will be used with (e.g. dictionary.metadata). It stays usable, so the same one can be passed again.
  • Returns: UserDictionary

loadUserDictionaryFromBytesFor(csv, dictionary)

Builds a user dictionary from CSV bytes (UTF-8) for a loaded system dictionary: the CSV is built with the dictionary's metadata and checked against its connection matrix, so a row whose context ID lies outside the matrix fails with an error that names its CSV line and surface. The dictionary is borrowed and stays usable.

  • Parameters:
    • csv (Uint8Array) -- The user dictionary CSV content
    • dictionary (Dictionary) -- The system dictionary the user dictionary will be used with
  • Returns: UserDictionary

loadUserDictionaryBinFromBytes(bytes)

Loads a prebuilt user dictionary (the output of lindera build --user) from bytes.

  • Parameters: bytes (Uint8Array) -- The .bin content
  • Returns: UserDictionary

version() / getVersion()

Returns the version string of the lindera-wasm package.

  • Returns: string
import { version } from 'lindera-wasm-ipadic';

console.log(version()); // e.g., "7.0.0"

Utility Classes

Mode

The tokenization mode: normal, or decompose with a Penalty. It is the counterpart of the Rust Mode (see Tokenization Modes), and setMode() and the Tokenizer constructor take it.

Mode has no public constructor: new Mode() is a TypeScript error, and in JavaScript it creates an object whose methods throw. Create a mode with a static method instead:

Static methodDescription
Mode.normal()Normal mode: standard tokenization based on dictionary cost
Mode.decompose(penalty?)Decompose mode: compound words are decomposed using penalty-based segmentation. penalty is a Penalty; null or undefined (the default) uses the default penalty. A value that is not a Penalty throws an error string (penalty must be a Penalty)
Mode.fromStr(name)The mode named "normal" or "decompose" (with the default penalty). The name is case sensitive; any other name throws an error string that contains Invalid mode
Property / MethodTypeDescription
namestring"normal" or "decompose" (read-only)
penaltyPenalty or undefinedA copy of the penalty of decompose mode, or undefined for normal mode (read-only)
isNormal()booleanWhether this is normal mode
isDecompose()booleanWhether this is decompose mode
equals(other)booleanWhether other has the same name and penalty. A value that is not a Mode throws Error("expected instance of Mode")
toString()stringThe name, as name returns it

A Mode cannot be changed. Mode.decompose() keeps its own copy of the Penalty, and penalty returns another copy, so changing either Penalty afterwards does not change the mode. The Penalty passed to Mode.decompose() stays usable, and so does a Mode passed to setMode() or to the Tokenizer constructor.

import { Mode, Penalty } from 'lindera-wasm-ipadic';

const mode = Mode.decompose(new Penalty(undefined, 0));
console.log(mode.name, mode.isNormal(), mode.isDecompose());
// decompose false true
console.log(`${mode}`, mode.penalty.kanji_penalty_length_penalty);
// decompose 0
console.log(mode.equals(Mode.decompose()), Mode.fromStr("decompose").equals(Mode.decompose()));
// false true
console.log(Mode.normal().penalty);
// undefined

Penalty

Configuration for decompose mode. Controls how aggressively compound words are decomposed.

const penalty = new Penalty(
    kanjiThreshold?,     // Kanji length threshold (default: 2)
    kanjiPenalty?,       // Kanji length penalty (default: 3000)
    otherThreshold?,     // Other character length threshold (default: 7)
    otherPenalty?,       // Other character length penalty (default: 1700)
);
PropertyTypeDefaultDescription
kanji_penalty_length_thresholdnumber2Length threshold for kanji compound splitting
kanji_penalty_length_penaltynumber3000Penalty cost for kanji compounds exceeding threshold
other_penalty_length_thresholdnumber7Length threshold for non-kanji compound splitting
other_penalty_length_penaltynumber1700Penalty cost for non-kanji compounds exceeding threshold

Pass it to Mode.decompose(), and the mode to setMode() or to the Tokenizer constructor. The Penalty is read, not consumed, so the same one can be passed again. Without length penalties, decompose mode keeps long words whole, as normal mode does:

import { Mode, Penalty, Tokenizer, TokenizerBuilder, loadDictionary } from 'lindera-wasm-ipadic';

const text = "関西国際空港限定トートバッグ";
const surfaces = (tokenizer) => tokenizer.tokenize(text).map((token) => token.surface);

const builder = new TokenizerBuilder();
builder.setDictionary("embedded://ipadic");
console.log(surfaces(builder.setMode(Mode.normal()).build()));
// [ '関西国際空港', '限定', 'トートバッグ' ]

console.log(surfaces(builder.setMode(Mode.decompose()).build()));
// [ '関西', '国際', '空港', '限定', 'トートバッグ' ]

const mode = Mode.decompose(new Penalty(undefined, 0, undefined, 0));
console.log(surfaces(builder.setMode(mode).build()));
// [ '関西国際空港', '限定', 'トートバッグ' ]

const tokenizer = new Tokenizer(loadDictionary("embedded://ipadic"), mode);
console.log(surfaces(tokenizer));
// [ '関西国際空港', '限定', 'トートバッグ' ]

LinderaError

Error type for Lindera operations.

const error = new LinderaError("message");
console.log(error.message);    // "message"
console.log(error.toString()); // "message"
Property / MethodTypeDescription
messagestringError message
toString()stringReturns the error message

[!NOTE] LinderaError is exported as a utility class, but the current error paths in TokenizerBuilder, Tokenizer, Mode, and the dictionary-loading functions (lindera-wasm/src/tokenizer.rs, lindera-wasm/src/mode.rs, lindera-wasm/src/dictionary.rs) all reject with JsValue::from_str(...), not a JsLinderaError/LinderaError instance. In practice, failures thrown by these APIs surface in JavaScript as plain strings. The exception is an argument that is not an instance of the class a method takes, such as a mode name passed to setMode(): the JS glue checks it before calling into Rust and throws an Error whose message is expected instance of <class> (here, expected instance of Mode). Catch them with catch (e) { ... } and treat e as a string or an Error, not as a LinderaError instance.

Snake-Case Aliases

For consistency with the Python API, these methods are also available in snake_case form:

camelCasesnake_case
setMode()set_mode()
setDictionary()set_dictionary()
setDictionaryInstance()set_dictionary_instance()
setUserDictionaryInstance()set_user_dictionary_instance()
setKeepWhitespace()set_keep_whitespace()
setSkipWhitespace()set_skip_whitespace()
setSpacePenalty()set_space_penalty()
appendCharacterFilter()append_character_filter()
appendTokenFilter()append_token_filter()
tokenizeSurfaces()tokenize_surfaces()
tokenizeNbest()tokenize_nbest()
loadDictionary()load_dictionary()
loadDictionaryFromBytes()load_dictionary_from_bytes()
loadUserDictionaryFromBytes()load_user_dictionary_from_bytes()
loadUserDictionaryFromBytesFor()load_user_dictionary_from_bytes_for()
loadUserDictionaryBinFromBytes()load_user_dictionary_bin_from_bytes()

The methods of Mode (fromStr(), isNormal(), isDecompose()) have no snake_case aliases.

Dictionary Management

Loading Dictionaries from OPFS

The recommended way to use dictionaries in WASM is to download them from GitHub Releases and load them via OPFS. This avoids embedding large dictionaries in the WASM binary.

Loading from Bytes

Use loadDictionaryFromBytes() to construct a Dictionary from raw byte arrays stored in OPFS or other browser storage.

loadDictionaryFromBytes(metadata, dictTrie, dictValsIdx, dictVals, dictWordsIdx, dictWords, matrixMtx, charDef, unk)

  • Parameters:
    • metadata (Uint8Array) -- Contents of metadata.json
    • dictTrie (Uint8Array) -- Contents of dict.trie (char-wise double-array trie)
    • dictValsIdx (Uint8Array) -- Contents of dict.valsidx (word values index)
    • dictVals (Uint8Array) -- Contents of dict.vals (word value data)
    • dictWordsIdx (Uint8Array) -- Contents of dict.wordsidx (word details index)
    • dictWords (Uint8Array) -- Contents of dict.words (word details)
    • matrixMtx (Uint8Array) -- Contents of matrix.mtx (connection cost matrix)
    • charDef (Uint8Array) -- Contents of char_def.bin (character definitions)
    • unk (Uint8Array) -- Contents of unk.bin (unknown word dictionary)
  • Returns: Dictionary
import { loadDictionaryFromBytes, Mode, TokenizerBuilder } from 'lindera-wasm';
import { loadDictionaryFiles } from 'lindera-wasm/opfs';

// Load dictionary files from OPFS
const files = await loadDictionaryFiles("ipadic");

// Create a Dictionary from bytes
const dictionary = loadDictionaryFromBytes(
    files.metadata,
    files.dictTrie,
    files.dictValsIdx,
    files.dictVals,
    files.dictWordsIdx,
    files.dictWords,
    files.matrixMtx,
    files.charDef,
    files.unk,
);

// Use with TokenizerBuilder
const builder = new TokenizerBuilder();
builder.setDictionaryInstance(dictionary);
builder.setMode(Mode.normal());
const tokenizer = builder.build();

See OPFS Dictionary Storage for the full OPFS workflow including downloading and caching.

Embedded Dictionaries (Advanced)

If you built with an embed-* feature flag, you can load embedded dictionaries via the embedded:// URI scheme. This increases the WASM binary size significantly.

[!NOTE] lindera-wasm-ipadic in the examples below is an illustrative package name for a local build with the embed-ipadic feature, not something published to npm. Only lindera-wasm is actually published; see NPM Package Naming Convention.

Loading an Embedded Dictionary

import { loadDictionary } from 'lindera-wasm-ipadic';

const dictionary = loadDictionary("embedded://ipadic");

Available embedded dictionary URIs (depending on which features were enabled at build time):

URIFeature Flag
embedded://ipadicembed-ipadic
embedded://unidicembed-unidic
embedded://ko-dicembed-ko-dic
embedded://cc-cedictembed-cc-cedict
embedded://jiebaembed-jieba

Using with TokenizerBuilder

const builder = new TokenizerBuilder();
builder.setDictionary("embedded://ipadic");
builder.setMode(Mode.normal());
const tokenizer = builder.build();

Using with Tokenizer Constructor

import { loadDictionary, Mode, Tokenizer } from 'lindera-wasm-ipadic';

const dictionary = loadDictionary("embedded://ipadic");
const tokenizer = new Tokenizer(dictionary, Mode.normal());

Dictionary Class

The Dictionary class represents a loaded morphological analysis dictionary.

Properties

PropertyTypeDescription
namestringDictionary name (e.g., "ipadic")
encodingstringCharacter encoding of the dictionary
metadataMetadataFull metadata object
console.log(dictionary.name);     // "ipadic"
console.log(dictionary.encoding); // "UTF-8"

User Dictionaries

User dictionaries allow you to add custom words that are not in the system dictionary.

There is no filesystem on WebAssembly, so user dictionaries are loaded from bytes obtained in JavaScript — from fetch, an <input type="file"> element, or OPFS.

Loading a User Dictionary from CSV Bytes

Pass the system dictionary the user dictionary will be used with, so the CSV is interpreted with its schema and checked against its connection matrix. The CSV content must be UTF-8.

import { loadUserDictionaryFromBytesFor } from 'lindera-wasm';

const response = await fetch('/dictionaries/user_dict.csv');
const csvBytes = new Uint8Array(await response.arrayBuffer());
const userDict = loadUserDictionaryFromBytesFor(csvBytes, dictionary);

A row whose context ID lies outside the matrix fails with an error that names its CSV line and surface (see Context IDs). The dictionary is only borrowed, so it can still be passed to Tokenizer afterwards.

loadUserDictionaryFromBytes(csv, metadata) builds the CSV with metadata only (e.g. dictionary.metadata). Its context IDs are checked when it is combined with the system dictionary, and the error names the entry by its word id.

Bytes from OPFS work the same way:

const root = await navigator.storage.getDirectory();
const handle = await root.getFileHandle('user_dict.csv');
const csvBytes = new Uint8Array(await (await handle.getFile()).arrayBuffer());
const userDict = loadUserDictionaryFromBytesFor(csvBytes, dictionary);

Loading a Prebuilt User Dictionary (.bin)

A user dictionary compiled with lindera build --user loads directly:

import { loadUserDictionaryBinFromBytes } from 'lindera-wasm';

const response = await fetch('/dictionaries/user_dict.bin');
const binBytes = new Uint8Array(await response.arrayBuffer());
const userDict = loadUserDictionaryBinFromBytes(binBytes);

Using a User Dictionary with Tokenizer

import { loadDictionaryFromBytes, loadUserDictionaryFromBytesFor, Mode, Tokenizer } from 'lindera-wasm';
import { loadDictionaryFiles } from 'lindera-wasm/opfs';

const files = await loadDictionaryFiles("ipadic");
const dictionary = loadDictionaryFromBytes(
    files.metadata, files.dictTrie, files.dictValsIdx, files.dictVals,
    files.dictWordsIdx, files.dictWords, files.matrixMtx, files.charDef, files.unk,
);
const response = await fetch('/dictionaries/user_dict.csv');
const csvBytes = new Uint8Array(await response.arrayBuffer());
const userDict = loadUserDictionaryFromBytesFor(csvBytes, dictionary);
const tokenizer = new Tokenizer(dictionary, Mode.normal(), userDict);

The dictionary and the user dictionary stay usable after they are passed to the Tokenizer constructor, setDictionaryInstance() or setUserDictionaryInstance(), so one loaded dictionary can serve several tokenizers and builders.

User Dictionary CSV Format

The user dictionary CSV follows the same format as the Lindera user dictionary. For IPADIC the simple format is:

東京スカイツリー,カスタム名詞,トウキョウスカイツリー
東武スカイツリーライン,カスタム名詞,トウブスカイツリーライン

Each line contains: surface,part_of_speech,reading. Rows in the full dictionary format (13+ fields for IPADIC) are also accepted. The content must be UTF-8.

Building Dictionaries

Dictionaries cannot be built in WebAssembly: building reads a source directory and writes an output directory, and there is no filesystem on wasm32-unknown-unknown. Build dictionaries with the lindera CLI (or a native binding) and load the result here as bytes — see OPFS Dictionary Management for downloading prebuilt dictionaries.

A dictionary built from a metadata.json that ships space_penalty rules keeps them, and loadDictionaryFromBytes() loads them with the rest of the metadata. ko-dic's metadata.json ships mecab-ko-dic's rules, so a tokenizer built on a ko-dic applies the left-space penalty by default; see setSpacePenalty() for turning it off.

Metadata

The Metadata class configures dictionary parameters.

Constructor

const metadata = new Metadata(name?, encoding?);
  • Parameters:
    • name (string, optional) -- Dictionary name (default: "default")
    • encoding (string, optional) -- Character encoding (default: "UTF-8")

Static Methods

Metadata.createDefault()

Creates a Metadata instance with default values.

const metadata = Metadata.createDefault();

Metadata Properties

PropertyTypeDefaultDescription
namestring"default"Dictionary name
encodingstring"UTF-8"Character encoding
dictionary_schemaSchemaIPADIC schemaSchema for the main dictionary
user_dictionary_schemaSchemaMinimal schemaSchema for user dictionaries

All properties support both getting and setting:

const metadata = Metadata.createDefault();
metadata.name = "custom_dict";
metadata.encoding = "EUC-JP";
console.log(metadata.name); // "custom_dict"

Setting dictionary_schema or user_dictionary_schema stores a copy of the Schema, which stays usable, so one schema can be set on several Metadata.

[!NOTE] Unlike the Python, Node.js, Ruby, and PHP bindings, the WASM Metadata class does not expose default_word_cost, default_left_context_id, default_right_context_id, default_field_value, flexible_csv, or skip_invalid_cost_or_id as gettable/settable properties (see lindera-wasm/src/metadata.rs). These always fall back to the shared binding defaults (word cost -10000, context IDs 1288, field value "*", flags false) and cannot be customized from JavaScript. The space_penalty rules are not exposed either. They take effect only through the dictionary they were loaded with (see setSpacePenalty()); passing dictionary.metadata on, for example to loadUserDictionaryFromBytes(), does not apply them anywhere else.

You can also access the metadata from a loaded dictionary via dictionary.metadata.

Schema

The Schema class defines the field structure of dictionary entries.

Schema Constructor

const schema = new Schema(["surface", "left_id", "right_id", "cost", "pos", "reading"]);

Schema Static Methods

  • Schema.create_default() -- Creates a built-in 13-field schema loosely modeled on IPADIC's layout: the four system fields (surface, left_context_id, right_context_id, cost) followed by nine generic feature fields (major_pos, pos_detail_1-pos_detail_3, conjugation_type, conjugation_form, base_form, reading, pronunciation). The four part-of-speech names differ from the real lindera-ipadic dictionary schema, which uses part_of_speech and part_of_speech_subcategory_1-_3; the remaining fields match it in name and order. To match an actual IPADIC dictionary's schema, use dictionary.metadata.dictionary_schema from a loaded dictionary instead

Schema Methods

MethodReturnsDescription
get_field_index(name)number | undefinedGet field index by name
field_count()numberTotal number of fields
get_field_name(index)string | undefinedGet field name by index (undefined past the end; a negative or non-integer index throws)
get_custom_fields()string[]Fields beyond index 3 (morphological features)
get_all_fields()string[]All field names
get_field_by_name(name)FieldDefinition | undefinedGet full field definition

FieldDefinition

PropertyTypeDescription
indexnumberField position index
namestringField name
field_typeFieldTypeField type enum
descriptionstring | undefinedOptional description

FieldType

ValueDescription
FieldType.SurfaceWord surface text
FieldType.LeftContextIdLeft context ID
FieldType.RightContextIdRight context ID
FieldType.CostWord cost
FieldType.CustomMorphological feature field

Browser Usage

ES Module Import

In browser environments, you must initialize the WASM module before using any Lindera functions. The default export __wbg_init handles this initialization.

The recommended approach is to load dictionaries from OPFS rather than embedding them in the WASM binary:

import __wbg_init, { Mode, TokenizerBuilder, loadDictionaryFromBytes } from 'lindera-wasm';
import { downloadDictionary, loadDictionaryFiles, hasDictionary } from 'lindera-wasm/opfs';

async function main() {
    // Initialize the WASM module (must be called once before using any API)
    await __wbg_init();

    // Download dictionary if not cached
    if (!await hasDictionary("ipadic")) {
        await downloadDictionary(
            "https://github.com/lindera/lindera/releases/download/<version>/lindera-ipadic-<version>.zip",
            "ipadic",
        );
    }

    // Load dictionary from OPFS
    const files = await loadDictionaryFiles("ipadic");
    const dictionary = loadDictionaryFromBytes(
        files.metadata, files.dictTrie, files.dictValsIdx, files.dictVals,
        files.dictWordsIdx, files.dictWords, files.matrixMtx, files.charDef, files.unk,
    );

    const builder = new TokenizerBuilder();
    builder.setDictionaryInstance(dictionary);
    builder.setMode(Mode.normal());
    const tokenizer = builder.build();

    const tokens = tokenizer.tokenize("形態素解析を行います");
    tokens.forEach(token => {
        console.log(`${token.surface}: ${token.details.join(',')}`);
    });
}

main();

Using Embedded Dictionaries (Advanced)

If you built with an embed-* feature flag, you can use embedded dictionaries instead of OPFS:

[!NOTE] lindera-wasm-ipadic is an illustrative package name here, not something published to npm. Only lindera-wasm is actually published; see NPM Package Naming Convention for how to build and name a package like this yourself.

import __wbg_init, { Mode, TokenizerBuilder } from 'lindera-wasm-ipadic';

async function main() {
    await __wbg_init();

    const builder = new TokenizerBuilder();
    builder.setDictionary("embedded://ipadic");
    builder.setMode(Mode.normal());
    const tokenizer = builder.build();

    const tokens = tokenizer.tokenize("形態素解析を行います");
    tokens.forEach(token => {
        console.log(`${token.surface}: ${token.details.join(',')}`);
    });
}

main();

HTML Example

A minimal HTML page using lindera-wasm with OPFS dictionary loading:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Lindera WASM Demo</title>
</head>
<body>
    <textarea id="input" rows="4" cols="50">関西国際空港限定トートバッグ</textarea>
    <br>
    <button id="tokenize" disabled>Tokenize</button>
    <pre id="output">Loading dictionary...</pre>

    <script type="module">
        import __wbg_init, { Mode, TokenizerBuilder, loadDictionaryFromBytes } from './pkg/lindera_wasm.js';
        import { downloadDictionary, loadDictionaryFiles, hasDictionary } from './pkg/opfs.js';

        let tokenizer;

        async function init() {
            await __wbg_init();

            // Download dictionary if not cached
            if (!await hasDictionary("ipadic")) {
                document.getElementById('output').textContent = 'Downloading dictionary...';
                await downloadDictionary(
                    "https://github.com/lindera/lindera/releases/download/<version>/lindera-ipadic-<version>.zip",
                    "ipadic",
                );
            }

            // Load dictionary from OPFS
            const files = await loadDictionaryFiles("ipadic");
            const dictionary = loadDictionaryFromBytes(
                files.metadata, files.dictTrie, files.dictValsIdx, files.dictVals,
                files.dictWordsIdx, files.dictWords, files.matrixMtx, files.charDef, files.unk,
            );

            const builder = new TokenizerBuilder();
            builder.setDictionaryInstance(dictionary);
            builder.setMode(Mode.normal());
            tokenizer = builder.build();

            document.getElementById('tokenize').disabled = false;
            document.getElementById('output').textContent = 'Ready!';
        }

        document.getElementById('tokenize').addEventListener('click', () => {
            const text = document.getElementById('input').value;
            const tokens = tokenizer.tokenize(text);
            const output = tokens.map(t =>
                `${t.surface}\t${t.details.join(',')}`
            ).join('\n');
            document.getElementById('output').textContent = output;
        });

        init();
    </script>
</body>
</html>

Webpack Configuration

The lindera-wasm package is built with wasm-pack's web target, and modern bundlers can consume it directly. When using Webpack 5, enable the asyncWebAssembly experiment:

// webpack.config.js
module.exports = {
    experiments: {
        asyncWebAssembly: true,
    },
    module: {
        rules: [
            {
                test: /\.wasm$/,
                type: "webassembly/async",
            },
        ],
    },
};

Then import the package and call the default-exported async init function once before using any API (the web-target build requires it, bundled or not):

import __wbg_init, { Mode, TokenizerBuilder, loadDictionaryFromBytes } from 'lindera-wasm';
import { loadDictionaryFiles } from 'lindera-wasm/opfs';

await __wbg_init();

// Load dictionary from OPFS (see OPFS Dictionary Storage for setup)
const files = await loadDictionaryFiles("ipadic");
const dictionary = loadDictionaryFromBytes(
    files.metadata, files.dictTrie, files.dictValsIdx, files.dictVals,
    files.dictWordsIdx, files.dictWords, files.matrixMtx, files.charDef, files.unk,
);

const builder = new TokenizerBuilder();
builder.setDictionaryInstance(dictionary);
builder.setMode(Mode.normal());
const tokenizer = builder.build();

Vite / Rollup Setup

Vite supports the web-target WASM build out of the box. Install lindera-wasm from npm, or place a locally built pkg/ directory in your project and import directly:

import __wbg_init, { Mode, TokenizerBuilder, loadDictionaryFromBytes } from './pkg/lindera_wasm.js';
import { loadDictionaryFiles } from './pkg/opfs.js';

await __wbg_init();
// Load dictionary from OPFS and use TokenizerBuilder as shown above

When installing from npm, exclude the package from Vite's dependency pre-bundling:

// vite.config.js
export default {
    optimizeDeps: {
        exclude: ["lindera-wasm"],
    },
};

Chrome Extension Considerations

Chrome extensions using Manifest V3 restrict WebAssembly.compile and WebAssembly.instantiate by default. To use lindera-wasm in an extension, you need to add wasm-unsafe-eval to your Content Security Policy:

{
    "content_security_policy": {
        "extension_pages": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self'"
    }
}

Note that wasm-unsafe-eval only allows WebAssembly execution and does not permit arbitrary JavaScript eval().

Performance Tips

  • Initialize once: Call __wbg_init() once at application startup, not on every tokenization request.
  • Reuse the tokenizer: Create the Tokenizer instance once and reuse it for multiple calls to tokenize().
  • Web Workers: For heavy tokenization workloads, consider running Lindera in a Web Worker to avoid blocking the main thread.

OPFS Dictionary Storage

Lindera WASM provides OPFS (Origin Private File System) helper utilities for persistent dictionary caching in web browsers. This allows you to download dictionaries once and reuse them across sessions without embedding them in the WASM binary.

Overview

The OPFS helpers are distributed as a separate JavaScript module (opfs.js) alongside the WASM package. They provide functions to download, store, load, and manage dictionaries using the browser's Origin Private File System.

Dictionaries are stored under the OPFS path lindera/dictionaries/<name>/.

Import

import { downloadDictionary, loadDictionaryFiles, removeDictionary,
         listDictionaries, hasDictionary } from 'lindera-wasm/opfs';

Functions

downloadDictionary(url, name, options?)

Downloads a dictionary zip archive, extracts it, and stores the files in OPFS.

The archive should be a zip file containing the 9 required dictionary files, optionally nested in a subdirectory.

  • Parameters:
    • url (string) -- URL of the dictionary zip archive
    • name (string) -- Name to store the dictionary under (e.g., "ipadic")
    • options (object, optional):
      • onProgress (function) -- Progress callback
      • fetchInit (RequestInit, optional) -- Additional options passed through to fetch() (e.g., custom headers, credentials, an AbortSignal)
  • Returns: Promise<void>
await downloadDictionary(
    "https://example.com/ipadic.zip",
    "ipadic",
    {
        onProgress: (progress) => {
            switch (progress.phase) {
                case "downloading":
                    console.log(`Downloading: ${progress.loaded}/${progress.total} bytes`);
                    break;
                case "extracting":
                    console.log("Extracting archive...");
                    break;
                case "storing":
                    console.log("Storing in OPFS...");
                    break;
                case "complete":
                    console.log("Done!");
                    break;
            }
        },
    },
);

Progress Callback

The onProgress callback receives an object with the following shape:

PropertyTypeDescription
phasestring"downloading", "extracting", "storing", or "complete"
loadednumber | undefinedBytes downloaded (only during "downloading" phase)
totalnumber | undefinedTotal bytes if known (only during "downloading" phase)

loadDictionaryFiles(name)

Loads dictionary files from OPFS as an object of Uint8Array values.

The returned object can be passed directly to loadDictionaryFromBytes().

  • Parameters: name (string) -- The dictionary name (e.g., "ipadic")
  • Returns: Promise<DictionaryFiles>
const files = await loadDictionaryFiles("ipadic");

DictionaryFiles

PropertyTypeSource File
metadataUint8Arraymetadata.json
dictTrieUint8Arraydict.trie (char-wise double-array trie)
dictValsIdxUint8Arraydict.valsidx (word values index)
dictValsUint8Arraydict.vals (word value data)
dictWordsIdxUint8Arraydict.wordsidx (word details index)
dictWordsUint8Arraydict.words (word details)
matrixMtxUint8Arraymatrix.mtx (connection cost matrix)
charDefUint8Arraychar_def.bin (character definitions)
unkUint8Arrayunk.bin (unknown word dictionary)

removeDictionary(name)

Removes a dictionary from OPFS.

  • Parameters: name (string) -- The dictionary name to remove
  • Returns: Promise<void>
await removeDictionary("ipadic");

listDictionaries()

Lists all dictionaries stored in OPFS.

  • Returns: Promise<string[]> -- Array of dictionary names
const names = await listDictionaries();
console.log(names); // e.g., ["ipadic", "unidic"]

hasDictionary(name)

Checks if a dictionary exists in OPFS.

  • Parameters: name (string) -- The dictionary name to check
  • Returns: Promise<boolean>
if (await hasDictionary("ipadic")) {
    console.log("Dictionary is cached");
}

Complete Workflow

A typical workflow for using OPFS-based dictionaries:

import __wbg_init, { Mode, TokenizerBuilder, loadDictionaryFromBytes } from 'lindera-wasm';
import { downloadDictionary, loadDictionaryFiles, hasDictionary } from 'lindera-wasm/opfs';

async function main() {
    await __wbg_init();

    const DICT_NAME = "ipadic";
    const DICT_URL = "https://github.com/lindera/lindera/releases/download/<version>/lindera-ipadic-<version>.zip";

    // Download dictionary if not already cached
    if (!await hasDictionary(DICT_NAME)) {
        await downloadDictionary(DICT_URL, DICT_NAME, {
            onProgress: ({ phase, loaded, total }) => {
                if (phase === "downloading" && total) {
                    console.log(`${(loaded / total * 100).toFixed(1)}%`);
                }
            },
        });
    }

    // Load dictionary from OPFS
    const files = await loadDictionaryFiles(DICT_NAME);
    const dictionary = loadDictionaryFromBytes(
        files.metadata, files.dictTrie, files.dictValsIdx, files.dictVals,
        files.dictWordsIdx, files.dictWords, files.matrixMtx, files.charDef, files.unk,
    );

    // Build tokenizer
    const builder = new TokenizerBuilder();
    builder.setDictionaryInstance(dictionary);
    builder.setMode(Mode.normal());
    const tokenizer = builder.build();

    // Tokenize
    const tokens = tokenizer.tokenize("形態素解析を行います");
    tokens.forEach(token => {
        console.log(`${token.surface}\t${token.details.join(',')}`);
    });
}

main();

Required Dictionary Files

A valid dictionary archive must contain these 9 files:

FileDescription
metadata.jsonDictionary metadata (name, encoding, schema, etc.)
dict.trieChar-wise double-array trie structure
dict.valsidxWord values index
dict.valsWord value data
dict.wordsidxWord details index
dict.wordsWord details (morphological features)
matrix.mtxConnection cost matrix
char_def.binCharacter category definitions
unk.binUnknown word dictionary

Browser Compatibility

OPFS requires a secure context (HTTPS or localhost) and is supported in:

  • Chrome 86+
  • Edge 86+
  • Firefox 111+
  • Safari 15.2+

The zip extraction uses the DecompressionStream API, which requires:

  • Chrome 80+
  • Edge 80+
  • Firefox 113+
  • Safari 16.4+

Lindera IPADIC

Lindera IPADIC is a Japanese dictionary crate based on IPADIC. IPADIC is the most common dictionary for Japanese morphological analysis.

Contents

  • Dictionary Format -- Field definitions for system and user dictionaries
  • Build -- How to build the dictionary from source
  • Examples -- Tokenization examples

API Reference

Lindera IPADIC

Dictionary version

This repository contains mecab-ipadic.

Dictionary format

Refer to the manual for details on the IPADIC dictionary format and part-of-speech tags.

IndexName (Japanese)Name (English)Notes
0表層形Surface
1左文脈IDLeft context ID
2右文脈IDRight context ID
3コストCost
4品詞Part-of-speech
5品詞細分類1Part-of-speech subcategory 1
6品詞細分類2Part-of-speech subcategory 2
7品詞細分類3Part-of-speech subcategory 3
8活用型Conjugation type
9活用形Conjugation form
10原形Base form
11読みReading
12発音Pronunciation

User dictionary format (CSV)

Simple version

IndexName (Japanese)Name (English)Notes
0表層形Surface
1品詞Part-of-speech
2読みReading

Fields not covered by this simple schema (such as base_form and pronunciation) are filled with the dictionary's default_field_value (* for IPADIC).

Detailed version

IndexName (Japanese)Name (English)Notes
0表層形Surface
1左文脈IDLeft context ID
2右文脈IDRight context ID
3コストCost
4品詞Part-of-speech
5品詞細分類1Part-of-speech subcategory 1
6品詞細分類2Part-of-speech subcategory 2
7品詞細分類3Part-of-speech subcategory 3
8活用型Conjugation type
9活用形Conjugation form
10原形Base form
11読みReading
12発音Pronunciation
13--After 13, it can be freely expanded.

API reference

The API reference is available. Please see following URL:

Build

This page describes how to build the IPADIC dictionary from source files.

Build system dictionary

Download the IPADIC source files and build the dictionary:

# Download and extract IPADIC source files
% curl -L -o /tmp/mecab-ipadic-2.7.0-20250920.tar.gz "https://Lindera.dev/mecab-ipadic-2.7.0-20250920.tar.gz"
% tar zxvf /tmp/mecab-ipadic-2.7.0-20250920.tar.gz -C /tmp

# Build the dictionary
% lindera build \
  --src /tmp/mecab-ipadic-2.7.0-20250920 \
  --dest /tmp/lindera-ipadic-2.7.0-20250920 \
  --metadata ./lindera-ipadic/metadata.json

Build user dictionary

Build a user dictionary from a CSV file:

% lindera build \
  --src ./resources/user_dict/ipadic_simple_userdic.csv \
  --dest ./resources/user_dict \
  --metadata ./lindera-ipadic/metadata.json \
  --user

For more details about user dictionary format, see Dictionary Format.

Embedding in binary

To embed the IPADIC dictionary directly into the binary:

cargo build --features=embed-ipadic

This allows using embedded://ipadic as the dictionary path without external dictionary files.

Examples

This page shows tokenization examples using the IPADIC dictionary.

Tokenize with external IPADIC

% echo "日本語の形態素解析を行うことができます。" | lindera tokenize \
  --dict /tmp/lindera-ipadic-2.7.0-20250920
日本語  名詞,一般,*,*,*,*,日本語,ニホンゴ,ニホンゴ
の      助詞,連体化,*,*,*,*,の,ノ,ノ
形態素  名詞,一般,*,*,*,*,形態素,ケイタイソ,ケイタイソ
解析    名詞,サ変接続,*,*,*,*,解析,カイセキ,カイセキ
を      助詞,格助詞,一般,*,*,*,を,ヲ,ヲ
行う    動詞,自立,*,*,五段・ワ行促音便,基本形,行う,オコナウ,オコナウ
こと    名詞,非自立,一般,*,*,*,こと,コト,コト
が      助詞,格助詞,一般,*,*,*,が,ガ,ガ
でき    動詞,自立,*,*,一段,連用形,できる,デキ,デキ
ます    助動詞,*,*,*,特殊・マス,基本形,ます,マス,マス
。      記号,句点,*,*,*,*,。,。,。
EOS

Tokenize with embedded IPADIC

% echo "日本語の形態素解析を行うことができます。" | lindera tokenize \
  --dict embedded://ipadic
日本語  名詞,一般,*,*,*,*,日本語,ニホンゴ,ニホンゴ
の      助詞,連体化,*,*,*,*,の,ノ,ノ
形態素  名詞,一般,*,*,*,*,形態素,ケイタイソ,ケイタイソ
解析    名詞,サ変接続,*,*,*,*,解析,カイセキ,カイセキ
を      助詞,格助詞,一般,*,*,*,を,ヲ,ヲ
行う    動詞,自立,*,*,五段・ワ行促音便,基本形,行う,オコナウ,オコナウ
こと    名詞,非自立,一般,*,*,*,こと,コト,コト
が      助詞,格助詞,一般,*,*,*,が,ガ,ガ
でき    動詞,自立,*,*,一段,連用形,できる,デキ,デキ
ます    助動詞,*,*,*,特殊・マス,基本形,ます,マス,マス
。      記号,句点,*,*,*,*,。,。,。
EOS

NOTE: To include IPADIC dictionary in the binary, you must build with the --features=embed-ipadic option.

Tokenize with user dictionary (CSV format)

% echo "東京スカイツリーの最寄り駅はとうきょうスカイツリー駅です" | lindera tokenize \
  --dict embedded://ipadic \
  --user-dict ./resources/user_dict/ipadic_simple_userdic.csv
東京スカイツリー        カスタム名詞,*,*,*,*,*,*,トウキョウスカイツリー,*
の      助詞,連体化,*,*,*,*,の,ノ,ノ
最寄り駅        名詞,一般,*,*,*,*,最寄り駅,モヨリエキ,モヨリエキ
は      助詞,係助詞,*,*,*,*,は,ハ,ワ
とうきょうスカイツリー駅        カスタム名詞,*,*,*,*,*,*,トウキョウスカイツリーエキ,*
です    助動詞,*,*,*,特殊・デス,基本形,です,デス,デス
EOS

Tokenize with user dictionary (binary format)

% echo "東京スカイツリーの最寄り駅はとうきょうスカイツリー駅です" | lindera tokenize \
  --dict /tmp/lindera-ipadic-2.7.0-20250920 \
  --user-dict ./resources/user_dict/ipadic_simple_userdic.bin
東京スカイツリー        カスタム名詞,*,*,*,*,*,*,トウキョウスカイツリー,*
の      助詞,連体化,*,*,*,*,の,ノ,ノ
最寄り駅        名詞,一般,*,*,*,*,最寄り駅,モヨリエキ,モヨリエキ
は      助詞,係助詞,*,*,*,*,は,ハ,ワ
とうきょうスカイツリー駅        カスタム名詞,*,*,*,*,*,*,トウキョウスカイツリーエキ,*
です    助動詞,*,*,*,特殊・デス,基本形,です,デス,デス
EOS

Rust API example

use lindera::dictionary::load_dictionary;
use lindera::mode::Mode;
use lindera::segmenter::Segmenter;
use lindera::analysis::tokenizer::Tokenizer;
use lindera::LinderaResult;

fn main() -> LinderaResult<()> {
    let dictionary = load_dictionary("embedded://ipadic")?;
    let segmenter = Segmenter::new(Mode::Normal, dictionary, None);
    let tokenizer = Tokenizer::new(segmenter);

    let text = "日本語の形態素解析を行うことができます。";
    let mut tokens = tokenizer.tokenize(text)?;
    for token in tokens.iter_mut() {
        let details = token.details().join(",");
        println!("{}\t{}", token.surface.as_ref(), details);
    }
    Ok(())
}

Lindera IPADIC NEologd

Lindera IPADIC NEologd is a Japanese dictionary crate based on IPADIC NEologd, which includes neologisms (new words). It extends the standard IPADIC dictionary with additional vocabulary covering recent terms and proper nouns.

Contents

  • Dictionary Format -- Field definitions for system and user dictionaries
  • Build -- How to build the dictionary from source
  • Examples -- Tokenization examples

API Reference

Lindera IPADIC NEologd

Dictionary version

This repository contains mecab-ipadic-neologd.

Dictionary format

Refer to the manual for details on the IPADIC dictionary format and part-of-speech tags.

IndexName (Japanese)Name (English)Notes
0表層形Surface
1左文脈IDLeft context ID
2右文脈IDRight context ID
3コストCost
4品詞Part-of-speech
5品詞細分類1Part-of-speech subcategory 1
6品詞細分類2Part-of-speech subcategory 2
7品詞細分類3Part-of-speech subcategory 3
8活用型Conjugation type
9活用形Conjugation form
10原形Base form
11読みReading
12発音Pronunciation

User dictionary format (CSV)

Simple version

IndexName (Japanese)Name (English)Notes
0表層形Surface
1品詞Part-of-speech
2読みReading

Detailed version

IndexName (Japanese)Name (English)Notes
0表層形Surface
1左文脈IDLeft context ID
2右文脈IDRight context ID
3コストCost
4品詞Part-of-speech
5品詞細分類1Part-of-speech subcategory 1
6品詞細分類2Part-of-speech subcategory 2
7品詞細分類3Part-of-speech subcategory 3
8活用型Conjugation type
9活用形Conjugation form
10原形Base form
11読みReading
12発音Pronunciation
13--After 13, it can be freely expanded.

API reference

The API reference is available. Please see following URL:

Build

This page describes how to build the IPADIC NEologd dictionary from source files.

Build system dictionary

Download the IPADIC NEologd source files and build the dictionary:

% curl -L -o /tmp/mecab-ipadic-neologd-0.0.7-20200820.tar.gz "https://lindera.dev/mecab-ipadic-neologd-0.0.7-20200820.tar.gz"
% tar zxvf /tmp/mecab-ipadic-neologd-0.0.7-20200820.tar.gz -C /tmp

% lindera build \
  --src /tmp/mecab-ipadic-neologd-0.0.7-20200820 \
  --dest /tmp/lindera-ipadic-neologd-0.0.7-20200820 \
  --metadata ./lindera-ipadic-neologd/metadata.json

Build user dictionary

Build a user dictionary from a CSV file:

% lindera build \
  --src ./resources/user_dict/ipadic_simple_userdic.csv \
  --dest ./resources/user_dict \
  --metadata ./lindera-ipadic-neologd/metadata.json \
  --user

For more details about user dictionary format, see Dictionary Format.

Embedding in binary

To embed the IPADIC NEologd dictionary directly into the binary:

cargo build --features=embed-ipadic-neologd

This allows using embedded://ipadic-neologd as the dictionary path without external dictionary files.

Examples

This page shows tokenization examples using the IPADIC NEologd dictionary.

Tokenize with external IPADIC NEologd

% echo "日本語の形態素解析を行うことができます。" | lindera tokenize \
  --dict /tmp/lindera-ipadic-neologd-0.0.7-20200820
日本語  名詞,一般,*,*,*,*,日本語,ニホンゴ,ニホンゴ
の      助詞,連体化,*,*,*,*,の,ノ,ノ
形態素解析      名詞,固有名詞,一般,*,*,*,形態素解析,ケイタイソカイセキ,ケイタイソカイセキ
を      助詞,格助詞,一般,*,*,*,を,ヲ,ヲ
行う    動詞,自立,*,*,五段・ワ行促音便,基本形,行う,オコナウ,オコナウ
こと    名詞,非自立,一般,*,*,*,こと,コト,コト
が      助詞,格助詞,一般,*,*,*,が,ガ,ガ
でき    動詞,自立,*,*,一段,連用形,できる,デキ,デキ
ます    助動詞,*,*,*,特殊・マス,基本形,ます,マス,マス
。      記号,句点,*,*,*,*,。,。,。
EOS

Notice that NEologd treats "形態素解析" (morphological analysis) as a single compound noun, whereas standard IPADIC splits it into "形態素" and "解析".

Tokenize with embedded IPADIC NEologd

% echo "日本語の形態素解析を行うことができます。" | lindera tokenize \
  --dict embedded://ipadic-neologd
日本語  名詞,一般,*,*,*,*,日本語,ニホンゴ,ニホンゴ
の      助詞,連体化,*,*,*,*,の,ノ,ノ
形態素解析      名詞,固有名詞,一般,*,*,*,形態素解析,ケイタイソカイセキ,ケイタイソカイセキ
を      助詞,格助詞,一般,*,*,*,を,ヲ,ヲ
行う    動詞,自立,*,*,五段・ワ行促音便,基本形,行う,オコナウ,オコナウ
こと    名詞,非自立,一般,*,*,*,こと,コト,コト
が      助詞,格助詞,一般,*,*,*,が,ガ,ガ
でき    動詞,自立,*,*,一段,連用形,できる,デキ,デキ
ます    助動詞,*,*,*,特殊・マス,基本形,ます,マス,マス
。      記号,句点,*,*,*,*,。,。,。
EOS

NOTE: To include IPADIC NEologd dictionary in the binary, you must build with the --features=embed-ipadic-neologd option.

Tokenize with user dictionary (CSV format)

% echo "東京スカイツリーの最寄り駅はとうきょうスカイツリー駅です" | lindera tokenize \
  --dict embedded://ipadic-neologd \
  --user-dict ./resources/user_dict/ipadic_simple_userdic.csv
東京スカイツリー        カスタム名詞,*,*,*,*,*,*,トウキョウスカイツリー,*
の      助詞,連体化,*,*,*,*,の,ノ,ノ
最寄り駅        名詞,一般,*,*,*,*,最寄り駅,モヨリエキ,モヨリエキ
は      助詞,係助詞,*,*,*,*,は,ハ,ワ
とうきょうスカイツリー駅        カスタム名詞,*,*,*,*,*,*,トウキョウスカイツリーエキ,*
です    助動詞,*,*,*,特殊・デス,基本形,です,デス,デス
EOS

Tokenize with user dictionary (binary format)

% echo "東京スカイツリーの最寄り駅はとうきょうスカイツリー駅です" | lindera tokenize \
  --dict /tmp/lindera-ipadic-neologd-0.0.7-20200820 \
  --user-dict ./resources/user_dict/ipadic_simple_userdic.bin
東京スカイツリー        カスタム名詞,*,*,*,*,*,*,トウキョウスカイツリー,*
の      助詞,連体化,*,*,*,*,の,ノ,ノ
最寄り駅        名詞,一般,*,*,*,*,最寄り駅,モヨリエキ,モヨリエキ
は      助詞,係助詞,*,*,*,*,は,ハ,ワ
とうきょうスカイツリー駅        カスタム名詞,*,*,*,*,*,*,トウキョウスカイツリーエキ,*
です    助動詞,*,*,*,特殊・デス,基本形,です,デス,デス
EOS

Rust API example

use lindera::dictionary::load_dictionary;
use lindera::mode::Mode;
use lindera::segmenter::Segmenter;
use lindera::analysis::tokenizer::Tokenizer;
use lindera::LinderaResult;

fn main() -> LinderaResult<()> {
    let dictionary = load_dictionary("embedded://ipadic-neologd")?;
    let segmenter = Segmenter::new(Mode::Normal, dictionary, None);
    let tokenizer = Tokenizer::new(segmenter);

    let text = "日本語の形態素解析を行うことができます。";
    let mut tokens = tokenizer.tokenize(text)?;
    for token in tokens.iter_mut() {
        let details = token.details().join(",");
        println!("{}\t{}", token.surface.as_ref(), details);
    }
    Ok(())
}

Lindera UniDic

Lindera UniDic is a Japanese dictionary crate based on UniDic, which uses uniform word unit definitions. UniDic provides more detailed morphological information than IPADIC, with 21 fields per entry.

Contents

  • Dictionary Format -- Field definitions for system and user dictionaries
  • Build -- How to build the dictionary from source
  • Examples -- Tokenization examples

API Reference

Lindera UniDic

Dictionary version

This repository contains unidic-mecab.

Dictionary format

Refer to the manual for details on the unidic-mecab dictionary format and part-of-speech tags.

IndexName (Japanese)Name (English)Notes
0表層形Surface
1左文脈IDLeft context ID
2右文脈IDRight context ID
3コストCost
4品詞大分類Part-of-speech
5品詞中分類Part-of-speech subcategory 1
6品詞小分類Part-of-speech subcategory 2
7品詞細分類Part-of-speech subcategory 3
8活用型Conjugation type
9活用形Conjugation form
10語彙素読みReading
11語彙素(語彙素表記 + 語彙素細分類)Lexeme
12書字形出現形Orthographic surface form
13発音形出現形Phonological surface form
14書字形基本形Orthographic base form
15発音形基本形Phonological base form
16語種Word type
17語頭変化型Initial mutation type
18語頭変化形Initial mutation form
19語末変化型Final mutation type
20語末変化形Final mutation form

User dictionary format (CSV)

Simple version

IndexName (Japanese)Name (English)Notes
0表層形Surface
1品詞大分類Part-of-speech
2語彙素読みReading

Detailed version

IndexName (Japanese)Name (English)Notes
0表層形Surface
1左文脈IDLeft context ID
2右文脈IDRight context ID
3コストCost
4品詞大分類Part-of-speech
5品詞中分類Part-of-speech subcategory 1
6品詞小分類Part-of-speech subcategory 2
7品詞細分類Part-of-speech subcategory 3
8活用型Conjugation type
9活用形Conjugation form
10語彙素読みReading
11語彙素(語彙素表記 + 語彙素細分類)Lexeme
12書字形出現形Orthographic surface form
13発音形出現形Phonological surface form
14書字形基本形Orthographic base form
15発音形基本形Phonological base form
16語種Word type
17語頭変化型Initial mutation type
18語頭変化形Initial mutation form
19語末変化型Final mutation type
20語末変化形Final mutation form
21--After 21, it can be freely expanded.

API reference

The API reference is available. Please see following URL:

Build

This page describes how to build the UniDic dictionary from source files.

Build system dictionary

Download the UniDic source files and build the dictionary:

% curl -L -o /tmp/unidic-mecab-2.1.2.tar.gz "https://Lindera.dev/unidic-mecab-2.1.2.tar.gz"
% tar zxvf /tmp/unidic-mecab-2.1.2.tar.gz -C /tmp

% lindera build \
  --src /tmp/unidic-mecab-2.1.2 \
  --dest /tmp/lindera-unidic-2.1.2 \
  --metadata ./lindera-unidic/metadata.json \
  --context-id-freq ./lindera-unidic/context_id_freq.txt

[!TIP] lindera-unidic/metadata.json sets connection_id_mapping: true, so the builder relabels the connection-cost matrix's context IDs by access frequency to improve cache locality when looking up connection costs. Passing --context-id-freq / -f with the bundled context_id_freq.txt histogram gives this remapping real corpus frequency data to rank IDs by. Omitting the flag silently falls back to a much weaker entry-count-based proxy instead of failing, so the build still succeeds but without the full benefit. Either way, tokenization output is unaffected -- the remap is a bijective relabeling that only changes a build-time optimization, never correctness.

Build user dictionary

Build a user dictionary from a CSV file:

% lindera build \
  --src ./resources/user_dict/unidic_simple_userdic.csv \
  --dest ./resources/user_dict \
  --metadata ./lindera-unidic/metadata.json \
  --user

For more details about user dictionary format, see Dictionary Format.

Embedding in binary

To embed the UniDic dictionary directly into the binary:

cargo build --features=embed-unidic

This allows using embedded://unidic as the dictionary path without external dictionary files.

Examples

This page shows tokenization examples using the UniDic dictionary.

Tokenize with external UniDic

% echo "日本語の形態素解析を行うことができます。" | lindera tokenize \
  --dict /tmp/lindera-unidic-2.1.2
日本    名詞,固有名詞,地名,国,*,*,ニッポン,日本,日本,ニッポン,日本,ニッポン,固,*,*,*,*
語      名詞,普通名詞,一般,*,*,*,ゴ,語,語,ゴ,語,ゴ,漢,*,*,*,*
の      助詞,格助詞,*,*,*,*,ノ,の,の,ノ,の,ノ,和,*,*,*,*
形態    名詞,普通名詞,一般,*,*,*,ケイタイ,形態,形態,ケータイ,形態,ケータイ,漢,*,*,*,*
素      接尾辞,名詞的,一般,*,*,*,ソ,素,素,ソ,素,ソ,漢,*,*,*,*
解析    名詞,普通名詞,サ変可能,*,*,*,カイセキ,解析,解析,カイセキ,解析,カイセキ,漢,*,*,*,*
を      助詞,格助詞,*,*,*,*,ヲ,を,を,オ,を,オ,和,*,*,*,*
行う    動詞,一般,*,*,五段-ワア行,連体形-一般,オコナウ,行う,行う,オコナウ,行う,オコナウ,和,*,*,*,*
こと    名詞,普通名詞,一般,*,*,*,コト,事,こと,コト,こと,コト,和,コ濁,基本形,*,*
が      助詞,格助詞,*,*,*,*,ガ,が,が,ガ,が,ガ,和,*,*,*,*
でき    動詞,非自立可能,*,*,上一段-カ行,連用形-一般,デキル,出来る,でき,デキ,できる,デキル,和,*,*,*,*
ます    助動詞,*,*,*,助動詞-マス,終止形-一般,マス,ます,ます,マス,ます,マス,和,*,*,*,*
。      補助記号,句点,*,*,*,*,,。,。,,。,,記号,*,*,*,*
EOS

Notice that UniDic splits "日本語" into "日本" and "語", and "形態素" into "形態" and "素", reflecting its uniform word unit definitions.

Tokenize with embedded UniDic

% echo "日本語の形態素解析を行うことができます。" | lindera tokenize \
  --dict embedded://unidic
日本    名詞,固有名詞,地名,国,*,*,ニッポン,日本,日本,ニッポン,日本,ニッポン,固,*,*,*,*
語      名詞,普通名詞,一般,*,*,*,ゴ,語,語,ゴ,語,ゴ,漢,*,*,*,*
の      助詞,格助詞,*,*,*,*,ノ,の,の,ノ,の,ノ,和,*,*,*,*
形態    名詞,普通名詞,一般,*,*,*,ケイタイ,形態,形態,ケータイ,形態,ケータイ,漢,*,*,*,*
素      接尾辞,名詞的,一般,*,*,*,ソ,素,素,ソ,素,ソ,漢,*,*,*,*
解析    名詞,普通名詞,サ変可能,*,*,*,カイセキ,解析,解析,カイセキ,解析,カイセキ,漢,*,*,*,*
を      助詞,格助詞,*,*,*,*,ヲ,を,を,オ,を,オ,和,*,*,*,*
行う    動詞,一般,*,*,五段-ワア行,連体形-一般,オコナウ,行う,行う,オコナウ,行う,オコナウ,和,*,*,*,*
こと    名詞,普通名詞,一般,*,*,*,コト,事,こと,コト,こと,コト,和,コ濁,基本形,*,*
が      助詞,格助詞,*,*,*,*,ガ,が,が,ガ,が,ガ,和,*,*,*,*
でき    動詞,非自立可能,*,*,上一段-カ行,連用形-一般,デキル,出来る,でき,デキ,できる,デキル,和,*,*,*,*
ます    助動詞,*,*,*,助動詞-マス,終止形-一般,マス,ます,ます,マス,ます,マス,和,*,*,*,*
。      補助記号,句点,*,*,*,*,,。,。,,。,,記号,*,*,*,*
EOS

NOTE: To include UniDic dictionary in the binary, you must build with the --features=embed-unidic option.

Rust API example

use lindera::dictionary::load_dictionary;
use lindera::mode::Mode;
use lindera::segmenter::Segmenter;
use lindera::analysis::tokenizer::Tokenizer;
use lindera::LinderaResult;

fn main() -> LinderaResult<()> {
    let dictionary = load_dictionary("embedded://unidic")?;
    let segmenter = Segmenter::new(Mode::Normal, dictionary, None);
    let tokenizer = Tokenizer::new(segmenter);

    let text = "日本語の形態素解析を行うことができます。";
    let mut tokens = tokenizer.tokenize(text)?;
    for token in tokens.iter_mut() {
        let details = token.details().join(",");
        println!("{}\t{}", token.surface.as_ref(), details);
    }
    Ok(())
}

Lindera SudachiDict

Lindera SudachiDict is a Japanese dictionary crate based on SudachiDict, the actively maintained dictionary used by the Sudachi morphological analyzer. It is updated several times a year upstream, so recent vocabulary (令和, スマホ, 推し活, ...) is in-vocabulary. Each entry has 19 fields, including SudachiDict-specific columns such as the normalized form, split references, and synonym group IDs.

The dictionary source files are mirrored at lindera/sudachidict. The bundled version is 20260723 (small + core + notcore lexicons).

[!NOTE] This dictionary reproduces SudachiDict's vocabulary and lattice behavior, but not the Sudachi engine plugins (katakana/numeric joining, input normalization, A/B/C split modes). See Behavioral differences from Sudachi for details.

Contents

  • Dictionary Format -- Field definitions for system and user dictionaries
  • Build -- How to build the dictionary from source
  • Examples -- Tokenization examples

API Reference

Lindera SudachiDict

Dictionary version

This repository contains SudachiDict 20260723 (small + core + notcore lexicons).

Dictionary format

Refer to the SudachiDict documentation for details on the dictionary format and part-of-speech tags.

IndexName (Japanese)Name (English)Notes
0表層形Surface
1左文脈IDLeft context ID
2右文脈IDRight context ID
3コストCost
4見出し(解析結果表示用)Display surface
5品詞大分類Part-of-speech
6品詞中分類Part-of-speech subcategory 1
7品詞小分類Part-of-speech subcategory 2
8品詞細分類Part-of-speech subcategory 3
9活用型Conjugation type
10活用形Conjugation form
11読みReading
12正規化表記Normalized form
13辞書形IDDictionary form word ID
14分割タイプSplit modeA/B/C
15A単位分割情報Split references (A)
16B単位分割情報Split references (B)
17語構成Word structure
18同義語グループIDSynonym group IDs

[!NOTE] Because the display surface sits at index 4 (before the part-of-speech columns), the first token detail is the display surface -- not the part-of-speech as in IPADIC or UniDic. Code that reads the details positionally must account for this; schema-aware access such as token.get("part_of_speech") returns the part-of-speech directly. The part-of-speech token filters (japanese_stop_tags, japanese_keep_tags, japanese_compound_word, and japanese_number with tags) resolve the part-of-speech columns through the dictionary schema, so they match this dictionary the same way they match IPADIC or UniDic (see lindera/lindera#997).

User dictionary format (CSV)

Simple version

IndexName (Japanese)Name (English)Notes
0表層形Surface
1品詞大分類Part-of-speech
2読みReading

Detailed version

The detailed version follows the dictionary format above (19 columns).

IndexName (Japanese)Name (English)Notes
0-18-Same as the dictionary format
19--After 19, it can be freely expanded.

API reference

The API reference is available. Please see following URL:

Build

This page describes how to build the SudachiDict dictionary from source files.

Build system dictionary

Download the SudachiDict source archive and build the dictionary:

% curl -L -o /tmp/sudachidict-20260723.tar.gz "https://lindera.dev/sudachidict-20260723.tar.gz"
% tar zxvf /tmp/sudachidict-20260723.tar.gz -C /tmp

% lindera build \
  --src /tmp/sudachidict-20260723 \
  --dest /tmp/lindera-sudachidict \
  --metadata ./lindera-sudachidict/metadata.json

The archive bundles the raw lexicons (small + core + notcore), the connection matrix, and pre-aligned char.def / unk.def, so no manual preprocessing is needed. The built dictionary is about 570MB.

[!TIP] To build from a newer upstream SudachiDict release than the bundled one, see SudachiDict (Custom Build), which builds directly from the upstream raw distribution.

Build user dictionary

Build a user dictionary from a CSV file:

% lindera build \
  --src ./resources/user_dict/sudachidict_simple_userdic.csv \
  --dest ./resources/user_dict \
  --metadata ./lindera-sudachidict/metadata.json \
  --user

For more details about user dictionary format, see Dictionary Format.

Embedding in binary

To embed the SudachiDict dictionary directly into the binary:

cargo build --features=embed-sudachidict

This allows using embedded://sudachidict as the dictionary path without external dictionary files.

[!NOTE] The embedded dictionary adds roughly 570MB to the binary. If binary size matters, prefer building the dictionary to a directory as shown above and loading it by path.

Examples

This page shows tokenization examples using the SudachiDict dictionary.

Tokenize with external SudachiDict

% echo "令和五年に始まった。推し活が楽しい。" | lindera tokenize \
  --dict /tmp/lindera-sudachidict
令和	令和,名詞,固有名詞,一般,*,*,*,レイワ,令和,*,A,*,*,*,*
五	五,名詞,数詞,*,*,*,*,ゴ,五,*,A,*,*,*,017040
年	年,名詞,普通名詞,助数詞可能,*,*,*,ネン,年,*,A,*,*,*,*
に	に,助詞,格助詞,*,*,*,*,ニ,に,*,A,*,*,*,*
始まっ	始まっ,動詞,一般,*,*,五段-ラ行,連用形-促音便,ハジマッ,始まる,380965,A,*,*,*,000378
た	た,助動詞,*,*,*,助動詞-タ,終止形-一般,タ,た,83558,A,*,*,*,*
。	。,補助記号,句点,*,*,*,*,。,。,*,A,*,*,*,*
推し活	推し活,名詞,普通名詞,一般,*,*,*,オシカツ,推し活,*,A,*,*,*,*
が	が,助詞,格助詞,*,*,*,*,ガ,が,*,A,*,*,*,*
楽しい	楽しい,形容詞,一般,*,*,形容詞,終止形-一般,タノシイ,楽しい,519727,A,*,*,*,*
。	。,補助記号,句点,*,*,*,*,。,。,*,A,*,*,*,*
EOS

Notice that 令和 is a single proper noun (IPADIC and UniDic 2.1.2 split it into 令 and 和) and 推し活 is in-vocabulary. The first detail column is the display surface, followed by the part-of-speech columns and SudachiDict-specific columns (normalized form, split mode, synonym group IDs).

Tokenize with embedded SudachiDict

% echo "令和五年に始まった。推し活が楽しい。" | lindera tokenize \
  --dict embedded://sudachidict

The output is the same as above.

NOTE: To include SudachiDict dictionary in the binary, you must build with the --features=embed-sudachidict option.

Rust API example

use lindera::dictionary::load_dictionary;
use lindera::mode::Mode;
use lindera::segmenter::Segmenter;
use lindera::analysis::tokenizer::Tokenizer;
use lindera::LinderaResult;

fn main() -> LinderaResult<()> {
    let dictionary = load_dictionary("embedded://sudachidict")?;
    let segmenter = Segmenter::new(Mode::Normal, dictionary, None);
    let tokenizer = Tokenizer::new(segmenter);

    let text = "令和五年に始まった。推し活が楽しい。";
    let mut tokens = tokenizer.tokenize(text)?;
    for token in tokens.iter_mut() {
        // Schema-aware access: works regardless of column positions
        let pos = token.get("part_of_speech").unwrap_or_default().to_string();
        let details = token.details().join(",");
        println!("{}\t{}\t{}", token.surface.as_ref(), pos, details);
    }
    Ok(())
}

Lindera ko-dic

Lindera ko-dic is a Korean dictionary crate based on mecab-ko-dic.

Contents

  • Dictionary Format -- Field definitions for system and user dictionaries
  • Build -- How to build the dictionary from source
  • Examples -- Tokenization examples

API Reference

Lindera ko-dic

Dictionary version

This repository contains mecab-ko-dic.

Dictionary format

Information about the dictionary format and part-of-speech tags used by mecab-ko-dic id documented in this Google Spreadsheet, linked to from mecab-ko-dic's repository readme.

Note how ko-dic has one less feature column than NAIST JDIC, and has an altogether different set of information (e.g. doesn't provide the "original form" of the word).

The tags are a slight modification of those specified by 세종 (Sejong), whatever that is. The mappings from Sejong to mecab-ko-dic's tag names are given in tab 태그 v2.0 on the above-linked spreadsheet.

The dictionary format is specified fully (in Korean) in tab 사전 형식 v2.0 of the spreadsheet. Any blank values default to *.

IndexName (Korean)Name (English)Notes
0표면Surface
1왼쪽 문맥 IDLeft context ID
2오른쪽 문맥 IDRight context ID
3비용Cost
4품사 태그Part-of-speech tagSee 태그 v2.0 tab on spreadsheet
5의미 부류Meaning(too few examples for me to be sure)
6종성 유무Presence or absenceT for true; F for false; else *
7읽기Readingusually matches surface, but may differ for foreign words e.g. Chinese character words
8타입TypeOne of: Inflect (활용); Compound (복합명사); or Preanalysis (기분석)
9첫번째 품사First part-of-speeche.g. given a part-of-speech tag of "VV+EM+VX+EP", would return VV
10마지막 품사Last part-of-speeche.g. given a part-of-speech tag of "VV+EM+VX+EP", would return EP
11표현Expression활용, 복합명사, 기분석이 어떻게 구성되는지 알려주는 필드 -- Field that tells how inflected forms, compound nouns, and pre-analyzed entries are composed

Left-space penalty

mecab-ko (the MeCab fork mecab-ko-dic is built for) raises the connection cost of a word whose left side is preceded by skipped whitespace (whitespace that the previous word's surface does not end with) when its pos-id.def id is listed in dicrc:

left-space-penalty-factor = 100,3000,120,6000,172,3000,183,3000,184,3000,185,3000,200,3000,210,6000,220,3000,221,3000,222,3000,230,3000

Lindera does not store pos-id.def ids, but every listed id corresponds to the entry's first part-of-speech tag (for Inflect rows, the First part-of-speech column), so the same rules are written by tag. ko-dic ships them in its metadata.json (space_penalty), and Segmenter::new applies them by default. Opt out with Segmenter::space_penalty(None), "space_penalty": false in the segmenter config, or lindera tokenize --disable-space-penalty; explicit rules go through Segmenter::space_penalty, a space_penalty object, or --space-penalty-rules. The shipped rules are present only in a ko-dic built by Lindera 6.1.0 or later; with a ko-dic from the v6.0.0 release the penalty silently stays off until the dictionary is rebuilt, while --space-penalty-rules works regardless.

pos-id.def idsFirst part-of-speech tagsCost
100, 200 (Inflect)EC, EF, EP, ETM, ETN3000
172, 230 (Inflect)VCP3000
183, 184, 185, 220, 221, 222 (Inflect)XSA, XSN, XSV3000
120, 210 (Inflect)JC, JKB, JKC, JKG, JKO, JKQ, JKS, JKV, JX6000
{
  "rules": [
    { "pos": ["EC", "EF", "EP", "ETM", "ETN", "VCP", "XSA", "XSN", "XSV"], "cost": 3000 },
    { "pos": ["JC", "JKB", "JKC", "JKG", "JKO", "JKQ", "JKS", "JKV", "JX"], "cost": 6000 }
  ]
}
echo "검색 이 잘 된다" | lindera tokenize --dict embedded://ko-dic

이 after the space is read as the interjection IC (as mecab-ko does) instead of the subject particle JKS; with --disable-space-penalty it reads as JKS. See the Segmenter documentation for the semantics.

Whitespace

mecab-ko skips whitespace when it builds the lattice, so a word after a space connects to the word before it. The SPACE unknown-word entry (unk.def: SPACE,1798,3563,996) is therefore never connected to anything during training, and its row and column of matrix.def are all zero. Lindera skips whitespace the same way by default. With --disable-skip-whitespace (Segmenter::skip_whitespace(false), "skip_whitespace": false) whitespace stays in the lattice as that entry, which resets the context at every space: 2년 전 대회 then reads 전 as 저/NP + ㄴ/JX instead of the noun NNG, and 하고 있다 reads 있 as a main verb instead of the auxiliary VX.

User dictionary format (CSV)

Simple version

IndexName (Korean)Name (English)Notes
0표면Surface
1품사 태그part-of-speech tagSee 태그 v2.0 tab on spreadsheet
2읽기readingusually matches surface, but may differ for foreign words e.g. Chinese character words

Detailed version

IndexName (Korean)Name (English)Notes
0표면Surface
1왼쪽 문맥 IDLeft context ID
2오른쪽 문맥 IDRight context ID
3비용Cost
4품사 태그part-of-speech tagSee 태그 v2.0 tab on spreadsheet
5의미 부류meaning(too few examples for me to be sure)
6종성 유무presence or absenceT for true; F for false; else *
7읽기readingusually matches surface, but may differ for foreign words e.g. Chinese character words
8타입typeOne of: Inflect (활용); Compound (복합명사); or Preanalysis (기분석)
9첫번째 품사first part-of-speeche.g. given a part-of-speech tag of "VV+EM+VX+EP", would return VV
10마지막 품사last part-of-speeche.g. given a part-of-speech tag of "VV+EM+VX+EP", would return EP
11표현expression활용, 복합명사, 기분석이 어떻게 구성되는지 알려주는 필드 -- Field that tells how inflected forms, compound nouns, and pre-analyzed entries are composed
12--After 12, it can be freely expanded.

API reference

The API reference is available. Please see following URL:

Build

Build system dictionary

Download and extract the mecab-ko-dic source files, then build the dictionary:

% curl -L -o /tmp/mecab-ko-dic-2.1.1-20180720.tar.gz "https://Lindera.dev/mecab-ko-dic-2.1.1-20180720.tar.gz"
% tar zxvf /tmp/mecab-ko-dic-2.1.1-20180720.tar.gz -C /tmp
% lindera build \
  --src /tmp/mecab-ko-dic-2.1.1-20180720 \
  --dest /tmp/lindera-ko-dic-2.1.1-20180720 \
  --metadata ./lindera-ko-dic/metadata.json \
  --context-id-freq ./lindera-ko-dic/context_id_freq.txt

[!TIP] lindera-ko-dic/metadata.json sets connection_id_mapping: true, so the builder relabels the connection-cost matrix's context IDs by access frequency to improve cache locality when looking up connection costs. Passing --context-id-freq / -f with the bundled context_id_freq.txt histogram gives this remapping real corpus frequency data to rank IDs by. Omitting the flag silently falls back to a much weaker entry-count-based proxy instead of failing, so the build still succeeds but without the full benefit. Either way, tokenization output is unaffected -- the remap is a bijective relabeling that only changes a build-time optimization, never correctness.

Build user dictionary

% lindera build \
  --src ./resources/user_dict/ko-dic_simple_userdic.csv \
  --dest ./resources/user_dict \
  --metadata ./lindera-ko-dic/metadata.json \
  --user

Embedding the dictionary

To embed the ko-dic dictionary directly into the binary, build with the following feature flag:

% cargo build --features=embed-ko-dic

Examples

Tokenize with external ko-dic

% echo "한국어의형태해석을실시할수있습니다." | lindera tokenize \
  --dict /tmp/lindera-ko-dic-2.1.1-20180720
한국어  NNG,*,F,한국어,Compound,*,*,한국/NNG/*+어/NNG/*
의      JKG,*,F,의,*,*,*,*
형태    NNG,*,F,형태,*,*,*,*
해석    NNG,행위,T,해석,*,*,*,*
을      JKO,*,T,을,*,*,*,*
실시    NNG,행위,F,실시,*,*,*,*
할      XSV+ETM,*,T,할,Inflect,XSV,ETM,하/XSV/*+ᆯ/ETM/*
수      NNB,*,F,수,*,*,*,*
있      VV,*,T,있,*,*,*,*
습니다  EF,*,F,습니다,*,*,*,*
.       SF,*,*,*,*,*,*,*
EOS

Tokenize with embedded ko-dic

% echo "한국어의형태해석을실시할수있습니다." | lindera tokenize \
  --dict embedded://ko-dic
한국어  NNG,*,F,한국어,Compound,*,*,한국/NNG/*+어/NNG/*
의      JKG,*,F,의,*,*,*,*
형태    NNG,*,F,형태,*,*,*,*
해석    NNG,행위,T,해석,*,*,*,*
을      JKO,*,T,을,*,*,*,*
실시    NNG,행위,F,실시,*,*,*,*
할      XSV+ETM,*,T,할,Inflect,XSV,ETM,하/XSV/*+ᆯ/ETM/*
수      NNB,*,F,수,*,*,*,*
있      VV,*,T,있,*,*,*,*
습니다  EF,*,F,습니다,*,*,*,*
.       SF,*,*,*,*,*,*,*
EOS

NOTE: To include ko-dic dictionary in the binary, you must build with the --features=embed-ko-dic option.

Rust API example

use lindera::dictionary::load_dictionary;
use lindera::mode::Mode;
use lindera::segmenter::Segmenter;
use lindera::analysis::tokenizer::Tokenizer;
use lindera::LinderaResult;

fn main() -> LinderaResult<()> {
    let dictionary = load_dictionary("embedded://ko-dic")?;
    let segmenter = Segmenter::new(Mode::Normal, dictionary, None);
    let tokenizer = Tokenizer::new(segmenter);

    let text = "한국어의형태해석을실시할수있습니다.";
    let mut tokens = tokenizer.tokenize(text)?;
    for token in tokens.iter_mut() {
        let details = token.details().join(",");
        println!("{}\t{}", token.surface.as_ref(), details);
    }
    Ok(())
}

Lindera CC-CEDICT

Lindera CC-CEDICT is a Chinese dictionary crate based on CC-CEDICT-MeCab.

Contents

  • Dictionary Format -- Field definitions for system and user dictionaries
  • Build -- How to build the dictionary from source
  • Examples -- Tokenization examples

API Reference

Lindera CC-CE-DICT

Dictionary version

This repository contains CC-CEDICT-MeCab.

Dictionary format

IndexName (Chinese)Name (English)Notes
0表面形式Surface
1左语境IDLeft context ID
2右语境IDRight context ID
3成本Cost
4词类Part-of-speech
5词类1Part-of-speech subcategory 1
6词类2Part-of-speech subcategory 2
7词类3Part-of-speech subcategory 3
8併音Pinyin
9繁体字Traditional
10簡体字Simplified
11定义Definition

User dictionary format (CSV)

Simple version

IndexName (Chinese)Name (English)Notes
0表面形式Surface
1词类Part-of-speech
2併音Pinyin

Detailed version

IndexName (Chinese)Name (English)Notes
0表面形式Surface
1左语境IDLeft context ID
2右语境IDRight context ID
3成本Cost
4词类Part-of-speech
5词类1Part-of-speech subcategory 1
6词类2Part-of-speech subcategory 2
7词类3Part-of-speech subcategory 3
8併音Pinyin
9繁体字Traditional
10簡体字Simplified
11定义Definition
12--After 12, it can be freely expanded.

API reference

The API reference is available. Please see following URL:

Build

Build system dictionary

Download and extract the CC-CEDICT-MeCab source files, then build the dictionary:

% curl -L -o /tmp/CC-CEDICT-MeCab-0.1.0-20261021.tar.gz "https://lindera.dev/CC-CEDICT-MeCab-0.1.0-20261021.tar.gz"
% tar zxvf /tmp/CC-CEDICT-MeCab-0.1.0-20261021.tar.gz -C /tmp
% lindera build \
  --src /tmp/CC-CEDICT-MeCab-0.1.0-20261021 \
  --dest /tmp/lindera-cc-cedict-0.1.0-20261021 \
  --metadata ./lindera-cc-cedict/metadata.json

Build user dictionary

% lindera build \
  --src ./resources/user_dict/cc-cedict_simple_userdic.csv \
  --dest ./resources/user_dict \
  --metadata ./lindera-cc-cedict/metadata.json \
  --user

Embedding the dictionary

To embed the CC-CEDICT dictionary directly into the binary, build with the following feature flag:

% cargo build --features=embed-cc-cedict

Examples

Tokenize with external CC-CEDICT

% echo "可以进行中文形态学分析。" | lindera tokenize \
  --dict /tmp/lindera-cc-cedict-0.1.0-20261021
可以    *,*,*,*,ke3 yi3,可以,可以,can/may/possible/able to/not bad/pretty good/
进行    *,*,*,*,jin4 xing2,進行,进行,to advance/to conduct/underway/in progress/to do/to carry out/to carry on/to execute/
中文    *,*,*,*,Zhong1 wen2,中文,中文,Chinese language/
形态学  *,*,*,*,xing2 tai4 xue2,形態學,形态学,morphology (in biology or linguistics)/
分析    *,*,*,*,fen1 xi1,分析,分析,to analyze/analysis/CL:個|个[ge4]/
。      *,*,*,*,*,*,*,*
EOS

Tokenize with embedded CC-CEDICT

% echo "可以进行中文形态学分析。" | lindera tokenize \
  --dict embedded://cc-cedict
可以    *,*,*,*,ke3 yi3,可以,可以,can/may/possible/able to/not bad/pretty good/
进行    *,*,*,*,jin4 xing2,進行,进行,to advance/to conduct/underway/in progress/to do/to carry out/to carry on/to execute/
中文    *,*,*,*,Zhong1 wen2,中文,中文,Chinese language/
形态学  *,*,*,*,xing2 tai4 xue2,形態學,形态学,morphology (in biology or linguistics)/
分析    *,*,*,*,fen1 xi1,分析,分析,to analyze/analysis/CL:個|个[ge4]/
。      *,*,*,*,*,*,*,*
EOS

NOTE: To include CC-CEDICT dictionary in the binary, you must build with the --features=embed-cc-cedict option.

Rust API example

use lindera::dictionary::load_dictionary;
use lindera::mode::Mode;
use lindera::segmenter::Segmenter;
use lindera::analysis::tokenizer::Tokenizer;
use lindera::LinderaResult;

fn main() -> LinderaResult<()> {
    let dictionary = load_dictionary("embedded://cc-cedict")?;
    let segmenter = Segmenter::new(Mode::Normal, dictionary, None);
    let tokenizer = Tokenizer::new(segmenter);

    let text = "可以进行中文形态学分析。";
    let mut tokens = tokenizer.tokenize(text)?;
    for token in tokens.iter_mut() {
        let details = token.details().join(",");
        println!("{}\t{}", token.surface.as_ref(), details);
    }
    Ok(())
}

Lindera Jieba

Lindera Jieba is a Chinese dictionary crate based on mecab-jieba.

Contents

  • Dictionary Format -- Field definitions for system and user dictionaries
  • Build -- How to build the dictionary from source
  • Examples -- Tokenization examples

API Reference

Lindera Jieba

Dictionary version

This repository contains mecab-jieba.

Dictionary format

IndexName (Chinese)Name (English)Notes
0表面形式Surface
1左语境IDLeft context ID
2右语境IDRight context ID
3成本Cost
4词类Part-of-speech
5字符类型Character type
6併音Pinyin
7繁体字Traditional
8簡体字Simplified
9定义Definition
10字符数Character count
11首字符First character
12末字符Last character
13频率等级Frequency band

User dictionary format (CSV)

Simple version

IndexName (Chinese)Name (English)Notes
0表面形式Surface
1词类Part-of-speech
2併音Pinyin

Detailed version

IndexName (Chinese)Name (English)Notes
0表面形式Surface
1左语境IDLeft context ID
2右语境IDRight context ID
3成本Cost
4词类Part-of-speech
5字符类型Character type
6併音Pinyin
7繁体字Traditional
8簡体字Simplified
9定义Definition
10字符数Character count
11首字符First character
12末字符Last character
13频率等级Frequency band
14--After 14, it can be freely expanded.

API reference

The API reference is available. Please see following URL:

Build

Build system dictionary

Download and extract the mecab-jieba source files, then build the dictionary:

% curl -L -o /tmp/mecab-jieba-0.1.3.tar.gz "https://lindera.dev/mecab-jieba-0.1.3.tar.gz"
% tar zxvf /tmp/mecab-jieba-0.1.3.tar.gz -C /tmp
% lindera build \
  --src /tmp/mecab-jieba-0.1.3/dict-src \
  --dest /tmp/lindera-jieba-0.1.3 \
  --metadata ./lindera-jieba/metadata.json

Build user dictionary

% lindera build \
  --src ./resources/user_dict/jieba_simple_userdic.csv \
  --dest ./resources/user_dict \
  --metadata ./lindera-jieba/metadata.json \
  --user

Embedding the dictionary

To embed the Jieba dictionary directly into the binary, build with the following feature flag:

% cargo build --features=embed-jieba

Examples

Tokenize with external Jieba

% echo "可以进行中文形态学分析。" | lindera tokenize \
  --dict /tmp/lindera-jieba-0.1.3
可以    c,CHINESE,ke3 yi3,可以,可以,can/may/possible/able to/not bad/pretty good,2,可,以,high
进行    v,CHINESE,jin4 xing2,進行,进行,(of a process etc) to proceed; to be in progress; to be underway/(of people) to carry out; to conduct (an investigation or discussion etc)/(of an army etc) to be on the march; to advance,2,进,行,high
中文    nz,CHINESE,Zhong1 wen2,中文,中文,Chinese language,2,中,文,high
形态    n,CHINESE,xing2 tai4,形態,形态,shape/form/pattern/morphology,2,形,态,high
学      n,CHINESE,xue2,學,学,to learn/to study/to imitate/science/-ology,1,学,学,high
分析    vn,CHINESE,fen1 xi1,分析,分析,to analyze/analysis/CL:個|个[ge4],2,分,析,high
。      w,*,*,*,*,*,*,*,*,*
EOS

Tokenize with embedded Jieba

% echo "可以进行中文形态学分析。" | lindera tokenize \
  --dict embedded://jieba
可以    c,CHINESE,ke3 yi3,可以,可以,can/may/possible/able to/not bad/pretty good,2,可,以,high
进行    v,CHINESE,jin4 xing2,進行,进行,(of a process etc) to proceed; to be in progress; to be underway/(of people) to carry out; to conduct (an investigation or discussion etc)/(of an army etc) to be on the march; to advance,2,进,行,high
中文    nz,CHINESE,Zhong1 wen2,中文,中文,Chinese language,2,中,文,high
形态    n,CHINESE,xing2 tai4,形態,形态,shape/form/pattern/morphology,2,形,态,high
学      n,CHINESE,xue2,學,学,to learn/to study/to imitate/science/-ology,1,学,学,high
分析    vn,CHINESE,fen1 xi1,分析,分析,to analyze/analysis/CL:個|个[ge4],2,分,析,high
。      w,*,*,*,*,*,*,*,*,*
EOS

NOTE: To include Jieba dictionary in the binary, you must build with the --features=embed-jieba option.

Rust API example

use lindera::dictionary::load_dictionary;
use lindera::mode::Mode;
use lindera::segmenter::Segmenter;
use lindera::analysis::tokenizer::Tokenizer;
use lindera::LinderaResult;

fn main() -> LinderaResult<()> {
    let dictionary = load_dictionary("embedded://jieba")?;
    let segmenter = Segmenter::new(Mode::Normal, dictionary, None);
    let tokenizer = Tokenizer::new(segmenter);

    let text = "可以进行中文形态学分析。";
    let mut tokens = tokenizer.tokenize(text)?;
    for token in tokens.iter_mut() {
        let details = token.details().join(",");
        println!("{}\t{}", token.surface.as_ref(), details);
    }
    Ok(())
}

SudachiDict (Custom Build)

This page describes how to build SudachiDict — the actively maintained Japanese dictionary used by Sudachi — as a Lindera dictionary directly from the upstream raw distribution, using only the generic dictionary builder and a metadata file. No engine changes are required.

[!TIP] SudachiDict is also available as an official dictionary crate: lindera-sudachidict. Prefer the crate for normal use (--features embed-sudachidict, lindera download sudachidict, or the pre-built release archive). Use this page when you want to track a newer upstream release than the bundled one, or to build a custom variant (for example, small + core only).

Related issue: lindera/lindera#487

[!NOTE] This recipe produces a dictionary whose vocabulary and lattice behavior match Sudachi, but it does not reproduce Sudachi's engine plugins. See Behavioral differences below before relying on it.

Why

The classic MeCab-format dictionaries (IPADIC, UniDic 2.1.2) stopped receiving vocabulary updates years ago. SudachiDict is updated several times a year and is distributed in a MeCab-compatible raw CSV format, which the generic builder can compile directly. The official lindera-sudachidict crate bundles the 20260723 release; building from the upstream raw distribution with this recipe lets you pick up newer releases yourself. With the 20260723 release:

  • 令和 is a single proper noun (IPADIC and UniDic 2.1.2 split it into 令|和)
  • スマホ, テレワーク, 推し活, コロナ禍 are in-vocabulary
  • On a 321K-character novel from 1905, the unknown-token rate drops from 2.00% (IPADIC) to 0.51%

Sources

SudachiDict raw lexicons (check the listing for the latest date):

% mkdir -p /tmp/sudachidict-src
% cd /tmp/sudachidict-src
% for f in small_lex core_lex notcore_lex; do
    curl -LO "http://sudachi.s3-website-ap-northeast-1.amazonaws.com/sudachidict-raw/20260723/${f}.zip"
    unzip -o "${f}.zip" && rm "${f}.zip"
  done

The connection matrix. SudachiDict's own build downloads this file — it is the UniDic 2.1.2 matrix (SudachiDict entries use UniDic context IDs):

% curl -LO "https://d2ej7fkh96fzlu.cloudfront.net/sudachidict-raw/matrix.def.zip"
% unzip -o matrix.def.zip && rm matrix.def.zip

Character and unknown-word definitions from the UniDic 2.1.2 source (the same archive lindera-unidic builds from):

% curl -L -o /tmp/unidic-mecab-2.1.2.tar.gz "https://Lindera.dev/unidic-mecab-2.1.2.tar.gz"
% tar zxf /tmp/unidic-mecab-2.1.2.tar.gz -C /tmp
% cp /tmp/unidic-mecab-2.1.2/char.def /tmp/unidic-mecab-2.1.2/unk.def .

Align unk.def with the SudachiDict column layout

SudachiDict lexicon rows have 19 columns; column 4 (0-based) is the display surface, so morphological details start at column 5. UniDic's unk.def has its part-of-speech at column 4. Insert a placeholder display column so unknown words expose their details at the same indices as dictionary words:

% awk -F, 'BEGIN{OFS=","} {out=$1 OFS $2 OFS $3 OFS $4 OFS "*"; for(i=5;i<=NF;i++) out=out OFS $i; print out}' unk.def > unk.def.tmp && mv unk.def.tmp unk.def

Metadata

Save as sudachidict-metadata.json. The first four fields are the builder's reserved columns; the rest name SudachiDict's columns in order:

{
  "name": "sudachidict",
  "encoding": "UTF-8",
  "default_word_cost": -10000,
  "default_left_context_id": 0,
  "default_right_context_id": 0,
  "default_field_value": "*",
  "flexible_csv": true,
  "skip_invalid_cost_or_id": true,
  "dictionary_schema": {
    "fields": [
      "surface",
      "left_context_id",
      "right_context_id",
      "cost",
      "display_surface",
      "part_of_speech",
      "part_of_speech_subcategory_1",
      "part_of_speech_subcategory_2",
      "part_of_speech_subcategory_3",
      "conjugation_type",
      "conjugation_form",
      "reading",
      "normalized_form",
      "dictionary_form_id",
      "split_mode",
      "split_a",
      "split_b",
      "word_structure",
      "synonym_group_ids"
    ]
  },
  "user_dictionary_schema": { "fields": ["surface", "part_of_speech", "reading"] },
  "skip_whitespace": false
}

skip_whitespace: false keeps whitespace in the lattice, as Sudachi does and as SudachiDict's costs assume; without it, Lindera skips whitespace as MeCab does (see Segmenter).

Build and verify

% lindera build \
  --src /tmp/sudachidict-src \
  --dest /tmp/lindera-sudachidict \
  --metadata ./sudachidict-metadata.json

Building small + core + notcore (2026-07-23) with Lindera v6 takes about 10 seconds and produces a ~570MB dictionary. Note that built dictionaries must be rebuilt when the on-disk dictionary format version changes across Lindera releases (see Migration v5 to v6).

% echo "令和五年に始まった。推し活が楽しい。" | lindera tokenize --dict /tmp/lindera-sudachidict
令和	令和,名詞,固有名詞,一般,*,*,*,レイワ,令和,*,A,*,*,*,*
五	五,名詞,数詞,*,*,*,*,ゴ,五,*,A,*,*,*,017040
年	年,名詞,普通名詞,助数詞可能,*,*,*,ネン,年,*,A,*,*,*,*
...
推し活	推し活,名詞,普通名詞,一般,*,*,*,オシカツ,推し活,*,A,*,*,*,*

Details expose SudachiDict's extra columns (normalized form, split references, synonym group IDs), so downstream code can use them.

[!NOTE] Because display_surface sits before the part-of-speech columns, details[0] is the display surface — not the part-of-speech as in IPADIC or UniDic. Code that reads the details positionally must account for this; schema-aware access such as token.get("part_of_speech") returns the part-of-speech directly. The part-of-speech token filters (japanese_stop_tags, japanese_keep_tags, japanese_compound_word, and japanese_number with tags) resolve the part-of-speech columns through the dictionary schema, so they match this dictionary the same way they match IPADIC or UniDic.

Behavioral differences from Sudachi

The lattice layer is verified equivalent: the matrix file is byte-identical (sha256) to the one SudachiDict's official build uses, and with Sudachi's path-rewrite and input-normalization plugins disabled, the Sudachi engine produces the same minimum-cost paths as this dictionary under Lindera. The remaining differences are Sudachi engine plugins, not dictionary data:

Sudachi featureEffectStatus under Lindera
JoinKatakanaOovPluginRewrites paths to join katakana runs (e.g. サブスク instead of the cheaper サブ+スク path)Not replicated
JoinNumericPluginJoins numeric sequencesNot replicated
DefaultInputTextPluginNFKC + lowercase normalization before lookup (e.g. half-width AI hits a normalized entry)Partially available via Lindera character filters (unicode_normalize)
A/B/C split modesSplits lexicon entries via split_a / split_b referencesNot applied; entries are used whole. The split references are preserved in details, so a post-processing step can apply them

License

SudachiDict is Apache-2.0. matrix.def, char.def, and unk.def are part of UniDic (BSD/GPL/LGPL triple license — see the SudachiDict LEGAL notice). A dictionary built with this recipe combines both; review the licenses before redistribution.

Migrating from v3 to v4

Lindera v4.0.0 is a major release that bundles the breaking changes that were deliberately deferred during the v3 series. Each change is small on its own; this guide lists every one so you can upgrade with confidence.

The breaking changes were verified mechanically against a cargo public-api diff of the v3.0.7 and v4 public surfaces.

Overview

ChangeAffectsWhat you do
Default schema field names use pos_detail_*Python, Node.js, Ruby, PHPUpdate field names middle_pos / small_pos / fine_pos to pos_detail_1 / pos_detail_2 / pos_detail_3
Token.details is always a listPython, Node.js, RubyRemove null / None / nil handling
Binding Segmenter removedPython, WASMUse the tokenizer instead
LINDERA_CACHE env var removedRust build, CLIUse LINDERA_DICTIONARIES_PATH
User-dictionary binary format changedAll (prebuilt .bin)Rebuild user dictionaries from their CSV source
lindera-dictionary viterbi internals encapsulatedRust crate usersUse the new accessors

The top-level lindera crate's public Rust API is unchanged between v3.0.7 and v4.

Default dictionary schema field names

Schema.create_default() (and the default dictionary schema) now names the three part-of-speech detail fields pos_detail_1, pos_detail_2, and pos_detail_3 (indices 5, 6, 7), instead of middle_pos, small_pos, and fine_pos. This makes every binding match the core lindera::dictionary::Schema::default(), which already used pos_detail_*.

This affects the Python, Node.js, Ruby, and PHP bindings. The WASM binding already used pos_detail_*, so it is unchanged.

In Python:

schema = Schema.create_default()
# v3: schema.fields[5] == "middle_pos"
# v4: schema.fields[5] == "pos_detail_1"

# v3
index = schema.get_field_index("middle_pos")
# v4
index = schema.get_field_index("pos_detail_1")

If you reference these field names by string anywhere (lookups, custom schemas, serialized configuration), update them to the pos_detail_* form.

Token.details is always a list

Token.details is now always a list of strings and is never null / None / nil. A token with no details is represented by an empty list. Previously the Python, Node.js, and Ruby bindings wrapped it in a nullable type even though it was always populated in practice; the PHP and WASM bindings were already non-nullable.

In Python the type changes from list[str] | None to list[str]:

# v3 — defensive null check was required by the type
if token.details is not None:
    pos = token.details[0]

# v4 — details is always a list
pos = token.details[0]

In Node.js the type changes from Array<string> | null to Array<string>, and in Ruby from Array | nil to Array. Remove any null / nil checks accordingly.

Binding Segmenter removed

The vestigial Segmenter wrappers were removed from the bindings. They had no constructor and could not be used; segmentation has always been reachable through the tokenizer.

  • Python: the lindera.segmenter submodule and lindera.segmenter.Segmenter are gone.
  • WASM: the Segmenter class export is gone.

Tokenize through the tokenizer instead:

from lindera import Tokenizer, TokenizerBuilder

tokenizer = TokenizerBuilder().build()
tokens = tokenizer.tokenize("関西国際空港")

LINDERA_CACHE environment variable removed

The deprecated LINDERA_CACHE build-time environment variable was removed. Use LINDERA_DICTIONARIES_PATH, which has been the supported variable for several releases:

# v3 (deprecated)
export LINDERA_CACHE=/path/to/dicts

# v4
export LINDERA_DICTIONARIES_PATH=/path/to/dicts

User-dictionary binary format changed

User dictionaries now use the same 8-bit variant-count encoding as system dictionaries (supporting up to 255 variants per surface, previously 31). As a result, a user-dictionary .bin file built with v3 is decoded incorrectly by v4. There is no format-version guard, so the failure is silent — tokens are produced, but with the wrong details.

Rebuild user dictionaries from their CSV source with v4:

lindera build --user \
  --src user_dict.csv \
  --dest ./build \
  --metadata lindera-ipadic/metadata.json

If you load a user dictionary from a .csv file (rather than a prebuilt .bin), it is rebuilt at load time and no action is needed.

Rust library: lindera-dictionary viterbi internals

This affects only direct users of the lindera-dictionary crate; the lindera crate API is unchanged.

The internal viterbi structs no longer expose public fields. Use the accessors instead:

#![allow(unused)]
fn main() {
// v3 — direct field access
let id = word_id.id;
let cost = word_entry.word_cost;

// v4 — accessors
let id = word_id.id();
let cost = word_entry.word_cost();
}

Other changes in lindera_dictionary::viterbi:

  • EdgeType was removed.
  • WordEntry gained new(), word_cost(), and word_id(); WordId gained id().
  • WordEntry::serialize, WordEntry::deserialize, and WordEntry::SERIALIZED_LEN are no longer public.
  • util::read_aligned_file and the embedded_dictionary! macro were added.

This list was derived from the complete machine-generated cargo public-api diff for the lindera-dictionary crate between the v3.0.7 and v4 releases.

Upgrade checklist

  • Replace middle_pos / small_pos / fine_pos with pos_detail_1 / pos_detail_2 / pos_detail_3 (Python, Node.js, Ruby, PHP).
  • Remove null / None / nil checks on Token.details (Python, Node.js, Ruby).
  • Replace any use of the binding Segmenter with the tokenizer (Python, WASM).
  • Replace LINDERA_CACHE with LINDERA_DICTIONARIES_PATH.
  • Rebuild prebuilt user-dictionary .bin files from their CSV source.
  • Switch direct lindera-dictionary viterbi field access to the new accessors (Rust).

Migrating from v4 to v5

Lindera v5.0.0 restructures the workspace around a lean core: the default build of the lindera crate is now a pure morphological segmenter, and the dictionary-training pipeline lives in its own crate. This guide lists every breaking change and the one-line fixes for each.

Overview

ChangeAffectsWhat you do
Analysis chain moved to the new lindera-analysis crateRust users of Tokenizer, character filters, or token filtersDepend on lindera-analysis and update import paths
lindera-dictionary no longer has a train featureDirect lindera-dictionary --features train usersDepend on lindera-trainer (or the lindera facade's train feature)
Build cache variable renamedUsers setting LINDERA_DICTIONARIES_PATHRename it to LINDERA_BUILD_DICTIONARY_CACHE_DIR (the old name was removed in v7.0.0)

The language bindings (Python, Node.js, Ruby, PHP, WASM) and the CLI are unaffected: they enable the required features themselves, and their APIs and output are unchanged. Tokenization output is also unchanged — v5 produces byte-for-byte identical tokens to v4 for the same input and dictionary.

The analysis chain moved to lindera-analysis

In v5.0 the lindera crate is a pure morphological segmenter: the character_filter, token_filter, and tokenizer modules moved to the new lindera-analysis crate (mirroring Lucene's split between a tokenizer core and analyzer modules).

If you use Tokenizer or any filter, depend on the new crate and update the import paths — the APIs themselves are unchanged:

# v4
[dependencies]
lindera = "4.0"

# v5
[dependencies]
lindera = "5.0"
lindera-analysis = "5.0"
#![allow(unused)]
fn main() {
// v4
use lindera::tokenizer::Tokenizer;
use lindera::token_filter::japanese_stop_tags::JapaneseStopTagsTokenFilter;

// v5
use lindera_analysis::tokenizer::Tokenizer;
use lindera_analysis::token_filter::japanese_stop_tags::JapaneseStopTagsTokenFilter;
}

[!NOTE] Since v7.0.0 the lindera crate re-exports lindera-analysis as lindera::analysis, so the separate dependency above is no longer needed and the same items are available as lindera::analysis::…. See Migration v6 to v7.

If you only segment text, nothing changes in your code — and your dependency tree shrinks (kanaria, unicode-normalization, unicode-segmentation, unicode-blocks, and serde_yaml_ng are no longer built):

#![allow(unused)]
fn main() {
use std::borrow::Cow;

use lindera::dictionary::load_dictionary;
use lindera::mode::Mode;
use lindera::segmenter::Segmenter;

let dictionary = load_dictionary("/path/to/ipadic")?;
let segmenter = Segmenter::new(Mode::Normal, dictionary, None);
let tokens = segmenter.segment(Cow::Borrowed("関西国際空港限定トートバッグ"))?;
}

Training moved to the lindera-trainer crate

The CRF training pipeline (TrainerConfig, Trainer, Corpus, Model, SerializableModel) moved from lindera-dictionary's train-gated trainer module into the new lindera-trainer crate. As a result, lindera-dictionary no longer depends on lindera-crf or regex.

Through the lindera facade nothing changes — the train feature now pulls lindera-trainer and re-exports it under the same path:

#![allow(unused)]
fn main() {
// Works in both v4 and v5 (with the `train` feature):
use lindera::dictionary::trainer::{Corpus, Trainer, TrainerConfig};
}

Only direct users of lindera-dictionary --features train need to switch:

# v4
[dependencies]
lindera-dictionary = { version = "4.0", features = ["train"] }

# v5
[dependencies]
lindera-dictionary = "5.0"
lindera-trainer = "5.0"
#![allow(unused)]
fn main() {
// v4
use lindera_dictionary::trainer::{Corpus, Trainer, TrainerConfig};

// v5
use lindera_trainer::{Corpus, Trainer, TrainerConfig};
}

The lindera train → lindera export → lindera build CLI workflow is unchanged.

Build cache environment variable renamed

The build-time dictionary cache variable LINDERA_DICTIONARIES_PATH is renamed to LINDERA_BUILD_DICTIONARY_CACHE_DIR to make its contract explicit: it is read only at build time by the dictionary crates' build scripts, and it designates an auto-managed cache holding the downloaded dictionary archives and the built binary dictionaries.

The old name kept working through v6.x as a deprecated fallback (the new name won when both were set) and was removed in v7.0.0.

# v4
export LINDERA_DICTIONARIES_PATH=/path/to/cache

# v5
export LINDERA_BUILD_DICTIONARY_CACHE_DIR=/path/to/cache

Migrating from v5 to v6

Lindera v6.0.0 rebuilds prebuilt user dictionaries, changes tokenization output in two targeted ways (a Decompose-mode accuracy fix and a new default-on unknown-word feature), reshapes the JavaScript bindings' token type, renames the published binding packages on npm and PyPI to bare names, and renames several lindera-dictionary Rust APIs. Most users only need to rebuild their dictionaries; direct users of lindera_dictionary::viterbi/mode types, JavaScript users, and anyone relying on exact legacy output for out-of-vocabulary text have more to check.

If you are upgrading from v5.2 or earlier, the system-dictionary format change described below applies to you as well. It shipped in v5.3.0, so upgrading from v5.3.0 does not require rebuilding system dictionaries — but it does require rebuilding user dictionaries, for an unrelated reason.

Overview

ChangeAffectsWhat you do
Published package names change on npm and PyPInpm users of lindera-nodejs / lindera-wasm-web / lindera-wasm-bundler, PyPI users of lindera-pythonInstall lindera (npm, PyPI) or lindera-wasm (npm, WASM) and update require/import specifiers — see the next section
Prebuilt user-dictionary .bin files no longer loadAnyone loading a user dictionary from .binRebuild from the CSV source with lindera build --user
Trained model.dat files no longer loadAnyone re-using a model.dat produced by an earlier buildRe-run lindera train
Decompose-mode length penalty is now exact for non-3-byte charactersMode::Decompose output on text with 1-, 2-, or 4-byte UTF-8 charactersNothing — this is a correctness fix; re-check Decompose output if you pin it exactly
Unknown-word length ladder is on by defaultNormal- and Decompose-mode output on out-of-vocabulary textSet unknown_word_ladder(false) / --disable-unknown-word-ladder for v5-identical output
New max_grouping_len option (--max-grouping-len, config key, builder/worker setters)Anyone who wants MeCab's max-grouping-size capNothing — the default is unchanged (unbounded)
JS bindings return plain objects; the Token class is goneNode.js and WASM usersReplace token.getDetail(i) with token.details[i]; WASM users also switch to camelCase field names
Several lindera_dictionary::viterbi/mode items renamed or removed, and Lattice::set_text/set_text_nbest take two more argumentsDirect users of Lattice/Edge/Penalty/Mode (not the lindera crate's Segmenter/Tokenizer/Worker API)Update call sites per the table below
Built dictionary format is version 2 (dict.da → dict.trie + dict.valsidx) — shipped in v5.3.0Self-built system dictionaries created by v5.2 or earlierRebuild with lindera build, or re-download with lindera download
loadDictionaryFromBytes() takes 9 arguments — shipped in v5.3.0WASM users loading dictionaries from bytes, upgrading from v5.2 or earlierPass dictTrie and dictValsIdx instead of dictDa
OPFS DictionaryFiles replaces dictDa with dictTrie + dictValsIdx — shipped in v5.3.0WASM users of the opfs helpers, upgrading from v5.2 or earlierRe-download OPFS-cached dictionaries

Package renames on npm, PyPI, and crates.io

Starting with v6.0.0, the language bindings are published under bare names — the registry already scopes the package to its ecosystem, so the -nodejs/-python suffixes carried no information:

Registryv5 namev6 name
npm (Node.js)lindera-nodejslindera
npm (Node.js platform packages)lindera-nodejs-<platform> (e.g. lindera-nodejs-darwin-arm64)lindera-<platform> (e.g. lindera-darwin-arm64)
npm (WASM)lindera-wasm-web and lindera-wasm-bundlerlindera-wasm
PyPIlindera-pythonlindera

The RubyGems gem name (lindera) and the core Rust crates on crates.io are unchanged.

Node.js

Update the dependency and the module specifier; the API itself is unaffected:

// v5
const { TokenizerBuilder } = require("lindera-nodejs");

// v6
const { TokenizerBuilder } = require("lindera");

The platform packages (lindera-darwin-arm64, lindera-linux-x64-gnu, …) are optionalDependencies of the main package, so npm selects the right one automatically — you never install them by name.

The old lindera-nodejs and lindera-nodejs-* packages are npm-deprecated: they remain installable at their last v5 versions but receive no further releases.

Python

Only the install name changes — the import name was always lindera and stays that way:

# v5
pip install lindera-python

# v6
pip install lindera

A final lindera-python release is published as a transition stub that depends on lindera, so an unchanged pip install lindera-python still ends up with the real package during the transition. Update your requirements to lindera anyway — the stub will not be maintained.

WASM

The two v5 packages are consolidated into a single lindera-wasm package, built with wasm-pack --target web. A web-target build requires calling the default-exported async init function once before using any API:

import __wbg_init, { TokenizerBuilder } from "lindera-wasm";

await __wbg_init();
  • Coming from lindera-wasm-web: only the package name changes — your code already calls the init function.
  • Coming from lindera-wasm-bundler: install lindera-wasm and add the init call — the bundler-target build initialized implicitly, the web-target build does not. Modern bundlers consume the web-target build directly (Vite out of the box; Webpack 5 with the asyncWebAssembly experiment). See Browser Usage for bundler configuration.

The old lindera-wasm-web and lindera-wasm-bundler packages are npm-deprecated at their last v5 versions.

crates.io

The binding crates (lindera-python, lindera-nodejs, lindera-ruby, lindera-wasm) are no longer published to crates.io — they were never usable as Rust dependencies, only as sources for the registry packages above. The existing 5.3.0 releases remain available there. The core crates (lindera, lindera-dictionary, the dictionary crates, and so on) continue to be published as before.

[!NOTE] Since v7.0.0 the lindera crate is a facade over lindera-segmenter and lindera-analysis, and lindera-binding-core is published as lindera-binding. See Migration v6 to v7.

Prebuilt user dictionaries must be rebuilt

Lindera v6 upgrades daachorse from 4.x to 5.0, which changed the serialized form of the Aho-Corasick automaton that a user dictionary embeds. A .bin file built by any v5 release therefore fails to load:

LinderaError(kind=Deserialize, source=InvalidAutomatonError: invalid serialized automaton)

Unlike system dictionaries, user-dictionary .bin files carry no format_version, so there is no version check to produce a friendlier message — the failure surfaces as the deserialize error above.

Rebuild from the CSV source with v6:

lindera build --user \
  --src ./user_dict.csv \
  --dest ./build \
  --metadata ./lindera-ipadic/metadata.json

If you load a user dictionary from a .csv file rather than a prebuilt .bin, it is compiled at load time and no action is needed.

Trained model files must be regenerated

lindera train now writes a byte-reproducible model.dat: training the same inputs twice with the same flags produces identical bytes, so the artifact can be checksummed, cached and diffed.

Getting there required storing the connection matrix and the unknown-word categories as ordered maps rather than hash maps — rkyv lays an archived HashMap out in source iteration order, and that order is seeded per instance. This changes the on-disk layout, so a model.dat written by an earlier build fails to load:

failed to deserialize model: ... re-run `lindera train` to regenerate it.

model.dat is an intermediate between lindera train and lindera export, not a distributed artifact — regenerate it by re-running lindera train. Dictionaries already exported from an old model are unaffected, and the trained weights are identical either way: only the serialization order changed.

Two related notes:

  • SerializableModel::connection_matrix and SerializableModel::unk_categories are now BTreeMap rather than HashMap. This matters only if you read those public fields directly; the writer methods are unchanged.
  • Reproducibility holds for any --max-threads value: the gradient and the loss are summed over a fixed partition of the training data in a fixed order, so the thread count affects only speed, never the trained model.
  • lindera export output is byte-reproducible too: metadata.json no longer carries an updated_at timestamp, and every export writer emits entries in a deterministic order. Accordingly, the ModelInfo.updated_at field was removed from the Rust API (lindera-dictionary); old metadata.json files that still contain the key keep parsing.
  • DictionaryRewriter::rewrite_cached and clear_cache were removed: the cache was keyed on the full feature string, which is unique per row in a real lexicon, so it never hit and only retained memory. Call rewrite instead — it no longer requires &mut.
  • The features parameter of the six FeatureExtractor::extract_* methods changed from &[String] to &[&str], so callers no longer have to allocate one String per feature field. A caller holding a &[String] can pass &v.iter().map(|s| s.as_str()).collect::<Vec<_>>().
  • lindera-wasm removed the filesystem-based exports loadUserDictionary, buildDictionary, buildUserDictionary, and TokenizerBuilder.setUserDictionary(uri) (plus their snake_case aliases): there is no filesystem on wasm32-unknown-unknown, so they always failed at runtime. Load system dictionaries with loadDictionaryFromBytes() (or embedded://), and user dictionaries with the new loadUserDictionaryFromBytes() / loadUserDictionaryBinFromBytes() + setUserDictionaryInstance(). loadDictionary() now rejects file URIs and bare paths up front with an error naming the bytes API.
  • The fixed partition changed the summation order, so weights trained before this change are not byte-identical to weights trained after it — at any thread count, including --max-threads 1. Re-run lindera train if you need artifacts that compare equal from now on.

Dictionary format version 2 (shipped in v5.3.0)

This change shipped in v5.3.0, not in v6.0.0. If you are upgrading from v5.3.0 your system dictionaries already use format version 2 and load unchanged. Follow this section only when upgrading from v5.2 or earlier.

The system prefix dictionary is no longer stored as a serialized daachorse Aho-Corasick automaton (dict.da). The builder now constructs a char-wise double-array trie with crawdad at build time and writes it as dict.trie, together with a u32 prefix-sum index into the word values (dict.valsidx). At runtime the trie is walked in place over its serialized bytes: loading it is an O(1) header check instead of a deserialization pass, and under mmap it stays lazily paged like every other large component.

A built dictionary directory now contains these 9 files:

FileDescription
metadata.jsonDictionary metadata, now including format_version
dict.trieChar-wise double-array trie structure (new)
dict.valsidxWord values index (new)
dict.valsWord value data
dict.wordsidxWord details index
dict.wordsWord details (morphological features)
matrix.mtxConnection cost matrix
char_def.binCharacter category definitions
unk.binUnknown word dictionary

All files other than dict.trie and dict.valsidx are unchanged from v5; dict.da no longer exists.

Loading an old dictionary now fails with a clear error

metadata.json in a built dictionary records format_version: 2, and the loader checks it. Loading a dictionary built by v5.2 or earlier fails with an actionable error instead of misreading the headerless binary files:

Dictionary 'ipadic' has format version 1, but this build of Lindera reads format version 2. To fix this, rebuild it with `lindera build`, or download a matching prebuilt dictionary with `lindera download`.

Tokenization output changes

Unlike the format change above, these two changes can alter the tokens Lindera produces for the same input and dictionary.

Decompose-mode penalty accuracy fix

Mode::Decompose's length penalty used to approximate a span's character count as (stop_byte - start_byte) / 3, which is exact only for text made entirely of 3-byte UTF-8 characters (common Japanese kanji/kana). Lindera's Viterbi lattice is now character-indexed internally, so the penalty uses the real character count. This only changes Decompose output for spans containing 1-byte (ASCII), 2-byte, or 4-byte (e.g. emoji, some CJK extension characters) UTF-8 characters; pure-Japanese Decompose output is unaffected. There is no option to restore the old (inexact) behavior, since it was a bug fix rather than a behavior choice.

Unknown-word length ladder (new, on by default)

For a run of out-of-vocabulary characters, MeCab and Vibrato generate a "ladder" of candidate unknown-word lengths (1..=LENGTH, where LENGTH is a per-category field in the dictionary's char.def) and let the Viterbi search pick whichever length yields the lowest-cost path. Lindera parsed LENGTH from char.def but never used it at runtime, so it only ever generated a single 1-character candidate or (for grouping categories) one candidate spanning the whole run — never anything in between.

Starting in v6.0.0, Lindera generates this candidate ladder by default. For IPADIC this affects the KANJI, HIRAGANA, and KATAKANA categories (LENGTH=2); most other categories declare LENGTH=0 and are unaffected. The practical effect is that consecutive out-of-vocabulary kanji or kana characters can now be grouped into multi-character unknown words when that scores lower than splitting them one character at a time — for example, an unrecognized two-kanji compound is now tokenized as one word instead of two.

This is additive to (and independent of) max_grouping_len, the new option described below for capping whole-run grouping.

To reproduce v5-identical output on text containing out-of-vocabulary kanji/kana runs, disable the ladder:

let segmenter = Segmenter::new(mode, dictionary, None)
    .unknown_word_ladder(false);
lindera tokenize -d ipadic --disable-unknown-word-ladder input.txt

AnalysisWorker/SegmentWorker expose the same toggle via set_unknown_word_ladder(bool), and TokenizerBuilder via set_segmenter_unknown_word_ladder(bool).

New option: max_grouping_len (default unchanged)

v6 adds MeCab's max-grouping-size semantics: at each position, when the groupable run of out-of-vocabulary characters starting there extends more than max_grouping_len characters beyond the first, the grouped candidate is not emitted for that position; the single-character candidate (plus the length ladder and dictionary words) remains, and the remaining tail is grouped again once it fits, so no unknown token exceeds max_grouping_len + 1 characters. The default is unbounded, which is exactly what v5 did, so this changes nothing unless you opt in:

let segmenter = Segmenter::new(mode, dictionary, None)
    .max_grouping_len(Some(24)); // MeCab's own default
lindera tokenize -d ipadic --max-grouping-len 24 input.txt

Also available as set_max_grouping_len(Option<usize>) on SegmentWorker/AnalysisWorker, set_segmenter_max_grouping_len(usize) on TokenizerBuilder, and the max_grouping_len key in a segmenter config.

Rust API changes in lindera_dictionary

These affect only code that uses lindera_dictionary::viterbi::{Lattice, Edge} or lindera_dictionary::mode::{Mode, Penalty} directly (for example, custom lattice inspection or alternative tokenizer front-ends). The lindera crate's Segmenter, Tokenizer, SegmentWorker, and AnalysisWorker APIs are unaffected — they gained the unknown_word_ladder/max_grouping_len options above but no signatures changed.

v5v6Why
Lattice::text_len()Lattice::char_len()The lattice is now character-indexed instead of byte-indexed; the rename makes call sites that assumed bytes fail to compile instead of silently misbehaving
Lattice::edges_at(pos)Lattice::edges_at_char(pos)Same reason: pos is now a character position
Lattice::paths_at(pos)Lattice::paths_at_char(pos)Same reason
Lattice::capacity() / shrink_to(n)Same names, now denominated in characters (slots) instead of bytesA byte length remains a valid, conservative shrink_to argument (a sentence never has more characters than bytes)
Edge::num_chars()RemovedThe packed Edge no longer stores its end position (it always equals the lattice slot it lives in), so this can no longer be computed from the edge alone
Penalty::penalty(&Edge)Penalty::penalty(&Edge, num_chars: usize)The caller now passes the exact character span (see the Decompose accuracy fix above)
Mode::penalty_cost(&Edge)RemovedHad no callers in the codebase; re-implement via Penalty::penalty if needed
Lattice::set_text(..., search_mode)Lattice::set_text(..., search_mode, max_grouping_len, unknown_word_ladder)Two trailing arguments for the new options; pass None and true to match v6 defaults, or None and false for v5 behavior. set_text_nbest changed identically
—Lattice::take_max_char_len() (new)Additive: reports and resets the largest sentence seen, for worker-style shrink accounting

set_text and set_text_nbest also gained a documented panic: they assert that the sentence is shorter than u16::MAX characters, because an edge now stores its start position as a u16. Every path through Segmenter is safe because it splits sentences at MAX_SENTENCE_BYTES; only code that drives a Lattice directly needs to split its own input.

JavaScript bindings: tokens are plain objects

Both JS bindings previously returned Token class instances. Those instances own memory outside the JavaScript heap, released only when the host runs a finalizer — which napi defers to the event loop, and which wasm-bindgen leaves to the caller. A synchronous loop that tokenizes a large input without yielding therefore accumulated memory even under forced GC. In v6 both bindings return plain objects instead, which the JavaScript GC owns outright.

Practical benefits: memory stays flat in batch loops, and results work directly with JSON.stringify, structuredClone, and worker transfer.

getDetail(i) is removed (Node.js and WASM)

A plain object has no methods, so read the array instead:

// v5
const pos = token.getDetail(0);

// v6
const pos = token.details[0];

Out-of-range reads now yield undefined rather than null.

Node.js: tokenizeObjects is removed

tokenizeObjects existed only as a v5-era workaround for the memory problem above. tokenize now returns the same plain objects, so drop the call:

// v5
const tokens = tokenizer.tokenizeObjects(text);

// v6
const tokens = tokenizer.tokenize(text);

tokenizeNbest returns plain objects too, so it no longer has the accumulation behavior documented as a known limitation in v5. NbestResult stopped being a class along with Token: it is now a plain object with the same tokens and cost properties.

Because neither is a class any more, they are no longer exported from the package. Token, JsToken, NbestResult, and JsNbestResult are all gone from index.js, so require("lindera").Token is undefined and instanceof checks against them no longer compile or run:

// v5
const { Token } = require("lindera-nodejs");
if (tokens[0] instanceof Token) { /* ... */ }

// v6 — the values are plain objects; check a property instead
if (typeof tokens[0].surface === "string") { /* ... */ }

TypeScript users: the interface describing a token is now called Token (it was JsTokenData while Token was taken by the class).

WASM: field names are now camelCase

The removed Token class exposed snake_case fields while its own toJSON() already emitted camelCase. v6 settles on camelCase, matching the Node.js binding:

// v5
token.byte_start, token.byte_end, token.word_id, token.is_unknown

// v6
token.byteStart, token.byteEnd, token.wordId, token.isUnknown

surface, position, and details are unchanged. Code that already called toJSON() and read the result was using these names, and keeps working — except that toJSON() itself is gone, since the token is already a plain object:

// v5
const data = token.toJSON();

// v6
const data = token; // already plain

Token is also no longer exported from the module, so import { Token } from 'lindera-wasm' fails. There is nothing to import in its place — tokenize hands back plain objects directly.

tokenizeNbest in WASM already returned plain objects in v5 and is unchanged.

Who does not need to act

  • Python, Ruby, and PHP bindings: unaffected by the JavaScript changes above. They keep their Token classes, because their finalization is deterministic and never had the memory problem that motivated the JS rework. They each gained a plain-data conversion method — to_dict(), to_h (also to_hash), and toArray() respectively — but nothing was removed or renamed.
  • User dictionaries loaded from .csv: compiled at load time, so they pick up the new format automatically. (Prebuilt .bin files do need rebuilding — see the first section.)
  • lindera crate's public API: Segmenter, Tokenizer, SegmentWorker, and AnalysisWorker signatures are unchanged (aside from the additive unknown_word_ladder/max_grouping_len options); code that loads a dictionary from a path or URI compiles and runs unchanged once the dictionary itself is rebuilt or re-downloaded, and embed-* dictionaries are compiled by each dictionary crate's build script, so they always match the running version.

Upgrade checklist

Everyone:

  • Rebuild prebuilt user-dictionary .bin files from their CSV source with lindera build --user. This is required regardless of which v5 you are coming from.
  • If you pin exact tokenization output for out-of-vocabulary or non-Japanese text, re-check it — the Decompose penalty fix and the unknown-word ladder (default on) can both change it. Set unknown_word_ladder(false) if you need v5-identical unknown-word output.

Package names (see Package renames on npm, PyPI, and crates.io):

  • npm: depend on lindera instead of lindera-nodejs, and on lindera-wasm instead of lindera-wasm-web / lindera-wasm-bundler; update require/import specifiers to match.
  • PyPI: pip install lindera instead of lindera-python; import lindera is unchanged.
  • Former lindera-wasm-bundler users: add await __wbg_init() before the first API call — the consolidated package is a web-target build and does not initialize implicitly.

JavaScript (Node.js and WASM):

  • Replace token.getDetail(i) with token.details[i].
  • Node.js: replace tokenizeObjects(text) with tokenize(text); stop importing Token/NbestResult (they are no longer exported); TypeScript users rename the JsTokenData type to Token.
  • WASM: switch token field reads to camelCase (byteStart, byteEnd, wordId, isUnknown), drop toJSON() calls, and stop importing Token.

Direct lindera_dictionary users:

  • Update call sites per the Rust API table above, including the two new set_text/set_text_nbest arguments.

Upgrading from v5.2 or earlier (these shipped in v5.3.0):

  • Rebuild self-built system dictionaries with lindera build, or re-download prebuilt ones with lindera download.
  • WASM: update loadDictionaryFromBytes() calls to the 9-argument signature (dictTrie and dictValsIdx in place of dictDa).
  • WASM: remove v5-era dictionaries from OPFS and download v6 archives.

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, renames LINDERA_CONFIG_PATH to LINDERA_CONFIG_FILE, keeping the old name with a deprecation warning until v8.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 it regularizes with L2 and a coefficient of 1.0 by default, as MeCab's learner does. 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

ChangeAffectsWhat 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-analysisBump 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::analysisAdd "analysis" to features (and keep "mmap" listed for memory-mapped loading)
The former lindera crate is published as lindera-segmenterNobody directlyNothing — lindera = "7" re-exports it
The raw lindera-dictionary modules are reachable under lindera::dictionaryDirect lindera-dictionary usersNothing — 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 goneAnyone enabling a dictionary crate by name in features = [...]Use the embed-* features instead
lindera-binding-core is renamed to lindera-bindingRust users of the binding helper crateDepend on lindera-binding and replace lindera_binding_core:: with lindera_binding::
LINDERA_DICTIONARIES_PATH is removedAnyone still setting the deprecated build-cache variableSet LINDERA_BUILD_DICTIONARY_CACHE_DIR; the old name is now ignored
LINDERA_CONFIG_PATH is renamed to LINDERA_CONFIG_FILEAnyone who sets LINDERA_CONFIG_PATH for TokenizerBuilder::new(), lindera tokenize or a language bindingSet LINDERA_CONFIG_FILE; the old name works with a deprecation warning until v8.0.0
LINDERA_WORKDIR is renamed to LINDERA_WORK_DIROnly a dictionary crate whose own build script sets LINDERA_WORKDIR for embedded_dictionary!Set LINDERA_WORK_DIR; a crate that uses build_embedded_dictionary needs nothing
Rust 1.89 or newer is requiredAnyone who builds Lindera with Rust 1.88, including a source build of the Python or Ruby packageUpdate the toolchain (rustup update stable)
Dictionary format version 3: dictionary directories built or downloaded with v6.2.0 or earlier no longer loadAnyone 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 dictionariesRebuild 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 columnsAnyone 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 keptText with U+3000 (IPADIC, IPADIC-NEologd, UniDic), text with spaces (SudachiDict), and a few entries that ended with whitespaceExpect 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 MeCabAnyone 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 doesExpect 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 decidesText 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 linesExpect Ð 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 MeCabAnyone who segments Japanese text with 、 or 。 inside a lineExpect 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 inputAnyone who asks for N-best results (segment_nbest, tokenize_nbest, lindera tokenize -N, the bindings' N-best methods) for input with more than one sentenceExpect 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 MeCabAnyone 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 MeCabAnyone 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_categoriesExpect 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 399Anyone 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 costsExpect 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 entryAnyone who segments text with 〇 (U+3007) with JiebaExpect 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 GSDAnyone who segments text with Jieba; users of simple Jieba user dictionariesExpect 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 MeCabAnyone who segments text with ― (U+2015), — (U+2014), ~ (U+FF5E) or 〜 (U+301C) with IPADIC or IPADIC-NEologdExpect 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 MeCabAnyone 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 usersExpect 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 MeCabAnyone 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 lineCLI users whose input lines start or end with whitespace, or who read the wakati or N-best output of lines without tokensExpect 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 setWASM 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 defaultPHP 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 wrappingCode 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 NaNPass 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::DecomposeBinding 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 functionsPass 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 3.0.0Users of lindera train and lindera export, and of the training functions of the Python, Node.js, Ruby and PHP bindingsTrain 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 train regularizes with L2 and a coefficient of 1.0 by default, as MeCab's learner doesUsers of lindera train, of Trainer::new in lindera_trainer and lindera_crf, and of the training functions of the Python, Node.js, Ruby and PHP bindings, who do not give both the regularization type and the coefficientExpect more accurate models and a larger matrix.def; pass --regularization l1 --lambda 0.01 (.use_l2(false).regularization_cost(0.01) in Rust, regularization l1 and lambda 0.01 in the bindings, #1262) to use v6's default settings
lindera train starts its first line search at a unit step, as MeCab's learner and libLBFGS doUsers of lindera train, of Trainer in lindera_trainer and lindera_crf, and of the training functions of the Python, Node.js, Ruby and PHP bindingsNothing; expect every trained model to differ from v6's, even with v6's settings, and L1 on separable data to keep training where it stopped after a few iterations
lindera build rejects context IDs outside matrix.defAnyone 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 panicFix 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.defAnyone 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 acceptsDefine 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 doesAnyone 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 cellExpect 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 errorCLI 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-freqExpect [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 8Code that creates or reads dictionary metadata: Rust (Metadata, lindera_binding::CoreMetadata, PrefixDictionaryBuilderOptions) and the Metadata classes of the Python, Ruby, PHP and Node.js bindingsRemove 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_intoNothing for a call written as a statement; otherwise ignore the new value
UnknownDictionary::gen_unk_words, UnknownDictionary::compatible_unk_index, UnkWord and WordIdx are removedRust 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 meaningRust 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
lindera_trainer: FeatureExtractor's feature ID maps are read through a FeatureIdMap, and TrainerConfig::surface_features() is removedRust code that uses lindera-trainer (also lindera::dictionary::trainer) and reads the ID maps of a FeatureExtractor it builds, or calls surface_features()Call unigram_feature_ids(), left_feature_ids() and right_feature_ids() instead of reading the fields; rebuild the map from surfaces() and features() (see Rust API changes in lindera_trainer)

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 training functions and lindera train regularize with L2 and a coefficient of 1.0 by default, where v6 used L1 and 0.01 (see lindera train regularizes with L2 by default). 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::core shares its name with Rust's core crate. A glob import such as use lindera::dictionary::*; brings that module into scope and shadows the core crate for the rest of the module, so paths like core::fmt::Debug stop 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

LINDERA_CONFIG_PATH renamed to LINDERA_CONFIG_FILE

TokenizerBuilder::new(), which lindera tokenize and every language binding call, reads the YAML configuration file named by LINDERA_CONFIG_FILE, so that the environment variables name a file with _FILE and a directory with _DIR (#1304). The old name, LINDERA_CONFIG_PATH, keeps working until v8.0.0:

  • With only LINDERA_CONFIG_PATH set, the file it names is read, and a warning goes to standard error, once per process:

    Warning: LINDERA_CONFIG_PATH is deprecated and will be removed in v8.0.0; set LINDERA_CONFIG_FILE instead
    
  • With both set, the file of LINDERA_CONFIG_FILE is read, and the warning says that LINDERA_CONFIG_PATH is ignored.

The warning is written to standard error directly, not through the log crate, so that the users of the language bindings, which install no logger, see it too. Rename the variable wherever it is set (shell profiles, CI configuration, container images):

# v6
export LINDERA_CONFIG_PATH=./resources/config/lindera.yml

# v7
export LINDERA_CONFIG_FILE=./resources/config/lindera.yml

LINDERA_WORKDIR renamed to LINDERA_WORK_DIR

The build script of a dictionary crate passes the directory of the dictionary it built to the compiler as LINDERA_WORK_DIR, and embedded_dictionary! embeds the dictionary from there; up to v6 the variable was LINDERA_WORKDIR (#1304). Both the code that sets it (build_embedded_dictionary) and the macro are in lindera-dictionary, so a dictionary crate that uses them, as the bundled ones do, needs no change. Only a crate whose own build script sets LINDERA_WORKDIR for embedded_dictionary! has to set LINDERA_WORK_DIR instead; otherwise it fails to compile:

error: environment variable `LINDERA_WORK_DIR` not defined at compile time

Users do not set this variable, and the environment variables of the installation page no longer list it.

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.

DictionaryWhat you do
Embedded (embed-* features)Nothing: it is built when the crate is compiled
Downloaded with lindera downloadDownload it again with the 7.0.0 CLI, which stores it under a directory of its own version
Built with lindera buildBuild it again with the 7.0.0 CLI
Downloaded from the release assetsDownload the 7.0.0 asset
Saved in the WASM binding's OPFS storageRemove 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 namev6v7
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:

DictionaryChange
IPADIC, IPADIC-NEologdU+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
UniDicU+3000 is the entry 空白 instead of an unknown 名詞,普通名詞,サ変可能
SudachiDictU+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, JiebaNo 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.

DictionaryEffectExample
ko-dicSpaced Korean text now follows mecab-ko2년 전 대회: 전 is NNG, not 저/NP + ㄴ/JX; 하고 있다: 있 is VX, not VV
IPADIC, IPADIC-NEologd, UniDicA word after a half-width space connects to the word before it, as in MeCabIPADIC Google が 新しい: が is a case particle, not a conjunction; 東京 都: 都 is a suffix, as in MeCab
SudachiDictNo change: its metadata.json sets skip_whitespace to false, because Sudachi keeps whitespace in the lattice and the costs assume it—
CC-CEDICTNo change in the best path (the dictionary has no connection costs); N-best costs no longer include the whitespace nodes—
JiebaNo 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):

Characterv6 categoriesv7 categoriesEffect
Ð (U+00D0)SPACE, ALPHAALPHAEvery 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, SYMBOLSYMBOLDictionary 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, KANJINUMERICSYMBOL, KANJINUMERICRuns 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)v6v7 (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)v6v7
東京、です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)v6v7 (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 their char.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.2 dictionary (not measured with 0.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, and T (an entry, −400) followed by the unknown word he (−3,200) beats the unknown word The. 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 G and oogle for Google (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, GROUP and LENGTH settings and its unk.def entries.
  • 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)v6v7 (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 category HANJANUMERIC groups, so a numeral and the hanja after it become one SH word, as in MeCab.
  • 〇: its default category is SYMBOL, 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, cos and Tony) and the 2 made only of digits (88 and 996) 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:

Inputv6v7
他穿着一件T恤。T and 恤T恤
我们去卡拉OK唱歌。卡拉 and OK卡拉OK
请把文件存到U盘里。U and 盘U盘
二〇〇八年北京奧運會二, 〇〇 (unknown), 八, 年, 北京 and 奧運會二, 〇, 〇, 八, 年, 北京 and 奧運會
两个队PK了一场。PK, an unknown wordPK, 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盘 or X光, 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, 〇 is CHINESENUMERIC CHINESE, the categories of the other numerals, instead of SYMBOL CHINESENUMERIC.
  • 〇 is an entry like 零: part of speech m, pinyin ling2 and definition zero.

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 〇:

Inputv6v7
二〇〇八年北京奧運會二, 〇〇 (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:

Input0.1.20.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:

DictionaryGSD devGSD testGSDSimp devGSDSimp test
0.1.277.0977.3177.0777.70
0.1.388.8487.7189.5988.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)v6v7 (as MeCab)
CD―ROM (IPADIC)CD, ― and ROMCD―ROM (名詞,一般)
―― (IPADIC)An unknown noun (名詞,サ変接続)記号,一般
あいいれなぁ~い (IPADIC-NEologd)あいいれなぁ, ~ and いあいいれなぁ~い (形容詞,自立)
CD―ROM (IPADIC-NEologd)CD, ― and ROMCD―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:

APIChange
RustMetadata::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
PythonThe normalize_details keyword argument of Metadata, the property and the to_dict() key are gone; passing the keyword raises TypeError
RubyMetadata.new takes 8 positional arguments instead of 9, so passing 9 raises ArgumentError; the normalize_details reader and the to_h key are gone
PHPThe 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.jsThe normalizeDetails option of Metadata, the property and the toObject() key are gone; TypeScript rejects the option, and at run time it is ignored
WASMNo 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)v6v7 (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)v6v7 (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:

DictionarySentences that changeCloser to / farther from the gold segmentationSentences segmented as MeCab, before → after
CC-CEDICT1,267634 / 455 (sign test p = 6.5 × 10⁻⁸)3,694 → 4,951
Jieba31 / 14,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)v6v7 (as MeCab)
Two spaces, 東京, a space東京 at bytes 0-6東京 at bytes 2-8
U+3000, 東京東京 at 0-6U+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-6U+00A0 (unknown, 記号,一般) at 0-2, 東京 at 2-8, U+00A0 at 8-10
  • Spaces and tabs are SPACE characters, 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 1 and EOS), 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 \r of a line that ends with \r\n is removed, and only that one \r. MeCab removes only the \n and outputs the \r as an unknown word; Lindera removes it so that text with Windows line endings does not end every line with a \r token.

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 withIgnored up to v6.2.0
setDictionaryInstance(), such as a dictionary loaded from OPFS with loadDictionaryFromBytes()setKeepWhitespace(), appendCharacterFilter() and appendTokenFilter()
setDictionary(), such as an embedded:// dictionaryThe 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.

Callv6.2.0v7.0.0
PHP new Lindera\Metadata(default_word_cost: 100000)cost -31072ValueError
Node.js new Metadata({ defaultLeftContextId: 2 ** 32 + 5 })context ID 5Error with the code InvalidArg
Node.js schema.getFieldName(2 ** 32)"surface", the first fieldnull
WASM new Penalty(-1)threshold 4294967295a thrown string
tokenizeNbest('東京', -1) in PHP, Node.js and WASMall 33 segmentationsthe 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) or undefined (WASM). An index past the end still returns null or undefined.
  • In Node.js and WASM, a number that is not an integer, such as 1.5, NaN or Infinity, 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.
  • An iteration or thread count of 0 for the training functions, which v6 passed to the trainer: training then failed only after the corpus was read and every lattice was built. The counts take 1 or more, and the Python and Ruby bindings reject 0 too, with a ValueError and a RangeError (#1275).

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:

Bindingv6v7
Pythonbuilder.set_mode("decompose")builder.set_mode(Mode.decompose(penalty))
Rubybuilder.set_mode('decompose')builder.set_mode(Lindera::Mode.decompose(penalty))
PHP$builder->setMode('decompose')$builder->setMode(Lindera\Mode::decompose($penalty))
Node.jsbuilder.setMode("decompose")builder.setMode(Mode.decompose(penalty))
WASMbuilder.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 Mode classes: a Mode has no constructor; Mode.normal(), Mode.decompose() and Mode.from_str() create one. It has a name, a penalty (a copy, or none for normal mode), the normal and decompose checks, and equality of the name and the penalty (==, or equals() in Node.js and WASM). Python's Mode(mode_str) and its attributes Mode.Normal / Mode.Decompose, Ruby's Lindera::Mode.new, PHP's new Lindera\Mode(), Node.js's JsMode and WASM's Mode.Normal / Mode.Decompose are gone. The Python and Ruby constructors accepted "Normal" and "Decompose", and PHP's any case; from_str does not. A Python Mode is not hashable, as the Rust Mode is not Hash.
  • Node.js: the TypeScript type JsPenalty is renamed Penalty, and its fields become optional; a field left out keeps its default. TypeScript code that names JsPenalty uses Penalty. The package exports no value for the type, so JavaScript code does not change.
  • Rust: TokenizerBuilder::set_segmenter_mode with a Mode::Decompose whose 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 --mode gives, carries the default penalty and is not affected.
  • Binding authors: CoreTokenizerBuilder::set_mode, CoreTokenizer::from_segmenter and CoreTokenizer::from_segmenter_with_space_penalty take a lindera::mode::Mode instead of the &str that v6.2.0's lindera-binding-core took. set_mode no longer returns a Result. lindera_binding::parse_mode parses a mode name, as the bindings' from_str do.

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 the unk.def entry, so only an unk.def with one entry per class, in the order DEFAULT, HIRAGANA, KATAKANA, KANJI, ALPHA, NUMERIC, worked.
  • Each char.def category got one label, with the feature string of its last unk.def line, and lindera export wrote one unk.def line per category. mecab-ipadic's unk.def has 40 lines for 11 categories; they became 11 lines, and the unknown ALPHA, HIRAGANA and KATAKANA words 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. An unk.def line with fewer than five fields was silently skipped.
  • When regularization left no bigram weight for some context IDs, lindera export wrote a matrix.def smaller than the IDs of the entries (0 0 when no weight was left at all), and the built dictionary panicked at tokenization. model_info.connection_matrix_size in metadata.json was a square of the larger ID.
  • lindera export wrote 32767 (i16::MAX) to matrix.def for 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's mecab-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 v6's default regularization, L1 with --lambda 0.01, writing 0 raised the boundary F1 from 0.9552 to 0.9572 in every fold (#1168).
  • model_info.training_iterations in metadata.json was the --max-iterations value, not the number of iterations that ran.
  • Training stopped at --max-iterations unless one of argmin's own tests held: a gradient norm below the square root of the machine epsilon, or an iteration that left the objective unchanged. With an objective in the hundreds or thousands the first practically never holds, so training ran to the cap. An L2 run whose line search failed panicked (#1167).
  • The export functions of the Python, Node.js, Ruby and PHP bindings wrote lex.csv, matrix.def, unk.def, metadata.json and a fixed char.def with the categories DEFAULT, HIRAGANA, KATAKANA, KANJI, ALPHA and NUMERIC, not the char.def the model was trained with, and no feature.def, rewrite.def, left-id.def or right-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 ", and lindera export wrote it unquoted, so lindera build read the rest of lex.csv as 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 as U09:*, which every feature with fewer columns shared. MeCab's learner skips the template. The unk.def features of mecab-jieba have five columns, and its feature.def reads %F[6] to %F[9], so its 11 unknown-word labels had four features, U06:* to U09:*, that no seed row had.
  • Model::write_dictionary gave an entry of the user lexicon that Model::read_user_lexicon reads whose context IDs and cost are 0,0,0 the 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 in char.def as both IDs, which is not a context ID.
  • Model::write_dictionary gave every such entry with 0,0,0 the same cost, tocost(0.8, cost_factor), whatever its feature. Model::read_user_lexicon read an empty context ID or cost, and a value that is not a number, as 0, and a negative context ID wrapped around to 16 bits (-1 became 65535); the parameters were filled in only when all three were 0. So an entry with an empty cost and given context IDs got the cost 0, and one with empty context IDs and a given cost got the BOS/EOS IDs 0,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, GROUP and LENGTH settings, for every unk.def entry of the category.
  • Every unk.def line is an entry and a CRF label of its own, with the ID of its category as %t, and lindera export writes 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.def entry 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 train prints the counts in a Lattice statistics: line, and warns on stderr when no gold token is a seed word, or fewer than half of the gold tokens whose surface has seed rows are, with the first gold token whose surface has seed rows but none with its feature string.
  • matrix.def covers every context ID, plus 0 for BOS/EOS, and model_info.connection_matrix_size gives 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_iterations is the number of L-BFGS iterations that ran: --max-iterations when that stopped training, fewer when it converged earlier.
  • Training also stops once the relative change of the objective has stayed below --eta (Trainer::eta; default 0.00005, the default of MeCab's learner) for 3 iterations in a row; --eta 0 turns the test off. The bindings use the default. lindera train prints why the optimizer stopped, with the number of iterations and of evaluations of the loss and its gradient, and warns on standard error when an iteration left the objective unchanged or the line search failed far from the optimum. Near the optimum, where which of the two happens depends on the platform, it reports that training converged (#1300). An L2 run whose line search fails keeps the best weights of the run.
  • The export functions of the Python, Node.js, Ruby and PHP bindings write the files lindera export writes, with the same code: the char.def, feature.def and rewrite.def the model was trained with, and left-id.def and right-id.def too.
  • model.dat has the format version 3.0.0. lindera export and the bindings' export functions reject a model.dat of any other version, including every model.dat written by v6.2.0 or earlier, with an error that ends in "re-run lindera train to regenerate it".
  • The seed lexicon is read as CSV, as lindera build reads lex.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 export quotes a surface that contains ,, " or a line break. The user lexicon that Model::read_user_lexicon reads 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 and resources/training/, only the unk.def lines 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_dictionary fills in the parameters of a user lexicon entry separately, as mecab-dict-index does: an empty cost is computed, and when either context ID is empty or negative, both are inferred. 0,0,0 still 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 calcCost computes: the sum of the trained weights of the entry's unigram features, converted as the costs of lex.csv are, 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.csv gives 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, as mecab-dict-index fails for a user dictionary entry whose feature has no context ID.

Every lattice now has the seed words. On the mecab-ipadic data of #1110, the new default, L2 with --lambda 1.0 (MeCab's -c 1.0), converges in about as many iterations as MeCab's learner (see lindera train regularizes with L2 by default), where v6's L1 with --lambda 0.01 still reaches --max-iterations (#1167); training there takes about twice as long as MeCab's learner, about two thirds of it outside the optimizer: before it starts (#1263) and after it stops. 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 train regularizes with L2 by default

lindera train, Trainer::new of lindera_trainer and lindera_crf, and the training functions of the Python, Node.js, Ruby and PHP bindings regularize with L2 and a coefficient of 1.0 by default: the objective of MeCab's learner with its default -c 1.0 (#1261). v6 used L1 with 0.01, and lindera_crf::Trainer::new L1 with 0.1. MeCab's learner adds ‖w‖² / (2C) to the loss, so the coefficient, --lambda, is 1 / C in general (-c 0.5 is --lambda 2.0); its help no longer gives it the range 0.0 to 1.0, which was never enforced. The new constant DEFAULT_LAMBDA of lindera_trainer and lindera_crf holds the default.

On a five-fold cross-validation with mecab-ipadic's files (#1110), with the default cap of 100 iterations, means over the folds:

RegularizationIterationsBoundary F1Span + POS (4 columns) F1matrix.def
L2, --lambda 1.0 (v7's default)77 to 82, converged0.96130.939718.9 MB
L1, --lambda 0.01 (v6's default)100, the cap0.95780.93508.3 MB
L2, --lambda 0.01 (--lambda 0.01 only)100, the cap0.95820.936119.0 MB
L1, --lambda 1.0 (--regularization l1 only)100, the cap0.96050.93803.3 MB
MeCab's learner, -c 1.075 to 83 evaluations, converged0.96110.939822.5 MB (fold 0)

lindera export gives labels with equal weights one context ID. L1 sets many weights to 0, so many labels share an ID; L2 sets none to 0, and its matrix.def there has about 1,200 by 1,200 IDs, close to the 1316 by 1316 of mecab-ipadic's own. The built dictionary is 4.0 % larger than with v6's default settings there (fold 0). On resources/training/, training stops after 8 iterations instead of 15, and the exported unk.def gets other context IDs and costs; the tokenize output in the docs does not change. These numbers include the first line search of lindera train starts its first line search at a unit step.

What you do: to use v6's default settings, give both. A setting given alone keeps its value, and the other one takes the new default:

v6 invocationMeaning in v7To use v6's settings
No regularization flagL2, --lambda 1.0--regularization l1 --lambda 0.01
--lambda X onlyL2, --lambda XAdd --regularization l1
--regularization l1 onlyL1, --lambda 1.0Add --lambda 0.01
BothUnchanged—

The model still differs from v6's, since the first line search starts at another step (see the next section). To stop as v6 did, also pass --eta 0 (see lindera train builds its lattices as MeCab's learner does). In Rust, call .use_l2(false).regularization_cost(0.01) on lindera_trainer::Trainer::new(config)?, and .regularization(Regularization::L1, 0.1)? on lindera_crf::Trainer::new(). The bindings take the type, the Elastic Net ratio and eta as the CLI does (#1262): pass regularization="l1", lambda_=0.01 in Python, regularization: "l1", lambda: 0.01 in Node.js, a lambda of 0.01 and regularization: 'l1' in Ruby, and regularization: 'l1', lambda: 0.01 in PHP. A model records the type, and the Elastic Net ratio, next to the coefficient: in model.dat and as regularization_type and elastic_net_l1_ratio in the model_info of the exported metadata.json (#1272).

A lambda that is NaN or infinite is now rejected: lindera train and lindera_crf::Trainer::regularization fail with `lambda must be finite and

= 0`, where v6 accepted it, and the bindings raise before they read any file (#1262).

lindera train starts its first line search at a unit step

The first line search of the L-BFGS and OWL-QN optimizers of lindera train, of Trainer in lindera_trainer and lindera_crf, and of the training functions of the Python, Node.js, Ruby and PHP bindings starts at a step of 1 / |d|, d being the first search direction, so that its first trial point lies at unit distance from the initial weights, as in MeCab's learner and libLBFGS; every later one starts at a step of 1, as before (#1290). v6 started the first one at 1 too, so its first trial point lay as far from the initial weights as the gradient was long: 1.1e5 on the corpus of #1260, where, with L1 and --lambda 0.01, the first search went so far that the loss became rounding error and training stopped with "no progress" after 3 iterations. It now converges after 53.

Every trained model changes, with every regularization type. On the cross-validation of the previous section, the accuracy stays within the spread between folds, and the first line search takes 1 evaluation instead of about 7 with L2 and 13 with L1 (means over the folds, the same build with each first step):

RegularizationFirst stepIterationsEvaluationsBoundary F1Span + POS (4 columns) F1
L2, --lambda 1.0184.494.20.96130.9398
L2, --lambda 1.01 / |d|79.483.80.96130.9397
L1, --lambda 0.011100113.80.95720.9344
L1, --lambda 0.011 / |d|100101.40.95780.9350
L1, --lambda 1.01100112.80.96070.9381
L1, --lambda 1.01 / |d|100101.20.96050.9380
Elastic Net, --lambda 1.0197.6110.40.96140.9393
Elastic Net, --lambda 1.01 / |d|98.8100.00.96120.9391

What you do: nothing. With v6's settings (see the previous section), a model is no longer v6's byte for byte.

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.json sets skip_invalid_cost_or_id drops such a lex.csv row instead, with a warning, which the CLI writes to standard error (see The CLI writes library warnings to standard error). unk.def lines are always checked.
  • An unk.def line with fewer than four fields, such as a comment or an empty line, is an error instead of a panic, also in DictionaryBuilder::build_unknown_dictionary and parse_unk of lindera_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 the Tokenizer constructors and builders of the language bindings return an error that names the first entry outside the matrix (#1171; see Context IDs). Segmenter::new does not check, as before. lindera build --user checks the rows already when the new --system-dict option names the system dictionary (DictionaryBuilder::with_system_matrix in the Rust API), and names the first row outside the matrix by its CSV line and surface (#1182). A .csv user dictionary that the configuration path, lindera tokenize --user-dict or the bindings' TokenizerBuilder loads by path, or that the new load_user_dictionary_for of 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_for in Python and Ruby, loadUserDictionaryFor in Node.js, Lindera\Dictionary::loadUserFor in PHP and loadUserDictionaryFromBytesFor in 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_for in Python and Ruby, buildUserDictionaryFor in Node.js and Lindera\Dictionary::buildUserFor in PHP (#1188). The builders that take metadata are unchanged too: their output_dir is, 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 --user or from loading a .csv user dictionary, name the row's CSV line (at line 1). Before, they counted the rows in the order of their surfaces (at row 2 for 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.def that 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, GROUP and LENGTH settings of another category, which changed the unknown words.
  • A char.def category without an unk.def line. 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.def line whose category char.def does not define. The line was never used, and lindera export wrote 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 tokenize warns when it cuts a sentence because more than 32 KiB of text have no sentence delimiter.
  • lindera build warns when it drops a lex.csv row because of skip_invalid_cost_or_id, and when the dictionary's metadata.json sets connection_id_mapping (ko-dic, UniDic) and --context-id-freq is not given. Pass the context_id_freq.txt of 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).

v6v7
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_detailsRemoved
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 cReturns the categories of the last line that covers c, its default category first and the others in category id order
DICTIONARY_FORMAT_VERSION is 23
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::WordIdxRemoved
—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 twiceReturn 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 lineReturn 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:

  • BosContext and LatticeOptions::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_into and Lattice::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_exit and NBestGenerator::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. next returns the paths of next_with_bos without 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):

v6v7
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"3.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 dictionaryHolds the seed lexicon (system_lexicon()) and the unk.def entries (unk_handler())
TrainerConfig::from_readers skips an unk.def line with fewer than five fieldsReturns an error
TrainerConfig::from_readers accepts a char.def and an unk.def that mecab-dict-index rejectsReturns 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 charactersCaps 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 categoryCount 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.defWrite 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 settingThe number of L-BFGS iterations that ran
ModelMetadata records the regularization coefficient onlyNew: ModelMetadata::regularization_type (a RegularizationType) and ModelMetadata::elastic_net_l1_ratio (Some for Elastic Net only), exported as model_info.regularization_type and model_info.elastic_net_l1_ratio (#1272)
—New: Trainer::eta and Trainer::get_eta, the tolerance of the convergence test, and DEFAULT_ETA (5e-5), its default; lindera-crf adds the same Trainer::eta and DEFAULT_ETA, and StopReason with TrainingStats::evaluations and TrainingStats::stop_reason
Model::write_connection_costs and SerializableModel::write_connection_costs write i16::MAX for a connection without a trained weightWrite 0, the cost the connection has in training and in MeCab
Trainer::new regularizes with L1 (use_l2 false) and regularization_cost 0.01With L2 and DEFAULT_LAMBDA (1.0); use_l2(false) selects L1 (see lindera train regularizes with L2 by default)
—New: DEFAULT_LAMBDA (1.0), the default coefficient; lindera_crf adds the same constant, and its Trainer::new regularizes with Regularization::L2 and it instead of Regularization::L1 and 0.1
—New in lindera_crf: FeatureSet::with_shared_bigrams, which takes the bigram feature IDs as Arc<[Option<NonZeroU32>]> so that labels can share them. FeatureSet holds them this way, which changes the rkyv archive of FeatureSet, FeatureProvider and RawModel; model.dat does not hold them (#1263)
FeatureExtractor::unigram_feature_ids, left_feature_ids and right_feature_ids are public HashMap<String, NonZeroU32> fieldsPrivate; the methods unigram_feature_ids(), left_feature_ids() and right_feature_ids() return a &FeatureIdMap, new in feature_extractor and read-only: len, is_empty, get, contains_key, resolve (the feature of an ID) and iter, which lists the features in ascending ID order (#1286)
TrainerConfig::surface_features() returns a HashMap<String, String> of the seed rows' features by surfaceRemoved; TrainerConfig::features() returns the seed rows' feature strings next to surfaces(), and get_features still returns the feature of the last seed row with the surface (#1286)

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).

A FeatureIdMap keeps the feature strings in one buffer and only the IDs in its hash table, which takes less memory than a HashMap for the millions of features of a full dictionary; only the extractor adds to it. Code that used surface_features() can build the same map, where the last seed row with a surface wins:

#![allow(unused)]
fn main() {
let surface_features: HashMap<&str, &str> = config
    .surfaces()
    .iter()
    .map(String::as_str)
    .zip(config.features().iter().map(String::as_str))
    .collect();
}

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-cli keep their package names, and their APIs apart from the normalize_details setting of Metadata (see The normalize_details setting 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 a Mode instead of a name, and the Node.js TypeScript type JsPenalty, now Penalty (see Bindings: a mode is a Mode). The restructuring is internal to the Rust crates. LINDERA_CONFIG_PATH works with a deprecation warning until v8.0.0 (see LINDERA_CONFIG_PATH renamed to LINDERA_CONFIG_FILE). 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 (see lindera train builds its lattices as MeCab's learner does), and training regularizes with L2 and a coefficient of 1.0 unless both the type and the coefficient are given (see lindera train regularizes with L2 by default). 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, the char.def fix, 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 of lindera and lindera-analysis remain 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 other lindera::… items of v6 compiles unchanged with lindera = "7", unless it calls the lattice backtraces of lindera::dictionary::Lattice or creates metadata with lindera::dictionary::Metadata::new (see Rust API changes in lindera_dictionary). Dictionary directories loaded by path still have to be rebuilt. The default build now also compiles lindera-analysis and its dependencies (kanaria, regex, serde_yaml_ng, unicode-blocks, unicode-normalization, unicode-segmentation); to keep the v6 dependency tree, set default-features = false, features = ["mmap"].

Upgrade checklist

Rust crate users:

  • Bump lindera to "7" and, in the same commit, either bump lindera-analysis to "7" or remove it and import from lindera::analysis::….
  • Optionally replace lindera_analysis:: with lindera::analysis:: in use paths — both name the same types.
  • If you set default-features = false, add "analysis" (for the Tokenizer and filters) and "mmap" (for memory-mapped loading) to features as needed.
  • Replace features = ["lindera-<dictionary>"] with features = ["embed-<dictionary>"].
  • Optionally drop a direct lindera-dictionary dependency in favor of lindera::dictionary::core, ::builder, ::viterbi, and the other re-exported modules.
  • If you call Lattice::tokens_offset, tokens_offset_into, nbest_tokens_offset or NBestGenerator::next, destructure (start, end, word_id) and use the returned end instead of the next token's start.
  • If you use the () value of Lattice::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 the Metadata::normalize_details field and PrefixDictionaryBuilderOptions::normalize_details.
  • If you build Metadata with a struct literal that lists every field, add skip_whitespace: None or 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_words or UnknownDictionary::compatible_unk_index, or use UnkWord or WordIdx, list the unknown words with Lattice::unknown_word_candidates instead.
  • If you read SerializableModel::unk_category_names or unk_categories, read unk_entries instead, and if you set Trainer::max_grouping_len, pass it the cap that the dictionary will be used with.
  • If you pass TokenizerBuilder::set_segmenter_mode a Mode::Decompose whose penalty is not the default, expect that penalty to apply.

Binding authors:

  • Depend on lindera-binding instead of lindera-binding-core, and replace lindera_binding_core:: with lindera_binding::.
  • Delete the normalize_details argument of CoreMetadata::new, which now takes 10 arguments, and stop using the CoreMetadata::normalize_details field.
  • If you build CoreMetadata with a struct literal that lists every field, add skip_whitespace: None or end it with ..Default::default().
  • Pass a lindera::mode::Mode instead of a mode name to CoreTokenizerBuilder::set_mode, which no longer returns a Result, and to CoreTokenizer::from_segmenter and CoreTokenizer::from_segmenter_with_space_penalty; parse a name with lindera_binding::parse_mode.

Build environments:

  • Use Rust 1.89 or newer.
  • Rename LINDERA_DICTIONARIES_PATH to LINDERA_BUILD_DICTIONARY_CACHE_DIR in shell profiles, CI configuration, and container images.
  • If the build script of your own dictionary crate sets LINDERA_WORKDIR for embedded_dictionary!, set LINDERA_WORK_DIR instead.

Everyone who sets LINDERA_CONFIG_PATH:

  • Rename it to LINDERA_CONFIG_FILE in shell profiles, CI configuration, and container images. The old name works with a deprecation warning until v8.0.0.

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_type or conjugation_form by 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 with japanese_stop_tags or 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.def at least one category.
  • If your char.def has single-code-point or overlapping lines, retrain: the %t feature 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 export rejects a model.dat written by v6.2.0 or earlier, and the training lattices, the unknown-word labels and the exported unk.def and matrix.def have changed.
  • Give every line of your unk.def at least five fields: the category, the two context IDs, the cost and at least one feature column.
  • If your char.def and unk.def were copied from resources/training/ and have no SPACE category, add it as those files now do, so that the trained dictionary tokenizes without --keep-whitespace (#1166).
  • If training reports a category of char.def or unk.def, 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 (#1195).
  • Expect training to stop before --max-iterations once the objective converges, and read the line lindera train prints to see why it stopped; pass --eta 0 (Trainer::eta(0.0) in Rust) to stop only at the cap and on the optimizer's own tests, as v6 did (#1167).
  • Expect training to regularize with L2 and a coefficient of 1.0 unless you give both the type and the coefficient; pass --regularization l1 --lambda 0.01 (.use_l2(false).regularization_cost(0.01) in Rust) to use v6's default settings. If you pass only --lambda (lambda in the bindings), it is now the L2 coefficient; with --regularization l1 only, the coefficient is 1.0. The bindings take the type as regularization (#1261, #1262).
  • Expect every trained model to differ from v6's, even with v6's settings, since the first line search starts at a unit step (#1290).
  • Expect a NaN or infinite lambda to be rejected (#1262).

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.csv or unk.def or the size in matrix.def, or set skip_invalid_cost_or_id to drop such lex.csv rows. 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.def or unk.def, 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 (#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 .bin again 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, and unknown_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 one SH word), 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 as PK to 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-20261021 archive.

Users of Jieba with text that has 〇:

  • 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.

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.def and right-id.def of mecab-jieba-0.1.3.
  • Build a Jieba dictionary directory from the mecab-jieba-0.1.3 archive.

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 the mapping character 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_details where you create Metadata: the Python keyword argument, the ninth positional argument of Ruby's Metadata.new, the last argument of PHP's Metadata constructor, and the normalizeDetails option in Node.js. Stop reading the property and the key of the same name in to_dict(), to_h, toArray() and toObject().
  • 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-whitespace to output the spaces there, -N to 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, expect setKeepWhitespace() and the appended filters to apply to a dictionary set with setDictionaryInstance(), and a user dictionary set with setUserDictionaryInstance() to apply to a dictionary set with setDictionary().
  • 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 an Exception. Cast strings such as $argv values 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 Mode to set_mode / setMode and to the Tokenizer constructor instead of "normal" or "decompose": Mode.normal() or Mode.decompose() (Ruby Lindera::Mode.normal, PHP Lindera\Mode::normal()), with a Penalty to customize decompose mode, or Mode.from_str(name) (Mode.fromStr in PHP, Node.js and WASM) for a name read at run time. Replace uses of the old Mode types, such as Python's Mode("normal"), Node.js's JsMode and WASM's Mode.Normal, in the same way.
  • With Node.js and TypeScript, rename the type JsPenalty to Penalty.
  • With the CLI, expect warnings on standard error, for example for a sentence cut after 32 KiB without a delimiter; set RUST_LOG=error to 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, the lindera train and lindera build items, and the lindera tokenize, WASM, PHP, integer-argument, Mode, JsPenalty and CLI warning items.

Development Guide

This section provides information for developers who want to build, test, or contribute to Lindera.

Build & Test

Build

Default Build

Build the workspace with default features (mmap):

cargo build

Build with Training Support

Include CRF-based dictionary training functionality:

cargo build --features train

Build CLI Only

cargo build -p lindera-cli

The CLI has the train feature enabled by default.

Test

Single Test

Run a specific test within a crate (recommended for development):

cargo test -p <crate> <test_name>

Training Feature Tests

cargo test -p lindera-trainer

All Features for a Crate

Run the full test suite for a single crate:

cargo test -p <crate> --all-features

Note: CI does not use --all-features -- it runs each crate with a curated, crate-specific feature combination (see .github/workflows/regression.yml). The Makefile's per-crate pattern targets (make test-<crate>, make lint-<crate>) apply the same feature combinations CI uses and are the closest local equivalent. CI also tests lindera-segmenter and lindera once more with every dictionary embedded (on Linux only), which runs the tests gated on the other dictionaries; to do the same locally, run cargo test -p lindera --features embed-ipadic,embed-ipadic-neologd,embed-unidic,embed-sudachidict,embed-ko-dic,embed-cc-cedict,embed-jieba (and likewise for lindera-segmenter).

Workspace-Wide Tests

cargo test

Quality Checks

Format Check

Verify code formatting matches the project style:

cargo fmt --all -- --check

To auto-fix formatting:

cargo fmt --all

Note: cargo fmt formats the Rust code only. For a language binding, make format-lindera-<binding> also formats the binding's Python, JavaScript, Ruby or PHP code, with the tools and versions make lint-lindera-<binding> checks it with (see below). make format-scripts does the same for the files outside the crates that make lint-scripts checks.

Lint

Run Clippy with warnings treated as errors:

cargo clippy -- -D warnings

Note: CI runs cargo clippy --all-targets -- -D warnings for each crate, with the features it tests that crate with (see .github/workflows/test-crate.yml, and the language binding jobs in .github/workflows/regression.yml), so the test code is linted too. make lint-<crate> runs the same clippy command. Run clippy locally (e.g. via make lint) before opening a PR.

For a language binding, make lint-lindera-<binding> then runs the binding's language-side linter, as the binding's CI job does: ruff for Python, Prettier for Node.js and WASM, RuboCop for Ruby, and PHP CS Fixer for PHP. Their versions are pinned (ruff and Prettier in the Makefile, RuboCop in lindera-ruby/Gemfile, PHP CS Fixer in composer.json), so a new release of a tool cannot fail CI on unchanged code. The targets install the pinned versions themselves, which needs the language's toolchain (Python 3, Node.js with npx, Bundler, Composer) and network access.

make lint-scripts lints the hand-written files that belong to no binding, as the Lint scripts CI job does: the benchmark harness in scripts/benchmarks/, which the bindings share, and the JavaScript the docs load (docs/mermaid-init.js). It runs the same pinned tools, and checks each file with the rules of the binding written in its language, so it needs all four toolchains. make lint runs it after the crates.

Documentation

API Documentation

Generate and open Rust API documentation:

cargo doc --no-deps --open

Check the crates' API documentation for rustdoc warnings, such as a broken intra-doc link or a public item linking to a private one:

make check-docs

Note: CI runs make check-docs and fails on any rustdoc warning. It documents every crate but the language bindings with all features: the libraries twice, once with public items only, as docs.rs publishes them, and once with private items too, whose doc comments only that second pass checks; then lindera-cli's binary, where the CLI's code lives, with its private items. It builds with dummy dictionaries (DOCS_RS=1) in its own target directory, target/check-docs, so it downloads no dictionary and leaves target/ alone.

The language bindings are checked one at a time, since building each needs its language's toolchain:

make check-docs-lindera-python   # also -nodejs, -ruby, -php, -wasm

Note: Each binding's CI job runs its target and fails on any rustdoc warning. The target documents the binding twice, public items only and then with private items too, with default features, which already enable every feature that adds or removes an item; no dictionary is built. lindera-ruby runs through the same RbConfig wrapper as make test-lindera-ruby, and lindera-wasm is documented for wasm32-unknown-unknown.

mdBook Documentation

Build the user-facing documentation:

mdbook build docs

Preview locally at http://localhost:3000:

mdbook serve docs

Markdown Lint

Check documentation for Markdown style issues:

markdownlint-cli2 "docs/src/**/*.md"

Rules are configured in .markdownlint.json at the repository root.

Feature Flags

Lindera uses Cargo feature flags to control optional functionality and dictionary embedding.

Core Features

FeatureDescriptionDefault
analysisThe analysis chain (lindera::analysis: character filters, token filters, and the Tokenizer), re-exported from lindera-analysisYes
mmapMemory-mapped loading as the default for filesystem dictionaries (use_mmap); memmap2 is linked either wayYes
trainCRF-based dictionary training (depends on lindera-trainer; lindera::dictionary::trainer)See below
  • analysis and mmap are both enabled by default in the lindera facade. default-features = false turns both off and leaves the pure segmenter (the lindera-segmenter crate); re-enable them selectively with features = ["mmap"] or features = ["analysis"].
  • Since v7.0 the analysis chain is part of lindera again: lindera::analysis re-exports the lindera-analysis crate. In v5.x and v6.x it had to be added as a separate dependency.
  • train is enabled by default in lindera-cli, lindera-python, lindera-nodejs, lindera-ruby, and lindera-php. It is disabled by default in the lindera library crate -- enable it explicitly with features = ["train"] for library usage. It is not available in lindera-wasm.

The recommended approach is to use pre-built dictionaries as external files. Download a dictionary from GitHub Releases and specify its path at runtime:

#![allow(unused)]
fn main() {
let dictionary = load_dictionary("/path/to/ipadic")?;
}

No additional feature flags are required for this usage.

Dictionary Embedding Features (Advanced)

These features embed pre-built dictionaries directly into the binary, eliminating the need for external dictionary files at runtime. This is intended for advanced users who need self-contained binaries.

FeatureDictionaryLanguage
embed-ipadicIPADICJapanese
embed-ipadic-neologdIPADIC NEologdJapanese
embed-unidicUniDicJapanese
embed-sudachidictSudachiDictJapanese
embed-ko-dicko-dicKorean
embed-cc-cedictCC-CEDICTChinese
embed-jiebaJiebaChinese

None of these are enabled by default. Enable them as needed:

[dependencies]
lindera = { version = "7", features = ["embed-ipadic"] }

When embedding is enabled, you can load the dictionary with:

#![allow(unused)]
fn main() {
let dictionary = load_dictionary("embedded://ipadic")?;
}

[!NOTE] Dictionary data carries its own license, separate from Lindera's. When you distribute a binary with an embedded dictionary (or redistribute a built dictionary), reproduce the attribution from the corresponding dictionary crate's NOTICE.txt (e.g. lindera-unidic/NOTICE.txt) in the documentation or other materials provided with your distribution.

Combination Features

These meta-features enable multiple dictionaries at once for multilingual applications.

FeatureIncluded Dictionaries
embed-cjkIPADIC + ko-dic + Jieba
embed-cjk2UniDic + ko-dic + Jieba
embed-cjk3IPADIC NEologd + ko-dic + Jieba
embed-cjk4SudachiDict + ko-dic + Jieba

Combining Feature Flags

Multiple feature flags can be combined. For example, to embed both Japanese and Korean dictionaries:

[dependencies]
lindera = { version = "7", features = ["embed-ipadic", "embed-ko-dic"] }

Or from the command line:

cargo build --features embed-ipadic,embed-ko-dic

Notes

  • Embedding dictionaries increases binary size significantly. Only embed dictionaries you actually need.
  • The train feature adds a dependency on lindera-trainer (and through it lindera-crf) and increases compile time. It is not needed for tokenization-only use cases.
  • The mmap feature enables memory-mapped dictionary loading for filesystem-based dictionaries, requested via --mmap (CLI) or the use_mmap segmenter config key. Every large component stays lazily paged and costs no anonymous memory: the word-list files (dict.vals/dict.wordsidx/dict.words), the connection-cost matrix (matrix.mtx), and the prefix-dictionary trie (dict.trie), which is walked in place over its serialized bytes. Nothing is fully materialized at load. It has no effect on embedded dictionaries, whose data is read in place from the binary regardless.

Project Structure

Lindera is organized as a Cargo workspace with multiple crates.

Directory Layout

lindera/
├── lindera-crf/            # CRF engine (pure Rust, no_std)
├── lindera-dictionary/     # Dictionary base library
├── lindera-trainer/        # CRF-based dictionary training
├── lindera/                # Facade crate (re-exports lindera-segmenter and lindera-analysis); tests, examples, benches
├── lindera-segmenter/      # Morphological segmentation library (Segmenter API)
├── lindera-analysis/       # Analysis chain (character/token filters, tokenizer)
├── lindera-cli/            # CLI tool
├── lindera-binding/        # FFI-independent helpers shared by the language bindings
├── lindera-ipadic/         # IPADIC dictionary (Japanese)
├── lindera-ipadic-neologd/ # IPADIC NEologd dictionary (Japanese)
├── lindera-unidic/         # UniDic dictionary (Japanese)
├── lindera-sudachidict/    # SudachiDict dictionary (Japanese)
├── lindera-ko-dic/         # ko-dic dictionary (Korean)
├── lindera-cc-cedict/      # CC-CEDICT dictionary (Chinese)
├── lindera-jieba/          # Jieba dictionary (Chinese)
├── lindera-python/         # Python bindings (PyO3)
├── lindera-nodejs/         # Node.js bindings (NAPI-RS)
├── lindera-ruby/           # Ruby bindings (Magnus + rb-sys)
├── lindera-php/            # PHP bindings (ext-php-rs)
├── lindera-wasm/           # WebAssembly bindings (wasm-bindgen)
├── resources/              # Test resources and sample data
└── docs/                   # Documentation (mdBook)

Crate Descriptions

Core Crates

lindera-crf

Pure Rust implementation of Conditional Random Fields (CRF). Supports no_std environments. Uses rkyv for fast zero-copy serialization. This crate provides the statistical learning engine used in dictionary training.

lindera-dictionary

Base library for dictionary handling: loading, building, and querying dictionaries.

lindera-trainer

CRF training pipeline for creating custom dictionaries. Builds on lindera-dictionary runtime types and the lindera-crf engine. Consumed through the lindera facade's train feature (re-exported as lindera::dictionary::trainer).

ModuleRole
config.rsConfiguration management (seed dict, char.def, feature.def, rewrite.def)
corpus.rsTraining corpus processing
feature_extractor.rsFeature template parsing and feature ID management
feature_rewriter.rsMeCab-compatible feature rewriting (3-section format)
model.rsTrained model storage, serialization, and dictionary output

lindera-segmenter

The morphological segmentation library (published as lindera up to v6). Integrates the dictionary crates and provides the Segmenter API. Its dictionary module also re-exports the raw lindera-dictionary modules (builder, core, loader, viterbi, ...).

lindera

The facade crate and the single dependency for Rust users. It re-exports every module of lindera-segmenter and, with the default analysis feature, lindera-analysis as lindera::analysis. The directory also holds the integration tests, examples, and benches that exercise the public paths.

lindera-analysis

Lucene-style analysis chain on top of lindera-segmenter (used as lindera::analysis through the facade): character filters, token filters, and the Tokenizer that composes them around a Segmenter.

lindera-cli

Command-line interface for tokenization, dictionary training, export, and building. The train feature is enabled by default.

lindera-binding

FFI-independent helpers shared by all five language bindings (lindera-python, lindera-nodejs, lindera-ruby, lindera-php, lindera-wasm): a core tokenizer/schema/metadata layer that each binding wraps in its own language-native API. It depends on the lindera facade.

Dictionary Crates

Each dictionary crate contains pre-built dictionary data for a specific language and dictionary source.

CrateLanguageDictionary Source
lindera-ipadicJapaneseIPADIC
lindera-ipadic-neologdJapaneseIPADIC NEologd (extended vocabulary)
lindera-unidicJapaneseUniDic
lindera-sudachidictJapaneseSudachiDict
lindera-ko-dicKoreanko-dic
lindera-cc-cedictChineseCC-CEDICT
lindera-jiebaChineseJieba

Bindings

lindera-python

Python bindings built with PyO3. Exposes the Lindera tokenizer API to Python applications.

lindera-nodejs

Node.js bindings built with NAPI-RS. Exposes the Lindera tokenizer API to Node.js applications.

lindera-ruby

Ruby bindings built with Magnus and rb-sys. Exposes the Lindera tokenizer API as a Ruby gem.

lindera-php

PHP bindings built with ext-php-rs. Exposes the Lindera tokenizer API as a PHP extension. The extension registers as lindera and is published on Packagist as lindera/lindera for PIE: the package's composer.json lives at the repository root and points PIE at lindera-php/, where config.m4 and Makefile.frag drive cargo build through the phpize build.

lindera-wasm

WebAssembly bindings built with wasm-bindgen. Enables tokenization in browsers and Node.js.

Other Directories

resources/

Test resources including sample dictionaries, user dictionaries, and test corpora used by the test suite.

docs/

User-facing documentation built with mdBook. The table of contents is defined in docs/src/SUMMARY.md. A Japanese translation is available under docs/ja/.

lindera/examples/ and lindera-analysis/examples/

Runnable example programs demonstrating common usage patterns. Run with:

cargo run -p lindera --features=embed-ipadic --example=segment
cargo run -p lindera-analysis --features=embed-ipadic --example=tokenize

Training Pipeline

Lindera provides CRF-based dictionary training functionality for creating custom morphological analysis models. This feature requires the train feature flag.

Overview

The training pipeline follows three stages:

lindera train --> model.dat --> lindera export --> dictionary files --> lindera build --> compiled dictionary
  1. Train: Learn CRF weights from an annotated corpus and seed dictionary, producing a binary model file.
  2. Export: Convert the trained model into Lindera dictionary source files.
  3. Build: Compile the source files into a binary dictionary that Lindera can load at runtime.

How training works

lindera train builds one lattice for each sentence of the corpus, as MeCab's learner does. At every position, the lattice has the seed words that start there and the unknown words that the runtime creates there from char.def and unk.def (see Character Definition and Unknown Word Definition). The CRF learns the weights that make the annotated path, the gold tokens, the cheapest. Each gold token is labelled with the first of these that fits:

  1. The first seed row, in file order, with the same span and the same whole feature string. Unlike MeCab's learner, which compares only the first eval-size columns, every column must match, so a token whose reading or other columns differ from the seed row falls through to the next steps.
  2. The first unk.def entry, in file order, of an unknown-word candidate with the same span whose feature columns other than * all equal the gold token's columns.
  3. A virtual label built from the gold feature string, as MeCab's learner adds a virtual node. Gold tokens with the same feature string share one virtual label. Virtual labels are trained but not exported.

Every other candidate is a negative example. Once the lattices are built, lindera train prints how the gold tokens were labelled; with the files in resources/training/:

Lattice statistics: gold_lexicon=14 gold_unknown=0 gold_virtual=0 virtual_labels=0 negative_lexicon=0 negative_unknown=17

gold_lexicon, gold_unknown and gold_virtual count the gold tokens labelled by each of the three steps, virtual_labels counts the virtual labels, and negative_lexicon and negative_unknown count the negative edges from the seed lexicon and from the unknown-word candidates.

A seed row that no gold token is labelled with is only ever a negative example. So lindera train warns on standard error when no gold token is a seed word, or when fewer than half of the gold tokens whose surface has seed rows are seed words, as when the corpus does not give its gold tokens the feature strings of the seed rows. The warning shows the first gold token whose surface has seed rows but none with its feature string, next to the feature strings of the first five of those rows, and training goes on. For example, with every feature of resources/training/corpus.txt cut to its first six columns, without the base form and the readings:

Warning: 0 of the 14 gold tokens are seed words, and 14 are not although seed rows have their surface. A gold token is a seed word only when a seed row has its surface and its whole feature string, as the seed lexicon writes it; otherwise the seed rows with its surface are only wrong candidates at its position. The first such gold token, and the seed rows with its surface:
  gold token: 外国	名詞,一般,*,*,*,*
  seed row:   外国	名詞,一般,*,*,*,*,外国,ガイコク,ガイコク

Gold tokens whose surface no seed row has do not count toward the warning, so a seed lexicon that covers little of the corpus gives none.

The CRF is then trained with L-BFGS (OWL-QN when the regularization has an L1 part). As in MeCab's learner and libLBFGS, the first line search starts at a step of 1 / |d|, d being the first search direction, so that its first trial point lies at unit distance from the initial weights, and every later one at a step of 1 (#1290). Training stops at the first of these:

  • converged: the relative change of the objective, |previous - current| / |previous|, stayed below eta for 3 iterations in a row. The default eta is that of MeCab's learner, which applies the same test to every evaluation, line-search trials included, where Lindera counts iterations. Training also converged when it stopped in either of the two ways below near the optimum, where the norm of the gradient is under 1e-5 times its norm at the initial weights: there the objective changes by less than its rounding, and which of the two comes first depends on the platform (#1300).
  • no progress: an iteration left the objective unchanged far from the optimum, where the norm of the gradient was still at least 1e-5 times its norm at the initial weights. When this happens after a few iterations, the model is barely trained. lindera train writes a warning on standard error whenever training stops this way, after any number of iterations.
  • The line search of an iteration failed far from the optimum; the model keeps the best weights of the run, and lindera train writes a warning on standard error. A search direction with a component that is not finite, which the optimizer builds from a step that left the gradient unchanged, fails the line search too (#1301).
  • max-iterations was reached before the objective converged.

lindera train prints which one stopped it, with the number of iterations and of evaluations, each evaluation being one forward-backward pass over every lattice; with the files in resources/training/:

L-BFGS stopped after 8 iterations and 9 evaluations of the loss and its gradient: converged

Required Input Files

1. Seed Lexicon (seed.csv)

Base vocabulary dictionary in MeCab CSV format.

外国,0,0,0,名詞,一般,*,*,*,*,外国,ガイコク,ガイコク
人,0,0,0,名詞,接尾,一般,*,*,*,人,ジン,ジン
参政,0,0,0,名詞,サ変接続,*,*,*,*,参政,サンセイ,サンセイ

Each line contains: surface,left_id,right_id,cost,pos,pos_detail1,pos_detail2,pos_detail3,inflection_type,inflection_form,base_form,reading,pronunciation

The left_id, right_id, and cost fields are set to 0 in the seed dictionary -- the trainer will compute appropriate values from the CRF model.

The seed lexicon is read as CSV, as lindera build reads lex.csv:

  • A field may be quoted, as mecab-ko-dic's comma surface is: ",",1792,3558,788,SC,*,*,*,*,*,*,*.
  • A line that starts with # is a row, not a comment, as UniDic's # surface is. Blank lines and rows with fewer than five fields are skipped.
  • The feature of a row is the text after its fourth field, as written, quotes included. lindera export writes it back as it is, and quotes a surface that contains ,, " or a line break.
  • A quoted field that spans lines is an error that names the line. It is most likely a quote that is never closed, which would otherwise swallow the rows after it.
  • The rows keep their order, which gives their labels their ids.

To read the columns of a feature, for the rewrite rules, the feature templates and unknown-word matching, lindera train splits it as MeCab's learner does (tokenizeCSV), not as the CSV reader above does. The features of the corpus are split the same way.

  • A quoted column is one column, with "" read as ". The definition of mecab-jieba's 4S店, "authorized full-service car dealership (offering sales, spare parts, after-sales service and customer support)", is one column.
  • Spaces and tabs before a column are skipped: CC-CEDICT's COVID-19, the coronavirus disease identified in 2019/ is the two columns COVID-19 and the coronavirus disease identified in 2019/.
  • The text from a closing quote to the next , is dropped: "bamboo" radical in Chinese characters (Kangxi radical 118)/ is the column bamboo.
  • A , at the end of a feature does not start an empty last column.

2. Training Corpus (corpus.txt)

Annotated text data in tab-separated format. Each line is surface<TAB>pos_info, and sentences are separated by EOS.

外国	名詞,一般,*,*,*,*,外国,ガイコク,ガイコク
人	名詞,接尾,一般,*,*,*,人,ジン,ジン
参政	名詞,サ変接続,*,*,*,*,参政,サンセイ,サンセイ
権	名詞,接尾,一般,*,*,*,権,ケン,ケン
EOS

これ	連体詞,*,*,*,*,*,これ,コレ,コレ
は	助詞,係助詞,*,*,*,*,は,ハ,ワ
テスト	名詞,サ変接続,*,*,*,*,テスト,テスト,テスト
EOS

A gold token is a seed word only when a seed row has its surface and its whole feature string, as the seed lexicon writes it, quotes included. In the example above, each feature is that of the seed row with the same surface. A gold token whose feature differs in any column, the reading for instance, is learned as an unknown word or a virtual label, and the seed rows with its surface become negative examples at its position; lindera train warns when most gold tokens are like this (see How training works).

Training quality depends heavily on the quantity and quality of this corpus.

3. Character Definition (char.def)

Defines character type categories and Unicode code point ranges.

# Category definition: category_name compatibility_flag continuity_flag length
DEFAULT 0 1 0
SPACE 0 1 0
HIRAGANA 1 1 0
KATAKANA 1 1 0
KANJI 0 0 2
ALPHA 1 1 0
NUMERIC 1 1 0

# Character range mapping
0x0020 SPACE             # Space (reserved for SPACE)
0x0009 SPACE             # Tab
0x000A SPACE             # Line feed
0x000B SPACE             # Vertical tab
0x000D SPACE             # Carriage return
0x3041..0x3096 HIRAGANA  # Hiragana
0x30A1..0x30F6 KATAKANA  # Katakana
0x4E00..0x9FAF KANJI     # Kanji
0x0030..0x0039 NUMERIC   # Numbers
0x0041..0x005A ALPHA     # Uppercase letters
0x0061..0x007A ALPHA     # Lowercase letters

Parameters control the unknown words that a character's default category creates: whether they are created even where a dictionary word starts (INVOKE), whether a run in which each character shares a category with the one before continues as a single token (GROUP), and up to which length shorter candidates are created (LENGTH).

A range line can list more than one category (for example 0x4E00 KANJINUMERIC KANJI), and every range line needs at least one. When several lines cover the same code point, the last line decides its categories, as in MeCab. The first category on that line is the character's default category: the only one that creates unknown-word candidates, and the one the %t feature template uses.

As in MeCab, a range line can name only categories defined on an earlier line, and each category is defined once. lindera train and lindera build reject a char.def that breaks either, with an error that names the line and the category. For example, lindera train with 0x30FC KATAKANA inserted as the first line of char.def, before the line that defines KATAKANA:

Error: LinderaError { kind: Args, source: failed to parse char.def: LinderaError(kind=Content, source=char.def: line 1: category KATAKANA is not defined; a range line can only name a category defined on an earlier line) }

Keep the SPACE category, which MeCab requires in every char.def along with DEFAULT, and its line in unk.def: every category needs at least one unk.def line (see the next section). The segmenter treats its characters as whitespace: by default it skips them, as MeCab does, and does not output them. Without --keep-whitespace, a dictionary whose char.def has no SPACE category cannot be used: lindera tokenize fails with SPACE category is not defined in the dictionary (char.def).

Training creates the unknown-word candidates with the runtime's own code (Lattice::unknown_word_candidates), so it learns weights for the candidates that the built dictionary produces:

  • At each position, the character's default category creates the candidates. It creates none when its INVOKE is 0 and a seed word starts there.
  • With GROUP 1, the run in which each character shares a category with the one before becomes one candidate.
  • With LENGTH n, the candidates of 1 to n characters are created too, while each character shares a category with the first one (the length ladder). In the example above, KANJI 0 0 2 gives a kanji where no seed word starts the candidates of one and two kanji.
  • Each unk.def entry of the category gets every one of these spans.

Grouping is unbounded, as at runtime by default. The Rust API's Trainer::max_grouping_len caps it with the runtime's meaning (characters beyond the first); the CLI has no option for it.

4. Unknown Word Definition (unk.def)

Defines the unknown words that each character category creates. Each line is category,left_id,right_id,cost,feature..., where category is a category name from char.def. As in the seed lexicon, the ids and the cost are set to 0; export writes the trained values.

# Unknown word definitions
DEFAULT,0,0,0,名詞,一般,*,*,*,*,*,*,*
SPACE,0,0,0,記号,空白,*,*,*,*,*,*,*
HIRAGANA,0,0,0,名詞,一般,*,*,*,*,*,*,*
KATAKANA,0,0,0,名詞,一般,*,*,*,*,*,*,*
KANJI,0,0,0,名詞,一般,*,*,*,*,*,*,*
ALPHA,0,0,0,名詞,固有名詞,一般,*,*,*,*,*,*
NUMERIC,0,0,0,名詞,数,*,*,*,*,*,*,*
  • Every line is an entry of its own, and a category can have several: one for each part of speech its unknown words can take. mecab-ipadic's unk.def, for example, has 40 lines for 11 categories, six of them for KANJI (名詞,一般, 名詞,サ変接続 and four kinds of 名詞,固有名詞).
  • Each entry becomes a CRF label of its own, whose %t is the id of its category. A candidate of the category is created once for each entry, so training learns which entry fits where.
  • The order of the lines is kept: lindera export writes one line for each entry, in the same order.
  • Lines that start with # and empty lines are skipped. A line with fewer than five fields (the category, the two ids, the cost and at least one feature column) is an error:
Error: LinderaError { kind: Args, source: unk.def line 1: expected at least 5 fields (category, left id, right id, cost, feature), got 4 }
  • As in MeCab, every line names a category that char.def defines, and every category of char.def has at least one line. lindera train and lindera build reject an unk.def that breaks either. The error names the category and, for a line whose category char.def does not define, the line, counted with the comment and empty lines. For example, lindera train with SYMBOL,0,0,0,記号,一般,*,*,*,*,*,*,* appended to the unk.def above as its ninth line, while char.def has no SYMBOL category:
Error: LinderaError { kind: Args, source: failed to check unk.def: LinderaError(kind=Content, source=unk.def: line 9: category SYMBOL is not defined in char.def) }

5. Feature Template (feature.def)

MeCab-compatible feature extraction patterns that define what information the CRF model uses for learning.

# Unigram features (word-level)
UNIGRAM U00:%F[0]           # POS
UNIGRAM U01:%F[0],%F?[1]    # POS + POS detail (%F?[n] = optional, skipped if *)
UNIGRAM U02:%F[6]           # Base form
UNIGRAM U03:%w              # Surface form

# Bigram features (context combination)
BIGRAM B00:%L[0]/%R[0]      # Left POS / Right POS
BIGRAM B01:%L[0],%L[1]/%R[0],%R[1]  # Left POS detail / Right POS detail

Template variables:

VariableDescription
%F[n] / %F?[n]Feature field at index n (? = optional, skipped if value is *)
%L[n]Left context feature field (from rewrite.def left section)
%R[n]Right context feature field (from rewrite.def right section)
%wSurface form of the word
%uUnigram rewritten feature string
%lLeft rewritten feature string
%rRight rewritten feature string

The fields of %F[n], %L[n] and %R[n] are the columns of the rewritten features, split as described in Seed Lexicon. %u, %l and %r are the rewritten features as rewrite.def writes them, with a column that contains , or " quoted.

A template that reads past the last column of a feature gives no feature for it, with or without ?, as in MeCab's learner. For example, UNIGRAM U06:%F[6] gives nothing for the unk.def feature n,*,*,*,*, which has five columns. A bigram template gives nothing when either %L[n] or %R[n] reads past the last column.

6. Feature Rewrite Rules (rewrite.def)

Feature normalization rules in MeCab-compatible 3-section format. Sections are separated by blank lines.

# Section 1: Unigram rewrite rules
名詞,固有名詞,*  名詞,固有名詞
助動詞,*,*,*,特殊・デス  助動詞
*  *

# Section 2: Left context rewrite rules
名詞,固有名詞,*  名詞,固有名詞
助詞,*  助詞
*  *

# Section 3: Right context rewrite rules
名詞,固有名詞,*  名詞,固有名詞
助詞,*  助詞
*  *

Each line is pattern<TAB>replacement. Patterns use * as a wildcard and are matched by prefix. The first matching rule in each section is applied. Different rules can be applied to unigram, left context, and right context independently, enabling fine-grained feature normalization to reduce sparsity.

The pattern, the replacement and the feature are split into columns as described in Seed Lexicon, so a quoted column with , in it is one column. As MeCab does, the rewritten columns are joined with ,, and a column that contains , or " is quoted, with its quotes doubled: with the left rule * $5, the feature n,dian4,店,店,"shop, store",1 is rewritten to "shop, store".

Training Parameters

ParameterDescriptionDefault
lambdaRegularization coefficient (controls overfitting), finite and >= 0; with l2, MeCab's -c C is 1 / C1.0
regularizationRegularization type: l1, l2, or elasticnet, in any case (elastic_net and elastic-net also select Elastic Net)l2
elastic-net-l1-ratioL1 ratio for Elastic Net regularization (0.0-1.0, only used with --regularization elasticnet)0.5
max-iterationsMaximum number of training iterations, 1 or more100
etaTolerance of the convergence test (see How training works); 0 disables the test0.00005
max-threadsNumber of parallel processing threads, 1 or moreCPU core count

A lambda of 0 disables regularization for every type. For Elastic Net, a ratio of 0 uses only L2, and a ratio of 1 uses only L1.

The defaults are those of MeCab's learner: L2 regularization with lambda 1.0 is its objective with -c 1.0. On the mecab-ipadic cross-validation data of #1110, they score as MeCab's learner does and converge in about as many iterations, 77 to 82 (#1261, #1290): boundary F1 0.9613 and span + POS (4 columns) F1 0.9397 on average over 5 folds, where v6's default settings, L1 with lambda 0.01, reached max-iterations on every fold with 0.9578 and 0.9350. L1 sets many weights to 0, and lindera export gives labels with equal weights one context ID, so the L1 matrix.def is smaller: 8.3 MB there against 18.9 MB with L2, which makes the built dictionary 4.0 % larger. Training there takes about twice as long as MeCab's learner, and about two thirds of the time is spent outside the optimizer: before it starts (#1263) and after it stops.

CLI Usage

Train

lindera train \
    --seed seed.csv \
    --corpus corpus.txt \
    --char-def char.def \
    --unk-def unk.def \
    --feature-def feature.def \
    --rewrite-def rewrite.def \
    --max-iterations 100 \
    --max-threads 4 \
    --output model.dat

Export

Convert the trained model into dictionary source files:

lindera export \
    --model model.dat \
    --metadata metadata.json \
    --output ./dict-source

This produces the following files:

FileDescription
lex.csvLexicon with trained costs
matrix.defConnection cost matrix over every context ID, plus 0 for BOS/EOS, even when regularization leaves no bigram weight. A connection without a trained weight costs 0, as in training and in MeCab
unk.defUnknown word definition: one line for each input unk.def entry, in input order, with its category, trained IDs and cost, and feature
char.defCharacter definition
feature.defFeature template
rewrite.defFeature rewrite rules
left-id.defLeft context ID mapping
right-id.defRight context ID mapping
metadata.jsonDictionary metadata; its model_info.connection_matrix_size is the size of matrix.def, rows (right IDs) by columns (left IDs)

For example, if the unk.def above has a second KANJI line, KANJI,0,0,0,名詞,サ変接続,*,*,*,*,*,*,*, after the first one, the exported unk.def keeps both, in input order:

DEFAULT,1,1,31699,名詞,一般,*,*,*,*,*,*,*
SPACE,10,7,0,記号,空白,*,*,*,*,*,*,*
HIRAGANA,1,1,31699,名詞,一般,*,*,*,*,*,*,*
KATAKANA,1,1,31699,名詞,一般,*,*,*,*,*,*,*
KANJI,1,1,31699,名詞,一般,*,*,*,*,*,*,*
KANJI,3,3,4179,名詞,サ変接続,*,*,*,*,*,*,*
ALPHA,11,10,10957,名詞,固有名詞,一般,*,*,*,*,*,*
NUMERIC,11,10,10957,名詞,数,*,*,*,*,*,*,*

Build

Compile the exported source files into a binary dictionary:

lindera build \
    --src ./dict-source \
    --dest ./dict-compiled \
    --metadata ./dict-source/metadata.json

lindera build checks that the context IDs of every lex.csv row and unk.def line lie inside matrix.def, as MeCab's mecab-dict-index does (see Context ID check). The files that lindera export writes pass this check.

Tokenize

Tokenize text with the built dictionary:

echo "これは外国人参政権です。" | lindera tokenize -d ./dict-compiled

With the files in resources/training/ as the inputs above, the output is:

これ	名詞,代名詞,一般,*,*,*,これ,コレ,コレ
は	助詞,係助詞,*,*,*,*,は,ハ,ワ
外国	名詞,一般,*,*,*,*,外国,ガイコク,ガイコク
人	名詞,接尾,一般,*,*,*,人,ジン,ジン
参政	名詞,サ変接続,*,*,*,*,参政,サンセイ,サンセイ
権	名詞,接尾,一般,*,*,*,権,ケン,ケン
です	助動詞,*,*,*,特殊・デス,基本形,です,デス,デス
。	記号,句点,*,*,*,*,。,。,。
EOS

A space in the input, as in これは 外国人参政権です。, is skipped and gives the same output. With --keep-whitespace, it is output as the SPACE unknown word:

これ	名詞,代名詞,一般,*,*,*,これ,コレ,コレ
は	助詞,係助詞,*,*,*,*,は,ハ,ワ
 	記号,空白,*,*,*,*,*,*,*
外国	名詞,一般,*,*,*,*,外国,ガイコク,ガイコク
人	名詞,接尾,一般,*,*,*,人,ジン,ジン
参政	名詞,サ変接続,*,*,*,*,参政,サンセイ,サンセイ
権	名詞,接尾,一般,*,*,*,権,ケン,ケン
です	助動詞,*,*,*,特殊・デス,基本形,です,デス,デス
。	記号,句点,*,*,*,*,。,。,。
EOS

Output Model Format

The trained model is serialized in rkyv binary format for fast loading. It contains:

  • Feature weights learned by the CRF
  • Label set (the seed rows, then the unk.def entries)
  • Part-of-speech information
  • The unk.def entries, in file order, and the content of char.def, feature.def and rewrite.def
  • Training metadata (format version, the regularization coefficient, type and Elastic Net ratio, iterations, feature/label counts)

The format version is 3.0.0 (lindera_trainer::model::MODEL_FORMAT_VERSION), and it changes whenever the layout or the meaning of the model changes. lindera export rejects a model.dat written by another version, including every model.dat written before 7.0.0, with an error that ends in "re-run lindera train to regenerate it". Train such a model again with the current version.

The output is byte-reproducible: training the same inputs twice with the same flags produces identical model.dat bytes, so the artifact can be checksummed and diffed. This holds for any --max-threads value: the gradient and the loss are summed over a fixed partition of the training data in a fixed order, so the thread count changes only how long training takes, never its result.

API Usage

For the full lindera-trainer API surface, see Lindera Trainer Architecture and API Reference.

#![allow(unused)]
fn main() {
use std::fs::File;
use lindera_trainer::{Corpus, Trainer, TrainerConfig};

// Load configuration from files
let seed_file = File::open("resources/training/seed.csv")?;
let char_file = File::open("resources/training/char.def")?;
let unk_file = File::open("resources/training/unk.def")?;
let feature_file = File::open("resources/training/feature.def")?;
let rewrite_file = File::open("resources/training/rewrite.def")?;

let config = TrainerConfig::from_readers(
    seed_file,
    char_file,
    unk_file,
    feature_file,
    rewrite_file
)?;

// Initialize and configure trainer
let trainer = Trainer::new(config)?
    .max_iter(100)
    .num_threads(4);

// Load corpus
let corpus_file = File::open("resources/training/corpus.txt")?;
let corpus = Corpus::from_reader(corpus_file)?;

// Execute training
let model = trainer.train(corpus)?;

// Save model (binary format)
let mut output = File::create("trained_model.dat")?;
model.write_model(&mut output)?;

// Output in Lindera dictionary format. output_user.csv holds the entries
// loaded by read_user_lexicon, in input order; without that call it is empty.
let mut lex_out = File::create("output_lex.csv")?;
let mut conn_out = File::create("output_conn.dat")?;
let mut unk_out = File::create("output_unk.def")?;
let mut user_out = File::create("output_user.csv")?;
model.write_dictionary(&mut lex_out, &mut conn_out, &mut unk_out, &mut user_out)?;

Ok::<(), Box<dyn std::error::Error>>(())
}

For generating effective dictionaries for real applications:

Corpus Size

LevelSentencesUse Case
Minimum100+Basic operation verification
Recommended1,000+Practical applications
Ideal10,000+Commercial quality

Quality Guidelines

  • Vocabulary diversity: Balanced distribution of different parts of speech, coverage of inflections and suffixes, appropriate inclusion of technical terms and proper nouns.
  • Consistency: Apply analysis criteria consistently across the corpus.
  • Verification: Manually verify morphological analysis results. Maintain an error rate below 5%.

Contributing

Thank you for your interest in contributing to Lindera! This page provides guidelines to help you get started.

Getting Started

  1. Fork the repository on GitHub.

  2. Clone your fork locally:

    git clone https://github.com/<your-username>/lindera.git
    cd lindera
    
  3. Create a feature branch:

    git checkout -b feature/my-feature
    
  4. Make your changes, then verify they pass all checks:

    cargo fmt --all -- --check
    cargo clippy -- -D warnings
    cargo test
    make check-docs
    

    Note: CI enforces all four: it checks formatting, runs clippy (warnings as errors) and the tests for each crate with the features it tests that crate with, and fails on rustdoc warnings in any crate. make check-docs leaves out the language bindings, which need their languages' toolchains to build; if you change one, also run make check-docs-lindera-<binding>, as that binding's CI job does. make lint and make test apply the same per-crate features, and make lint-lindera-<binding> also runs the language-side linter that the binding's CI job runs. make lint ends with make lint-scripts, which lints the scripts outside the crates as the Lint scripts CI job does; see Build & Test.

  5. Commit and push your changes, then open a pull request.

Code Style

  • Follow the existing code style in the repository.
  • Run cargo fmt before committing.
  • All public and private items (types, functions, modules, fields, constants, type aliases) must have documentation comments (///).
  • Trait implementation methods should also have documentation comments describing implementation-specific behavior.
  • Function and method documentation should include # Arguments and # Returns sections where applicable. CI fails on these sections under the Japanese names # 引数 and # 戻り値.
  • Code comments, documentation comments, commit messages, log messages, and error messages should be written in English.
  • Avoid unwrap() and expect() in production code (test code is fine).
  • Use unsafe blocks only when necessary, and always include a // SAFETY: ... comment.
  • Use file-based module style (src/tokenizer.rs) instead of mod.rs style.

Language Binding Parity

Lindera ships bindings for Python, Node.js, Ruby, PHP, and WASM. When adding or reviewing a binding feature, apply the following policy:

  • Keep the feature set aligned. Capabilities — tokenization (tokenize / tokenize_surfaces / tokenize_nbest), user dictionaries, character filters, and token filters — should be available in every binding. A feature added to one binding should come with a plan (or a tracking issue) for the others.
  • Let representation and lifetime management follow each language. Whether tokens are class instances or plain data, naming conventions (camelCase vs. snake_case), and serialization helpers (to_dict / to_h / toArray / toJSON) are per-language decisions and do not need to match across bindings. The JS bindings return plain objects because their hosts defer class finalizers, while Python, Ruby, and PHP keep classes because their finalization is deterministic.

Put shared logic in lindera-binding (e.g. TokenView) so each binding only maps core data into its FFI types.

Testing

  • Write unit tests for all new functionality.

  • Run the relevant test(s) during development for fast feedback:

    cargo test -p <crate> <test_name>
    
  • When working on training pipeline functionality, run the lindera-trainer crate's tests:

    cargo test -p lindera-trainer
    

Commit Messages

Follow the Conventional Commits specification. Write commit messages in English.

Examples:

  • feat: add Korean dictionary support
  • fix: correct character category ID in trainer
  • docs: update installation instructions
  • refactor: split large training method into smaller functions

Documentation

  • If your change affects user-facing documentation, update the relevant files in docs/src/.

  • After editing Markdown files, verify there are no lint errors:

    markdownlint-cli2 "docs/src/**/*.md"
    
  • Rules are configured in .markdownlint.json at the repository root.

Dependencies

When adding new dependencies, verify license compatibility. Lindera uses the MIT / Apache-2.0 dual license.

Feature Flags

Use #[cfg(feature = "train")] for conditional compilation of training-related code. See Feature Flags for a full list.

Reporting Issues

When reporting a bug, please include:

  • Lindera version (lindera --version or check Cargo.toml)
  • Rust version (rustc --version)
  • Operating system
  • Steps to reproduce the issue
  • Expected and actual behavior