Webエンジニア向けプログラミング解説動画をYouTubeで配信中!
▶ チャンネル登録はこちら

【ITニュース解説】Building a Multilingual Book Platform with FastAPI and PostgreSQL Full-Text Search

2026年09月30日に「Dev.to」が公開したITニュース「Building a Multilingual Book Platform with FastAPI and PostgreSQL Full-Text Search」について初心者にもわかりやすく解説しています。

作成日: 更新日:

ITニュース概要

FastAPIとPostgreSQLで多言語書籍プラットフォームを構築。1万冊超の多言語対応検索が課題だったが、PostgreSQLの全文検索機能(tsvector, GINインデックス)を導入。これにより、従来の約100倍高速で言語のニュアンスを理解する検索を実現した。外部サービスに頼らずDB機能を活用した事例だ。

ITニュース解説

AIを活用した書籍翻訳サービスを開発しているLectuLibreという会社が、多言語対応の書籍プラットフォームを構築した経験について解説する。このプラットフォームは、ユーザーがアップロードしたEPUBやPDF形式の書籍を、ClaudeやDeepSeekといった大規模言語モデル(LLM)を使って複数の言語に翻訳し、提供するサービスである。翻訳処理自体は最先端のAI技術を使う華やかな部分だが、翻訳された書籍をユーザーに提供するバックエンドのインフラ、特に「検索機能」の実現は、予想以上に困難な技術的課題であった。

このプラットフォームのバックエンドは、Python言語とFastAPIというWebアプリケーションフレームワーク、そしてPostgreSQLというリレーショナルデータベースで構築され、VPS(仮想プライベートサーバー)上で運用されている。サービス開始当初は問題なく機能していたが、5つの言語で1万冊を超える書籍が蓄積されると、検索機能のパフォーマンスが著しく低下するという問題に直面した。この解説では、PostgreSQLが標準で提供する全文検索機能を使って、どのように多言語対応の全文検索問題を解決したのか、そしてその過程で得られた貴重な教訓について詳しく説明する。

サービス開始当初は、書籍のタイトル、著者、説明などのメタデータをデータベースのシンプルなbooksテーブルに保存していた。検索機能の実装には、SQLのILIKE演算子を使用し、例えばSELECT * FROM books WHERE title ILIKE '%query%' OR author ILIKE '%query%';のような形で検索を実行していた。この方法は、書籍数が数百冊程度の小規模なうちは問題なく機能していたが、書籍数が1万冊を超えると、検索クエリの実行に数百ミリ秒、時には500ミリ秒以上もかかるようになり、ユーザー体験を損なうほど遅くなってしまった。さらに深刻な問題は、この検索方法が言語特有のニュアンスを全く考慮していなかったことである。例えば、単語の語幹(単語の原型)を認識しないため、スペイン語で「走る」を意味する「correr」で検索しても、「走っている」を意味する「corriendo」や「走った」を意味する「corrió」を含む書籍を見つけることができなかった。また、検索にとって重要ではない単語(ストップワード)の除去や、アクセント記号(ダイアクリティクス)の適切な扱いもできていなかった。

これらの問題を解決するため、新しい検索ソリューションには明確な要件が設定された。一つは、正確な語幹認識とストップワード除去を伴う多言語対応であること。次に、アクセント記号や大文字・小文字を区別しない検索に対応すること。さらに、理想的には50ミリ秒未満という高速な検索結果を返すこと。そして最後に、複雑な外部インフラを追加することなく、将来の規模拡大にも対応できることだった。当初はElasticsearchやMeilisearchのような高度な外部検索エンジンも検討されたが、現在の書籍数(1万冊以上)であればPostgreSQLに組み込まれている全文検索機能で十分であり、外部サービスを運用する手間(運用の複雑さやコスト)を省けるという結論に至った。

