【ITニュース解説】Building a chatbot with Python (Backend)
2025年09月16日に「Dev.to」が公開したITニュース「Building a chatbot with Python (Backend)」について初心者にもわかりやすく解説しています。
ITニュース概要
PythonでRAGチャットボットのバックエンド構築手順を解説。PDFからテキストを抽出し、分割。それをベクトル化し、FAISSで検索可能なインデックスを作成する前処理パイプラインだ。
ITニュース解説
この解説は、Pythonというプログラミング言語を使ってチャットボットの裏側(バックエンド)を作る方法について、システムエンジニアを目指す初心者の皆さんにも分かりやすく説明する。具体的には、チャットボットが質問に答えるために必要な「知識」をPDFファイルから効率的に準備するプロセスに焦点を当てる。これは「オフライン前処理パイプライン」と呼ばれるもので、チャットボットが実際に稼働する前に、あらかじめ情報を整理しておく作業だ。この準備作業は、RAG(Retrieval Augmented Generation)という技術を使ったチャットボットにとって非常に重要となる。RAGチャットボットは、質問が来たときに、まず関連する情報を探し出し(Retrieval)、その情報に基づいて回答を生成する(Generation)仕組みだからだ。
このスクリプトの主な役割は六つある。一つ目は、特定のフォルダーに保存されているPDFファイルを読み込むこと。二つ目は、読み込んだPDFファイルから文字情報(テキスト)を取り出すことだ。三つ目は、取り出した長いテキストを、後で扱いやすいように小さなまとまり(チャンク)に分割すること。このとき、隣接するチャンク同士が少し重なるようにすることで、情報が途切れてしまうのを防ぐ工夫がされる。四つ目は、これらのテキストチャンクを、コンピュータが理解しやすい数値の表現(ベクトル埋め込み)に変換すること。五つ目は、これらのベクトル埋め込みを使って、FAISS(ファイエス)と呼ばれる高速な類似性検索のためのインデックス(索引)を構築し、ファイルとして保存すること。そして六つ目は、ベクトル化する前の元のテキストチャンクも、後で利用できるようにファイルに保存しておくことだ。
まず、スクリプトが使う主なツール(ライブラリ)とその準備について説明する。osは、フォルダー内のファイルを一覧表示したり、ファイルのパスを組み立てたりといった、ファイルシステムに関する操作を行うために使われる。pickleは、Pythonのオブジェクト(データ構造)をそのままファイルに保存したり、ファイルから読み込んだりするためのものだ。ここではテキストチャンクを保存するのに使われる。numpyは、数値の配列(ベクトルの集まりなど)を効率的に扱うためのライブラリで、計算処理を高速に行うことができる。PyPDF2は、PDFファイルからテキストを抽出するための専用ツールである。SentenceTransformerは、テキストをベクトル埋め込みに変換するモデルを提供するライブラリだ。これにより、テキストの意味的な内容を数値で表現できるようになる。faissは、数百万、数十億といった大量のベクトルデータの中から、特定のベクトルに「似ている」ものを非常に高速に探し出すためのライブラリである。
次に、スクリプト内で共通して使われる設定(定数)を見てみよう。embedderという変数は、SentenceTransformer("all-MiniLM-L6-v2")という特定のモデルを読み込んだインスタンスを指す。このモデルは、テキストをベクトルに変換する「脳みそ」のようなものだ。初めて実行するときは、このモデルのデータ(重み)をインターネットからダウンロードするため、少し時間がかかる場合がある。INDEX_FILEとCHUNKS_FILEは、それぞれFAISSインデックスとテキストチャンクを保存するファイルの名前を定義している。
PDFを読み込むための関数load_pdfは、指定されたPDFファイルのパスを受け取り、PyPDF2を使ってページごとにテキストを抽出し、それらをつなぎ合わせて一つの長い文字列として返す。これにより、PDFの全内容をテキストデータとして扱えるようになる。
そして、長いテキストを分割するchunk_text関数がある。この関数は、与えられたテキストをchunk_size(例えば500文字)ごとに区切って小さなチャンクにする。このとき、次のチャンクの開始位置を前のチャンクの終了位置よりも少し手前(overlap、例えば100文字)にすることで、チャンク同士に重複を持たせる。例えば、最初のチャンクが0〜500文字なら、次のチャンクは400〜900文字といった具合だ。このように重複させることで、ある文脈がチャンクの区切りにまたがっていても、情報が失われることなく次のチャンクで補完される可能性が高まり、後で検索する際に文脈を失うリスクを減らせる。
これらの基本機能を使って、実際の処理パイプラインが動く様子を順を追って見ていこう。
最初のステップは、すべてのPDFファイルからチャンクを集めることだ。pdf_folderという変数で指定されたフォルダー内にあるすべてのPDFファイルを一つずつ開き、load_pdf関数でテキストを抽出し、さらにchunk_text関数でそのテキストをチャンクに分割する。これらのチャンクはすべてall_chunksというリストに集められる。このとき、チャンクがリストに追加された順番が重要になる。なぜなら、後でFAISSインデックスに登録される際にもこの順番が使われ、インデックス内のIDと元のチャンクを対応付けるのに必要だからだ。
二番目のステップは、集めたチャンクをベクトル埋め込みに変換することだ。embedder.encode(all_chunks)という処理で、SentenceTransformerモデルが、すべてのテキストチャンクを数値のベクトル(float32型の数値配列)に変換する。このベクトルは、テキストの意味的な特徴を数学的に表現したものだと考えると良い。例えば、「りんご」と「バナナ」は似た意味を持つので、ベクトル空間上でも近い位置に配置される。このようにテキストを数値化することで、コンピュータがテキストの意味を「理解」し、後で「似ているテキスト」を探し出すことができるようになる。すべてのチャンクを一気にベクトル化しようとすると、メモリを大量に消費して処理が停止してしまう(OOM: Out Of Memory)可能性があるため、batch_size(一度に処理するチャンクの数)を指定して、少しずつ処理するように工夫が凝らされている。また、FAISSが期待するデータ型に合わせるため、np.array(vectors).astype('float32')で明示的にfloat32型に変換している。
三番目のステップは、FAISSインデックスを作成することだ。まず、vectors.shape[1]で、作成されたベクトルの次元数(ベクトルの長さ)を取得する。次に、faiss.IndexFlatL2(dim)という命令で、FAISSのインデックスオブジェクトを生成する。IndexFlatL2は、与えられたすべてのベクトルに対して、L2距離(ユークリッド距離、つまり一般的な意味での「距離」)を使って、最も近いベクトルを正確に(力ずくで)探し出すタイプのインデックスだ。これはシンプルで正確な方法だが、ベクトルの数が非常に多くなると検索に時間がかかるという側面もある。その後、index.add(vectors)という命令で、準備したすべてのベクトルをこのインデックスに追加する。FAISSインデックスにベクトルが追加される順序は、元のall_chunksリストの順序と同じなので、インデックス内のIDと元のテキストチャンクを容易に対応付けられる。
最後のステップは、作成したインデックスと元のチャンクをファイルに保存することだ。faiss.write_index(index, INDEX_FILE)という命令で、構築したFAISSインデックスがfaiss_index.binというファイルに保存される。このファイルには、ベクトル検索に必要な情報が効率的な形式で格納されている。また、with open(CHUNKS_FILE, "wb") as f: pickle.dump(all_chunks, f)というコードで、元のテキストチャンクのリストall_chunksがchunks.pklというファイルに保存される。これらのファイルは、チャットボットが実際に稼働する際に、検索機能の基盤として読み込まれることになる。例えば、ユーザーが質問をしたとき、その質問もベクトルに変換され、FAISSインデックスを使って質問ベクトルに最も似たチャンク(つまり関連情報)がfaiss_index.binから素早く検索され、その検索結果のIDを使ってchunks.pklから対応する元のテキストチャンクが取り出される。そして、そのテキストチャンクに基づいて回答が生成される、という流れだ。
このスクリプトを実行するためには、PDFファイルをdocs/というフォルダーに用意しておき、ターミナルでpython -m backendというコマンドを実行するだけだ。すると、現在のディレクトリにfaiss_index.binとchunks.pklという二つのファイルが生成され、チャットボットのバックエンド前処理が完了する。