obsidian:~/vault$ search --hybrid obsidian

Obsidian MCP + 하이브리드 검색: 2026년 레퍼런스

# 앱 기본 사항은 공식 Obsidian 문서를 사용하고, MCP, 하이브리드 검색, 16,894개 파일 규모의 AI vault 색인화에는 Blake의 레퍼런스를 사용하세요.

words: 11382 read_time: 57m updated: 2026-05-29 09:46
$ retriever search --hybrid obsidian

Obsidian은 단순한 노트 앱이 아닙니다. local-first 방식의 일반 텍스트, 그래프 구조 markdown corpus이며, 검색 인프라를 더하면 AI context 저장소가 됩니다. 파일 16,894개. chunk 49,746개. 23ms 쿼리. API 호출 0회. 83 MB SQLite 파일 1개. 이 가이드는 vault(Obsidian 보관소) 아키텍처부터 hybrid 검색, MCP 통합, 운영 워크플로까지 전체 시스템을 다룹니다.


핵심 요약

노트 작성이 아니라 컨텍스트 엔지니어링입니다. AI에서 Obsidian vault의 가치는 노트 자체가 아니라, 노트를 질의 가능하게 만드는 검색 계층에 있어요. 검색 기능이 없는 16,000개 파일 vault는 쓰기 전용 데이터베이스입니다. hybrid 검색과 MCP 통합을 갖춘 200개 파일 vault는 AI 지식 베이스입니다. 검색 인프라가 제품이고, 노트는 원재료입니다.

hybrid 검색은 순수 키워드 검색이나 순수 의미 검색보다 뛰어납니다. BM25는 정확한 식별자와 기능 이름을 잡아냅니다. 벡터 검색은 서로 다른 용어로 표현된 동의어와 개념적 일치를 잡아냅니다. Reciprocal Rank Fusion (RRF)은 점수 보정 없이 두 결과를 병합합니다. 어느 한 방법만으로는 두 실패 모드를 모두 다룰 수 없습니다. MS MARCO passage ranking 연구도 이 패턴을 확인해 줍니다. hybrid 검색은 단독 방식보다 일관되게 더 나은 성능을 보입니다.3 hybrid retriever 심층 분석에서는 RRF 수학, 실제 숫자로 보는 예제, 실패 모드 분석, 인터랙티브 fusion 계산기를 다룹니다.

MCP는 AI 도구에 vault 직접 접근 권한을 제공합니다. Model Context Protocol (MCP) 서버는 retriever를 Claude Code, Codex CLI, Cursor, 기타 AI 도구가 직접 호출할 수 있는 도구로 노출합니다. 에이전트는 vault를 질의하고, 출처 표시가 포함된 순위 결과를 받은 뒤, 전체 파일을 불러오지 않고도 해당 컨텍스트를 사용합니다. MCP 서버는 검색 엔진을 감싸는 얇은 래퍼입니다.

Local-first는 API 비용이 없고 프라이버시를 완전히 보장한다는 뜻입니다. 전체 스택은 단일 머신에서 실행됩니다. 저장소는 SQLite, embeddings(임베딩)는 Model2Vec, 키워드 검색은 FTS5, 벡터 KNN은 sqlite-vec를 사용합니다. 클라우드 서비스도, API 호출도, 네트워크 의존성도 없습니다. 개인 노트는 머신 밖으로 나가지 않습니다. 49,746개 chunk를 전체 다시 임베딩하는 비용은 OpenAI API 가격 기준 대략 $0.30 정도지만, 실제 비용은 지연 시간, 프라이버시 노출, 그리고 오프라인에서 작동해야 하는 시스템에 네트워크 의존성이 생긴다는 점입니다.4

증분 인덱싱은 10초 안에 시스템을 최신 상태로 유지합니다. 파일 수정 시간 비교로 변경 사항을 감지합니다. 수정된 파일만 다시 chunking(청킹)하고 다시 임베딩합니다. 전체 재인덱싱은 Apple M-series 하드웨어에서 약 4분이 걸립니다. 일반적인 하루 편집분에 대한 증분 업데이트는 10초 안에 실행됩니다. 시스템은 수동 개입 없이 최신 상태를 유지합니다.

이 아키텍처는 200개 노트부터 20,000개 이상 노트까지 확장됩니다. 동일한 3계층 설계(수집, 검색, 통합)는 어떤 vault 규모에서도 작동합니다. 작은 vault에서는 BM25 전용 검색으로 시작하세요. 키워드 충돌이 문제가 되면 벡터 검색을 추가하세요. 정확한 일치와 의미적 일치가 모두 필요해지면 RRF fusion을 추가하세요. 각 계층은 독립적으로 유용하며, 독립적으로 제거할 수도 있습니다.


이 가이드를 활용하는 방법

이 가이드는 전체 시스템을 다룹니다. 어디에서 시작할지는 현재 상황에 따라 달라집니다.

현재 상황 여기서 시작하세요 다음으로 살펴보세요
Obsidian + AI가 처음인 경우 AI 인프라에 Obsidian을 사용하는 이유, 빠른 시작 Vault 아키텍처, MCP 서버 아키텍처
이미 vault가 있고 AI 접근을 원할 경우 MCP 서버 아키텍처, Claude Code 통합 Embedding 모델, Full-Text Search
검색 시스템을 구축하는 경우 전체 검색 파이프라인, Reciprocal Rank Fusion 성능 튜닝, 문제 해결
팀 또는 엔터프라이즈 맥락인 경우 의사결정 프레임워크, Knowledge Graph 패턴 개발자 워크플로 레시피, 마이그레이션 가이드

Contract로 표시된 섹션에는 구현 세부 사항, 설정 블록, 실패 모드가 포함되어 있습니다. Narrative로 표시된 섹션은 개념, 아키텍처 결정, 설계 선택의 근거에 초점을 둡니다. Recipe로 표시된 섹션은 단계별 워크플로를 제공합니다.


AI 인프라에 Obsidian을 사용하는 이유

이 가이드의 핵심 주장입니다. Obsidian vault는 local-first, plaintext, graph-structured 방식이며 사용자가 스택의 모든 계층을 제어할 수 있기 때문에 개인 AI 지식 베이스를 위한 최고의 기반입니다.

Obsidian이 다른 대안과 달리 AI에 제공하는 것

Plaintext markdown 파일. 모든 노트는 파일 시스템의 .md 파일입니다. 독점 형식도, 데이터베이스 내보내기도, 콘텐츠를 읽기 위한 API도 필요 없습니다. 파일을 읽을 수 있는 모든 도구는 vault를 읽을 수 있습니다. grep, ripgrep, Python의 pathlib, SQLite FTS5 모두 원본 파일에서 직접 작동합니다. 검색 시스템을 구축할 때 인덱싱하는 대상은 API 응답이 아니라 파일입니다. 원본이 파일 시스템이기 때문에 인덱스는 항상 원본과 일치합니다.

Local-first 아키텍처. Vault는 사용자의 머신에 있습니다. 서버도, 클라우드 동기화 의존성도, API 속도 제한도, 자신의 콘텐츠를 처리하는 방식에 적용되는 서비스 약관도 없습니다. 외부 서비스 없이 노트를 임베딩하고, 인덱싱하고, chunk로 나누고, 검색할 수 있습니다. AI 인프라에서 이것이 중요한 이유는 검색 파이프라인이 API 엔드포인트의 응답 속도가 아니라 디스크가 허용하는 속도로 실행되기 때문입니다. 프라이버시 측면에서도 중요합니다. 자격 증명, 건강 데이터, 금융 정보, 개인적인 생각이 담긴 노트가 머신 밖으로 나가지 않습니다.

wiki-link를 통한 그래프 구조. Obsidian의 [[wiki-link]] 문법은 노트 사이에 방향성 그래프를 만듭니다. OAuth 구현에 대한 노트는 token rotation, session management, API security에 대한 노트로 연결됩니다. 그래프 구조는 개념 사이의 사람 손으로 큐레이션된 관계를 인코딩합니다. 벡터 임베딩은 의미적 유사성을 포착하지만, wiki-link는 작성자가 해당 주제를 생각하면서 만든 의도적 연결을 포착합니다. 그래프는 임베딩이 복제할 수 없는 신호입니다.

Plugin 생태계. Obsidian에는 2,500개 이상의 커뮤니티 plugin이 있습니다(2026년 3월 기준, 2025년 중반의 1,800개 이상에서 증가). Dataview는 vault를 데이터베이스처럼 질의합니다. Templater는 JavaScript 로직이 포함된 템플릿에서 노트를 생성합니다. Git 통합은 vault를 저장소와 동기화합니다. Linter는 형식 일관성을 강제합니다. Bases core plugin(v1.9.10에서 도입)은 frontmatter 속성을 필드로 사용해 vault 파일 위에 테이블, 갤러리, 캘린더, kanban board 같은 데이터베이스형 보기를 추가하고, 이를 .base 파일로 저장합니다.15 이러한 plugin은 기본 plaintext 형식을 바꾸지 않으면서 vault에 구조를 더합니다. 검색 시스템은 plugin 자체가 아니라 이러한 plugin의 출력을 인덱싱합니다.

500만 명 이상의 사용자. Obsidian에는 템플릿, 워크플로, plugin, 문서를 만들어 내는 대규모 활성 커뮤니티가 있습니다. Vault 구성이나 plugin 설정에서 문제를 만나면 누군가가 이미 해결책을 문서화했을 가능성이 높습니다. 커뮤니티는 Obsidian 주변 도구도 만들어 냅니다. MCP 서버, 인덱싱 스크립트, 게시 파이프라인, API 래퍼 등이 여기에 포함됩니다.

파일 시스템만으로는 제공하지 못하는 것

Markdown 파일 디렉터리는 plaintext의 장점이 있지만, Obsidian이 추가하는 다음 3가지는 없습니다.

  1. 양방향 링크. Obsidian은 backlink를 자동으로 추적합니다. Note A에서 Note B로 링크하면, Note B에는 Note A가 자신을 참조한다는 내용이 표시됩니다. 그래프 패널은 연결 클러스터를 시각화합니다. 이러한 양방향 인식은 원시 파일 시스템이 제공하지 않는 메타데이터입니다.

  2. Plugin 렌더링이 포함된 실시간 미리보기. Dataview 쿼리, Mermaid 다이어그램, callout block이 실시간으로 렌더링됩니다. 저장 형식은 plaintext로 유지되지만, 작성 경험은 텍스트 편집기보다 훨씬 풍부합니다. 사용자는 풍부한 환경에서 작성하고 정리하며, 검색 시스템은 원시 markdown을 인덱싱합니다.

  3. 커뮤니티 인프라. Plugin 탐색, theme marketplace, sync 서비스(선택 사항), publish 서비스(선택 사항), 문서 생태계가 있습니다. 개별 기능은 독립형 도구로도 재현할 수 있지만, Obsidian은 이를 일관된 워크플로로 묶어 제공합니다.

Obsidian이 하지 않는 일(그리고 직접 구축해야 하는 것)

Obsidian에는 검색 인프라가 포함되어 있지 않습니다. 기본 검색(full-text, filename, tag)은 있지만 embedding 파이프라인, 벡터 검색, fusion ranking, MCP 서버, 자격 증명 필터링, chunking 전략, 외부 AI 도구를 위한 통합 hook은 없습니다. 이 가이드는 Obsidian 위에 구축하는 인프라를 다룹니다. Vault는 기반입니다. 검색 파이프라인, MCP 서버, 통합 hook이 인프라입니다.

여기서 설명하는 아키텍처는 markdown-first이지, Obsidian 전용이 아닙니다. Logseq, Foam, Dendron 또는 평범한 markdown 파일 디렉터리를 사용하더라도 검색 파이프라인은 동일하게 작동합니다. Chunker는 .md 파일을 읽습니다. Embedder는 텍스트 문자열을 처리합니다. Indexer는 SQLite에 씁니다. 이 구성 요소 중 어느 것도 Obsidian 전용 기능에 의존하지 않습니다. Obsidian의 기여는 retriever가 인덱싱할 markdown 파일을 만들어 내는 작성 및 정리 환경입니다.


빠른 시작: 첫 AI 연결 vault

이 섹션에서는 5분 안에 vault를 AI 도구와 연결해요. Obsidian을 설치하고, vault를 만들고, MCP 서버를 설치한 다음 첫 쿼리를 실행하게 됩니다. 빠른 시작에서는 바로 결과를 확인할 수 있도록 커뮤니티 MCP 서버를 사용해요. 이후 섹션에서는 프로덕션 용도에 맞는 맞춤형 검색 파이프라인을 구축하는 방법을 다룹니다.

사전 준비 사항

  • macOS, Linux 또는 Windows
  • Node.js 18+ (MCP 서버용)
  • Obsidian 1.12+ (CLI 통합용, 이전 버전도 MCP 전용 구성에서는 작동)
  • Claude Code, Codex CLI 또는 Cursor 설치 완료

1단계: vault 만들기

obsidian.md에서 Obsidian을 다운로드하고 새 vault를 만드세요. 기억하기 쉬운 위치를 선택하세요. MCP 서버에는 절대 경로가 필요해요.

# Example vault location
~/Documents/knowledge-base/

retriever가 사용할 수 있도록 노트 몇 개를 추가하세요. 10-20개 정도의 노트만 있어도 결과를 확인하기에 충분해요. 각 노트는 의미 있는 제목과 최소 1개 문단의 내용을 가진 .md 파일이어야 합니다.

2단계: MCP 서버 설치하기

여러 커뮤니티 MCP 서버를 사용하면 vault에 바로 접근할 수 있어요. 생태계는 2025-2026년에 크게 성장했습니다. 최근 주요 업데이트로는 MCPVault v0.11.0(2026년 3월)이 있으며, 이 버전은 frontmatter와 hashtag를 개수와 함께 스캔하는 list_all_tags, 개선된 점 포함 폴더 처리, .base.canvas 파일 지원을 추가했어요.13 이 패키지는 npm에서 @bitbonsai/mcpvault로 이름도 변경되었습니다.

