技術 約3分で読めます

kuromoji.jsのpath.join()バグとCDN辞書読み込みの解決策

やりたかったこと

Labページに日本語の形態素解析ツールを追加したかった。品詞情報付きで。

最初の選択肢:Sudachi

SudachiはWASM版があるが、辞書ファイルが50MB以上。Vercelの転送量を即座に食い尽くすので却下。

次の選択肢:kuromoji.js

kuromoji.jsは辞書が約12MB(gzip圧縮時)で比較的軽量。辞書をjsDelivrのCDNから配信すれば、Vercelの帯域を消費しないはず。

kuromoji.builder({
  dicPath: 'https://cdn.jsdelivr.net/npm/kuromoji@0.1.2/dict/'
}).build((err, tokenizer) => {
  // ...
});

ローカルでは動く

開発環境では問題なく動作。しかし本番ビルドすると…

本番で404エラー

GET https://lilting.ch/cdn.jsdelivr.net/npm/kuromoji@0.1.2/dict/base.dat.gz 404

なぜかlilting.ch/cdn.jsdelivr.net/...という相対パスになっている。

原因:path.join()がURLを壊す

kuromoji.jsは内部でNode.jsのpath.join()を使って辞書パスを組み立てている。

// kuromoji.js内部
path.join(dicPath, 'base.dat.gz')

path.join()はファイルシステム用の関数なので、URLのhttps://の二重スラッシュを正規化してしまう:

path.join('https://cdn.jsdelivr.net/dict/', 'base.dat.gz')
// → 'https:/cdn.jsdelivr.net/dict/base.dat.gz'  // スラッシュが1つ消える

結果、ブラウザはこれを相対パスとして解釈し、現在のドメインからの相対パスになる。

これはGitHub Issue #37で報告されている既知のバグ。

解決策:@patdx/kuromojiでカスタムローダー

@patdx/kuromojiはkuromojiのフォークで、カスタムローダーをサポートしている。

pnpm add @patdx/kuromoji

カスタムローダーを実装して、path.join()を完全に回避:

import * as kuromoji from '@patdx/kuromoji';

const CDN_DICT_BASE = 'https://cdn.jsdelivr.net/npm/kuromoji@0.1.2/dict/';

// gzip解凍
async function decompressGzip(data: ArrayBuffer): Promise<ArrayBuffer> {
  const ds = new DecompressionStream('gzip');
  const stream = new Response(data).body!.pipeThrough(ds);
  return new Response(stream).arrayBuffer();
}

const customLoader: kuromoji.LoaderConfig = {
  async loadArrayBuffer(filename: string): Promise<ArrayBufferLike> {
    const url = CDN_DICT_BASE + filename;
    const res = await fetch(url);
    if (!res.ok) throw new Error(`Failed: ${url}`);

    const data = await res.arrayBuffer();
    // .gzファイルは解凍が必要
    return filename.endsWith('.gz') ? decompressGzip(data) : data;
  }
};

const tokenizer = await new kuromoji.TokenizerBuilder({
  loader: customLoader
}).build();

ポイント

  1. 直接fetchpath.join()を使わず、文字列連結でURLを組み立てる
  2. gzip解凍:辞書は.gz圧縮されているので、ブラウザのDecompressionStream APIで解凍
  3. Vercel帯域ゼロ:辞書はjsDelivrから配信されるので、Vercelの転送量を消費しない

まとめ

方法問題
Sudachi WASM辞書50MB+、重すぎ
kuromoji.js + CDNpath.join()バグでURL壊れる
@patdx/kuromoji + カスタムローダー動く

ブラウザで形態素解析を実装する場合、kuromoji.jsは便利だがCDNから辞書を読み込む場合はpath.join()問題に注意。フォーク版を使ってカスタムローダーを実装するのが確実。


形態素解析ツール