先日はWikipediaAPIを使用して、Wikipediaの記事を取得する方法について書いてみました。今回はそのデータを活用して、RAG(Retrieval-Augmented Generation)システムを構築してみようと思います。
参考
uepon.hatenadiary.com
RAGは、大規模言語モデル(LLM)に外部の知識を組み合わせることで、より正確で信頼性の高い回答を生成する技術の1つです。
Wikipediaの記事データを活用すれば、実用的なRAGシステムにもつながると思います。データはChromaDBというベクトルデータベースとして使用し、Google Gemini APIと組み合わせることで、質問に対して関連情報を検索し、的確な回答を生成するシステムを作ります。今回Geminiを使用したのは無料枠があるためです🤗別にOpenAIでも、Claudeでも問題はありません。
RAGシステムとは何か
別のエントリでも度々触れていますので、簡単な説明にとどめますが、RAGシステムは、以下の3つのステップで動作しています。
- 1) 検索(Retrieval) … 質問に関連する情報をデータベースから検索
- 2) 拡張(Augmentation) … 検索した情報をコンテキストとして整理
- 3) 生成(Generation) … LLMが検索情報を参照しながら回答を生成
本来、LLMは学習データの範囲内でしか回答できませんが(※)、RAGを使うことで最新情報や専門知識を活用した回答が可能になります。
※すでに、大手のChat型のLLMサービスでは検索機能が組み込まれています。そのため意図的にその機能をOFFにしない限り情報を検索して回答します。
システム構成と技術スタック
構築するシステムの構成は以下のようになります。
wikipedia-rag/
├── README.md
├── pyproject.toml # uv用プロジェクト設定
├── .env # API設定ファイル
├── rag_system.py # RAGシステム本体
├── data_loader.py # データ読み込み
├── test_rag.py # テスト・デモ
├── data/
│ └── wikipedia/ # Wikipediaのmarkdownファイルが格納されるディレクトリ
└── chroma_db/ # ChromaDBの永続化データ
今回のシステムは3つのPythonプログラムで構成されています。
1. rag_system.py(今回のRAGシステムの中心となるプログラム)
- ChromaDBとの接続・管理
- ベクトル検索による類似情報の取得
- Gemini APIを使った回答生成
- 検索結果からのコンテキスト構築
このプログラムにWikipediaRAGクラスを定義し、他のプログラムから利用できるようにしています。
2. data_loader.py(データ登録ツール)
このプログラムは最初に一度実行し、データベースにWikipedia記事を登録します。
3. test_rag.py(テスト・デモツール)
このプログラムでシステムの動作確認や実際の利用を体験できます。
ChromaDBとは
ChromaDBは、AIアプリケーション向けに設計されたオープンソースのベクトルデータベースです。
www.trychroma.com
主な特徴
- 埋め込みベクトルの自動生成 … テキストを自動的にベクトル化
- 類似度検索 … コサイン類似度などを使った検索
- シンプルなAPI … Pythonから簡単に利用できる
従来のデータベースがキーワードの完全一致で検索するのに対し、ChromaDBは「意味が近い(本来、コサイン類似度が高いといったほうがいいと思います)」コンテンツを見つけることができます。例えば「AI」と検索すれば、「人工知能」や「機械学習」に関する記事も適切に取得できます。
👉️コサイン類似度とは、2つのデータがどちらも同じような特徴を持っているかを測るための指標となります。
たとえば、どちらも同じ話題を多く含んでいれば高く、まったく違う話題なら低くなります。
この度合いを数字の0〜1で表現し、1に近いほど特徴の重なりが大きいと判断することができます。
環境構築
今回は以下の環境を前提としています。
uvのインストール(未インストールの場合のみ)
$ curl -LsSf https://astral.sh/uv/install.sh | sh
👉 uvを使うと依存関係の管理が簡単になるだけでなく、パッケージの導入が爆速になります。
プロジェクトのセットアップ
# 作業ディレクトリの作成と移動
$ mkdir ~/workspace/wikipedia-rag
$ cd ~/workspace/wikipedia-rag
# uvでプロジェクトを初期化、仮想環境の作成と有効化
$ uv init
$ uv venv
$ source .venv/bin/activate
必要なパッケージのインストール
# 必要なパッケージを一括インストール
$ uv pip install chromadb google-generativeai python-dotenv tqdm
RAGを使用したLLMの生成にGeminiを使用しているので、APIキーを取得します。
⚠️ APIキーは機密情報です。Gitにコミットしないよう注意してください。
.gitignoreに.envを追加することを必須にしてください。
環境設定ファイルの作成
プロジェクトのルートディレクトリに.envファイルを作成します。
⚠️ your_api_key_hereを先程取得した実際のAPIキーに置き換えてください。
.env
# .envファイル
GOOGLE_API_KEY=your_api_key_here
GEMINI_MODEL=models/gemini-2.5-pro
# Wikipediaデータ用のディレクトリを作成
$ mkdir -p data/wikipedia
このシステムでは、Wikipedia記事を構造化したmarkdown形式で保存したファイルを使用します。
このデータの生成方法は以下をご参照ください。
リンク
たとえば、WikipediaAPIで取得したデータは以下のような構造になっています。
⚠️ 実際のデータではこの通りにならないこともあります。その場合はRAGの精度が低くなるのでご注意ください。
# 機械学習
**ページID**: 185375
**URL**: https://ja.wikipedia.org/wiki/%E6%A9%9F%E6%A2%B0%E5%AD%A6%E7%BF%92
**言語**: ja
**取得日時**: 2025-11-10T22:43:32.686744
---
## 要約
機械学習(きかいがくしゅう、英: machine learning)とは、経験からの学習により
自動で改善するコンピューターアルゴリズムもしくはその研究領域で、人工知能の
一種であるとみなされている。
---
## カテゴリ
- Category:機械学習
- Category:サイバネティックス
- Category:出典を必要とする節のある記事/2022年7月
---
## セクション構造
- **定義** (レベル 0)
- **理論** (レベル 0)
- **統計的機械学習** (レベル 1)
- **数理最適化** (レベル 1)
---
## 本文
機械学習(きかいがくしゅう、英: machine learning)とは...
(以下、詳細な説明が続く)
---
## リンク情報
**内部リンク総数**: 219
### 主要な内部リンク(最大100件)
- 人工知能
- ディープラーニング
- ニューラルネットワーク
👉 データは、メタデータ、要約、カテゴリ、本文などが格納されています。各セクション間は---で区切られています。
データファイルの配置
準備したmarkdownファイルをdata/wikipedia/ディレクトリに移動しておきます。
# 例: ファイルをコピー(パスは例です)
$ cp ~/downloads/機械学習_complete.md data/wikipedia/
$ cp ~/downloads/自然言語処理_complete.md data/wikipedia/
# または、複数ファイルを一括コピー(パスは例です)
$ cp ~/downloads/*_complete.md data/wikipedia/
RAGシステムの実装
ここからはコード実装になっていきます。
1. RAGシステム本体(rag_system.py)
まず、RAGシステムのコアとなるWikipediaRAGクラスとなります。
import os
import chromadb
from chromadb.config import Settings
import google.generativeai as genai
from dotenv import load_dotenv
load_dotenv()
class WikipediaRAG:
def __init__(self, persist_directory="./chroma_db"):
"""RAGシステムの初期化"""
self.client = chromadb.PersistentClient(
path=persist_directory,
settings=Settings(anonymized_telemetry=False)
)
self.collection = self.client.get_or_create_collection(
name="wikipedia_articles",
metadata={"description": "Wikipedia記事のベクトルデータベース"}
)
genai.configure(api_key=os.getenv("GOOGLE_API_KEY"))
self.model = genai.GenerativeModel(
os.getenv("GEMINI_MODEL", "gemini-1.5-flash")
)
def search_similar_content(self, query, n_results=3):
"""類似コンテンツの検索"""
results = self.collection.query(
query_texts=[query],
n_results=n_results
)
formatted_results = []
if results['documents'][0]:
for i, (doc, metadata) in enumerate(
zip(results['documents'][0], results['metadatas'][0])
):
formatted_results.append({
'content': doc,
'metadata': metadata,
'distance': results['distances'][0][i] if results['distances'] else None
})
return formatted_results
def generate_answer(self, query, n_results=3, temperature=0.7):
"""質問に対する回答を生成"""
search_results = self.search_similar_content(query, n_results)
if not search_results:
return "申し訳ございません。関連する情報が見つかりませんでした。"
context = self._build_context(search_results)
prompt = f"""以下の情報を参考に、質問に答えてください。
参考情報:
{context}
質問: {query}
回答は以下の点に注意してください:
- 参考情報に基づいて正確に回答する
- 情報が不足している場合はその旨を明記する
- 簡潔でわかりやすく説明する
"""
response = self.model.generate_content(
prompt,
generation_config={
'temperature': temperature,
'top_p': 0.95,
'top_k': 40,
}
)
return response.text
def _build_context(self, search_results):
"""検索結果からコンテキストを構築"""
context_parts = []
for i, result in enumerate(search_results, 1):
title = result['metadata'].get('title', '不明')
content = result['content']
context_parts.append(f"[情報{i}] タイトル: {title}\n{content}\n")
return "\n".join(context_parts)
def get_collection_stats(self):
"""コレクションの統計情報を取得"""
count = self.collection.count()
return {
'total_documents': count,
'collection_name': self.collection.name
}
2. データローダー(data_loader.py)
次に、Wikipediaのmarkdownファイルを読み込んでChromaDBに登録するスクリプトを作成します。
データを更新するにはこれを使用します。
import os
import re
import argparse
from pathlib import Path
from tqdm import tqdm
from rag_system import WikipediaRAG
def parse_wikipedia_markdown(file_path):
"""Wikipediaのmarkdownファイルをパース(実データ形式対応)"""
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
title_match = re.search(r'^#\s+(.+)$', content, re.MULTILINE)
title = title_match.group(1).strip() if title_match else os.path.basename(file_path)
page_id_match = re.search(r'\*\*ページID\*\*:\s*(\d+)', content)
page_id = page_id_match.group(1) if page_id_match else ""
url_match = re.search(r'\*\*URL\*\*:\s*(.+)$', content, re.MULTILINE)
url = url_match.group(1).strip() if url_match else ""
lang_match = re.search(r'\*\*言語\*\*:\s*(\w+)', content)
language = lang_match.group(1) if lang_match else ""
datetime_match = re.search(r'\*\*取得日時\*\*:\s*(.+)$', content, re.MULTILINE)
fetch_datetime = datetime_match.group(1).strip() if datetime_match else ""
summary_match = re.search(
r'---\s*##\s+要約\s*\n\s*(.+?)\s*---',
content,
re.DOTALL
)
summary = summary_match.group(1).strip() if summary_match else ""
categories = []
category_match = re.search(
r'---\s*##\s+カテゴリ\s*\n(.+?)\s*---',
content,
re.DOTALL
)
if category_match:
category_text = category_match.group(1)
categories = [
line.strip('- ').strip().replace('Category:', '')
for line in category_text.split('\n')
if line.strip().startswith('-')
]
section_structure = []
section_match = re.search(
r'---\s*##\s+セクション構造\s*\n(.+?)\s*---',
content,
re.DOTALL
)
if section_match:
section_text = section_match.group(1)
for line in section_text.split('\n'):
if line.strip().startswith('-'):
section_structure.append(line.strip('- ').strip())
body_match = re.search(
r'---\s*##\s+本文\s*\n(.+?)(?:\n---\n|\Z)',
content,
re.DOTALL
)
body = body_match.group(1).strip() if body_match else ""
link_count_match = re.search(r'\*\*内部リンク総数\*\*:\s*(\d+)', content)
link_count = link_count_match.group(1) if link_count_match else "0"
return {
'title': title,
'page_id': page_id,
'url': url,
'language': language,
'fetch_datetime': fetch_datetime,
'summary': summary,
'categories': categories,
'section_structure': section_structure,
'body': body,
'link_count': link_count,
'full_content': content
}
def load_wikipedia_data(data_dir, reset=False):
"""Wikipediaデータの読み込みと登録"""
rag = WikipediaRAG()
if reset:
confirm = input("既存のデータをリセットしますか? (y/N): ")
if confirm.lower() == 'y':
rag.client.delete_collection("wikipedia_articles")
rag.collection = rag.client.get_or_create_collection(
name="wikipedia_articles"
)
print("データをリセットしました。")
data_path = Path(data_dir)
if not data_path.exists():
print(f"エラー: ディレクトリ '{data_dir}' が見つかりません。")
return
md_files = list(data_path.glob("*.md"))
if not md_files:
print(f"{data_dir} にmarkdownファイルが見つかりません。")
return
print(f"{len(md_files)}件のファイルを読み込みます...\n")
success_count = 0
error_count = 0
for file_path in tqdm(md_files, desc="データ読み込み中"):
try:
data = parse_wikipedia_markdown(file_path)
doc_id = f"wiki_{data['page_id']}" if data['page_id'] else f"wiki_{Path(file_path).stem}"
rag.collection.add(
documents=[data['full_content']],
metadatas=[{
'title': data['title'],
'page_id': data['page_id'],
'url': data['url'],
'language': data['language'],
'fetch_datetime': data['fetch_datetime'],
'summary': data['summary'][:500],
'categories': ','.join(data['categories'][:10]),
'link_count': data['link_count'],
'source': str(file_path)
}],
ids=[doc_id]
)
success_count += 1
except Exception as e:
error_count += 1
print(f"\nエラー ({file_path.name}): {e}")
stats = rag.get_collection_stats()
print(f"\n完了: {success_count}件の記事を登録しました")
if error_count > 0:
print(f"エラー: {error_count}件の記事で問題が発生しました")
print(f"データベース総数: {stats['total_documents']}件")
def main():
"""メイン処理"""
parser = argparse.ArgumentParser(
description='Wikipedia記事をChromaDBに読み込みます'
)
parser.add_argument(
'--data-dir',
default='./data/wikipedia',
help='Wikipediaのmarkdownファイルが格納されているディレクトリ (デフォルト: ./data/wikipedia)'
)
parser.add_argument(
'--reset',
action='store_true',
help='既存のデータをリセットしてから読み込む'
)
args = parser.parse_args()
print("=== Wikipedia RAG データローダー ===\n")
print(f"データディレクトリ: {args.data_dir}\n")
load_wikipedia_data(args.data_dir, args.reset)
if __name__ == "__main__":
main()
⚠️ 実際のデータでは想定通りにならないこともあります。markdownファイルの形式を確認して、必要に応じてパーサーを調整してください。
3. テストスクリプト(test_rag.py)
システムの動作確認用のインタラクティブなテストスクリプトを作成します。
from rag_system import WikipediaRAG
def test_search():
"""類似情報検索のテスト"""
rag = WikipediaRAG()
query = input("\n検索キーワードを入力: ")
n_results = int(input("取得件数 (デフォルト: 3): ") or "3")
print(f"\n検索中: '{query}'...\n")
results = rag.search_similar_content(query, n_results)
if not results:
print("関連する情報が見つかりませんでした。")
return
print(f"{len(results)}件の関連情報が見つかりました:\n")
for i, result in enumerate(results, 1):
metadata = result['metadata']
print(f"--- [{i}] {metadata.get('title', '不明')} ---")
print(f"類似度スコア: {1 - result['distance']:.4f}")
print(f"ページID: {metadata.get('page_id', 'N/A')}")
print(f"URL: {metadata.get('url', 'N/A')}")
print(f"取得日時: {metadata.get('fetch_datetime', 'N/A')}")
summary = metadata.get('summary', '')
if summary:
display_summary = summary[:150] + '...' if len(summary) > 150 else summary
print(f"要約: {display_summary}")
categories = metadata.get('categories', '')
if categories:
cat_list = categories.split(',')[:3]
print(f"カテゴリ: {', '.join(cat_list)}")
print()
def test_qa():
"""質問応答のテスト"""
rag = WikipediaRAG()
query = input("\n質問を入力: ")
print(f"\n回答を生成中...\n")
answer = rag.generate_answer(query)
print("=" * 60)
print("【回答】")
print("=" * 60)
print(answer)
print("=" * 60)
def interactive_mode():
"""インタラクティブモード"""
rag = WikipediaRAG()
print("\nインタラクティブモードを開始します")
print("終了するには 'quit' または 'exit' と入力してください\n")
while True:
query = input("\n質問: ").strip()
if query.lower() in ['quit', 'exit', 'q']:
print("終了します")
break
if not query:
continue
print("\n回答を生成中...\n")
answer = rag.generate_answer(query)
print("-" * 60)
print(answer)
print("-" * 60)
def show_statistics():
"""統計情報の表示"""
rag = WikipediaRAG()
stats = rag.get_collection_stats()
print(f"\n統計情報:")
print(f" - コレクション名: {stats['collection_name']}")
print(f" - 登録記事数: {stats['total_documents']}件")
if stats['total_documents'] > 0:
print("\nサンプルデータ:")
results = rag.collection.peek(limit=3)
if results and results.get('metadatas'):
for i, metadata in enumerate(results['metadatas'], 1):
print(f" {i}. {metadata.get('title', '不明')}")
print(f" ページID: {metadata.get('page_id', 'N/A')}")
print(f" カテゴリ数: {len(metadata.get('categories', '').split(','))}")
def main():
"""メインメニュー"""
while True:
print("\n" + "=" * 60)
print("Wikipedia RAG システム - テストメニュー")
print("=" * 60)
print("1. 類似情報検索テスト")
print("2. 質問応答テスト")
print("3. インタラクティブモード")
print("4. 統計情報の表示")
print("5. 終了")
print("=" * 60)
choice = input("\n選択 (1-5): ").strip()
if choice == '1':
test_search()
elif choice == '2':
test_qa()
elif choice == '3':
interactive_mode()
elif choice == '4':
show_statistics()
elif choice == '5':
print("\n終了します")
break
else:
print("\n無効な選択です")
if __name__ == "__main__":
main()
👉 ページID、取得日時、カテゴリなどの詳細情報を表示できます。
システムの実行方法
1. データの読み込み(ChromaDBへの格納)
初回実行時の流れ
# ヘルプを表示
$ python data_loader.py --help

# 基本的な実行(デフォルトのdata/wikipediaディレクトリのデータを使用)
$ python data_loader.py
# データディレクトリを指定して実行
$ python data_loader.py --data-dir ./my_data/wiki
# 既存データをリセットして読み込み
$ python data_loader.py --reset
# オプションを組み合わせることも可能
$ python data_loader.py --data-dir ./my_data/wiki --reset

2. 処理の確認
$ python test_rag.py

CLIメニューから以下の機能を試せるようになっています。
- 1) 類似情報検索テスト … キーワードで関連記事を検索



- 4) 統計情報の表示 … 登録されている記事数を確認

3. 基本的な使い方
先程のCLIツールを使用しなくても、クラス化されているのでPythonスクリプトから直接使用することもできます。
from rag_system import WikipediaRAG
rag = WikipediaRAG()
results = rag.search_similar_content("機械学習", n_results=3)
for result in results:
metadata = result['metadata']
print(f"タイトル: {metadata['title']}")
print(f"ページID: {metadata['page_id']}")
print(f"URL: {metadata['url']}")
print(f"カテゴリ: {metadata['categories']}")
print()
answer = rag.generate_answer(
"機械学習とディープラーニングの違いは何ですか?"
)
print(answer)
4. 実データでの動作確認
アップロードしたサンプルデータ(機械学習、自然言語処理)を使って、実際に動作を確認してみましょう。
# 機械学習や自然言語などのWikipediaデータをサンプルデータとして準備し、ディレクトリにコピーしてから
# データを読み込み
$ python data_loader.py
# テストプログラムを実行
$ python test_rag.py
試してみる質問の例
👉 実際のWikipediaデータを使うことで、具体的で信頼性の高い回答が得られます。
5. カスタマイズとチューニング
ソースコード内のコードを変更することで、カスタマイズやチューニングが可能です。
検索結果数の調整
検索する関連情報の数を調整できます。
answer = rag.generate_answer(
query="質問内容",
n_results=5
)
👉 複雑な質問ほど、多くの情報を参照した方が良い回答が得られます。
メタデータを使ったフィルタリング
実データのメタデータを活用して、検索を絞り込むことができます:
results = rag.collection.query(
query_texts=["機械学習の応用"],
n_results=3,
where={"categories": {"$contains": "機械学習"}}
)
results = rag.collection.query(
query_texts=["自然言語処理"],
n_results=3,
where={"page_id": "67"}
)
results = rag.collection.query(
query_texts=["深層学習"],
n_results=3,
where={
"$and": [
{"categories": {"$contains": "機械学習"}},
{"language": "ja"}
]
}
)
生成パラメータの調整
LLMの回答の創造性や多様性を調整できます。
answer = rag.generate_answer(
query="質問内容",
temperature=0.9
)
- temperature=0.0: より確定的で一貫性のある回答
- temperature=1.0: より創造的で多様な回答
モデルの変更
.envファイルでGeminiモデルを変更できます:
# より高性能なモデル(レスポンスは遅い)
GEMINI_MODEL=models/gemini-1.5-pro
# より高速なモデル(デフォルト)
GEMINI_MODEL=models/gemini-1.5-flash
2025/11/15時点での使用可能なモデルは以下の通り。(2.5以前のものは省略しています)
models/gemini-2.5-pro-preview-03-25
models/gemini-2.5-flash-preview-05-20
models/gemini-2.5-flash
models/gemini-2.5-flash-lite-preview-06-17
models/gemini-2.5-pro-preview-05-06
models/gemini-2.5-pro-preview-06-05
models/gemini-2.5-pro
models/gemini-2.5-flash-preview-tts
models/gemini-2.5-pro-preview-tts
models/gemini-2.5-flash-lite
models/gemini-2.5-flash-image-preview
models/gemini-2.5-flash-image
models/gemini-2.5-flash-preview-09-2025
models/gemini-2.5-flash-lite-preview-09-2025
models/gemini-2.5-computer-use-preview-10-2025
カテゴリフィルタリング
特定のカテゴリに絞った検索機能を追加することで、より精度の高い回答が可能になります。
例えば「機械学習カテゴリ内でのみ検索」が可能になります。
results = rag.collection.query(
query_texts=[query],
n_results=n_results,
where={"categories": {"$contains": "機械学習"}}
)
時系列フィルタリング
取得日時のメタデータを活用して、最新の情報のみを検索対象にする機能を追加できます。
results = rag.collection.query(
query_texts=[query],
n_results=n_results,
where={"fetch_datetime": {"$gte": "2025-01-01"}}
)
おわりに
RAGは、LLMの実用化において非常に重要な技術です。この内容が参考になれば…と思っていたのですが、GoogleがFileSearchAPIを発表したので、もう少しするとこの知識も陳腐化してしまうのかも😅
Gemini APIのFile Search Toolとして提供されている機能で、ユーザーが任意のファイルをアップロードすると、その内容をAIが自動でチャンク(分割処理)してインデックス化し、意味と文脈を理解したセマンティック検索が可能になります。このAPIを使うことで、開発者は複雑なRAG(Retrieval Augmented Generation)処理を簡単に実装でき、ファイル内の情報を効率的に検索・活用できます。
今回の内容こっちで作れよって言われそうですが…まあ勉強にはなりました😅
github.com
参考リンク