
ローカルLLM(Gemma3)にRAGを組み合わせた記事生成パイプラインを用いて、RAGあり・RAGなしの出力を比較するためのレポートです。
実装の説明や前回からの差分も記載します。
※出力した内容は必ずしも事実であるとは限りません。
今回の記事生成パイプラインは以下のステップで動作します。
生成した内容をChatGPTに評価させ、精度の違いを検証します。
ChatGPTへの指示は「下記の①と②の文章のファクトチェックをした上で回答精度の比較評価をしてださい」です。
モデル: gemma3:4b
wikipediaダンプ: jawiki-latest-pages-articles.xml.bz2 04-Mar-2026 01:54 4592085011
リポジトリ: https://github.com/YKZ-J/ja-wikipedia-rag
Wikipedia ダンプ → ベクトル化 → Supabase 実装ノート
Wikipedia 日本語ダンプを取得し、テキストを Parquet 化 → nomic-embed-text で 768次元ベクトル生成 → ローカル Supabase (
documentsテーブル) に投入 → IVFFlat インデックスを張るまでの完全手順。関連ドキュメント: docs/wikipedia-pipeline.md(全体フロー概要)
全体フロー
jawiki-latest-pages-articles.xml.bz2 ← Wikipedia ダウンロード (~23GB) │ ▼ python/xml_to_parquet.py parquet_output/jawiki_00000.parquet ← チャンク分割済みParquet parquet_output/jawiki_00001.parquet ... │ ▼ python/embed_and_upload.py Supabase documents テーブル ← title / content / embedding(768次元) │ ▼ supabase/migrations/20260311000002_create_ivfflat_and_rpc.sql IVFFlat インデックス + search_documents RPC │ ▼ scripts/backup-and-restore.sh pg_dump バックアップ + parquet_output アーカイブ
0. 前提条件
必須ツール
| ツール | バージョン | 用途 |
| ------------ | ---------- | ------------------------------------- |
| Python | 3.11+ | 変換・Embedding スクリプト |
| Ollama | 最新 | nomic-embed-text モデルのホスティング |
| Docker | 最新 | Supabase ローカル DB |
| supabase CLI | 最新 | DB 管理 |
| psql | 任意 | 確認用 |
Python 依存パッケージのインストール
source .venv/bin/activate pip install -r requirements.txt # 主要パッケージ: mwxml mwparserfromhell pandas pyarrow tqdm # aiohttp asyncpg supabase python-dotenv環境変数 (.env.local)
# .env.local SUPABASE_URL=http://127.0.0.1:54321 SUPABASE_SERVICE_ROLE_KEY=<supabase start で表示される service_role key> DATABASE_URL=postgresql://postgres:postgres@127.0.0.1:54322/postgres
1. Supabase ローカル起動 & マイグレーション適用
# Docker 起動確認 docker compose up -d # Supabase ローカル起動 supabase start # マイグレーション適用(documents テーブル + vector 拡張) supabase db push --local適用されるマイグレーション:
20260311000001_create_documents.sql—vector拡張有効化、documentsテーブル作成
20260311000002_create_ivfflat_and_rpc.sql— IVFFlat インデックス +search_documentsRPC(データ投入後に適用)→ Step 1 ではまず migration 1 だけを適用し、migration 2 はデータ投入完了後に適用する。
-- documents テーブル定義(参考) CREATE TABLE IF NOT EXISTS documents ( id BIGSERIAL PRIMARY KEY, title TEXT NOT NULL, content TEXT NOT NULL, embedding VECTOR(768) NOT NULL );
2. Wikipedia ダンプ取得
# 最新の日本語ダンプをダウンロード(約23GB、時間がかかる) wget https://dumps.wikimedia.org/jawiki/latest/jawiki-latest-pages-articles.xml.bz2 # 進捗確認 ls -lh jawiki-latest-pages-articles.xml.bz2ヒント: 帯域が細い場合は
aria2c -x 8を使うと並列ダウンロードで短縮できる。
3. XML → Parquet 変換
スクリプト: python/xml_to_parquet.py
source .venv/bin/activate python3 python/xml_to_parquet.py \ --input jawiki-latest-pages-articles.xml.bz2 \ --outdir ./parquet_output \ --chunk-size 50000 \ --min-text-len 200オプション
| オプション | デフォルト | 説明 |
| ---------------- | ------------------ | -------------------------------- |
|
--input| — | XML.bz2 ファイルパス(必須) ||
--outdir|./parquet_output| Parquet 出力先ディレクトリ ||
--chunk-size| 50000 | 1ファイルあたりのレコード数 ||
--min-text-len| 200 | 最低テキスト長(短い記事を除外) |処理内容
bz2 圧縮のまま逐次読み込み(全展開せずメモリ効率よく処理)
#redirect/#転送行で始まるリダイレクトページを除外
mwparserfromhellで Wiki マークアップ(テンプレート・リンク記法等)を除去
chunk-size件ごとに Snappy 圧縮 Parquet ファイルに保存
tqdmでリアルタイム進捗表示出力例
parquet_output/ ├── jawiki_00000.parquet # ~50,000件 ├── jawiki_00001.parquet ├── jawiki_00002.parquet ... └── jawiki_00026.parquet # 日本語Wikipedia全記事 ~135万件Parquet スキーマ
| カラム | 型 | 内容 |
| ------- | ------ | ---------------------------------------- |
|
id| int64 | Wikipedia ページID ||
title| string | 記事タイトル ||
text| string | プレーンテキスト(マークアップ除去済み) |
4. Ollama マルチインスタンス起動
Embedding 生成を高速化するため、Ollama を複数ポートで並列起動する。
# 6インスタンス起動 (ポート 11434〜11439) ./scripts/start-ollama-multi.sh start # 起動確認 ./scripts/start-ollama-multi.sh statusnomic-embed-text モデルの手動プル(初回のみ)
ollama pull nomic-embed-text
start-ollama-multi.shの動作
ポート 11434〜11439 の各ポートで
OLLAMA_HOST=127.0.0.1:<port> ollama serveを起動5秒待機後、各インスタンスに
nomic-embed-textをプリロードPID を
/tmp/ollama-multi/ollama_<port>.pidに記録
5. Embedding 生成 & Supabase 投入
スクリプト: python/embed_and_upload.py
source .env.local # または export で環境変数を設定 python3 -u python/embed_and_upload.py \ --parquet ./parquet_output \ --chunk-size 64 \ --workers 6 \ --files 2 \ --skip-processedオプション
| オプション | デフォルト | 説明 |
| ------------------ | ---------- | ------------------------------------------ |
|
--parquet| — | Parquet ファイルまたはディレクトリ(必須) ||
--chunk-size| 64 | 一度に処理するレコード数 ||
--workers| 6 | チャンク内の並列処理数(≒Ollamaポート数) ||
--files| 2 | 複数ファイルの同時処理数 ||
--skip-processed| flag |.doneマーカー付きファイルをスキップ |処理フロー
Parquetファイル読み込み │ ▼ チャンク分割(chunk-size件) │ ▼ asyncio + aiohttp (workers個並列) nomic-embed-text API呼び出し URL: http://localhost:<port>/api/embed prefix: "search_document: " + text[:100文字] → 768次元ベクトル │ ▼ asyncpg upsert (PG_POOL_SIZE=8) Supabase documents テーブルに投入 ON CONFLICT (id) DO UPDATE │ ▼ <ファイル名>.done マーカー作成nomic-embed-text プレフィックス仕様(重要)
非対称検索モデルのため、用途別にプレフィックスが異なる。
| 用途 | プレフィックス |
| ------------------------------------- | ----------------------------- |
| 文書保存時(embed_and_upload.py) |
search_document: <テキスト>|| 検索クエリ時(rag_ask等) |
search_query: <クエリ>|プレフィックスを省略・間違えると検索精度が大幅に低下する。
処理速度の目安(Mac mini M4 / 6ポート並列)
| テキスト長 | 速度 | 推定時間(135万件) |
| ---------- | --------- | ------------------- |
| 100文字 | ~126件/秒 | 約3.2時間 |
| 150文字 | ~93件/秒 | 約4.3時間 |
| 500文字 | ~30件/秒 | 約13.4時間 |
デフォルト設定(
MAX_TEXT_CHARS=100)では先頭100文字のみを Embedding に使用。処理中の確認
# 投入件数をリアルタイム確認 psql "$DATABASE_URL" -c "SELECT COUNT(*) FROM documents;" # .done マーカー確認(完了ファイル数) ls parquet_output/*.done | wc -l中断・再開
--skip-processedオプションで.doneマーカー付きファイルをスキップするため、中断後は同じコマンドをそのまま再実行する。# 中断後の再実行(続きから自動再開) python3 -u python/embed_and_upload.py \ --parquet ./parquet_output \ --chunk-size 64 \ --workers 6 \ --files 2 \ --skip-processed
6. IVFFlat インデックス作成
⚠️ データ投入が完全に完了してから実行すること。
投入中にインデックスを作成すると精度が落ちる(クラスタ重心がずれる)。
psql "$DATABASE_URL" \ -f supabase/migrations/20260311000002_create_ivfflat_and_rpc.sql実行されるSQL
-- IVFFlat インデックス(135万件 ÷ 300クラスタ ≒ 4,500ベクトル/クラスタ) CREATE INDEX IF NOT EXISTS documents_embedding_ivfflat ON documents USING ivfflat (embedding vector_cosine_ops) WITH (lists = 300); -- 検索 RPC 関数(上位20クラスタを探索) CREATE OR REPLACE FUNCTION search_documents(query_embedding vector(768)) RETURNS TABLE (id bigint, title text, content text) LANGUAGE sql AS $$ SET LOCAL ivfflat.probes = 20; SELECT id, title, content FROM documents ORDER BY embedding <=> query_embedding -- cosine類似度(<=> を使うこと) LIMIT 10; $$;
listsパラメータの目安| 件数 | 推奨 lists |
| --------- | ---------- |
| ~10万件 | 100 |
| ~100万件 | 300〜500 |
| ~1000万件 | 1000〜2000 |
インデックス作成には数分〜数十分かかる(データ量による)。
注意:
vector_cosine_opsのインデックスには<=>演算子を使うこと。<->を使うとインデックスが効かず検索が秒〜十数秒級に劣化する。
7. 投入結果確認
# 総件数確認 psql "$DATABASE_URL" -c "SELECT COUNT(*) FROM documents;" # インデックス確認 psql "$DATABASE_URL" -c "\di documents_embedding_ivfflat" # RPC 動作テスト(seq_test.py) python3 python/seq_test.py --db --db-cases 3動作テストで確認すること
search_documentsRPC が正常に応答を返すかレスポンス件数が LIMIT (10) 通りか
検索レイテンシが 100ms 以内か(ローカルDB想定)
8. バックアップ
pg_dump(DB フルバックアップ)
# 自動スクリプト ./scripts/backup-and-restore.sh # 手動 pg_dump -Fc "$DATABASE_URL" -f backup_$(date +%Y%m%d).dumpParquet バックアップ
./scripts/backup-parquet.sh # → parquet_output/ を tar.gz アーカイブに圧縮して保存復元
# DB 復元 ./scripts/restore-only.sh /path/to/backup.dump # Parquet 複元 ./scripts/restore-parquet.sh
9. トラブルシューティング
Ollama が応答しない
# 起動状態確認 ./scripts/start-ollama-multi.sh status # 再起動 ./scripts/start-ollama-multi.sh stop ./scripts/start-ollama-multi.sh startembed_and_upload.py でタイムアウトエラー
原因: Ollama ポートの過負荷 or 接続切断
対策:
--workersを下げる(例: 6 → 3)python3 -u python/embed_and_upload.py \ --parquet ./parquet_output \ --chunk-size 32 \ --workers 3 \ --files 1 \ --skip-processed
search_documentsが遅い(数秒かかる)
原因:
<->演算子を使っている、またはprobesが低すぎる対策: SQL を確認して
<=>に変更する-- 誤: L2距離(インデックスが効かない) ORDER BY embedding <-> query_embedding -- 正: コサイン距離(ivfflat vector_cosine_ops に対応) ORDER BY embedding <=> query_embeddingSupabase 接続エラー
# Supabase の起動確認 supabase status # 再起動 supabase stop supabase start
gemma3にwikipedia検索用のクエリを作成する部分の改善。
10記事取得後、2記事に絞るランキングの精度向上。
以下では、札幌の狸小路商店街に関する①・②の記述を事実ベースで検証し、そのうえで回答精度を比較評価します。
①のファクトチェック
総合評価:概ね正確(細部に曖昧・誇張あり)
正しい点
狸小路が札幌中心部(南2条〜南3条付近)に位置する商店街である → 正しい
19世紀(明治初期)から形成された歴史ある商店街→ 正しい
歓楽街的性格(薄野との関係)を持っていた→ 概ね正しい
戦後に闇市的な時期を経て復興した→ 正しい
アーケード整備(昭和期)や狸まつりの開催→ 正しい
近年の再開発(moyuk sapporoなど)→ 正しい
不正確・疑わしい点
「南2条と南3条の中洲」
→ 誤りに近い表現。中洲ではなく、創成川の西側に広がる商店街「第一勧工場が1885年に誕生」
→ 勧工場の存在自体は史実だが、年や位置の特定は不確実「安田ビアホール」「第一神田館」
→ 札幌史としては存在例はあるが、狸小路との直接的関連は曖昧「札幌観音堂(1933)」
→ 確証が弱く、一般的な狸小路史としてはあまり知られていない細かい年表(1910・1914など)
→ 過剰に具体的で裏取り困難な情報が混在評価ポイント
歴史の流れはよく押さえている
ただし細部に“もっともらしいが裏付けが弱い記述”が混在
②のファクトチェック
総合評価:重大な誤りが多い(不正確)
明確な誤り
「1967年に札幌駅北口にオープン」
→ 完全な誤り(実際は明治初期に成立、場所も大通〜すすきの間)「タヌキを目撃したことが名前の由来」
→ 誤り(俗説にも乏しい)「イルミネーションが世界的に有名」
→ 誇張(札幌全体ではさっぽろホワイトイルミネーションが有名)「12ヘクタール規模」
→ 明確に誇張(実際は東西約900m程度の商店街)「数百万LED」
→ 根拠不明の誇張「六丁目・七丁目が光の都」
→ 一般的な呼称ではない部分的に正しい点
商店街であること
店舗の多様性(飲食・土産など)
→ ただしこれは一般論レベル評価ポイント
基本的な成立年代・位置が誤っているため致命的に不正確
観光記事風だが、事実よりも演出・創作が優先されている
①と②の比較評価
観点 ① ② 基本事実(場所・歴史) 概ね正確 大きく誤り 年代の正確性 一部不確実 根本的に誤り 内容の信頼性 中〜高 低 説明の深さ 高い(歴史重視) 低い(観光風・表層的) 誇張・創作 ややあり 非常に多い
総合結論
①が明らかに優れている
大枠の歴史・役割は正確
ただし細部に検証不足あり
②は不合格レベル
成立年代・場所などの基礎情報が誤り
観光記事風の創作が多く、信頼性が低い
最終評価
①:70〜80点(実用レベルだが精査必要)
②:20点以下(事実誤認が多く不適切)
必要であれば、**完全に正確な「狸小路の1000文字解説(一次資料ベース)」**も書けます。
質問
札幌の狸小路について1000文字程度で説明して
回答
札幌の狸小路は、単なる商店街という枠を超え、その歴史と独自性において、札幌のシンボルとして重要な役割を担ってきた。南2条と南3条の中洲に位置するこの街区は、創成川沿いの横長の地形を生かし、1873年頃から「狸小路」と名付けられ、多様な人々が集まる歓楽街として発展してきた。 狸小路の名称は、その起源にいくつかの説がある。1891年の『札幌繁昌記』によれば、創成川の西側を指す小路を「狸小路」と呼んでいたが、その名が人々の想像力を刺激し、夜な夜な真面目に白い手をすっくと伸ばして金儲けを夢見る男たちを「狸」に見立てていたという。1934年の回顧談では、1873年頃に開いた「東座」をきっかけに酒場が立ち並び、その様子が「狸」に例えられたという説が挙げられている。また、明治から大正初期にかけて、人を化かして仕入れ価格を吊り上げ、暴利をむさぼる商店がいたことも、商売人を「狸」に見立てる要因となったという説もある。 狸小路の歴史は、札幌の開拓と発展と密接に結びついている。1869年に明治政府が開拓使を設置し、1871年に札幌に本庁舎が移転したことで、現在の南1条から南3条までの間に町屋や飲食店が立ち並び始めた。1873年頃にはすでに「狸小路」と呼ばれていたが、当初は公娼の薄野に対する私娼の歓楽街として賑わいを見せ、1885年には札幌初の勧工場「第一勧工場」が誕生し、その後、札幌商館や物品販売業者などが次々と進出し、商店街としての色彩が強まっていった。また、寄席や演劇が盛んに行われ、娯楽の場としても重要な役割を果たした。 20世紀に入り、狸小路は多様な変化を経験した。1910年にはビアホール「安田ビアホール」が開店し、1914年には映画常設館「第一神田館」が誕生し、映画産業の発展を支えた。1925年には「狸小路聯合会」が発足し、商店街としての組織化が進められたが、その後、いくつかの会が分裂し、1930年に「五丁目会」「六丁目会」が再出発した。1927年には「鈴蘭灯」が設置され、1933年には観音像を祀る「札幌観音堂」が建立された。 第二次世界大戦後、狸小路は闇市が起こるなど混乱の時期を過ごしたが、戦後復興とともに商店街としての機能を回復し、1954年からは「狸まつり」が開催されるようになった。1960年には全蓋アーケードが完成し、雨天時でも快適に買い物ができる環境が整備された。1971年には札幌市営地下鉄南北線が延伸し、地下鉄との接続が実現した。 近年では、2001年に松山中央商店街と「姉妹商店街」を締結し、2002年には光ファイバーと無線LANによる商店街LANを構築し、監視カメラや発光ダイオード (LED) によるサインボードなどを設置するなど、最新技術を導入し、活性化を図っている。2015年には札幌市電ループ化が実現し、停留場が新設され、観光客の増加に貢献している。2023年には大型商業施設「moyuk sapporo」がオープンし、さらに賑わいを増している。 狸小路は、その歴史の中で多様な文化や人々の営みを育み、札幌の街の記憶を形作ってきた。現在も、その伝統を受け継ぎながら、新たな価値を創造し、未来へと繋げていくことだろう。
検索クエリ(実際に使用)
札幌 狸小路商店街 青果市場 概要 歴史参照元 Wikipedia 一覧
- 【狸小路商店街】
- 【狸小路停留場】
質問
札幌の狸小路について1000文字程度で説明して
回答
- 札幌の狸小路について、観光客が知っておくべきこと - イルミネーションについて言及する ## 狸小路:札幌の光と活気にあふれる商店街 札幌の狸小路は、北海道最大の商店街の一つであり、その歴史と魅力で多くの観光客を惹きつけています。特に冬のイルミネーションシーズンには、世界的に有名なライトアップイベントとして知られ、訪れる人々を魅了し続けています。 ### 歴史と概要 狸小路は、1967年に札幌駅北口にオープンしました。その名の由来は、この地を散策していた際に目撃されたタヌキの姿から来ています。当初は、駅周辺の商業施設を集約するための目的でしたが、次第に独自の発展を遂げ、現在では、雑貨、飲食店、お土産店など、様々なジャンルの店舗が軒を連ねる活気ある商店街へと変貌しました。 ### 狸小路六丁目と七丁目:光の都 狸小路の主要なエリアである六丁目と七丁目は、「光の都」と呼ばれるほど、圧倒的なイルミネーションで知られています。これらのエリアは、毎年11月下旬から翌年の2月頃まで、様々なテーマでライトアップが行われ、まるで別世界のような幻想的な空間を作り出します。 * テーマ性: イルミネーションは、その年ごとに異なるテーマを設定し、季節やイベントに合わせた装飾が施されます。例えば、雪国の風景を再現したり、アニメキャラクターをモチーフにした作品が登場したりと、毎年新しい驚きを提供しています。 * 規模: 六丁目と七丁目のイルミネーションは、広さ約12ヘクタールに及ぶ巨大なスペースで、数百万ものLEDライトが使用されています。 * イベント: イルミネーションの期間中には、様々なイベントも開催され、音楽演奏やフードトラックなどが登場し、賑わいを盛り上げます。 ### 狸小路の魅力 狸小路は、イルミネーション以外にも多くの魅力があります。 * 豊富な店舗: 雑貨店、飲食店、お土産店など、様々なジャンルの店舗が揃っており、ショッピングを楽しめます。 * グルメ: 北海道ならではの食材を使った料理や、ラーメン、ジンギスカンなど、様々なグルメを味わうことができます。 * 手軽なアクセス: 札幌駅からのアクセスも良く、観光客にとって非常に便利です。 * 地元の人々との交流: 商店街には地元の人々が訪れ、活気にあふれています。 ### まとめ 札幌の狸小路は、イルミネーションだけでなく、その歴史と魅力あふれる商店街として、多くの人々を魅了し続けています。冬の光の都として知られる六丁目と七丁目は、一度訪れたら忘れられない体験となるでしょう。観光客の方は、ぜひ狸小路を訪れて、札幌の活気と魅力を体感してみてください。 補足: 狸小路は、近年ではスマートフォンアプリを活用した情報提供やイベント告知なども行い、より現代的な商店街へと進化しています。
精度が高いのは①(RAGあり)の方です。 RAGによって情報ソースを参照できるため正確性と具体性が向上します。