こんにちは、たねやつです。
これまでのRAG連載で、外部の知識を使ってLLMに正確な回答をさせる方法を学びました。今回はその応用編として、単なるテキスト検索から一歩進み、育児記録のような「日付」というメタデータを持つデータに対応した、より賢く、より実用的なRAGボットの構築に挑戦します。
- この記事でできること
- 今回の課題:どうやって「いつ?」に答えるか
- 準備するもの
- ステップ1: 新しいデータローダーの実装
- ステップ2: ベクトルDBの再構築
- データベース構築の全体のコード
- ステップ3: メタデータと連携するカスタムRetriever
- ステップ4: 育児記録ボットの実行
- Q&Aシステムの全体のコード
- まとめ
この記事でできること
- 1行1レコードのような構造化テキストをRAGの知識源にする方法がわかります。
- 検索結果に日付などのメタデータを含める方法を学べます。
- 育児記録のような時系列データに特化したQ&Aボットを構築できます。
今回の課題:どうやって「いつ?」に答えるか
これまでのRAGでは、文書の内容で類似検索を行っていましたが、「その出来事がいつ起きたか」という情報(メタデータ)はうまく扱えませんでした。この課題を解決するため、今回は以下の工夫を加えます。
- チャンキングの変更: 1ファイル単位ではなく、1行を1つのデータ(チャンク)として扱います。
- メタデータの分離: 読み込んだ各行を「日付」と「出来事」に分割し、検索対象となる「出来事」と、後で参照するための「日付」を別々に管理します。
準備するもの
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.binとmetadata_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ボットを作ってみてください。