2026년 4월 변화 — 선호되는 연결 방식으로서의 Obsidian CLI: Obsidian 1.12.0은 1급 CLI를 도입했고, 공개 1.12.7 설치 프로그램(2026년 3월 23일)은 독립 실행 바이너리 + TUI + 소켓 파일 개선을 포함해 터미널 워크플로를 더 쉽게 설치하고 실행할 수 있게 했어요.16 커뮤니티 도구는 Local REST API plugin(mcp-obsidian의 기반)에서 CLI 기반 통합으로 활발히 이동하고 있습니다. 더 빠르고 안정적이기 때문이에요. MarkusPfundstein/mcp-obsidian repo는 2025년 6월 이후 커밋이 없고 태그된 릴리스도 전혀 없습니다. 유지 관리 모드로 보고, CLI 기반 서버나 아래에 나열된 더 새로운 커뮤니티 대안을 우선하세요.20 권장 설정은 이 가이드 뒤쪽의 “AI 워크플로용 Obsidian CLI” 섹션을 참고하세요.

서버 작성자 전송 방식 Plugin 필요 여부 핵심 기능
obsidian-mcp-server StevenStavrakis STDIO 아니요 가볍고 파일 기반
mcp-obsidian MarkusPfundstein STDIO Local REST API REST를 통한 전체 vault CRUD — 유지 관리 모드, 2025년 6월 이후 커밋 없음20
obsidian-mcp-tools jacksteamdev STDIO 예(plugin) Semantic search + Templater
obsidian-claude-code-mcp iansinnott WebSocket 예(plugin) Claude Code용 자동 탐색
obsidian-mcp-server cyanheads STDIO Local REST API tag, frontmatter 관리
Hybrid Search MCP community STDIO 아니요 BM25 + semantic search MCP 서버 + CLI. 2026년 4월 기준 새롭고 활발히 유지 관리됨.

빠른 시작에서는 .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 파일을 읽고, 기본 검색을 수행한 뒤 결과를 반환합니다. 이것이 최소 기능 버전이에요.

이 빠른 시작에서 제공하지 않는 것: - Hybrid retrieval(BM25 + vector search + RRF fusion) - Embedding 기반 semantic search - Credential filtering - Incremental indexing - Hook 기반 자동 context injection

이 가이드의 나머지 부분에서는 이러한 기능을 하나씩 구축하는 방법을 다룹니다. 빠른 시작은 개념을 검증합니다. 전체 파이프라인은 프로덕션 품질의 retrieval을 제공합니다.


AI 워크플로용 Obsidian CLI

Obsidian 1.12(2026년 2월)는 AI 워크플로를 위한 새로운 통합 지점을 여는 내장 command line interface를 도입했어요.16 CLI는 Obsidian GUI의 원격 제어 역할을 합니다. Obsidian이 실행 중이어야 하며, 실행 중이 아니면 첫 명령에서 자동으로 시작돼요. Settings > General > Command line interface에서 활성화하세요.

AI 인프라에서 CLI가 중요한 이유

CLI는 이전에는 GUI나 plugin API이 필요했던 Obsidian native 작업에 프로그래밍 방식으로 접근할 수 있게 해줘요. AI 워크플로에서 핵심 기능은 다음과 같습니다.

  • 스크립트와 hook에서 검색. obsidian search "query"obsidian search:context "query"는 모든 shell script, hook 또는 자동화 파이프라인에서 vault 검색을 실행해요. search:context 변형은 일치하는 줄과 주변 context를 반환하므로, 결과를 AI prompt에 넣을 때 유용합니다.
  • Daily note 자동화. obsidian daily는 오늘의 daily note를 열거나 생성해요. shell scripting과 함께 사용하면 자동화된 daily briefing 워크플로를 만들 수 있습니다. 예를 들어 hook이 AI 생성 요약을 daily note에 추가할 수 있어요.
  • Template 기반 노트 생성. obsidian template listobsidian template create는 Templater 또는 core template에서 노트를 생성해, AI agent가 markdown 파일을 직접 쓰지 않고도 구조화된 vault entry를 만들 수 있게 해줘요.
  • Property 관리. obsidian property setobsidian property get은 frontmatter property를 읽고 쓰며, 스크립트에서 YAML를 파싱하지 않고도 metadata를 업데이트할 수 있게 해줘요.
  • Plugin 제어. obsidian plugin enable/disable/list는 plugin을 프로그래밍 방식으로 관리하며, batch 작업 중 indexing plugin을 전환할 때 유용해요.
  • Task 관리. obsidian task list/add/complete는 구조화된 task 접근을 제공하며, vault에서 work item을 관리하는 AI agent에 유용해요.

AI 접근을 위한 CLI vs MCP

CLI와 MCP 서버는 서로 다른 역할을 맡으며, 경쟁 관계가 아니라 상호 보완적입니다.

측면 Obsidian CLI MCP Server
호출자 Shell script, hook, cron job AI agent(Claude Code, Codex, Cursor)
프로토콜 POSIX process(stdin/stdout/stderr) MCP(STDIO 또는 HTTP를 통한 JSON-RPC)
강점 Obsidian native 작업(template, plugin, property) Custom retrieval(embeddings, BM25, RRF fusion)
한계 Vector search 없음, embedding pipeline 없음 Obsidian 내부 작업 접근 불가
적합한 용도 Automation script, intake pipeline, hook action 세션 중 실시간 AI agent query

권장 사항: Intake 자동화(노트 생성, property 관리, Obsidian native search 실행)에는 CLI를 사용하고, retrieval(embeddings를 사용한 hybrid search)에는 MCP를 사용하세요. PreToolUse hook은 ranked result를 위해 전체 MCP retriever로 넘어가기 전에 빠른 사전 확인으로 obsidian search:context를 호출할 수 있어요.

예시: CLI 기반 intake hook

#!/bin/bash
# Hook: append today's signals to daily note via CLI
DATE=$(date +%Y-%m-%d)
SUMMARY="$1"
obsidian daily  # ensure daily note exists
obsidian file append "Daily Notes/${DATE}.md" "## AI Summary\n${SUMMARY}"

Obsidian Agent 플러그인

점점 늘어나는 Obsidian 플러그인 범주는 AI coding agent를 vault UI에 직접 임베드해, 외부 MCP 서버 설정의 대안을 제공합니다. 이 플러그인들은 외부 도구에서 연결하는 대신 Obsidian 사이드바 안에서 AI agent를 실행합니다.

Claudian

Claudian은 vault 안에 AI 협업자로 Claude Code을 임베드합니다. vault 디렉터리는 Claude의 작업 디렉터리가 되어, 파일 읽기/쓰기, 검색, bash 명령, 다단계 workflow 같은 agentic 기능을 모두 사용할 수 있게 합니다.17

AI 인프라에 중요한 기능: - 컨텍스트 인식 prompt. 현재 포커스된 노트를 자동으로 첨부하고, @notename 파일 멘션, 태그 기반 제외, editor selection을 컨텍스트로 지원합니다. - Vision 지원. drag-and-drop, 붙여넣기, 파일 경로로 이미지를 분석합니다. vault에 캡처한 스크린샷과 다이어그램을 처리할 때 유용합니다. - Slash commands. /command로 실행되는 재사용 가능한 prompt template을 만들어 표준화된 vault 작업을 수행할 수 있습니다. - 권한 모드. 안전 blocklist와 vault confinement를 포함해 YOLO(자동 승인), Safe(작업마다 승인), Plan(계획 전용) 모드를 제공합니다.

Agent Client

Agent Client는 Agent Client Protocol (ACP)을 통해 Claude Code, Codex CLI, Gemini CLI를 통합 Obsidian 사이드바로 가져옵니다.18

주요 기능: - Multi-agent 전환. 같은 패널에서 Claude Code, Codex, Gemini CLI와 대화하고, 필요에 따라 agent를 전환할 수 있습니다. - 노트 멘션. @notename을 사용해 prompt에 노트 내용을 포함할 수 있습니다. Claudian과 비슷하지만 특정 agent에 종속되지 않습니다. - Shell 실행. 채팅 안에서 terminal 명령을 inline으로 실행합니다. build scripts, git 명령, 모든 terminal 작업을 대화에서 벗어나지 않고 수행할 수 있습니다. - 작업 승인. 파일 읽기, 편집, 명령 실행을 세밀하게 제어할 수 있습니다.

Agent 플러그인과 외부 MCP를 선택하는 기준

시나리오 Agent 플러그인 외부 MCP
AI 지원으로 vault 노트 작성 및 편집 더 좋음 — agent가 editor 컨텍스트를 볼 수 있음 작동하지만 editor 인식 없음
여러 repo에 걸친 code development 제한적 — vault 범위로 제한됨 더 좋음 — 전체 filesystem을 포함한 project 범위
대규모 indexed corpus에서 retrieval 기본 검색만 가능 전체 hybrid retrieval pipeline
노트 작성 중 빠른 vault Q&A 이상적 — context switching 없음 terminal로 전환 필요

권장 사항: vault 중심 workflow(노트 작성, 정리, 요약)에는 agent 플러그인을 사용하세요. AI agent가 전체 retrieval pipeline과 vault 밖 codebase 접근 권한을 필요로 하는 development workflow에는 외부 MCP 서버를 사용하세요. 두 접근 방식은 함께 사용할 수 있습니다. 노트 작업에는 Obsidian 안에서 Claudian을 실행하고, 개발에는 외부에서 MCP와 함께 Claude Code을 사용하세요.


의사결정 프레임워크: Obsidian과 대안 비교

모든 use case에 Obsidian이 필요한 것은 아닙니다. 이 섹션에서는 Obsidian이 적합한 기반인 경우, 과한 경우, 다른 도구가 더 잘 맞는 경우를 정리합니다.

Decision Tree

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 Plain Filesystem CLAUDE.md
Local-first 아니요(cloud) 일부(iCloud)
Plaintext 예(markdown) 아니요(blocks) 아니요(proprietary)
Graph structure 예(wiki-links) 일부(mentions) 아니요 아니요 아니요
AI index 가능성 직접 파일 접근 API 필요 export 필요 직접 파일 접근 이미 context에 포함
Plugin ecosystem 2,500개 이상 plugins Integrations 없음 N/A N/A
Offline 가능 전체 캐시된 read-only 일부 전체 전체
10K+ 노트 확장성 예(API 사용 시) 성능 저하 아니요(single file)
비용 무료(core) $10/mo+ 무료 무료 무료

Obsidian이 과한 경우

  • 단일 project context. AI가 현재 codebase에 대한 context만 필요하다면 CLAUDE.md, AGENTS.md, 또는 project-level documentation에 넣으세요. 이 파일들은 repo와 함께 이동하고 자동으로 로드됩니다.
  • Structured data. 콘텐츠가 tables, records, schemas라면 database를 사용하세요. Obsidian 노트는 prose-first입니다. Dataview로 frontmatter 필드를 쿼리할 수는 있지만, structured queries는 실제 database가 더 잘 처리합니다.
  • 임시 research. project가 끝난 뒤 노트를 버릴 예정이라면 markdown files가 있는 scratch directory가 더 단순합니다. 일시적인 콘텐츠를 위해 retrieval infrastructure를 만들지 마세요.

Obsidian이 적합한 경우

  • 수개월 또는 수년에 걸쳐 knowledge를 축적하는 경우. corpus가 커질수록 가치가 복리처럼 쌓입니다. 6개월 동안 매일 query하는 200개 노트 vault는 한 번만 query하는 5,000개 노트 vault보다 더 큰 가치를 제공합니다.
  • 하나의 corpus에 여러 domain이 있는 경우. programming, architecture, security, design, personal projects에 대한 노트를 담은 vault는 project-specific CLAUDE.md가 제공할 수 없는 cross-domain retrieval의 이점을 얻습니다.
  • Privacy-sensitive content. Local-first는 retrieval pipeline이 콘텐츠를 외부 서비스로 보내지 않는다는 뜻입니다. vault에는 cloud service에 업로드하지 않을 콘텐츠를 포함해, 사용자가 넣는 모든 콘텐츠가 들어갑니다.

Mental Model: 3개 Layer

시스템에는 독립적으로 작동하지만 결합될 때 가치가 커지는 3개 layer가 있습니다. 각 layer는 서로 다른 관심사와 실패 모드를 가집니다.

┌─────────────────────────────────────────────────────┐
                 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에는 트윗 스크린샷, 주석 없이 복사해 붙여넣은 글, context 없는 미완성 생각 같은 noise가 쌓입니다. intake layer는 진입 시점의 품질 관리를 담당합니다. scoring pipeline, tagging convention, manual review process처럼 vault에 retrieval할 가치가 있는 콘텐츠만 들어가도록 보장하는 모든 mechanism이 여기에 해당합니다.

Retrieval은 vault를 query 가능하게 만듭니다. 이 layer는 engine입니다. 노트를 search unit으로 chunking하고, chunk를 vector space에 embedding하며, keyword와 semantic search를 위해 indexing하고, RRF로 결과를 fuse합니다. retrieval layer는 파일 디렉터리를 query 가능한 knowledge base로 변환합니다. 이 layer가 없으면 vault는 manual browsing과 basic search로는 탐색할 수 있지만, AI tools가 programmatically 접근할 수는 없습니다.

Integration은 retrieval layer를 AI tools에 연결합니다. MCP 서버는 retrieval을 호출 가능한 tool로 노출합니다. Hooks는 context를 자동으로 주입합니다. Skills는 새 knowledge를 다시 vault에 저장합니다. integration layer는 knowledge base와 이를 소비하는 AI agents 사이의 interface입니다.

이 layer들은 설계상 decoupled되어 있습니다. intake scoring pipeline은 embeddings에 대해 알지 못합니다. retriever는 signal routing rules에 대해 알지 못합니다. MCP 서버는 노트가 어떻게 만들어졌는지 알지 못합니다. 이런 decoupling 덕분에 어떤 layer든 독립적으로 개선할 수 있습니다. intake pipeline을 바꾸지 않고 embedding model을 교체할 수 있습니다. retriever를 수정하지 않고 새 MCP capability를 추가할 수 있습니다. index를 건드리지 않고 signal scoring heuristics를 변경할 수 있습니다.


AI 활용을 위한 보관소 아키텍처

