Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

9. 実践事例:英辞郎データ(235万件)の高速BM25検索アーキテクチャ

実プロジェクトにおけるBM25の適用課題

前章までに、Python と LangChain(rank_bm25)を用いた BM25 検索の実装や、インデックスの永続化手法を解説しました。 しかし、実際のプロダクト開発において、数百万件規模のテキストデータを扱う場合、インメモリ中心のナイーブなアプローチではメモリ枯渇や起動遅延などの重大な壁に直面します。

本章では、AI プロンプト作成支援ツール「Prompt Studio」(apps/prompt-studio/)において、約 160MB・2,349,580 件の英辞郎辞書テキスト(EIJIRO-1447-UTF8.TXT)から英語・日本語の高速な BM25 全文検索を実現するために実施した、技術選定、アーキテクチャ設計、性能検証、およびトラブルシューティングの実践記録を解説します。

1. 背景と要件

Prompt Studio では、AI プロンプト作成中に英単語や日本語訳をストレスなく補完・辞書引きできるようにするため、以下の厳しい要件が求められました。

  • 対象データ: 英辞郎 UTF-8 テキストデータ(EIJIRO-1447-UTF8.TXT / EIJIRO.txt)
  • データ規模: ファイルサイズ約 160MB、総エントリ数 2,349,580 行(約 235 万行)
  • 機能要件:
    • 英語見出し語および日本語訳の両方からの全文検索
    • BM25 スコアリングによる関連度ソート
    • 最大 100 件の高速返却(UI 入力補完のためレスポンス数十ミリ秒以内)
    • 初回起動時に自動インデックス作成を行い、2回目以降は再インデックス不要で即時起動できること

2. アプローチの検討と決定

初期検討: Pure Python (rank_bm25) + メモリ保持の限界

当初は、第5章や第8章で紹介したような Python の軽量ライブラリ rank_bm25(BM25Okapi)を用いて、テキストを形態素解析・分かち書きしてインメモリで転置インデックスを保持し、pickle で永続化する構成を検討しました。

しかし、実データ(235万件)で検証したところ、以下の致命的な問題が判明しました。

  1. メモリフットプリントの爆発: 235 万エントリのトークンリストおよびスコア表を Python オブジェクトとしてヒープ上に展開すると、プロセス常駐メモリが数 GB 〜 10GB 超に達し、デスクトップ環境でメモリ圧迫を引き起こす。
  2. 起動時間の肥大化: 巨大なインデックスを pickle.load() でデシリアライズするだけでも数十秒〜数分を要し、開発時オートリロードやプロセスの起動・停止が実質不可能になる。
  3. スケーラビリティの限界: Pure Python によるスコア計算ループでは、ヒット件数が多いクエリで数百ミリ秒〜数秒のレイテンシが発生し、キーストロークごとの即時補完に追従できない。

採用方針: SQLite FTS5 + 2-gram ハイブリッド構成

大規模なインメモリ展開を避け、OS のページキャッシュを活用したディスクベースの転置インデックスエンジンとして、Python 3 標準の SQLite FTS5 (Full-Text Search 5) を採用しました。

比較項目Pure Python (rank_bm25)SQLite FTS5 (採用)
外部依存rank-bm25 などの外部パッケージが必要Python 3 標準組み込み(ゼロ外部依存)
常駐メモリ数 GB 〜 10GB 超(全件ヒープ展開)数十 MB(ディスクベース + ページキャッシュ)
起動・準備時間起動ごとにデシリアライズで数十秒〜数分0 ms(DB ファイルオープンのみ)
スコアリングPython インタプリタ実行SQLite 内蔵 bm25()(C 言語レベル実行)
永続化管理自前の pickle キャッシュ管理が必要単一の .db ファイルで完結

3. 実装アーキテクチャ

3.1 データパースと正規表現

英辞郎テキストは以下の形式で構成されています。

■見出し語 [品詞] : 訳語

正規表現 ^■([^:]+?)(?:\s*\{([^}]+)\})?\s*:\s*(.+)$ を用いて、見出し語(word)、品詞(pos)、訳語(translation)をストリーミングで高精度にパースします。

3.2 日英ハイブリッド・トークナイズ(文字バイグラム + unicode61)

SQLite FTS5 標準の tokenize = 'unicode61' は英数字の単語境界(空白や記号)で自然にトークナイズされますが、空白区切りのない日本語(漢字・ひらがな・カタカナ)は1つの連続トークンとして扱われてしまい、部分一致検索がヒットしません。