PostgreSQLの全文検索機能は、tsvector(テキストを検索に適した形式に変換したもの)とtsquery(検索キーワードを検索に適した形式に変換したもの)という特別なデータ型、そして多くの言語に対応した組み込みのテキスト検索設定を提供している。各言語設定には、単語の語幹を認識する「ステマー」、検索に不要な単語のリストである「ストップワードリスト」、そしてテキストを単語に分解する「パースルール」が含まれる。この機能を利用する計画として、まず書籍ごとに、それが利用可能な各言語に対応するtsvectorをデータベースに保存することにした。そして、このtsvectorに対する高速な検索を実現するためにGINインデックスを使用し、検索時には適切な言語設定でtsqueryを生成してクエリを実行し、結果の関連度をts_rank関数で評価して表示することにした。一つの書籍が複数の言語で存在しうるため、言語ごとの検索ベクトルを保持する専用のテーブルを作成する設計を採用した。

データベースの構造としては、まず書籍の基本的な情報を格納するbooksテーブルがあり、そこには書籍IDや元の言語などの情報が含まれる。これに関連付けられた形でbook_search_vectorsという新しいテーブルを作成した。このテーブルは、book_id(どの書籍のベクトルか)、language(どの言語のベクトルか)、そしてvector(実際のtsvectorデータ)というカラムで構成される。この設計により、一つの書籍に対して複数の言語の検索ベクトルを柔軟に管理でき、特定の言語のみで検索したり、複数の言語を横断して検索したりすることが可能となる。また、book_search_vectorsテーブルのlanguageカラムとvectorカラムの組み合わせに対してGINインデックスを作成することで、検索処理の速度を大幅に向上させた。GINインデックスは、複雑なデータ型であるtsvectorの検索を効率的に行うための特別なインデックスである。

検索ベクトルを生成する処理は、書籍がデータベースに追加されたり、新しい翻訳が完了したりした際に行われる。PostgreSQLのto_tsvector関数は、指定された言語設定とテキスト(例えば書籍のタイトル、著者、説明文を結合したもの)を受け取り、それを検索に適したtsvector形式に変換する。この変換処理では、指定された言語のルールに基づいてストップワードが除去され、単語が語幹に変換される。システムが非同期処理を多用しているため、データベースとの接続にはasyncpgという非同期対応のPostgreSQLドライバーを直接使用して、効率的にtsvectorの生成と更新を行った。生成されたtsvectorはbook_search_vectorsテーブルに挿入され、もし既に同じ書籍と指定された言語のベクトルが存在する場合は、新しいベクトルで更新されるように設定している。PostgreSQLが標準でサポートしていない言語や特定の方言(例えば一部の地域スペイン語)の場合には、「simple」という汎用的な設定を利用した。「simple」設定はテキストを小文字化して単語に分割するが、語幹認識やストップワード除去は行わない。さらに、unaccent拡張機能を利用することで、アクセント記号を除去し、検索の柔軟性を高める工夫も施されている。

FastAPIで構築された検索エンドポイントでは、ユーザーからの検索キーワードと、オプションとして検索対象の言語フィルターを受け取る。もし言語が指定されていれば、その言語設定を使って検索クエリを生成する。言語が指定されていなければ、全ての言語を横断して検索するために「simple」設定を使用する。検索クエリの生成には、PostgreSQLのwebsearch_to_tsquery関数を使用した。この関数は、Google検索のような直感的なユーザー構文を解釈し、単語間に自動的にAND条件を追加してくれるため、開発の手間を省きつつ使いやすい検索機能を提供できる。実際のSQLクエリでは、book_search_vectorsテーブルとbooksテーブルを結合し、sv.vector @@ {tsquery}という条件でtsvectorとtsqueryがマッチするかどうかを判定する。言語フィルターが指定されていれば、AND language = '{lang}'という条件を追加して検索対象を絞り込む。検索結果はts_rank関数によって計算された関連度(検索キーワードとの一致度)が高い順に並べられ、上位20件がユーザーに返される。

