ハイライト
- 各種ツールの公式ドキュメントを収集 → Obsidian Vault化 → グラフ化 → RAGにした個人プロジェクトの記録です。
- 6製品・約1,900ページを取り込み、BM25+グラフ+ベクトルのハイブリッド検索をtorch非依存(量子化ONNX)で実装。
- 評価ハーネスで品質を数値化: hit@1=94% / hit@3=100% / MRR=0.97(31ケース)。
- 設計の肝は「検索はローカルの仕組みに寄せ、LLM(Claude)は最小限」。これにより弱いLLMやエージェント無しでも性能が保たれる「floor」を厚くしました。
- 記事後半で、他のObsidian×RAG方式(Claude直読み / Obsidian公式検索・REST API / MCP)も紹介します(公式doc/README準拠の整理で、実測での優劣比較ではありません)。
全工程を Claude Code(CLI)とペアで実装しました。スクリプトは Python、検索はローカル完結(検索自体にLLMは使わない)です。
想定読者: 自分のドキュメント/ナレッジをRAG化したい人、Obsidian×AIの構成を検討中の人、
「軽量・ローカル・測定できる」RAGに興味がある人。
環境: Windows + Python 3.11 + CPUのみ(GPU不要)。依存は requests / pyyaml / networkx /
rank-bm25 / snowballstemmer / fastembed と軽量。
なぜ「Obsidian → RAG」なのか
最近、公式ドキュメントをMarkdownで直接配布するサイトが増えています(llms.txt 配信、GitHubの docs/ 等)。これを使えば、
- 人間向け: Obsidianのグラフビューで関連を辿れる「取扱説明書」
- AI向け: そのままRAGの素材になる構造化Markdown
を一度に作れます。Vaultを唯一の正として、そこからグラフもRAGも生成する、という方針にしました。
取得(fetch) → 整形(normalize) → Vault(Obsidian) → グラフ(networkx)
↓
RAG検索(BM25+グラフ+ベクトル)
↓
評価(hit@1 / hit@3 / MRR)
作った順の記録(履歴)
1. 取得(fetch)— スクレイピングは最終手段
「スクレイピング」から始めたものの、結論は clone/Markdown配信を最優先でした。優先順は:
- tier1: git clone — docsがGitHubにMarkdownである(Obsidian, Hugging Face 等)
-
tier2: llms.txt配信 — サイトがLLM向けに
.mdを配る(Claude Code, Gemini, OpenClaw) - tier3: HTMLスクレイピング — 最終手段。robots.txt・規約を確認(今回は未実装)
製品ごとに sources.yaml に1ブロック書くだけで足せる形にしました。途中で出会った“製品固有の癖”の例:
- 巨大リポジトリ(Hugging Face transformers)→ sparse checkoutで
docs/だけ取得(全体~GBを回避、8.9MB/16秒) - Gemini →
.md.txt拡張子&タイトル省略の索引 - OpenClaw → 索引が素のページURL(
.mdを付けて取得)、各ページに既存frontmatter
2. 整形(normalize)— 形式の違いを1か所に閉じ込める
製品によって原形式が違う(MDX / 元Obsidian / doc-builder)ので、format で分岐:
-
obsidian形式 … 本文の[[WikiLink]]を温存、frontmatterに自前フィールドをマージ - それ以外 … Web Markdown変換(内部リンクのWikiLink化、コールアウト変換、ライセンスヘッダ除去 等)
内部リンクのWikiLink化は製品形式に依存しない汎用ロジックにしました:「ドキュメントホスト一致+既知ページへの末尾一致」で内部リンクを判定(相対 /en/x も絶対 https://host/.../x も対応、外部リンクは温存)。
3. グラフ化(networkx)
Vaultの [[WikiLink]] をパースして有向グラフ化(GraphML/JSON出力)。リンク解決は大文字小文字無視・frontmatter aliases・フォルダ末尾一致・製品をまたぐ同名は同製品優先。コードフェンスや画像添付、表の \| エスケープ、bashの [[ ... ]] などは除外。
結果: 1,908ノード / 9,276エッジ、孤立0。中心性(被参照数)で settings/mcp/hooks 等が核ページと判明し、これが後で検索のブーストに効きます。
4. RAG検索(rag_search)
最初は BM25+グラフ中心性+近傍ポインタ。設計の核は:
RAGの旨味は「膨大な資料から必要な箇所だけ取り出してコンテキストを縮小すること」。だから検索はローカル、Claudeは小さなチャンクだけ読む。
Vault全体をgrepしたりページを丸読みしません。検索が小チャンクを先に選ぶので、Claudeのトークン使用もコンテキストも最小です。
5. 精度改善(ここが本番)
テストで弱点を見つけては潰す、を繰り返しました。すべて仕組み側(後述のB)の改善です:
-
語幹化+ストップワード除去 …
quantize / quantization / quantizedを同じ語幹に。語形の揺れを吸収 -
同義語辞書(
synonyms.yaml)…forget→clear,money→cost,dangerous→destructiveを機械展開 -
frontmatter意図メタの取り込み …
description / summary / read_when / aliasesを検索テキストに(人間が書いた「何のページか/いつ読むか」を機械索引) - タイトル/パス一致ブースト・ナビ節(Related/See also)減点・MOC除外・製品スコープ
- ベクトル(bge-small)+RRF順位融合でハイブリッド化
6. 評価ハーネス(改善のOS)
「目視テスト」を再現可能な回帰テストに変えました。evals.yaml(質問→期待ページ)を採点し、hit@1/hit@3/MRRを種別・製品別に算出。以降の全変更を数値で採否判定します。
Phase0 ベースライン hit@1=85% / hit@3=100% / MRR=0.93
+ 見出しbreadcrumb → 89% / 0.94
+ TITLE_ALPHA 0.6→1.0 → 93% / 0.96 (評価でスイープして最適値を発見・27ケース時点)
(その後 GAS を追加し6製品・31ケースに拡張 → 現在 94% / 0.97)
hit@3=100%=正解が必ずtop3に入る。Claudeはtop-Kチャンクを読むので、エンドツーエンドの回答品質は実質完成水準です。
RAGのしくみ(要点)
「Claudeの賢さ」と「仕組み」を分ける(A/B分離)
このプロジェクトで一番大事にした視点です。Claudeを弱いLLMに替えても・エージェント無しでも変わらない部分(B=仕組み)を厚くする:
| 揺れの種類 | 例 | 担当 |
|---|---|---|
| 語形差 | quantize / quantization | 語幹化 … B(仕組み) |
| 同義語(ゼロ語彙) | forget→clear, money→cost | 同義語辞書 … B |
| 意図・別名 | 「automate work」→read_whenにその文言 | frontmatter索引 … B |
| 言い換え | go back→checkpoint/rewind | ベクトル+RRF … B(小モデル) |
| 残りの曖昧さ | 文脈依存の難問 | Claudeのキーワード選定・再定式化 … A |
→ 大半を仕組み(B)で吸収し、Claude(A)は「最後の曖昧さ」を埋めるラストワンマイルに縮小。floor(最低保証性能)が高い構成です。
スコアの式(概念)
最終 = base ×(1+中心性α·indeg) ×(1+タイトルα·一致率) ×(ナビ節なら 0.25)
base = BM25のみ: 生BM25
ハイブリッド: RRF(1/(K+BM25順位)·(1-α) + 1/(K+cos順位)·α)
RRF(Reciprocal Rank Fusion)を使うのがポイント。スコアの大小でなく順位で融合するのでスケール非依存。BM25が外しても、ベクトルが上位に置けば融合順位で救われます(線形合成より頑健、と評価で確認)。
実装の一部
トークン化(語幹化+ストップワード):
def tokenize(text: str) -> list[str]:
out = []
for t in TOKEN_RE.findall(text.lower()):
if t in STOPWORDS:
continue
out.append(_stem(t) if t.isalpha() else t) # snowball stemmer
return out
RRF融合:
def rrf_fuse(bm, cos, alpha, K=60):
rbm = rank_desc(bm) # 1=最良
rcos = rank_desc(cos)
return (1 - alpha) / (K + rbm) + alpha / (K + rcos)
埋め込みモデルは比較の上 bge-small-en-v1.5(67MB/384次元)を採用。英語コーパス+英訳クエリに最適・最軽量で、fastembed の量子化ONNXなのでtorch不要(torch ~500MBを後で削除できた)。CPU埋め込みが重いので製品ごとの遅延ビルドにしています。
他のObsidian×RAG方式との比較
「ObsidianをRAGに使う」には、自作以外にも選択肢があります。どれが優れているかは用途次第です。
⚠️ 前置き(重要): 以下の A〜C は筆者が実運用したものではなく、各ツールの公式ドキュメント/README/ソースを読んで整理した「こういう手法もあるらしい」という紹介です。実測での優劣比較ではありません。断定を避け、一次情報で確認できた範囲だけを書き、根拠は本節末の「参考ソース」にまとめました。最新仕様や実際の使用感は各自でご確認ください(筆者が実際に手を動かして測ったのは AとD だけです)。
A. Claude Code が Vault を直接読む(grep / Read)
- やり方: エージェントにVaultをgrepさせReadさせて答えさせる(本プロジェクトの出発点でもある)。
- 長所: セットアップほぼ不要。小さなVaultなら十分。
- 短所: ページを丸ごと読むとコンテキストが膨らみRAGの旨味(縮小)が薄れる。関連度順に並べる仕組みは無い。大規模Vaultでトークンを使いがち。賢いLLMを前提にしやすい。
B. Obsidian の検索(公式CLI / REST API)を使わせる
-
やり方: Obsidianには公式CLIがあり(公式ヘルプ obsidian.md/help/cli。Obsidian 1.12系で導入=公開版は 1.12.4/2026-02。
obsidian search query="..."や、任意コマンドに--copyを付けて出力をクリップボードへ。公式ページは用途の一つに「agentic tools(エージェント型ツール)にvaultへのアクセスを与える」ことを挙げている)、また Local REST API プラグイン(全文検索/search/simple/とJsonLogicのメタ検索)をClaude Codeから叩く、といった構成が考えられます。 -
長所(公式docベース): 検索オペレータが豊富。公式ヘルプ Search には
line:/block:/section:(見出し間)/task:、file:/path:/tag:、正規表現(/.../)が載っており、一致の“範囲”を行・ブロック・セクション単位に絞れる(=マッチ範囲はサブノート粒度)。 -
短所(同上): Obsidianアプリの常駐が前提(CLI/プラグインとも本体に紐づく)。デスクトップ検索UIの並び替えはファイル名・更新/作成日時のみで、関連度順の選択肢が無く、結果はファイル単位でまとまる(公式docのソート一覧より)。意味検索・同義語・語幹化は公式docに記載が見当たらない。
- ※補足: 一致の範囲は行/ブロック/セクションに絞れるが、返る単位はファイルで、RAGが欲しい「関連度順に並んだ小チャンク」がそのまま出るわけではない。ただし Local REST API の
/search/simple/はソース上、各結果に関連度スコア(score)を付けて降順ソートで返すため、「スコアが一切無い」わけではない(あくまでファイル単位)。
- ※補足: 一致の範囲は行/ブロック/セクションに絞れるが、返る単位はファイルで、RAGが欲しい「関連度順に並んだ小チャンク」がそのまま出るわけではない。ただし Local REST API の
C. MCPサーバ経由(mcp-obsidian など)
- やり方: mcp-obsidian(READMEによればLocal REST APIプラグイン経由)や cyanheads/obsidian-mcp-server 等のMCPサーバで、検索・読み書きをツールとしてClaude等に渡す。
- 長所(READMEベース): MCPクライアント(Claude/Cursor等)から統合しやすい。書き込み・編集に対応するものもある。
-
検索粒度: MCPは統合の“器”で、検索粒度は下地のエンジン次第のようだ。
-
REST API系(下地はLocal REST API)→ OpenAPI仕様上、
/search/simple/はファイル単位+一致周辺のスニペット(contextLengthで長さ調整・既定100)を、内蔵fuzzyの関連度スコア付きで返す。/search/(JsonLogic)でメタデータ絞り込み。セマンティック(ベクトル)検索は仕様・ソースに見当たらない。 -
意味検索系(obsidian-mcp-tools + Smart Connections)→ Smart Connectionsの
/search/smartに問い合わせる。ソースの型定義(SmartBlock/vec/行範囲)からブロック単位のベクトル類似とみられる(READMEは「意味・文脈で検索」までの表現で、"block-level"は明記なし)。- ⚠️ 訂正メモ: 連携先の obsidian-mcp-tools(MCPブリッジ側)は2026年5月13日にアーカイブ(read-only)された(GitHubのアーカイブ表示・最終リリース v0.2.33 より)。一方、Smart Connections本体(
brianpetro/obsidian-smart-connections)はアーカイブされておらず、2026-06にv4.5.3をリリースするなど現役(2026-07時点)。両者は別プロジェクトなので混同しないこと。
- ⚠️ 訂正メモ: 連携先の obsidian-mcp-tools(MCPブリッジ側)は2026年5月13日にアーカイブ(read-only)された(GitHubのアーカイブ表示・最終リリース v0.2.33 より)。一方、Smart Connections本体(
-
REST API系(下地はLocal REST API)→ OpenAPI仕様上、
- 短所: Obsidian常駐+プラグイン+MCPサーバの三層になりがち。検索の質は下地任せで、BM25+グラフ+ベクトルのような複合ランキングを器側で持つわけではない。
D. 本記事の自作グラフRAG
- やり方: Vaultをチャンク化し、BM25+グラフ中心性+ベクトルをRRF融合。ローカル完結・評価駆動。
- 長所: チャンク粒度でコンテキストを絞れる&関連度順で返す。キーワードと意味のハイブリッド。torch非依存で軽量、Obsidian常駐不要。検索段でLLMを使わない(機密×ローカルと相性が良い)。
-
短所: 全部自作=保守は自分持ちで粗も残る(例:
modelがmodel_doc/接頭辞に過剰一致するアーティファクト)。Obsidianの成熟した検索やインデックス速度には及ばない。毎クエリBM25再構築なので大規模では最適化が要る。同義語辞書・評価ケースは手で育てる前提。- ※自作を持ち上げすぎないための注記: 上で B/C の弱点に挙げた「関連度スコア」「意味検索」は、B/C も一定は持っています(RESTの
/search/simple/はfuzzyスコアを返す、Smart Connections はベクトル検索)。Dの違いは「BM25+グラフ中心性+ベクトルをRRFで束ね、チャンク単位で返す」という組み方であって、「Dだけが関連度順・意味検索を出せる」という意味ではありません。 - ※「evalで測れる」も長所に数えません。A/B/Cも同じevalで測れます(本記事はDで実装しただけ)。
- ※自作を持ち上げすぎないための注記: 上で B/C の弱点に挙げた「関連度スコア」「意味検索」は、B/C も一定は持っています(RESTの
対照表(特徴の整理)
⚠️ この表は自作(D)の当人が書いており、A〜Cは実測ではなく公式doc/README/ソースの読み取りに基づきます。優劣の採点ではなく確認できた特徴の対照にとどめ、Dに甘くならないよう努めました。各行の根拠は表下の「参考ソース」を参照してください。
| 観点 | A. Claude直読み | B. 公式CLI/REST検索 | C. MCP | D. 自作グラフRAG |
|---|---|---|---|---|
| 検索の方式 | grepと本文読み | 内蔵fuzzy+オペレータ(block/section/line/regex) | 下地依存(REST=fuzzy/SC=ベクトル) | BM25+グラフ+ベクトルをRRF融合 |
| 関連度の順位付け | 無し | 内蔵fuzzyのスコアで順位化(ファイル単位) | REST=fuzzyスコア/SC=ベクトル類似 | 複合スコアをチャンク単位で |
| 返る単位 | ページ丸ごと | ファイル+スニペット | ファイル+スニペット(SCはブロック) | チャンク |
| コンテキスト量 | 多くなりがち | スニペット長を調整可 | 同(下地依存) | チャンク単位で小さめ |
| 検索の速さ | 中 | 速い(Obsidian内蔵検索) | 速い(同) | 毎クエリBM25再構築・要最適化 |
| セットアップ | ほぼ不要 | 本体+CLI/プラグイン | 本体+プラグイン+MCPの三層 | コード一式の作り込み |
| 保守・成熟度 | 保守不要 | Obsidian本体で成熟 | MCP実装による | 自作=自分で保守(粗も残る) |
| Obsidian常駐 | 不要 | 必要 | 必要 | 不要 |
| 検索段のLLM | 使う(LLMが読む) | 使わない | 使わない | 使わない |
| 書き込み・編集 | 可 | 可 | 可(得意なものも) | 読み取り特化 |
※「品質をhit@k/MRRで測れる」のはDの固有の強みではありません。どの方式も同じevalで測れます(本記事はたまたまDで実装しただけ)。また上表の速度・成熟度は既存ツール(B/C)側が有利な項目で、そこは自作Dの弱点です。
参考ソース(B/C の記述の根拠)
- Obsidian公式CLI: 公式ヘルプ / 製品ページ("agentic tools"の表現)obsidian.md/cli。CLIは 1.12系で導入(公開版 v1.12.4・2026-02-27 のchangelogに "introduces the Obsidian CLI")。※「v1.12.7で登場」は誤りで、1.12.7は既存CLIの高速化リリース。
- 組み込み検索のオペレータ・ソート順: Obsidian Help / Search
- Local REST API(
/search/simple/のscore・contextLength、/search/のJsonLogic、Obsidian常駐前提): coddingtonbear/obsidian-local-rest-api(OpenAPI仕様・ソース) - MCPサーバ: MarkusPfundstein/mcp-obsidian / cyanheads/obsidian-mcp-server
- 意味検索系: jacksteamdev/obsidian-mcp-tools(2026-05-13 にアーカイブ/read-only)+ brianpetro/obsidian-smart-connections(現役・v4.5.3/2026-06)
- ※いずれも筆者は実運用しておらず、上記のドキュメント/README/ソースの読み取りに基づく(2026-07時点)。仕様は更新され得ます。
使い分け(あくまで一般論):
- 小さなVaultをサッと使い、作り込みたくない → A
- Obsidianを常用し編集もしたい・成熟したツールに乗りたい → B/C(筆者は未検証なので各自で確認を)
- チャンク単位の関連度順とコンテキスト最小化が欲しい・検索段をLLM非依存にしたい・品質を測りながら詰めたい → D(ただし自作の保守コストを払える人向け)
応用: この設計は「機密情報 × ローカルLLM」に強いかも?
本記事は公開ドキュメントが題材ですが、同じパイプラインは自分の機密ノートにもそのまま使えます。
しかも「検索=ローカルの仕組み、LLM=最小」という設計が、機密用途で効いてきます。
漏洩面はどこか
| 工程 | データの流れ | 機密の観点 |
|---|---|---|
| 取得・整形・グラフ化・検索(BM25/グラフ/ベクトル) | 完全ローカル(fastembedも推論はローカル) | ✅ 外に出ない |
| クエリ英訳・回答生成 | クラウドLLM(Claude等) | ❌ ここだけが漏洩面 |
→ 機密化の対策は実質「回答LLMをローカルに差し替えるだけ」。一番難しい「賢いエージェント無しで当てる検索」は
仕組み(B)で作り込み済みなので、弱いローカルLLMでも品質が落ちにくい。さらにチャンク単位で渡すので
小さいコンテキスト窓のローカルモデルにも収まる。つまりこの設計は機密×ローカルに偶然うまく適合します。
おすすめ構成
機密Vault(同期オフ・制限モード・暗号化ディスク)
→ 既存の normalize / embed_index / rag_search(全ローカル)
→ top-Kの小チャンク
→ ローカルLLM(Ollama 等・オフライン)が回答 ← Claudeをここに差し替えるだけ
- 回答合成は「検索済みチャンクから書くだけ」の軽い処理 → 7〜14Bの量子化モデルでも十分(Qwen/Llama/Gemma系)
- 埋め込み(bge-small)は一度だけ信頼ネットでDLすれば以降オフライン可
注意点(要点)
- Obsidian: クラウド同期(iCloud/Dropbox/OneDrive/Sync)を切る/制限モードでコミュニティプラグインを最小化(プラグインはVault全権+ネット権限を持つ)/Local REST APIやMCPサーバは常駐=露出面なので機密用途では原則立てない
- ローカルLLM: 「ローカル」を謳うUIでもクラウドfallback/テレメトリがあり得る → ネットを切って動くか検証(エアギャップテスト)。モデル/ランナーは信頼できる配布元から
-
マシン: ディスク暗号化(BitLocker/FileVault/LUKS)。
.cache/embeddingsも機密ノート由来=機密扱い
日本語で完結させたいなら
英訳ステップもクラウドに出したくない場合は、多言語埋め込み(Ruri v3等)+日本語ローカルLLMに替えると、
英訳を撤廃して入口から出口まで日本語・完全ローカルにできます(本記事は英訳前提の英語モデル構成)。
やってみて分かったこと
- スクレイピングは最終手段。clone/llms.txt配信で大半が綺麗に・合法的に取れる。
-
形式差は
format1か所に閉じ込める。製品固有のノイズは normalize に除去を1個足すだけ。 - 「賢さ依存(A)」と「仕組み(B)」を意識的に分けると、堅牢性(floor)が上がる。語形・同義語・意図まで仕組みで吸収できる。
- 評価ハーネスが効く。「同義語は直った」を、数値は「top3には入るが#1ではない」と精密に切り分け、チューニングを過学習させずに進められた。
- 軽さは正義。torch非依存(量子化ONNX)で実用品質が出せた。
hit@3=100% に達した今、残る伸びしろ(#1化)は磨き込み領域。Claudeがtop-Kを読む運用では差はごく小さいので、過学習を避けてここで一区切り、という判断もデータが支えてくれます。
おわりに
「ObsidianをRAGに変換する」は、Vaultを唯一の正とし、そこから人間用(グラフ)とAI用(チャンク索引)を生成すると綺麗に収まりました。MCPやREST APIに乗せる手もありますが、検索品質を測りながら軽量に詰めたいなら自作グラフRAGが刺さります。
そして「検索=ローカルの仕組み、LLM=最小」に振り切った設計は、副産物として
機密情報×ローカルLLMにもほぼそのまま移行できます(回答LLMを差し替えるだけ)。
「賢いLLMに全部任せる」のではなく「仕組み(B)の floor を厚くして、LLMは最後の一押しに使う」——
この分担が、軽量性・測定可能性・プライバシーのすべてに効いた、というのが一番の学びでした。
同じことをやってみたい方の参考になれば嬉しいです。