AI 검색에 최적화된 보관소는 개인 탐색에 최적화된 보관소와 다른 관례를 따릅니다. 이 섹션에서는 폴더 구조, 노트 스키마, 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 문장이 들어 있는 모든 폴더입니다.

인덱싱에서 제외해야 하는 폴더: Templates는 실제 콘텐츠가 아니라 placeholder 변수를 담고 있고, attachments는 바이너리 파일이며, Obsidian 설정과 검색 인덱스에 넣고 싶지 않은 민감한 콘텐츠가 들어 있는 모든 폴더도 제외해야 합니다.

.indexignore 파일

보관소 루트에 .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/

인덱서는 스캔하기 전에 이 파일을 읽고 일치하는 경로를 완전히 건너뜁니다. 제외된 경로의 파일은 chunking되지 않고, 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용 heading 컨텍스트에 사용됩니다
  • type — 유형별로 필터링된 쿼리를 가능하게 합니다(“MOC만 보여줘” 또는 “signals만”)
  • tags — FTS5 heading 컨텍스트에 0.3 가중치로 인덱싱되어, 본문에서 다른 용어를 쓰더라도 키워드 매칭을 제공합니다

선택 사항이지만 유용한 필드:

  • domain — domain 범위 쿼리를 가능하게 합니다(“security 노트만 검색”)
  • source — 수집한 콘텐츠의 출처입니다. 검색기는 결과에 원본 URL을 포함할 수 있습니다
  • status — 활성 검색에서 보관된 노트나 초안 노트를 제외할 수 있습니다

Chunking 관례

검색기는 H2(##) heading 경계에서 chunking합니다. 즉 노트 구조가 검색 단위의 세밀함에 직접 영향을 줍니다.

검색에 좋은 구조:

## 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개의 chunk를 만듭니다. 각 chunk는 embedding이 의미를 포착하기에 충분한 컨텍스트를 담고 있습니다. “expired token handling”에 관한 쿼리는 세 번째 chunk와 구체적으로 매칭됩니다.

검색에 좋지 않은 구조:

# 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 heading이 없는 긴 섹션 하나는 하나의 큰 chunk를 만듭니다. embedding은 섹션 안의 모든 주제를 평균화합니다. 어떤 하위 주제에 대한 쿼리든 노트 전체와 똑같이 매칭됩니다.

경험칙: 한 섹션에서 2개 이상의 개념을 다룬다면 H2 하위 섹션으로 나누세요. 나머지는 chunker가 처리합니다.

노트에 넣지 말아야 할 것

검색 품질을 떨어뜨리는 콘텐츠는 다음과 같습니다.

  • 전체 글을 주석 없이 그대로 복사해 붙여 넣은 원문. 검색기는 원문의 키워드를 인덱싱하므로, 직접 작성하지 않은 콘텐츠가 보관소를 희석합니다. 대신 요약을 추가하거나, 핵심 포인트를 추출하거나, 원본 URL로 링크하세요.
  • 텍스트 설명이 없는 스크린샷. 검색기는 markdown 텍스트를 인덱싱합니다. alt text나 주변 설명이 없는 이미지는 BM25와 vector search 모두에서 보이지 않습니다.
  • Credential 문자열. API 키, 토큰, 비밀번호, 연결 문자열입니다. credential filtering이 있더라도 가장 안전한 방법은 노트에 secrets를 절대 붙여 넣지 않는 것입니다. 대신 이름으로 참조하세요(“~/.env의 Cloudflare API token”).
  • 큐레이션되지 않은 자동 생성 콘텐츠. 도구가 노트(회의 transcript, Readwise highlights, RSS import)를 생성했다면 영구 보관소에 넣기 전에 검토하고 주석을 추가하세요. 큐레이션되지 않은 자동 import는 검색 가능한 가치를 더하지 않고 양만 늘립니다.

AI 워크플로를 위한 Plugin 생태계

AI 검색 품질을 높이는 Obsidian plugin은 세 가지 범주로 나눌 수 있어요. 구조화 plugin은 일관성을 강제하고, 쿼리 plugin은 metadata를 드러내며, sync plugin은 vault를 최신 상태로 유지해요.

필수 Plugin

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 전체에 formatting 규칙을 적용해요. 일관된 heading 계층 구조(H1은 제목, H2는 섹션, H3는 하위 섹션)를 유지하면 chunker가 예측 가능한 결과를 만들 수 있어요. 검색에 중요한 Linter 규칙은 다음과 같아요.

  • Heading increment: heading 수준이 순서대로 이어지도록 강제하기(H1에서 H3로 건너뛰지 않기)
  • YAML title: 파일 이름과 일치시키기
  • Trailing spaces: 제거하기(FTS5 tokenization artifact 방지)
  • Consecutive blank lines: 1개로 제한하기(더 깔끔한 chunk)

Git integration. vault를 위한 버전 관리예요. 시간에 따른 변경 사항을 추적하고, 여러 기기 간에 sync하며, 실수로 삭제한 내용을 복구할 수 있어요. Git은 indexer가 incremental change detection에 사용하는 mtime 데이터도 제공해요.

Indexing에 도움이 되는 Plugin

Smart Connections. Obsidian 안에서 AI 기반 semantic search를 제공하는 Obsidian plugin이에요. Smart Connections v4는 기본적으로 local embeddings를 만들어요. vault가 한 번 index되면 semantic connection과 lookup이 API 호출 없이 완전히 offline으로 작동해요.11 v4.5.0(2026년 5월 5일)에서는 footer connection이 Smart Connections Core의 일부가 되어, 모든 설치 환경에서 side panel을 열지 않아도 footer에 관련 노트 connection을 표시할 수 있어요. 최근 v4 release에는 connection list를 위한 graph view, 설정 가능한 dock 위치, 중단된 indexing run 이후 개선된 block-embedding recovery, 그리고 Smart Connections, Smart Chat, Smart Composer가 상태를 공유할 수 있게 해주는 cross-plugin 환경인 “Substrate”도 추가됐어요.21 이 가이드의 검색 시스템은 Obsidian 외부에서 실행되는 Python pipeline이지만, Smart Connections는 글을 쓰는 동안 semantic relationship을 탐색하는 데 유용해요. 두 시스템은 같은 content를 index하지만 서로 다른 use case를 담당해요. Smart Connections는 editor 안에서 discovery를 돕고, 외부 retriever는 MCP를 통해 AI tool integration을 지원해요.

2026년 4월에 출시된 AI-native plugin. 새로운 community plugin들이 Claude Code / Codex / Gemini-CLI workflow를 직접 겨냥하고 있어요.

Plugin 출시 하는 일
Cortex 4월 4일 Claude Code로 구동되는 vault agent예요. vault를 단순한 note store가 아니라 agent workspace로 다뤄요
VaultSearch 4월 7일 Local-first hybrid search: BM25 + semantic + fuzzy를 제공해요. 이 가이드의 retrieval stack과 직접 겹쳐요
LLM Wiki 4월 9일 vault를 비공개로 쿼리할 수 있는 지식 베이스로 바꿔요
Drift 4월 11일 AI 기반 Obsidian editing을 위한 VS Code 스타일 diff viewer예요. Claude Code workflow에 맞춰져 있어요
EngramQuest 4월 11일 노트에서 memory challenge를 생성해요. Claude Code / Gemini CLI / Cursor용 “AI Skills”를 함께 제공해요
Hybrid Search MCP 3월(아직 신규) BM25 + semantic search를 갖춘 MCP server + CLI예요. AI assistant를 위해 만들어졌어요

이 영역은 막 생겨나는 표면적이라고 보면 돼요. 앞으로 몇 분기 안에 이 중 여러 plugin이 통합되거나 Smart Connections / Obsidian core에 흡수될 가능성이 높아요. 오늘 하나를 고른다면 VaultSearch와 Hybrid Search MCP가 이 가이드의 외부 retriever와 철학적으로 가장 가까워요.

Dataview 참고: 오래된 Obsidian query plugin인 Dataview는 2025년 4월에 0.5.70을 마지막으로 release했고, 이후 사실상 dormant 상태예요. 새 작업에는 Obsidian에 내장된 Bases 기능(1.9+)이 사실상의 후속 기능이며 권장 경로예요.

Metadata Menu. 필드 값 autocomplete가 있는 구조화된 frontmatter editing을 제공해요. type, domain, tags 필드의 오타를 줄여줘요. 일관된 metadata는 검색 필터링 정확도를 높여요.

Indexing에 해가 되는 Plugin

Excalidraw. 그림을 markdown 파일 안에 포함된 JSON로 저장해요. JSON는 문법적으로 유효한 markdown이지만, chunking하고 embedding하면 쓸모없는 결과를 만들어요. .indexignore를 사용하거나 파일 확장자로 필터링해 Excalidraw 파일을 index에서 제외하세요.

Kanban. board state를 특수한 형식의 markdown으로 저장해요. 이 형식은 prose retrieval이 아니라 Kanban rendering을 위해 설계됐어요. chunker는 card title과 metadata의 fragment를 만들지만, 이런 fragment는 embedding 품질이 좋지 않아요. Kanban board는 index에서 제외하세요.

Calendar. 최소한의 content만 있는 daily note를 만들어요. 대개 date header만 있는 경우가 많아요. 비어 있거나 거의 비어 있는 note는 품질 낮은 chunk를 만들어요. daily note를 사용한다면 그 안에 실질적인 content를 쓰거나, daily note 폴더를 index에서 제외하세요.

중요한 Plugin 설정

File recovery → Enabled. 실수로 note가 삭제되는 일을 막아줘요. 검색과 직접 관련은 없지만, 의존하는 지식 베이스에는 매우 중요해요.

Strict line breaks → Disabled. Markdown 표준 줄바꿈(문단은 double newline)이 Obsidian strict mode(<br>에 single newline)보다 더 깔끔한 chunk를 만들어요.

Default new file location → Designated folder. 새 파일을 00-inbox/로 보내면 분류되지 않은 note가 domain 폴더를 어지럽히지 않아요. inbox는 staging area이며, 파일은 triage 후 domain 폴더로 이동해요.

Wiki-link format → Shortest path when possible. 더 짧은 link target은 retriever가 link structure를 index할 때 해석하기 더 쉬워요.


Embedding Models: 선택과 설정

Embedding model은 텍스트 청크를 의미 검색용 숫자 벡터로 변환해요. 모델 선택은 검색 품질, index 크기, embedding 속도, runtime 의존성을 결정해요. 이 섹션에서는 Model2Vec의 potion-base-8M을 기본값으로 선택한 이유와 대안을 선택해야 하는 경우를 설명해요.

Model2Vec potion-base-8M을 선택한 이유

Model: minishlab/potion-base-8M Parameters: 7.6 million Dimensions: 256 Size: ~30 MB Dependencies: model2vec (numpy only, no PyTorch) Inference: CPU-only, static word embeddings (no attention layers)

Model2Vec은 sentence transformer의 지식을 static token embeddings로 증류해요. BERT, MiniLM, 그 밖의 transformer 모델처럼 입력 전체에 attention layers를 실행하는 대신, Model2Vec은 미리 계산된 token embeddings의 가중 평균으로 벡터를 만들어요.5 실무적인 결과는 분명해요. 순차 계산이 없기 때문에 embedding 속도가 transformer 기반 모델보다 50-500배 빨라요.

현재 Model2Vec 결과 페이지에서 potion-base-8M은 all-MiniLM-L6-v2의 전체 작업 점수 대비 약 92%에 도달해요(51.32 vs 55.80). 그러면서도 속도는 몇 자릿수 더 빨라요.6 남는 품질 격차는 속도와 단순성의 장점을 얻기 위한 trade-off예요. 짧은 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]

Lazy loading. 모델은 import 시점이 아니라 처음 사용할 때 로드돼요. retriever가 BM25-only fallback 모드로 동작할 때(예: embedding venv가 설치되지 않은 경우) embedder 모듈을 import해도 비용이 들지 않아요.

분리된 virtual environment. 이 모델은 전용 venv(예: ~/.claude/venvs/memory/)에서 실행되어 나머지 toolchain과의 의존성 충돌을 피해요. _activate_venv() 기능은 runtime에 venv의 site-packagessys.path에 추가해요.

# Create isolated venv
python3 -m venv ~/.claude/venvs/memory
~/.claude/venvs/memory/bin/pip install model2vec

Batch processing. embedder는 Model2Vec의 overhead를 분산하기 위해 텍스트를 64개 단위 batch로 처리해요. indexer는 청크를 하나씩 embedding하지 않고 embed_batch()에 전달해요.

대안을 선택해야 하는 경우

Model Dim Size Speed Quality (MTEB) Best for
potion-base-8M 256 30 MB 500x 51.32 기본값: 로컬, 빠름, GPU 없음
potion-base-32M 256 120 MB 400x 52.83 더 높은 품질, 여전히 static
potion-retrieval-32M 256 120 MB 400x 35.06 (retrieval) Retrieval에 최적화된 static
potion-multilingual-128M 256 ~500 MB 300x 다국어 vault (101개 언어)
all-MiniLM-L6-v2 384 80 MB 1x 55.80 더 높은 품질, 여전히 로컬
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을 선택하세요 static embedding 계열을 벗어나지 않으면서 potion-base-8M보다 더 나은 품질이 필요할 때 적합해요. 이 모델은 baai/bge-base-en-v1.5에서 증류한 더 큰 vocabulary를 사용해 52.83의 전체 작업 점수를 달성해요(potion-base-8M보다 약 3% 높음). 동시에 동일한 256차원 출력과 numpy-only 의존성을 유지해요.8 모델 파일이 4배 커져 메모리 사용량은 늘어나지만, embedding 속도는 여전히 transformer 모델보다 몇 자릿수 빨라요.

potion-retrieval-32M을 선택하세요 주요 사용 사례가 retrieval일 때 적합해요(vault 검색이 바로 여기에 해당해요). 이 variant는 retrieval 작업에 맞춰 potion-base-32M에서 fine-tuning되었고, Model2Vec의 retrieval benchmark table에서 35.06점을 기록해요. potion-base-32M은 32.67점이에요.8 trade-off는 일반 목적 embedding 품질보다 retrieval에 최적화되어 있다는 점이에요.

