Mozc は、堅牢なアーキテクチャと拡張性の高い設計により、日本語入力という複雑な問題に対してエレガントな解を提供している。
Windows版はWindows Storeに登録されていないようなので、自分でビルドしないといけないようだ。ビルドのための環境構築や、エラー処理などが多少なりとも伴うので、ここはClaude Codeに助けてもらったら、結構短時間で出来上がった。
中々の使い心地なので、以下Mozcの特徴を上げてみた。
クライアント・サーバー型アーキテクチャ
Mozc の最大の特徴は、IME(入力メソッド)をクライアントとサーバーに分離した設計にある。ユーザーのキー入力はまずクライアント(Windows では TIP DLL)が受け取り、プラットフォーム固有の IPC(Windows はnamed pipe、Linux は Unix ドメインソケット、macOS は Mach port)を通じて mozc_server プロセスへ送信される。変換処理はすべてサーバー側で完結し、候補ウィンドウの描画は別プロセス mozc_renderer が担う。この三層構造により、変換エンジンのクラッシュがアプリケーションを巻き込まず、安定性が確保されている。通信プロトコルには Protocol Buffers が採用され、型安全かつ効率的なシリアライゼーションを実現している。
セッション管理と入力状態遷移
サーバー内部では、セッションが DIRECT → PRECOMPOSITION → COMPOSITION → CONVERSION という状態遷移を管理する。Composer モジュールがローマ字からひらがなへのリアルタイム変換を担い、入力文字列を CharChunk という最小単位(例:「きゃ」)に分割して保持する。変換規則は data/preedit/ 以下のテーブルデータとして外部化されており、ローマ字テーブルの差し替えも容易だ。
変換エンジンの核心 ― ラティス構造とビタビアルゴリズム
変換の中核を担う ImmutableConverter は、入力文字列に対してラティス(格子)構造を構築する。辞書から候補となる形態素ノードを列挙し、品詞間の接続コストを Connector が、セグメント境界の妥当性を Segmenter が判定する。このラティス上でビタビアルゴリズムを適用し、最小コスト経路を探索することで最適な変換候補を得る。さらに NBestGenerator が N-best パスを生成し、複数の変換候補をユーザーに提示する。
階層的辞書システム
辞書は三層構造をとる。主辞書である SystemDictionary は LOUDS(Level-Order Unary Degree Sequence)というコンパクトなトライ構造で格納され、メモリ効率に優れる。これに加え、漢字逆引き用の ValueDictionary、ユーザーが登録した単語を保持する UserDictionary が統合される。検索は前方一致(形態素解析用)、予測一致(サジェスト用)、完全一致、逆引きの4種を使い分ける。
リライターチェーンによる候補拡張
変換候補の生成後、MergerRewriter が複数の専門リライターを直列に適用する。DateRewriter は「きょう」を当日の日付に変換し、CalculatorRewriter は数式を計算結果に展開する。CollocationRewriter は共起関係に基づいて候補を調整し、SingleKanjiRewriter が単漢字候補を追加する。このパイプライン設計により、新たな変換機能の追加がモジュール単位で可能となっている。
予測変換と学習
DictionaryPredictor が辞書ベースの予測候補を生成する一方、UserHistoryPredictor がユーザーの変換履歴から学習した候補を提示する。これらは独立して動作し、最終的にスコアに基づいて統合・ランキングされる。
データ駆動設計
DataManager が言語モデル、接続コスト表、品詞マッチャー、共起データなどをメモリマップドファイルとして遅延読み込みする。変換ロジックとデータが分離されているため、辞書データの更新だけで変換品質を改善できる構造だ。
ビタビアルゴリズムと格子探索
変換エンジンの核心は ImmutableConverter のビタビ実装です。入力の各位置に対してラティス(格子)を構築し、begin_nodes[pos] / end_nodes[pos] の二次元配列でノードを管理します。各ノードのコストは:
cost = 前ノードの累積コスト + 接続コスト(品詞bigram) + 単語コスト
で計算され、最小コスト経路を動的計画法で求めます。さらに 予測用の高速ビタビ も存在し、個別ノードではなく品詞ID単位で最良コストを追跡することで状態空間を縮小しています。
人名や「1000円」のような数字+接尾辞は 制約付きビタビ で再分割され、constrained_prev ポインタで遷移先を強制します。分割コストは (複合語コスト - 接続コスト) / 2 - 1 で、分割を微妙に優遇するヒューリスティクスが入っています。
LOUDS トライによる省メモリ辞書
システム辞書は LOUDS(Level-Ordered Unary Degree Sequence) で格納されています。子ノード数Nを「1がN個 + 末尾の0」のビット列で表現し、幅優先順に並べることでツリー全体を1ビット/ノードで表現します。Select0/Select1 操作でノード間を高速に移動し、多段キャッシュで rank/select クエリを加速しています。終端ノードは別のビットベクトルで管理され、rank 操作でキーIDを算出します。
FlatConcurrentCache の擬似LRU
変換候補のキャッシュには独自の FlatConcurrentCache が使われています。バケットごとに SpinLock を持ち、各エントリに1バイトの access_clock タイムスタンプを付与。グローバルカウンタが255に達すると全エントリのクロックを右シフトし、古いエントリを自然に減衰させる 擬似LRU を実現しています。バケットサイズはデフォルト9エントリで、8ビットのサブハッシュで高速フィルタリングを行います。
ユーザー履歴の学習と減衰
UserHistoryPredictor のスコアリングは:
score = 最終アクセス時刻(Unix時間) - 文字数 + bigram補正(7日分)
Unix タイムスタンプをそのままスコアに使うことで、古い候補が自然に低スコアになる 暗黙的な時間減衰 を実現しています。bigram チェーンは最大6エントリまで保持し、連続入力パターンを学習します。また、数字キー+ASCII のみの候補はパスワード等と判断して学習をスキップする プライバシー保護 も組み込まれています。
C++20 の積極活用
.bazelrc で -std=c++20 が指定されており:
- constexpr FlatMap — ローマ字変換テーブル等を コンパイル時ソート・検証 して静的データとして埋め込み
- if constexpr — std::is_trivially_destructible_v で不要なデストラクタ呼び出しをコンパイル時に除去
- [[nodiscard]] / [[unlikely]] — アロケータやスローパスでのブランチヒント
- std::same_as concepts — テンプレートオーバーロードの制約
- std::construct_at / std::destroy_at — Arena アロケータでの明示的な生存期間管理
Arena アロケータと ObjectPool
Arena はチャンク単位でメモリを事前確保し、ラティスノード等の大量生成に使用されます。ObjectPool は解放されたオブジェクトをベクターに保持し、再利用時にのみデストラクタを実行 する遅延破棄パターンを採用。破棄順序は LIFO(逆順)で、オブジェクト間の依存関係を尊重します。
接続コストの圧縮格納
品詞ペア間の接続コスト行列は Succinct Bit Vector を用いた疎行列形式で格納されています。行単位でチャンク/コンパクトビットインデックスを持ち、コスト値は量子化されて1-2バイトに圧縮。頻出遷移には atomic なキャッシュ層 (CachingConnector) が被さり、ビタビのアクセスパターンに特化した右ノードLID単位のキャッシュ戦略を取っています。
Abseil への全面依存
エラー処理は absl::StatusOr で統一され、例外は使いません。同期プリミティブは absl::Mutex と absl::Notification、ハッシュマップは absl::flat_hash_map、文字列操作は absl::string_view です。スレッド安全性アノテーション (ABSL_GUARDED_BY 等) によりコンパイル時のデータ競合検出も有効化されています。
計算機リライターの Lemon パーサー
電卓機能は Lemon パーサージェネレータ で生成された parser.c.inc を匿名名前空間に包含する形で実装されています。日本語の「ー」をマイナス、「・」を除算として扱う演算子マップを持ち、全角→半角正規化後にトークナイズ→パースの二段階処理を行います。
プラットフォーム抽象化
constexpr enum PlatformType と TargetIsWindows() 等の constexpr 関数により、プリプロセッサではなく if constexpr でプラットフォーム分岐 しています。これにより、他プラットフォームのコードも構文的に有効なまま維持され、型チェックが全パスで働きます。