先日はWikipediaAPIを使用して、Wikipediaの記事を取得する方法について書いてみました。今回はそのデータを活用して、RAG(Retrieval-Augmented Generation)システムを構築してみようと思います。
参考
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の永続化データ
各Pythonプログラムの役割
今回のシステムは3つのPythonプログラムで構成されています。
1. rag_system.py(今回のRAGシステムの中心となるプログラム)
- ChromaDBとの接続・管理
- ベクトル検索による類似情報の取得
- Gemini APIを使った回答生成
- 検索結果からのコンテキスト構築
このプログラムにWikipediaRAGクラスを定義し、他のプログラムから利用できるようにしています。
2. data_loader.py(データ登録ツール)
このプログラムは最初に一度実行し、データベースにWikipedia記事を登録します。
3. test_rag.py(テスト・デモツール)
このプログラムでシステムの動作確認や実際の利用を体験できます。
ChromaDBとは
ChromaDBは、AIアプリケーション向けに設計されたオープンソースのベクトルデータベースです。
主な特徴
従来のデータベースがキーワードの完全一致で検索するのに対し、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
Google Gemini APIキーの取得
RAGを使用したLLMの生成にGeminiを使用しているので、APIキーを取得します。
- 1) Google AI Studio にアクセス
- 2) 「Get API Key」をクリックしてAPIキーを生成
- 3) 生成された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データの準備
このシステムでは、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システムの初期化""" # ChromaDBクライアントの設定 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記事のベクトルデータベース"} ) # Gemini APIの設定 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) # ページIDの抽出 page_id_match = re.search(r'\*\*ページID\*\*:\s*(\d+)', content) page_id = page_id_match.group(1) if page_id_match else "" # URLの抽出 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 # markdownファイルの読み込み 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) # ChromaDBに登録 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], # 500文字に制限 'categories': ','.join(data['categories'][:10]), # 上位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] # 最初の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) 類似情報検索テスト … キーワードで関連記事を検索

- 2) 質問応答テスト … 自然言語での質問に回答

- 3) インタラクティブモード … 対話的に連続で質問可能

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

3. 基本的な使い方
先程のCLIツールを使用しなくても、クラス化されているのでPythonスクリプトから直接使用することもできます。
from rag_system import WikipediaRAG # RAGシステムの初期化 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 # デフォルトは3 )
👉 複雑な質問ほど、多くの情報を参照した方が良い回答が得られます。
メタデータを使ったフィルタリング
実データのメタデータを活用して、検索を絞り込むことができます:
# 特定のカテゴリで検索 results = rag.collection.query( query_texts=["機械学習の応用"], n_results=3, where={"categories": {"$contains": "機械学習"}} ) # 特定のページIDで検索 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 # 0.0〜1.0、デフォルトは0.7
)
- 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": "機械学習"}} )
時系列フィルタリング
取得日時のメタデータを活用して、最新の情報のみを検索対象にする機能を追加できます。
# 2025年以降のデータのみを検索 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)処理を簡単に実装でき、ファイル内の情報を効率的に検索・活用できます。
今回の内容こっちで作れよって言われそうですが…まあ勉強にはなりました😅