週末ラボ1: 自分の資料でRAGを作り、20問で当たり外れを測る
所要時間: 約3〜3.5時間(準備30分+作業2.5〜3時間。ほかに、モデルなどのダウンロード時間が回線次第で30分以上かかります) / 難易度: Pythonスクリプトを動かせれば可 / 費用: 0円(すべて自分のPCで動く)
動作確認: 2026-10-18。スクリプトはPython 3.12・chromadb 1.5.9 で、検索・集計部分の動作を確認済み。Ollama本体と埋め込みモデルを使った通し実行は、作成環境にOllamaが無かったため未確認です(APIの形式は公式ドキュメントで確認済み)。
このラボのゴール
社内の規程やFAQを使う「RAG(検索して答えるAI)」を小さく作り、検索が正しい資料を拾えたかを20問で数えます。
終わったあと、顧客に次のことを自分の言葉で説明できるようになります。
- 「RAGの誤答」は1種類ではない。①資料に答えが無い ②検索で外した ③検索は当たったが回答が誤り ④元資料が古い・矛盾している に分けると、直す場所(資料の整備/検索の設定/プロンプトやモデル/資料の更新ルール)が変わる。
- 「何割当たるか」は、実際に聞かれる質問で作った小さなテストセットで測れる。20問程度でも傾向はつかめるが、20問の結果には大きな幅がある(18/20=90%でも、95%区間はおよそ70〜97%)。
- 意味で探す検索(ベクトル検索)とキーワード検索は得意分野が違う。型番・規程番号・エラーコードのような語はキーワード検索の方が拾いやすいことがある。
用語(このラボで使う意味)
| 用語 | 意味 |
|---|---|
| チャンク | 資料を一定の文字数で区切った断片。検索はチャンク単位で行う |
| 埋め込み(ベクトル) | 文章の意味を数値の並びにしたもの。近い意味の文章は近い数値になる |
| top-k | 検索結果の上位k件。RAGでは上位k件だけをAIに渡すのが一般的 |
| hit@k | 「正解の根拠が書かれたチャンクが上位k件に入った問題の割合」 |
| ゴールデンセット | 質問と「正解の根拠がどの資料のどこにあるか」を人が決めた評価用の問題集 |
前提
- Windows 11、PowerShell(Windows 標準の「Windows PowerShell 5.1」でも「PowerShell 7」でも可)
- 空きディスク 約10GB(Ollama本体+モデル2つ。埋め込みモデル bge-m3 は約1.2GB、回答用の gemma3:4b は約3.4GB)
- メモリ 16GB以上を推奨(gemma3:4b を動かすため。8GBでは回答生成が遅い/動かない可能性があります。未確認)
- 使う資料は公開済みのもの、または自分で作ったダミーにしてください(下の「安全上の注意」参照)
手順
0. 準備(約30分)
(1) Python 3.12
まだ入っていなければ、Python公式が推奨する「Python install manager」を入れてから 3.12 を追加します(Microsoft Store または python.org から入手)。
py install 3.12
py list
py installが「不明なオプション」等で失敗したら、従来の py ランチャー(python.org の 3.12 インストーラ付属)が入っています。py -0で 3.12 が一覧にあればそのまま次へ進めます。無ければ python.org から Python 3.12 のインストーラを入れてください。(py install/py listは新しい Python install manager のコマンドで、py -0は新旧どちらのpyでも使えます。出典は下の「公式ドキュメント」の Python on Windows)
(2) Ollama(自分のPCでAIモデルを動かすソフト)
https://ollama.com/download から OllamaSetup.exe を実行します(管理者権限は不要)。インストール後、新しいPowerShellで:
ollama --version
ollama pull bge-m3
ollama pull gemma3:4b
ollama list
モデル2つで約4.6GBをダウンロードします。回線によっては30分以上かかるため、前日に
ollama pullだけ済ませておくと当日が楽です。このあと入れる chromadb も、依存パッケージを含めて数百MBほど取得します(目安。未計測)。
Ollama はインストール後、http://localhost:11434 で待ち受けます。
(3) 作業フォルダと仮想環境
mkdir C:\lab\rag
cd C:\lab\rag
mkdir docs
# Python の日本語表示が文字化けしないように(このウィンドウの中だけ有効)
$env:PYTHONUTF8 = "1"
py -V:3.12 -m venv .venv
.\.venv\Scripts\python.exe -m pip install chromadb==1.5.9
$env:PYTHONUTF8 = "1"はこのPowerShellウィンドウを閉じると消えます。ウィンドウを開き直したら、手順3の実行の前にもう一度入力してください。
Activate.ps1で仮想環境を有効にする方法もありますが、PowerShellの実行ポリシーで止まることがあるため、このラボでは.\.venv\Scripts\python.exeを直接呼びます。
(4) SQLite FTS5 が使えるか確認
キーワード検索に Python 同梱の SQLite の全文検索機能(FTS5、trigram トークナイザ)を使います。trigram は SQLite 3.34.0(2020-12)で追加された機能です。
.\.venv\Scripts\python.exe -c "import sqlite3; print(sqlite3.sqlite_version); c=sqlite3.connect(':memory:'); c.execute('create virtual table t using fts5(x, tokenize=trigram)'); print('FTS5 trigram OK')"
FTS5 trigram OK と出ればOKです(作成環境の Python 3.12 では SQLite 3.42.0 で動作しました)。
1. 資料を用意する(約20分)
C:\lab\rag\docs に、10〜30本の資料を .txt または .md(文字コード UTF-8)で置きます。
- 例: 自社サイトで公開済みのFAQ、公開している利用規約・プライバシーポリシー、自分で書いたダミーの社内規程
- PDFやWordはテキストをコピーして
.txtに貼り付ければ十分です(表や図は崩れます。それも「RAGの現実」として観察対象です) - ファイル名に版や日付を入れておくと、手順5の「元資料が古い」の判定が楽になります(例:
経費規程_2024-04.txt) - 古い版と新しい版をわざと両方入れておくと、④の失敗が観察できます
- サブフォルダを作る場合も、ファイル名はすべて別の名前にしてください(スクリプトはファイル名で資料を区別します)
2. 20問のゴールデンセットを作る(約60分) ― このラボの本体
C:\lab\rag\golden.csv を作ります(Excelで作って「CSV UTF-8」で保存して可)。列は4つです。
| 列 | 内容 |
|---|---|
| qid | 問題番号(Q01〜Q20) |
| question | 質問文。実際に社員や顧客が聞きそうな言い方で書く |
| source | 正解が書いてある資料のファイル名。資料に答えが無い問題は空欄 |
| evidence | 正解の根拠となる資料中の短い一節(10〜20字程度)をそのままコピー |
例:
qid,question,source,evidence
Q01,経費っていつまでに出せばいい?,経費規程_2025-04.txt,翌月10日までに申請
Q02,パスワードは何文字以上?,情報セキュリティ規程.txt,12文字以上
Q03,ERR-4031が出たときの対処は,FAQ_システム.txt,ERR-4031
Q04,社員旅行の補助金はいくら?,,
作り方のコツ:
- 20問の内訳の目安: ふつうの質問12問、言い換え・略語・口語の質問4問、型番やコードを含む質問2問、資料に答えが無い質問2問
- 質問は資料の文をコピーしない(コピーすると検索が不自然に当たりやすくなります)
- evidence は資料からそのままコピーします。スクリプトは空白を無視して「その一節を含むチャンクが上位に出たか」で判定します
- evidence が長すぎるとチャンクの境目で切れて「外れ」扱いになります。15字前後が目安です
3. スクリプトを置いて実行する(約30分)
下の rag_eval.py を C:\lab\rag\rag_eval.py として保存(UTF-8)し、チャンク長を変えて2回実行します。
cd C:\lab\rag
.\.venv\Scripts\python.exe rag_eval.py --docs docs --golden golden.csv --chunk 300
.\.venv\Scripts\python.exe rag_eval.py --docs docs --golden golden.csv --chunk 800
出力例(数値は例):
チャンク数: 142 / 問題数: 20 / チャンク長: 300字
[vector] 資料に答えがある問題 18 問で集計
hit@1: 11/18 = 61% (95%区間 39%〜80%)
hit@3: 15/18 = 83% (95%区間 61%〜94%)
hit@5: 16/18 = 89% (95%区間 67%〜97%)
→ results_vector_c300.csv に保存
[keyword] 資料に答えがある問題 18 問で集計
...
[vector]= 埋め込み(bge-m3)+Chroma による意味検索、[keyword]= SQLite FTS5(trigram, BM25順位)によるキーワード検索- 「資料に答えが無い問題」(source が空欄)は hit@k の分母から外しています
rag_eval.py(全文)
"""週末ラボ1: 自分の資料でRAGを作り、20問で当たり外れを測る(最小構成)
使い方(PowerShell):
python rag_eval.py --docs docs --golden golden.csv --chunk 400
python rag_eval.py --docs docs --golden golden.csv --chunk 400 --answer # 回答も作る
- ベクトル検索: Ollama の埋め込みモデル(既定 bge-m3) + Chroma
- キーワード検索: SQLite FTS5 (trigram トークナイザ, bm25 順位)
- 結果: 画面に hit@1/3/5 と 95%区間、results_<方式>_c<チャンク長>.csv に問題ごとの順位
"""
import argparse
import csv
import json
import math
import pathlib
import re
import sqlite3
import urllib.request
import chromadb
from chromadb.config import Settings
OLLAMA = "http://localhost:11434"
KS = (1, 3, 5)
# ---------- 1. 資料を読み込んでチャンクに分ける ----------
def load_chunks(doc_dir, size, overlap):
chunks = []
seen = {}
for path in sorted(pathlib.Path(doc_dir).rglob("*")):
if path.suffix.lower() not in (".txt", ".md"):
continue
if path.name in seen: # 同じファイル名があると、どの資料が当たったか区別できない
raise SystemExit(f"同じファイル名が2つあります: {seen[path.name]} と {path}。どちらかの名前を変えてから再実行してください")
seen[path.name] = path
text = re.sub(r"\s+", " ", path.read_text(encoding="utf-8")).strip()
step = max(size - overlap, 1)
start, i = 0, 0
while start < len(text):
piece = text[start:start + size]
chunks.append({"id": f"{path.name}#{i}", "source": path.name, "text": piece})
if start + size >= len(text):
break
start += step
i += 1
return chunks
def norm(s):
"""空白を除いて比較する(根拠フレーズの照合用)"""
return re.sub(r"\s+", "", s or "")
# ---------- 2. Ollama で埋め込みを作る ----------
def post_json(url, payload, timeout=300):
req = urllib.request.Request(
url, data=json.dumps(payload).encode("utf-8"),
headers={"Content-Type": "application/json"})
with urllib.request.urlopen(req, timeout=timeout) as res:
return json.loads(res.read().decode("utf-8"))
def embed(texts, model, batch=16):
vectors = []
for i in range(0, len(texts), batch):
out = post_json(f"{OLLAMA}/api/embed", {"model": model, "input": texts[i:i + batch]})
vectors.extend(out["embeddings"])
return vectors
# ---------- 3a. ベクトル検索 (Chroma) ----------
def vector_search(chunks, questions, k, model):
client = chromadb.EphemeralClient( # メモリ上だけ。毎回作り直す
settings=Settings(anonymized_telemetry=False)) # 利用統計の送信を止める
col = client.get_or_create_collection(
"chunks", embedding_function=None,
configuration={"hnsw": {"space": "cosine"}})
col.add(ids=[c["id"] for c in chunks],
embeddings=embed([c["text"] for c in chunks], model),
documents=[c["text"] for c in chunks],
metadatas=[{"source": c["source"]} for c in chunks])
res = col.query(query_embeddings=embed(questions, model), n_results=min(k, len(chunks)))
return [[{"id": i, "source": m["source"], "text": d}
for i, m, d in zip(ids, metas, docs)]
for ids, metas, docs in zip(res["ids"], res["metadatas"], res["documents"])]
# ---------- 3b. キーワード検索 (SQLite FTS5 trigram + bm25) ----------
def keyword_search(chunks, questions, k):
db = sqlite3.connect(":memory:")
db.execute("CREATE VIRTUAL TABLE t USING fts5(cid UNINDEXED, source UNINDEXED, body, tokenize='trigram')")
db.executemany("INSERT INTO t VALUES (?,?,?)",
[(c["id"], c["source"], c["text"]) for c in chunks])
results = []
for q in questions:
q2 = re.sub(r"[\s\"'??。、,.!!「」()()]", "", q)
grams = sorted({q2[i:i + 3] for i in range(len(q2) - 2)})
if not grams:
results.append([])
continue
match = " OR ".join(f'"{g}"' for g in grams)
rows = db.execute("SELECT cid, source, body FROM t WHERE t MATCH ? ORDER BY rank LIMIT ?",
(match, k)).fetchall()
results.append([{"id": r[0], "source": r[1], "text": r[2]} for r in rows])
return results
# ---------- 4. 当たり判定と集計 ----------
def hit_rank(hits, source, evidence):
"""正解の資料かつ根拠フレーズを含むチャンクが何位に出たか(出なければ None)"""
for rank, h in enumerate(hits, start=1):
if h["source"] == source and (not evidence or norm(evidence) in norm(h["text"])):
return rank
return None
def wilson(x, n, z=1.96):
"""二項割合の95%区間(Wilson法)"""
if n == 0:
return (float("nan"), float("nan"))
p = x / n
denom = 1 + z * z / n
center = (p + z * z / (2 * n)) / denom
half = z * math.sqrt(p * (1 - p) / n + z * z / (4 * n * n)) / denom
return (center - half, center + half)
def answer(question, hits, chat_model):
context = "\n\n".join(f"[{h['id']}] {h['text']}" for h in hits[:3])
out = post_json(f"{OLLAMA}/api/chat", {
"model": chat_model, "stream": False, "options": {"temperature": 0},
"messages": [
{"role": "system", "content": "次の資料だけを根拠に日本語で簡潔に答えてください。"
"資料に答えが無ければ『資料に記載がありません』と答えてください。\n\n" + context},
{"role": "user", "content": question}]})
return out["message"]["content"].strip()
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--docs", default="docs")
ap.add_argument("--golden", default="golden.csv")
ap.add_argument("--chunk", type=int, default=400, help="1チャンクの文字数")
ap.add_argument("--overlap", type=int, default=50)
ap.add_argument("--mode", choices=["vector", "keyword", "both"], default="both")
ap.add_argument("--embed-model", default="bge-m3")
ap.add_argument("--answer", action="store_true", help="上位3件で回答も作る(ベクトル検索側)")
ap.add_argument("--chat-model", default="gemma3:4b")
args = ap.parse_args()
chunks = load_chunks(args.docs, args.chunk, args.overlap)
with open(args.golden, encoding="utf-8-sig", newline="") as f:
golden = list(csv.DictReader(f))
questions = [g["question"] for g in golden]
print(f"チャンク数: {len(chunks)} / 問題数: {len(golden)} / チャンク長: {args.chunk}字")
modes = ["vector", "keyword"] if args.mode == "both" else [args.mode]
for mode in modes:
if mode == "vector":
all_hits = vector_search(chunks, questions, max(KS), args.embed_model)
else:
all_hits = keyword_search(chunks, questions, max(KS))
rows, ranks = [], []
for g, hits in zip(golden, all_hits):
answerable = bool(g["source"].strip())
rank = hit_rank(hits, g["source"].strip(), g["evidence"].strip()) if answerable else None
if answerable:
ranks.append(rank)
row = {"qid": g["qid"], "question": g["question"], "source": g["source"],
"answerable": "yes" if answerable else "no(資料に無い)",
"hit_rank": rank if rank else "-",
"top1": hits[0]["id"] if hits else "",
"top1_text": hits[0]["text"][:80] if hits else ""}
if args.answer and mode == "vector":
row["answer"] = answer(g["question"], hits, args.chat_model)
row["分類(手で記入)"] = ""
rows.append(row)
n = len(ranks)
if n == 0:
print("source 列が全部空欄です。golden.csv を確認してください")
return
print(f"\n[{mode}] 資料に答えがある問題 {n} 問で集計")
for k in KS:
x = sum(1 for r in ranks if r is not None and r <= k)
lo, hi = wilson(x, n)
print(f" hit@{k}: {x}/{n} = {x / n:.0%} (95%区間 {lo:.0%}〜{hi:.0%})")
out = f"results_{mode}_c{args.chunk}.csv"
with open(out, "w", encoding="utf-8-sig", newline="") as f:
w = csv.DictWriter(f, fieldnames=list(rows[0].keys()))
w.writeheader()
w.writerows(rows)
print(f" → {out} に保存")
if __name__ == "__main__":
main()
4. 回答も作って、外れを分類する(約40分)
上位3チャンクを使って、ローカルの gemma3:4b に回答させます(資料は外部に送られません)。
.\.venv\Scripts\python.exe rag_eval.py --docs docs --golden golden.csv --chunk 300 --mode vector --answer
results_vector_c300.csv をExcelで開くと、問題ごとに hit_rank(正解チャンクの順位。- は5位以内に無し)、1位のチャンク、answer(AIの回答)が並びます。空いている「分類(手で記入)」列に、次のどれかを記入します。
| 分類 | 判定のしかた | 直す場所の例 |
|---|---|---|
| ⓪正解 | 資料に答えがあり、answer が正しい(資料に無い問題で「記載がありません」と答えられた場合も⓪) | 不要 |
| ①資料に無い | source が空欄なのに、AIがもっともらしい答えを作った | 資料の追加、「分からない」と答えさせる指示 |
| ②検索で外した | hit_rank が -、または4〜5位(AIに渡る上位3件に入っていない) | チャンク長、キーワード検索との併用、資料の見出し整備 |
| ③検索は当たったが回答が誤り | hit_rank が1〜3位なのに answer が誤り | プロンプト、モデル、渡す件数 |
| ④元資料が古い・矛盾 | 1位のチャンクが旧版の資料、または資料同士が食い違う | 旧版の削除、版・日付のメタデータ、更新ルール |
この4分類は、RAGの失敗点を7つに整理した論文(Barnett ほか 2024 "Seven Failure Points When Engineering a RAG System")の考え方を、顧客説明用に4つにまとめ直したものです。
5. 1枚にまとめる(約20分)
次の表を埋めます。これがそのまま顧客への説明資料の骨格になります。
| 条件 | hit@1 | hit@3 | hit@5 | 95%区間(hit@3) |
|---|---|---|---|---|
| ベクトル / 300字 | ||||
| ベクトル / 800字 | ||||
| キーワード / 300字 | ||||
| キーワード / 800字 |
| 分類 | 件数 | 代表例(問題番号) |
|---|---|---|
| ⓪正解 | ||
| ①資料に無い | ||
| ②検索で外した | ||
| ③検索は当たったが回答が誤り | ||
| ④元資料が古い・矛盾 |
観察ポイント
- チャンク長: 300字と800字で hit@k はどう動いたか。短いと文脈が切れて外れ、長いと関係ない話が混ざって順位が下がる、のどちらが多かったか
- ベクトル vs キーワード: 型番・コードを含む問題(Q03のような)はどちらが当てたか。言い換え・口語の問題はどちらが当てたか。両方の結果を合わせれば何問拾えるか(これが「ハイブリッド検索」の発想です)
- k の効果: hit@1 と hit@5 の差が大きいなら「AIに渡す件数を増やす」で改善する余地がある。ただし渡す量が増えると回答側が読み落とすこともある
- 区間の幅: 18問で hit@3 が 15/18 でも、95%区間は約61〜94%。「83%当たる」と言い切れない理由を数字で確認する(ただし、同じ問題で2条件を比べるときは、区間の重なりではなく「片方だけ当たった問題の数」で見ます)
- 資料に無い質問: AIは「記載がありません」と言えたか、もっともらしい作り話をしたか
ふりかえりシート
- あなたの資料で一番多かった失敗は①〜④のどれでしたか。それを直すのは誰の仕事(資料の担当者/システム担当/AIの設定)ですか。
- チャンク長・検索方式の違いで、hit@3 は何問動きましたか。2つの結果CSVを並べ、「Aだけ当たった問題」と「Bだけ当たった問題」を数えてください。同じ問題で比べているので、差を見るときは片方だけ当たった問題の数が大事です(例: 片方だけ当たりが 4対0 なら差がありそう、2対1 なら偶然の範囲)。各条件の95%区間が重なるかどうかだけで判断しないこと。
- 顧客が「うちのRAGは9割当たるとベンダーに言われた」と言ったら、どんな質問を返しますか(何問で、どんな質問で、何を「当たり」と数えたか)。
- 20問を100問に増やすなら、どこから質問を集めますか(問い合わせ履歴・ヘルプデスクのチケット等)。その際の個人情報の扱いは。
- ④(元資料が古い)を防ぐ運用ルールを1つ書いてください。
- 結果を使って、顧客向けの説明を3文で書いてください。型: 「実際の質問○問で測ると、正しい資料を上位3件に拾えたのは○問(95%区間○〜○%)でした。外れの主な原因は(①〜④)で、直すのは(誰)です。次は(何問に増やす/何を変える)で再測定します。」
安全上の注意
- 個人情報・機密情報を含む資料は使わない。練習には公開済み資料かダミーを使う。
- このラボの構成(Ollama+Chroma+SQLite)は資料を外部に送りません。ただし、資料をClaudeなど外部のAIサービスに貼り付けて比較する場合は、その時点で外部送信になります。公開資料だけにしてください。
- Chroma は既定で匿名の利用統計を送る設定になっているため、スクリプト内で
anonymized_telemetry=Falseを指定しています。 - Ollama は既定で自分のPC(localhost)だけで待ち受けます。設定(
OLLAMA_HOST)を変えて外部に公開しないでください。 - Ollama には、外部のサーバーで動く「クラウドモデル」もあります。Ollama の公式ドキュメントでは、クラウドモデルへの入力と回答は Ollama のサーバーで処理されると書かれています(学習には使わないとも記載)。名前の付き方は、公式ドキュメントの例では
gemma4:cloudのようにcloudで終わります(-cloudで終わる名前も見かけますが、今回確認した公式ページでは未確認)。このラボではollama listに出るローカルモデル(bge-m3, gemma3:4b)だけを使い、cloudが付く名前のモデルやollama signinは使わないでください。クラウド機能を完全に止めたい場合は、公式 FAQ に、環境変数OLLAMA_NO_CLOUD=1(または~/.ollama/server.jsonに"disable_ollama_cloud": true)を設定して Ollama を再起動する方法があります(確認日 2026-10-19。版により変わる可能性があります)。 golden.csvや結果CSVに、実際の問い合わせ文をそのまま入れる場合は、氏名・連絡先などを伏せてから使う。
データの保存場所と後片付け
| もの | 保存場所 | 消し方 |
|---|---|---|
| 資料・golden.csv・結果CSV(資料の抜粋とAIの回答を含む) | C:\lab\rag | Remove-Item -Recurse -Force C:\lab\rag |
| ベクトル(Chroma) | メモリ上のみ(EphemeralClient)。終了時に消える | 不要 |
| キーワード索引(SQLite) | メモリ上のみ(:memory:) | 不要 |
| モデル | %USERPROFILE%\.ollama\models(約4.6GB。OLLAMA_MODELS で場所を変えている場合はその場所) | ollama rm bge-m3 / ollama rm gemma3:4b |
| Ollama本体 | 設定 → アプリ → インストールされているアプリ(公式: 「Add or remove programs」) | アンインストール。先にモデルを ollama rm で消しておく(OLLAMA_MODELS で場所を変えていると、アンインストールではモデルが消えません) |
結果CSVは資料本文の一部を含みます。メール添付やクラウド共有をする前に中身を確認してください。
うまくいかないとき
urllib.error.URLError… Ollama が起動していません。スタートメニューから Ollama を起動するか、ollama listが動くか確認model "bge-m3" not found…ollama pull bge-m3をやり直す- 文字化け・
UnicodeDecodeError… 資料・CSVを UTF-8 で保存し直す(Excelでは「CSV UTF-8(コンマ区切り)」) - キーワード検索がほとんど当たらない … trigram は3文字未満の語では一致しません。質問が短すぎないか確認
- 回答生成が極端に遅い …
--chat-model gemma3:1bで小さいモデルに替える(精度は下がります)
公式ドキュメント(確認日 2026-10-18)
- Chroma Getting Started: https://docs.trychroma.com/docs/overview/getting-started
- Chroma 距離関数の設定(
configuration={"hnsw": {"space": "cosine"}}、既定は l2): https://docs.trychroma.com/docs/collections/configure - Chroma 事前計算した埋め込みの追加: https://docs.trychroma.com/docs/collections/add-data
- Ollama Windows(モデルの保存場所・アンインストール): https://docs.ollama.com/windows
- Ollama FAQ(モデルの保存場所、クラウド機能の無効化): https://docs.ollama.com/faq
- Ollama クラウドモデル: https://docs.ollama.com/cloud
- Ollama CLI(
ollama rm): https://docs.ollama.com/cli - Ollama 埋め込みAPI
/api/embed: https://docs.ollama.com/api/embed - Ollama チャットAPI
/api/chat: https://docs.ollama.com/api/chat - bge-m3(Ollama): https://ollama.com/library/bge-m3 (100以上の言語に対応と記載。日本語での精度は公式には示されていないので、このラボで自分で測ります)
- gemma3(Ollama): https://ollama.com/library/gemma3
- SQLite FTS5(trigram トークナイザ、bm25、rank): https://www.sqlite.org/fts5.html / trigram 追加の版: https://www.sqlite.org/releaselog/3_34_0.html
- Python on Windows(install manager): https://docs.python.org/3/using/windows.html
- Anthropic: Contextual Retrieval(BM25併用・top-kの比較): https://www.anthropic.com/news/contextual-retrieval
- Barnett ほか 2024 Seven Failure Points When Engineering a RAG System: https://arxiv.org/abs/2401.05856
このラボで確認できなかったこと
-
Ollama+bge-m3+gemma3:4b を使った通し実行(作成環境にOllamaが無いため)。スクリプトの検索・集計・CSV出力は、ダミーの埋め込みで動作を確認しました。
-
通し実行で失敗したら、まず埋め込みAPIだけを単独で試してください(
1024と出れば Ollama 側は正常で、問題はスクリプトか資料側です)。.\.venv\Scripts\python.exe -c "import json,urllib.request as u; r=u.urlopen(u.Request('http://localhost:11434/api/embed', data=json.dumps({'model':'bge-m3','input':['テスト']}).encode(), headers={'Content-Type':'application/json'})); print(len(json.load(r)['embeddings'][0]))" -
必要メモリ量の正確な下限(Ollamaの公式ページに gemma3:4b の必要メモリの記載なし)。
ふりかえりのメモ(この端末にだけ保存)