PDF検索自動化で業務時間を1/3に削減した実例レポート

まずはこのステップを実行してみて - PDF検索の自動化で作業時間と手間を一気に減らす実践ヒント

  1. メタデータやキーワードをPDFプロパティに必ず3つ以上追加する

    87%まで検索精度が上がり、探す手間も半減

  2. 月1回はAIツールで検索履歴を分析し、よく使うフォルダ構成へ整理する

    必要なファイルが常時トップ表示され、平均4分→20秒台に短縮

  3. APIや自動抽出機能を使いPDFからテキスト・画像情報を5件以上一括取得してみる

    人手ゼロでも高精度データ化できて新しい業務時間創出につながる

  4. (週1回)古い不要ページや重複ファイルは必ず削除・統合しておく

    年6時間以上の無駄カット、管理もラクになる

技術文書検索を効率化する方法とPDF活用の答え

正直なところ、従来の検索方法って、本当に技術文書を探すにはちょっと…限界あるよね。えーと、500以上もPDFファイルが溜まってる状況で、その中から知りたい情報にたどり着こうと試みたこと、皆さん一度はあるかな。「Report_Final_v2_NEW_Latest.pdf」みたいな名前のファイルに目を通しても、肝心の情報には到底辿りつけない時だってあった気がするし……うん、ときどき絶望的な感じになるよ。だって、ごく普通の検索ツールは結局キーワードとの一致しか見ていなくて、その中身自体を解釈してくれるわけじゃないから。そういうことなんだろうな、たぶん。

プロジェクトディレクトリを整理してAIドキュメント管理を始める

うーん、科学論文からインサイトを抜き出すとか、あとは堅い契約書の要約だとか、製品仕様比べたりするような作業って…正直、昔ながらの手法じゃ全然足りないよなと思ったんです。なんか、壁にぶつかってた感じがあるというか。そこで悩みに悩んで(しかも一晩寝付けず)、私はついに数千件ものPDFドキュメントから情報を読んで理解してくれて、さらに質問にも答えてくれる**マルチモーダルRAG AI**を自力で組もうと腹を括りました。ま、その決断でまた疲れが増したけど…。

そのスケールアップ――10件とかの試作から、一気に10,000件扱える仕組み――これが成立するには、とにかくディレクトリ構成しっかりしなくちゃ話にならなくて。参考までに現時点のディレクトリは以下です。

ai-doc-assistant/
├── ingest/
│ ├── extract_text.py
│ ├── extract_images.py
├── process/
│ ├── chunk_text.py
│ ├── embed_chunks.py
├── index/
│ └── vector_store.py
├── backend/
│ ├── qa_chain.py
│ └── server.py
├── interface/
│ └── ui.py

各モジュールはひとつずつ役割が明確で、「Ingest」(つまり取り込み)→「Process」(処理)→「Index」(索引化)→「Answer」(応答)→「Display」(表示)、そういう流れになっています。本当にシンプルだけど意外と泥臭い作業。

それとテキスト抽出についてはPyMuPDFを選びましたね…。ページ単位できっちりテキストやメタデータ残せるので、わりと細かな情報もうまく拾えます。こういう地道さ、大事。でも、正直ここが思ったより骨だった…長かったなぁ…。

プロジェクトディレクトリを整理してAIドキュメント管理を始める

PyMuPDFでPDFからテキスト情報を抽出してみよう

本段では、fitzというライブラリを使いPDFファイルからテキストや画像を抜き出して、その上で大規模言語モデル向けのチャンク分割まで至る具体的な手順が示されている。眠たいときに限ってこういう作業、意外と面倒なんだよね。まず最初に取り組むのはPythonコードによる処理で、PDFから個々のファイル名やページ番号、それからテキストも抽出できる方法について説明がある。その実装では各ページごとの情報管理とかデータの必要な部分だけ選んで扱うこともできるらしくて、まあ柔軟性がわりと高めかもしれない。

