娘が4月から幼稚園に通い始めました。🌸
園からは予定表、給食献立、同意書、感染症対応などのプリントが紙で届くのですが、後から知りたくなるのは文書そのものではなく、「明日の持ち物は何か」「この病気にかかったらどう対応すべきか」といった断片的な情報です。
紙のまま管理すると、必要な情報にたどり着くのが手間になると感じ、自然言語で検索する形にできないかを考えていました。
そこで、LINE を入口にして、Notion に整理した情報を Cloud Run 上のアプリケーション経由で検索できる、小規模な文書検索システムを試作しました。
こういった類の課題解決をするための商用サービスはいくつかありますが、普段の私の業務ではあまり触れない領域でもあり、個人的な興味から実際に手を動かして構成を考えてみました。
今回は、LINE・Notion・Cloud Run を使って小規模な文書検索システムを組むにあたって考えたことと、最終的に採用した構成についてまとめます。
構成としては、LINE から質問を受け付け、バックエンドで文書を検索し、必要に応じて LLM で回答を生成する仕組みです。
- ユーザーインターフェース: LINE 公式アカウント
- アプリケーション: FastAPI(Python)
- 実行環境: Cloud Run
- インフラ管理: Terraform
- データ管理: Notion を編集 UI にし、同期済み JSON を検索
- 回答方式: ルールベース応答 + 軽量な文書検索 + 必要時のみ LLM
挨拶や定型案内、資料に存在しない質問の打ち切りはアプリケーション側で処理し、LLM 呼び出しを必要な場面に限定しています。
前提と制約
最初に思いつくのは、紙を OCR(光学文字認識) で文字を抽出して、どこかにためておいて検索する、という構成です。
ただ、少し考えるとすぐにいくつか問題がありました。
- 文書は紙で届くため、最初の入力が非構造
- 新しい文書が継続的に追加される
- 利用者は専用アプリをインストールしたくない
- 小規模運用なので、できるだけ無料または低コストにしたい
- OCR の品質は文書レイアウトや撮影品質にかなり左右される
この時点で、「完全自動で OCR で文字を抽出して構造化し、常に正確に答える」構成は現実的ではなさそうだと感じました。特に表組みや複数カラムのプリントは、OCR 単体だと順序が崩れやすく、重要な情報が欠落しやすいためです。
また、入力元が紙である以上、モデルや検索基盤を豪華にしても、元データが崩れていれば精度は上がりません。今回の規模だと、検索基盤を重くするより、文書をどう整えて保持するかの方が重要そうでした。
全体構成
最終的には、次のような役割分担にしました。
- 紙文書を画像として取り込む
- 必要に応じて OCR テキストを補助情報として用意する
- 元画像を確認しながら Markdown に整形する
- 整形結果を Notion に蓄積する
- Notion から同期した保存済み JSON を対象に検索する
- 検索結果が十分にある場合のみ LLM に要約させる
検索精度を左右するのは、モデルの性能よりも「どの文書をどういう形で持っているか」の方が支配的です。きれいなテキストが蓄積されていれば、小規模な検索でもかなり実用になります。

※チャット上で /sync というコマンドを入力すると Notion の内容が同期されるようにしています。
インターフェースとしての LINE
利用頻度は高くなくても、使いたい瞬間にすぐ開けることが重要でした。そのため、専用フロントエンドを作るより、既存のメッセージングチャネルを UI として使う方が合理的だと考えました。
LINE を使うことで、以下の利点がありました。
- 利用者に新しいアプリのインストールを求めない
- 通知・履歴・入力 UI をそのまま利用できる
- 少人数運用では専用フロントエンドの保守コストを避けられる
その代わり、バックエンド側では LINE の webhook を公開で受ける必要があります。今回は、Cloud Run 自体は公開しつつ、LINE の署名検証と許可ユーザー ID の制限で利用者を絞る形にしました。
データ管理の置き方
入力元が紙文書であり、文書ごとにフォーマットが違う状況では、最初から厳密なスキーマを決めるより、まず柔らかく保持できる形の方が扱いやすいと考えました。
当初は Markdown ファイルをそのままアプリ側で読む構成も考えていました。人間にも読みやすく、差分も追いやすいので、最初に試す形としては自然です。
ただ、この形だと Cloud Run 上ではファイル更新のたびに再デプロイが必要になります。
そこで、最終的には編集 UI は Notion、検索対象は同期済み JSON という分担にしました。これにより次の利点がありました。
- 利用者が Notion 上で直接更新できる
- Cloud Run への再デプロイなしでデータ更新できる
- 検索時は保存済みデータだけを見るのでレイテンシが安定する
- Notion 障害時でも最後の同期結果で回答できる
この判断により、「まず取り込む」「あとで少しずつ整える」という運用のしやすさを保ったまま、更新フローも軽くできました。
取り込みフロー
OCR 単体では、次のような問題が出ます。
- 行や列の順序が崩れる
- 見出しと本文の対応が壊れる
- 一部の文字が落ちる
- レイアウト情報が失われる
この部分は検索システム本体とは少し性質が違うので、取り込み用のワークフローとして切り分けて考えることにしました。
そのため、最終的には「半自動ワークフロー」に寄せました。
ここでは自動化の度合いを上げることよりも、誤変換に気づけることと、あとから運用者が見返したときに状態を追えることを優先しています。
- 未処理の画像を作業用の置き場に集める
- 画像内容を確認する
- 人間に分かる名前へ整理する
- 元資料を保管用の置き場に退避する
- 転記用の Markdown を生成する
- Notion へ転記する
完全自動化は見栄えがよい一方で、誤変換に気づきにくく、運用者以外が状況を追いにくいことがあります。今回は、「未処理」「処理済み原本」「Notion 上の正本」「検索用データ」の状態を分けて扱うことで、運用の見通しがかなり良くなりました。

LLM の役割
すべての質問を LLM に投げる構成は、コスト・レイテンシ・失敗時挙動の面で扱いづらいことが分かりました。特に無料枠ではレート制限にすぐ達し、雑談や資料外質問で API を消費するのは効率が悪いです。
そのため、回答経路をいくつかに分けています。
- 挨拶: 固定応答
- お礼: 固定応答
- 「何ができるの?」: データからローカル生成
- 資料にない質問: ローカルで打ち切り
- 資料に根拠がある質問: 検索結果をもとに回答生成
この分岐を入れたことで、LLM は「なんでも答えるエンジン」ではなく、「検索で根拠が取れたときに自然な日本語へ整える役割」に近づきました。

今後の改善
- 検索前処理の改善
- 日付やイベント名の正規化
- 文書追加時の Markdown 整形支援の強化
- 機能案内のカテゴリ要約改善
- デプロイフローの自動化
特に今後精度を上げるなら、まずモデルを変える前に検索前処理を改善するのが筋だと考えています。たとえば「4/21」「4月21日」「来週月曜」が同じ候補へ寄るような前処理が入るだけでも、体感品質はかなり変わるはずです。
まとめ
この取り組みを通して一番大きかったのは、文書検索システムの品質は LLM の性能だけでは決まらない、という点です。小規模運用では、
- どの文書をどう保持するか
- OCR と人手確認をどう分担するか
- どの質問を LLM に渡さないか
- 運用状態をどう見える化するか
といった周辺設計の方が、使い勝手を大きく左右します。
低コスト・少人数・既存 UI 活用という制約の中では、理想的な完全自動化よりも、壊れにくく、変更しやすく、利用者に説明しやすい構成の方が長く使えます。
今回の構成はその一例として、同じように小規模な文書検索やナレッジ整理を考えている場面でも応用できるのではないでしょうか。