この最適化の結果、以前のILIKEを使った検索では平均500〜800ミリ秒かかっていた処理が、tsvectorとGINインデックスを導入することで、5〜15ミリ秒で完了するようになった。これは、検索速度が50倍から100倍に改善されたことを意味する。インデックスの作成や更新によるデータベースの書き込み処理のオーバーヘッドは約20%増加したが、検索(読み込み)の頻度が書き込みをはるかに上回るため、これは十分に許容できる範囲である。さらに、PostgreSQLのメモリ設定の監視も行い、GINインデックスのスキャンが遅かった原因がwork_memというメモリ設定の値が低すぎることだと判明した。このwork_memを4MBから16MBに増やすことで、インデックスの処理速度が30%向上し、全体的なパフォーマンスに貢献した。

この開発過程でいくつかの課題とトレードオフにも直面した。一つは言語設定の複雑さである。PostgreSQLは主要な約30の言語に対応しているが、全ての言語や特定の方言をカバーしているわけではない。例えば、ラテンアメリカスペイン語のような特定の方言に対しては、既存のスペイン語設定をコピーしてストップワードを調整するなど、カスタム設定を作成する必要があった。組み込みでサポートされていない言語に対しては、「simple」設定を使用せざるを得ず、その場合は語幹認識やストップワード除去の精度が犠牲になることを受け入れた。

検索ベクトルの更新方法も課題の一つだった。当初はデータベースのトリガー機能を使って、書籍の挿入や更新時に自動的にtsvectorを更新しようと試みた。しかし、システムの非同期ワークフローとの相性が悪く、翻訳の更新時に検索データが古いままになるという問題が発生した。最終的には、翻訳が完了した後にPythonコード内で明示的にベクトルを更新する方式に移行し、これにより更新処理の制御が容易になり、デバッグもしやすくなった。

ユーザーが言語を指定せずに検索する場合の「多言語横断検索」も考慮すべき点だった。この場合、「simple」設定を使用して語幹認識を行わないことで、言語を問わずある程度の検索精度を保つことができる。将来的には、より高い関連度を得るために、各言語で個別に検索した結果を統合するような複雑なアプローチも検討できるが、現時点ではそこまでの必要性はないと判断している。

PostgreSQLとElasticsearchの選択についても、現在の規模(1万冊の書籍、100万未満の検索ベクトル)であればPostgreSQLで十分に要件を満たせるという結論になった。Elasticsearchクラスターを導入・運用する手間とコストを省けたことは大きな利点であり、書籍数が現在の10倍以上に増えるようなことがあれば、その時に再検討するという方針である。

これらの経験からいくつかの重要な教訓が得られた。まず、新しい外部サービスを追加する前に、既存のデータベースが持つ強力な機能を最大限に活用すべきだということ。PostgreSQLの全文検索機能は非常に強力で、過小評価されがちである。次に、インデックスの適切な設定とデータベースのチューニングがパフォーマンス改善には不可欠であること。GINインデックスは全文検索において必須であり、work_memやmaintenance_work_memのようなPostgreSQLのメモリ関連設定値を適切に調整することが重要である。さらに、多言語対応のプラットフォームを構築する場合は、開発の初期段階から多言語を考慮した設計をすべきだということ。検索ベクトルを言語ごとに分離した設計は、後からの大規模な移行作業を避ける上で非常に有効だった。そして最後に、実際のデータでテストすることの重要性である。語幹認識やストップワード除去といった機能がユーザーの検索体験にどれほど大きな影響を与えるかを、本番に近いデータで検証することで強く認識できた。

FastAPIとPostgreSQLを組み合わせて多言語対応の書籍プラットフォームを構築したこの経験は、多くの学びをもたらした。PostgreSQLの組み込み全文検索機能を効果的に活用することで、複雑なインフラを追加することなく、高速で言語のニュアンスを理解する検索機能を実現できたのである。もし同様のシステム構築を検討しているならば、まずはPostgreSQLの全文検索機能を試してみることを強く推奨する。それだけで要件を満たせる可能性は大いにある。

関連コンテンツ

関連IT用語

関連ITニュース