potion-multilingual-128M을 선택하세요 vault에 여러 언어의 노트가 있을 때 적합해요. 2025년 5월에 출시된 이 101개 언어 모델은 다국어 작업에서 가장 성능이 좋은 static embedding model이며, 다른 potion 모델과 같은 numpy-only 의존성을 유지하면서 어떤 언어의 텍스트든 embeddings를 생성해요.12 더 큰 모델 파일(~500 MB)은 cross-lingual 기능을 위한 trade-off예요. 영어 콘텐츠와 함께 일본어, 중국어, 독일어 또는 그 밖의 비영어 노트가 있을 때 사용하세요.

all-MiniLM-L6-v2를 선택하세요 속도보다 retrieval 품질이 더 중요하고 PyTorch가 설치되어 있을 때 적합해요. 384차원 벡터는 256차원 벡터와 비교해 SQLite database 크기를 약 50% 늘려요. M-series hardware에서 15,000개 파일을 전체 reindex할 때 embedding 속도는 1분 미만에서 약 10분으로 느려져요.

nomic-embed-text-v1.5를 선택하세요 가능한 최고의 로컬 retrieval 품질이 필요하고 더 느린 indexing을 감수할 수 있을 때 적합해요. 768차원 벡터는 database 크기를 대략 3배로 늘려요. PyTorch와 최신 CPU 또는 GPU가 필요해요.

text-embedding-3-small을 선택하세요 network latency와 privacy를 trade-off로 받아들일 수 있을 때 적합해요. API는 가장 높은 품질의 embeddings를 만들지만, cloud 의존성, token당 비용($0.02/million tokens), 그리고 콘텐츠를 OpenAI 서버로 전송한다는 점이 생겨요.

그 밖의 모든 경우에는 potion-base-8M을 유지하세요. 반복적인 indexing(개발 중 reindex)에는 속도 이점이 중요하고, numpy-only 의존성은 PyTorch 설치의 복잡성을 피하게 해주며, 256차원 벡터는 database를 작게 유지해요.

Quantization과 차원 축소

Model2Vec v0.5.0+는 낮은 정밀도와 줄어든 차원으로 모델을 로드하는 기능을 지원해요.8 모델을 바꾸지 않고도 제한된 hardware에 배포하거나 database 크기를 줄이고 싶을 때 유용해요.

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)

Quantized models는 메모리 사용량을 크게 줄이면서도 거의 동일한 retrieval 품질을 유지해요. 차원 축소는 Matryoshka 방식의 truncation을 따라요. 처음 N개 차원이 가장 많은 정보를 담아요. 256차원에서 128차원으로 줄이면 짧은 텍스트 retrieval에서 품질 손실을 최소화하면서 vector storage를 절반으로 줄일 수 있어요.

Model2Vec v0.8.x는 tokenizer/persistence 내부 구조를 업데이트하고, Python 3.9 지원을 deprecated 처리하며, 공개 결과를 더 새로운 MTEB table 기준으로 갱신해요. production indexer를 업그레이드하기 전에 model2vec을 pin하거나 테스트하세요. library 업그레이드는 embedding model name이 그대로여도 model-loading path를 바꿀 수 있기 때문이에요.10

Vault별 Embeddings를 위한 Fine-Tuning

Model2Vec v0.4.0+는 static embeddings 위에 custom classification model을 훈련하는 기능을 지원해요. v0.7.0은 distillation을 위한 vocabulary quantization과 configurable pooling을 추가하고, v0.8.x는 tokenizer와 persistence 동작을 refactor해요.10 이는 전문 vocabulary(의료 노트, 법률 reference, domain-specific jargon)가 있는 vault에서 중요해요. 기본 potion 모델이 의미적 nuance를 충분히 포착하지 못할 수 있기 때문이에요.

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이 충분한 retrieval 품질을 제공해요. Fine-tuning은 일반 목적 모델이 포착하지 못하는 domain-specific 연결을 retrieval이 반복적으로 놓칠 때만 가치가 있어요.

Model Hash 추적

indexer는 model name과 vocabulary size에서 파생한 hash를 저장해요. embedding model을 바꾸면 indexer가 다음 incremental run에서 mismatch를 감지하고 자동으로 full reindex를 트리거해요.

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]

이렇게 하면 서로 다른 모델에서 나온 벡터가 같은 database에 섞이는 일을 막을 수 있어요. 그런 혼합은 의미 없는 cosine similarity 점수를 만들어내요.

Failure Modes

Model download failure. 첫 실행에서는 Hugging Face에서 모델을 다운로드해요. 다운로드가 실패하면(network issue, corporate firewall 등) retriever는 BM25-only 모드로 fallback해요. 모델은 첫 다운로드 이후 로컬에 cache돼요.

Dimension mismatch. database를 지우지 않고 모델을 바꾸면 저장된 벡터와 새 embeddings의 차원이 달라져요. indexer는 model hash를 통해 이를 감지하고 full reindex를 트리거해요. hash check가 실패하면(proper hash가 없는 custom model 등) sqlite-vec은 dimension이 맞지 않는 KNN queries에서 error를 발생시켜요.

큰 vault에서의 memory pressure. 50,000개 이상의 청크를 한 번의 batch로 embedding하면 상당한 메모리를 사용할 수 있어요. indexer는 peak memory usage를 제한하기 위해 64개 단위 batch로 처리해요. 그래도 메모리가 문제라면 batch size를 줄이세요.


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 매개변수는 텍스트의 중복 사본을 저장하는 대신 FTS5가 chunks 테이블을 직접 참조하도록 지정해요. 이렇게 하면 저장 공간 요구량이 절반으로 줄지만, 청크가 삽입, 업데이트, 삭제될 때 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%만큼 기여해요

이 가중치는 조정할 수 있어요. 볼트의 제목이 콘텐츠 품질을 잘 예측할 만큼 설명적이라면 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” → 볼트에는 “login error recovery”와 “session expiration handling”에 대한 노트가 있어요. 키워드가 다르기 때문에 BM25는 일치시키지 못해요.
  • 쿼리: “what is the best way to manage state” → 볼트에는 “Redux store patterns”와 “context providers”에 대한 노트가 있어요. “state management”가 특정 기술 이름으로 표현되어 있기 때문에 BM25는 놓쳐요.

BM25는 규모가 커지면 키워드 충돌에도 실패해요. 15,000개 파일이 있는 볼트에서 “configuration”을 검색하면 거의 모든 프로젝트 노트가 설정을 언급하기 때문에 수백 개의 노트가 일치해요. 결과는 기술적으로 맞지만 실제로는 쓸모가 없어요. 순위 지정만으로는 어떤 “configuration” 노트가 현재 쿼리와 관련 있는지 판단할 수 없어요.

FTS5 토크나이저

FTS5는 기본적으로 ASCII와 Unicode 텍스트를 처리하는 unicode61 토크나이저를 사용해요. CJK(중국어, 일본어, 한국어) 콘텐츠가 많은 볼트에서는 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배) 부분 문자열 일치를 가능하게 해요.

유지 관리

기반 chunks 테이블이 변경되면 FTS5는 명시적인 동기화가 필요해요:

# 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에 vector KNN (K-Nearest Neighbors) 검색을 가져와요. 이 섹션에서는 sqlite-vec 설정, 노트에서 검색 가능한 벡터까지 이어지는 embedding pipeline, 그리고 구체적인 쿼리 패턴을 다뤄요.

sqlite-vec Virtual Table

CREATE VIRTUAL TABLE chunk_vecs USING vec0(
    id INTEGER PRIMARY KEY,
    embedding float[256]
);

vec0 모듈은 256차원 float 벡터를 packed binary data로 저장해요. id 컬럼은 chunks 테이블과 1:1로 매핑되어, 벡터 결과와 chunk metadata를 조인할 수 있어요.

Embedding Pipeline

pipeline은 노트에서 검색 가능한 벡터까지 이렇게 흘러가요.

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

Vector Serialization

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 Query

vector search 쿼리는 입력 쿼리를 임베딩한 다음, cosine distance 기준으로 가장 가까운 K개의 chunks를 찾아요.

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 연산자는 approximate nearest neighbor search를 수행해요. k 파라미터는 반환할 결과 수를 제어해요. distance 컬럼에는 cosine distance가 들어 있어요(0 = 동일, 2 = 반대).

Distance Constraints를 사용한 KNN Pagination

sqlite-vec v0.1.7부터 KNN 쿼리는 WHERE distance < ? 제약을 지원해요. 덕분에 이전 페이지를 다시 스캔하지 않고도 큰 결과 집합을 cursor-based pagination으로 탐색할 수 있어요.14 이후 v0.1.8과 v0.1.9 stable release는 새로운 query model release라기보다 packaging 및 DELETE bug fix release이므로, 이 pagination 패턴의 기능 경계는 여전히 v0.1.7이에요.23

def _paginated_vector_search(self, query_vec, page_size=20, max_distance=None):
    """Paginate through KNN results using distance constraints."""
    packed = _serialize_vector(query_vec)
    constraint = f"AND distance < {max_distance}" if max_distance else ""

    results = self.db.execute(f"""
        SELECT cv.id, cv.distance, c.file_path, c.chunk_text
        FROM chunk_vecs cv
        JOIN chunks c ON cv.id = c.id
        WHERE embedding MATCH ?
            AND k = ?
            {constraint}
        ORDER BY distance
    """, [packed, page_size]).fetchall()

    # Use last result's distance as cursor for next page
    next_cursor = results[-1][1] if results else None
    return results, next_cursor

이 방식은 큰 k를 가져온 뒤 Python에서 slicing하던 기존 패턴을 대체해서, 큰 vault를 대상으로 탐색 쿼리를 실행할 때 메모리 사용량을 줄여요.

vec0 Tables의 DELETE 지원

sqlite-vec v0.1.7은 vec0 virtual table에 native DELETE 지원을 추가했고, v0.1.9는 12자를 넘는 metadata text column과 관련된 DELETE error path를 수정했어요.1423 이전에는 벡터를 제거하려면 테이블을 drop한 뒤 다시 만들어야 했어요. 이제 indexer의 file-removal path는 벡터를 직접 삭제할 수 있어요.

# Before v0.1.7: required workaround (drop + recreate, or mark as inactive)
# After v0.1.7: direct DELETE works
db.execute("DELETE FROM chunk_vecs WHERE id = ?", [chunk_id])

이렇게 하면 노트가 삭제되거나 이동될 때 incremental reindexing이 더 단순해져요. indexer는 더 이상 별도의 “active IDs” shadow table을 유지하거나 batch rebuild를 할 필요가 없어요.

Vector Search가 유리한 경우

Vector search는 특정 단어보다 개념이 더 중요한 쿼리에서 뛰어난 성능을 보여요.

  • Query: “how to handle authentication failures” → “login error recovery”에 관한 노트를 찾아요(같은 semantic space, 다른 keyword)
  • Query: “what patterns exist for caching” → “memoization,” “Redis TTL strategies,” “HTTP cache headers”에 관한 노트를 찾아요(관련 개념, 다양한 용어)
  • Query: “approaches to testing asynchronous code” → “pytest-asyncio fixtures,” “mock event loops,” “async test patterns”에 관한 노트를 찾아요(구현 세부 사항으로 표현된 같은 개념)

Vector Search가 실패하는 경우

Vector search는 정확한 identifier에 약해요.

  • Query: _rrf_fuse → “fusion algorithms”와 “rank merging”에 관한 노트를 반환하지만, 실제 function definition은 개념 설명보다 낮게 랭크될 수 있어요
  • Query: PostToolUse → 특정 hook name보다는 “tool lifecycle hooks”와 “post-execution handlers”에 관한 노트를 반환해요

Vector search는 structured data에도 약해요. JSON 설정 파일, YAML 블록, code snippet은 의미보다 구조적 패턴을 포착하는 embedding을 만들어요. "review": true가 있는 JSON 파일은 code review에 대한 prose discussion과 다르게 임베딩돼요.

Graceful Degradation

sqlite-vec를 로드하지 못하면(확장 누락, 호환되지 않는 플랫폼, 손상된 library), retriever는 BM25-only search로 fallback해요.

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

retriever는 vector query를 시도하기 전에 vec_available을 확인해요. 비활성화되어 있으면 모든 검색은 BM25만 사용하고, RRF fusion 단계는 건너뛰어요.


Reciprocal Rank Fusion (RRF)

RRF는 점수 보정 없이 2개의 순위 목록을 병합해요. 이 섹션에서는 알고리즘, 실제 쿼리 추적 예시, k 파라미터 튜닝, 그리고 대안 대신 RRF를 선택한 이유를 다뤄요. 순위를 직접 편집할 수 있는 대화형 계산기, 시나리오 프리셋, 시각적 아키텍처 탐색기를 보려면 hybrid retriever 심층 분석을 참고하세요.

알고리즘

RRF는 각 목록에서 문서가 차지한 순위만 기준으로 점수를 부여해요.

score(d) = Σ (weight_i / (k + rank_i))

여기서: - k는 평활화 상수예요(60, Cormack et al.3 기준) - rank_i는 결과 목록 i에서 문서의 1부터 시작하는 순위예요 - weight_i는 목록별 선택적 가중치예요(기본값 1.0)

여러 목록에서 높은 순위를 받은 문서는 더 높은 fusion 점수를 받아요. 하나의 목록에만 나타난 문서는 해당 단일 소스에서 나온 점수만 받아요.

대안보다 RRF를 선택한 이유

가중 선형 결합은 BM25 점수와 cosine distance를 보정해야 해요. BM25 점수는 상한이 없고 corpus 크기에 따라 스케일이 달라져요. Cosine distance는 [0, 2] 범위로 제한돼요. 둘을 결합하려면 정규화가 필요하고, 정규화 파라미터는 데이터셋에 따라 달라져요. RRF는 순위 위치만 사용하며, 순위는 점수 산정 방식과 관계없이 항상 1부터 시작하는 정수예요.

학습된 fusion 모델은 라벨이 붙은 학습 데이터, 즉 쿼리-문서 관련성 쌍이 필요해요. 개인 지식 베이스에는 이런 학습 데이터가 없어요. 유용한 모델을 학습하려면 수백 개의 쿼리-문서 쌍을 직접 판정해야 해요. RRF는 학습 데이터 없이 작동해요.

