Ollamaに自分の情報を持たせる③ ― 実資料をmetadataで整理し、安全に再indexできるRAGへ
前回のOllamaに自分の情報を持たせる②は、ollama_private_knowledge_rag_r002でRAGの評価基盤を追加しました。
単に「期待した資料が検索できた」という確認だけではなく、
Hit@1
Hit@3
Recall@K
MRR
でRetrievalを測り、実際のOllama回答についても最低限のGeneration回帰を行えるようにしました。
ここまでで、
RAGが動く
↓
RAGを評価できる
ところまでは来ています。
次に困るのは、サンプル3文書ではなく、本当に使いたい設計書やREADMEを増やしたときです。
たとえば、DatasetVersionという言葉が複数のプロジェクトにあったり、r002とr003の設計書を両方残していたりすると、Vectorの類似度だけでは別の資料が上位に入ることがあります。
また、r002までは再index時にCollectionを作り直していました。小さなローカルRAGならこれでも十分ですが、資料が増えてくると、indexを作り直している途中の扱いも気になります。
そこで③では、ollama_private_knowledge_rag_r003として次を追加します。
参考のために、ZIPファイルを置いておきますので、ダウンロードして活用してください。
Markdownへmetadataを付ける
↓
project / type / versionで検索対象を絞る
↓
新しいQdrant Collectionへindexを作る
↓
完成してからaliasを切り替える
LoRAは今回もまだ入れません。
先に「自分の資料をきちんと管理して検索できる状態」を作ります。
r003で追加するもの
今回の変更は大きく6つです。
1. YAML Front Matter
2. Qdrant payload filter
3. payload index
4. Blue/Green再index
5. Knowledge inventory
6. 評価質問とmetadataの整合性検査
全体は次のようになります。
1. なぜmetadataが必要なのか
r001とr002では、主に次の情報をQdrantへ保存していました。
source
heading
text
chunk_index
サンプルを動かすにはこれで十分です。
ただ、実資料を入れ始めると、少し足りません。
例えば次のような構成を考えます。
knowledge/
├── flood_platform/
│ ├── architecture.md
│ ├── api.md
│ └── operation.md
│
├── gis_mcp/
│ ├── architecture.md
│ └── operation.md
│
└── old/
└── flood_platform_r002.md
ここで、
「operationの再index方法は?」
と質問すると、内容が似ていればflood_platformとgis_mcpの両方が候補になります。
さらに旧版の資料まで残していると、現在使いたい版とは違う説明が混ざる可能性があります。
そこで、Vector検索だけに任せず、文書へ次の属性を付けます。
project
どのプロジェクトか
document_type
design / manual / guide など
version
r002 / r003 など
tags
river / rag / qdrant など
これを検索前の条件として使います。
2. MarkdownへYAML Front Matterを付ける
r003ではMarkdown先頭へYAML Front Matterを書けるようにしました。
---
project: flood-analysis
document_type: design
version: r003
tags:
- river
- rag
- architecture
---
# 洪水解析システム設計
ここから本文です。
本文と管理情報を分けておくのがポイントです。
RAGへ登録するときは、Front Matterを本文から取り除きます。
Markdown
│
├─ Front Matter
│ ├─ project
│ ├─ document_type
│ ├─ version
│ └─ tags
│
└─ 本文
↓
Chunk化
LLMへはYAML設定そのものを資料本文として渡しません。
検索条件として使う情報と、人が読む本文を混ぜないようにしています。
3. Front Matterがない既存資料もそのまま使える
過去に作ったMarkdownすべてへ、いきなりFront Matterを付けるのは大変です。
そのためr003では、Front Matterなしでも読み込めます。
例えば、
knowledge/
└── flood_analysis/
└── README.md
なら、既定値は次のようになります。
project = flood_analysis
document_type = document
version = unversioned
tags = []
先頭ディレクトリ名をprojectに使います。
まず既存資料を動かし、重要な文書からFront Matterを追加していくこともできます。
4. index:falseで下書きを除外する
knowledge/には置いておきたいけれど、まだ検索させたくない資料もあります。
その場合は、
---
index: false
---
# 作業中メモ
とします。
ファイルを別ディレクトリへ移動しなくても、index対象から外れます。
実装では、読み込み時点で除外します。
# 作業途中のメモをknowledge配下へ置くことはあるため、index:falseなら
# ファイルを削除せず検索対象から外す。Front Matter自体はLLMへ渡さない。
if not metadata["index"]:
continue
こういう処理は、コメントに「何をしているか」だけでなく、「なぜそうしているか」も残しています。
5. metadataは全Chunkへ付ける
文書単位のmetadataは、分割後のすべてのChunkへ引き継ぎます。
architecture.md
project = flood-analysis
version = r003
↓
┌─────────────────────┐
│ Chunk 0 │
│ project=flood... │
│ version=r003 │
└─────────────────────┘
┌─────────────────────┐
│ Chunk 1 │
│ project=flood... │
│ version=r003 │
└─────────────────────┘
Qdrantの検索単位は文書ではなくPoint、今回の場合はChunkです。
そのため、metadataを文書の先頭Chunkだけへ付けてもfilterには使えません。
DocumentChunkへmetadataを持たせ、Qdrant登録時にpayloadへ展開します。
payload={
"text": chunk.text,
"source": chunk.source,
"title": chunk.title,
"heading_path": list(chunk.heading_path),
"chunk_index": chunk.chunk_index,
**chunk.metadata,
}
6. Qdrantのpayload filterを使う
r003では検索APIへfiltersを追加しました。
例えば、sample_projectのr003だけを検索する場合です。
{
"question": "RAGの構成は?",
"top_k": 5,
"filters": {
"projects": ["sample_project"],
"versions": ["r003"]
}
}
さらに設計書だけへ絞ることもできます。
{
"question": "RAGの構成は?",
"filters": {
"projects": ["sample_project"],
"document_types": ["design"],
"versions": ["r003"]
}
}
今回使える条件は次です。
projects
document_types
versions
tags_any
同じ項目へ複数値を入れた場合はORです。
project = flood OR gis
別項目との間はANDです。
(project = flood OR gis)
AND
version = r003
tags_anyは、指定したtagのどれか1つが文書側にあれば対象にします。
Qdrantではpayloadに対してFieldConditionやMatchAnyを指定できます。
今回は検索結果を取得してからPythonで除外するのではなく、Qdrantへquery_filterとして渡します。
Qdrant公式ドキュメントでも、payload filterを使ってVector検索の対象を絞り込めます。
7. filterする項目だけpayload indexを作る
物理Collectionを作るときに、次のfieldへkeyword indexを作ります。
project
document_type
version
tags
コードは次の形です。
for field_name in (
"project",
"document_type",
"version",
"tags",
):
client.create_payload_index(
collection_name=generation,
field_name=field_name,
field_schema=models.PayloadSchemaType.KEYWORD,
wait=True,
)
何でもindexを付けるのではなく、filterに使う項目へ絞っています。
Qdrantのドキュメントでも、payload indexはfilterで使うfieldを対象にすることが勧められています。
8. CLIからfilter付きで質問する
JSONを毎回書くのは面倒なので、r003ではquery.pyを追加しました。
python scripts/query.py \
--project sample_project \
--version r003 \
'仕様書を変更したら何を更新する?'
設計書だけなら、
python scripts/query.py \
--project sample_project \
--document-type design \
--version r003 \
'RAGの構成は?'
検索結果だけを見る場合は、
python scripts/query.py \
--search-only \
--project sample_project \
--version r003 \
'RAGの構成は?'
とします。
前回から続けている方針どおり、回答だけを見るのではなく、まずRetrieval結果を確認できるようにしています。
9. Promptにもmetadataを残す
検索時にfilterしたmetadataは、Promptを組み立てるときにも残します。
[reference 1]
source: sample_project/architecture.md
heading: RAG Architecture > 構成
project: sample_project
document_type: design
version: r003
tags: ['sample', 'architecture', 'rag']
retrieval_score: 0.6123
content:
...
モデルから見ても、
どのprojectの
どの版の
どの種類の資料か
が分かる状態にします。
もちろん、metadataを書いただけでモデルの誤りがなくなるわけではありません。
それでも、旧版と新版を同じPromptへ混ぜるよりは、参照資料の区別がしやすくなります。
10. r002の再index方式を見直す
ここがr003でもう1つ大きく変えたところです。
r002では、ざっくり書くと次の方式でした。
現在のCollectionを削除
↓
新しく作る
↓
Chunkを登録
ローカルの小さなRAGでは分かりやすい方法です。
ただ、再index途中に問題が起きたとき、元のCollectionをすでに消しているのは少し気になります。
そこでr003では、Qdrant aliasを使います。
11. Qdrant aliasでBlue/Green再indexする
r003では、.envの、
QDRANT_COLLECTION=private_knowledge_r003
を物理Collection名ではなくalias名として使います。
実際のCollectionは再indexごとに作ります。
private_knowledge_r003__gen__20260902T..._a1b2c3d4
流れは次です。
大事なのは、最初に現在のCollectionを消さないことです。
まず新しい世代を完成させます。
Embedding生成
↓
新Collection
↓
upload
↓
件数確認
ここまで成功して初めてaliasを切り替えます。
Qdrantではaliasの複数変更を原子的に適用できます。
そのため、
旧alias削除
新alias作成
を同じalias updateへ入れています。
12. upload件数が合わなければ切り替えない
r003では、登録したChunk数とQdrantのPoint数をalias切替前に比較します。
uploaded_count = store.count_points(generation)
if uploaded_count != len(chunks):
raise RuntimeError(
f"uploaded point count mismatch: "
f"expected={len(chunks)} actual={uploaded_count}"
)
一致しなければ、新しい世代だけ削除します。
新世代作成
↓
upload
↓
件数不一致
↓
新世代削除
↓
現行aliasはそのまま
この順序は単体テストでも固定しました。
create
upload
count
activate
cleanup
件数不一致時は、
activateしない
ことを確認しています。
13. 古い世代を少し残す
aliasを切り替えたあと、古い物理Collectionを全部残し続けると容量を使います。
初期値は、
INDEX_KEEP_GENERATIONS=2
です。
つまり、基本的には、
現行世代
1つ前の世代
を残します。
容量が厳しければ1へ変更できます。
14. 現在どの世代を使っているか確認する
新しく、
GET /api/v1/index/status
を追加しました。
curl http://localhost:8000/api/v1/index/status
返り値は次の形です。
{
"alias_name": "private_knowledge_r003",
"active_collection": "private_knowledge_r003__gen__...",
"point_count": 7,
"generations": [
"private_knowledge_r003__gen__..."
]
}
「いま検索しているのがどの物理indexなのか」を確認できます。
15. Qdrantへ入れる前にknowledgeを点検する
実資料を増やしたら、すぐ再indexするのではなく、一度一覧を確認します。
docker compose \
-f compose.yaml \
-f compose.gpu.yaml \
exec -T api \
python scripts/inspect_knowledge.py \
--knowledge-dir /app/knowledge
今回のサンプルなら、次のような結果になります。
knowledge inventory: documents=3 chunks=7
projects={'sample_project': 3}
document_types={'design': 1, 'guide': 1, 'manual': 1}
versions={'r003': 3}
文書ごとの情報も出します。
sample_project/README.md
project=sample_project
type=guide
version=r003
sample_project/architecture.md
project=sample_project
type=design
version=r003
結果は、
evaluation/results/knowledge_inventory.json
にも残します。
大量に登録してからmetadataの付け忘れに気付くより、登録前に確認する方が直しやすいです。
16. 評価質問にもfilterを持たせる
実運用では、評価も普段の検索条件と同じにする必要があります。
r003のquestions.jsonlでは、次のようにしました。
{
"id": "Q002",
"question": "質問から回答までのRAG処理の流れを説明してください。",
"expected_sources": [
"sample_project/architecture.md"
],
"top_k": 5,
"required_hit_at": 3,
"filters": {
"projects": ["sample_project"],
"versions": ["r003"]
}
}
Retrieval評価だけ別projectまで検索している状態では、実際の使い方と数字が合いません。
そのため評価ケースにもfilterを保存します。
17. 評価設定の間違いも先に確認する
資料を更新していると、
Markdownをr003へ変更
↓
evaluation側はversion=r002のまま
ということが起こります。
この状態では、検索精度が悪いわけではないのに評価がFAILします。
そこで、
python scripts/check_evaluation_coverage.py
を追加しました。
実際にはAPIコンテナ内で、
docker compose \
-f compose.yaml \
-f compose.gpu.yaml \
exec -T api \
python scripts/check_evaluation_coverage.py \
--knowledge-dir /app/knowledge
と実行します。
確認するのは、
expected sourceが存在するか
↓
そのsourceが指定filterの対象になるか
です。
検索品質の問題と、評価データ設定の問題を分けます。
18. 実サービスでmetadata filterをテストする
単体テストだけでなく、実際のOllama + Qdrantを使う回帰も追加しました。
テスト内では、同じ検索語を持つ2つの文書を作ります。
project alpha
FILTER_REGRESSION
ALPHA_ONLY_MARKER
project beta
FILTER_REGRESSION
BETA_ONLY_MARKER
まず、
project=alpha
version=r003
で検索します。
結果がalphaだけになることを確認します。
次に、
tags_any=green
でbetaだけが返ることを確認します。
実行は、
docker compose \
-f compose.yaml \
-f compose.gpu.yaml \
exec -T api \
python scripts/run_metadata_filter_regression.py
です。
通常のRAGとは別aliasを使うので、普段のindexは変更しません。
19. Blue/Green切替も実サービスで確認する
run_index_regression.pyもr003用に更新しました。
初期版
↓
generation A
↓
alias → A
文書更新
↓
generation B
↓
alias → B
↓
Aの古い本文は検索側から見えない
文書削除
↓
generation C
↓
alias → C
↓
削除文書は現行世代に存在しない
さらに、保持世代数を超えていないことも確認します。
20. r003のruntime検証をまとめて実行する
r003では次を1本にまとめました。
./scripts/run_r003_validation.sh
中では、
Knowledge inventory
↓
Evaluation coverage
↓
Retrieval評価
↓
Generation評価
↓
Blue/Green再index回帰
↓
metadata filter回帰
↓
GPU実使用確認
↓
index status
の順で確認します。
全部通れば、
R003_RUNTIME_VALIDATION=PASS
と表示します。
21. r003の単体テスト
今回、Front MatterとBlue/Green関係のテストを追加しました。
こちらで確認した範囲では、
pytest
24 passed
です。
追加した主な内容は、
Front Matterの分離
metadata既定値
index:false
不正metadataの拒否
filterのAND / OR
filter値の正規化
knowledge inventory
alias切替前のPoint件数確認
失敗時にaliasを切り替えない
です。
22. 実資料はまず10~30文書から始める
r003で実資料を入れる準備はできました。
ただ、ここでいきなり数千ファイルを入れる予定はありません。
まずは、よく使う資料を10~30文書程度選びます。
設計書
README
運用手順
API仕様
障害対応メモ
ソース構成説明
このくらいから始めます。
それぞれへFront Matterを付けます。
project
document_type
version
tags
その後、評価質問を30~50件程度作ります。
実資料 10~30文書
↓
Front Matter整理
↓
Knowledge inventory
↓
評価質問 30~50件
↓
Retrieval評価
↓
Generation評価
この順なら、資料を増やしたことで何が悪くなったのか追いやすくなります。
23. まだLoRAへ進まない理由
ここまで来ても、まだLoRAは使っていません。
理由は、LoRAで直すべき問題とRAGで直すべき問題を分けたいからです。
例えば、
必要な資料が検索されない
なら、LoRAではなくRetrieval側です。
project filterが足りない
Chunkが大きすぎる
Top-Kが合っていない
質問と資料の表現差が大きい
などを確認します。
一方、
必要な資料は毎回正しく取れている
それでも回答の組み立て方が安定しない
のであれば、LoRA候補です。
Retrievalは正しい
↓
Promptにも正しい資料が入っている
↓
それでも回答傾向が合わない
↓
LoRAを検討
この順番は変えません。
24. r003での現在地
ここまでのシリーズを整理すると、次のようになります。
①
SYSTEM / RAG / LoRAの役割を整理
↓
②
GPUでSYSTEM + RAGを動かす
Retrieval / Generationを評価
↓
③
実資料向けmetadata
Qdrant filter
Blue/Green再index
↓
次
実資料を増やして検索精度を調整
↓
必要ならLoRA
「自分の情報をモデルに持たせる」と言っても、モデルの重みへ全部覚えさせる必要はありません。
今の段階では、
SYSTEM
基本ルール
RAG
現在の資料
metadata
どの資料を使うか
Qdrant alias
indexを安全に更新する
というところまで分けられています。
まとめ
r003では、RAGのモデルを大きくするのではなく、資料の扱いを整えました。
今回追加したのは、
YAML Front Matter
metadata filter
payload index
Blue/Green再index
Knowledge inventory
evaluation coverage
です。
特に重要なのは、
似ている資料を全部検索する
のではなく、
対象project
対象version
対象文書種別
↓
その中でVector検索する
構成にしたことです。
そして再indexも、現在のCollectionを先に消すのではなく、新しい世代を完成させてからaliasを切り替えます。
これで、サンプルを動かすRAGから、実際の設計書やREADMEを継続して入れていくRAGへ少し近づきました。
次回は、まずは実資料を10~30文書程度入れ、評価質問を30~50件へ増やします。
その結果を見ながら、Chunk、Top-K、score、filterを調整します。
それでも残る「回答の癖」や「専門タスクの判断方法」を切り出せたところで、LoRAへ進みます。
参考資料
- Ollama - Embeddings
- Ollama - Generate embeddings API
- Ollama - Chat API
- Qdrant - Filtering
- Qdrant - Indexing
- Qdrant - Collections / Aliases

