1. 導入:RAGの「アホアホマン」からの卒業
悩み: Obsidianのメモが増えるたびに、検索が重くなる。
課題: 毎回ディスクをなめる従来の検索(アホアホマン方式)の限界。
解決策: 「DBを入れずに、M4 Maxのメモリに全て載せればいいじゃない」という富豪的発想。
2. アーキテクチャ:なぜDB不要なのか?
ハードウェア: Mac Studio (M4 Max / 36GB RAM) の圧倒的スペック紹介。
ソフトウェア: MCP (Model Context Protocol) サーバーを自作。
ロジック: 起動時に全メモを
Mapオブジェクトにロード。検索はString.includes()だけの爆速処理。「10万件載っても大丈夫」の試算: 実際、テキスト10万件なら数百MBで収まるという「気づき」。
3. 実装のこだわりポイント
バックアップ機能:
write_note時の.bak生成というプロの配慮。キャッシュ同期: ファイルを書き換えた瞬間にメモリも更新するリアルタイム性。
AIとの親和性: ヒット箇所の「前後数十文字(Excerpt)」をAIに渡すことで、回答精度を高める工夫。
以下、サンプルコード
------
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
import * as fs from "fs/promises";
import * as path from "path";
import { glob } from "glob";
// Vaultのパス(環境変数から取得)
const VAULT_PATH = path.resolve(process.env.OBSIDIAN_VAULT_PATH || "");
/**
* 【富豪的設計】インメモリ全文キャッシュ
* M4 Maxの広大なメモリを信じ、全データをMapに展開する
*/
const vaultCache = new Map();
/**
* インデックス作成:全ファイルをメモリにロード
*/
async function refreshIndex() {
console.error("🚀 Indexing vault...");
const files = await glob("**/*.md", {
cwd: VAULT_PATH,
ignore: ["**/node_modules/**", "**/.git/**", "**/bin/**"]
});
for (const file of files) {
const fullPath = path.join(VAULT_PATH, file);
try {
const stats = await fs.stat(fullPath);
const content = await fs.readFile(fullPath, "utf-8");
vaultCache.set(file, { content, mtime: stats.mtimeMs });
} catch (e) {
console.error(`⚠️ Skip ${file}: ${e.message}`);
}
}
console.error(`✅ Indexed ${vaultCache.size} files. Ready for ultra-fast search.`);
}
// サーバー起動時に一括ロード
await refreshIndex();
const server = new Server(
{ name: "obsidian-pro-server", version: "2.0.0" },
{ capabilities: { tools: {} } }
);
/**
* ツール定義:AIに「何ができるか」を教える
*/
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: "search_vault",
description: "メモリ上のキャッシュから、ファイル名および本文を高速全文検索します。",
inputSchema: {
type: "object",
properties: {
query: { type: "string", description: "検索ワード" }
},
required: ["query"],
},
},
{
name: "read_note",
description: "指定されたファイルをメモリから即座に読み込みます。",
inputSchema: {
type: "object",
properties: { relativePath: { type: "string", description: "vaultからの相対パス" } },
required: ["relativePath"],
},
},
{
name: "write_note",
description: "ファイルを上書き保存し、自動的に.bakバックアップを作成します。メモリも即時更新します。",
inputSchema: {
type: "object",
properties: {
relativePath: { type: "string" },
content: { type: "string" }
},
required: ["relativePath", "content"],
},
}
],
}));
/**
* 各ツールの実行ロジック
*/
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
// パス安全性チェック
const getSafePath = (rel) => {
const fullPath = path.join(VAULT_PATH, rel);
if (!fullPath.startsWith(VAULT_PATH)) throw new Error("Security Error: Access denied");
return fullPath;
};
// --- 1. 高速全文検索 (Search) ---
if (name === "search_vault") {
const q = (args?.query || "").toLowerCase();
const results = [];
// メモリ上を総当たり(10万件でもM4 Maxなら一瞬)
for (const [relPath, data] of vaultCache.entries()) {
if (relPath.toLowerCase().includes(q) || data.content.toLowerCase().includes(q)) {
const index = data.content.toLowerCase().indexOf(q);
// ヒット箇所の前後30/60文字を抜粋してAIに渡す
const excerpt = data.content.substring(
Math.max(0, index - 30),
Math.min(data.content.length, index + 60)
).replace(/\n/g, " ");
results.push(`- Path: ${relPath}\n Excerpt: "...${excerpt}..."`);
}
}
return {
content: [{
type: "text",
text: results.length > 0
? `Found ${results.length} matches:\n\n${results.join("\n")}`
: "No matches found."
}]
};
}
// --- 2. 読み込み (Read) ---
if (name === "read_note") {
const cached = vaultCache.get(args.relativePath);
if (cached) return { content: [{ type: "text", text: cached.content }] };
// 未インデックスの場合のみディスク参照
try {
const content = await fs.readFile(getSafePath(args.relativePath), "utf-8");
return { content: [{ type: "text", text: content }] };
} catch (e) { return { content: [{ type: "text", text: "File not found." }], isError: true }; }
}
// --- 3. 書き込み (Write) ---
if (name === "write_note") {
try {
const targetPath = getSafePath(args.relativePath);
// 安全第一:上書き前にバックアップ作成
try {
const oldContent = await fs.readFile(targetPath, "utf-8");
await fs.writeFile(`${targetPath}.bak`, oldContent);
} catch (e) { /* 新規作成時はパス */ }
await fs.mkdir(path.dirname(targetPath), { recursive: true });
await fs.writeFile(targetPath, args.content, "utf-8");
// メモリも即時同期
vaultCache.set(args.relativePath, {
content: args.content,
mtime: Date.now()
});
return { content: [{ type: "text", text: `Success: Updated ${args.relativePath} and synced memory.` }] };
} catch (e) { return { content: [{ type: "text", text: e.message }], isError: true }; }
}
throw new Error("Tool not found");
});
// サーバー起動
const transport = new StdioServerTransport();
await server.connect(transport);
0 件のコメント:
コメントを投稿