たねやつの木

Photographs, Keyboards and Programming

EmbeddingGemmaでRAG構築! (第2回) ~FAISSで作るカスタム検索エンジン~

こんにちは、たねやつです。

前回は、RAGの基礎となるベクトルデータベースをEmbeddingGemmaFAISSを使って構築しました。

【RAG入門編】EmbeddingGemmaとFAISSで始めるベクトルデータベース構築

今回はそのデータベースを使い、ユーザーからの質問に最も関連性の高い文書を検索してくる「Retriever(レトリーバー)」を実装します。RAGの「R」、すなわちRetrieval(検索)を担う重要な部分です。

この記事でできること

  • 保存した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つの情報を返します。

  1. distances: クエリと見つかった文書との「距離」。値が小さいほど類似度が高いことを意味します。
  2. 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という技術は何か?」と解釈した上で、

  1. RAGの文書: 「LLMの回答精度を向上させるための技術です...」
  2. 埋め込みの文書: 「ベクトルと呼ばれる数値の配列に変換する技術です...」

この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システムを完成させます。