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サーバーをインストールする
obsidian-mcpコミュニティサーバーを使えば、すぐにVaultにアクセスできます。以下のコマンドでインストールします:
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のファイルを読み取り、基本的な検索を実行して結果を返します。これが最小限の実用バージョンです。
このクイックスタートに含まれないもの: - Hybrid検索(BM25 + ベクトル検索 + RRF fusion) - 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をクエリ可能にします。これがエンジンです:ノートを検索単位にチャンキングし、チャンクをベクトル空間にembeddingし、キーワード検索とセマンティック検索用にインデックスを作成し、RRFで結果を統合します。Retrievalレイヤーは、ファイルのディレクトリをクエリ可能なナレッジベースに変換します。このレイヤーがなければ、Vaultは手動ブラウジングと基本的な検索でナビゲートできますが、AIツールからプログラム的にアクセスすることはできません。
Integration(統合) はRetrievalレイヤーをAIツールに接続します。MCPサーバーが検索を呼び出し可能なツールとして公開します。Hookがコンテキストを自動的に注入します。Skillが新しい知識をVaultに取り込みます。Integrationレイヤーは、ナレッジベースとそれを利用するAIエージェントとの間のインターフェースです。
各レイヤーは設計上分離されています。Intakeのスコアリングパイプラインはembeddingについて何も知りません。検索エンジンはシグナルルーティングルールについて何も知りません。MCPサーバーはノートがどのように作成されたかについて何も知りません。この分離により、各レイヤーを独立して改善できます。Intakeパイプラインを変更せずにembeddingモデルを入れ替えることができます。検索エンジンを修正せずに新しい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の文章を含むすべてのフォルダ — プロジェクト、エリア、リソース、シグナル、デイリーノートが該当します。
インデックスから除外すべきフォルダ: テンプレート(コンテンツではなくプレースホルダー変数を含むため)、添付ファイル(バイナリファイル)、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/
インデクサーはスキャン前にこのファイルを読み込み、一致するパスを完全にスキップします。除外されたパスのファイルはチャンク化されず、embeddings も生成されず、検索結果にも表示されません。
ノートスキーマ
すべてのノートには YAML frontmatter を設定する必要があります。検索エンジンは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のみ表示」や「シグナルのみ」など)tags— FTS5の見出しコンテキストに0.3のウェイトでインデックスされ、本文で異なる用語が使われていてもキーワードマッチを提供します
任意ですが有用なフィールド:
domain— ドメイン限定のクエリを可能にします(「セキュリティノートのみ検索」など)source— キャプチャしたコンテンツの出典情報。検索エンジンは結果にソースURLを含めることができますstatus— アクティブな検索からアーカイブ済みまたは下書きのノートを除外できます
チャンキング規則
検索エンジンは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つの独立して検索可能なチャンクを生成します。各チャンクにはembeddingsがその意味を捉えるのに十分なコンテキストがあります。「期限切れトークンの処理」に関するクエリは、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つの大きなチャンクを生成します。embeddingsはセクション内のすべてのトピックにわたって平均化されます。どのサブトピックに関するクエリでも、ノート全体に均等にマッチしてしまいます。
経験則: セクションが複数の概念をカバーしている場合は、H2サブセクションに分割してください。チャンカーが残りを処理します。
ノートに含めるべきでないコンテンツ
検索品質を低下させるコンテンツ:
- 注釈なしの記事全文のコピー&ペースト。 検索エンジンが元の記事のキーワードをインデックスするため、自分が書いていないコンテンツでVaultが薄まります。代わりに要約を追加するか、キーポイントを抽出するか、ソースURLにリンクしてください。
- テキスト説明のないスクリーンショット。 検索エンジンはMarkdownテキストをインデックスします。altテキストや周囲の説明がない画像は、BM25とベクトル検索の両方から見えません。
- 認証情報の文字列。 APIキー、トークン、パスワード、接続文字列など。認証情報フィルタリングがあっても、最も安全なアプローチはノートにシークレットを貼り付けないことです。代わりに名前で参照してください(「
~/.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
## 参考情報
Linter. Vault全体にフォーマットルールを適用します。一貫した見出し階層(H1はタイトル、H2はセクション、H3はサブセクション)により、チャンカーが予測可能な結果を生成します。検索に影響するLinterルール:
- 見出しの段階:見出しレベルの連続性を強制します(H1からH3への飛ばしを禁止)
- YAMLタイトル:ファイル名と一致させます
- 末尾スペース:削除します(FTS5のトークン化におけるアーティファクトを回避)
- 連続する空行:1行に制限します(よりクリーンなチャンクを生成)
Git連携. Vaultのバージョン管理を行います。変更履歴の追跡、マシン間の同期、誤削除からの復旧が可能です。Gitはインデクサーがインクリメンタルな変更検出に使用するmtimeデータも提供します。
インデックス作成に役立つプラグイン
Smart Connections. Obsidian内でAIによるセマンティック検索を提供するObsidianプラグインです。独自のembeddingインデックスを作成します。本ガイドの検索システムはObsidianの外部で動作しますが(Pythonパイプラインとして実行)、Smart Connectionsは執筆中にセマンティックな関連性を探索するのに役立ちます。両システムは同じコンテンツをインデックスしますが、異なるユースケースに対応します:Smart Connectionsはエディター内での発見に、外部リトリーバーはAIツールとの統合に使用します。
Metadata Menu. フィールド値のオートコンプリート付きで構造化されたfrontmatter編集を提供します。type、domain、tagsフィールドのタイプミスを削減します。一貫したメタデータにより、検索フィルタリングの精度が向上します。
インデックス作成に悪影響を与えるプラグイン
Excalidraw. マークダウンファイル内にJSONとして図面を保存します。JSONは構文的に有効なマークダウンですが、チャンク化してembeddingを生成すると意味のないデータになります。.indexignoreを使用するか、ファイル拡張子でフィルタリングして、Excalidrawファイルをインデックスから除外してください。
Kanban. ボードの状態を特殊なフォーマットのマークダウンとして保存します。このフォーマットはKanbanの表示用に設計されており、文章の検索には適していません。チャンカーはカードのタイトルやメタデータの断片を生成し、embeddingの品質が低下します。Kanbanボードはインデックスから除外してください。
Calendar. 最小限のコンテンツ(多くの場合、日付の見出しのみ)でデイリーノートを作成します。空または内容の少ないノートは低品質のチャンクを生成します。デイリーノートを使用する場合は、実質的なコンテンツを記述するか、デイリーノートフォルダをインデックスから除外してください。
重要なプラグイン設定
ファイルリカバリ → 有効. 誤ったノート削除から保護します。検索とは直接関係ありませんが、依存するナレッジベースにとって重要な設定です。
厳密な改行 → 無効. マークダウン標準の改行(段落間はダブル改行)は、Obsidianの厳密モード(シングル改行で<br>)よりもクリーンなチャンクを生成します。
新規ファイルのデフォルト保存場所 → 指定フォルダ. 新しいファイルを00-inbox/にルーティングすることで、未分類のノートがドメインフォルダを汚染しないようにします。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 のみ、静的単語 embedding(attention レイヤーなし)
Model2Vec は、sentence transformer の知識を静的トークン embedding に蒸留します。BERT、MiniLM、その他の transformer モデルのように入力に対して attention レイヤーを実行する代わりに、Model2Vec は事前計算されたトークン embedding の加重平均によってベクトルを生成します。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]
遅延読み込み。 モデルはインポート時ではなく、最初の使用時に読み込まれます。retriever が BM25 のみのフォールバックモードで動作している場合(例:embedding 用の venv がインストールされていない場合)、embedder モジュールのインポートにコストはかかりません。
隔離された仮想環境。 モデルは専用の 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 のオーバーヘッドを分散させます。indexer は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%増加します。embedding 速度は、M シリーズハードウェアでの15,000ファイルのフルリインデックスで1分未満から約10分に低下します。
nomic-embed-text-v1.5 を選ぶ場合: ローカルで最高の検索品質が必要で、インデキシングの遅延を許容できる場合です。768次元ベクトルにより、データベースサイズはおよそ3倍になります。PyTorch とモダンな CPU または GPU が必要です。
text-embedding-3-small を選ぶ場合: ネットワーク遅延とプライバシーのトレードオフが許容できる場合です。API は最高品質の embedding を生成しますが、クラウド依存、トークン単位のコスト(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次元への削減は、短いテキストの検索において最小限の品質低下でベクトルストレージを半分にします。
モデルハッシュによる追跡
indexer は、モデル名とボキャブラリサイズから導出されたハッシュを保存します。embedding モデルを変更すると、次回のインクリメンタル実行時に indexer が不一致を検出し、自動的にフルリインデックスをトリガーします。
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 からモデルをダウンロードします。ダウンロードが失敗した場合(ネットワーク障害、企業ファイアウォール)、retriever は BM25 のみのモードにフォールバックします。初回ダウンロード後、モデルはローカルにキャッシュされます。
次元の不一致。 データベースをクリアせずにモデルを切り替えると、保存されたベクトルの次元が新しい embedding と異なります。indexer はモデルハッシュを通じてこれを検出し、フルリインデックスをトリガーします。ハッシュチェックが失敗した場合(適切なハッシュのないカスタムモデル)、sqlite-vec は次元の不一致により KNN クエリでエラーを返します。
大規模 vault でのメモリ圧迫。 50,000件以上のチャンクを単一バッチで embedding すると、大量のメモリを消費する可能性があります。indexer は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」に関するノートが含まれています。「state management」が具体的な技術名で表現されているため、BM25は見逃します。
BM25は大規模なVaultでのキーワード衝突でも失敗します。15,000ファイルのVaultでは、「configuration」の検索で数百のノートがマッチします。ほぼすべてのプロジェクトノートが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-Nearest Neighbors)検索を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で対応し、ベクトル検索結果とチャンクメタデータ間のJOINを可能にします。
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らに従い601)
- rank_iは結果リストiにおけるドキュメントの1始まりのランクです
- weight_iはリストごとのオプションの重み係数です(デフォルト1.0)
複数のリストで上位にランクされたドキュメントは、より高い統合スコアを受け取ります。1つのリストにのみ出現するドキュメントは、その単一ソースからのスコアのみを受け取ります。
RRFが他の手法より優れている理由
重み付き線形結合では、BM25スコアとcosine距離の校正が必要です。BM25スコアは上限がなく、コーパスサイズに応じてスケールします。cosine距離は[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秒未満 |
| エンベディングモデル | potion-base-8M(256次元) |
| BM25候補プール | 30 |
| ベクトル候補プール | 30 |
| デフォルト結果上限 | 10 |
| デフォルトトークンバジェット | 4,000トークン |
コンテンツハッシュと変更検出
インデクサーは、前回のインデックス実行以降にどのファイルが変更されたかを把握する必要があります。このセクションでは、変更検出のメカニズムとハッシュ戦略について説明します。
ファイル更新時刻の比較
インデクサーはchunksテーブルのすべてのチャンクに対してmtime_ns(ナノ秒単位のファイル更新時刻)を保存しています。増分実行時のインデクサーの動作は以下の通りです:
- Vault内の許可されたフォルダにあるすべての
.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比較は、変更されていないファイルの不必要な再インデックス(偽陽性)を引き起こすことがありますが、実際の変更を見逃すことはありません。偽陽性のコストは、実行ごとに数回の余分なエンベディング呼び出し程度です。速度の差(100ms対3秒)により、AIインタラクションのたびに実行されるシステムにとって、mtimeが実用的な選択となっています。
削除の処理
Vaultからファイルが削除された場合、インデクサーはデータベースからそのファイルのすべてのチャンクを削除します:
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のcontent-syncテーブルでは、削除された各行に対して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キー」で検索すると、APIキーの管理について議論しているノートが返されますが、実際のキーを含むノートは返されません。
-
削除ではなく置換。
[REDACTED:pattern-name]トークンにより、周囲のテキストの意味的な文脈が保持されます。エンベディングは「クレデンシャルのようなものがここにあった」という情報をキャプチャしますが、クレデンシャル自体はエンコードしません。 -
値ではなくパターンをログに記録。 フィルターはどのパターンにマッチしたかをログに記録しますが(例:「oauth-debug.mdから2件のクレデンシャルをスクラブ [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ベースの認可、構造化されたツール出力(型付きリターンスキーマ)、およびelicitation(サーバー主導のユーザープロンプト)が追加されました。16 次の仕様リリース(暫定的に2026年6月)では、長時間実行タスク向けの非同期オペレーション、デフォルトのトランスポートモードとしてのステートレスリクエスト処理、および
.well-knownURLによるサーバーディスカバリーが提案されています。16 個人用Vaultサーバーの場合、STDIOが最もシンプルな選択肢です。仕様変更は主に、マルチテナントルーティングとロードバランシングを備えたエンタープライズHTTPデプロイメントに影響します。トランスポートの選択に影響する更新については、MCPロードマップを確認してください。
機能設計
MCPサーバーは最小限のツールセットを公開すべきです:
search — 主要なツールです。ハイブリッド検索を実行し、ランク付けされた結果を返します。
{
"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クエリあたり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検索のガイダンスを含めてください:
## 利用可能なツール
### Obsidian Vault(MCP: obsidian)
`obsidian_search` ツールを使用して、ナレッジベースから関連するコンテキストを検索できます。
以下のような情報が必要な場合にVaultを検索してください:
- コンセプトやパターンの背景情報
- 過去の意思決定やその根拠
- 実装のための参考資料
クエリの例:
- "authentication patterns in FastAPI"
- "how does the review aggregator work"
- "sqlite-vec configuration"
Claude Code との違い
| 機能 | Claude Code | Codex CLI |
|---|---|---|
| MCP 設定 | settings.json |
config.toml |
| Hooks | ~/.claude/hooks/ |
非対応 |
| Skills | ~/.claude/skills/ |
非対応 |
| 指示ファイル | CLAUDE.md |
AGENTS.md |
| 承認モード | --dangerously-skip-permissions |
suggest / auto-edit / full-auto |
主な違い: Codex CLI はHooksをサポートしていません。自動コンテキスト注入パターン(PreToolUse Hook)は利用できません。代わりに、AGENTS.md に作業開始前にVaultを検索するよう明示的な指示を記載してください。
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トークンのコンテキストが返されます。セッション中に10回クエリを行うと、エージェントはVaultコンテキストだけで15,000〜20,000トークンを消費します。
キャッシュありの場合: 事前構築された3つのコンテキストブロックが合計4,500トークンを消費します。追加の検索はユニーククエリごとに1,500〜2,000トークンを使用します。10回のクエリのうち6回がキャッシュブロックでカバーされる場合、エージェントの消費量は 4,500 +(4 × 1,500)= 10,500トークンとなり、キャッシュなしの場合のおよそ半分です。
PostToolUse Hooks によるコンテキスト圧縮
ツールの出力は冗長になりがちです:スタックトレース、ファイル一覧、テスト結果など。PostToolUse Hookを使用すると、これらの出力がコンテキストウィンドウを消費する前に圧縮できます。
問題
テストを実行する 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件失敗。
Hookの実装
#!/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
再帰トリガーの防止
圧縮Hookが出力を生成すると、ガードがない場合に自身を再トリガーする可能性があります:
# 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つの次元(各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だけで構成され、散文のないノートです。チャンカーにembedする対象のテキストがないため、これらのノートはインデックスの質が低くなります。リンクされたノートが関連する理由を説明する段落を少なくとも1つ追加してください。
あらゆるものへの双方向リンク。 すべての参照がwiki-linkである必要はありません。「OAuth」に言及しただけでは [[OAuth 2.0 Overview]] にする必要はありません。wiki-linkは、クリックすると有用なコンテキストが得られる、意図的でナビゲーション可能な関係のために使用してください。
開発者ワークフローレシピ
Vault検索と日常の開発タスクを組み合わせた実践的なワークフローです。
朝のコンテキストロード
関連するコンテキストをロードして1日を始めます:
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はレビュー対象のコードに関連する過去の意思決定、アーキテクチャ上の制約、コーディング規約を返します。レビューはdiffだけでなく、組織的な知識に基づいて行われます。
パフォーマンスチューニング
このセクションでは、さまざまな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-linkに置き換えます
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月2日 | potion-base-32MとP 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から静的embeddingsを生成する蒸留アプローチについて説明しています。 ↩
-
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. 密ベクトル表現がオープンドメイン質問応答において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+の量子化・次元削減機能です。 ↩↩↩