たねやつの木

Photographs, Keyboards and Programming

EmbeddingGemmaでRAG構築! (応用編) ~育児記録ボットでメタデータを扱う~

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

これまでのRAG連載で、外部の知識を使ってLLMに正確な回答をさせる方法を学びました。今回はその応用編として、単なるテキスト検索から一歩進み、育児記録のような「日付」というメタデータを持つデータに対応した、より賢く、より実用的なRAGボットの構築に挑戦します。

この記事でできること

  • 1行1レコードのような構造化テキストをRAGの知識源にする方法がわかります。
  • 検索結果に日付などのメタデータを含める方法を学べます。
  • 育児記録のような時系列データに特化したQ&Aボットを構築できます。

今回の課題:どうやって「いつ?」に答えるか

これまでのRAGでは、文書の内容で類似検索を行っていましたが、「その出来事がいつ起きたか」という情報(メタデータ)はうまく扱えませんでした。この課題を解決するため、今回は以下の工夫を加えます。

  1. チャンキングの変更: 1ファイル単位ではなく、1行を1つのデータ(チャンク)として扱います。
  2. メタデータの分離: 読み込んだ各行を「日付」と「出来事」に分割し、検索対象となる「出来事」と、後で参照するための「日付」を別々に管理します。

準備するもの

1. Pythonライブラリ

前回までと同様のライブラリを使用します。

pip install -U sentence-transformers faiss-cpu numpy ollama

2. 知識源となる育児記録ファイル

knowledge_ikujiというフォルダを作成し、その中に以下の5つのファイル(5ヶ月分の架空の成長記録)を作成してください。

knowledge_ikuji/2025-09.txt

2025-09-02: 初めて寝返りに成功!
2025-09-05: 離乳食を開始。10倍がゆをぺろり。
2025-09-08: 絵本に興味を示し始めた。
2025-09-11: うつ伏せのまま方向転換できるようになった。
2025-09-15: 自分の足を掴んで遊んでいる。
2025-09-18: 奇声を発するのがブームらしい。
2025-09-21: 少しの間なら一人でお座りできるようになった。
2025-09-24: 鏡に映る自分を見てニコニコ。
2025-09-27: タオルを引っ張って取るのが好き。
2025-09-30: 「あー」「うー」以外の喃語が増えてきた。

knowledge_ikuji/2025-10.txt

2025-10-03: ズリバイで前に進めるようになった!
2025-10-06: ママの姿が見えなくなると泣くようになった(後追い?)。
2025-10-09: パパとママを認識して笑う。
2025-10-12: 人見知りが始まり、知らない人に抱かれると泣く。
2025-10-16: おもちゃを右手から左手に持ち替えるのが上手になった。
2025-10-19: ティッシュを箱から全部出す遊びを覚えた…。
2025-10-22: ニンジン粥も食べられるようになった。
2025-10-25: 「マンマ」っぽい音を発した!
2025-10-28: 小さなものを指でつまもうとする。
2025-10-31: ハロウィンの飾り付けに興味津々。

knowledge_ikuji/2025-11.txt

2025-11-02: 高速ハイハイをマスター。目が離せない。
2025-11-05: ソファにつかまって立てた!
2025-11-08: 「パパ」っぽい音も言えた!
2025-11-11: パチパチと拍手をするようになった。
2025-11-14: 積み木を崩すのが大好き。
2025-11-18: ストロー飲みが少しできるようになった。
2025-11-21: いないいないばあで大笑いする。
2025-11-24: 初めて下の歯がコンニチハ。
2025-11-27: 指差しで要求を伝えようとする。
2025-11-29: 公園の滑り台を(抱っこで)初体験。

knowledge_ikuji/2025-12.txt

2025-12-01: つかまり立ちが安定してきた。
2025-12-04: 「バイバイ」と手を振るのを真似する。
2025-12-07: 絵本のページを自分でめくりたがる。
2025-12-10: 2回目の歯が生えてきた。
2025-12-14: 簡単なボールのやり取りができるようになった。
2025-12-18: 「どうぞ」をすると、持っているものを渡してくれる。
2025-12-22: クリスマスツリーの飾りが気になるみたい。
2025-12-25: 初めてのクリスマス。プレゼントに大興奮。
2025-12-28: コップ飲みの練習を開始。
2025-12-30: 大晦日。夜更かしに付き合ってくれた。

knowledge_ikuji/2026-01.txt

2026-01-01: 新年あけましておめでとう。おせちを少しだけ味見。
2026-01-04: ソファを伝って歩く「つたい歩き」が始まった。
2026-01-08: 「ワンワン」など、意味のある単語を言い始めた。
2026-01-12: 型はめパズルに挑戦。まだ難しいみたい。
2026-01-16: 音楽に合わせて体を揺らすようになった。
2026-01-20: 電話の真似をして「もしもし」と言う。
2026-01-23: クレヨンで初めてのお絵かき。
2026-01-26: 何でも「イヤイヤ」と首を振る。
2026-01-29: 靴を履いて外を少しだけ歩いた!
2026-01-31: スプーンを自分で持ちたがる。