次に、第3ステップとなるとPDF内に埋め込まれている画像(たとえばグラフとか図表)にも焦点が移り、fitz.openを活用したスクリプト例も紹介されていた。ここでは各ページで検出された全イメージを、「ページ番号_インデックス.png」という命名規則に従って所定フォルダへ保存する仕様みたいだった。それでいて抽出された図版は一旦CLIP経由で加工・変換され、その後テキスト情報ともひとまとめになって記録された、ときちんと言及されている。「…こんな地味さ満載な工程、多分誰も振り返らないけど必要だし。」

さらに第4ステップだけど、本全体をそのままLLMに投げ込むなんて到底無理筋なので、「重複区間付きチャンク化アルゴリズム」(chunk_text)が対応策として採用されていたみたい。サンプルコードにはC++式記法が見られて、一つあたり約500文字サイズ&オーバーラップ部分100文字という条件になっていた。「文脈保ったままバラせ」とでも言わんばかり。この設計によってチャンク間でも話の流れがぶち切れないよう意識されているっぽいな、と変な感心すら湧いた。

PDF図表・画像データもAI検索対象にする手順

一貫した返答をもらうってさ、実際にはそんな簡単でもなくて。うーん……細かい手順がちゃんと大事なんだと思う。

## ステップ5:テキストチャンクの埋め込み生成
このフェーズでは、「sentence-transformers」というツールを用いて各チャンクをベクトルへ変換する感じだった気がする。ああ、コード書く余力ないな、とかちょっと思いつつ——

python
from sentence_transformers import SentenceTransformer
python
model = SentenceTransformer('all-MiniLM-L6-v2')
def embed_chunks(chunks):
return model.encode(chunks)


こうして得られるベクトルって、表面上のキーワード抽出じゃなくて**意味そのもの**を射抜く構造なんだよね。だから別の言い方に置き換えたとしても、後で一番親和性の高いコンセプトに辿り着けたりする。不安になったりもするけど。

## ステップ6:FAISSによる高速なベクトル検索エンジンの構築
埋め込み結果を無駄なく保存できて、そのまますぐ取り出せるようFAISS(読み: ファイス)が投入されたわけで。

python
import faiss
import numpy as np
python
def build_index(embeddings):
dim = embeddings.shape
index = faiss.IndexFlatL2(dim)
index.add(embeddings)
return index


こんな具合にインデックス組み立てることで、10,000件超えるドキュメントもほとんど一瞬……いや、本当に1秒も待たずにサッと検索できちゃった。不思議な時代だよなぁ、ほんと。

PDF図表・画像データもAI検索対象にする手順

テキスト分割とコンテクスト重視でRAG向きデータ準備

アシスタントをマルチモーダル対応にする目的で、ちょっとCLIPを導入して図のインデックス作成も試みたんだけど…うまく動いてくれたかは微妙。でもまあ、この手法だと何となく「system rebootのプロセス図を見せて」みたいなリクエストにも、答えるための準備ができるっていう感じになるかな。ああ、正直ずっと考えていたほど簡単ではないよ。画像処理なんて、本当ややこしいし。

<code>python
from transformers import CLIPProcessor, CLIPModel
from PIL import Image
</code>


python
clip_model = CLIPModel.from_pretrained("openai/clip-vit-base-patch32")
processor = CLIPProcessor.from_pretrained("openai/clip-vit-base-patch32")

<pre><code class="language-python">def embed_image(image_path):
image = Image.open(image_path)
inputs = processor(images=image, return_tensors="pt")
with torch.no_grad():
image_features = clip_model.get_image_features(**inputs)
return image_features.squeeze().numpy()
</code>


ほんと疲れるけど仕方ないね…。LangChainでトップドキュメント取得してLLMへの連携部分もゴニョゴニョ実装したことがあったっけ。ちょうどこの手順が地味に厄介だった覚えある。

<code class="language-python">
from langchain.chains import RetrievalQA
from langchain.vectorstores import FAISS
from langchain.llms import Ollama