外部の重い形態素解析エンジン(MeCab や Janome)を導入すると依存関係が肥大化するため、文字バイグラム(2-gram)ハイブリッド方式 を自前実装しました。

  1. インデックス作成時:
    • 日本語の文字種(ひらがな・カタカナ・漢字・全角記号等)を検出した場合、その部分のみ2文字ずつのバイグラムに展開して FTS5 カラムに格納します。
    • 例: サイバーパンク → サイ イバ バー ーパ パン ンク
    • 英語部分はそのまま格納され、FTS5 の unicode61 が英単語単位でトークナイズします。
  2. 検索クエリ時:
    • 入力クエリに日本語が含まれる場合、同様にバイグラム展開して AND 検索条件を構築します。
    • 英語クエリの場合は、トークンを空白区切りでそのまま FTS5 クエリに渡します。
  3. FTS5 テーブル定義:
CREATE VIRTUAL TABLE eijiro_fts USING fts5(
    word,
    pos UNINDEXED,
    translation,
    tokenize = 'unicode61'
);

3.3 BM25 スコアリングと重み付け

FTS5 組み込みの bm25(eijiro_fts, 10.0, 1.0) 関数を使用します。 見出し語(word)に対するマッチの重みを 10.0、訳語(translation)に対する重みを 1.0 に設定することで、見出し語が完全または部分一致した項目が検索結果の上位に優先表示されます。

SELECT word, pos, translation, bm25(eijiro_fts, 10.0, 1.0) AS score
FROM eijiro_fts
WHERE eijiro_fts MATCH ?
ORDER BY score
LIMIT ?;

Note

SQLite FTS5 の bm25() は負の値(0 に近いほどスコアが低く、負に大きいほど関連度が高い)を返します。 クライアント表示時には round(abs(score), 3) で正のスコアに正規化して利用します。

4. 性能検証結果

実データ(2,349,580 件)を用いた実測ベンチマーク値は以下のとおりです。

評価項目実測値備考
インデックス構築時間49.4 秒50,000 件単位のバッチトランザクション挿入
インデックス DB サイズ573 MB元テキスト(160MB)に対し約 3.5 倍の転置インデックス
サーバー起動時間< 10 msDB が存在すれば即時レディ状態
英語検索レスポンス1 〜 3 msクエリ例: cyberpunk(FTS5 転置インデックス直接ヒット)
日本語検索レスポンス20 〜 40 msクエリ例: サイバーパンク(バイグラム転置インデックスヒット)
プロセス常駐メモリ約 40 MBFastAPI + Uvicorn 起動時のメモリ使用量

5. 遭遇した課題と対策(トラブルシューティング)

デスクトップ Web アプリケーションへの組み込みにあたり、以下の実践的な課題に直面し、対処を行いました。

5.1 Vite 8 ウォッチャーの巨大ファイルロック問題 (EBUSY)

  • 事象: apps/prompt-studio/data/ に 160MB のテキストデータおよび 573MB の SQLite DB を配置した際、Vite のファイル監視(chokidar / fsevents)が巨大ファイルをスキャン・排他ロックし、Windows 上で EBUSY: resource busy or locked エラーが発生してフロントエンドがクラッシュした。
  • 対策: apps/prompt-studio/vite.config.ts において、server.watch.ignored に ['**/data/**'] を追加し、データディレクトリを監視対象から完全に除外した。

5.2 Uvicorn のオートリロード監視ループ

  • 事象: バックエンド開発サーバー(uvicorn main:app --reload)が、インデックス生成中や DB アクセス時に DB ファイルの更新を検知してリロードループに陥る問題があった。
  • 対策: apps/prompt-studio/main.py の uvicorn.run にて、reload_excludes=["data/**", "*.txt", "*.TXT", "*.db*", "*.pkl"] を指定し、巨大ファイルやバイナリの監視を明示的に遮断した。

5.3 日本語クエリの URL エンコード

  • 事象: クライアントから日本語クエリを送信する際、シェルや HTTP クライアントの文字コード差異によってリクエストが不正となる事象が発生した。
  • 対策: クライアント側(React SPA)の encodeURIComponent を用いたパラメータ送信を徹底し、バックエンド側では FastAPI の Query(..., min_length=1) でデコード・検証を行う構成とした。

6. まとめ

235 万件規模の大規模辞書テキストであっても、Python 標準の SQLite FTS5 と文字バイグラムを活用することで、外部ミドルウェアを一切追加せずに、常駐メモリ 40MB・応答数ミリ秒の極めて軽快な BM25 検索基盤を構築できることが実証されました。

次章では、この大規模辞書データを対象に、「Chroma 等のベクトルデータベースを組み合わせたハイブリッド運用(Dense + Sparse)」の有効性とコスト、ボトルネックを検証した評価結果を解説します。