0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Ollamaに自分の情報を持たせる③ ― 実資料をmetadataで整理し、安全に再indexできるRAGへ

0
Posted at

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の整合性検査

全体は次のようになります。

ChatGPT Image 2026年9月5日 19_15_04.png


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

流れは次です。

ChatGPT Image 2026年9月5日 19_21_14.png

大事なのは、最初に現在の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へ進みます。


参考資料


0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?