retriever = FAISS.load_local("index", embeddings=model)
llm = Ollama(model="mistral")
qa = RetrievalQA.from_chain_type(llm=llm, retriever=retriever)


ちなみに、この流れなら「Product Xの保証範囲は?」とか、パッと答えられるし出典までちゃんと付与できるって仕組みになったんだよね。いやほんと助かったというべきか…。

<pre><code class="language-html"><code class="language-bash">qa.run("What's the warranty coverage of Product X?")</code>

sentence-transformersによる意味ベースの埋め込み生成法

信頼性を少しでも高めようと、僕は全回答に参考文献をつける方針にしたんだ。まったく…弁護士じゃあるまいしね。それでもまあ、その仕組みのおかげで発言には必ず根拠が示せる状態になっている。 どの主張も辿れば裏づけがある形だ。

【ステップ10: 統合用ローカルAPIの作成】
自分なりに試行錯誤して、一連のパイプライン全体をFastAPIで覆った感じだった。

from fastapi import FastAPI
from pydantic import BaseModel


class Query(BaseModel):
question: str
app = FastAPI()
@app.post("/ask")
def ask(query: Query):
response = qa.run(query.question)
return {"answer": response}


こうやっておけば、バックエンドはウェブアプリやSlack──ついでに音声システムとも気軽につなげられる余地が出てきたよ。ふう…。

【ステップ11: GradioによるWeb UIの構築】
さらに(ちょっとため息)、クライアントやチームメンバーへの見せ場づくりとして、Gradioで素直なUIを作った。

import gradio as gr


def answer_question(q):
return qa.run(q)
gr.Interface(fn=answer_question, inputs="text", outputs="text", title="AI PDF Assistant").launch()


みんな会社マニュアルを題材に好き勝手質問して、このボットがそれなりの答え返す様子には結構感心された。ああ、本当に動いたんだな…みたいな感覚かな。

【ステップ12: ファイルアップロードとリアルタイム取込対応】
ユーザー側では新しいPDFファイルをUI上へポイッと放り込むだけ――それで自動的に処理・インデックス化まで済むようになっちゃった。不思議なくらい簡単そうなのに、中身は妙に煩雑なんだよね…。

sentence-transformersによる意味ベースの埋め込み生成法

FAISS活用で1万件超ドキュメントも高速全文検索可能にする方法

FastAPIで「/upload」エンドポイントを設ける時、まあ、この手のPDFファイルアップロード処理ってだいたい似たような流れになるんだけど…ふと他に応用できそうな構造だなとか思いつつ、また元の話に戻すけど。次のコードみたいに書いてある。

python
@app.post("/upload")
async def upload_pdf(file: UploadFile):
save_path = f"./docs/{file.filename}"
with open(save_path, "wb") as f:
f.write(await file.read())
# Extract, chunk, embed, and update FAISS index


うーん……この作り方によって、要はアシスタント自身が勝手に知識を増やせるようになった。言い換えれば、新しいドキュメントを投げ入れるたび、その内容ごと「頭良くなる」みたいな状態になるんだけど、それちょっと便利すぎて少し怖さもあるというか。でも実際そんな感じ。

## ステップ13:Ollama+Dockerでローカル展開(コーヒー休憩が欲しい)

完全な制御――つまり自分以外信じないぜ的DIY精神でね、全部まとめてコンテナ化したわけだ。もうこれで仮想マシン飛ばそうが何しようが復元楽勝っぽい。以下がそのDockerfile。

FROM python:3.10
WORKDIR /app
COPY . .
RUN pip install -r requirements.txt
CMD ["uvicorn", "backend.server:app", "--host", "0.0.0.0", "--port", "8000"]

あ、突然どうでもいいこと思い出した…。いや、この設計ならクラウド一切使わずにノートPC単体でも動かせるじゃない?いつでも物理的コントロール感。こういうのって妙に安心する気持ちが湧いてくる瞬間ない?

