Example vault location
#
重要ポイント
ノートテイキングではなく、コンテキストエンジニアリングです。 AI にとっての Obsidian vault の価値は、ノートそのものではなく、ノートをクエリ可能にする検索レイヤーにあります。16,000 ファイルの vault でも検索機能がなければ、書き込み専用のデータベースにすぎません。一方、200 ファイルの vault でもハイブリッド検索と MCP 統合があれば、AI ナレッジベースとして機能します。検索インフラこそが製品であり、ノートはその原材料です。
ハイブリッド検索は、純粋なキーワード検索や純粋なセマンティック検索を上回ります。 BM25 は正確な識別子や関数名を捕捉します。ベクトル検索は、異なる用語間の同義語や概念的な一致を捕捉します。Reciprocal Rank Fusion(RRF)は、スコアのキャリブレーションを必要とせずに両方をマージします。どちらの手法も単独では両方の失敗パターンをカバーできません。MS MARCO のパッセージランキングに関する研究でもこのパターンが確認されています:ハイブリッド検索は、いずれかの手法を単独で使用した場合よりも一貫して優れた結果を示します。1 ハイブリッド検索の詳細解説では、RRF の数学的根拠、実際の数値を使った実例、失敗モード分析、およびインタラクティブな融合カリキュレーターを取り上げています。
MCP により、AI ツールが vault に直接アクセスできます。 Model Context Protocol(MCP)サーバーは、Claude Code、Codex CLI、Cursor、その他の AI ツールが直接呼び出せるツールとして検索エンジンを公開します。エージェントが vault にクエリを送信し、ソース帰属付きのランク付けされた結果を受け取り、ファイル全体を読み込むことなくコンテキストを利用します。MCP サーバーは検索エンジンの薄いラッパーです。
ローカルファーストにより、API コストはゼロで、完全なプライバシーが確保されます。 スタック全体が単一マシン上で動作します:ストレージには SQLite、embeddings には Model2Vec、キーワード検索には FTS5、ベクトル KNN には sqlite-vec を使用します。クラウドサービスなし、API 呼び出しなし、ネットワーク依存なし。個人のノートがマシンから出ることはありません。49,746 チャンクの完全な再 embedding は OpenAI の API 料金で約 $0.30 相当ですが、真のコストはレイテンシ、プライバシーの露出、そしてオフラインで動作すべきシステムに対するネットワーク依存です。2
インクリメンタルインデックスにより、システムは 10 秒以内に最新状態を維持します。 ファイルの変更時刻を比較して変更を検出します。変更されたファイルのみが再チャンキングおよび再 embedding されます。フルリインデックスは Apple M シリーズハードウェアで約 4 分かかります。通常の 1 日分の編集に対するインクリメンタル更新は 10 秒以内で完了します。手動介入なしにシステムが常に最新に保たれます。
アーキテクチャは 200 から 20,000 以上のノートまでスケールします。 同じ 3 層設計(取り込み、検索、統合)があらゆる vault サイズで機能します。小規模な vault に対して BM25 のみの検索から始めましょう。キーワードの衝突が問題になったらベクトル検索を追加します。正確な一致とセマンティックな一致の両方が必要になったら RRF 融合を追加します。各レイヤーは独立して有用であり、独立して取り外し可能です。
このガイドの使い方
このガイドはシステム全体をカバーしています。現在の状況に応じて、読み始める箇所が異なります:
| あなたの状況 | ここから始める | 次に探索する |
|---|---|---|
| Obsidian + AI が初めて | AI インフラとしての Obsidian を選ぶ理由、クイックスタート | Vault アーキテクチャ、MCP サーバーアーキテクチャ |
| 既存の vault に AI アクセスを追加したい | MCP サーバーアーキテクチャ、Claude Code 統合 | Embedding モデル、全文検索 |
| 検索システムを構築中 | 完全な検索パイプライン、Reciprocal Rank Fusion | パフォーマンスチューニング、トラブルシューティング |
| チームまたはエンタープライズ環境 | 意思決定フレームワーク、ナレッジグラフパターン | 開発者ワークフローレシピ、移行ガイド |
Contract と記載されたセクションには、実装の詳細、設定ブロック、および失敗モードが含まれます。Narrative と記載されたセクションは、概念、アーキテクチャの意思決定、および設計選択の背景にある理由に焦点を当てています。Recipe と記載されたセクションは、ステップバイステップのワークフローを提供します。
AI インフラとしての Obsidian を選ぶ理由
このガイドの基本テーゼ:Obsidian vault は、ローカルファースト、プレーンテキスト、グラフ構造であり、ユーザーがスタックのすべてのレイヤーをコントロールできるため、個人の AI ナレッジベースに最適な基盤です。
Obsidian が AI に提供する他にはない特性
プレーンテキストの markdown ファイル。 すべてのノートはファイルシステム上の .md ファイルです。独自フォーマットなし、データベースエクスポートなし、コンテンツを読むための API 不要。ファイルを読めるツールであれば、vault を読めます。grep、ripgrep、Python の pathlib、SQLite FTS5 — いずれもソースファイルに対して直接動作します。検索システムを構築する際は、API レスポンスではなくファイルをインデックスします。ソースがファイルシステムそのものであるため、インデックスは常にソースと一致しています。
ローカルファーストアーキテクチャ。 vault はあなたのマシン上にあります。サーバーなし、クラウド同期への依存なし、API レート制限なし、自分のコンテンツの処理方法を規定する利用規約なし。外部サービスを一切使わずに、ノートの embedding、インデックス作成、チャンキング、検索が可能です。これは AI インフラにとって重要です。なぜなら、検索パイプラインは API エンドポイントの応答速度ではなく、ディスク速度で動作するからです。プライバシーの面でも重要です:認証情報、健康データ、金融情報、プライベートな記録を含む個人ノートがマシンから出ることはありません。
wiki-link によるグラフ構造。 Obsidian の [[wiki-link]] 構文は、ノート間の有向グラフを作成します。OAuth 実装に関するノートは、トークンローテーション、セッション管理、API セキュリティに関するノートにリンクします。グラフ構造は、概念間の人間がキュレーションした関係性をエンコードしています。ベクトル embeddings はセマンティックな類似性を捕捉しますが、wiki-link はトピックについて考えている際に著者が作成した意図的なつながりを捕捉します。グラフは embeddings では再現できないシグナルです。
プラグインエコシステム。 Obsidian には 1,800 以上のコミュニティプラグインがあります。Dataview は vault をデータベースのようにクエリします。Templater は JavaScript ロジックを使ってテンプレートからノートを生成します。Git 連携は vault をリポジトリに同期します。Linter はフォーマットの一貫性を強制します。これらのプラグインは、基盤となるプレーンテキスト形式を変更することなく vault に構造を追加します。検索システムはプラグインそのものではなく、プラグインの出力をインデックスします。
500 万人以上のユーザー。 Obsidian には、テンプレート、ワークフロー、プラグイン、ドキュメントを生み出す大規模で活発なコミュニティがあります。vault の整理やプラグイン設定で問題に遭遇した場合、誰かがすでに解決策を文書化している可能性が高いです。コミュニティは Obsidian 関連ツールも開発しています:MCP サーバー、インデックススクリプト、パブリッシングパイプライン、API ラッパーなどです。
ファイルシステムだけでは得られないもの
markdown ファイルのディレクトリにはプレーンテキストの利点がありますが、Obsidian が追加する 3 つの要素が欠けています:
-
双方向リンク。 Obsidian はバックリンクを自動的に追跡します。ノート A からノート B にリンクすると、ノート B にはノート A からの参照が表示されます。グラフパネルは接続クラスタを可視化します。この双方向の認識は、生のファイルシステムでは提供されないメタデータです。
-
プラグインレンダリング付きライブプレビュー。 Dataview クエリ、Mermaid ダイアグラム、コールアウトブロックがリアルタイムでレンダリングされます。テキストエディタよりもリッチな執筆体験でありながら、ストレージフォーマットはプレーンテキストのままです。リッチな環境で執筆・整理し、検索システムは生の markdown をインデックスします。
-
コミュニティインフラ。 プラグインディスカバリ、テーママーケットプレイス、同期サービス(オプション)、パブリッシュサービス(オプション)、およびドキュメントエコシステム。個々の機能はスタンドアロンツールで再現できますが、Obsidian はそれらを一貫したワークフローにパッケージ化しています。
Obsidian が提供しないもの(自分で構築するもの)
Obsidian には検索インフラが含まれていません。基本的な検索機能(全文検索、ファイル名、タグ)はありますが、embedding パイプライン、ベクトル検索、融合ランキング、MCP サーバー、クレデンシャルフィルタリング、チャンキング戦略、外部 AI ツールとの統合フックはありません。このガイドは、Obsidian の上に構築するインフラをカバーしています。 vault は基盤です。検索パイプライン、MCP サーバー、および統合フックがインフラです。
ここで説明するアーキテクチャは markdown ファーストであり、Obsidian 専用ではありません。 Logseq、Foam、Dendron、または markdown ファイルのプレーンディレクトリを使用している場合でも、検索パイプラインは同一に動作します。チャンカーは .md ファイルを読み取り、エンベッダーはテキスト文字列を処理し、インデクサーは SQLite に書き込みます。これらのコンポーネントはいずれも Obsidian 固有の機能に依存していません。Obsidian の貢献は、検索エンジンがインデックスする markdown ファイルを生成する、執筆と整理の環境です。
クイックスタート:初めてのAI連携Vault
このセクションでは、5分でVaultをAIツールに接続します。Obsidianのインストール、Vaultの作成、MCPサーバーのインストール、そして最初のクエリ実行までを行います。クイックスタートでは、すぐに結果を確認できるコミュニティMCPサーバーを使用します。本番環境向けのカスタム検索パイプラインの構築については、後のセクションで解説します。
前提条件
- macOS、Linux、またはWindows
- Node.js 18+(MCPサーバー用)
- Claude Code、Codex CLI、またはCursorがインストール済み
ステップ1:Vaultを作成する
obsidian.mdからObsidianをダウンロードし、新しいVaultを作成します。覚えやすい場所を選んでください — MCPサーバーには絶対パスが必要です。
# Example vault location
~/Documents/knowledge-base/
検索対象となるノートをいくつか追加します。結果を確認するには10〜20個のノートで十分です。各ノートは.mdファイルで、意味のあるタイトルと少なくとも1段落の内容を含めてください。
ステップ2:MCPサーバーをインストールする
複数のコミュニティMCPサーバーが、すぐにVaultにアクセスできる機能を提供しています。2025〜2026年にかけてエコシステムは大きく成長しました:
| サーバー | 作者 | トランスポート | プラグイン要否 | 主な機能 |
|---|---|---|---|---|
| obsidian-mcp-server | StevenStavrakis | STDIO | 不要 | 軽量、ファイルベース |
| mcp-obsidian | MarkusPfundstein | STDIO | Local REST API | REST経由のVault CRUD |
| obsidian-mcp-tools | jacksteamdev | STDIO | 要(プラグイン) | セマンティック検索 + Templater |
| obsidian-claude-code-mcp | iansinnott | WebSocket | 要(プラグイン) | Claude Code向け自動検出 |
| obsidian-mcp-server | cyanheads | STDIO | Local REST API | タグ、frontmatter管理 |
クイックスタートでは、.mdファイルを直接読み取るファイルベースのサーバーが最もシンプルです:
npm install -g obsidian-mcp-server
ステップ3:AIツールを設定する
Claude Code — ~/.claude/settings.jsonに追加:
{
"mcpServers": {
"obsidian": {
"command": "obsidian-mcp-server",
"args": ["--vault", "/absolute/path/to/your/vault"]
}
}
}
Codex CLI — .codex/config.tomlに追加:
[mcp_servers.obsidian]
command = "obsidian-mcp-server"
args = ["--vault", "/absolute/path/to/your/vault"]
Cursor — .cursor/mcp.jsonに追加:
{
"mcpServers": {
"obsidian": {
"command": "obsidian-mcp-server",
"args": ["--vault", "/absolute/path/to/your/vault"]
}
}
}
ステップ4:最初のクエリを実行する
AIツールを開き、Vault内のノートで回答できる質問をしてみましょう:
Search my Obsidian vault for notes about [topic you wrote about]
AIツールがMCPサーバーを呼び出し、サーバーがVaultを検索して一致するコンテンツを返します。ファイルパスと関連する抜粋が結果として表示されるはずです。
構築したものの概要
ローカルのナレッジベースを標準プロトコルを通じてAIツールに接続しました。MCPサーバーがVaultのファイルを読み取り、基本的な検索を行い、結果を返します。これは最小限の実用的なバージョンです。
このクイックスタートでは得られないもの: - ハイブリッド検索(BM25 + ベクトル検索 + RRFフュージョン) - Embeddingベースのセマンティック検索 - クレデンシャルフィルタリング - インクリメンタルインデクシング - Hookベースの自動コンテキスト注入
このガイドの残りの部分で、これらの機能をそれぞれ構築する方法を解説します。クイックスタートはコンセプトの実証です。フルパイプラインが本番品質の検索を実現します。
意思決定フレームワーク:Obsidian vs 代替ツール
すべてのユースケースにObsidianが必要なわけではありません。このセクションでは、Obsidianが適切な基盤となる場合、オーバーキルとなる場合、そして他のツールの方が適している場合を整理します。
デシジョンツリー
START: What is your primary content type?
│
├─ Structured data (tables, records, schemas)
│ → Use a database. SQLite, PostgreSQL, or a spreadsheet.
│ → Obsidian is for prose, not tabular data.
│
├─ Ephemeral context (current project, temporary notes)
│ → Use CLAUDE.md / AGENTS.md in the project repo.
│ → These travel with the code and reset per project.
│
├─ Team wiki (shared documentation, onboarding)
│ → Evaluate Notion, Confluence, or a shared git repo.
│ → Obsidian vaults are personal-first. Team sync is possible
│ but not native.
│
└─ Growing personal knowledge corpus
│
├─ < 50 notes
│ → A folder of markdown files + grep is sufficient.
│ → Obsidian adds value mainly through the link graph,
│ which needs density to be useful.
│
├─ 50 - 500 notes
│ → Obsidian adds value. Wiki-links create a navigable graph.
│ → BM25-only search (FTS5) is sufficient at this scale.
│ → Skip vector search and RRF until keyword collisions appear.
│
├─ 500 - 5,000 notes
│ → Full hybrid retrieval becomes valuable. Keyword collisions
│ increase. Semantic search catches queries that BM25 misses.
│ → Add vector search + RRF fusion at this scale.
│
└─ 5,000+ notes
→ Full pipeline is essential. BM25-only returns too much noise.
→ Credential filtering becomes critical (more notes = more
accidentally pasted secrets).
→ Incremental indexing matters (full reindex takes minutes).
→ MCP integration pays dividends on every AI interaction.
比較マトリクス
| 基準 | Obsidian | Notion | Apple Notes | プレーンファイルシステム | CLAUDE.md |
|---|---|---|---|---|---|
| ローカルファースト | はい | いいえ(クラウド) | 部分的(iCloud) | はい | はい |
| プレーンテキスト | はい(markdown) | いいえ(ブロック) | いいえ(プロプライエタリ) | はい | はい |
| グラフ構造 | はい(wiki-link) | 部分的(メンション) | いいえ | いいえ | いいえ |
| AIインデックス可能 | ファイル直接アクセス | APIが必要 | エクスポートが必要 | ファイル直接アクセス | コンテキストに含まれる |
| プラグインエコシステム | 1,800以上のプラグイン | インテグレーション | なし | N/A | N/A |
| オフライン対応 | 完全 | 読み取り専用キャッシュ | 部分的 | 完全 | 完全 |
| 10K以上のノートに対応 | はい | はい(API使用) | 性能低下 | はい | いいえ(単一ファイル) |
| コスト | 無料(コア機能) | $10/月〜 | 無料 | 無料 | 無料 |
Obsidianがオーバーキルな場合
- 単一プロジェクトのコンテキスト。 AIが現在のコードベースに関するコンテキストのみを必要とする場合は、
CLAUDE.md、AGENTS.md、またはプロジェクトレベルのドキュメントに記述してください。これらのファイルはリポジトリと一緒に移動し、自動的に読み込まれます。 - 構造化データ。 コンテンツがテーブル、レコード、またはスキーマの場合は、データベースを使用してください。Obsidianのノートは散文が中心です。Dataviewはfrontmatterフィールドをクエリできますが、構造化クエリには本格的なデータベースの方が適しています。
- 一時的なリサーチ。 プロジェクト終了後にノートを破棄する場合は、markdownファイルを含むスクラッチディレクトリの方がシンプルです。一時的なコンテンツのために検索インフラを構築する必要はありません。
Obsidianが適切な選択となる場合
- 数ヶ月から数年にわたって知識を蓄積する場合。 コーパスの成長に伴い価値が複利的に増加します。200個のノートを持つVaultを6ヶ月間毎日クエリする方が、5,000個のノートを持つVaultを1回だけクエリするよりも大きな価値を提供します。
- 1つのコーパスに複数のドメインが含まれる場合。 プログラミング、アーキテクチャ、セキュリティ、デザイン、個人プロジェクトに関するノートを含むVaultは、プロジェクト固有の
CLAUDE.mdでは実現できないクロスドメイン検索の恩恵を受けます。 - プライバシーに配慮が必要なコンテンツ。 ローカルファーストであるため、検索パイプラインがコンテンツを外部サービスに送信することはありません。Vaultにはクラウドサービスにアップロードしたくないコンテンツも含め、あらゆるものを保存できます。
メンタルモデル:3つのレイヤー
このシステムは独立して動作する3つのレイヤーで構成されていますが、組み合わせることで相乗効果を発揮します。各レイヤーはそれぞれ異なる関心事と異なる障害モードを持っています。
┌─────────────────────────────────────────────────────┐
│ INTEGRATION LAYER │
│ MCP servers, hooks, skills, context injection │
│ Concern: delivering context to AI tools │
│ Failure: wrong context, too much context, stale │
└──────────────────────┬──────────────────────────────┘
│ query + ranked results
┌──────────────────────┴──────────────────────────────┐
│ RETRIEVAL LAYER │
│ BM25, vector KNN, RRF fusion, token budget │
│ Concern: finding the right content for any query │
│ Failure: wrong ranking, missed results, slow queries │
└──────────────────────┬──────────────────────────────┘
│ chunked, embedded, indexed
┌──────────────────────┴──────────────────────────────┐
│ INTAKE LAYER │
│ Note creation, signal triage, vault organization │
│ Concern: what enters the vault and how it's stored │
│ Failure: noise, duplicates, missing structure │
└─────────────────────────────────────────────────────┘
Intake(取り込み)は、Vaultに何が入るかを決定します。キュレーションがなければ、Vaultにはノイズが蓄積されます。ツイートのスクリーンショット、注釈のないコピペ記事、文脈のない中途半端なメモなどです。Intakeレイヤーは、エントリーポイントにおける品質管理を担います。スコアリングパイプライン、タグ付け規約、手動レビュープロセスなど、Vaultに検索する価値のあるコンテンツだけが含まれるようにするあらゆる仕組みがこれにあたります。
Retrieval(検索)は、Vaultをクエリ可能にします。これがエンジンです。ノートを検索単位にチャンキング(chunking)し、チャンクをベクトル空間にエンベディング(embeddings)し、キーワード検索とセマンティック検索のためにインデックスを作成し、RRFで結果を統合します。Retrievalレイヤーは、ファイルのディレクトリをクエリ可能なナレッジベースに変換します。このレイヤーがなければ、Vaultは手動ブラウジングと基本的な検索でしかナビゲートできず、AIツールからプログラム的にアクセスすることはできません。
Integration(統合)は、RetrievalレイヤーをAIツールに接続します。MCPサーバーが検索機能を呼び出し可能なツールとして公開します。Hooksが自動的にコンテキストを注入します。Skillsが新しい知識をVaultにキャプチャして戻します。Integrationレイヤーは、ナレッジベースとそれを利用するAIエージェントの間のインターフェースです。
各レイヤーは設計上分離されています。Intakeのスコアリングパイプラインはembeddingsについて何も知りません。Retrieverはシグナルルーティングルールについて何も知りません。MCPサーバーはノートがどのように作成されたかについて何も知りません。この分離により、各レイヤーを独立して改善できます。Intakeパイプラインを変更せずにembeddingモデルを置き換えられます。Retrieverを修正せずに新しいMCP機能を追加できます。インデックスに触れずにシグナルスコアリングのヒューリスティクスを変更できます。
AI活用のためのVaultアーキテクチャ
AI検索に最適化されたVaultは、個人的なブラウジングに最適化されたVaultとは異なる規約に従います。このセクションでは、フォルダ構造、ノートスキーマ、frontmatter規約、および検索品質を向上させる具体的なパターンについて解説します。
フォルダ構造
トップレベルフォルダには番号プレフィックスを使用して、予測可能な組織階層を作成します。番号は優先度を意味するものではなく、関連するドメインをグループ化し、構造を見やすくするためのものです。
vault/
├── 00-inbox/ # Unsorted captures, pending triage
├── 01-projects/ # Active project notes
├── 02-areas/ # Ongoing areas of responsibility
├── 03-resources/ # Reference material by topic
│ ├── programming/
│ ├── security/
│ ├── ai-engineering/
│ ├── design/
│ └── devops/
├── 04-archive/ # Completed projects, old references
├── 05-signals/ # Scored signal intake
│ ├── ai-tooling/
│ ├── security/
│ ├── systems/
│ └── ...12 domain folders
├── 06-daily/ # Daily notes (if used)
├── 07-templates/ # Note templates (excluded from index)
├── 08-attachments/ # Images, PDFs (excluded from index)
├── .obsidian/ # Obsidian config (excluded from index)
└── .indexignore # Paths to exclude from retrieval index
インデックス対象にすべきフォルダ: Markdownの文章を含むすべてのフォルダ — projects、areas、resources、signals、daily notesが該当します。
インデックスから除外すべきフォルダ: Templates(プレースホルダー変数を含み、コンテンツではないため)、attachments(バイナリファイル)、Obsidianの設定ファイル、および検索インデックスに含めたくない機密コンテンツを含むフォルダです。
.indexignoreファイル
Vault のルートに.indexignoreファイルを作成し、検索インデックスから除外するパスを明示的に指定します。構文は.gitignoreと同じです:
# Obsidian internal
.obsidian/
# Templates contain placeholders, not content
07-templates/
# Binary attachments
08-attachments/
# Personal health/medical notes
02-areas/health/
# Financial records
02-areas/finance/personal/
# Career documents (resumes, salary data)
02-areas/career/private/
インデクサーはスキャン前にこのファイルを読み込み、一致するパスを完全にスキップします。除外されたパス内のファイルはチャンキングもエンベディングもされず、検索結果にも表示されません。
ノートスキーマ
すべてのノートにYAML frontmatterを設定する必要があります。Retrieverはfrontmatterフィールドをフィルタリングとコンテキスト補強に使用します:
---
title: "OAuth Token Rotation Patterns"
type: note # note | signal | project | moc | daily
domain: security # primary domain for routing
tags:
- authentication
- oauth
- token-management
created: 2026-01-15
updated: 2026-02-28
source: "" # URL if captured from external source
status: active # active | archived | draft
---
検索に必須のフィールド:
title— 検索結果の表示とBM25の見出しコンテキストに使用されますtype— タイプフィルター付きクエリを可能にします(「MOCだけを表示」や「signalsのみ」など)tags— FTS5の見出しコンテキストに0.3のウェイトでインデックスされ、本文が異なる用語を使用している場合でもキーワードマッチを提供します
任意だが有用なフィールド:
domain— ドメインスコープ付きクエリを可能にします(「セキュリティノートのみを検索」など)source— キャプチャしたコンテンツの帰属表示に使用。RetrieverはソースURLを検索結果に含めることができますstatus— アーカイブ済みやドラフトのノートをアクティブな検索から除外できます
チャンキング規約
RetrieverはH2(##)の見出し境界でチャンキングします。つまり、ノートの構造が検索の粒度に直接影響します:
検索に適した構造:
## Token Rotation Strategy
The rotation interval depends on the threat model...
## Implementation with refresh_token
The OAuth 2.0 refresh token flow requires...
## Error Handling: Expired Tokens
When a token expires mid-request...
3つのH2セクションが3つの独立して検索可能なチャンクを生成します。各チャンクはembeddingがその意味を捉えるのに十分なコンテキストを持っています。「期限切れトークンの処理」に関するクエリは、3番目のチャンクに具体的にマッチします。
検索に不適切な構造:
# OAuth Notes
Token rotation depends on threat model. The OAuth 2.0 refresh
token flow requires storing the refresh token securely. When a
token expires mid-request, the client should retry after refresh.
The rotation interval is typically 15-30 minutes for access tokens
and 7-30 days for refresh tokens...
H2見出しのない1つの長いセクションは、1つの大きなチャンクを生成します。embeddingはセクション内のすべてのトピックの平均を取ります。どのサブトピックに関するクエリでも、ノート全体に対して同程度にマッチしてしまいます。
経験則: セクションが複数の概念を扱っている場合は、H2サブセクションに分割してください。チャンカーが残りを処理します。
ノートに入れるべきでないもの
検索品質を低下させるコンテンツ:
- 注釈なしの記事の丸ごとコピペ。 Retrieverが元の記事のキーワードをインデックスし、自分が書いていないコンテンツでVaultが希釈されます。代わりに、要約を追加するか、要点を抽出するか、ソースURLへのリンクを貼ってください。
- テキスト説明のないスクリーンショット。 RetrieverはMarkdownテキストをインデックスします。alt textや周辺の説明のない画像は、BM25にもベクトル検索にも見えません。
- 認証情報の文字列。 APIキー、トークン、パスワード、接続文字列。credential filteringがあっても、シークレットをノートに貼り付けないのが最も安全なアプローチです。代わりに名前で参照してください(「
~/.env内のCloudflare APIトークン」など)。 - キュレーションされていない自動生成コンテンツ。 ツールがノートを生成した場合(会議の文字起こし、Readwiseのハイライト、RSSインポートなど)、永続的なVaultに入れる前にレビューと注釈を行ってください。キュレーションされていない自動インポートは、検索可能な価値を追加せずにボリュームだけを増やします。
AIワークフローのためのプラグインエコシステム
AIによる検索でVaultの品質を向上させるObsidianプラグインは、3つのカテゴリに分類されます:構造化(一貫性の確保)、クエリ(メタデータの公開)、同期(Vaultの最新状態維持)です。
必須プラグイン
Dataview。 frontmatterフィールドを使用して、Vaultをデータベースのようにクエリできます。動的なインデックスを作成できます:「過去30日以内に更新されたsecurityタグ付きの全ノート」や「ステータスがactiveの全プロジェクトノート」などです。Dataviewは検索を直接支援するわけではありませんが、Vaultのカバレッジのギャップを特定し、更新が必要なノートを見つけるのに役立ちます。
TABLE type, domain, updated
FROM "03-resources"
WHERE status = "active"
SORT updated DESC
LIMIT 20
Templater。 動的フィールドを含むテンプレートからノートを作成します。created、type、domainフィールドを事前に入力するテンプレートを使用することで、すべての新規ノートが正しいfrontmatterで始まるようにできます。一貫したfrontmatterは検索フィルタリングの精度を向上させます。
<%* /* New Resource Note Template */ %>
---
title: "<% tp.file.cursor() %>"
type: note
domain: <% tp.system.suggester(["programming", "security", "ai-engineering", "design", "devops"], ["programming", "security", "ai-engineering", "design", "devops"]) %>
tags: []
created: <% tp.date.now("YYYY-MM-DD") %>
updated: <% tp.date.now("YYYY-MM-DD") %>
source: ""
status: active
---
## Key Points
## Details
## References
Linter。 Vault全体でフォーマットルールを強制します。一貫した見出し階層(H1はタイトル、H2はセクション、H3はサブセクション)により、チャンカーが予測可能な結果を生成します。検索に影響するLinterルールは以下の通りです:
- 見出しの増分:見出しレベルの順序を強制(H1からH3への飛びを防止)
- YAMLタイトル:ファイル名と一致させる
- 末尾のスペース:削除(FTS5のトークン化アーティファクトを回避)
- 連続する空行:1行に制限(よりクリーンなチャンク)
Git連携。 Vaultのバージョン管理です。変更を時系列で追跡し、マシン間で同期し、誤った削除からの復元が可能です。Gitはインデクサーが増分変更検出に使用するmtimeデータも提供します。
インデックス作成に役立つプラグイン
Smart Connections。 Obsidian内でAIを活用したセマンティック検索を提供するObsidianプラグインです。Smart Connections v4はデフォルトでローカルembeddingsを作成します。Vaultがインデックス化されると、セマンティックな接続と検索はAPI呼び出しなしで完全にオフラインで動作します。21 本ガイドの検索システムはObsidianの外部で動作しますが(Pythonパイプラインとして実行)、Smart Connectionsは執筆中にセマンティックな関連性を探索するのに有用です。2つのシステムは同じコンテンツをインデックス化しますが、用途が異なります:Smart Connectionsはエディタ内での発見に、外部リトリーバーはMCPを介したAIツール連携に使用します。
Metadata Menu。 フィールド値のオートコンプリート付きで構造化されたfrontmatter編集を提供します。type、domain、tagsフィールドの入力ミスを減らします。一貫したメタデータは検索フィルタリングの精度を向上させます。
インデックス作成に悪影響を与えるプラグイン
Excalidraw。 図面をmarkdownファイルに埋め込まれたJSONとして保存します。このJSONは構文的には有効なmarkdownですが、チャンク化してembeddingすると無意味なデータになります。.indexignoreで除外するか、ファイル拡張子でフィルタリングしてExcalidrawファイルをインデックスから除外してください。
Kanban。 ボードの状態を特殊なフォーマットのmarkdownとして保存します。このフォーマットはKanbanの表示用に設計されており、文章の検索には適していません。チャンカーはカードタイトルやメタデータの断片を生成し、それらはうまくembeddingされません。Kanbanボードはインデックスから除外してください。
Calendar。 最小限のコンテンツ(多くの場合、日付の見出しのみ)でデイリーノートを作成します。空またはほぼ空のノートは低品質なチャンクを生成します。デイリーノートを使用する場合は、実質的な内容を書くか、デイリーノートフォルダをインデックスから除外してください。
重要なプラグイン設定
ファイルリカバリ → 有効化。 誤ったノート削除を防止します。検索に直接関係しませんが、依存するナレッジベースにとって重要です。
厳密な改行 → 無効化。 Markdown標準の改行(段落に二重改行)は、Obsidianの厳密モード(<br>に単一改行)よりもクリーンなチャンクを生成します。
新規ファイルのデフォルト保存先 → 指定フォルダ。 新規ファイルを00-inbox/にルーティングすることで、未分類のノートがドメインフォルダを汚染しないようにします。受信箱はステージングエリアであり、トリアージ後にファイルがドメインフォルダに移動されます。
Wiki-linkフォーマット → 可能な場合は最短パス。 より短いリンクターゲットは、リンク構造のインデックス作成時にリトリーバーが解決しやすくなります。
Embeddingモデル:選定と設定
Embeddingモデルは、テキストチャンクを数値ベクトルに変換し、セマンティック検索を可能にします。モデルの選択は、検索品質、インデックスサイズ、Embedding速度、ランタイム依存関係を左右します。このセクションでは、Model2Vecのpotion-base-8Mがデフォルトとして選ばれている理由と、代替モデルを選ぶべきケースについて説明します。
Model2Vec potion-base-8Mを選ぶ理由
モデル: minishlab/potion-base-8M
パラメータ数: 760万
次元数: 256
サイズ: 約30 MB
依存関係: model2vec(numpyのみ、PyTorch不要)
推論: CPUのみ、静的単語Embeddings(アテンション層なし)
Model2Vecは、Sentence Transformerの知識を静的なトークンEmbeddingsに蒸留します。BERT、MiniLM、その他のTransformerモデルのように入力に対してアテンション層を実行するのではなく、Model2Vecは事前計算されたトークンEmbeddingsの重み付き平均によってベクトルを生成します。3 実用上の結果として、逐次計算が不要なため、Embedding速度はTransformerベースのモデルと比較して50〜500倍高速になります。
MTEBベンチマークスイートにおいて、potion-base-8Mはall-MiniLM-L6-v2の性能の89%を達成しています(平均50.03 対 56.09)。4 11%の品質差は、速度とシンプルさの利点とのトレードオフです。短いMarkdownチャンク(一般的なVaultでは平均200〜400語)の場合、両モデルとも短く焦点の絞られたテキストに対して類似した表現に収束するため、品質差は長い文書ほど顕著ではありません。
設定
# embedder.py
DEFAULT_MODEL = "minishlab/potion-base-8M"
EMBEDDING_DIM = 256
class Model2VecEmbedder:
def __init__(self, model_name=DEFAULT_MODEL):
self._model_name = model_name
self._model = None
def _ensure_model(self):
if self._model is not None:
return
_activate_venv() # Add isolated venv to sys.path
from model2vec import StaticModel
self._model = StaticModel.from_pretrained(self._model_name)
def embed_batch(self, texts):
self._ensure_model()
vecs = self._model.encode(texts)
return [v.tolist() for v in vecs]
遅延読み込み。 モデルはインポート時ではなく、最初の使用時に読み込まれます。Embedderモジュールのインポートは、リトリーバーがBM25のみのフォールバックモードで動作している場合(例:Embedding用venvがインストールされていない場合)、コストがかかりません。
分離された仮想環境。 モデルは専用のvenv(例:~/.claude/venvs/memory/)で実行され、ツールチェーンの他の部分との依存関係の競合を回避します。_activate_venv()関数は、実行時にvenvのsite-packagesをsys.pathに追加します。
# Create isolated venv
python3 -m venv ~/.claude/venvs/memory
~/.claude/venvs/memory/bin/pip install model2vec
バッチ処理。 Embedderはテキストを64個ずつのバッチで処理し、Model2Vecのオーバーヘッドを分散させます。インデクサーはチャンクを1つずつEmbeddingするのではなく、embed_batch()にまとめて渡します。
代替モデルを選ぶべきケース
| モデル | 次元数 | サイズ | 速度 | 品質(MTEB) | 最適な用途 |
|---|---|---|---|---|---|
| potion-base-8M | 256 | 30 MB | 500x | 50.03 | デフォルト:ローカル、高速、GPU不要 |
| potion-base-32M | 256 | 120 MB | 400x | 52.46 | より高品質、静的モデル |
| potion-retrieval-32M | 256 | 120 MB | 400x | 36.35(検索) | 検索に最適化された静的モデル |
| all-MiniLM-L6-v2 | 384 | 80 MB | 1x | 56.09 | より高品質、ローカル動作 |
| nomic-embed-text-v1.5 | 768 | 270 MB | 0.5x | 62.28 | ローカル最高品質 |
| text-embedding-3-small | 1536 | API | N/A | 62.30 | APIベース、最高品質 |
potion-base-32Mを選ぶ場合: 静的Embeddingファミリーから離れずに、potion-base-8Mよりも高い品質が必要な場合です。2025年1月にリリースされたこのモデルは、baai/bge-base-en-v1.5から蒸留されたより大きな語彙を使用し、MTEB平均52.46を達成しています(potion-base-8Mから5%の改善)。同じ256次元の出力とnumpyのみの依存関係を維持しています。18 4倍大きなモデルファイルによりメモリ使用量は増加しますが、Embedding速度はTransformerモデルと比較して桁違いに高速なままです。
potion-retrieval-32Mを選ぶ場合: 主な用途が検索である場合(Vault検索がまさにそれです)。このバリアントはpotion-base-32Mから検索タスクに特化してファインチューニングされており、MTEBの検索ベンチマークでベースモデルの33.52に対して36.35を記録しています。18 ファインチューニングにより汎用性能と引き換えに検索特化の性能向上を得ているため、MTEB全体の平均は49.73に低下します。
all-MiniLM-L6-v2を選ぶ場合: 速度よりも検索品質が重要で、PyTorchがインストールされている場合です。384次元のベクトルにより、SQLiteデータベースのサイズは256次元と比較して約50%増加します。M系ハードウェアで15,000ファイルの完全な再インデックスを行う場合、Embedding速度は1分未満から約10分に低下します。
nomic-embed-text-v1.5を選ぶ場合: ローカルで可能な最高の検索品質が必要で、インデックス作成の遅延を許容できる場合です。768次元のベクトルにより、データベースサイズはおよそ3倍になります。PyTorchと最新のCPUまたはGPUが必要です。
text-embedding-3-smallを選ぶ場合: ネットワークレイテンシとプライバシーが許容可能なトレードオフである場合です。APIは最高品質のEmbeddingsを生成しますが、クラウド依存、トークンあたりのコスト(100万トークンあたり$0.02)、およびコンテンツがOpenAIのサーバーに送信されるという条件が加わります。
それ以外のすべてのケースではpotion-base-8Mを使用してください。 速度の優位性は反復的なインデックス作成(開発中の再インデックス)に不可欠であり、numpyのみの依存関係によりPyTorchのインストールの複雑さを回避でき、256次元のベクトルによりデータベースをコンパクトに保てます。
量子化と次元削減
Model2Vec v0.5.0以降では、精度と次元を削減してモデルを読み込むことができます。18 これは、制約のあるハードウェアへのデプロイや、モデルを変更せずにデータベースサイズを削減する場合に有用です:
from model2vec import StaticModel
# Load with int8 quantization (25% of original size)
model = StaticModel.from_pretrained("minishlab/potion-base-8M", quantize=True)
# Load with reduced dimensions (e.g., 128 instead of 256)
model = StaticModel.from_pretrained("minishlab/potion-base-8M", dimensionality=128)
量子化されたモデルは、メモリフットプリントをわずかにしつつ、ほぼ同等の検索品質を維持します。次元削減はMatryoshkaスタイルの切り捨てに従います — 最初のN次元が最も多くの情報を持ちます。256次元から128次元に削減すると、短いテキストの検索において品質への影響を最小限に抑えつつ、ベクトルストレージを半分にできます。
2025年5月時点で、Model2Vecは(WordPieceに加えて)BPEおよびUnigramトークナイザーもサポートしており、静的モデルに蒸留できるSentence Transformerの範囲が拡大しています。20
Vault固有のEmbeddingsのファインチューニング
Model2Vec v0.4.0以降では静的Embeddingsの上にカスタム分類モデルのトレーニングが可能で、v0.7.0では語彙量子化と蒸留用の設定可能なプーリングが追加されています。20 これは、専門的な語彙を持つVault(医療メモ、法律文献、ドメイン固有の専門用語)で、デフォルトのpotionモデルでは意味的なニュアンスを捉えきれない場合に関連します:
from model2vec import StaticModel
from model2vec.train import train_model
# Fine-tune on vault-specific data
model = StaticModel.from_pretrained("minishlab/potion-base-8M")
trained_model = train_model(model, train_texts, train_labels)
trained_model.save_pretrained("./vault-embeddings")
ほとんどのVaultでは、デフォルトのpotion-base-8Mで十分な検索品質が得られます。ファインチューニングは、汎用モデルでは捉えられないドメイン固有の関連性を検索が一貫して見逃す場合にのみ価値があります。
モデルハッシュの追跡
インデクサーは、モデル名と語彙サイズから導出されたハッシュを保存します。Embeddingモデルを変更すると、インデクサーは次回の差分実行時に不一致を検出し、自動的に完全な再インデックスをトリガーします。
def _compute_model_hash(self):
"""Hash model name + vocab size for compatibility tracking."""
key = f"{self._model_name}:{self._model.vocab_size}"
return hashlib.sha256(key.encode()).hexdigest()[:16]
これにより、異なるモデルのベクトルが同じデータベース内で混在することを防ぎます。混在した場合、cosine similarityのスコアが無意味なものになります。
障害モード
モデルダウンロードの失敗。 初回実行時にHugging Faceからモデルがダウンロードされます。ダウンロードが失敗した場合(ネットワーク問題、企業ファイアウォール)、リトリーバーはBM25のみのモードにフォールバックします。モデルは初回ダウンロード後にローカルにキャッシュされます。
次元の不一致。 データベースをクリアせずにモデルを切り替えると、保存されたベクトルの次元が新しいEmbeddingsと異なります。インデクサーはモデルハッシュを通じてこれを検出し、完全な再インデックスをトリガーします。ハッシュチェックが失敗した場合(適切なハッシュのないカスタムモデル)、sqlite-vecは次元が一致しないKNNクエリでエラーを返します。
大規模Vaultでのメモリ負荷。 50,000以上のチャンクを単一バッチでEmbeddingすると、大量のメモリを消費する可能性があります。インデクサーは64個ずつのバッチで処理し、ピークメモリ使用量を制限しています。それでもメモリが問題になる場合は、バッチサイズを削減してください。
FTS5によるフルテキスト検索
SQLiteのFTS5拡張機能は、BM25ランキングを備えたフルテキスト検索を提供します。FTS5はhybrid retrievalパイプラインにおけるキーワード検索コンポーネントです。このセクションでは、FTS5の設定、BM25が優れている場面、および特有の失敗パターンについて説明します。
FTS5仮想テーブル
CREATE VIRTUAL TABLE chunks_fts USING fts5(
chunk_text,
section,
heading_context,
content=chunks,
content_rowid=id
);
コンテンツ同期モード。 content=chunksパラメータは、テキストの複製を保存するのではなく、chunksテーブルを直接参照するようFTS5に指示します。これによりストレージ要件が半分になりますが、チャンクの挿入・更新・削除時にFTS5を手動で同期する必要があります。
カラム。 3つのカラムがインデックスされます:
- chunk_text — 各チャンクの主要コンテンツ(BM25ウェイト:1.0)
- section — H2見出しテキスト(BM25ウェイト:0.5)
- heading_context — ノートタイトル、タグ、メタデータ(BM25ウェイト:0.3)
BM25ランキング
BM25は、単語頻度、逆文書頻度、文書長の正規化によってドキュメントをランク付けします。FTS5のbm25()補助関数は、カラムごとのウェイトを受け取ります:
SELECT
c.id, c.file_path, c.section, c.chunk_text,
bm25(chunks_fts, 1.0, 0.5, 0.3) AS score
FROM chunks_fts
JOIN chunks c ON chunks_fts.rowid = c.id
WHERE chunks_fts MATCH ?
ORDER BY score
LIMIT 30;
カラムウェイト(1.0、0.5、0.3)の意味は以下の通りです:
- chunk_textでのキーワード一致がスコアに最も大きく寄与します
- section(見出し)での一致はその半分の寄与です
- heading_context(タイトル、タグ)での一致は30%の寄与です
これらのウェイトは調整可能です。Vault内の見出しがコンテンツの質を強く予測する記述的なものであれば、sectionのウェイトを上げてください。タグが包括的で正確であれば、heading_contextのウェイトを上げてください。
BM25が優れる場面
BM25は、正確な識別子を含むクエリに優れています:
- 関数名:
_rrf_fuse、embed_batch、get_stale_files - CLIフラグ:
--incremental、--vault、--model - 設定キー:
bm25_weight、max_tokens、batch_size - エラーメッセージ:
SQLITE_LOCKED、ConnectionRefusedError - 専門用語:
PostToolUse、PreToolUse、AGENTS.md
これらのクエリでは、BM25が正確な一致を即座に見つけます。ベクトル検索は意味的に関連するコンテンツを返しますが、概念的な議論の方が正確な一致よりも上位にランク付けされる可能性があります。
BM25が失敗する場面
BM25は、保存されたコンテンツと異なる用語を使用するクエリでは失敗します:
- クエリ:「how to handle authentication failures」→ Vaultには「login error recovery」や「session expiration handling」に関するノートが含まれています。キーワードが異なるため、BM25は一致しません。
- クエリ:「what is the best way to manage state」→ Vaultには「Redux store patterns」や「context providers」に関するノートが含まれています。「状態管理」が特定の技術名で表現されているため、BM25は見逃します。
BM25はまた、大規模な環境ではキーワードの衝突でも失敗します。15,000ファイルのVaultでは、「configuration」の検索がほぼすべてのプロジェクトノートに含まれるため、数百のノートに一致します。結果は技術的には正しいですが、実用的には役に立ちません — ランキングでは、現在のクエリにどの「configuration」ノートが関連するかを判断できません。
FTS5トークナイザー
FTS5はデフォルトでunicode61トークナイザーを使用し、ASCIIおよびUnicodeテキストを処理します。CJK(中国語、日本語、韓国語)コンテンツが多いVaultでは、trigramトークナイザーの使用を検討してください:
-- For CJK-heavy vaults
CREATE VIRTUAL TABLE chunks_fts USING fts5(
chunk_text, section, heading_context,
content=chunks, content_rowid=id,
tokenize='trigram'
);
デフォルトのunicode61トークナイザーは単語境界で分割するため、単語間にスペースがない言語では適切に動作しません。trigramトークナイザーは3文字ごとに分割し、インデックスサイズの増加(約3倍)と引き換えに部分文字列マッチングを可能にします。
メンテナンス
FTS5は、基盤となるchunksテーブルが変更された際に明示的な同期が必要です:
# After inserting chunks
cursor.execute("""
INSERT INTO chunks_fts(chunks_fts)
VALUES('rebuild')
""")
rebuildコマンドは、コンテンツテーブルからFTS5インデックスを再構築します。一括挿入(フルリインデックス)の後に実行しますが、個別の増分更新の後には実行しないでください — その場合はINSERT INTO chunks_fts(rowid, chunk_text, section, heading_context)を使用して個々の行を同期します。
sqlite-vecによるベクトル検索
sqlite-vec拡張機能は、ベクトルKNN(K近傍法)検索をSQLiteに導入します。このセクションでは、sqlite-vecの設定、ノートから検索可能なベクトルへのembeddingパイプライン、および具体的なクエリパターンについて説明します。
sqlite-vec仮想テーブル
CREATE VIRTUAL TABLE chunk_vecs USING vec0(
id INTEGER PRIMARY KEY,
embedding float[256]
);
vec0モジュールは、256次元のfloatベクトルをパックされたバイナリデータとして保存します。idカラムはchunksテーブルと1:1で対応し、ベクトル検索結果とチャンクメタデータの結合を可能にします。
Embeddingパイプライン
パイプラインはノートから検索可能なベクトルへと流れます:
Note (.md file)
→ Chunker: split at H2 boundaries
→ Chunks (30-2000 chars each)
→ Credential filter: scrub secrets
→ Embedder: Model2Vec encode
→ Vectors (256-dim float arrays)
→ sqlite-vec: store as packed binary
→ Ready for KNN queries
ベクトルのシリアライズ
Pythonのstructモジュールは、sqlite-vecストレージ用にfloatベクトルをシリアライズします:
import struct
def _serialize_vector(vec):
"""Pack float list into binary for sqlite-vec."""
return struct.pack(f"{len(vec)}f", *vec)
def _deserialize_vector(blob, dim=256):
"""Unpack binary blob to float list."""
return list(struct.unpack(f"{dim}f", blob))
KNNクエリ
ベクトル検索クエリは、入力クエリをembeddingし、コサイン距離で最も近いK個のチャンクを見つけます:
def _vector_search(self, query_text, limit=30):
query_vec = self.embedder.embed_batch([query_text])[0]
packed = _serialize_vector(query_vec)
results = self.db.execute("""
SELECT
cv.id,
cv.distance,
c.file_path,
c.section,
c.chunk_text
FROM chunk_vecs cv
JOIN chunks c ON cv.id = c.id
WHERE embedding MATCH ?
AND k = ?
ORDER BY distance
""", [packed, limit]).fetchall()
return results
sqlite-vecのMATCH演算子は近似最近傍探索を実行します。kパラメータは返す結果の数を制御します。distanceカラムにはコサイン距離が含まれます(0 = 同一、2 = 正反対)。
ベクトル検索が優れる場面
ベクトル検索は、特定の単語よりも概念が重要なクエリに優れています:
- クエリ:「how to handle authentication failures」→ 「login error recovery」に関するノートを検出します(同じ意味空間、異なるキーワード)
- クエリ:「what patterns exist for caching」→ 「memoization」、「Redis TTL strategies」、「HTTP cache headers」に関するノートを検出します(関連する概念、多様な用語)
- クエリ:「approaches to testing asynchronous code」→ 「pytest-asyncio fixtures」、「mock event loops」、「async test patterns」に関するノートを検出します(同じ概念が実装の詳細を通じて表現されている)
ベクトル検索が失敗する場面
ベクトル検索は、正確な識別子に対して苦手です:
- クエリ:
_rrf_fuse→ 「fusion algorithms」や「rank merging」に関するノートを返しますが、実際の関数定義が概念的な議論よりも下位にランク付けされる可能性があります - クエリ:
PostToolUse→ 特定のフック名ではなく、「tool lifecycle hooks」や「post-execution handlers」に関するノートを返します
ベクトル検索はまた、構造化データにも苦手です。JSON設定ファイル、YAMLブロック、コードスニペットは、意味的な内容ではなく構造的なパターンを捉えたembeddingを生成します。"review": trueを含むJSONファイルは、コードレビューに関する散文的な議論とは異なるembeddingになります。
グレースフルデグラデーション
sqlite-vecの読み込みに失敗した場合(拡張機能の欠如、互換性のないプラットフォーム、破損したライブラリ)、リトリーバーはBM25のみの検索にフォールバックします:
class VectorIndex:
def __init__(self, db_path):
self.db = sqlite3.connect(db_path)
self._vec_available = False
try:
self.db.enable_load_extension(True)
self.db.load_extension("vec0")
self._vec_available = True
except Exception:
pass # BM25-only mode
@property
def vec_available(self):
return self._vec_available
リトリーバーはベクトルクエリを試みる前にvec_availableを確認します。無効の場合、すべての検索はBM25のみを使用し、RRFフュージョンステップはスキップされます。
Reciprocal Rank Fusion(RRF)
RRFは、スコアのキャリブレーションを必要とせずに2つのランク付けリストを統合します。このセクションでは、アルゴリズム、クエリトレースの実例、kパラメータのチューニング、そしてRRFが他の手法よりも選ばれる理由を解説します。編集可能なランク、シナリオプリセット、ビジュアルアーキテクチャエクスプローラーを備えたインタラクティブな計算ツールについては、hybrid retrieverの詳細解説をご覧ください。
アルゴリズム
RRFは、各リストにおけるランク位置のみに基づいて、各ドキュメントにスコアを割り当てます:
score(d) = Σ (weight_i / (k + rank_i))
各変数の意味:
- kは平滑化定数です(Cormackら1に従い60を使用)
- rank_iは結果リストiにおける、そのドキュメントの1始まりのランクです
- weight_iはリストごとのオプションの重み乗数です(デフォルト1.0)
複数のリストで上位にランクされたドキュメントは、より高い統合スコアを受け取ります。1つのリストにのみ出現するドキュメントは、その単一ソースからのスコアのみを受け取ります。
RRFが他の手法よりも優れている理由
重み付き線形結合では、BM25スコアとcosine distanceのキャリブレーションが必要です。BM25スコアは上限がなく、コーパスサイズに応じてスケールします。Cosine distanceは[0, 2]の範囲に制限されています。両者を組み合わせるには正規化が必要であり、その正規化パラメータはデータセットに依存します。RRFはランク位置のみを使用するため、スコアリング手法に関係なく常に1から始まる整数値となります。
学習型融合モデルには、ラベル付きの学習データ(クエリとドキュメントの関連性ペア)が必要です。個人のナレッジベースでは、このような学習データは存在しません。有用なモデルを訓練するには、数百のクエリとドキュメントのペアを手動で評価する必要があります。RRFは学習データなしで機能します。
Condorcet投票法(Borda count、Schulze method)は理論的には洗練されていますが、実装やチューニングがより複雑です。オリジナルのRRF論文では、TREC評価データにおいてRRFがCondorcet手法を上回ることが実証されています。1
実際の統合プロセス
クエリ:「how does the review aggregator handle disagreements」
BM25はreview-aggregator.pyをポジション3にランク付けします(「review」「aggregator」「disagreements」の正確なキーワードマッチ)が、2つの設定ファイルをより上位に配置します(「review」がより顕著にマッチするため)。ベクトル検索は同じチャンクをポジション1にランク付けします(コンフリクト解決に対するセマンティックマッチ)。RRF統合後:
| チャンク | BM25 | Vec | 統合スコア |
|---|---|---|---|
| review-aggregator.py “Disagreement Resolution” | #3 | #1 | 0.0323 |
| code-review-patterns.md “Multi-Reviewer” | #4 | #2 | 0.0317 |
| deliberation-config.json “Review Weights” | #1 | — | 0.0164 |
両方のリストで上位にランクされたチャンクがトップに浮上します。1つのリストにのみ出現するチャンクは単一ソースのスコアとなり、両リストでランクされた結果の下に位置します。実際の不一致解決ロジックが勝つのは、両方の手法がそれを発見したからです — BM25はキーワードで、ベクトル検索はセマンティクスで見つけました。
ランクごとのRRF計算を含む完全なステップバイステップのトレースについては、インタラクティブRRF計算ツールで異なるk値を試してみてください。
実装
RRF_K = 60
def _rrf_fuse(self, bm25_results, vec_results,
bm25_weight=1.0, vec_weight=1.0):
"""Fuse BM25 and vector results using Reciprocal Rank Fusion."""
scores = {}
for rank, r in enumerate(bm25_results, start=1):
cid = r["id"]
if cid not in scores:
scores[cid] = {
"rrf_score": 0.0,
"file_path": r["file_path"],
"section": r["section"],
"chunk_text": r["chunk_text"],
"bm25_rank": None,
"vec_rank": None,
}
scores[cid]["rrf_score"] += bm25_weight / (self._rrf_k + rank)
scores[cid]["bm25_rank"] = rank
for rank, r in enumerate(vec_results, start=1):
cid = r["id"]
if cid not in scores:
scores[cid] = {
"rrf_score": 0.0,
"file_path": r["file_path"],
"section": r["section"],
"chunk_text": r["chunk_text"],
"bm25_rank": None,
"vec_rank": None,
}
scores[cid]["rrf_score"] += vec_weight / (self._rrf_k + rank)
scores[cid]["vec_rank"] = rank
fused = sorted(
scores.values(),
key=lambda x: x["rrf_score"],
reverse=True,
)
return fused
kのチューニング
k定数は、上位ランクの結果と下位ランクの結果にどの程度の重みを与えるかを制御します:
- 低いk(例:10): 上位ランクの結果が支配的になります。ランク1のスコアは1/11 = 0.091、ランク10のスコアは1/20 = 0.050(1.8倍の差)。個々のランカーがトップの結果を正しく判定していると信頼できる場合に適しています。
- デフォルトk(60): バランス型です。ランク1のスコアは1/61 = 0.0164、ランク10のスコアは1/70 = 0.0143(1.15倍の差)。ランクの差が圧縮され、複数のリストに出現することにより大きな重みが与えられます。
- 高いk(例:200): ランク位置よりも両方のリストに出現することがはるかに重要になります。ランク1のスコアは1/201、ランク10のスコアは1/210 — ほぼ同一です。個々のランカーがノイズの多いランキングを生成するが、リスト間の一致は信頼できる場合に使用します。
k=60から始めてください。オリジナルのRRF論文では、この値が多様なTRECデータセットにわたって堅牢であることが確認されています。自身のクエリ分布で失敗事例を測定した後にのみチューニングを行ってください。
タイブレーク
2つのチャンクが同一のRRFスコアを持つ場合(まれですが、一方のリストで同じランクかつもう一方のリストに出現しない場合に発生する可能性があります)、以下の順序でタイブレークを行います:
- 1つのリストにのみ出現するチャンクよりも、両方のリストに出現するチャンクを優先します
- 両方のリストに出現するチャンク同士では、合計ランクが低い方を優先します
- 1つのリストにのみ出現するチャンク同士では、そのリスト内でランクが低い方を優先します
完全な検索パイプライン
このセクションでは、クエリが入力から出力まで、BM25検索、ベクトル検索、RRF融合、トークンバジェットの切り詰め、コンテキスト組み立てという全パイプラインを通じてどのように処理されるかを追跡します。
エンドツーエンドのフロー
User query: "PostToolUse hook for context compression"
│
├─ BM25 Search (FTS5)
│ → MATCH "PostToolUse hook context compression"
│ → Top 30 results ranked by BM25 score
│ → 12ms
│
├─ Vector Search (sqlite-vec)
│ → Embed query with Model2Vec
│ → KNN k=30 on chunk_vecs
│ → Top 30 results ranked by cosine distance
│ → 8ms
│
└─ RRF Fusion
→ Merge 60 candidates (may overlap)
→ Score by rank position
→ Top 10 results
→ 3ms
│
└─ Token Budget
→ Truncate to max_tokens (default 4000)
→ Estimate at 4 chars per token
→ Return results with metadata
→ <1ms
合計レイテンシ:約23ms(Apple M3 Proハードウェア上の49,746チャンクのデータベースの場合)。
検索API
class HybridRetriever:
def search(self, query, limit=10, max_tokens=4000,
bm25_weight=1.0, vec_weight=1.0):
"""
Search the vault using hybrid BM25 + vector retrieval.
Args:
query: Search query text
limit: Maximum results to return
max_tokens: Token budget for total result text
bm25_weight: Weight for BM25 results in RRF
vec_weight: Weight for vector results in RRF
Returns:
List of SearchResult with file_path, section,
chunk_text, rrf_score, bm25_rank, vec_rank
"""
# BM25 search
bm25_results = self._bm25_search(query, limit=30)
# Vector search (if available)
if self.index.vec_available:
vec_results = self._vector_search(query, limit=30)
fused = self._rrf_fuse(
bm25_results, vec_results,
bm25_weight, vec_weight,
)
else:
fused = bm25_results # BM25-only fallback
# Token budget truncation
results = []
token_count = 0
for r in fused[:limit]:
chunk_tokens = len(r["chunk_text"]) // 4
if token_count + chunk_tokens > max_tokens:
break
results.append(r)
token_count += chunk_tokens
return results
トークンバジェットの切り詰め
max_tokensパラメータは、AIツールが処理できる量を超えるコンテキストをリトリーバーが返すことを防ぎます。推定値は1トークンあたり4文字を使用しています(英語の散文では妥当な近似値です)。結果は貪欲法で切り詰められます:ランク順に結果を追加し、バジェットを使い切るまで続けます。
これは保守的な戦略です。より洗練されたアプローチでは、結果ごとの品質スコアを考慮し、長くて品質の低い結果よりも短くて品質の高い結果を優先します。貪欲法はよりシンプルであり、RRFランキングがすでに関連性の順に結果を並べているため、実際にはうまく機能します。
データベーススキーマ(完全版)
-- Chunk content and metadata
CREATE TABLE chunks (
id INTEGER PRIMARY KEY,
file_path TEXT NOT NULL,
section TEXT NOT NULL,
chunk_text TEXT NOT NULL,
heading_context TEXT DEFAULT '',
mtime_ns INTEGER NOT NULL,
embedded_at REAL NOT NULL
);
CREATE INDEX idx_chunks_file ON chunks(file_path);
CREATE INDEX idx_chunks_mtime ON chunks(mtime_ns);
-- FTS5 for BM25 search (content-synced to chunks table)
CREATE VIRTUAL TABLE chunks_fts USING fts5(
chunk_text, section, heading_context,
content=chunks, content_rowid=id
);
-- sqlite-vec for vector KNN search
CREATE VIRTUAL TABLE chunk_vecs USING vec0(
id INTEGER PRIMARY KEY,
embedding float[256]
);
-- Model metadata for compatibility tracking
CREATE TABLE model_meta (
key TEXT PRIMARY KEY,
value TEXT
);
グレースフルデグラデーションパス
Full pipeline: BM25 + Vector + RRF → Best results
No sqlite-vec: BM25 only → Good results (no semantic)
No model download: BM25 only → Good results (no semantic)
No FTS5: Vector only → Decent results (no keyword)
No database: Error → Prompt user to run indexer
リトリーバーは初期化時に利用可能な機能を確認し、クエリ戦略を適応させます。コンポーネントが欠けている場合、品質は低下しますがエラーは発生しません。唯一のハードエラーは、データベースファイルが存在しない場合です。
本番環境の統計
16,894ファイル、49,746チャンク、83 MBのSQLiteデータベース、Apple M3 Proで測定:
| メトリクス | 値 |
|---|---|
| 総ファイル数 | 16,894 |
| 総チャンク数 | 49,746 |
| データベースサイズ | 83 MB |
| BM25クエリレイテンシ(p50) | 12ms |
| ベクトルクエリレイテンシ(p50) | 8ms |
| RRF融合レイテンシ | 3ms |
| エンドツーエンド検索レイテンシ(p50) | 23ms |
| フルリインデックス時間 | 約4分 |
| インクリメンタルリインデックス時間 | 10秒未満 |
| Embeddingモデル | potion-base-8M(256次元) |
| BM25候補プール | 30 |
| ベクトル候補プール | 30 |
| デフォルト結果リミット | 10 |
| デフォルトトークンバジェット | 4,000トークン |
コンテンツハッシュと変更検出
インデクサーは、前回のインデックス実行以降にどのファイルが変更されたかを知る必要があります。このセクションでは、変更検出メカニズムとハッシュ戦略について解説します。
ファイル更新時刻の比較
インデクサーはchunksテーブル内のすべてのチャンクに対してmtime_ns(ナノ秒単位のファイル更新時刻)を保存します。インクリメンタル実行時に、インデクサーは以下を行います:
- 許可されたフォルダ内のすべての
.mdファイルについてボルトをスキャンします - ファイルシステムから各ファイルの
mtime_nsを読み取ります - データベースに保存されている
mtime_nsと比較します - 3つのカテゴリを特定します:
- 新規ファイル: パスがファイルシステムに存在するがデータベースに存在しない
- 変更ファイル: パスが両方に存在するが
mtime_nsが異なる - 削除ファイル: パスがデータベースに存在するがファイルシステムに存在しない
def get_stale_files(self, vault_mtimes):
"""Find files whose mtime changed or are new."""
stored = dict(self.db.execute(
"SELECT DISTINCT file_path, mtime_ns FROM chunks"
).fetchall())
stale = []
for path, mtime in vault_mtimes.items():
if path not in stored or stored[path] != mtime:
stale.append(path)
return stale
def get_deleted_files(self, vault_paths):
"""Find files in database that no longer exist in vault."""
stored_paths = set(r[0] for r in self.db.execute(
"SELECT DISTINCT file_path FROM chunks"
).fetchall())
return stored_paths - set(vault_paths)
なぜmtimeであってコンテンツハッシュではないのか
コンテンツハッシュ(ファイル内容のSHA-256)はmtime比較よりも信頼性が高くなります。たとえば、ファイルが内容を変更せずにタッチされた場合(git checkoutで元のmtimeが復元されるなど)も検出できます。しかし、ハッシュにはインクリメンタル実行のたびにすべてのファイルを読み取る必要があります。16,894ファイルの場合、ファイル内容の読み取りには2〜3秒かかります。ファイルシステムからのmtime読み取りは100ms未満です。
トレードオフ:mtime比較は、変更されていないファイルの不要な再インデックス(偽陽性)を時折引き起こしますが、実際の変更を見逃すことはありません。偽陽性のコストは、1回の実行あたり数回の追加Embedding呼び出しです。速度の差(100ms対3秒)により、すべてのAIインタラクションのたびに実行されるシステムにとって、mtimeが実用的な選択となります。
削除の処理
ファイルがボルトから削除された場合、インデクサーはデータベースからそのファイルのすべてのチャンクを削除します:
def remove_file(self, file_path):
"""Remove all chunks and vectors for a file."""
chunk_ids = [r[0] for r in self.db.execute(
"SELECT id FROM chunks WHERE file_path = ?",
[file_path],
).fetchall()]
for cid in chunk_ids:
self.db.execute(
"DELETE FROM chunk_vecs WHERE id = ?", [cid]
)
self.db.execute(
"DELETE FROM chunks WHERE file_path = ?",
[file_path],
)
FTS5のコンテンツ同期テーブルでは、削除された各行に対してINSERT INTO chunks_fts(chunks_fts, rowid, ...) VALUES('delete', ?, ...)による明示的な削除が必要です。インデクサーはファイル削除プロセスの一部としてこれを処理します。
インクリメンタルリインデックスとフルリインデックス
インデクサーは2つのモードをサポートしています:インクリメンタル(高速、日常使用向け)とフル(低速、必要時のみ)。このセクションでは、それぞれの使い分け、冪等性の保証、および破損からの復旧について説明します。
インクリメンタルリインデックス
使用するタイミング: ノートの編集後に行う日常的なインデックス作成。デフォルトのモードです。
処理内容: 1. Vault内のファイル変更をスキャン(mtime比較) 2. 削除されたファイルのチャンクを削除 3. 変更されたファイルを再チャンク化・再エンベディング 4. 新規ファイルのチャンクを挿入 5. FTS5インデックスを同期
所要時間の目安: 16,000ファイルのVaultで1日分の編集に対して10秒未満。
python index_vault.py --incremental
フルリインデックス
使用するタイミング: - エンベディングモデルを変更した後(モデルハッシュの不一致を検出した場合) - スキーママイグレーション後(新しいカラム、変更されたインデックス) - データベースの破損後(整合性チェックが失敗した場合) - インクリメンタルインデックスで予期しない結果が生じた場合
処理内容: 1. 既存データをすべて削除(チャンク、ベクトル、FTS5エントリ) 2. Vault全体をスキャン 3. すべてのファイルをチャンク化 4. すべてのチャンクをエンベディング 5. FTS5インデックスをゼロから構築
所要時間の目安: Apple M3 Proで16,894ファイルに対して約4分。
python index_vault.py --full
冪等性
両モードとも冪等です。同じコマンドを2回実行しても同じ結果が得られます。インデクサーはファイルの既存チャンクを削除してから新しいチャンクを挿入するため、すでに最新のデータベースに対してインクリメンタルインデックスを再実行しても変更はゼロになります。フルインデックスの再実行では、同一のデータベースが生成されます。
破損からの復旧
SQLiteデータベースが破損した場合(書き込み中の電源喪失、ディスクエラー、トランザクション中のプロセス強制終了):
# Check integrity
sqlite3 vectors.db "PRAGMA integrity_check;"
# If corruption detected, full reindex rebuilds from source files
python index_vault.py --full
信頼できるデータソースは常にVaultのファイルであり、データベースではありません。データベースはいつでも再構築可能な派生的な成果物です。これは重要な設計上の特性です:データベースのバックアップは一切不要です。
--incremental フラグ
インデクサーが --incremental で実行された場合:
- モデルハッシュのチェック。 保存されたモデルハッシュと現在のモデルを比較します。異なる場合は、自動的にフルリインデックスモードに切り替え、ユーザーに警告します。
- ファイルスキャン。 許可されたフォルダーを走査し、ファイルパスとmtimeを収集します。
- 変更検出。 保存されたデータと比較します。
- バッチ処理。 変更されたファイルを64件ずつのバッチで再チャンク化・再エンベディングします。
- 進捗レポート。 処理済みファイル数と経過時間を出力します。
- グレースフルシャットダウン。 SIGINTを受信した場合、現在のファイルの処理を完了してから停止します。
クレデンシャルフィルタリングとデータ境界
個人のノートにはシークレットが含まれていることがあります:APIキー、Bearerトークン、データベース接続文字列、デバッグ中に貼り付けられた秘密鍵など。クレデンシャルフィルターは、これらが検索インデックスに入り込むのを防ぎます。
問題
OAuth連携のデバッグに関するノートには、以下のような内容が含まれている可能性があります:
The token was: eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...
I used this curl command:
curl -H "Authorization: Bearer sk-ant-api03-abc123..."
フィルタリングがなければ、JWTとAPIキーの両方がチャンク化、エンベディング、データベースへの保存の対象となります。「authentication」で検索すると、実際のシークレットを含むチャンクが返されてしまいます。さらに悪いことに、リトリーバーがMCPを通じてAIツールに結果を渡す場合、シークレットがAIのコンテキストウィンドウに表示され、ツールのログにも記録される可能性があります。
パターンベースのフィルタリング
クレデンシャルフィルターは、保存前のすべてのチャンクに対して実行され、25のベンダー固有パターンと汎用パターンをマッチングします:
ベンダー固有パターン:
| パターン | 例 | 正規表現 |
|---|---|---|
| OpenAI APIキー | sk-... |
sk-[a-zA-Z0-9_-]{20,} |
| Anthropic APIキー | sk-ant-api03-... |
sk-ant-api\d{2}-[a-zA-Z0-9_-]{20,} |
| GitHub PAT | ghp_... |
gh[ps]_[a-zA-Z0-9]{36,} |
| AWSアクセスキー | AKIA... |
AKIA[0-9A-Z]{16} |
| Stripeキー | sk_live_... |
[sr]k_(live\|test)_[a-zA-Z0-9]{24,} |
| Cloudflareトークン | ... |
各種パターン |
汎用パターン:
| パターン | 検出方法 |
|---|---|
| JWTトークン | eyJ[a-zA-Z0-9_-]+\.eyJ[a-zA-Z0-9_-]+ |
| Bearerトークン | Bearer\s+[a-zA-Z0-9_\-\.]+ |
| 秘密鍵 | -----BEGIN (RSA\|EC\|OPENSSH) PRIVATE KEY----- |
| 高エントロピーbase64 | 4.5ビット/文字を超えるエントロピーを持つ40文字以上の文字列 |
| パスワード代入 | password\s*[:=]\s*["'][^"']+["'] |
フィルターの実装
def clean_content(text):
"""Scrub credentials from text before indexing."""
result = ScanResult(is_clean=True, match_count=0, patterns=[])
for pattern in CREDENTIAL_PATTERNS:
matches = pattern.regex.findall(text)
if matches:
text = pattern.regex.sub(
f"[REDACTED:{pattern.name}]", text
)
result.is_clean = False
result.match_count += len(matches)
result.patterns.append(pattern.name)
return text, result
主要な設計上の選択:
-
エンベディング前にフィルタリング。 クリーニングされたテキストがエンベディングの対象となります。ベクトル表現にはクレデンシャルパターンがエンコードされることはありません。「API key」というクエリでは、実際のキーを含むノートではなく、APIキーの管理について議論しているノートが返されます。
-
削除ではなく置換。
[REDACTED:pattern-name]トークンにより、周囲のテキストのセマンティックなコンテキストが保持されます。エンベディングは「ここにクレデンシャルのようなものがあった」ということを、クレデンシャル自体をエンコードすることなくキャプチャします。 -
値ではなくパターンをログに記録。 フィルターはマッチしたパターン名(例:「Scrubbed 2 credential(s) from oauth-debug.md [jwt, bearer-token]」)をログに記録しますが、クレデンシャルの値そのものは決してログに記録しません。
パスベースの除外
.indexignore ファイルはパスによる粗い粒度の除外を提供します。クレデンシャルフィルターはインデックス対象ファイル内の細かい粒度のスクラビングを提供します。両方が必要です:
.indexignoreは、機密コンテンツが含まれていることが明らかなフォルダー全体に使用します(健康に関するノート、財務記録、キャリア関連のドキュメント)- クレデンシャルフィルターは、インデックス対象のコンテンツに誤って埋め込まれたシークレットに使用します
データ分類
多様なコンテンツを含むVaultでは、ノートを機密レベル別に分類することを検討してください:
| レベル | 例 | インデックス対象? | フィルター適用? |
|---|---|---|---|
| 公開 | ブログの下書き、技術ノート | はい | はい |
| 社内 | プロジェクト計画、アーキテクチャ決定 | はい | はい |
| 機密 | 給与データ、健康記録 | いいえ(.indexignore) | 該当なし |
| 制限付き | クレデンシャル、秘密鍵 | いいえ(.indexignore) | 該当なし |
MCP サーバーアーキテクチャ
Model Context Protocol(MCP)サーバーは、AIエージェントが呼び出せるツールとしてリトリーバーを公開します。このセクションでは、サーバーの設計、機能の範囲、および権限の境界について説明します。
プロトコルの選択:STDIO vs HTTP
MCPは2つのトランスポートモードをサポートしています:
STDIO — AIツールがMCPサーバーを子プロセスとして起動し、stdin/stdoutを介して通信します。これはローカルツールの標準モードです。Claude Code、Codex CLI、CursorはすべてSTDIO MCPサーバーをサポートしています。
{
"mcpServers": {
"obsidian": {
"command": "python",
"args": ["/path/to/obsidian_mcp.py"],
"env": {
"VAULT_PATH": "/path/to/vault",
"DB_PATH": "/path/to/vectors.db"
}
}
}
}
HTTP — MCPサーバーがスタンドアロンのHTTPサービスとして動作します。リモートアクセス、マルチクライアント構成、またはVaultが共有サーバー上にあるチーム構成に適しています。
{
"mcpServers": {
"obsidian": {
"url": "http://localhost:3333/mcp"
}
}
}
推奨: 個人のVaultにはSTDIOを使用してください。よりシンプルで、より安全(ネットワーク露出なし)であり、サーバーのライフサイクルはAIツールによって管理されます。複数のツールや複数のマシンが同じVaultに同時アクセスする必要がある場合にのみHTTPを使用してください。
MCP仕様の進化。 2025年6月のMCP仕様では、OAuth 2.1認可、構造化されたツール出力(型付き戻り値スキーマ)、およびエリシテーション(サーバー起点のユーザープロンプト)が追加されました。2025年11月のリリースでは、Streamable HTTPがファーストクラスのトランスポートモードとして出荷され、
.well-knownURLディスカバリによるサーバー機能の自動ブラウジング、ツールが読み取り専用か変更操作かを宣言する構造化ツールアノテーション、およびSDKティアの標準化システムが含まれました。1619 次の仕様リリース(暫定的に2026年中頃)では、長時間実行タスク向けの非同期オペレーション、ヘルスケアや金融などの業界向けドメイン固有プロトコル拡張、およびマルチエージェントワークフロー向けのエージェント間通信標準が提案されています。19 個人のVaultサーバーにはSTDIOが最もシンプルな方法です。Streamable HTTPトランスポートと.well-knownディスカバリは、主にマルチテナントルーティングとロードバランシングを伴うエンタープライズHTTPデプロイメントに有用です。トランスポートの選択に影響する更新については、MCPロードマップを確認してください。
機能設計
MCPサーバーは最小限のツールセットを公開するべきです:
search — 主要なツールです。hybridリトリーバルを実行し、ランク付けされた結果を返します。
{
"name": "obsidian_search",
"description": "Search the Obsidian vault using hybrid BM25 + vector retrieval",
"parameters": {
"query": { "type": "string", "description": "Search query" },
"limit": { "type": "integer", "default": 5 },
"max_tokens": { "type": "integer", "default": 2000 }
}
}
read_note — パスを指定してノートの全内容を読み取ります。エージェントが検索結果の完全なコンテキストを確認したい場合に便利です。
{
"name": "obsidian_read_note",
"description": "Read the full content of a note by file path",
"parameters": {
"file_path": { "type": "string", "description": "Relative path within vault" }
}
}
list_notes — フィルター(フォルダ、タグ、タイプ、または日付範囲)に一致するノートを一覧表示します。エージェントが特定のクエリを持っていない場合の探索に便利です。
{
"name": "obsidian_list_notes",
"description": "List notes matching filters",
"parameters": {
"folder": { "type": "string", "description": "Folder path within vault" },
"tag": { "type": "string", "description": "Tag to filter by" },
"limit": { "type": "integer", "default": 20 }
}
}
get_context — 検索を実行し、会話に注入するのに適したコンテキストブロックとして結果をフォーマットするコンビニエンスツールです。
{
"name": "obsidian_get_context",
"description": "Get formatted context from vault for a topic",
"parameters": {
"topic": { "type": "string", "description": "Topic to get context for" },
"max_tokens": { "type": "integer", "default": 2000 }
}
}
権限の境界
MCPサーバーは厳格な境界を適用するべきです:
-
読み取り専用。 サーバーはVaultとインデックスデータベースを読み取ります。ノートの作成、変更、削除は行いません。書き込み操作(新しいノートのキャプチャ)は、MCPサーバーではなく、別のフックやスキルによって処理されます。
-
Vaultスコープ。 サーバーは設定されたVaultパス内のファイルのみを読み取ります。パストラバーサルの試み(
../../etc/passwd)は拒否する必要があります。 -
資格情報フィルタリングされた出力。 データベースに事前フィルタリングされたコンテンツが含まれている場合でも、多層防御として出力時に資格情報フィルタリングを適用します。
-
トークン制限付きレスポンス。 すべてのツールレスポンスに
max_tokensを適用し、AIツールが過度に大きなコンテキストブロックを受け取ることを防止します。
エラーハンドリング
MCPツールは、AIツールがリカバリできるよう構造化されたエラーメッセージを返すべきです:
def search(self, query, limit=5, max_tokens=2000):
if not self.db_path.exists():
return {
"error": "Index database not found. Run the indexer first.",
"suggestion": "python index_vault.py --full"
}
results = self.retriever.search(query, limit, max_tokens)
if not results:
return {
"results": [],
"message": f"No results found for '{query}'. Try broader terms."
}
return {
"results": [
{
"file_path": r["file_path"],
"section": r["section"],
"text": r["chunk_text"],
"score": round(r["rrf_score"], 4),
}
for r in results
],
"count": len(results),
"query": query,
}
Claude Codeとの統合
Claude CodeはObsidianリトリーバルシステムの主要な利用者です。このセクションでは、MCPの設定、フック統合、およびobsidian_bridge.pyパターンについて説明します。
MCPの設定
Obsidian MCPサーバーを~/.claude/settings.jsonに追加します:
{
"mcpServers": {
"obsidian": {
"command": "python",
"args": ["/path/to/obsidian_mcp.py"],
"env": {
"VAULT_PATH": "/absolute/path/to/vault",
"DB_PATH": "/absolute/path/to/vectors.db"
}
}
}
}
設定を追加したら、Claude Codeを再起動します。MCPサーバーが子プロセスとして起動します。動作を確認するには:
> What tools do you have from the obsidian MCP server?
Claude Codeは利用可能なツール(obsidian_search、obsidian_read_noteなど)を一覧表示するはずです。
フック統合
フックは定義されたライフサイクルポイントでClaude Codeの動作を拡張します。Obsidian統合に関連するフックは2つあります:
PreToolUseフック — エージェントがツールコールを処理する前にVaultをクエリします。関連するコンテキストを自動的に注入します。
#!/bin/bash
# ~/.claude/hooks/pre-tool-use/obsidian-context.sh
# Automatically inject vault context before tool execution
TOOL_NAME="$1"
PROMPT="$2"
# Only inject context for code-related tools
case "$TOOL_NAME" in
Edit|Write|Bash)
# Query the vault
CONTEXT=$(python /path/to/retriever.py search "$PROMPT" --limit 3 --max-tokens 1500)
if [ -n "$CONTEXT" ]; then
echo "---"
echo "Relevant vault context:"
echo "$CONTEXT"
echo "---"
fi
;;
esac
PostToolUseフック — 重要なツール出力をVaultにキャプチャし、将来のリトリーバルに備えます。
#!/bin/bash
# ~/.claude/hooks/post-tool-use/capture-insight.sh
# Capture significant outputs to vault (selective)
TOOL_NAME="$1"
OUTPUT="$2"
# Only capture substantial outputs
if [ ${#OUTPUT} -gt 500 ]; then
python /path/to/capture.py --text "$OUTPUT" --source "claude-code-$TOOL_NAME"
fi
obsidian_bridge.pyパターン
ブリッジモジュールは、フックやスキルが呼び出せるPython APIを提供します:
# obsidian_bridge.py
from retriever import HybridRetriever
_retriever = None
def get_retriever():
global _retriever
if _retriever is None:
_retriever = HybridRetriever(
db_path="/path/to/vectors.db",
vault_path="/path/to/vault",
)
return _retriever
def search_vault(query, limit=5, max_tokens=2000):
"""Search vault and return formatted context."""
retriever = get_retriever()
results = retriever.search(query, limit, max_tokens)
if not results:
return ""
lines = ["## Vault Context\n"]
for r in results:
lines.append(f"**{r['file_path']}** — {r['section']}")
lines.append(f"> {r['chunk_text'][:500]}")
lines.append("")
return "\n".join(lines)
/captureスキル
インサイトをVaultにキャプチャするためのClaude Codeスキルです:
/capture "OAuth token rotation requires both access and refresh token invalidation"
--domain security
--tags oauth,tokens
このスキルは00-inbox/に適切なfrontmatterを持つ新しいノートを作成し、インクリメンタルな再インデックスをトリガーして、新しいノートがすぐに検索可能になるようにします。
コンテキストウィンドウの管理
統合はClaude Codeのコンテキストウィンドウに配慮する必要があります:
- 注入するコンテキストはクエリあたり1,500〜2,000トークンに制限します。 これを超えると、エージェントの作業メモリと競合します。
- ソースの帰属を含めます。 エージェントがソースを参照できるよう、常にファイルパスとセクション見出しを含めてください。
- チャンクテキストを切り詰めます。 長いチャンクは完全に省略するのではなく、
...で切り詰めるべきです。通常、最初の300〜500文字に重要な情報が含まれています。 - すべてのツールコールでコンテキストを注入しないでください。 PreToolUseフックは、呼び出されるツールに基づいて選択的にコンテキストを注入するべきです。読み取り操作にはVaultコンテキストは不要です。書き込みおよびEdit操作では効果的です。
Codex CLI との統合
Codex CLI は config.toml を通じて MCP サーバーに接続します。統合パターンは Claude Code とは設定構文と指示の配信方法が異なります。
MCP の設定
.codex/config.toml または ~/.codex/config.toml に以下を追加します:
[mcp_servers.obsidian]
command = "python"
args = ["/path/to/obsidian_mcp.py"]
[mcp_servers.obsidian.env]
VAULT_PATH = "/absolute/path/to/vault"
DB_PATH = "/absolute/path/to/vectors.db"
AGENTS.md のパターン
Codex CLI はプロジェクトレベルの指示を AGENTS.md から読み込みます。Vault検索のガイダンスを含めてください:
## Available Tools
### Obsidian Vault (MCP: obsidian)
Use the `obsidian_search` tool to find relevant context from the knowledge base.
Search the vault when you need:
- Background on a concept or pattern
- Prior decisions or rationale
- Reference material for implementation
Example queries:
- "authentication patterns in FastAPI"
- "how does the review aggregator work"
- "sqlite-vec configuration"
Claude Code との違い
| 機能 | Claude Code | Codex CLI |
|---|---|---|
| MCP 設定 | settings.json |
config.toml |
| フック | ~/.claude/hooks/ |
非対応 |
| スキル | ~/.claude/skills/ |
非対応 |
| 指示ファイル | CLAUDE.md |
AGENTS.md |
| 承認モード | --dangerously-skip-permissions |
suggest / auto-edit / full-auto |
主な違い: Codex CLI はフックをサポートしていません。自動コンテキスト注入パターン(PreToolUse フック)は利用できません。代わりに、作業開始前にVaultを検索するよう明示的な指示を AGENTS.md に記載してください。
Cursor およびその他のツール
Cursor や MCP をサポートする他のAIツールは、同じ Obsidian MCP サーバーに接続できます。このセクションでは、主要なツールの設定方法を説明します。
Cursor
プロジェクトルートの .cursor/mcp.json に以下を追加します:
{
"mcpServers": {
"obsidian": {
"command": "python",
"args": ["/path/to/obsidian_mcp.py"],
"env": {
"VAULT_PATH": "/absolute/path/to/vault",
"DB_PATH": "/absolute/path/to/vectors.db"
}
}
}
}
Cursor の .cursorrules ファイルにVaultの利用指示を記載できます:
When working on implementation tasks, search the Obsidian vault
for relevant context before writing code. Use the obsidian_search
tool with descriptive queries about the concept you're implementing.
互換性マトリクス
| ツール | MCP サポート | トランスポート | 設定ファイルの場所 |
|---|---|---|---|
| Claude Code | 完全対応 | STDIO | ~/.claude/settings.json |
| Codex CLI | 完全対応 | STDIO | .codex/config.toml |
| Cursor | 完全対応 | STDIO | .cursor/mcp.json |
| Windsurf | 完全対応 | STDIO | .windsurf/mcp.json |
| Continue.dev | 部分対応 | HTTP | ~/.continue/config.json |
| Zed | 開発中 | STDIO | 設定UI |
MCP 非対応ツールへのフォールバック
MCP をサポートしないツールの場合、リトリーバーを CLI としてラップできます:
# Search from command line
python retriever_cli.py search "query text" --limit 5
# Output formatted for copy-paste into any tool
python retriever_cli.py context "query text" --format markdown
この CLI は構造化されたテキストを出力し、任意のAIツールの入力に手動でペーストできます。MCP 統合ほどスマートではありませんが、あらゆるツールで利用可能です。
構造化ノートからのプロンプトキャッシング
Vault内の構造化ノートは、AI操作全体でトークン使用量を削減する再利用可能なコンテキストブロックとして機能します。このセクションでは、キャッシュキーの設計とトークンバジェット管理について説明します。
パターン
操作のたびにコンテキストを検索する代わりに、Vault内の適切に構造化されたノートからコンテキストブロックを事前に構築してキャッシュします:
# cache_keys.py
CONTEXT_BLOCKS = {
"auth-patterns": {
"vault_query": "authentication patterns implementation",
"max_tokens": 1500,
"ttl_hours": 24, # Rebuild daily
},
"api-conventions": {
"vault_query": "API design conventions REST patterns",
"max_tokens": 1000,
"ttl_hours": 168, # Rebuild weekly
},
"project-architecture": {
"vault_query": "current project architecture decisions",
"max_tokens": 2000,
"ttl_hours": 12, # Rebuild twice daily
},
}
キャッシュの無効化
キャッシュの無効化は2つのシグナルに基づいています:
- TTLの期限切れ。 各コンテキストブロックには有効期限(TTL)があります。TTLが期限切れになると、Vaultを再クエリしてブロックが再構築されます。
- Vaultの変更検知。 キャッシュされたコンテキストブロックに寄与したファイルの変更をインデクサーが検知すると、そのブロックは即座に無効化されます。
トークンバジェット管理
セッションはコンテキストバジェットの合計で開始されます。キャッシュブロックはそのバジェットの一部を消費します:
Total context budget: 8,000 tokens
├─ System prompt: 1,500 tokens
├─ Cached blocks: 3,000 tokens (pre-loaded)
├─ Dynamic search: 2,000 tokens (on-demand)
└─ Conversation: 1,500 tokens (remaining)
キャッシュブロックはセッション開始時に読み込まれます。動的検索結果はクエリごとに残りのバジェットを使用します。このハイブリッドアプローチにより、エージェントは頻繁に必要なコンテキストのベースラインを確保しながら、特定のクエリに対するバジェットも維持できます。
キャッシュ適用前後のトークン使用量比較
キャッシュなしの場合: 関連するクエリごとにVault検索が実行され、1,500〜2,000トークンのコンテキストが返されます。1セッションで10回のクエリを実行すると、エージェントは15,000〜20,000トークンのVaultコンテキストを消費します。
キャッシュありの場合: 3つの事前構築コンテキストブロックで合計4,500トークンを消費します。追加の検索ではユニークなクエリごとに1,500〜2,000トークンが加算されます。10回のクエリのうち6回がキャッシュブロックでカバーされる場合、エージェントは4,500 +(4 × 1,500)= 10,500トークンを消費します。これはキャッシュなしの場合のおよそ半分です。
PostToolUse フックによるコンテキスト圧縮
ツールの出力は冗長になりがちです:スタックトレース、ファイル一覧、テスト結果など。PostToolUse フックを使用すると、これらの出力がコンテキストウィンドウを消費する前に圧縮できます。
課題
テストを実行する Bash ツールの呼び出しは、以下のような出力を返す場合があります:
PASSED tests/test_auth.py::test_login_success
PASSED tests/test_auth.py::test_login_failure
PASSED tests/test_auth.py::test_token_refresh
PASSED tests/test_auth.py::test_session_expiry
... (200 more lines)
FAILED tests/test_api.py::test_rate_limit_exceeded
完全な出力は5,000トークンですが、重要な情報はわずか2行です:200件成功、1件失敗。
フックの実装
#!/bin/bash
# ~/.claude/hooks/post-tool-use/compress-output.sh
# Compress verbose tool outputs to preserve context window
TOOL_NAME="$1"
OUTPUT="$2"
OUTPUT_LEN=${#OUTPUT}
# Only compress large outputs
if [ "$OUTPUT_LEN" -lt 2000 ]; then
exit 0 # Pass through unchanged
fi
case "$TOOL_NAME" in
Bash)
# Compress test output
if echo "$OUTPUT" | grep -q "PASSED\|FAILED"; then
PASSED=$(echo "$OUTPUT" | grep -c "PASSED")
FAILED=$(echo "$OUTPUT" | grep -c "FAILED")
FAILURES=$(echo "$OUTPUT" | grep "FAILED")
echo "Tests: $PASSED passed, $FAILED failed"
if [ "$FAILED" -gt 0 ]; then
echo "Failures:"
echo "$FAILURES"
fi
fi
;;
esac
再帰トリガーの防止
圧縮フックが出力を生成すると、ガードがない場合に自身を再トリガーする可能性があります:
# Guard against recursive invocation
if [ -n "$COMPRESS_HOOK_ACTIVE" ]; then
exit 0
fi
export COMPRESS_HOOK_ACTIVE=1
圧縮ヒューリスティクス
| 出力タイプ | 検出方法 | 圧縮戦略 |
|---|---|---|
| テスト結果 | PASSED / FAILED キーワード |
成功/失敗をカウントし、失敗のみ表示 |
| ファイル一覧 | コマンド内の ls または find |
最初の20件に切り詰め+合計数を表示 |
| スタックトレース | Traceback キーワード |
最初と最後のフレーム+エラーメッセージを保持 |
| Git ステータス | modified: / new file: |
ステータスごとの件数を要約 |
| ビルド出力 | warning: / error: |
情報行を除去し、警告/エラーのみ保持 |
シグナル取り込みとトリアージパイプライン
取り込みレイヤーは、Vault に何を入れるかを決定します。キュレーションがなければ、Vault にはノイズが蓄積されます。このセクションでは、シグナルをドメインフォルダにルーティングするスコアリングパイプラインについて解説します。
ソース
シグナルは複数のチャネルから取得されます:
- RSSフィード: 技術ブログ、セキュリティアドバイザリー、リリースノート
- ブックマーク: Obsidian Web Clipperまたはブックマークレットで保存したブラウザブックマーク
- ニュースレター: メールニュースレターからの重要な抜粋
- 手動キャプチャ: 読書中、会話中、またはリサーチ中に書いたメモ
- ツール出力: フックを通じてキャプチャされた重要なAIツールの出力
スコアリングの4つの軸
各シグナルは4つの軸(それぞれ0.0〜1.0)でスコアリングされます:
| 軸 | 判断基準 | 低スコア(0.0-0.3) | 高スコア(0.7-1.0) |
|---|---|---|---|
| 関連性 | アクティブなドメインに関連するか? | 周辺的、スコープ外 | 進行中の作業に直接関連 |
| 実行可能性 | この情報を活用できるか? | 純粋な理論、応用不可 | 適用可能な具体的なテクニックやパターン |
| 深度 | コンテンツはどの程度実質的か? | 見出しレベル、浅いサマリー | 具体例を伴う詳細な分析 |
| 権威性 | ソースの信頼性は? | 匿名ブログ、未検証 | 一次ソース、査読済み、著名な専門家 |
複合スコアとルーティング
composite = (relevance * 0.35) + (actionability * 0.25) +
(depth * 0.25) + (authority * 0.15)
| スコア範囲 | アクション |
|---|---|
| 0.55以上 | ドメインフォルダに自動ルーティング |
| 0.40 - 0.55 | 手動レビューのキューに追加 |
| 0.40未満 | 破棄(保存しない) |
ドメインルーティング
0.55以上のスコアを獲得したシグナルは、キーワードマッチングとトピック分類に基づいて12のドメインフォルダのいずれかにルーティングされます:
05-signals/
├── ai-tooling/ # Claude, LLMs, AI development tools
├── security/ # Vulnerabilities, auth, cryptography
├── systems/ # Architecture, distributed systems
├── programming/ # Languages, patterns, algorithms
├── web/ # Frontend, backends, APIs
├── data/ # Databases, data engineering
├── devops/ # CI/CD, containers, infrastructure
├── design/ # UI/UX, product design
├── mobile/ # iOS, Android, cross-platform
├── career/ # Industry trends, hiring, growth
├── research/ # Academic papers, whitepapers
└── other/ # Signals that don't fit a domain
本番環境の統計
14ヶ月間の運用実績:
| 指標 | 値 |
|---|---|
| 処理されたシグナル総数 | 7,771 |
| 自動ルーティング(0.55超) | 4,832(62%) |
| レビュー待ち(0.40-0.55) | 1,543(20%) |
| 破棄(0.40未満) | 1,396(18%) |
| アクティブなドメインフォルダ数 | 12 |
| 1日あたりの平均シグナル数 | 約18 |
ナレッジグラフパターン
Obsidianのwiki-linkグラフは、ノート間の関係性をエンコードします。このセクションでは、リンクのセマンティクス、コンテキスト拡張のためのグラフトラバーサル、およびグラフ品質を低下させるアンチパターンについて解説します。
Backlinkのセマンティクス
すべてのwiki-linkはグラフ内の有向エッジを生成します。Obsidianはフォワードリンクとbacklinkの両方を追跡します:
- フォワードリンク: ノートAが
[[Note B]]を含む → AがBにリンク - Backlink: ノートBに、ノートAが参照していることが表示される
グラフは、コンテキストに応じて異なるタイプの関係性をエンコードします:
| リンクパターン | セマンティクス | 例 |
|---|---|---|
| インラインリンク | 「〜に関連する」 | “See [[OAuth Token Rotation]] for details” |
| ヘッダーリンク | 「〜のサブトピックを持つ」 | ”## Related\n- [[Token Rotation]]\n- [[Session Management]]” |
| タグ型リンク | 「〜に分類される」 | ”[[type/reference]]” |
| MOCリンク | 「〜の一部である」 | 関連ノートをまとめたMaps of Contentノート |
Maps of Content(MOC)
MOCは、関連するノートをナビゲーション可能な構造に整理するインデックスノートです:
---
title: "Authentication & Security MOC"
type: moc
domain: security
---
## Core Concepts
- [[OAuth 2.0 Overview]]
- [[JWT Token Anatomy]]
- [[Session Management Patterns]]
## Implementation Patterns
- [[OAuth Token Rotation]]
- [[Refresh Token Security]]
- [[PKCE Flow Implementation]]
## Failure Modes
- [[Token Expiry Handling]]
- [[Session Fixation Prevention]]
- [[CSRF Defense Strategies]]
MOCは検索において2つの点で有益です:
- 直接マッチ。 「authentication overview」の検索がMOC自体にマッチし、エージェントに関連ノートのキュレーション済みリストを提供します。
- コンテキスト拡張。 特定のノートを見つけた後、リトリーバーはそのノートがいずれかのMOCに含まれているかを確認し、MOCの構造を結果に含めることで、エージェントにより広いトピックの全体像を提供できます。
コンテキスト拡張のためのグラフトラバーサル
リトリーバーの将来的な拡張案:上位の結果を取得した後、リンクをたどってコンテキストを拡張します:
def expand_context(results, depth=1):
"""Follow wiki-links from top results to find related context."""
expanded = set()
for result in results:
# Parse wiki-links from chunk text
links = extract_wiki_links(result["chunk_text"])
for link_target in links:
# Resolve link to file path
target_path = resolve_wiki_link(link_target)
if target_path and target_path not in expanded:
expanded.add(target_path)
# Include target's most relevant chunk
target_chunks = get_chunks_for_file(target_path)
# ... rank and include best chunk
return results + list(expanded_results)
これは現在のリトリーバーには実装されていませんが、グラフ構造の自然な拡張として考えられます。
アンチパターン
孤立クラスター。 互いにリンクしているが、Vault の残りの部分とは接続のないノートのグループです。Obsidianのグラフパネルでは、切り離された島として可視化されます。孤立クラスターは、MOCの欠如またはクロスドメインリンクの不足を示しています。
タグの無秩序な増殖。 タグの一貫性のない使用や、過度に細分化されたタグの作成です。5,000件のノートに対して500個のユニークタグがある Vault では、10タグに1ノートの平均となり、フィルタリングに役立ちません。ドメインフォルダに対応する20〜50個の上位レベルタグに集約しましょう。
リンク過多・コンテンツ不足のノート。 wiki-linkだけで構成され、散文のないノートです。チャンカーにはembeddingするテキストがないため、インデックス品質が低下します。リンクされたノート同士がなぜ関連するのかを説明する、少なくとも1段落のコンテキストを追加してください。
何でも双方向リンク。 すべての言及がwiki-linkである必要はありません。「OAuth」を文中で軽く言及する場合、[[OAuth 2.0 Overview]] にする必要はありません。wiki-linkは、クリックして有用なコンテキストが得られるような、意図的でナビゲーション可能な関係性に限定しましょう。
開発者ワークフローレシピ
Vault の検索と日常の開発タスクを組み合わせた実用的なワークフローです。
朝のコンテキストロード
関連するコンテキストをロードして一日を始めます:
Search my vault for notes about [current project] updated in the last week
リトリーバーがアクティブなプロジェクトに関する最近のノートを返し、前回の作業状況を素早く思い出すことができます。昨日のコミットメッセージを読み返すよりも効果的です。
コーディング中のリサーチキャプチャ
機能の実装中に、エディタを離れることなくインサイトをキャプチャします:
/capture "FastAPI dependency injection with async generators requires yield,
not return. The generator is the dependency lifecycle."
--domain programming
--tags fastapi,dependency-injection
キャプチャされたインサイトは即座にインデックスされ、将来の検索で利用可能になります。数ヶ月の蓄積で、これらのマイクロキャプチャは実装固有のナレッジコーパスを構築します。
プロジェクトキックオフ
新しいプロジェクトや機能を開始するとき:
- Vault を検索:「[技術/パターン]について何を知っているか?」
- 上位5件の結果から、過去の意思決定と注意点をレビュー
- そのドメインのMOCが存在するか確認し、なければ作成
- 障害モードを検索:「[技術]の問題点」
Vault 検索によるデバッグ
エラーや予期しない動作に遭遇した場合:
Search my vault for [error message or symptom]
過去のデバッグノートには、多くの場合、根本原因と修正方法が含まれています。プロジェクト間で繰り返し発生する問題に対して特に有用です — Vault はあなたが忘れたことを覚えています。
コードレビューの準備
PRをレビューする前に:
Search my vault for patterns and conventions about [module being changed]
Vault は、レビュー対象のコードに関連する過去の意思決定、アーキテクチャ上の制約、コーディング規約を返します。レビューが差分だけでなく、組織的なナレッジに基づいて行われるようになります。
パフォーマンスチューニング
このセクションでは、さまざまなVaultサイズと使用パターンに応じた最適化戦略について説明します。
インデックスサイズの管理
| Vaultサイズ | チャンク数 | DBサイズ | フルリインデックス | インクリメンタル |
|---|---|---|---|---|
| 500ノート | ~1,500 | 3 MB | 15秒 | 1秒未満 |
| 2,000ノート | ~6,000 | 12 MB | 45秒 | 2秒 |
| 5,000ノート | ~15,000 | 30 MB | 2分 | 4秒 |
| 15,000ノート | ~50,000 | 83 MB | 4分 | 10秒未満 |
| 50,000ノート | ~150,000 | 250 MB | 15分 | 30秒 |
50,000ノート以上の場合は、以下を検討してください: - バッチサイズを64から128に増やして、embeddingの生成を高速化する - WALモード(デフォルト)を使用して、並行アクセスに対応する - フルリインデックスをオフピーク時間帯に実行する
クエリの最適化
WALモード。 SQLiteのWrite-Ahead Loggingモードは、インデクサーが書き込みを行っている間も並行読み取りを可能にします:
db.execute("PRAGMA journal_mode=WAL")
これは、MCPサーバーがクエリを処理している間にインデクサーがインクリメンタルアップデートを実行する場合に重要です。
コネクションプーリング。 MCPサーバーは、クエリごとに新しいコネクションを開くのではなく、データベースコネクションを再利用するべきです。WALモードと組み合わせた単一の長寿命コネクションで、並行読み取りをサポートできます。
# MCP server initialization
db = sqlite3.connect(DB_PATH, check_same_thread=False)
db.execute("PRAGMA journal_mode=WAL")
db.execute("PRAGMA mmap_size=268435456") # 256 MB mmap
メモリマップドI/O。 mmap_sizeプラグマは、SQLiteにデータベースファイルのメモリマップドI/Oを使用するよう指示します。83 MBのデータベースの場合、ファイル全体をメモリにマッピングすることで、ほとんどのディスク読み取りを排除できます。
FTS5の最適化。 フルリインデックスの後、以下を実行してください:
INSERT INTO chunks_fts(chunks_fts) VALUES('optimize');
これにより、FTS5の内部b-treeセグメントがマージされ、以降の検索でのクエリレイテンシが低減されます。
スケーリングベンチマーク
Apple M3 Pro、36 GB RAM、NVMe SSDで測定:
| 操作 | 500ノート | 5Kノート | 15Kノート | 50Kノート |
|---|---|---|---|---|
| BM25クエリ | 2ms | 5ms | 12ms | 25ms |
| ベクトルクエリ | 1ms | 3ms | 8ms | 20ms |
| RRFフュージョン | 1ms未満 | 1ms未満 | 3ms | 5ms |
| フル検索 | 3ms | 8ms | 23ms | 50ms |
すべてのベンチマークには、データベースアクセス、クエリ実行、および結果フォーマットが含まれています。MCP STDIO通信のネットワークレイテンシとして1〜2msが追加されます。
トラブルシューティング
インデックスのドリフト
症状: 検索が古い結果を返す、または最近追加されたノートが検索結果に表示されない。
原因: ノートの追加後にインクリメンタルインデクサーが実行されなかった、またはファイルのmtimeが更新されなかった(例:タイムスタンプが保持されたまま別のマシンから同期された場合)。
修正方法: フルリインデックスを実行してください:python index_vault.py --full
Embeddingモデルの変更
症状: embeddingモデルの変更後、ベクトル検索が意味不明な結果を返す。
原因: 古いベクトル(以前のモデルで生成されたもの)が新しいクエリベクトルと比較されています。次元数やベクトル空間のセマンティクスに互換性がありません。
修正方法: インデクサーはモデルハッシュの不一致を検出し、自動的にフルリインデックスをトリガーするはずです。トリガーされない場合は、データベースを手動で削除してリインデックスしてください:
rm vectors.db
python index_vault.py --full
FTS5のメンテナンス
症状: 多数のインクリメンタルアップデート後に、FTS5クエリが不正確または不完全な結果を返す。
原因: 多数の小さなアップデートにより、FTS5の内部セグメントが断片化している可能性があります。
修正方法: 再構築と最適化を実行してください:
INSERT INTO chunks_fts(chunks_fts) VALUES('rebuild');
INSERT INTO chunks_fts(chunks_fts) VALUES('optimize');
MCPのタイムアウト
症状: AIツールがMCPサーバーのタイムアウトを報告する。
原因: 最初のクエリでモデルの読み込み(遅延初期化)がトリガーされ、2〜5秒かかります。AIツールのデフォルトのMCPタイムアウトがそれより短い可能性があります。
修正方法: サーバー起動時にモデルをプリウォームしてください:
# In MCP server initialization
retriever = HybridRetriever(db_path, vault_path)
retriever.search("warmup", limit=1) # Trigger model load
SQLiteのファイルロック
症状: SQLITE_BUSYまたはSQLITE_LOCKEDエラーが発生する。
原因: 複数のプロセスが同時にデータベースに書き込みを行っています。WALモードでは並行読み取りが可能ですが、書き込みは1つのプロセスのみです。
修正方法: データベースに書き込むのはインデクサーのみであることを確認してください。MCPサーバーとフックは読み取りのみを行うべきです。並行書き込みが必要な場合は、WALモードを使用してビジータイムアウトを設定してください:
db.execute("PRAGMA busy_timeout=5000") # Wait up to 5 seconds
sqlite-vecが読み込めない
症状: ベクトル検索が無効になり、リトリーバーがBM25のみのモードで動作する。
原因: sqlite-vec拡張がインストールされていない、ライブラリパスに見つからない、またはSQLiteのバージョンと互換性がない。
修正方法:
# Install via pip
pip install sqlite-vec
# Or compile from source
git clone https://github.com/asg017/sqlite-vec
cd sqlite-vec && make
拡張が正しく読み込まれるか確認してください:
import sqlite3
db = sqlite3.connect(":memory:")
db.enable_load_extension(True)
db.load_extension("vec0")
print("sqlite-vec loaded successfully")
大規模Vaultのメモリ問題
症状: 大規模Vault(50,000ノート以上)のフルリインデックス中にメモリ不足エラーが発生する。
原因: embeddingのバッチサイズが大きすぎる、またはすべてのファイル内容が同時にメモリに読み込まれている。
修正方法: バッチサイズを減らし、ファイルをインクリメンタルに処理してください:
BATCH_SIZE = 32 # Reduce from 64
また、インデクサーがすべてのファイルをメモリに読み込むのではなく、各ファイルを1つずつ処理する(読み取り、チャンキング、embeddingを順番に実行する)ようにしてください。
移行ガイド
Apple Notesからの移行
- 「すべてを書き出す」オプション(macOS)でApple Notesを書き出すか、
apple-notes-liberatorなどの移行ツールを使用します - HTMLエクスポートを
markdownifyまたはpandocでMarkdownに変換します - 変換されたファイルをVaultの
00-inbox/フォルダーに移動します - 各ノートを確認し、frontmatterを追加します
- ノートを適切なドメインフォルダーに移動します
Notionからの移行
- Notionからエクスポート:Settings → Export → Markdown & CSV
- エクスポートされたzipファイルをVaultの
00-inbox/フォルダーに展開します - Notion固有のMarkdownアーティファクトを修正します:
- Notionは
- [ ]をチェックリストに使用しています — これは標準的なMarkdownです - NotionはプロパティテーブルをHTMLとして含めます — YAML frontmatterに変換してください
- Notionは画像を相対パスで埋め込みます — 画像を添付ファイルフォルダーにコピーしてください
- 標準的なfrontmatter(
type、domain、tags)を追加します - NotionのページリンクをObsidianのwiki-linksに置き換えます
Google Docsからの移行
- Google Takeoutを使用してすべてのドキュメントをエクスポートします
.docxファイルをMarkdownに変換します:pandoc -f docx -t markdown input.docx -o output.md- 一括変換:
for f in *.docx; do pandoc -f docx -t markdown "$f" -o "${f%.docx}.md"; done - Vaultに移動し、frontmatterを追加し、フォルダーに整理します
プレーンMarkdown(Obsidianなし)からの移行
すでにMarkdownファイルのディレクトリがある場合:
- そのディレクトリをObsidian Vaultとして開きます(Obsidian → Open Vault → Open folder)
- ディレクトリがバージョン管理されている場合、
.obsidian/を.gitignoreに追加します - frontmatterテンプレートを作成し、既存のファイルに適用します
- 読みながらノートを整理し、
[[wiki-links]]でリンクを作成し始めます - インデクサーをすぐに実行してください — 検索システムは初日から機能します
別の検索システムからの移行
別のembedding/検索システムから移行する場合:
- ベクトルの移行は試みないでください。 異なるモデルは互換性のないベクトル空間を生成します。新しいモデルでフルリインデックスを実行してください。
- インデックスではなく、コンテンツを移行してください。 Vaultのファイルが信頼できるソースです。インデックスは派生的なアーティファクトです。
- 移行後に検証してください。 答えがわかっている10〜20のクエリを実行し、結果が期待と一致するか確認してください。
変更履歴
| 日付 | 変更内容 |
|---|---|
| 2026年3月3日 | MCP仕様の進化を更新(2025年11月リリース:Streamable HTTP、.well-known、ツールアノテーション)。Model2VecのファインチューニングとBPE/Unigramトークナイザーのサポートを追加。コミュニティMCPサーバー比較表を追加。Smart Connectionsをv4に更新。 |
| 2026年3月2日 | potion-base-32Mとpotion-retrieval-32Mをモデル比較に追加。量子化/次元削減セクションを追加。MCP仕様の進化に関する注記を追加。 |
| 2026年3月1日 | 初版リリース |
参考文献
-
Cormack, G.V., Clarke, C.L.A., and Buettcher, S. Reciprocal Rank Fusion outperforms Condorcet and individual Rank Learning Methods. SIGIR, 2009. ランク付きリストを統合するパラメータフリーな手法として、k=60のRRFを提案しています。 ↩↩↩
-
OpenAI Embeddings Pricing. text-embedding-3-small: 100万トークンあたり$0.02。Vault全体の再インデックスにかかる推定コスト: 約$0.30。 ↩
-
van Dongen, T. et al. Model2Vec: Turn any Sentence Transformer into a Small Fast Model. arXiv, 2025. Sentence Transformerから静的エンベディングを生成する蒸留アプローチについて説明しています。 ↩
-
MTEB: Massive Text Embedding Benchmark. potion-base-8Mの平均スコアは50.03で、all-MiniLM-L6-v2の56.09に対して89%の性能を維持しています。 ↩
-
SQLite FTS5 Extension. FTS5はBM25ランキングと設定可能なカラムウェイトによる全文検索を提供します。 ↩
-
sqlite-vec: A vector search SQLite extension. SQLite内でKNNベクトル検索を行うための
vec0仮想テーブルを提供します。 ↩ -
Robertson, S. and Zaragoza, H. The Probabilistic Relevance Framework: BM25 and Beyond. Foundations and Trends in Information Retrieval, 2009. ↩
-
Karpukhin, V. et al. Dense Passage Retrieval for Open-Domain Question Answering. EMNLP, 2020. 密なベクトル表現がオープンドメインQAにおいてBM25を9〜19%上回ることを示しています。 ↩
-
Reimers, N. and Gurevych, I. Sentence-BERT: Sentence Embeddings using Siamese BERT-Networks. EMNLP, 2019. 密な意味的類似度に関する基礎的研究です。 ↩
-
Luan, Y. et al. Sparse, Dense, and Attentional Representations for Text Retrieval. TACL, 2021. Hybrid検索がMS MARCOにおいて単一モダリティのアプローチを一貫して上回ることを示しています。 ↩
-
SQLite Write-Ahead Logging. 単一ライターでの並行読み取りを実現するWALモードについて説明しています。 ↩
-
Gao, Y. et al. Retrieval-Augmented Generation for Large Language Models: A Survey. arXiv, 2024. RAGアーキテクチャとチャンキング戦略に関するサーベイです。 ↩
-
Thakur, N. et al. BEIR: A Heterogeneous Benchmark for Zero-shot Evaluation of Information Retrieval Models. NeurIPS, 2021. ↩
-
Model2Vec: Distill a Small Fast Model from any Sentence Transformer. Minish Lab, 2024. ↩
-
Obsidian Documentation. Obsidianの公式ドキュメントです。 ↩
-
Model Context Protocol Specification. AIツールをデータソースに接続するためのMCP標準仕様です。 ↩
-
著者の本番データ。16,894ファイル、49,746チャンク、83.56 MBのSQLiteデータベース、14か月間で7,771シグナルを処理。クエリレイテンシは
time.perf_counter()で計測しています。 ↩ -
Model2Vec Potion Models. Minish Lab, 2025. Potion-base-32M(MTEB 52.46)、potion-retrieval-32M(MTEB retrieval 36.35)、およびv0.5.0以降の量子化・次元削減機能について説明しています。 ↩↩↩
-
Update on the Next MCP Protocol Release. 2025年11月リリースでは、Streamable HTTPトランスポート、.well-known URLディスカバリ、構造化ツールアノテーション、およびSDKティアの標準化が実装されました。次回リリースは暫定的に2026年中頃を予定しており、非同期操作、ドメイン固有の拡張機能、およびエージェント間通信が含まれる見込みです。 ↩↩
-
Model2Vec Releases. v0.4.0(2025年2月): トレーニング・ファインチューニングサポート。v0.5.0(2025年4月): バックエンドの書き直し、量子化、次元削減。v0.7.0(2025年10月): 語彙量子化、BPE/Unigramトークナイザーサポート。 ↩↩
-
Smart Connections for Obsidian. Smart Connections v4: ローカルファーストのAIエンベディング。初回インデックス作成後はオフラインでセマンティック検索が動作します。 ↩