CSV索引用の形態素解析を磨き直した midori227 の記録
CSVをテキストエディタで覗き込むたびに、行数の多さと漢字の密度に肩がこわばった。
301行。全部読み切った。
辞書ファイルまで気を遣わないと、形態素解析エンジンはすぐ不安定になる。
つまずきも多かった。
midori227は、検索好きな自分のための索引アプリなのに、圧縮設定ひとつで動かなくなる。
写真展示の準備で夜ふかししていた五十代の目には、赤いコンソールエラーが堪える。
作ったもの
CSVの説明文を形態素解析に通し、抽出した二語キーワードでカテゴリをつくる索引アプリをブラウザだけで完結させた。
トップバーは翻訳ウィジェット付き、データはBlob経由でJSONダウンロードできる。
動画は旧サイトの01.mp4をそのまま読み込み、最初に動く様子を見せている。
索引生成の心臓部はcreateCategorizedIndexFromCSV()で、非同期処理と辞書ロードのエラーハンドリングを全部見直した。
最初に組んだときは、辞書パスをCDNに変えた瞬間に固まった。
Nodeの検証用スクリプトでブラウザ向けビルドをrequireしてしまい、XMLHttpRequest is not definedで停止した。対応を急いだ。
Node用ビルドに切り替え、さらに.htaccessで.gzを素通しさせるまで原因の特定に時間を要した。
加えて、CSVが未来日を含む想定で書いていたけれど、実際は301件すべてが今日以前。無駄な警告を出していた。
利用した形態素解析エンジンとライセンス
索引づくりには、JavaScriptで動作するフリーソフトの形態素解析ライブラリ kuromoji.js(形態素解析ライブラリ) を採用した。辞書フォーマットが豊富で、Node(Node.js、サーバー側のJavaScript実行環境)とブラウザの両方に対応できる点が魅力。ライセンスは Apache License 2.0(Apache 2.0ライセンス)。利用するときは著作権表示とライセンス文書をプロジェクトに同梱する必要があるので、NOTICEに追記し、ソースリポジトリのクレジットにも明記した。
前提を手早く整理
- 辞書配信…辞書を圧縮のまま配信しないと、ArrayBuffer(バイナリデータを扱う配列)の長さが合わずにクラッシュする。
AddEncoding identity .gzとSetEnv no-gzip 1は必須。
- 二語キーワード…名詞・形容詞・動詞だけを拾い、1文字語を除外した上で隣り合う単語を連結する。301件の説明文から3,157種類のペアが出た。
- しきい値…出現回数5回未満のペアは破棄する。残ったのは107件。1件でも落ちると、pill UI(ピル型のUI要素)がスカスカになるので注意。
- Blob管理…分類結果をJSON(JavaScript Object Notation、データ交換形式)で配るとき、
URL.createObjectURL()(オブジェクトURLを作成する関数)で作ったURLを放置するとメモリが漏れる。ダウンロードのたびにrevokeObjectURL(オブジェクトURLを破棄する関数)を呼ぶ。
301行のCSVと秒数の感覚
PowerShell(Windowsのコマンドラインシェル)で(Get-Content data.csv).Lengthを叩いたら301。全部を形態素解析に通すと、Node(Node.js、サーバー側のJavaScript実行環境)環境で約210ms。ブラウザだと手元のノートPCで320ms。キーワード付きのデータは288件で、13件は形容詞や名詞が1語も出ない説明文だった。数字で見えると安心する。
日付フィルターは未来日を除外する想定だったが、現状はゼロ件。
つまりfilteredRows.length === rows.length。
想定外だけれど、ロジックはそのまま残した。
未来記事が増えた瞬間に壊れたくない。
短文のメモで要点を残し、再現手順を安定化できた。
さらに、Playwrightを使った高さ測定スクリプトを走らせたところ、page.waitForTimeoutが非対応で全デモがエラーになった。
そこで初期高さを手動で調整し、計測周りの依存を最新APIに合わせるTODOを残した。
こういう雑音を記事に刻んでおくと、次のメンテで迷わない。
二語キーワードを数で追い込む
Nodeで形態素解析ライブラリをnpmから入れ直し、以下の数を実測した。
- 抽出された二語ペア総数: 3,157件
- 5回以上出現: 107件
- トップ5: 「表示 する」33回、「物体 検出」30回、「HTML コンテンツ」21回、「AI モデル」21回、「PC 推奨」20回
この数字が出た瞬間、検索パネルの設計に迷いがなくなった。
1度ですべてのカテゴリを並べると多すぎる。
ランキングから12件だけ表示し、残りはスクロールで展開する設計に切り替えた。
スライダーでしきい値を変えると、キーワードが一気に消える。5回より上にしておく理由が、視覚的に伝わる。
Blob URLとダウンロードフロー
最初の実装では、ダウンロードボタンを押すたびにBlob URLを再生成し、古いURLを放置していた。
Chromeのメモリが毎回0.6MBずつ増えていく。
分かりにくいリーク。
Blob URLを再作成するときは、前のURLをURL.revokeObjectURLで潰す。
JSONの推定サイズはEncoding後で約12KB。
小さい。
でも油断しない。
デバッグしながら感じたのは、Blob URLを新しいタブで開くと、すぐ閉じない限り常駐するということ。
記事でも「開いたら閉じてください」と明記した。
トークン列が空だったときの絶望
形態素解析を実行しても、二語ペアがゼロになるケースがある。
例えば形容詞も動詞も出ない案内文。
初期はエラーを投げていたが、今はUIで「短すぎます」と伝えて終わりにした。
怒らない。
5秒で縮むトップバーをそのまま新サイトに持ち込んだら、ユーザーが説明を読み終える前に小さくなる。
これは課題。
改善では再展開のたびに30秒猶予を付けた。
さらに、手動展開時にタイマーをクリアする。
肩の力が抜ける。
このツールで確認しながら、1文字語を除外する処理の妥当性を確かめた。短文を差し込む。「一度つまずいた。」「予想外。」そこで気持ちを立て直す。
トップバーの縮小タイマー
5秒で縮むトップバーをそのまま新サイトに持ち込んだら、ユーザーが説明を読み終える前に小さくなる課題があった。改善では再展開のたびに30秒猶予を付けた。さらに、手動展開時にタイマーをクリアする。読みやすさが上がる。
旧挙動と改善案を並べて試すと、どれだけ読みにくかったかがすぐ分かる。
UIは数字だけでは語れない。
短文も効いた。「読む前に閉じる。」という状況は避けられた。
gzip設定という地味な落とし穴
形態素解析エンジンの辞書は.zipに見えるけれど、.gz。
サーバーが自動解凍すると、ブラウザに渡るサイズが想定より大きくなって壊れる。
AddEncodingを仕込むまではログにoffset is out of boundsが大量に出た。
デバッグ効率が落ちた。
このトグルで、圧縮状態を切り替えつつログを疑似表示すると、原因の整理が速い。
ローカルApacheで.htaccessを触るときのメモとして残した。
フローの全体像を胃落ちさせる
索引生成は四つのフェーズ。
どこかで失敗すると作業が止まる。
動画も見返しながら、順番を忘れないようにまとめた。
短→長→短のリズムで読ませたい。
Flowを追うと、辞書ロード、二語抽出、Blob出力、それぞれにチェックポイントがある。
どこかでタイムアウトが起きても、段階ごとに再試行できるように設計した。
使ってみて
実際の挙動は以下から確認できる。
記事内で分割したサンプルCSVを使って索引を生成し、JSONを保存するまでの流れを体験できる。
ポイントは三つある。
- 301件のCSVを読み込む前に、辞書ロードが成功しているかをローディングメッセージで監視する
- Blob URLの寿命を必ず管理し、連続ダウンロードでメモリを浪費しない
- トップバーや翻訳ウィジェットは30秒猶予を入れて読みやすさを優先する
五十代の目でも読み切れるように、タイマーを緩め、UIの余白を広げた。
同じように辞書系アプリを作る人の参考になれば嬉しい。
今日のまとめ
- CSV(Comma-Separated Values、カンマ区切り値)301行の説明文を二語ペアに変換し、3,157種類中107件を索引に残した
- gzip(データ圧縮形式)自動展開が原因で形態素解析エンジンの辞書が壊れることを突き止め、
.htaccess(Webサーバーの設定ファイル)でidentity転送に切り替えた
- Blob URL(オブジェクトURL)の破棄と、30秒猶予を設けたトップバー制御でUI(ユーザーインターフェース)のストレスを減らした
- テキストが短すぎる場合は、警告を出して終わるようUXを調整した
- 解析用のNodeスクリプトとブラウザ版を分離し、検証が安全に回るようにした
さらに深く学ぶには
最後まで読んでくださり、ありがとうございました。
JSONと辞書でつまずいた誰かが、少しでも早く抜け出せますように。
検索好きな写真屋より。