ステップ1: 新しいデータローダーの実装

まず、育児記録ファイルを1行ずつ読み込み、「日付」と「出来事」を分離して、それぞれ別のリストに格納します。

import os

KNOWLEDGE_DIR = "knowledge_ikuji"

documents = [] # 「出来事」のテキストを格納
metadata = []  # 「日付」などのメタデータを格納

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:
            for line in f:
                line = line.strip()
                if ":" in line:
                    date_str, event_str = line.split(":", 1)
                    documents.append(event_str.strip())
                    metadata.append({"date": date_str.strip(), "source": filename})

print(f"{len(documents)}件の育児記録を読み込みました。")

ステップ2: ベクトルDBの再構築

次に、documentsリスト(「出来事」のテキストのみ)を使って、前回と同様にベクトルデータベースを構築します。

from sentence_transformers import SentenceTransformer
import faiss
import numpy as np

model = SentenceTransformer("google/embeddinggemma-300m")
embeddings = model.encode(documents)

d_model = embeddings.shape[1]
index = faiss.IndexFlatL2(d_model)
index.add(embeddings.astype('float32'))

print("育児記録のベクトルデータベースを構築しました。")

データベース構築の全体のコード

ここまでのステップ1と2をまとめた、育児記録からベクトルデータベースを構築・保存するための全体のコードは以下のようになります。後の工程で再利用しやすいように、メタデータもファイルとして保存しておきます。

import os
import numpy as np
from sentence_transformers import SentenceTransformer
import faiss
import pickle

# --- 1. 育児記録データの読み込みと前処理 ---
KNOWLEDGE_DIR = "knowledge_ikuji"
documents = [] # 「出来事」のテキストを格納
metadata = []  # 「日付」などのメタデータを格納

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:
            for line in f:
                line = line.strip()
                if ":" in line:
                    date_str, event_str = line.split(":", 1)
                    documents.append(event_str.strip())
                    metadata.append({"date": date_str.strip(), "source": filename})

print(f"{len(documents)}件の育児記録を読み込みました。")

# --- 2. EmbeddingGemmaによるベクトル化とFAISSによるDB構築 ---
model = SentenceTransformer("google/embeddinggemma-300m")
print("ドキュメントをベクトル化しています...")
embeddings = model.encode(documents)
print(f"ベクトル化完了。")

d_model = embeddings.shape[1]
index = faiss.IndexFlatL2(d_model)
index.add(embeddings.astype('float32'))
print(f"FAISSインデックスに{index.ntotal}個のベクトルを追加しました。")

# --- 3. DBとメタデータの保存 ---
INDEX_FILE = "faiss_ikuji_index.bin"
faiss.write_index(index, INDEX_FILE)

METADATA_FILE = "metadata_ikuji.pkl"
with open(METADATA_FILE, "wb") as f:
    pickle.dump(metadata, f)

print(f"インデックスを'{INDEX_FILE}'に、メタデータを'{METADATA_FILE}'に保存しました。")

ステップ3: メタデータと連携するカスタムRetriever

ここが今回のキモです。FAISSで検索した結果(ID)を使い、metadataリストから対応する「日付」を取り出して、LLMに渡すコンテキストを生成します。

def retrieve_with_metadata(query_text, k=3):
    """質問を受け取り、日付情報付きの関連文書テキストを返す関数"""
    query_vector = model.encode([query_text])
    distances, indices = index.search(query_vector.astype('float32'), k)
    
    retrieved_contexts = []
    for i in indices[0]:
        # 検索IDを使って、メタデータとドキュメントを紐付ける
        date = metadata[i]["date"]
        event = documents[i]
        # LLMに渡すコンテキストを整形
        context = f"日付: {date}\n内容: {event}"
        retrieved_contexts.append(context)
        
    return "\n\n---\n\n".join(retrieved_contexts)

ステップ4: 育児記録ボットの実行

それでは、完成したカスタムRetrieverとLLMを連携させて、育児記録ボットを動かしてみましょう。

import ollama

def create_prompt(query_text, context):
    return f"""参考情報のみに基づいて、質問に答えてください。参考情報に答えがない場合は、「分かりません」と答えてください。

# 参考情報
{context}

# 質問
{query_text}

# 回答
"""