Condorcet voting 방식(Borda count, Schulze method)은 이론적으로 우아하지만 구현하고 튜닝하기가 더 복잡해요. 원래 RRF 논문에서는 TREC 평가 데이터에서 RRF가 Condorcet 방식보다 더 나은 성능을 보인다는 점을 입증했어요.3

실제 Fusion

쿼리: “how does the review aggregator handle disagreements”

BM25는 review-aggregator.py를 3위로 올려요(“review”, “aggregator”, “disagreements”의 정확한 키워드 일치). 하지만 2개의 설정 파일을 더 높은 순위에 둬요(해당 파일들이 “review”와 더 두드러지게 일치하기 때문이에요). Vector search는 같은 chunk를 1위로 올려요(충돌 해결에 대한 의미적 일치). RRF fusion 이후 결과는 다음과 같아요.

Chunk BM25 Vec Fused Score
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

목록에서 모두 높은 순위를 받은 chunk가 상단에 노출돼요. 하나의 목록에만 나타난 chunk는 단일 소스 점수만 받아서 양쪽에서 순위를 받은 결과보다 아래로 내려가요. 실제 disagreement resolution 로직이 승리한 이유는 두 방식이 모두 그것을 찾아냈기 때문이에요. BM25는 키워드로, vector search는 의미로 찾아낸 거예요.

순위별 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배 차이). 개별 ranker가 최상위 결과를 정확히 찾는다고 신뢰할 때 좋아요.
  • 기본 k(60): 균형 잡힌 값이에요. 1위는 1/61 = 0.0164점, 10위는 1/70 = 0.0143점이에요(1.15배 차이). 순위 차이가 압축되어 여러 목록에 함께 나타나는 것에 더 많은 가중치가 실려요.
  • 높은 k(예: 200): 양쪽 목록에 모두 나타나는지가 순위 위치보다 훨씬 더 중요해져요. 1위는 1/201점, 10위는 1/210점으로 거의 같아요. 개별 ranker의 순위가 noisy하지만 목록 간 합의는 신뢰할 만할 때 사용하세요.

k=60으로 시작하세요. 원래 RRF 논문에서는 이 값이 다양한 TREC 데이터셋에서 견고하다는 점을 확인했어요. 자신의 쿼리 분포에서 실패 사례를 측정한 뒤에만 튜닝하세요.

동점 처리

2개의 chunk가 동일한 RRF 점수를 받을 때(드물지만, 한 목록에서 같은 순위를 가지고 다른 목록에는 나타나지 않으면 가능해요), 다음 기준으로 동점을 처리해요.

  1. 하나의 목록에만 나타난 chunk보다 양쪽 목록에 모두 나타난 chunk를 우선하세요
  2. 양쪽 목록에 모두 나타난 chunk 중에서는 합산 순위가 더 낮은 chunk를 우선하세요
  3. 하나의 목록에만 나타난 chunk 중에서는 해당 목록에서 순위가 더 낮은 chunk를 우선하세요

전체 Retrieval Pipeline

이 섹션에서는 입력부터 출력까지 전체 pipeline을 따라 query가 어떻게 처리되는지 살펴봅니다. BM25 search, vector search, RRF fusion, token budget truncation, context assembly를 포함합니다.

End-to-End 흐름

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

총 latency: 약 23ms입니다. Apple M3 Pro 하드웨어에서 49,746개 chunk가 있는 database를 기준으로 측정했습니다.

Search 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

Token Budget Truncation

max_tokens parameter는 retriever가 AI tool이 사용할 수 있는 것보다 많은 context를 반환하지 않도록 막아줍니다. 추정에는 token당 4자를 사용합니다. 이는 영어 prose에 적절한 근사치입니다. Results는 greedy 방식으로 잘립니다. 순위가 높은 순서대로 result를 추가하다가 budget이 소진되면 중단합니다.

이는 보수적인 전략입니다. 더 정교한 접근법이라면 result별 품질 score를 고려해 길고 품질이 낮은 result보다 짧고 품질이 높은 result를 우선할 수 있습니다. Greedy 접근법은 더 단순하며, RRF ranking이 이미 relevance 기준으로 result를 정렬하기 때문에 실제로도 잘 작동합니다.

Database Schema (전체)

-- 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
);

Graceful Degradation Path

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

Retriever는 초기화 시 capabilities를 확인하고 query strategy를 조정합니다. component가 없어도 quality는 낮아지지만 error가 발생하지는 않습니다. 유일한 hard failure는 database 파일이 없는 경우입니다.

Production Stats

16,894개 파일, 49,746개 chunk, 83 MB SQLite database, Apple M3 Pro 환경의 vault에서 측정했습니다.

Metric Value
전체 files 16,894
전체 chunks 49,746
Database 크기 83 MB
BM25 query latency (p50) 12ms
Vector query latency (p50) 8ms
RRF fusion latency 3ms
End-to-end search latency (p50) 23ms
전체 reindex 시간 약 4분
Incremental reindex 시간 <10초
Embedding model potion-base-8M (256-dim)
BM25 candidate pool 30
Vector candidate pool 30
기본 result limit 10
기본 token budget 4,000 tokens

Content Hashing과 Change Detection

Indexer는 마지막 index 실행 이후 어떤 files가 변경되었는지 알아야 합니다. 이 섹션에서는 change detection mechanism과 hashing strategy를 다룹니다.

File Modification Time 비교

Indexer는 chunks table의 모든 chunk에 대해 mtime_ns(file modification time, nanoseconds 단위)를 저장합니다. Incremental 실행 시 indexer는 다음을 수행합니다.

  1. 허용된 folders의 모든 .md files를 찾기 위해 vault를 scan합니다.
  2. Filesystem에서 각 file의 mtime_ns를 읽습니다.
  3. Database에 저장된 mtime_ns와 비교합니다.
  4. 세 가지 category를 식별합니다.
  5. 새 files: path가 filesystem에는 있지만 database에는 없습니다.
  6. 변경된 files: path가 양쪽에 모두 있지만 mtime_ns가 다릅니다.
  7. 삭제된 files: path가 database에는 있지만 filesystem에는 없습니다.
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)

Content Hash가 아니라 mtime을 쓰는 이유

Content hashing(file contents의 SHA-256)은 mtime 비교보다 더 신뢰할 수 있습니다. file이 실제로 변경되지 않았는데 touched된 경우, 예를 들어 git checkout이 원래 mtime을 복원한 경우도 감지할 수 있기 때문입니다. 하지만 hashing은 incremental 실행마다 모든 file을 읽어야 합니다. 16,894개 files의 file contents를 읽는 데는 2-3초가 걸립니다. Filesystem에서 mtimes를 읽는 데는 <100ms가 걸립니다.

Trade-off는 이렇습니다. mtime 비교는 변경되지 않은 files를 불필요하게 re-indexing하는 경우가 가끔 있지만(false positives), 실제 변경을 놓치지는 않습니다. False positives는 실행당 embedding calls를 몇 번 더 쓰는 비용만 발생시킵니다. 속도 차이(100ms vs 3초)를 고려하면, 모든 AI interaction에서 실행되는 system에는 mtime이 실용적인 선택입니다.

Deletions 처리

Vault에서 file이 삭제되면 indexer는 해당 file의 모든 chunks를 database에서 제거합니다.

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],
    )

DELETE FROM chunk_vecs statement는 sqlite-vec v0.1.7부터 native로 작동하며, v0.1.9에서는 metadata text columns가 더 긴 vec0 tables에 대한 DELETE operations 관련 bug fix가 포함되었습니다.1423 이전 versions에서는 workaround가 필요했습니다. 예를 들어 virtual table을 drop 후 recreate하거나, 외부 “active IDs” set을 유지해야 했습니다. pre-0.1.9 version을 실행 중이라면 metadata가 많은 schema에서 direct deletes에 의존하기 전에 upgrade하세요.

FTS5 content-sync tables는 제거된 각 row에 대해 INSERT INTO chunks_fts(chunks_fts, rowid, ...) VALUES('delete', ?, ...)를 통한 명시적 deletion이 필요합니다. Indexer는 file removal process의 일부로 이를 처리합니다.


Incremental 재색인과 Full 재색인

인덱서는 두 가지 모드를 지원해요. incremental(빠르고 일상적으로 사용)과 full(느리고 가끔 사용)입니다. 이 섹션에서는 각 모드를 언제 사용해야 하는지, 멱등성 보장, 손상 복구를 다뤄요.

Incremental 재색인

사용 시점: 노트를 편집한 뒤 매일 인덱싱할 때 사용해요. 기본 모드입니다.

작동 방식: 1. vault에서 파일 변경 사항을 스캔해요(mtime 비교) 2. 삭제된 파일의 chunk를 삭제해요 3. 변경된 파일을 다시 chunking하고 다시 embedding해요 4. 새 파일의 새 chunk를 삽입해요 5. FTS5 인덱스를 동기화해요

일반 소요 시간: 16,000개 파일 vault에서 하루치 편집 내용을 처리하는 데 10초 미만입니다.

python index_vault.py --incremental

Full 재색인

사용 시점: - embedding model을 변경한 뒤(model hash 불일치가 감지됨) - schema migration 뒤(새 열, 변경된 인덱스) - database 손상 뒤(integrity check 실패) - incremental 인덱싱에서 예상치 못한 결과가 나올 때

작동 방식: 1. 기존 데이터를 모두 삭제해요(chunks, vectors, FTS5 entries) 2. 전체 vault를 스캔해요 3. 모든 파일을 chunking해요 4. 모든 chunk를 embedding해요 5. FTS5 인덱스를 처음부터 빌드해요

일반 소요 시간: Apple M3 Pro에서 16,894개 파일 기준 약 4분입니다.

python index_vault.py --full

멱등성

두 모드는 모두 멱등적입니다. 같은 명령을 두 번 실행해도 같은 결과가 나와요. 인덱서는 새 chunk를 삽입하기 전에 해당 파일의 기존 chunk를 삭제하므로, 이미 최신 상태인 database에서 incremental 인덱싱을 다시 실행하면 변경 사항은 0개입니다. full 인덱싱을 다시 실행해도 동일한 database가 만들어져요.

손상 복구

SQLite database가 손상되면(쓰기 중 전원 손실, 디스크 오류, 트랜잭션 도중 프로세스 종료) 다음처럼 처리해요.

# Check integrity
sqlite3 vectors.db "PRAGMA integrity_check;"

# If corruption detected, full reindex rebuilds from source files
python index_vault.py --full

진실의 원천은 항상 database가 아니라 vault 파일입니다. database는 언제든 다시 빌드할 수 있는 파생 산출물이에요. 이것은 중요한 설계 속성입니다. database를 백업할 필요가 없습니다.

--incremental 플래그

인덱서가 --incremental로 실행되면 다음 과정을 거쳐요.

  1. Model hash 확인. 저장된 model hash를 현재 model과 비교해요. 다르면 자동으로 full 재색인 모드로 전환하고 사용자에게 경고해요.
  2. 파일 스캔. 허용된 폴더를 순회하며 파일 경로와 mtime을 수집해요.
  3. 변경 감지. 저장된 데이터와 비교해요.
  4. Batch 처리. 변경된 파일을 64개씩 batch로 다시 chunking하고 다시 embedding해요.
  5. 진행 상황 보고. 처리된 파일 수와 경과 시간을 출력해요.
  6. Graceful shutdown. SIGINT를 받으면 현재 파일 처리를 마친 뒤 중지해요.

자격 증명 필터링과 데이터 경계

개인 노트에는 비밀 정보가 들어 있을 수 있어요. API keys, bearer tokens, database connection strings, 디버깅 중 붙여 넣은 private keys 등이 포함됩니다. 자격 증명 필터는 이런 정보가 retrieval 인덱스에 들어가지 않도록 막아요.

문제

OAuth integration을 디버깅한 노트에는 다음 내용이 있을 수 있어요.

The token was: eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...
I used this curl command:
  curl -H "Authorization: Bearer sk-ant-api03-abc123..."

필터링이 없으면 JWT와 API key가 모두 chunking되고 embedding되어 database에 저장됩니다. “authentication”을 검색하면 실제 비밀 정보가 들어 있는 chunk가 반환돼요. 더 나쁘게는 retriever가 MCP를 통해 결과를 AI tool에 전달하면, 해당 비밀 정보가 AI의 context window와 잠재적으로 tool 로그에 나타납니다.

패턴 기반 필터링

자격 증명 필터는 저장 전에 모든 chunk에서 실행되며, 25개의 vendor-specific pattern과 generic pattern을 매칭해요.

Vendor-Specific Patterns:

Pattern Example Regex
OpenAI API key sk-... sk-[a-zA-Z0-9_-]{20,}
Anthropic API key sk-ant-api03-... sk-ant-api\d{2}-[a-zA-Z0-9_-]{20,}
GitHub PAT ghp_... gh[ps]_[a-zA-Z0-9]{36,}
AWS Access Key AKIA... AKIA[0-9A-Z]{16}
Stripe key sk_live_... [sr]k_(live\|test)_[a-zA-Z0-9]{24,}
Cloudflare token ... 다양한 패턴

Generic Patterns:

Pattern Detection
JWT tokens eyJ[a-zA-Z0-9_-]+\.eyJ[a-zA-Z0-9_-]+
Bearer tokens Bearer\s+[a-zA-Z0-9_\-\.]+
Private keys -----BEGIN (RSA\|EC\|OPENSSH) PRIVATE KEY-----
고엔트로피 base64 40자 이상이며 엔트로피가 4.5 bits/char를 초과하는 문자열
Password assignments 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

