vault-rag 全体構成・仕様資料

Obsidian Vault の全ノートを AI が「意味で検索」できるようにする常駐サーバの設計と運用

作成: 2026-08-19 | 実装: 40_Tools/rag-server/ | 運用手順の正本: 同ディレクトリ README.md | 索引実測: 53,050 チャンク

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
キーワードの一致ではなく「意味の近さ」で探すのがポイント。「子供 AI 教育」と書いていなくても「娘とChatGPTで遊んだ」というノートが見つかる。
💡 つまり: Vault の5万超のノート断片が「意味で引ける索引」になっていて、Claude や Codex はそこに質問を投げるだけで過去の蓄積を思い出せる。

1.この仕組みは何をしているか

ゼロから自作したのではなく、PyPI の既製パッケージ obsidian-notes-rag (v1.1.2 に固定) を土台に、薄いラッパー3枚だけを足して自分の運用に合わせた構成です。役割は3つ。

  1. 常駐サーバ — PC にサーバを1本だけ常駐させ、全 AI セッション (Claude Code 並列複数本 + Codex) が http://127.0.0.1:8765/mcp を共有して検索する
  2. 夜間の自動索引更新 — 毎朝のバッチが「昨日変わったノートだけ」を索引し直す。放っておいても索引が古くならない
  3. ゴミの排除 — worktree の複製ノートや node_modules 内の README など、検索結果を汚す md を索引から確実に外す
💡 設計の癖 (Vault 全体と共通の哲学): 既製品を最小パッチで使う / 非破壊 (失敗したら古いデータを残す) / 夜間自動化 / 正常時は黙り、例外だけ報告する。

2.設計思想 4本柱

「なぜこの形にしたか」の核。それぞれ実害・実測に基づく判断。

① 共有1常駐

セッションごとにサーバを複製起動する方式 (stdio) をやめ、HTTP サーバ1プロセスを全セッションで共有

理由: 並列5セッションで RAG プロセスが5ペア常駐し、PC 不安定化の「乗数」になっていた (2026-07-08 に切替)。

② ローカル埋め込み・課金なし

意味の数値化 (埋め込み) は PC 内の Ollama + bge-m3 モデルで実行。外部 API を使わない。

理由: Vault 全文5万チャンク超を外部に送らない (機密) + API 課金ゼロで毎晩回せる (コスト)。

③ 夜間増分 + fail-open

毎朝、変更されたノートだけを再索引。失敗しても他の朝バッチを止めず、報告1行だけ残して先へ進む。

理由: パッケージ標準は毎回全ノート再埋め込みで重すぎる。索引は「多少古くても止まらない」ことを優先。

④ 堅牢除外

パスに worktreesnode_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
読み方: 実線=データの流れ、点線=監視・注入などの裏方の働き。サーバ本体 (緑の中心) はあくまで既製パッケージで、ラッパーは「HTTP で1本にする」「除外を差し替える」ことだけをしている。
💡 つまり: どのセッションから検索しても、同じ1つのサーバ・同じ1つの索引に届く。索引の鮮度は夜間バッチが、サーバの生存はログオン時タスクと朝バッチが守っている。

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.2mcp<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
非破壊の要は ①→②→③ の順序。「新しいのが作れたと確認してから古いのを消す」ので、途中でクラッシュしても検索できる状態が壊れない。
💡 つまり: ノートを書く → 寝る → 朝には検索に載っている。人間がやることは何もない。結果は毎朝の brief に1行だけ届き、異常時だけ NG 行が出る。

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
観点BeforeAfter
プロセス数セッション数だけ複製 (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.tomlextra_exclude_patterns には頼らないこと。
💡 つまり: worktree (Vault の作業用複製が30本ある) をうっかり索引すると同じノートが30重に登録される。除外はこの事故の再発防止装置。

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.pyEXCLUDED_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本あり、索引すると同一ノートの多重登録を起こすため除外対象。