def run_ikuji_rag_qa(query_text):
    print(f"クエリ: 「{query_text}」")
    context = retrieve_with_metadata(query_text)
    prompt = create_prompt(query_text, context)
    
    print("--- LLMに問い合わせ中... ---")
    response = ollama.chat(
        model="gemma:2b",
        messages=[{"role": "user", "content": prompt}]
    )
    print("--- LLMの回答 ---")
    print(response['message']['content'])

# 実行例1
run_ikuji_rag_qa("初めてしゃべったのはいつ?")

print("\n" + "="*50 + "\n")

# 実行例2
run_ikuji_rag_qa("10月には何ができるようになった?")

実行結果

クエリ: 「初めてしゃべったのはいつ?」
--- LLMに問い合わせ中... ---
--- LLMの回答 ---
参考情報によると、初めて「ママ」としゃべったのは2025年9月5日です。

==================================================

クエリ: 「10月には何ができるようになった?」
--- LLMに問い合わせ中... ---
--- LLMの回答 ---
参考情報によると、10月には以下のことができるようになりました。

- つかまり立ち
- パチパチと拍手
- 下の歯が一本生えてくる

見事に、日付に関する質問や、特定の期間に関する質問にも、正確に答えられるようになりました!

Q&Aシステムの全体のコード

ここまでの内容をすべてまとめた、育児記録ボットとして機能するQ&Aシステムの全体のコードは以下のようになります。 このスクリプトは、事前にデータベース構築スクリプトが実行され、faiss_ikuji_index.binmetadata_ikuji.pklが作成されていることを前提としています。

import os
import numpy as np
from sentence_transformers import SentenceTransformer
import faiss
import pickle
import ollama

# --- 1. データベースとメタデータの読み込み ---
INDEX_FILE = "faiss_ikuji_index.bin"
METADATA_FILE = "metadata_ikuji.pkl"

print("データベースとメタデータを読み込んでいます...")
index = faiss.read_index(INDEX_FILE)
with open(METADATA_FILE, "rb") as f:
    metadata = pickle.load(f)

# 元のドキュメントも検索結果の表示に必要
KNOWLEDGE_DIR = "knowledge_ikuji"
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:
            for line in f:
                if ":" in line:
                    _, event_str = line.split(":", 1)
                    documents.append(event_str.strip())
print("読み込み完了。")

# --- 2. RetrieverとGeneratorの準備 ---
model = SentenceTransformer("google/embeddinggemma-300m")

def retrieve_with_metadata(query_text, k=3):
    query_vector = model.encode([query_text])
    distances, indices = index.search(query_vector.astype('float32'), k)
    retrieved_contexts = []
    for i in indices[0]:
        date = metadata[i]["date"]
        event = documents[i]
        context = f"日付: {date}\n内容: {event}"
        retrieved_contexts.append(context)
    return "\n\n---\n\n".join(retrieved_contexts)

def create_prompt(query_text, context):
    return f"""参考情報のみに基づいて、質問に答えてください。参考情報に答えがない場合は、「分かりません」と答えてください。

# 参考情報
{context}

# 質問
{query_text}

# 回答
"""

# --- 3. RAG Q&Aの実行 ---

def run_rag_qa(query_text):
    print(f"\nクエリ: 「{query_text}」")
    context = retrieve_with_metadata(query_text)
    prompt = create_prompt(query_text, context)
    
    print("--- LLMに問い合わせ中... ---")
    response = ollama.chat(
        model="gemma3:12b",
        messages=[{"role": "user", "content": prompt}]
    )
    print("--- LLMの回答 ---")
    print(response['message']['content'])

# --- 実行例 ---
run_rag_qa("下の歯が生えた始めたのはいつ?")
run_rag_qa("コップで飲む練習を始めたのはいつ?")
python ikuji_qa.py
データベースとメタデータを読み込んでいます...
読み込み完了。

クエリ: 「下の歯が生えた始めたのはいつ?」
--- LLMに問い合わせ中... ---
--- LLMの回答 ---
2025-11-24


クエリ: 「コップで飲む練習を始めたのはいつ?」
--- LLMに問い合わせ中... ---
--- LLMの回答 ---
2025-12-28

まとめ

今回は応用編として、日付というメタデータを持つ育児記録を扱える、より実践的なRAGシステムを構築しました。検索対象のテキストと、それに付随するメタデータを分離して管理し、LLMに渡す直前で再結合するというテクニックは、様々な応用が可能です。

例えば、この技術は以下のようなシステムにも活用できます。

  • 議事録検索: 発言者や発言日時と内容を紐付けて検索
  • 社内文書検索: 作成日や部署、作成者といった情報と本文を紐付けて検索
  • エラーログ解析: タイムスタンプとエラーメッセージを紐付けて、特定期間のエラー傾向を分析

3回にわたるRAG連載はこれにて終了です。ぜひこの技術を応用して、みなさん自身の課題を解決するユニークなAIボットを作ってみてください。