핵심 설계 선택:

  1. embedding 전에 필터링해요. 정리된 텍스트가 embedding 대상입니다. vector representation은 자격 증명 패턴을 절대 인코딩하지 않아요. “API key”를 검색하면 실제 key가 들어 있는 노트가 아니라 API key 관리에 대해 설명하는 노트가 반환됩니다.

  2. 삭제하지 않고 대체해요. [REDACTED:pattern-name] token은 주변 텍스트의 의미적 context를 보존해요. embedding은 자격 증명처럼 보이는 무언가가 여기에 있었다는 사실은 포착하지만, 자격 증명 자체는 인코딩하지 않습니다.

  3. 값이 아니라 패턴을 로그에 남겨요. 필터는 어떤 패턴이 매칭됐는지 기록해요(예: “Scrubbed 2 credential(s) from oauth-debug.md [jwt, bearer-token]”). 하지만 자격 증명 값은 절대 로그에 남기지 않습니다.

경로 기반 제외

.indexignore 파일은 경로 기준의 대략적인 제외를 제공합니다. 자격 증명 필터는 인덱싱된 파일 내부에서 세밀하게 scrub을 수행해요. 둘 다 필요합니다.

  • 민감한 내용이 들어 있다는 사실을 알고 있는 전체 폴더에는 .indexignore를 사용하세요(건강 노트, 재무 기록, 커리어 문서)
  • 그 외에는 인덱싱 가능한 콘텐츠에 실수로 포함된 비밀 정보를 처리하기 위해 자격 증명 필터를 사용하세요

데이터 분류

다양한 콘텐츠가 들어 있는 vault라면 민감도에 따라 노트를 분류하는 방식을 고려하세요.

Level Examples Index? Filter?
공개 블로그 초안, 기술 노트
내부 프로젝트 계획, 아키텍처 결정
민감 급여 데이터, 건강 기록 아니요(.indexignore) N/A
제한 Credentials, private keys 아니요(.indexignore) N/A

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 권한 부여, 구조화된 도구 출력(타입이 지정된 반환 스키마), elicitation(서버가 시작하는 사용자 프롬프트)이 추가되었습니다. 2025년 11월 릴리스에서는 Streamable HTTP가 1급 전송 모드로 제공되었고, 자동 서버 기능 탐색을 위한 .well-known URL discovery, 도구가 읽기 전용인지 변경 작업을 수행하는지 선언하는 구조화된 도구 annotations, SDK tier 표준화 시스템이 포함되었습니다.79 다음 사양 릴리스(잠정적으로 2026년 중반)는 오래 걸리는 작업을 위한 비동기 작업, 의료 및 금융 같은 산업별 프로토콜 확장, multi-agent 워크플로를 위한 agent-to-agent 통신 표준을 제안합니다.9 개인용 vault 서버에는 STDIO가 여전히 가장 단순한 경로입니다. Streamable HTTP 전송과 .well-known discovery는 multi-tenant 라우팅과 로드 밸런싱이 필요한 엔터프라이즈 HTTP 배포에서 주로 이점을 제공합니다. 전송 방식을 선택할 때 영향을 줄 수 있는 업데이트는 MCP roadmap을 확인하세요.

기능 설계

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 — 검색을 실행하고 결과를 대화에 삽입하기 적합한 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 서버는 엄격한 경계를 적용해야 합니다.

  1. 읽기 전용. 서버는 vault와 인덱스 데이터베이스를 읽습니다. 노트를 생성, 수정, 삭제하지 않습니다. 쓰기 작업(새 노트 캡처)은 MCP 서버가 아니라 별도의 hooks 또는 skills에서 처리합니다.

  2. Vault 범위 제한. 서버는 설정된 vault 경로 안의 파일만 읽습니다. 경로 순회 시도(../../etc/passwd)는 반드시 거부해야 합니다.

  3. 자격 증명 필터링된 출력. 데이터베이스에 이미 사전 필터링된 콘텐츠가 있더라도, defense-in-depth 조치로 출력 시 자격 증명 필터링을 적용하세요.

  4. 토큰 제한 응답. AI 도구가 지나치게 큰 context 블록을 받지 않도록 모든 도구 응답에 max_tokens를 적용하세요.

오류 처리

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 설정, Hook 통합, 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 등)를 나열해야 합니다.

Hook 통합

Hooks는 정의된 수명 주기 지점에서 Claude Code의 동작을 확장합니다. Obsidian 통합과 관련된 Hook은 2가지입니다.

PreToolUse hook — 에이전트가 도구 호출을 처리하기 전에 볼트를 쿼리합니다. 관련 컨텍스트를 자동으로 주입합니다.

#!/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 hook — 중요한 도구 출력을 다시 볼트에 저장해 나중에 검색할 수 있게 합니다.

#!/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 패턴

브리지 모듈은 Hook과 스킬에서 호출할 수 있는 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 스킬

인사이트를 다시 볼트에 저장하기 위한 Claude Code 스킬입니다.

/capture "OAuth token rotation requires both access and refresh token invalidation"
  --domain security
  --tags oauth,tokens

이 스킬은 적절한 frontmatter를 포함한 새 노트를 00-inbox/에 만들고, 증분 재색인을 트리거해 새 노트를 즉시 검색할 수 있게 합니다.

사용자 지정 명령 패턴

Claude Code 스킬은 볼트 작업을 이름이 지정된 명령으로 감쌀 수 있습니다. 실무자들은 볼트를 읽기 소스이자 쓰기 대상으로 다루는 Obsidian 전용 명령 라이브러리를 구축해 왔습니다.

신호 스캔. /scan-intel 명령은 외부 소스를 쿼리하고, 개인 연구 관심사에 맞춰 발견 내용을 점수화한 뒤, 기준을 통과한 신호를 frontmatter가 포함된 볼트 노트로 작성합니다.

/scan-intel --topics "agent infrastructure, security" --lookback 7d

이 명령은 설정된 소스(arXiv, HN, RSS)에서 데이터를 가져오고, 점수 모델(관련성, 실행 가능성, 깊이, 권위)을 적용한 뒤, 기준을 통과한 신호를 주제별 볼트 폴더에 작성합니다. 볼트는 자동화된 인텔리전스 파이프라인의 downstream consumer가 됩니다.

Captain’s log. /captains-log 명령은 모든 저장소의 일일 git 활동을 집계하고, 구조화된 저널 항목을 볼트에 작성하며, 내린 결정, 깨달음, 열린 스레드를 포함합니다.

/captains-log

이 명령은 GitHub에서 커밋 기록을 가져와 저장소별로 묶고, 서사형 저널 항목으로 형식화합니다. 시간이 지나면서 일일 로그는 무엇이 출시되었고 왜 그렇게 되었는지 검색 가능한 기록을 만듭니다.

Obsidian 캡처. /obsidian-capture 명령은 현재 Claude Code 세션의 인사이트를 받아 적절한 metadata와 함께 볼트에 직접 작성합니다.

/obsidian-capture "SAST gates in agent loops increase security degradation"
  --folder AI-Tools --tags security,agents

이 패턴은 어떤 볼트 작업으로도 확장됩니다. MOC 생성, 프로젝트 상태 노트 업데이트, 관련 신호 연결, 누적된 일일 로그에서 주간 다이제스트 생성 등이 가능합니다.

커뮤니티 예시. 실무자들은 자신들의 명령 라이브러리를 공개하고 있습니다. 한 개발자는 일일 리뷰, 프로젝트 계획, 연구 캡처, 콘텐츠 워크플로를 다루는 Obsidian + Claude Code 사용자 지정 명령 22개를 공유했습니다.1 또 다른 개발자는 코드 분석을 바탕으로 볼트에 다이어그램 노트를 생성하는 “Visual Explainer” 스킬을 만들었습니다.2 명령은 다양하지만 아키텍처는 일관적입니다. Claude Code 스킬은 인터페이스, 볼트 노트는 저장 계층, 검색 인프라는 쿼리 엔진 역할을 합니다.

컨텍스트 창 관리

통합은 Claude Code의 컨텍스트 창을 고려해야 합니다.

  • 주입되는 컨텍스트를 쿼리당 1,500-2,000 tokens로 제한하세요. 이보다 많으면 에이전트의 작업 메모리와 경쟁하게 됩니다.
  • 출처 표시를 포함하세요. 에이전트가 출처를 참조할 수 있도록 파일 경로와 섹션 제목을 항상 포함하세요.
  • chunk 텍스트를 잘라내세요. 긴 chunks는 완전히 생략하기보다 ...로 잘라내야 합니다. 보통 처음 300-500자가 핵심 정보를 담고 있습니다.
  • 모든 도구 호출에 주입하지 마세요. PreToolUse hook은 호출되는 도구에 따라 선택적으로 컨텍스트를 주입해야 합니다. 읽기 작업에는 볼트 컨텍스트가 필요하지 않습니다. Write 및 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를 읽습니다. 볼트 검색 지침을 포함하세요.

## 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
Hooks ~/.claude/hooks/ 지원되지 않음
스킬 ~/.claude/skills/ 지원되지 않음
지침 파일 CLAUDE.md AGENTS.md
승인 모드 --dangerously-skip-permissions suggest / auto-edit / full-auto

핵심 차이: Codex CLI는 hooks를 지원하지 않습니다. 자동 컨텍스트 주입 패턴(PreToolUse hook)은 사용할 수 없습니다. 대신 AGENTS.md에 작업을 시작하기 전에 에이전트가 볼트를 검색하도록 명시적인 지침을 포함하세요.

Cursor 및 기타 도구

MCP를 지원하는 Cursor와 기타 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 파일에는 저장소를 사용하라는 지침을 포함할 수 있습니다.

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
Claudian (Obsidian plugin) N/A (내장) Claude Code CLI Obsidian plugin 설정
Agent Client (Obsidian plugin) N/A (내장) ACP Obsidian plugin 설정

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 통합만큼 세련되지는 않지만, 어디서나 사용할 수 있습니다.


구조화된 노트에서 Prompt Caching 사용하기

저장소의 구조화된 노트는 AI 상호작용 전반에서 토큰 사용량을 줄이는 재사용 가능한 컨텍스트 블록 역할을 할 수 있습니다. 이 섹션에서는 캐시 키 설계와 토큰 예산 관리를 다룹니다.

패턴

상호작용할 때마다 컨텍스트를 검색하는 대신, 잘 구조화된 저장소 노트에서 컨텍스트 블록을 미리 만들고 캐시하세요.

# 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가지 신호를 기준으로 합니다.

  1. TTL 만료. 각 컨텍스트 블록에는 time-to-live가 있습니다. TTL이 만료되면 저장소를 다시 질의해 블록을 다시 만듭니다.
  2. 저장소 변경 감지. 인덱서가 캐시된 컨텍스트 블록에 기여한 파일의 변경을 감지하면, 해당 블록은 즉시 무효화됩니다.

토큰 예산 관리

세션은 전체 컨텍스트 예산으로 시작합니다. 캐시된 블록은 이 예산의 일부를 사용합니다.

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)

캐시된 블록은 세션 시작 시 로드됩니다. 동적 검색 결과는 각 질의에 따라 남은 예산을 채웁니다. 이 hybrid 접근 방식은 에이전트에 자주 필요한 컨텍스트의 기준선을 제공하면서도, 특정 질의를 위한 예산을 남겨 둡니다.

캐싱 전후 토큰 사용량

캐싱 없음: 관련 질의가 있을 때마다 저장소 검색이 실행되고, 1,500-2,000토큰의 컨텍스트가 반환됩니다. 한 세션에서 10번 질의하면 에이전트는 저장소 컨텍스트로 15,000-20,000토큰을 사용합니다.

캐싱 사용: 미리 만든 컨텍스트 블록 3개가 총 4,500토큰을 사용합니다. 추가 검색은 고유 질의마다 1,500-2,000토큰을 더합니다. 10번의 질의 중 6번이 캐시된 블록으로 처리되는 경우, 에이전트는 4,500 + (4 * 1,500) = 10,500토큰을 사용합니다. 캐싱하지 않을 때의 대략 절반 수준입니다.


컨텍스트 압축을 위한 PostToolUse Hooks

도구 출력은 장황할 수 있습니다. 예를 들면 stack trace, 파일 목록, 테스트 결과가 있습니다. 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개 항목 + 개수로 잘라내기
Stack trace Traceback 키워드 첫 번째와 마지막 frame + 오류 메시지 유지
Git status modified: / new file: 상태별 개수 요약
빌드 출력 warning: / error: 정보성 줄은 제거하고 warning/error 유지

신호 수집 및 분류 파이프라인

수집 레이어는 무엇이 vault에 들어올지 결정해요. 큐레이션이 없으면 vault에는 노이즈가 쌓여요. 이 섹션에서는 신호를 도메인 폴더로 라우팅하는 점수화 파이프라인을 다뤄요.

소스

신호는 여러 채널에서 들어와요:

  • RSS 피드: 기술 블로그, 보안 권고, 릴리스 노트
  • Web Clipper를 통한 북마크: 공식 Obsidian Web Clipper 확장 프로그램(Chrome, Firefox, Safari)은 브라우저 쪽 캡처에서 가장 충실도가 높은 수집 경로예요. 2026년 4월 릴리스 주기에는 AI 워크플로에 실질적으로 더 유용해졌어요:22
    • 1.4.0 (4월 9일): 대화형 YouTube transcript UI — 동영상을 고정하고, transcript를 훑어보고, 자동 스크롤하며, 현재 위치를 강조 표시할 수 있어요. 또한 “Open in Reader” 기본값을 통해 한 번의 클릭으로 캡처 내용을 Reader 모드로 바로 보낼 수 있어요.
    • 1.5.0–1.5.1 (4월 15일): Highlights viewer — vault 전체에서 캡처한 하이라이트를 탐색하고 검색할 수 있어요. Reader로 들어갈 때 페이드인 전환이 추가되었고, YouTube 재생/일시정지가 더 부드러워졌어요. 1.5.1에서는 webpack 컴파일 회귀 문제가 수정되었어요.
    • 1.6.0–1.6.2 (4월 21–23일): 모바일 지원을 포함한 하이라이터 UX 개편이 있었어요. Defuddle 0.18은 LinkedIn, Threads, Bluesky, Discourse, Medium용 소스별 추출기를 추가해요. 1.6.2는 Safari embedded-mode 클립보드 회귀 문제를 수정해요. 소스 도메인별로 템플릿을 설정해 YouTube transcript, GitHub README, 긴 글이 각각 아래 점수화 파이프라인에 맞는 frontmatter를 갖춘, 이름이 적절한 노트로 들어가게 하세요.
  • 뉴스레터: 이메일 뉴스레터의 핵심 발췌
  • 수동 캡처: 읽기, 대화, 리서치 중 작성한 노트
  • 도구 출력: hooks를 통해 캡처한 중요한 AI 도구 출력
  • iOS Share Extension: Obsidian의 iOS 앱(2026년 초 업데이트)에는 Safari, 소셜 네트워크, 기타 앱의 콘텐츠를 Obsidian을 열지 않고도 vault에 바로 저장하는 Share Extension이 포함되어 있어요.19 이 기능은 마찰이 적은 모바일 수집 경로를 만들어요. Safari에서 글을 공유하면 점수화할 준비가 된 vault 노트로 들어와요.
  • Obsidian CLI: 셸 스크립트와 hooks는 obsidian file create로 노트를 만들거나 obsidian file append로 기존 노트에 덧붙일 수 있어 데스크톱에서 자동화된 수집 파이프라인을 구현할 수 있어요.

