こんにちは、たねやつです。
前回は、RAGの基礎となるベクトルデータベースをEmbeddingGemmaとFAISSを使って構築しました。
【RAG入門編】EmbeddingGemmaとFAISSで始めるベクトルデータベース構築
今回はそのデータベースを使い、ユーザーからの質問に最も関連性の高い文書を検索してくる「Retriever(レトリーバー)」を実装します。RAGの「R」、すなわちRetrieval(検索)を担う重要な部分です。
- この記事でできること
- 準備するもの
- ステップ1: 知識源とFAISSインデックスの読み込み
- ステップ2: 質問(クエリ)のベクトル化
- ステップ3: FAISSによる類似文書検索
- ステップ4: 検索結果の表示
- 検索結果の再考察:なぜRAGが2位になったのか?
- 全体のコード
- まとめ
この記事でできること
- 保存したFAISSインデックスを読み込んで再利用する方法がわかります。
- ユーザーからの質問文をベクトルに変換し、類似文書を検索できるようになります。
- 質問応答システムの「検索」部分を担うRetrieverを実装できます。
準備するもの
- 前回の成果物:
faiss_index.binと、知識源のテキストファイルが入ったknowledgeフォルダ。 - Pythonライブラリ:
sentence-transformers,faiss-cpu,numpy。
ステップ1: 知識源とFAISSインデックスの読み込み
まず、前回保存したFAISSインデックスと、検索結果のIDと本文を対応させるための元のドキュメントを読み込みます。
import os import numpy as np from sentence_transformers import SentenceTransformer import faiss # --- 前回のおさらい: ドキュメントの読み込み --- KNOWLEDGE_DIR = "knowledge" documents = [] print(f"'{KNOWLEDGE_DIR}'からドキュメントを読み込んでいます...") for filename in os.listdir(KNOWLEDGE_DIR): if filename.endswith(".txt"): filepath = os.path.join(KNOWLEDGE_DIR, filename) with open(filepath, "r", encoding="utf-8") as f: documents.append(f.read()) print(f"{len(documents)}個のドキュメントを読み込みました。") # --- FAISSインデックスの読み込み --- INDEX_FILE = "faiss_index.bin" print(f"'{INDEX_FILE}'からインデックスを読み込んでいます...") index = faiss.read_index(INDEX_FILE) print("インデックスの読み込み完了。")
ステップ2: 質問(クエリ)のベクトル化
次に、検索したい質問文(クエリ)を用意し、前回ドキュメントをベクトル化したのと同じEmbeddingGemmaモデルを使ってベクトルに変換します。
# 埋め込みモデルのロード model = SentenceTransformer("google/embeddinggemma-300m") # 検索したい質問(クエリ) query_text = "RAGとは何ですか?" # クエリをベクトル化 print(f"クエリ「{query_text}」をベクトル化しています...") query_vector = model.encode([query_text]) # model.encodeは配列を期待するため[]で囲む print("クエリのベクトル化完了。")
ステップ3: FAISSによる類似文書検索
準備が整ったので、FAISSインデックスのsearchメソッドを使って、質問ベクトルに最も近い文書を検索します。
search(クエリベクトル, k):kは検索したい文書の数を指定します。
searchメソッドは、2つの情報を返します。
distances: クエリと見つかった文書との「距離」。値が小さいほど類似度が高いことを意味します。indices: 見つかった文書のID(インデックス番号)。0から始まる連番です。
# 検索の実行 (上位2件を取得) k = 2 print(f"類似文書を{k}件検索します...") distances, indices = index.search(query_vector.astype('float32'), k) print("検索完了。") print("--- 検索結果 ---") print(f"Indices: {indices}") print(f"Distances: {distances}")
ステップ4: 検索結果の表示
取得したID(indices)を使って、元のドキュメントリストから該当するテキストを抜き出し、結果を確認してみましょう。
# 検索結果を整形して表示 for i, (idx, dist) in enumerate(zip(indices[0], distances[0])): print(f"\n--- 類似度ランキング: {i+1}位 (Distance: {dist:.4f}) ---") print(documents[idx])
これを実行すると、以下のような結果が得られます。
--- 類似度ランキング: 1位 (Distance: 1.0988) --- 埋め込み(Embedding)とは、単語や文章を「ベクトル」と呼ばれる数値の配列に変換する技術です。このベクトルは、単語や文章の意味的な近さを表現しており、意味が近いほどベクトル空間上での距離が近くなります。セマンティック検索やRAGの文書検索など、様々な応用で利用されています。 --- 類似度ランキング: 2位 (Distance: 1.2297) --- RAG(Retrieval-Augmented Generation)は、LLMの回答精度を向上させるための技術です。ユーザーからの質問に対し、まず関連する情報をデータベースから検索(Retrieval)し、その検索結果を基にLLMが回答を生成(Generation)します。これにより、LLMが知らない情報についても、正確な回答を生成できるようになります。
検索結果の再考察:なぜRAGが2位になったのか?
お気づきでしょうか。実は、「RAGとは何ですか?」という質問に対して、RAGそのものを説明した文書は2位に来てしまいました。1位になったのは、RAGの重要な構成要素である「埋め込み(Embedding)」に関する文書です。
これは一見すると検索の失敗に見えますが、EmbeddingGemmaの思考を推測すると、非常に興味深いことがわかります。
モデルは「RAGとは何か?」という問いを、「RAGという技術は何か?」と解釈した上で、
- RAGの文書: 「LLMの回答精度を向上させるための技術です...」
- 埋め込みの文書: 「ベクトルと呼ばれる数値の配列に変換する技術です...」
この2つを比較したと考えられます。その結果、後者の「埋め込み」の文書の方が、「ベクトル」「数値」「セマンティック検索」といった、より具体的で技術的なキーワードを多く含んでいたため、モデルは「より技術的な説明をしているこちらの方が、質問の意図に近いだろう」と判断したのかもしれません。
このように、キーワードが完全一致しなくても、モデルが解釈した「意味」に基づいて検索結果が変わるのが、ベクトル検索の面白さであり、難しさでもあります。
どうすれば精度を上げられるか?
このような場合に検索精度を向上させるには、いくつかの方法が考えられます。
- クエリ(質問)をより具体的にする: 「RAGという技術の仕組みと利点を教えて」のように、質問に多くの情報を含めることで、モデルの解釈を誘導します。
- チャンキングの工夫: 今回はファイル全体を1つの文書として扱いましたが、より短い段落に分割(チャンキング)することで、情報の純度が高まり、検索精度が向上することがあります。
- ハイブリッド検索: ベクトル検索と、従来のキーワード検索を組み合わせることで、お互いの長所を活かし、短所を補うアプローチも有効です。
今回はこのまま進めますが、RAGの精度を追求していく上では、これら「Retrieverの改善」が非常に重要なテーマとなります。
全体のコード
ここまでのステップをまとめた、Retrieverの全体のコードは以下のようになります。
import os import numpy as np from sentence_transformers import SentenceTransformer import faiss # --- 1. 知識源とFAISSインデックスの読み込み --- KNOWLEDGE_DIR = "knowledge" documents = [] for filename in os.listdir(KNOWLEDGE_DIR): if filename.endswith(".txt"): filepath = os.path.join(KNOWLEDGE_DIR, filename) with open(filepath, "r", encoding="utf-8") as f: documents.append(f.read()) INDEX_FILE = "faiss_index.bin" index = faiss.read_index(INDEX_FILE) # --- 2. 質問(クエリ)のベクトル化 --- model = SentenceTransformer("google/embeddinggemma-300m") query_text = "RAGとは何ですか?" query_vector = model.encode([query_text]) # --- 3. FAISSによる類似文書検索 --- k = 2 distances, indices = index.search(query_vector.astype('float32'), k) # --- 4. 検索結果の表示 --- print(f"クエリ: 「{query_text}」") print("--- 検索結果 ---") for i, (idx, dist) in enumerate(zip(indices[0], distances[0])): print(f"\n--- 類似度ランキング: {i+1}位 (Distance: {dist:.4f}) ---") print(documents[idx])
まとめ
今回は、FAISSインデックスを読み込み、ユーザーの質問に類似した文書を検索するRetrieverを実装しました。これで、RAGシステムの「Retrieval」部分が完成しました。
次回の【完成編】では、今回作成したRetrieverと、Ollamaで動かすLLMを連携させ、ついにQ&Aシステムを完成させます。