0.そもそも RAG とは
前提知識ゼロから、この資料を読むのに必要な分だけ。
RAG (Retrieval-Augmented Generation) は「AI が答える前に、手元の資料棚から関連ページを探して読んでから答える」仕組みです。図書館に例えると、AI が司書に「子育てとAIについて書いたノートあったっけ?」と聞くと、司書が書庫から関連ノートの該当ページだけ抜き出して渡してくれる——その司書と書庫にあたる部分が vault-rag です。
これを機械的に実現するために、3つの下ごしらえをしています。
flowchart LR
subgraph Prep["下ごしらえ (索引づくり)"]
N["ノート (.md)"] --> C["① チャンク分割
(検索しやすい長さに切る)"]
C --> E["② 埋め込み
(意味を数値ベクトルに変換)"]
E --> DB[("③ ベクトルストアに保存
(意味で引ける書庫)")]
end
subgraph Search["検索するとき"]
Q["質問文"] --> QE["同じ方法で数値化"]
QE --> M["ベクトルが近い =
意味が近いチャンクを探す"]
DB --> M
M --> R["該当ノートの断片を返す"]
end
1.この仕組みは何をしているか
ゼロから自作したのではなく、PyPI の既製パッケージ obsidian-notes-rag (v1.1.2 に固定) を土台に、薄いラッパー3枚だけを足して自分の運用に合わせた構成です。役割は3つ。
- 常駐サーバ — PC にサーバを1本だけ常駐させ、全 AI セッション (Claude Code 並列複数本 + Codex) が
http://127.0.0.1:8765/mcpを共有して検索する - 夜間の自動索引更新 — 毎朝のバッチが「昨日変わったノートだけ」を索引し直す。放っておいても索引が古くならない
- ゴミの排除 — worktree の複製ノートや node_modules 内の README など、検索結果を汚す md を索引から確実に外す
2.設計思想 4本柱
「なぜこの形にしたか」の核。それぞれ実害・実測に基づく判断。
① 共有1常駐
セッションごとにサーバを複製起動する方式 (stdio) をやめ、HTTP サーバ1プロセスを全セッションで共有。
理由: 並列5セッションで RAG プロセスが5ペア常駐し、PC 不安定化の「乗数」になっていた (2026-07-08 に切替)。
② ローカル埋め込み・課金なし
意味の数値化 (埋め込み) は PC 内の Ollama + bge-m3 モデルで実行。外部 API を使わない。
理由: Vault 全文5万チャンク超を外部に送らない (機密) + API 課金ゼロで毎晩回せる (コスト)。
③ 夜間増分 + fail-open
毎朝、変更されたノートだけを再索引。失敗しても他の朝バッチを止めず、報告1行だけ残して先へ進む。
理由: パッケージ標準は毎回全ノート再埋め込みで重すぎる。索引は「多少古くても止まらない」ことを優先。
④ 堅牢除外
パスに worktrees や node_modules 等を含む md を索引から排除。
理由: worktree (作業用の Vault 複製) を索引すると同じノートが何重にも登録され、検索結果が濁る。
3.全体構成図
登場人物は5グループ。左上から「使う人 (AIセッション)」→「常駐サーバ」→「データ層」、下段が「夜間バッチ」と「起動経路」。
graph TB
subgraph Clients["クライアント層 (全セッションが共有)"]
C1["Claude Code
セッション A"]
C2["Claude Code
セッション B"]
C3["Codex / 他の
MCP クライアント"]
end
subgraph Server["常駐サーバ (1プロセスのみ)"]
S["serve_http.py
FastMCP streamable-http
127.0.0.1:8765/mcp"]
P["obsidian-notes-rag 1.1.2
(PyPI・バージョン固定)"]
X["rag_common.py
堅牢除外イテレータ"]
S --> P
X -. "monkeypatch で
VaultIndexer に注入" .-> P
end
subgraph Data["データ層 (LOCALAPPDATA)"]
VS[("ベクトルストア
SQLite / 約53,000 chunks")]
ST["incremental_state.json
(mtime+size の控え帳)"]
CF["config.toml
(設定の正本)"]
end
O["Ollama bge-m3
(ローカル埋め込み・課金なし)"]
MD["Vault 内 *.md
(除外適用後)"]
subgraph Batch["夜間バッチ (毎朝)"]
D["scheduled_review.bat daily"]
I["rag_index.py
増分索引"]
D --> I
end
subgraph Boot["起動経路"]
T["Task Scheduler
VaultRagServer (ログオン時)"]
B["start_rag_server.bat
(ポート生存確認つき)"]
T --> B
end
C1 & C2 & C3 -->|"HTTP (MCP)"| S
P <--> O
P <--> VS
MD --> X
I <--> O
I <--> VS
I <--> ST
D -. "生存確認" .-> B
B -. "落ちていれば起動" .-> S
4.コンポーネント一覧
実装ファイルはたった4つ。それぞれ役割が1つずつ。
| ファイル | 役割 | 設計上のポイント |
|---|---|---|
serve_http.py |
常駐サーバ本体。パッケージの FastMCP サーバを HTTP (8765) で起動 | ノート走査関数をクラスごと除外つき版に差し替える (monkeypatch)。これによりサーバ内の reindex ツール経由でも除外が効く。ログは server.log |
rag_index.py |
夜間の増分索引スクリプト | ファイルの「更新時刻+サイズ」の控え帳 (sidecar) と突合して変更検出。処理順は「新しい埋め込みを作ってから→古いのを消す」(失敗時は古い索引が残るので検索が壊れない)。100件ごとに途中保存。--full で全再構築 |
rag_common.py |
除外ルールを持つ共通のノート走査関数 | サーバと夜間バッチの両方がこの1つを使う (除外基準が二重管理にならない) |
start_rag_server.bat |
起動ランチャー | ポート 8765 が生きていれば何もしない (二重起動防止)。依存は obsidian-notes-rag==1.1.2 と mcp<2 にピン留め |
クライアントから見える顔 (MCP ツール5種)
AI セッション側からは、次の5つの道具として見えます。
| ツール | 何ができるか |
|---|---|
search_notes | 質問文で意味検索 (いちばん使う) |
get_similar | 「このノートに似たノート」を探す |
get_note_context | ヒットしたノートの前後文脈を取得 |
get_stats | 索引の統計 (チャンク数など) |
reindex | サーバ内から再索引 (除外パッチ適用済) |
5.データフロー
5.1 検索するとき (数秒で完結)
sequenceDiagram
participant CC as Claude セッション
participant S as vault-rag サーバ (8765)
participant O as Ollama (bge-m3)
participant DB as ベクトルストア (SQLite)
CC->>S: search_notes("質問文")
S->>O: 質問文を数値ベクトルに変換
O-->>S: ベクトル
S->>DB: 意味が近いチャンクを検索
DB-->>S: 上位チャンク + ノートパス
S-->>CC: 検索結果 (抜粋つき)
Note over CC,S: 全セッションが同一サーバ・同一索引を共有
5.2 夜間の索引更新 (毎朝の daily バッチ)
flowchart TD
A["毎朝: scheduled_review.bat daily"] --> B["rag_index.py 起動"]
B --> C{"Ollama は応答する?
(埋め込みテスト1回)"}
C -- NG --> Z["報告1行だけ残して終了
(他の朝バッチは止めない)"]
C -- OK --> D["Vault の md を走査
(除外ルール適用)"]
D --> E["控え帳 (更新時刻+サイズ) と突合"]
E --> F["消えたファイル
→ 索引からも削除"]
E --> G["変更・新規ファイルを1件ずつ処理"]
G --> H["① 新チャンクを埋め込み"]
H -- 成功 --> I["② 旧チャンクを削除"]
I --> J["③ 新チャンクを登録"]
J --> K["控え帳を更新
(100件ごとに途中保存)"]
H -- 失敗 --> L["旧チャンクを温存
エラーだけ記録して次へ"]
K --> M["daily brief に結果1行
「rag-index: OK 更新n件/削除n件/…」"]
L --> M
6.Before / After (2026-07-08 の転換点)
この日を境に「セッション毎の複製」から「共有1常駐」へ切り替えた。この資料でいちばん大事な意思決定。
graph LR
subgraph Before["Before: stdio 型 (セッション毎に複製)"]
A1["セッション 1"] --> R1["RAG プロセス 1"]
A2["セッション 2"] --> R2["RAG プロセス 2"]
A3["セッション N"] --> R3["RAG プロセス N"]
end
subgraph After["After: 共有 HTTP (1常駐)"]
B1["セッション 1"] --> H["常駐サーバ
127.0.0.1:8765"]
B2["セッション 2"] --> H
B3["セッション N"] --> H
end
Before -.->|"2026-07-08 切替"| After
| 観点 | Before | After |
|---|---|---|
| プロセス数 | セッション数だけ複製 (PC 不安定化の乗数) | 常に 1 |
| 索引の鮮度 | 手動 reindex 頼み (1ヶ月放置もあった) | 夜間増分で毎朝更新 |
| worktree の多重登録 | 除外なし → 同じノートが重複索引 | パーツ一致除外で防止 |
| 接続設定 | stdio 型 (セッションが各自起動) | http 型 (既存サーバへ接続するだけ) |
7.除外ルール
「Vault の md なら何でも索引する」ではなく、ノイズ源を名指しで外している。
パスのどこかに次のディレクトリ名が含まれる md は索引しない (.egg-info で終わる名前も除外):
パッケージ既定と同等
.obsidian .trash .venv node_modules __pycache__ .git build dist attachments
追加 (ハーネス/コード層)
.claude .codex .company .agents .cache .playwright-mcp .pytest_cache .wrangler _codex worktrees .next .netlify target test-results
Path.match("dir/**") 方式だが、Python 3.12 では ** が深い階層に効かないことを実測済み。だから「パスを構成要素に分解して名前が含まれるか見る」パーツ一致方式を自前実装した。config.toml の extra_exclude_patterns には頼らないこと。8.運用
平常時にやることはゼロ。覚えておくのは「手動起動の bat」と「全再構築コマンド」の2つだけ。
| 操作 | 方法 |
|---|---|
| 起動 (自動) | ログオン時に Task Scheduler VaultRagServer が起動 + 毎朝の daily バッチが生存確認 |
| 起動 (手動) | 40_Tools\rag-server\start_rag_server.bat をダブルクリック (生きていれば何もしないので安全) |
| 索引更新 (自動) | scheduled_review.bat daily が毎朝増分実行。結果は brief の rag-index: 行 |
| ログ | 40_Tools/rag-server/server.log |
| 設定の正本 | %LOCALAPPDATA%\obsidian-notes-rag\config.toml (vault パス / プロバイダ / モデル) |
| 増分の控え帳 | %LOCALAPPDATA%\obsidian-notes-rag\obsidian-notes-rag\incremental_state.json (消すと次回は全ファイル再埋め込みになるだけ。壊れはしない) |
全再構築 (索引を作り直したいとき)
uv run --no-project --with obsidian-notes-rag==1.1.2 --with "mcp<2" python 40_Tools/rag-server/rag_index.py --full
9.既知の障害モードと対処
全部いちどは実際に起きた (または実測で確認した) もの。症状から引く。
| 症状 | 原因 | 対処 |
|---|---|---|
| MCP 接続エラーが出る | サーバが停止している | start_rag_server.bat を叩く (二重起動はしない設計) |
| 検索も索引もできない | Ollama が起動していない | Ollama を起動する (SessionStart hook が警告してくれる) |
| サーバが無言で死ぬ (起動即クラッシュ) | 依存の mcp が 2.x に上がり内部モジュールが消えた (2026-08-01 に実害) |
--with "mcp<2" のピン留めを外さない (bat と全再構築コマンド両方に付与済) |
| 深夜だけ検索が失敗する | 夜間索引の実行中は SQLite が短時間ロックされる | 仕様として許容 (深夜のため)。再実行すれば通る |
| 除外したはずの md が索引に入る | Path.match("dir/**") 方式の設定に頼っている |
rag_common.py の EXCLUDED_PARTS に名前を追加する |
| パッケージ更新後に動かない | monkeypatch (走査関数の差し替え) の互換が切れた | ==1.1.2 固定の見直し時は iter_markdown_files の互換を必ず確認 |
10.用語集
- RAG (Retrieval-Augmented Generation)
- AI が答える前に、関連資料を検索して読んでから答える方式。「調べてから答える AI」。
- チャンク
- ノートを検索しやすい長さに切った断片。この Vault では約53,000個 (2026-08-19 実測)。
- 埋め込み (Embedding)
- 文章の「意味」を数値の並び (ベクトル) に変換すること。意味が近い文章ほど近い数値になるので、距離で検索できる。
- ベクトルストア
- 埋め込みを保存し「近いものを探す」検索ができるデータベース。ここでは SQLite ベース。
- Ollama / bge-m3
- Ollama = PC 内で AI モデルを動かすツール。bge-m3 = そこで動かしている埋め込み専用モデル (多言語対応・日本語OK)。外部 API を使わないので課金ゼロ。
- MCP (Model Context Protocol)
- AI セッションに外部の道具 (検索など) を差し込むための共通規格。vault-rag はこの規格のサーバとして振る舞う。
- stdio 型 / http 型
- MCP サーバへのつなぎ方2種。stdio = セッションが自分専用のサーバを起動する (複製が増える)。http = すでに動いているサーバに接続しに行く (共有できる)。
- sidecar (控え帳)
- 本体データベースとは別に持つ小さな状態ファイル。ここでは各ファイルの「更新時刻+サイズ」を記録し、変更検出に使う。
- monkeypatch
- 既製パッケージのソースを書き換えずに、実行時に一部の関数だけ自前のものに差し替える技法。パッケージ更新に弱いのでバージョン固定とセットで使う。
- fail-open
- 失敗しても全体を止めない方針。夜間索引が失敗しても他の朝バッチは走り、報告1行だけが残る。
- worktree
- git の機能で作る作業用の複製フォルダ。Vault に約30本あり、索引すると同一ノートの多重登録を起こすため除外対象。