점수화 차원

각 신호는 4가지 차원에서 점수를 매겨요(각각 0.0부터 1.0까지):

차원 질문 낮은 점수 (0.0-0.3) 높은 점수 (0.7-1.0)
관련성 이 내용이 내 활성 도메인과 관련 있나요? 주변적이거나 범위 밖 현재 작업과 직접적으로 관련 있음
실행 가능성 이 정보를 활용할 수 있나요? 순수 이론이며 적용점 없음 적용할 수 있는 구체적 기법이나 패턴
깊이 콘텐츠가 얼마나 실질적인가요? 헤드라인, 얕은 요약 예시가 포함된 상세 분석
권위 소스가 얼마나 신뢰할 만한가요? 익명 블로그, 검증되지 않음 1차 출처, 동료 검토됨, 인정받은 전문가

종합 점수와 라우팅

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
일평균 신호 수 ~18

Knowledge Graph 패턴

Obsidian의 wiki-link graph는 노트 간 관계를 인코딩해요. 이 섹션에서는 링크 의미론, 컨텍스트 확장을 위한 graph traversal, graph 품질을 떨어뜨리는 안티패턴을 다뤄요.

모든 wiki-link는 graph에 방향성 edge를 만들어요. Obsidian은 forward link와 backlink를 모두 추적해요:

  • Forward link: 노트 A에 [[Note B]]가 포함됨 → A가 B로 링크함
  • Backlink: 노트 B에는 노트 A가 자신을 참조한다는 내용이 표시됨

graph는 컨텍스트에 따라 다양한 유형의 관계를 인코딩해요:

링크 패턴 의미 예시
인라인 링크 “관련 있음” “자세한 내용은 [[OAuth Token Rotation]]를 참고하세요”
헤더 링크 “하위 주제가 있음” ”## Related\n- [[Token Rotation]]\n- [[Session Management]]”
태그형 링크 “다음으로 분류됨” ”[[type/reference]]”
MOC 링크 “일부임” 관련 노트를 나열하는 Map of Content 노트

Maps of Content (MOCs)

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는 검색에 두 가지 방식으로 도움이 돼요:

  1. 직접 매치. “authentication overview”를 검색하면 MOC 자체가 매치되어, 에이전트에 큐레이션된 관련 노트 목록을 제공해요.
  2. 컨텍스트 확장. 특정 노트를 찾은 뒤 retriever는 해당 노트가 어떤 MOC에 나타나는지 확인하고, 결과에 MOC 구조를 포함할 수 있어요. 이렇게 하면 에이전트가 더 넓은 주제의 지도를 얻을 수 있어요.

컨텍스트 확장을 위한 Graph Traversal

retriever의 향후 개선 사항: 상위 결과를 찾은 뒤 링크를 따라가며 컨텍스트를 확장해요:

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)

이 기능은 현재 retriever에는 구현되어 있지 않지만, graph 구조의 자연스러운 확장이라고 볼 수 있어요.

안티패턴

고립된 클러스터. 서로 연결되어 있지만 vault의 나머지 부분과는 연결이 없는 노트 그룹이에요. Obsidian의 graph 패널에서는 이런 그룹이 분리된 섬처럼 보여요. 고립된 클러스터는 MOC가 없거나 도메인 간 링크가 부족하다는 신호예요.

태그 난립. 태그를 일관성 없이 사용하거나 지나치게 세분화된 태그를 너무 많이 만드는 경우예요. 5,000개 노트에 고유 태그가 500개 있는 vault는 태그 10개당 노트 1개꼴이에요. 이런 태그는 필터링에 유용하지 않아요. 도메인 폴더와 매핑되는 20-50개의 상위 수준 태그로 통합하세요.

링크는 많고 내용은 적은 노트. wiki-link만 있고 설명 문장이 없는 노트예요. chunker가 embedding할 텍스트가 없기 때문에 이런 노트는 인덱싱 품질이 낮아요. 링크된 노트들이 왜 관련 있는지 설명하는 컨텍스트를 최소한 한 단락은 추가하세요.

모든 것에 양방향 링크 사용. 모든 참조가 wiki-link일 필요는 없어요. “OAuth”을 지나가듯 언급한다고 해서 [[OAuth 2.0 Overview]]가 필요한 것은 아니에요. 클릭했을 때 유용한 컨텍스트를 제공하는 의도적이고 탐색 가능한 관계에만 wiki-link를 사용하세요.

개발자 워크플로 레시피

보관함 검색을 일상적인 개발 작업과 결합하는 실용적인 워크플로입니다.

아침 컨텍스트 불러오기

하루를 시작할 때 관련 컨텍스트를 불러오세요.

Search my vault for notes about [current project] updated in the last week

검색기는 현재 진행 중인 프로젝트와 관련된 최근 노트를 반환해, 어디까지 작업했는지 빠르게 떠올릴 수 있게 해줘요. 어제의 commit 메시지를 다시 읽는 것보다 더 효과적이에요.

코딩 중 리서치 캡처

기능을 구현하는 동안 editor를 벗어나지 않고 인사이트를 캡처하세요.

/capture "FastAPI dependency injection with async generators requires yield,
not return. The generator is the dependency lifecycle."
  --domain programming
  --tags fastapi,dependency-injection

캡처한 인사이트는 즉시 색인되어 나중에 검색할 수 있어요. 몇 달이 지나면 이런 작은 캡처들이 구현별 지식 말뭉치를 만들어 줘요.

프로젝트 시작

새 프로젝트나 기능을 시작할 때는 다음을 따르세요.

  1. 보관함 검색: “[technology/pattern]에 대해 내가 아는 것은?”
  2. 상위 5개 결과를 검토해 이전 결정과 주의할 점을 확인하세요
  3. 해당 도메인에 MOC가 있는지 확인하고, 없다면 하나 만드세요
  4. 실패 모드를 검색하세요: “[technology] 관련 문제”

보관함 검색으로 디버깅하기

오류나 예상치 못한 동작을 만났을 때:

Search my vault for [error message or symptom]

이전 디버깅 노트에는 root cause와 fix가 들어 있는 경우가 많아요. 프로젝트 전반에서 반복되는 문제에 특히 유용합니다. 보관함은 사용자가 잊은 내용을 기억해줘요.

Code Review 준비

PR을 검토하기 전에:

Search my vault for patterns and conventions about [module being changed]

보관함은 검토 중인 코드와 관련된 이전 결정, 아키텍처 제약, 코딩 표준을 반환해요. 리뷰는 diff만이 아니라 조직의 축적된 지식에 기반하게 됩니다.


성능 튜닝

이 섹션에서는 보관함 크기와 사용 패턴별 최적화 전략을 다룹니다.

색인 크기 관리

보관함 크기 청크 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개 이상의 노트에서는 다음을 고려하세요. - 더 빠른 embedding을 위해 batch size를 64에서 128로 늘리기 - 동시 접근을 위해 WAL mode(기본값) 사용하기 - 사용량이 적은 시간대에 전체 재색인 실행하기

Query 최적화

WAL mode. SQLite의 Write-Ahead Logging mode는 indexer가 쓰기 작업을 하는 동안에도 동시 읽기를 가능하게 해줘요.

db.execute("PRAGMA journal_mode=WAL")

indexer가 증분 업데이트를 실행하는 동안 MCP 서버가 query를 처리할 때 중요해요.

Connection pooling. MCP 서버는 query마다 새 connection을 열기보다 database connection을 재사용해야 해요. WAL mode를 사용하는 하나의 오래 유지되는 connection은 동시 읽기를 지원합니다.

# 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

Memory-mapped I/O. mmap_size pragma는 database 파일에 memory-mapped I/O를 사용하라고 SQLite에 알려줘요. 83 MB database라면 전체 파일을 메모리에 매핑해 대부분의 disk read를 없앨 수 있어요.

FTS5 최적화. 전체 재색인 후 다음을 실행하세요.

INSERT INTO chunks_fts(chunks_fts) VALUES('optimize');

이 작업은 FTS5의 내부 b-tree segment를 병합해 이후 검색의 query latency를 줄여줘요.

확장 벤치마크

Apple M3 Pro, 36 GB RAM, NVMe SSD에서 측정했어요.

작업 500개 노트 5K 노트 15K 노트 50K 노트
BM25 query 2ms 5ms 12ms 25ms
Vector query 1ms 3ms 8ms 20ms
RRF fusion <1ms <1ms 3ms 5ms
전체 검색 3ms 8ms 23ms 50ms

모든 벤치마크에는 database access, query execution, result formatting이 포함되어 있어요. MCP STDIO 통신의 network latency는 1-2ms를 추가합니다.


문제 해결

색인 Drift

증상: 검색이 오래된 결과를 반환하거나 최근 추가한 노트를 놓칩니다.

원인: 노트를 추가한 뒤 incremental indexer가 실행되지 않았거나, 파일의 mtime이 업데이트되지 않았어요. 예를 들어 timestamp를 보존한 상태로 다른 기기에서 동기화된 경우가 있어요.

해결: 전체 재색인을 실행하세요: python index_vault.py --full

Embedding Model 교체

증상: embedding model을 바꾼 뒤 vector search가 말이 안 되는 결과를 반환합니다.

원인: 이전 모델의 old vector가 새 query vector와 비교되고 있어요. 차원이나 vector space semantics가 호환되지 않습니다.

해결: indexer는 model hash mismatch를 감지하고 자동으로 전체 재색인을 트리거해야 해요. 그렇지 않다면 database를 수동으로 비우고 다시 색인하세요.

rm vectors.db
python index_vault.py --full

FTS5 Maintenance

증상: 여러 번의 증분 업데이트 후 FTS5 query가 부정확하거나 불완전한 결과를 반환합니다.

원인: 많은 작은 업데이트 후 FTS5 내부 segment가 조각화될 수 있어요.

해결: rebuild와 optimize를 실행하세요.

INSERT INTO chunks_fts(chunks_fts) VALUES('rebuild');
INSERT INTO chunks_fts(chunks_fts) VALUES('optimize');

MCP Timeout

증상: AI tool이 MCP 서버가 timed out되었다고 보고합니다.

원인: 첫 query가 model loading(lazy initialization)을 트리거하며, 이 작업에는 2-5초가 걸려요. AI tool의 기본 MCP timeout이 더 짧을 수 있습니다.

해결: 서버 시작 시 모델을 미리 warm up하세요.

# In MCP server initialization
retriever = HybridRetriever(db_path, vault_path)
retriever.search("warmup", limit=1)  # Trigger model load

SQLite File Locks

증상: SQLITE_BUSY 또는 SQLITE_LOCKED 오류가 발생합니다.

원인: 여러 process가 동시에 database에 쓰고 있어요. WAL mode는 동시 읽기는 허용하지만 writer는 하나만 허용합니다.

해결: 하나의 process(indexer)만 database에 쓰도록 하세요. MCP 서버와 hook은 읽기만 해야 해요. 동시 쓰기가 필요하다면 WAL mode를 사용하고 busy timeout을 설정하세요.

db.execute("PRAGMA busy_timeout=5000")  # Wait up to 5 seconds

sqlite-vec가 Load되지 않음

증상: Vector search가 비활성화되고 retriever가 BM25-only mode로 실행됩니다.

원인: sqlite-vec extension이 설치되지 않았거나, library path에서 찾을 수 없거나, SQLite version과 호환되지 않습니다.

해결:

# Install via pip
pip install sqlite-vec

# Or compile from source
git clone https://github.com/asg017/sqlite-vec
cd sqlite-vec && make

extension이 load되는지 확인하세요.

import sqlite3
db = sqlite3.connect(":memory:")
db.enable_load_extension(True)
db.load_extension("vec0")
print("sqlite-vec loaded successfully")

대형 보관함 메모리 문제

증상: 대형 보관함(50,000개 이상 노트)을 전체 재색인하는 동안 out-of-memory 오류가 발생합니다.

원인: Embedding batch size가 너무 크거나, 모든 파일 내용을 동시에 메모리에 불러오고 있어요.

해결: batch size를 줄이고 파일을 증분으로 처리하세요.

BATCH_SIZE = 32  # Reduce from 64

또한 indexer가 모든 파일을 메모리에 한꺼번에 불러오는 대신, 파일을 하나씩 처리하는지 확인하세요. 각 파일을 읽고, chunking하고, embedding한 뒤 다음 파일로 넘어가야 해요.


Migration Guide

Apple Notes에서 이전하기

  1. Apple Notes의 “Export All” 옵션(macOS)으로 내보내거나 apple-notes-liberator 같은 migration tool을 사용하세요
  2. markdownify 또는 pandoc을 사용해 HTML exports를 markdown으로 변환하세요
  3. 변환된 파일을 보관함의 00-inbox/ 폴더로 이동하세요
  4. 각 노트를 검토하고 frontmatter를 추가하세요
  5. 노트를 적절한 도메인 폴더로 이동하세요