## ステップ14:SQLiteで与える長期メモリ――ちょっと記憶っぽさ

python
# アシスタントに”記憶力”という名の後付機能を盛り込むため、SQLite採用。
import sqlite3
python
def save_interaction(question, answer):
conn = sqlite3.connect("history.db")
conn.execute("INSERT INTO log (q, a) VALUES (?, ?)", (question, answer))
conn.commit()


こうやって愚直に質問と回答を書き溜めておくことで、一応履歴として参照できるし…んー途中「あれ、自分何してるんだっけ?」みたいになるけど、それはともかく。後からモデル再学習するときや応答内容の改善にもこの蓄積された対話データを活用可能なんですよね。

## ステップ15:最終考察 ― 検索型から推論型への変容

ここまで実装進めていると、「単なる検索窓口」じゃ終わらないものになってきた感覚ばかり強まった。不思議と昔書いた脚本思い出したりして……いやしかし本題は、表面的にはファイル管理でも裏では地味だけど確かな知見更新プロセスが走っているという事実。それこそ多分、小規模な現場とか独学勢にはありがたい存在になる、と漠然と思ったのでした。ま、いいか。

CLIPモデルで画像内容検索とテキスト回答を連携させるには

この段落、なんか…ふう、説明してるのはドキュメントとかチャート、それから音声にも「推論(reasoning)」できるAIってやつなんだよね。例えば、製品仕様をあれこれ比べてくれたり、マニュアルの中のあるセクションだけサッと要約してくれたり――まあ、パラグラフから関連しそうなビジュアルをさがしたりもできちゃうらしい。ほかにも、その時々でユーザーごとにちょっと違った答え方でサポート対応できる仕組みが入ってるんだってさ。ただの機械的なボットというより、「AIのチームメイト」って位置づけにわざと設計されてるのがポイント…という話。でも、この言い方どこまで本気なんだろ?いや、ごめん脱線しちゃった。(ま、いいか。)それでね―話を戻すと、とにかく一人ひとりに寄り添おうとしている雰囲気が感じられる。

> _**「自分専用でぱっぱっと使えるプロンプト集欲しい人、ここからどうぞ」**_
> _**もっと内容見たいなら、Ghost経由で私の配信購読も歓迎です...**_
> _**もし斬新な自動化ネタ思いついたら、コメント欄までぜひ知らせてほしい。**_

CLIPモデルで画像内容検索とテキスト回答を連携させるには

LangChain × LLM組み合わせで人間らしいQ&A体験を実現する手順

一番いいやつをスクリプトにぶち込んで、壊れる箇所がどこか逐一報告します。あ、ちなみに…こんな雰囲気のコンテンツ、もっと見たかったらAi Pandaのフォローもどうぞ~。それと、私が個人的に推している記事へのリンクもちょい下に載せてますので、お暇な時にでも覗いてやってください。なんか…🥰
**注:もしこの記事がまあ少しでも役立ったな~とか感じたら、この投稿に50回ぐらい👏押してくれたら嬉しいです。それから、よければコメント欄💬でひと言も残してくださいね。**

## 創業者より
**はじめまして。Sunilです。** ここまで読んでくれて本当にありがとう。そして、このコミュニティの仲間として居続けてくれることにも心から感謝しています。不器用ながら僕たちのチームは現在20万人超のサポーターへ向けてこういう配信活動を全部ボランティアでやってます(あ~正直体力持つかな…)。 **Mediumからは1円たりとも受け取っていません。**

API連携や自己学習で成長し続けるローカルAI PDFアシスタントの仕組み

もし何かしら気になることがあれば、えっと……良ければLinkedInとかTikTok、それからInstagramでもフォローしてくれると、なんだか嬉しいです。ま、いいか。それと――帰る前に、執筆者への「拍手」とか「フォロー」もまあ、お時間があればお願いしますね。ほんとに気まぐれで申し訳ないですが、ちょっとしたことでも応援してもらえると心の中で静かに感謝しています。

Related to this topic:

Comments