Notion에서 이전하기

  1. Notion에서 내보내세요: Settings → Export → Markdown & CSV
  2. export 파일의 압축을 풀어 보관함의 00-inbox/ 폴더에 넣으세요
  3. Notion-specific markdown artifact를 수정하세요:
  4. Notion은 checklist에 - [ ]를 사용해요. 이는 표준 markdown입니다
  5. Notion은 property table을 HTML로 포함해요. 이를 YAML frontmatter로 변환하세요
  6. Notion은 image를 relative path로 embed해요. image를 attachments 폴더로 복사하세요
  7. 표준 frontmatter(type, domain, tags)를 추가하세요
  8. Notion page link를 Obsidian wiki-link로 교체하세요

Google Docs에서 이전하기

  1. Google Takeout을 사용해 모든 document를 내보내세요
  2. .docx 파일을 markdown으로 변환하세요: pandoc -f docx -t markdown input.docx -o output.md
  3. 일괄 변환하세요: for f in *.docx; do pandoc -f docx -t markdown "$f" -o "${f%.docx}.md"; done
  4. 보관함으로 이동하고, frontmatter를 추가한 뒤, 폴더로 정리하세요

Plain Markdown에서 이전하기(Obsidian 없음)

이미 markdown 파일 디렉터리가 있다면:

  1. 해당 디렉터리를 Obsidian 보관함으로 여세요(Obsidian → Open Vault → Open folder)
  2. 디렉터리가 version-controlled 상태라면 .obsidian/.gitignore에 추가하세요
  3. frontmatter template을 만들고 기존 파일에 적용하세요
  4. 읽고 정리하면서 [[wiki-links]]로 노트를 연결하기 시작하세요
  5. 즉시 indexer를 실행하세요. retrieval system은 첫날부터 작동합니다

다른 Retrieval System에서 이전하기

다른 embedding/search system에서 이전하는 경우:

  1. vector를 이전하려고 하지 마세요. 서로 다른 모델은 호환되지 않는 vector space를 생성합니다. 새 모델로 전체 재색인을 실행하세요.
  2. index가 아니라 content를 이전하세요. 보관함 파일이 source of truth입니다. index는 파생 artifact예요.
  3. 이전 후 확인하세요. 답을 알고 있는 query 10-20개를 실행하고 결과가 기대와 일치하는지 확인하세요.

변경 내역

날짜 변경 사항
2026-05-28 Obsidian 1.13.0 desktop + 1.13.0 mobile (Catalyst early-access)이 출시되었습니다. Desktop: 자체 창에서 열리고 내장 검색과 키보드 탐색을 지원하는 개선된 설정 패널, Obsidian URI가 작업을 실행하기 전에 확인 대화상자를 표시하도록 변경, 네트워크 드라이브에서 HTML 리소스를 로드하기 전 새 경고 추가, Bookmarks 보기의 검색 추가, 향상된 editor 이미지 처리, File Explorer / Properties / Sync 개선, 다수의 developer-API 및 버그 수정. Mobile: 대상 위치를 설정할 수 있는 새 iOS Share Sheet, 탭 전환기에서 탭 순서 변경, 분할 화면과 고정 사이드바 크기를 조정하는 태블릿 길게 누르기 제스처, Bases의 테이블 보기 열 크기 조정 메뉴 항목 추가, iOS 및 검색 버그 수정. AI 워크플로에 주는 의미: Obsidian URI 확인 대화상자는 URI 기반 MCP/agent 통합에 의도적인 확인 단계를 추가합니다. Bases 열 크기 조정 메뉴는 agent가 쿼리하는 vault 전면 인덱스로 Bases를 더 쉽게 사용할 수 있게 합니다. iOS Share Sheet의 설정 가능한 대상은 이미 기본 수집 경로로 문서화된 iPhone 캡처 경로를 Claude/Codex 파이프라인에 더 빠르게 연결할 수 있게 합니다.
2026-05-06 소스로 검증한 최신 상태를 갱신했습니다. Smart Connections v4.5.0은 footer connections를 Core로 옮겼습니다. sqlite-vec v0.1.8/v0.1.9 stable release는 패키징과 DELETE 동작을 업데이트했습니다. Model2Vec v0.8.x는 tokenizer/persistence 내부 구조와 benchmark table을 업데이트했습니다. Obsidian CLI 연대기를 “1.12.7에서 CLI 도입”에서 “1.12.0에서 CLI 도입, 1.12.7에서 설치/런타임 패키징 개선”으로 바로잡았습니다.
2026-04-27 Web Clipper 4월 주기: 1.4.0 (대화형 YouTube transcript UI + Open in Reader 기본값), 1.5.0 (Highlights viewer), 1.6.0 (Highlighter UX 전면 개선 + LinkedIn/Threads/Bluesky/Discourse/Medium용 Defuddle 0.18 source extractors), 1.6.1 + 1.6.2 (Reader 및 Safari 수정). Web Clipper를 단순한 bookmark 언급이 아니라 AI 워크플로의 기본 browser-side 수집 경로로 다시 정리했습니다. 해당 기간에는 Obsidian desktop, Sync, Bases release가 없었습니다.
2026-04-16 Smart Connections v4.3.0 (graph view, 설정 가능한 dock, block-embedding 복구, Substrate cross-plugin env). 2026년 4월 AI-native plugin 흐름(Cortex, VaultSearch, LLM Wiki, Drift, EngramQuest, Hybrid Search MCP)을 문서화했습니다. MarkusPfundstein/mcp-obsidian은 maintenance-mode로 표시했습니다(마지막 commit 2025년 6월). Dataview는 휴면 상태이며, 새 작업에는 Bases가 후속 선택지입니다. Obsidian CLI 1.12.7은 AI assistant용 선호 bridge로 계속 유지됩니다.
2026-04-01 Obsidian CLI 섹션(v1.12의 AI 워크플로용 command)을 추가했습니다. agent plugin 섹션(Claudian, Agent Client)을 추가했습니다. vault 구성을 위한 Bases core plugin을 문서화했습니다. plugin 수를 2,500+로 업데이트했습니다. iOS Share Extension을 수집 소스로 추가했습니다. embedded agent plugin을 포함해 compatibility matrix를 업데이트했습니다.
2026-03-30 MCPVault v0.11.0: list_all_tags tool, .base/.canvas 지원, @bitbonsai/mcpvault로 이름 변경. Obsidian Desktop v1.12.7은 더 빠른 terminal 상호작용을 위해 CLI binary를 번들로 제공합니다.
2026-03-23 sqlite-vec v0.1.7 stable을 문서화했습니다. vec0 table의 DELETE 지원, pagination용 KNN distance constraint. DiskANN approximate nearest neighbor index가 향후 release용으로 발표되었습니다.
2026-03-07 embedding model 비교에 potion-multilingual-128M(101개 언어, 2025년 5월)을 추가했습니다. sqlite-vec는 v0.1.7-alpha.10 상태입니다(CI/CD 수정, 기능 변경 없음). MCP spec과 retrieval 기법이 최신 상태임을 확인했습니다.
2026-03-03 MCP spec 진화 내용을 업데이트했습니다(2025년 11월 출시: Streamable HTTP, .well-known, tool annotations). Model2Vec fine-tuning과 BPE/Unigram tokenizer 지원을 추가했습니다. community MCP server 비교표를 추가했습니다. Smart Connections를 v4로 업데이트했습니다.
2026-03-02 model 비교에 potion-base-32M과 potion-retrieval-32M을 추가했습니다. quantization/dimensionality reduction 섹션을 추가했습니다. MCP spec 진화 메모를 추가했습니다.
2026-03-01 최초 release

참고 자료


  1. Internet Vin, “22 commands I use with Obsidian and Claude Code,” 2026년 3월, x.com/internetvin/status/2026461256677245131

  2. Nicopreme, “Visual Explainer” agent skill with slash commands, x.com/nicopreme/status/2023495040258261460

  3. 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를 소개합니다. 

  4. OpenAI Embeddings Pricing. text-embedding-3-small: 100만 토큰당 $0.02. 전체 재인덱싱 1회당 예상 vault 비용은 약 $0.30입니다. 

  5. van Dongen, T. et al. Model2Vec: Turn any Sentence Transformer into a Small Fast Model. arXiv, 2025. sentence transformer에서 static embeddings를 생성하는 distillation 접근법을 설명합니다. 

  6. potion-base-8M Model CardModel2Vec results. 현재 공개된 표에서는 potion-base-8M이 51.32 Avg (All) / 51.08 Avg (MTEB)로 보고되며, all-MiniLM-L6-v2의 55.80 Avg (All) / 55.93 Avg (MTEB)와 비교하면 전체 작업 점수 기준으로 약 92%를 유지합니다. 

  7. Model Context Protocol Specification. AI 도구를 데이터 소스에 연결하기 위한 MCP 표준입니다. 

  8. Model2Vec Potion Models, potion-base-32M, potion-retrieval-32M. 현재 model card에서는 potion-base-32M이 52.83 Avg (All), potion-retrieval-32M이 retrieval 표에서 35.06으로 보고됩니다. 

  9. Update on the Next MCP Protocol Release. 2025년 11월 릴리스에는 Streamable HTTP transport, .well-known URL discovery, structured tool annotations, SDK tier 표준화가 포함되었습니다. 다음 릴리스는 async operations, domain-specific extensions, agent-to-agent communication을 포함해 2026년 중반으로 잠정 예정되어 있습니다. 

  10. Model2Vec Releases. v0.4.0 (2025년 2월): training/fine-tuning 지원. v0.5.0 (2025년 4월): backend 재작성, quantization, dimensionality reduction. v0.7.0 (2025년 10월): vocabulary quantization, BPE/Unigram tokenizer 지원. v0.8.0/v0.8.1 (2026년 3월): tokenizer 및 persistence 리팩터링, Python 3.9 지원 중단, MTEB V2 결과 업데이트, Windows 경로 호환성. 

  11. Smart Connections for Obsidian. Smart Connections v4: local-first AI embeddings, semantic search는 초기 인덱싱 후 오프라인에서도 작동합니다. 

  12. potion-multilingual-128M. Minish Lab, 2025년 5월. 101개 언어 static embedding 모델이며, multilingual static embeddings 중 가장 높은 성능을 냅니다. 다른 potion 모델과 동일하게 numpy-only 의존성을 사용합니다. 

  13. MCPVault v0.11.0. 2026년 3월. frontmatter와 hashtag를 개수와 함께 스캔하는 새 list_all_tags 도구가 추가되었습니다. 점이 포함된 폴더 처리, .base.canvas 파일 지원이 개선되었습니다. npm 패키지 이름은 @bitbonsai/mcpvault로 변경되었습니다. 

  14. sqlite-vec v0.1.7 Release. 2026년 3월 17일. 안정 릴리스: vec0 virtual table의 DELETE 지원, pagination을 위한 KNN distance constraints, fuzz testing 개선이 포함되었습니다. 향후 릴리스에 DiskANN approximate nearest neighbor indexing이 예정되어 있습니다. 

  15. Introduction to Bases. Obsidian core plugin은 v1.9.10에서 도입되었습니다. frontmatter 속성을 필드로 사용해 vault 파일 위에 database와 유사한 보기(표, 갤러리, 캘린더, kanban 보드)를 제공합니다. 파일은 .base 형식으로 저장됩니다. 

  16. Obsidian Desktop v1.12.0 ChangelogObsidian Desktop v1.12.7 Changelog. v1.12.0에서는 터미널 기반 vault 자동화를 위한 CLI가 도입되었고, v1.12.7에서는 standalone binary, TUI, socket-file 동작으로 설치 및 runtime packaging이 개선되었습니다. CLI documentation도 참고하세요. 

  17. Claudian. Claude Code를 vault 안의 AI 협업자로 포함하는 Obsidian plugin입니다. sidebar chat, context-aware prompts, vision support, slash commands, permission modes를 제공합니다. 

  18. Agent Client. Agent Client Protocol (ACP)을 통해 Claude Code, Codex CLI, Gemini CLI에 대한 통합 인터페이스를 제공하는 Obsidian plugin입니다. note mentions, shell execution, action approval을 지원합니다. 

  19. Obsidian iOS Changelog. 2026년 초 업데이트에는 다른 앱의 콘텐츠를 vault에 직접 저장하는 Share Extension, Daily Note 및 Bookmark widget 수정, View Note widget 새로고침 개선이 포함됩니다. 

  20. MarkusPfundstein/mcp-obsidian. 마지막 commit은 2025년 6월 28일입니다. tagged release는 없습니다. Forum 논의(2026년 4월)에 따르면 Obsidian 1.12.x가 first-class CLI를 제공한 이후 커뮤니티는 CLI 기반 통합으로 이동하고 있습니다. 이 가이드에서는 역사적 맥락과 기존 설정을 사용하는 사용자를 위해 보존하지만, 새 배포에는 권장하지 않습니다. 

  21. Smart Connections v4.5.0 Release. 2026년 5월 5일. footer connections가 Core 기능이 되었습니다. 최근 v4 릴리스에는 connection list의 graph views, 설정 가능한 connection-panel 위치, 개선된 block-embedding recovery, Substrate cross-plugin state, transformer fallback 수정, 중복 connection 계산 감소도 포함됩니다. 

  22. obsidianmd/obsidian-clipper releases — Web Clipper 버전별 기능 매핑의 주요 출처입니다. 2026년 4월 주기: 1.4.0 (4월 9일, YouTube transcript UI + Open in Reader 기본값), 1.5.0 (4월 15일, Highlights viewer + Reader fade-in), 1.5.1 (4월 15일, webpack compilation 수정), 1.6.0 (4월 21일, Highlighter UX + LinkedIn/Threads/Bluesky/Discourse/Medium extractors가 포함된 Defuddle 0.18), 1.6.1 (4월 22일, Reader outline 수정 + highlights search), 1.6.2 (4월 23일, Safari embedded-mode clipboard 수정). Mozilla Add-ons storeChrome Web Store에도 함께 등재되어 있습니다. 

  23. sqlite-vec v0.1.8, sqlite-vec v0.1.9, sqlite-vec v0.1.10-alpha.3. v0.1.8은 npm packaging을 수정했고, v0.1.9는 12자를 초과하는 metadata text column의 DELETE 버그를 수정했으며, v0.1.10-alpha.3은 적절한 INSERT OR REPLACE INTO 지원을 추가하지만 prerelease입니다. 

VAULT obsidian.md INDEXED