obsidian:~/vault$ search --hybrid obsidian

예제 보관소 위치

# AI 인프라로서의 Obsidian: 최종 기술 레퍼런스

words: 10506 read_time: 53m updated: 2026-04-16 21:32
$ retriever search --hybrid obsidian

핵심 요약

노트 작성이 아닌 컨텍스트 엔지니어링입니다. AI를 위한 Obsidian 볼트의 가치는 노트 자체가 아니라 노트를 쿼리할 수 있게 만드는 검색 레이어에 있습니다. 검색 기능이 없는 16,000개 파일의 볼트는 쓰기 전용 데이터베이스에 불과합니다. 반면 hybrid 검색과 MCP 통합을 갖춘 200개 파일의 볼트는 AI 지식 기반이 됩니다. 검색 인프라가 곧 제품이고, 노트는 원재료입니다.

Hybrid 검색이 순수 키워드 검색이나 순수 시맨틱 검색보다 우수합니다. BM25는 정확한 식별자와 함수 이름을 잡아냅니다. 벡터 검색은 서로 다른 용어 사이의 동의어와 개념적 매칭을 잡아냅니다. Reciprocal Rank Fusion (RRF)은 점수 보정 없이 두 결과를 병합합니다. 어느 한 방법만으로는 두 가지 실패 모드를 모두 커버할 수 없습니다. MS MARCO 패시지 랭킹 연구에서도 이 패턴이 확인됩니다: hybrid 검색은 단독 방법보다 일관되게 높은 성능을 보입니다.3 hybrid 검색기 심층 분석에서 RRF 수학, 실제 수치를 사용한 예제, 실패 모드 분석, 인터랙티브 융합 계산기를 다루고 있습니다.

MCP로 AI 도구가 볼트에 직접 접근할 수 있습니다. Model Context Protocol (MCP) 서버는 검색기를 Claude Code, Codex CLI, Cursor 및 기타 AI 도구가 직접 호출할 수 있는 도구로 노출합니다. 에이전트가 볼트를 쿼리하면 출처가 표시된 랭킹 결과를 받고, 전체 파일을 로드하지 않고도 컨텍스트를 활용합니다. MCP 서버는 검색 엔진을 감싸는 얇은 래퍼입니다.

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

증분 인덱싱으로 시스템을 10초 이내에 최신 상태로 유지합니다. 파일 수정 시간 비교로 변경을 감지합니다. 수정된 파일만 다시 청킹하고 재임베딩합니다. 전체 재인덱싱은 Apple M 시리즈 하드웨어에서 약 4분이 소요됩니다. 일반적인 하루 편집량의 증분 업데이트는 10초 이내에 완료됩니다. 수동 개입 없이 시스템이 항상 최신 상태를 유지합니다.

이 아키텍처는 200개에서 20,000개 이상의 노트까지 확장됩니다. 동일한 3계층 설계(수집, 검색, 통합)가 어떤 볼트 크기에서든 작동합니다. 소규모 볼트에서는 BM25 전용 검색으로 시작하세요. 키워드 충돌이 문제가 되면 벡터 검색을 추가하세요. 정확 매칭과 시맨틱 매칭이 모두 필요하면 RRF 융합을 추가하세요. 각 레이어는 독립적으로 유용하고 독립적으로 제거할 수 있습니다.


이 가이드 사용법

이 가이드는 전체 시스템을 다룹니다. 현재 상황에 따라 시작점을 선택하세요:

현재 상황 시작 지점 이후 탐색
Obsidian + AI 입문자 AI 인프라로서의 Obsidian, 빠른 시작 볼트 아키텍처, MCP 서버 아키텍처
기존 볼트에 AI 접근 추가 MCP 서버 아키텍처, Claude Code 통합 임베딩 모델, 전문 검색
검색 시스템 구축 완전한 검색 파이프라인, Reciprocal Rank Fusion 성능 튜닝, 트러블슈팅
팀 또는 엔터프라이즈 환경 의사결정 프레임워크, 지식 그래프 패턴 개발자 워크플로 레시피, 마이그레이션 가이드

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


AI 인프라로서의 Obsidian

이 가이드의 핵심 주장: Obsidian 볼트는 로컬 우선, 플레인텍스트, 그래프 구조이며 사용자가 스택의 모든 레이어를 제어하기 때문에 개인 AI 지식 기반을 위한 최고의 기반입니다.

Obsidian이 AI에 제공하는 다른 도구에 없는 장점

플레인텍스트 마크다운 파일. 모든 노트는 파일 시스템의 .md 파일입니다. 독점 포맷도, 데이터베이스 내보내기도, 콘텐츠를 읽기 위한 API도 필요 없습니다. 파일을 읽을 수 있는 모든 도구가 볼트를 읽을 수 있습니다. grep, ripgrep, Python의 pathlib, SQLite FTS5 — 모두 소스 파일에서 직접 동작합니다. 검색 시스템을 구축할 때 인덱싱하는 것은 API 응답이 아니라 파일입니다. 소스가 파일 시스템이기 때문에 인덱스는 항상 소스와 일치합니다.

로컬 우선 아키텍처. 볼트는 사용자의 기기에 있습니다. 서버도, 클라우드 동기화 의존성도, API 속도 제한도, 자체 콘텐츠 처리 방식을 규정하는 서비스 약관도 없습니다. 외부 서비스 없이 노트를 임베딩하고, 인덱싱하고, 청킹하고, 검색할 수 있습니다. AI 인프라에서 이것이 중요한 이유는 검색 파이프라인이 API 엔드포인트 응답 속도가 아닌 디스크 속도만큼 빠르게 실행되기 때문입니다. 프라이버시 측면에서도 중요합니다: 자격 증명, 건강 데이터, 재무 정보, 개인적인 메모가 포함된 노트가 절대 기기를 떠나지 않습니다.

Wiki-link를 통한 그래프 구조. Obsidian의 [[wiki-link]] 구문은 노트 간 방향 그래프를 생성합니다. OAuth 구현에 대한 노트는 토큰 갱신, 세션 관리, API 보안에 대한 노트로 연결됩니다. 그래프 구조는 개념 간 인간이 큐레이션한 관계를 인코딩합니다. 벡터 임베딩은 의미적 유사성을 포착하지만, wiki-link는 저자가 주제에 대해 사고하면서 만든 의도적 연결을 포착합니다. 이 그래프는 임베딩이 복제할 수 없는 신호입니다.

플러그인 생태계. Obsidian은 2,500개 이상의 커뮤니티 플러그인을 보유하고 있습니다(2026년 3월 기준, 2025년 중반 1,800개 이상에서 증가). Dataview는 볼트를 데이터베이스처럼 쿼리합니다. Templater는 JavaScript 로직이 포함된 템플릿으로 노트를 생성합니다. Git 통합은 볼트를 저장소에 동기화합니다. Linter는 포맷 일관성을 적용합니다. Bases 코어 플러그인(v1.9.10에서 도입)은 frontmatter 속성을 필드로 사용하여 볼트 파일 위에 데이터베이스형 뷰 — 테이블, 갤러리, 캘린더, 칸반 보드 — 를 추가하며, .base 파일로 저장됩니다.27 이러한 플러그인은 기본 플레인텍스트 포맷을 변경하지 않고 볼트에 구조를 추가합니다. 검색 시스템은 플러그인 자체가 아니라 이러한 플러그인의 출력을 인덱싱합니다.

500만 명 이상의 사용자. Obsidian은 템플릿, 워크플로, 플러그인, 문서를 제작하는 대규모 활성 커뮤니티를 보유하고 있습니다. 볼트 구성이나 플러그인 설정에서 문제가 발생하면 누군가 이미 해결책을 문서화했을 가능성이 높습니다. 커뮤니티는 Obsidian 인접 도구도 제작합니다: MCP 서버, 인덱싱 스크립트, 퍼블리싱 파이프라인, API 래퍼 등이 있습니다.

파일 시스템만으로는 얻을 수 없는 것

마크다운 파일 디렉토리는 플레인텍스트의 장점이 있지만 Obsidian이 추가하는 세 가지가 부족합니다:

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

  2. 플러그인 렌더링을 포함한 실시간 미리보기. Dataview 쿼리, Mermaid 다이어그램, 콜아웃 블록이 실시간으로 렌더링됩니다. 작성 환경은 텍스트 에디터보다 풍부하면서도 저장 포맷은 플레인텍스트를 유지합니다. 풍부한 환경에서 작성하고 구성하고, 검색 시스템은 원본 마크다운을 인덱싱합니다.

  3. 커뮤니티 인프라. 플러그인 검색, 테마 마켓플레이스, 동기화 서비스(선택), 퍼블리시 서비스(선택), 문서 생태계가 있습니다. 개별 기능은 독립 도구로 복제할 수 있지만, Obsidian은 이를 하나의 일관된 워크플로로 패키징합니다.

Obsidian이 하지 않는 것(직접 구축해야 하는 것)

Obsidian은 검색 인프라를 포함하지 않습니다. 기본 검색(전문 검색, 파일명, 태그)은 있지만 임베딩 파이프라인, 벡터 검색, 융합 랭킹, MCP 서버, 자격 증명 필터링, 청킹 전략, 외부 AI 도구를 위한 통합 훅은 없습니다. 이 가이드는 Obsidian 위에 구축하는 인프라를 다룹니다. 볼트는 기반이고, 검색 파이프라인, MCP 서버, 통합 훅이 인프라입니다.

여기서 설명하는 아키텍처는 마크다운 중심이며, Obsidian 전용이 아닙니다. Logseq, Foam, Dendron 또는 일반 마크다운 파일 디렉토리를 사용하더라도 검색 파이프라인은 동일하게 작동합니다. 청커는 .md 파일을 읽고, 임베더는 텍스트 문자열을 처리하고, 인덱서는 SQLite에 기록합니다. 이 구성 요소 중 어느 것도 Obsidian 고유 기능에 의존하지 않습니다. Obsidian이 기여하는 것은 검색기가 인덱싱하는 마크다운 파일을 생산하는 작성 및 구성 환경입니다.


빠른 시작: 첫 번째 AI 연결 볼트

이 섹션에서는 5분 안에 볼트를 AI 도구에 연결하는 방법을 안내해요. Obsidian을 설치하고, 볼트를 생성하고, MCP 서버를 설치한 다음, 첫 번째 쿼리를 실행하게 됩니다. 빠른 시작에서는 즉각적인 결과를 위해 커뮤니티 MCP 서버를 사용해요. 이후 섹션에서는 프로덕션용 맞춤 검색 파이프라인 구축을 다룹니다.

사전 요구 사항

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

1단계: 볼트 생성

obsidian.md에서 Obsidian을 다운로드하고 새 볼트를 생성하세요. 기억하기 쉬운 위치를 선택하세요 — MCP 서버에 절대 경로가 필요합니다.

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

검색기에 작업할 데이터를 제공하기 위해 몇 개의 노트를 추가하세요. 10~20개 정도의 노트만으로도 결과를 확인할 수 있어요. 각 노트는 의미 있는 제목과 최소 한 단락의 내용이 포함된 .md 파일이어야 합니다.

2단계: MCP 서버 설치

여러 커뮤니티 MCP 서버가 즉각적인 볼트 접근을 제공해요. 2025~2026년을 거치며 생태계가 크게 성장했습니다. 최근 주목할 만한 업데이트로는 MCPVault v0.11.0(2026년 3월)이 있는데, frontmatter와 해시태그를 카운트와 함께 스캔하는 list_all_tags 기능이 추가되었고, 점으로 시작하는 폴더 처리가 개선되었으며, .base.canvas 파일 지원이 추가되었어요.25 패키지 이름도 npm에서 @bitbonsai/mcpvault로 변경되었습니다.

서버 작성자 전송 방식 플러그인 필요 여부 주요 기능
obsidian-mcp-server StevenStavrakis STDIO 아니요 경량, 파일 기반
mcp-obsidian MarkusPfundstein STDIO 로컬 REST API REST를 통한 전체 볼트 CRUD
obsidian-mcp-tools jacksteamdev STDIO 예 (플러그인) 의미 검색 + Templater
obsidian-claude-code-mcp iansinnott WebSocket 예 (플러그인) Claude Code용 자동 탐색
obsidian-mcp-server cyanheads STDIO 로컬 REST API 태그, frontmatter 관리

빠른 시작에서 가장 간단한 옵션은 .md 파일을 직접 읽는 파일 기반 서버예요:

npm install -g obsidian-mcp-server

3단계: AI 도구 설정

Claude Code~/.claude/settings.json에 추가하세요:

{
  "mcpServers": {
    "obsidian": {
      "command": "obsidian-mcp-server",
      "args": ["--vault", "/absolute/path/to/your/vault"]
    }
  }
}

Codex CLI.codex/config.toml에 추가하세요:

[mcp_servers.obsidian]
command = "obsidian-mcp-server"
args = ["--vault", "/absolute/path/to/your/vault"]

Cursor.cursor/mcp.json에 추가하세요:

{
  "mcpServers": {
    "obsidian": {
      "command": "obsidian-mcp-server",
      "args": ["--vault", "/absolute/path/to/your/vault"]
    }
  }
}

4단계: 첫 번째 쿼리 실행

AI 도구를 열고 볼트 노트가 답할 수 있는 질문을 해보세요:

Search my Obsidian vault for notes about [topic you wrote about]

AI 도구가 MCP 서버를 호출하고, 서버가 볼트를 검색하여 일치하는 콘텐츠를 반환해요. 파일 경로와 관련 발췌문이 포함된 결과를 확인할 수 있습니다.

방금 구축한 것

표준 프로토콜을 통해 로컬 지식 베이스를 AI 도구에 연결했어요. MCP 서버가 볼트 파일을 읽고, 기본 검색을 수행하고, 결과를 반환합니다. 이것이 최소 기능 버전이에요.

이 빠른 시작에서 제공하지 않는 것: - Hybrid 검색 (BM25 + 벡터 검색 + RRF fusion) - 임베딩(embedding) 기반 의미 검색 - 자격 증명 필터링 - 증분 인덱싱 - 훅 기반 자동 컨텍스트 주입

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


AI 워크플로를 위한 Obsidian CLI

Obsidian 1.12(2026년 2월)에서 AI 워크플로를 위한 새로운 통합 표면을 여는 내장 명령줄 인터페이스가 도입되었어요.28 CLI는 Obsidian GUI의 리모컨 역할을 합니다 — Obsidian이 실행 중이어야 하며(첫 번째 명령 시 자동으로 실행됨), 설정 > 일반 > 명령줄 인터페이스에서 활성화할 수 있어요.

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

CLI는 이전에 GUI나 플러그인 API를 통해서만 가능했던 Obsidian 네이티브 작업에 대한 프로그래밍 방식의 접근을 제공해요. AI 워크플로에서 주요 기능은 다음과 같습니다:

  • 스크립트와 훅에서의 검색. obsidian search "query"obsidian search:context "query"는 모든 셸 스크립트, 훅, 또는 자동화 파이프라인에서 볼트 검색을 실행해요. search:context 변형은 주변 컨텍스트와 함께 일치하는 줄을 반환하므로 AI 프롬프트에 결과를 전달할 때 유용합니다.
  • 데일리 노트 자동화. obsidian daily는 오늘의 데일리 노트를 열거나 생성해요. 셸 스크립팅과 결합하면 자동화된 일일 브리핑 워크플로가 가능해집니다 — 훅이 AI가 생성한 요약을 데일리 노트에 추가할 수 있어요.
  • 템플릿 기반 노트 생성. obsidian template listobsidian template create는 Templater 또는 코어 템플릿에서 노트를 생성하여, AI 에이전트가 마크다운 파일을 직접 작성하지 않고도 구조화된 볼트 항목을 만들 수 있게 해요.
  • 속성 관리. obsidian property setobsidian property get은 frontmatter 속성을 읽고 쓸 수 있어서, YAML를 파싱하지 않고도 스크립트에서 메타데이터를 업데이트할 수 있어요.
  • 플러그인 제어. obsidian plugin enable/disable/list는 플러그인을 프로그래밍 방식으로 관리하며, 배치 작업 중 인덱싱 플러그인을 토글할 때 유용해요.
  • 작업 관리. obsidian task list/add/complete는 구조화된 작업 접근을 제공하여, 볼트에서 작업 항목을 관리하는 AI 에이전트에 유용합니다.

CLI vs MCP: AI 접근 방식 비교

CLI와 MCP 서버는 서로 다른 역할을 하며, 경쟁이 아닌 보완 관계예요:

측면 Obsidian CLI MCP 서버
호출자 셸 스크립트, 훅, cron 작업 AI 에이전트 (Claude Code, Codex, Cursor)
프로토콜 POSIX 프로세스 (stdin/stdout/stderr) MCP (JSON-RPC over STDIO 또는 HTTP)
강점 Obsidian 네이티브 작업 (템플릿, 플러그인, 속성) 맞춤 검색 (임베딩, BM25, RRF fusion)
한계 벡터 검색 없음, 임베딩 파이프라인 없음 Obsidian 내부 작업에 접근 불가
최적 용도 자동화 스크립트, 수집 파이프라인, 훅 액션 세션 중 실시간 AI 에이전트 쿼리

권장 사항: CLI는 수집 자동화(노트 생성, 속성 관리, Obsidian 네이티브 검색 실행)에, MCP는 검색(임베딩을 활용한 hybrid 검색)에 사용하세요. PreToolUse 훅에서 obsidian search:context를 빠른 사전 확인으로 호출한 후, 순위가 매겨진 결과를 위해 전체 MCP 검색기로 폴백하는 방식이 효과적이에요.

예시: CLI 기반 수집 훅

#!/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 에이전트 플러그인

볼트 UI에 AI 코딩 에이전트를 직접 내장하는 Obsidian 플러그인 카테고리가 성장하고 있어요. 외부 MCP 서버 설정의 대안으로, 외부 도구에서 연결하는 대신 Obsidian의 사이드바 안에서 AI 에이전트를 실행합니다.

Claudian

Claudian은 Claude Code를 볼트 내 AI 협업 도구로 내장해요. 볼트 디렉토리가 Claude의 작업 디렉토리가 되어, 파일 읽기/쓰기, 검색, bash 명령, 다단계 워크플로 등 완전한 에이전트 기능을 제공합니다.29

AI 인프라를 위한 주요 기능: - 컨텍스트 인식 프롬프트. 포커스된 노트를 자동으로 첨부하고, @notename 파일 멘션, 태그 기반 제외, 에디터 선택 영역을 컨텍스트로 지원해요. - 비전 지원. 드래그 앤 드롭, 붙여넣기, 또는 파일 경로를 통해 이미지를 분석할 수 있어서, 볼트에 캡처된 스크린샷과 다이어그램 처리에 유용해요. - 슬래시 명령. /command로 트리거되는 재사용 가능한 프롬프트 템플릿을 만들어 표준화된 볼트 작업이 가능해요. - 권한 모드. YOLO(자동 승인), Safe(각 작업 승인), Plan(계획만) 모드와 함께 안전 차단 목록 및 볼트 격리 기능을 제공해요.

Agent Client

Agent Client는 Agent Client Protocol(ACP)을 통해 Claude Code, Codex CLI, Gemini CLI를 통합된 Obsidian 사이드바로 가져와요.30

주요 기능: - 멀티 에이전트 전환. 같은 패널에서 Claude Code, Codex, Gemini CLI와 대화하며, 필요에 따라 에이전트를 전환할 수 있어요. - 노트 멘션. @notename을 사용하여 프롬프트에 노트 내용을 포함할 수 있으며, Claudian과 유사하지만 에이전트에 구애받지 않아요. - 셸 실행. 채팅 내에서 터미널 명령을 인라인으로 실행할 수 있어서, 대화를 떠나지 않고도 빌드 스크립트, git 명령 등 모든 터미널 작업을 수행할 수 있어요. - 작업 승인. 파일 읽기, 편집, 명령 실행에 대한 세분화된 제어가 가능해요.

에이전트 플러그인 vs 외부 MCP 사용 시기

시나리오 에이전트 플러그인 외부 MCP
AI 지원으로 볼트 노트 작성 및 편집 더 적합 — 에이전트가 에디터 컨텍스트를 인식 작동하지만 에디터 인식 없음
여러 저장소에 걸친 코드 개발 제한적 — 볼트 범위로 한정 더 적합 — 전체 파일 시스템 접근 가능한 프로젝트 범위
대규모 인덱싱된 코퍼스에서의 검색 기본 검색만 가능 전체 hybrid 검색 파이프라인
노트 작성 중 빠른 볼트 Q&A 이상적 — 컨텍스트 전환 불필요 터미널로 전환 필요

권장 사항: 볼트 중심 워크플로(노트 작성, 정리, 요약)에는 에이전트 플러그인을 사용하세요. AI 에이전트가 전체 검색 파이프라인과 볼트 외부 코드베이스 접근이 필요한 개발 워크플로에는 외부 MCP 서버를 사용하세요. 두 접근 방식은 공존할 수 있어요 — 노트 작업에는 Obsidian 내에서 Claudian을 실행하고, 개발에는 MCP와 함께 외부에서 Claude Code를 사용하면 됩니다.


의사결정 프레임워크: Obsidian vs 대안 도구

모든 사용 사례에 Obsidian이 필요한 것은 아닙니다. 이 섹션에서는 Obsidian이 적합한 기반인 경우, 과한 선택인 경우, 그리고 다른 도구가 더 잘 맞는 경우를 구분해 드립니다.

의사결정 트리

START: What is your primary content type?

├─ Structured data (tables, records, schemas)
   Use a database. SQLite, PostgreSQL, or a spreadsheet.
   Obsidian is for prose, not tabular data.

├─ Ephemeral context (current project, temporary notes)
   Use CLAUDE.md / AGENTS.md in the project repo.
   These travel with the code and reset per project.

├─ Team wiki (shared documentation, onboarding)
   Evaluate Notion, Confluence, or a shared git repo.
   Obsidian vaults are personal-first. Team sync is possible
    but not native.

└─ Growing personal knowledge corpus
   
   ├─ < 50 notes
      A folder of markdown files + grep is sufficient.
      Obsidian adds value mainly through the link graph,
       which needs density to be useful.
   
   ├─ 50 - 500 notes
      Obsidian adds value. Wiki-links create a navigable graph.
      BM25-only search (FTS5) is sufficient at this scale.
      Skip vector search and RRF until keyword collisions appear.
   
   ├─ 500 - 5,000 notes
      Full hybrid retrieval becomes valuable. Keyword collisions
       increase. Semantic search catches queries that BM25 misses.
      Add vector search + RRF fusion at this scale.
   
   └─ 5,000+ notes
       Full pipeline is essential. BM25-only returns too much noise.
       Credential filtering becomes critical (more notes = more
        accidentally pasted secrets).
       Incremental indexing matters (full reindex takes minutes).
       MCP integration pays dividends on every AI interaction.

비교 매트릭스

기준 Obsidian Notion Apple Notes 일반 파일 시스템 CLAUDE.md
로컬 우선 아니오 (클라우드) 부분적 (iCloud)
일반 텍스트 예 (markdown) 아니오 (블록) 아니오 (독점 형식)
그래프 구조 예 (wiki-link) 부분적 (멘션) 아니오 아니오 아니오
AI 인덱싱 가능 직접 파일 접근 API 필요 내보내기 필요 직접 파일 접근 이미 컨텍스트에 포함
플러그인 생태계 2,500개 이상 통합 기능 없음 해당 없음 해당 없음
오프라인 사용 완전 지원 읽기 전용 캐시 부분적 완전 지원 완전 지원
10K+ 노트 확장성 예 (API 사용 시) 성능 저하 아니오 (단일 파일)
비용 무료 (코어) $10/월 이상 무료 무료 무료

Obsidian이 과한 선택인 경우

  • 단일 프로젝트 컨텍스트. AI가 현재 코드베이스에 대한 컨텍스트만 필요하다면 CLAUDE.md, AGENTS.md, 또는 프로젝트 수준의 문서에 넣으세요. 이 파일들은 저장소와 함께 이동하며 자동으로 로드됩니다.
  • 구조화된 데이터. 콘텐츠가 테이블, 레코드, 스키마라면 데이터베이스를 사용하세요. Obsidian 노트는 산문 중심입니다. Dataview로 frontmatter 필드를 쿼리할 수는 있지만, 실제 데이터베이스가 구조화된 쿼리를 더 잘 처리합니다.
  • 임시 리서치. 프로젝트가 끝나면 버릴 노트라면 markdown 파일이 담긴 임시 디렉토리가 더 간단합니다. 일시적인 콘텐츠를 위해 검색 인프라를 구축하지 마세요.

Obsidian이 올바른 선택인 경우

  • 수개월 또는 수년에 걸쳐 지식을 축적하는 경우. 코퍼스가 성장할수록 가치가 복리로 늘어납니다. 200개의 노트가 담긴 볼트를 6개월간 매일 쿼리하는 것이 5,000개의 노트가 담긴 볼트를 한 번 쿼리하는 것보다 더 큰 가치를 제공합니다.
  • 하나의 코퍼스에 여러 도메인이 있는 경우. 프로그래밍, 아키텍처, 보안, 디자인, 개인 프로젝트에 대한 노트가 담긴 볼트는 프로젝트별 CLAUDE.md가 제공할 수 없는 교차 도메인 검색의 혜택을 받습니다.
  • 프라이버시에 민감한 콘텐츠. 로컬 우선이라는 것은 검색 파이프라인이 콘텐츠를 외부 서비스로 절대 전송하지 않는다는 의미입니다. 볼트에는 클라우드 서비스에 업로드하지 않을 콘텐츠를 포함해 무엇이든 넣을 수 있습니다.

멘탈 모델: 세 가지 레이어

이 시스템은 독립적으로 작동하지만 결합될 때 시너지를 내는 세 가지 레이어로 구성됩니다. 각 레이어는 서로 다른 관심사와 서로 다른 실패 모드를 가지고 있습니다.

┌─────────────────────────────────────────────────────┐
                 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)는 볼트에 무엇이 들어오는지를 결정합니다. 큐레이션 없이는 볼트에 노이즈가 쌓입니다: 트윗 스크린샷, 주석 없이 복사-붙여넣기한 아티클, 맥락 없는 반쪽짜리 생각들. 인테이크 레이어는 진입 시점에서 품질 관리를 담당합니다. 점수 매기기 파이프라인, 태깅 규칙, 수동 검토 프로세스 등 — 볼트에 검색할 가치가 있는 콘텐츠만 포함되도록 보장하는 모든 메커니즘이 여기에 해당합니다.

검색(Retrieval)은 볼트를 쿼리 가능하게 만듭니다. 이것이 엔진입니다: 노트를 검색 단위로 청킹(chunking)하고, 청크를 벡터 공간에 임베딩(embedding)하고, 키워드와 시맨틱 검색을 위해 인덱싱하고, RRF로 결과를 융합합니다. 검색 레이어는 파일 디렉토리를 쿼리 가능한 지식 베이스로 변환합니다. 이 레이어 없이는 볼트를 수동 탐색과 기본 검색으로만 탐색할 수 있을 뿐, AI 도구가 프로그래밍 방식으로 접근할 수 없습니다.

통합(Integration)은 검색 레이어를 AI 도구에 연결합니다. MCP 서버는 검색을 호출 가능한 도구로 노출합니다. 훅(hook)은 컨텍스트를 자동으로 주입합니다. 스킬(skill)은 새로운 지식을 볼트에 다시 캡처합니다. 통합 레이어는 지식 베이스와 이를 소비하는 AI 에이전트 사이의 인터페이스입니다.

각 레이어는 의도적으로 분리되어 있습니다. 인테이크 점수 매기기 파이프라인은 임베딩에 대해 아무것도 모릅니다. 검색기는 시그널 라우팅 규칙에 대해 아무것도 모릅니다. MCP 서버는 노트가 어떻게 생성되었는지 아무것도 모릅니다. 이러한 분리 덕분에 각 레이어를 독립적으로 개선할 수 있습니다. 인테이크 파이프라인을 변경하지 않고도 임베딩 모델을 교체할 수 있습니다. 검색기를 수정하지 않고도 새로운 MCP 기능을 추가할 수 있습니다. 인덱스를 건드리지 않고도 시그널 점수 매기기 휴리스틱을 변경할 수 있습니다.


AI 활용을 위한 Vault 아키텍처

AI 검색에 최적화된 vault는 개인 브라우징에 최적화된 vault와 다른 규칙을 따릅니다. 이 섹션에서는 폴더 구조, 노트 스키마, frontmatter 규칙, 그리고 검색 품질을 향상시키는 구체적인 패턴을 다룹니다.

폴더 구조

최상위 폴더에 번호 접두사를 사용하여 예측 가능한 조직 계층을 만드세요. 번호는 우선순위를 의미하지 않으며, 관련 도메인을 그룹화하고 구조를 한눈에 파악할 수 있게 해줍니다.

vault/
├── 00-inbox/              # Unsorted captures, pending triage
├── 01-projects/           # Active project notes
├── 02-areas/              # Ongoing areas of responsibility
├── 03-resources/          # Reference material by topic
   ├── programming/
   ├── security/
   ├── ai-engineering/
   ├── design/
   └── devops/
├── 04-archive/            # Completed projects, old references
├── 05-signals/            # Scored signal intake
   ├── ai-tooling/
   ├── security/
   ├── systems/
   └── ...12 domain folders
├── 06-daily/              # Daily notes (if used)
├── 07-templates/          # Note templates (excluded from index)
├── 08-attachments/        # Images, PDFs (excluded from index)
├── .obsidian/             # Obsidian config (excluded from index)
└── .indexignore            # Paths to exclude from retrieval index

인덱싱해야 하는 폴더: 마크다운 텍스트가 포함된 모든 폴더 — 프로젝트, 영역, 리소스, 시그널, 데일리 노트.

인덱싱에서 제외해야 하는 폴더: 템플릿(콘텐츠가 아닌 플레이스홀더 변수 포함), 첨부 파일(바이너리 파일), Obsidian 설정, 그리고 검색 인덱스에 포함시키고 싶지 않은 민감한 콘텐츠가 들어 있는 모든 폴더.

.indexignore 파일

vault 루트에 .indexignore 파일을 만들어 검색 인덱스에서 제외할 경로를 명시적으로 지정하세요. 구문은 .gitignore와 동일합니다:

# Obsidian internal
.obsidian/

# Templates contain placeholders, not content
07-templates/

# Binary attachments
08-attachments/

# Personal health/medical notes
02-areas/health/

# Financial records
02-areas/finance/personal/

# Career documents (resumes, salary data)
02-areas/career/private/

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

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

  • domain — 도메인 범위 쿼리 활성화(“보안 노트만 검색”)
  • source — 캡처된 콘텐츠의 출처 표기. 검색기가 결과에 소스 URL을 포함할 수 있습니다
  • status — 활성 검색에서 보관 또는 초안 노트를 제외할 수 있습니다

청킹 규칙

검색기는 H2(##) 제목 경계에서 청킹합니다. 즉, 노트 구조가 검색 세분화에 직접 영향을 미칩니다:

검색에 좋은 구조:

## Token Rotation Strategy

The rotation interval depends on the threat model...

## Implementation with refresh_token

The OAuth 2.0 refresh token flow requires...

## Error Handling: Expired Tokens

When a token expires mid-request...

세 개의 H2 섹션은 세 개의 독립적으로 검색 가능한 청크를 생성합니다. 각 청크에는 임베딩이 의미를 포착하기에 충분한 컨텍스트가 있습니다. “만료된 토큰 처리”에 대한 쿼리는 세 번째 청크에 구체적으로 매칭됩니다.

검색에 좋지 않은 구조:

# 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 제목이 없는 하나의 긴 섹션은 하나의 큰 청크를 생성합니다. 임베딩은 섹션 내 모든 주제에 걸쳐 평균화됩니다. 어떤 하위 주제에 대한 쿼리든 전체 노트에 동일하게 매칭됩니다.

경험 법칙: 한 섹션이 하나 이상의 개념을 다루고 있다면, H2 하위 섹션으로 나누세요. 나머지는 청커가 처리합니다.

노트에 넣지 말아야 할 것

검색 품질을 저하시키는 콘텐츠:

  • 주석 없이 전체 기사를 그대로 복사-붙여넣기. 검색기가 원본 기사의 키워드를 인덱싱하여, 직접 작성하지 않은 콘텐츠로 vault가 희석됩니다. 대신 요약을 추가하거나, 핵심 포인트를 추출하거나, 소스 URL로 링크하세요.
  • 텍스트 설명 없는 스크린샷. 검색기는 마크다운 텍스트를 인덱싱합니다. alt 텍스트나 주변 설명이 없는 이미지는 BM25와 벡터 검색 모두에서 보이지 않습니다.
  • 자격 증명 문자열. API 키, 토큰, 비밀번호, 연결 문자열. 자격 증명 필터링이 있더라도 가장 안전한 방법은 노트에 시크릿을 절대 붙여넣지 않는 것입니다. 대신 이름으로 참조하세요(“~/.env에 있는 Cloudflare API 토큰”).
  • 큐레이션 없는 자동 생성 콘텐츠. 도구가 노트를 생성하는 경우(회의 녹취록, Readwise 하이라이트, RSS 가져오기), 영구 vault에 들어가기 전에 검토하고 주석을 달아야 합니다. 큐레이션되지 않은 자동 가져오기는 검색 가능한 가치 없이 볼륨만 추가합니다.

AI 워크플로를 위한 플러그인 생태계

AI 검색을 위한 vault 품질을 향상시키는 Obsidian 플러그인은 세 가지 범주로 나뉩니다: 구조적(일관성 강화), 쿼리(메타데이터 노출), 동기화(vault를 최신 상태로 유지).

필수 플러그인

Dataview. frontmatter 필드를 사용하여 vault를 데이터베이스처럼 쿼리합니다. 동적 인덱스를 생성할 수 있습니다: “최근 30일 내에 업데이트된 security 태그가 있는 모든 노트” 또는 “active 상태인 모든 프로젝트 노트.” Dataview가 검색을 직접적으로 돕지는 않지만, vault 커버리지의 공백을 식별하고 업데이트가 필요한 노트를 찾는 데 도움이 됩니다.

TABLE type, domain, updated
FROM "03-resources"
WHERE status = "active"
SORT updated DESC
LIMIT 20

Templater. 동적 필드가 있는 템플릿으로 노트를 생성합니다. created, type, domain 필드를 미리 채우는 템플릿을 사용하여 모든 새 노트가 올바른 frontmatter로 시작하도록 보장합니다. 일관된 frontmatter는 검색 필터링을 향상시킵니다.

<%* /* New Resource Note Template */ %>
---
title: "<% tp.file.cursor() %>"
type: note
domain: <% tp.system.suggester(["programming", "security", "ai-engineering", "design", "devops"], ["programming", "security", "ai-engineering", "design", "devops"]) %>
tags: []
created: <% tp.date.now("YYYY-MM-DD") %>
updated: <% tp.date.now("YYYY-MM-DD") %>
source: ""
status: active
---

## Key Points

## Details

## 참고 자료

Linter. 볼트 전체에 서식 규칙을 적용해요. 일관된 제목 계층 구조(H1은 제목, H2는 섹션, H3는 하위 섹션)를 유지하면 청커가 예측 가능한 결과를 만들어요. 검색에 영향을 미치는 Linter 규칙은 다음과 같아요:

  • 제목 단계 증가: 순차적 제목 레벨 적용(H1에서 H3으로 건너뛰지 않기)
  • YAML 제목: 파일 이름과 일치시키기
  • 후행 공백: 제거(FTS5 토큰화 아티팩트 방지)
  • 연속 빈 줄: 1줄로 제한(더 깔끔한 청크 생성)

Git 연동. 볼트의 버전 관리를 담당해요. 시간에 따른 변경 사항을 추적하고, 기기 간 동기화하며, 실수로 삭제한 파일을 복구할 수 있어요. Git은 인덱서가 증분 변경 감지에 사용하는 mtime 데이터도 제공해요.

인덱싱에 도움이 되는 플러그인

Smart Connections. Obsidian 내에서 AI 기반 시맨틱 검색을 제공하는 Obsidian 플러그인이에요. Smart Connections v4는 기본적으로 로컬 임베딩(embeddings)을 생성하므로, 볼트가 인덱싱되면 시맨틱 연결과 검색이 API 호출 없이 완전히 오프라인으로 작동해요.23 이 가이드의 검색 시스템은 Obsidian 외부에서 실행되지만(Python 파이프라인으로), Smart Connections는 글을 쓰면서 시맨틱 관계를 탐색하는 데 유용해요. 두 시스템은 같은 콘텐츠를 인덱싱하지만 용도가 달라요: Smart Connections는 에디터 내 탐색용이고, 외부 검색기는 MCP를 통한 AI 도구 통합용이에요.

Metadata Menu. 필드 값 자동완성과 함께 구조화된 frontmatter 편집을 제공해요. type, domain, tags 필드의 오타를 줄여줘요. 일관된 메타데이터는 검색 필터링 정확도를 높여요.

인덱싱에 해로운 플러그인

Excalidraw. 마크다운 파일에 JSON로 드로잉을 저장해요. JSON는 구문상 유효한 마크다운이지만 청킹(chunking)과 임베딩 시 의미 없는 결과를 만들어요. .indexignore를 통해 또는 파일 확장자로 필터링하여 Excalidraw 파일을 인덱스에서 제외하세요.

Kanban. 보드 상태를 특수 형식의 마크다운으로 저장해요. 이 형식은 Kanban 렌더링용이지 산문 검색용이 아니에요. 청커가 카드 제목과 메타데이터의 단편을 생성하는데, 임베딩 품질이 좋지 않아요. Kanban 보드를 인덱스에서 제외하세요.

Calendar. 최소한의 콘텐츠(종종 날짜 헤더만)로 일간 노트를 생성해요. 비어 있거나 거의 빈 노트는 저품질 청크를 만들어요. 일간 노트를 사용한다면 실질적인 내용을 작성하거나, 일간 노트 폴더를 인덱스에서 제외하세요.

중요한 플러그인 설정

파일 복구 → 활성화. 실수로 노트를 삭제하는 것을 방지해요. 검색과 직접적인 관련은 없지만, 의존하는 지식 베이스에는 필수적이에요.

엄격한 줄 바꿈 → 비활성화. 마크다운 표준 줄 바꿈(단락 구분은 이중 줄 바꿈)이 Obsidian의 엄격 모드(단일 줄 바꿈으로 <br>)보다 더 깔끔한 청크를 만들어요.

새 파일 기본 위치 → 지정된 폴더. 새 파일을 00-inbox/로 라우팅하면 미분류 노트가 도메인 폴더를 오염시키지 않아요. 인박스는 준비 영역이며, 정리 후 파일을 도메인 폴더로 이동해요.

Wiki-link 형식 → 가능한 가장 짧은 경로. 짧은 링크 대상은 검색기가 링크 구조를 인덱싱할 때 해석하기 더 쉬워요.


Embedding 모델: 선택과 설정

Embedding 모델은 텍스트 청크를 시맨틱 검색용 수치 벡터로 변환해요. 모델 선택에 따라 검색 품질, 인덱스 크기, embedding 속도, 런타임 의존성이 결정돼요. 이 섹션에서는 Model2Vec의 potion-base-8M이 기본 선택인 이유와 대안을 선택해야 하는 시점을 설명해요.

Model2Vec potion-base-8M을 선택하는 이유

모델: minishlab/potion-base-8M 파라미터: 760만 차원: 256 크기: ~30 MB 의존성: model2vec (numpy만 필요, PyTorch 불필요) 추론: CPU 전용, 정적 워드 embedding (어텐션 레이어 없음)

Model2Vec은 sentence transformer의 지식을 정적 토큰 embedding으로 증류해요. BERT, MiniLM 등 다른 transformer 모델처럼 입력에 어텐션 레이어를 실행하는 대신, Model2Vec은 사전 계산된 토큰 embedding의 가중 평균을 통해 벡터를 생성해요.5 실질적인 결과는 이래요: 순차 연산이 없기 때문에 embedding 속도가 transformer 기반 모델보다 50~500배 빨라요.

MTEB 벤치마크 스위트에서 potion-base-8M은 all-MiniLM-L6-v2 성능의 89%를 달성해요 (평균 50.03 vs 56.09).6 11%의 품질 차이는 속도와 단순성의 이점에 대한 트레이드오프예요. 짧은 마크다운 청크(일반적인 vault에서 평균 200~400단어)의 경우, 두 모델 모두 짧고 집중된 텍스트에서 유사한 표현으로 수렴하기 때문에 긴 문서보다 품질 차이가 덜 두드러져요.

설정

# embedder.py
DEFAULT_MODEL = "minishlab/potion-base-8M"
EMBEDDING_DIM = 256

class Model2VecEmbedder:
    def __init__(self, model_name=DEFAULT_MODEL):
        self._model_name = model_name
        self._model = None

    def _ensure_model(self):
        if self._model is not None:
            return
        _activate_venv()  # Add isolated venv to sys.path
        from model2vec import StaticModel
        self._model = StaticModel.from_pretrained(self._model_name)

    def embed_batch(self, texts):
        self._ensure_model()
        vecs = self._model.encode(texts)
        return [v.tolist() for v in vecs]

지연 로딩. 모델은 임포트 시점이 아닌 첫 사용 시에 로드돼요. retriever가 BM25 전용 폴백 모드로 동작할 때(예: embedding venv가 설치되지 않은 경우) embedder 모듈을 임포트하는 비용은 전혀 없어요.

격리된 가상 환경. 모델은 나머지 도구 체인과의 의존성 충돌을 피하기 위해 전용 venv(예: ~/.claude/venvs/memory/)에서 실행돼요. _activate_venv() 함수는 런타임에 venv의 site-packagessys.path에 추가해요.

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

배치 처리. embedder는 Model2Vec의 오버헤드를 분산시키기 위해 64개 단위 배치로 텍스트를 처리해요. 인덱서는 한 번에 하나의 청크를 embedding하는 대신 embed_batch()에 청크를 전달해요.

대안을 선택해야 하는 경우

모델 차원 크기 속도 품질 (MTEB) 최적 용도
potion-base-8M 256 30 MB 500x 50.03 기본: 로컬, 빠름, GPU 불필요
potion-base-32M 256 120 MB 400x 52.46 더 높은 품질, 여전히 정적
potion-retrieval-32M 256 120 MB 400x 36.35 (retrieval) 검색 최적화 정적 모델
potion-multilingual-128M 256 ~500 MB 300x 다국어 vault (101개 언어)
all-MiniLM-L6-v2 384 80 MB 1x 56.09 더 높은 품질, 여전히 로컬
nomic-embed-text-v1.5 768 270 MB 0.5x 62.28 최고 로컬 품질
text-embedding-3-small 1536 API N/A 62.30 API 기반, 최고 품질

potion-base-32M을 선택하세요 — potion-base-8M보다 더 나은 품질을 원하면서 정적 embedding 계열을 벗어나고 싶지 않을 때요. 2025년 1월에 출시된 이 모델은 baai/bge-base-en-v1.5에서 증류된 더 큰 어휘를 사용하며, MTEB 평균 52.46을 달성해요 (potion-base-8M 대비 5% 향상). 동일한 256차원 출력과 numpy 전용 의존성을 유지해요.20 4배 더 큰 모델 파일로 메모리 사용량이 증가하지만, embedding 속도는 transformer 모델보다 여전히 수 자릿수 빨라요.

potion-retrieval-32M을 선택하세요 — 주요 사용 사례가 검색일 때(vault 검색이 바로 그래요). 이 변형은 검색 작업에 특화되어 potion-base-32M에서 파인튜닝되었으며, MTEB 검색 벤치마크에서 기본 모델의 33.52 대비 36.35를 기록해요.20 전체 MTEB 평균은 49.73으로 떨어지는데, 파인튜닝이 범용 성능을 검색 특화 성능과 교환하기 때문이에요.

potion-multilingual-128M을 선택하세요 — vault에 여러 언어로 된 노트가 포함되어 있을 때요. 2025년 5월에 출시된 이 101개 언어 모델은 다국어 작업에서 최고 성능의 정적 embedding 모델이며, 다른 potion 모델과 동일한 numpy 전용 의존성을 유지하면서 어떤 언어의 텍스트든 embedding을 생성해요.24 더 큰 모델 파일(~500 MB)은 교차 언어 기능에 대한 트레이드오프예요. 영어 콘텐츠와 함께 일본어, 중국어, 독일어 등 비영어 노트가 있을 때 사용하세요.

all-MiniLM-L6-v2를 선택하세요 — 검색 품질이 속도보다 중요하고 PyTorch가 설치되어 있을 때요. 384차원 벡터는 256차원 벡터 대비 SQLite 데이터베이스 크기를 약 50% 증가시켜요. embedding 속도는 M 시리즈 하드웨어에서 15,000개 파일 전체 재인덱싱 기준 1분 미만에서 약 10분으로 떨어져요.

nomic-embed-text-v1.5를 선택하세요 — 최고의 로컬 검색 품질이 필요하고 느린 인덱싱을 감수할 수 있을 때요. 768차원 벡터는 데이터베이스 크기를 대략 3배로 늘려요. PyTorch와 최신 CPU 또는 GPU가 필요해요.

text-embedding-3-small을 선택하세요 — 네트워크 지연 시간과 프라이버시가 허용 가능한 트레이드오프일 때요. API는 최고 품질의 embedding을 생성하지만 클라우드 의존성, 토큰당 비용($0.02/백만 토큰), 그리고 콘텐츠를 OpenAI 서버로 전송하는 점이 수반돼요.

그 외 모든 경우에는 potion-base-8M을 유지하세요. 속도 이점은 반복적 인덱싱(개발 중 재인덱싱)에 핵심적이고, numpy 전용 의존성은 PyTorch 설치 복잡성을 피할 수 있으며, 256차원 벡터는 데이터베이스를 컴팩트하게 유지해요.

양자화와 차원 축소

Model2Vec v0.5.0+는 정밀도와 차원이 축소된 모델 로딩을 지원해요.20 모델을 교체하지 않고도 제한된 하드웨어에 배포하거나 데이터베이스 크기를 줄이는 데 유용해요:

from model2vec import StaticModel

# Load with int8 quantization (25% of original size)
model = StaticModel.from_pretrained("minishlab/potion-base-8M", quantize=True)

# Load with reduced dimensions (e.g., 128 instead of 256)
model = StaticModel.from_pretrained("minishlab/potion-base-8M", dimensionality=128)

양자화된 모델은 메모리 사용량의 일부만으로 거의 동일한 검색 품질을 유지해요. 차원 축소는 Matryoshka 스타일 절단을 따르며, 처음 N개의 차원이 가장 많은 정보를 담고 있어요. 256에서 128차원으로 줄이면 짧은 텍스트 검색에서 최소한의 품질 저하로 벡터 저장 공간이 절반으로 줄어들어요.

2025년 5월 기준으로, Model2Vec은 WordPiece 외에도 BPE와 Unigram 토크나이저를 지원하며, 정적 모델로 증류할 수 있는 sentence transformer의 범위가 확장됐어요.22

Vault 특화 Embedding을 위한 파인튜닝

Model2Vec v0.4.0+는 정적 embedding 위에 커스텀 분류 모델 학습을 지원하고, v0.7.0에서는 어휘 양자화와 증류를 위한 설정 가능한 풀링이 추가됐어요.22 기본 potion 모델이 시맨틱 뉘앙스를 포착하지 못할 수 있는 전문 어휘(의학 노트, 법률 참조, 도메인 특화 용어)가 있는 vault에 유용해요:

from model2vec import StaticModel
from model2vec.train import train_model

# Fine-tune on vault-specific data
model = StaticModel.from_pretrained("minishlab/potion-base-8M")
trained_model = train_model(model, train_texts, train_labels)
trained_model.save_pretrained("./vault-embeddings")

대부분의 vault에서는 기본 potion-base-8M이 충분한 검색 품질을 제공해요. 파인튜닝은 범용 모델이 포착할 수 없는 도메인 특화 연결을 검색이 지속적으로 놓칠 때만 가치가 있어요.

모델 해시 추적

인덱서는 모델 이름과 어휘 크기에서 파생된 해시를 저장해요. embedding 모델을 변경하면 인덱서가 다음 증분 실행에서 불일치를 감지하고 자동으로 전체 재인덱싱을 트리거해요.

def _compute_model_hash(self):
    """Hash model name + vocab size for compatibility tracking."""
    key = f"{self._model_name}:{self._model.vocab_size}"
    return hashlib.sha256(key.encode()).hexdigest()[:16]

이렇게 하면 동일한 데이터베이스에서 서로 다른 모델의 벡터가 혼합되는 것을 방지하며, 혼합될 경우 cosine similarity 점수가 의미 없는 결과를 생성해요.

실패 모드

모델 다운로드 실패. 첫 실행 시 Hugging Face에서 모델을 다운로드해요. 다운로드가 실패하면(네트워크 문제, 기업 방화벽) retriever는 BM25 전용 모드로 폴백해요. 첫 다운로드 이후에는 모델이 로컬에 캐시돼요.

차원 불일치. 데이터베이스를 초기화하지 않고 모델을 전환하면, 저장된 벡터의 차원이 새 embedding과 달라요. 인덱서가 모델 해시를 통해 이를 감지하고 전체 재인덱싱을 트리거해요. 해시 검사가 실패하면(적절한 해시가 없는 커스텀 모델), sqlite-vec이 차원 불일치 KNN 쿼리에서 오류를 발생시켜요.

대규모 vault에서의 메모리 압박. 50,000개 이상의 청크를 단일 배치로 embedding하면 상당한 메모리를 소비할 수 있어요. 인덱서는 최대 메모리 사용량을 제한하기 위해 64개 단위 배치로 처리해요. 그래도 메모리가 부족하면 배치 크기를 줄이세요.


FTS5를 활용한 전문 검색

SQLite의 FTS5 확장은 BM25 랭킹을 지원하는 전문 검색 기능을 제공해요. FTS5는 hybrid 검색 파이프라인의 키워드 검색 구성 요소예요. 이 섹션에서는 FTS5 설정, BM25가 뛰어난 경우, 그리고 구체적인 실패 모드를 다뤄요.

FTS5 가상 테이블

CREATE VIRTUAL TABLE chunks_fts USING fts5(
    chunk_text,
    section,
    heading_context,
    content=chunks,
    content_rowid=id
);

Content-sync 모드. content=chunks 매개변수는 FTS5가 텍스트의 복사본을 별도로 저장하지 않고 chunks 테이블을 직접 참조하도록 지정해요. 이렇게 하면 저장 공간이 절반으로 줄지만, 청크가 삽입, 수정, 삭제될 때 FTS5를 수동으로 동기화해야 해요.

컬럼 구성. 세 개의 컬럼이 인덱싱돼요: - 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을 언급하기 때문에 수백 개의 노트가 일치해요. 결과는 기술적으로 정확하지만 실용적으로는 쓸모가 없어요 — 랭킹이 현재 쿼리와 관련된 “configuration” 노트를 판별할 수 없기 때문이에요.

FTS5 토크나이저

FTS5는 기본적으로 ASCII와 유니코드 텍스트를 처리하는 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배로 커져요.

유지보수

FTS5는 기본 chunks 테이블이 변경될 때 명시적인 동기화가 필요해요:

# After inserting chunks
cursor.execute("""
    INSERT INTO chunks_fts(chunks_fts)
    VALUES('rebuild')
""")

rebuild 명령은 콘텐츠 테이블에서 FTS5 인덱스를 재구성해요. 대량 삽입(전체 재인덱싱) 후에 실행하되, 개별 증분 업데이트 후에는 실행하지 마세요 — 개별 행 동기화에는 INSERT INTO chunks_fts(rowid, chunk_text, section, heading_context)를 사용하세요.


sqlite-vec를 활용한 벡터 검색

sqlite-vec 확장은 벡터 KNN(K-Nearest Neighbors) 검색을 SQLite에 도입해요. 이 섹션에서는 sqlite-vec 설정, 노트에서 검색 가능한 벡터까지의 embedding 파이프라인, 그리고 구체적인 쿼리 패턴을 다뤄요.

sqlite-vec 가상 테이블

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

vec0 모듈은 256차원 float 벡터를 패킹된 바이너리 데이터로 저장해요. id 컬럼은 chunks 테이블과 1:1로 매핑되어, 벡터 결과와 chunk 메타데이터 간의 조인을 가능하게 해요.

Embedding 파이프라인

파이프라인은 노트에서 검색 가능한 벡터까지 다음과 같이 진행돼요:

Note (.md file)
   Chunker: split at H2 boundaries
     Chunks (30-2000 chars each)
       Credential filter: scrub secrets
         Embedder: Model2Vec encode
           Vectors (256-dim float arrays)
             sqlite-vec: store as packed binary
               Ready for KNN queries

벡터 직렬화

Python의 struct 모듈은 sqlite-vec 저장을 위해 float 벡터를 직렬화해요:

import struct

def _serialize_vector(vec):
    """Pack float list into binary for sqlite-vec."""
    return struct.pack(f"{len(vec)}f", *vec)

def _deserialize_vector(blob, dim=256):
    """Unpack binary blob to float list."""
    return list(struct.unpack(f"{dim}f", blob))

KNN 쿼리

벡터 검색 쿼리는 입력 쿼리를 embedding한 다음, cosine distance 기준으로 가장 가까운 K개의 chunk를 찾아요:

def _vector_search(self, query_text, limit=30):
    query_vec = self.embedder.embed_batch([query_text])[0]
    packed = _serialize_vector(query_vec)

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

    return results

sqlite-vec의 MATCH 연산자는 근사 최근접 이웃 검색을 수행해요. k 파라미터는 반환할 결과 수를 제어하고, distance 컬럼에는 cosine distance가 담겨요 (0 = 동일, 2 = 정반대).

거리 제약 조건을 활용한 KNN 페이지네이션

sqlite-vec v0.1.7부터 KNN 쿼리가 WHERE distance < ? 제약 조건을 지원하면서, 이전 페이지를 다시 스캔하지 않고도 대규모 결과 집합에 대한 커서 기반 페이지네이션이 가능해졌어요.26

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에서 슬라이싱하던 패턴을 대체하여, 대규모 vault에서 탐색적 쿼리의 메모리 사용량을 줄여줘요.

vec0 테이블의 DELETE 지원

sqlite-vec v0.1.7에서 vec0 가상 테이블에 대한 네이티브 DELETE 지원이 추가되었어요.26 이전에는 벡터를 제거하려면 테이블을 드롭하고 다시 생성해야 했지만, 이제 인덱서의 파일 제거 경로에서 벡터를 직접 삭제할 수 있어요:

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

노트가 삭제되거나 이동될 때 증분 재인덱싱이 훨씬 간단해져요. 인덱서가 더 이상 섀도우 “활성 ID” 테이블을 유지하거나 일괄 재구축을 할 필요가 없어요.

벡터 검색이 효과적인 경우

벡터 검색은 특정 단어보다 개념이 중요한 쿼리에서 뛰어난 성능을 보여요:

  • 쿼리: “how to handle authentication failures” → “login error recovery”에 관한 노트를 찾아냄 (같은 의미 공간, 다른 키워드)
  • 쿼리: “what patterns exist for caching” → “memoization,” “Redis TTL strategies,” “HTTP cache headers”에 관한 노트를 찾아냄 (관련 개념, 다양한 용어)
  • 쿼리: “approaches to testing asynchronous code” → “pytest-asyncio fixtures,” “mock event loops,” “async test patterns”에 관한 노트를 찾아냄 (같은 개념이 구현 세부 사항으로 표현됨)

벡터 검색이 실패하는 경우

벡터 검색은 정확한 식별자 검색에서 어려움을 겪어요:

  • 쿼리: _rrf_fuse → “fusion algorithms”과 “rank merging”에 관한 노트를 반환하지만, 실제 함수 정의가 개념적 논의보다 낮은 순위에 놓일 수 있어요
  • 쿼리: PostToolUse → 특정 hook 이름이 아닌 “tool lifecycle hooks”와 “post-execution handlers”에 관한 노트를 반환해요

벡터 검색은 구조화된 데이터에서도 한계를 보여요. JSON 설정 파일, YAML 블록, 코드 스니펫은 의미적 내용이 아닌 구조적 패턴을 캡처하는 embedding을 생성해요. "review": true가 포함된 JSON 파일은 코드 리뷰에 관한 산문적 논의와는 다르게 embedding돼요.

우아한 성능 저하

sqlite-vec 로드에 실패하면 (확장 누락, 플랫폼 비호환, 라이브러리 손상 등), 검색기는 BM25 전용 검색으로 폴백해요:

class VectorIndex:
    def __init__(self, db_path):
        self.db = sqlite3.connect(db_path)
        self._vec_available = False
        try:
            self.db.enable_load_extension(True)
            self.db.load_extension("vec0")
            self._vec_available = True
        except Exception:
            pass  # BM25-only mode

    @property
    def vec_available(self):
        return self._vec_available

검색기는 벡터 쿼리를 시도하기 전에 vec_available을 확인해요. 비활성화된 경우 모든 검색은 BM25만 사용하며, RRF 융합 단계는 건너뛰게 돼요.


Reciprocal Rank Fusion (RRF)

RRF는 점수 보정 없이 두 개의 순위 목록을 병합합니다. 이 섹션에서는 알고리즘, 실제 쿼리 추적 과정, k 파라미터 튜닝, 그리고 대안 대비 RRF를 선택한 이유를 다룹니다. 편집 가능한 순위, 시나리오 프리셋, 시각적 아키텍처 탐색기가 포함된 인터랙티브 계산기는 hybrid retriever 심층 분석을 참고하세요.

알고리즘

RRF는 각 문서에 대해 각 목록에서의 순위 위치만을 기반으로 점수를 부여합니다:

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

각 변수의 의미는 다음과 같습니다: - k는 평활 상수입니다 (Cormack et al.3에 따라 60)3 - rank_i는 결과 목록 i에서 해당 문서의 1부터 시작하는 순위입니다 - weight_i는 목록별 선택적 가중치 승수입니다 (기본값 1.0)

여러 목록에서 높은 순위를 기록한 문서는 더 높은 융합 점수를 받습니다. 하나의 목록에만 등장하는 문서는 해당 단일 소스의 점수만 받습니다.

대안 대비 RRF를 선택한 이유

가중 선형 결합은 BM25 점수와 cosine similarity 거리를 보정해야 합니다. BM25 점수는 상한이 없고 코퍼스 크기에 따라 변동합니다. cosine similarity 거리는 [0, 2] 범위로 제한됩니다. 두 점수를 결합하려면 정규화가 필요하며, 정규화 파라미터는 데이터셋에 따라 달라집니다. RRF는 점수 산출 방식과 무관하게 항상 1부터 시작하는 정수인 순위 위치만 사용합니다.

학습 기반 융합 모델은 레이블이 지정된 학습 데이터, 즉 쿼리-문서 관련성 쌍이 필요합니다. 개인 지식 기반에는 이런 학습 데이터가 존재하지 않습니다. 유용한 모델을 학습시키려면 수백 개의 쿼리-문서 쌍을 직접 판단해야 합니다. RRF는 학습 데이터 없이도 작동합니다.

Condorcet 투표 방식(Borda count, Schulze method)은 이론적으로 우아하지만 구현과 튜닝이 더 복잡합니다. 원래 RRF 논문에서는 TREC 평가 데이터에서 RRF가 Condorcet 방식보다 우수한 성능을 보였음을 입증했습니다.3

실제 융합 과정

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

BM25는 review-aggregator.py를 3위에 배치합니다(“review,” “aggregator,” “disagreements”에 대한 정확한 키워드 매칭). 하지만 “review”가 더 많이 등장하는 두 개의 설정 파일이 더 높은 순위를 차지합니다. 벡터 검색은 동일한 청크를 1위에 배치합니다(충돌 해결에 대한 의미적 매칭). RRF 융합 후 결과는 다음과 같습니다:

청크 BM25 Vec 융합 점수
review-aggregator.py “Disagreement Resolution” #3 #1 0.0323
code-review-patterns.md “Multi-Reviewer” #4 #2 0.0317
deliberation-config.json “Review Weights” #1 0.0164

두 목록 모두에서 높은 순위를 기록한 청크가 상위에 올라옵니다. 하나의 목록에만 등장하는 청크는 단일 소스 점수만 받아 이중 순위 결과보다 아래로 밀립니다. 실제 분쟁 해결 로직이 최상위에 오는 이유는 두 방식 모두에서 발견되었기 때문입니다 — BM25는 키워드를 통해, 벡터 검색은 의미를 통해 찾아냈습니다.

순위별 RRF 수학 계산이 포함된 전체 단계별 추적 과정을 보고, 다양한 k 값을 시도하려면 인터랙티브 RRF 계산기를 사용하세요.

구현

RRF_K = 60

def _rrf_fuse(self, bm25_results, vec_results,
              bm25_weight=1.0, vec_weight=1.0):
    """Fuse BM25 and vector results using Reciprocal Rank Fusion."""
    scores = {}

    for rank, r in enumerate(bm25_results, start=1):
        cid = r["id"]
        if cid not in scores:
            scores[cid] = {
                "rrf_score": 0.0,
                "file_path": r["file_path"],
                "section": r["section"],
                "chunk_text": r["chunk_text"],
                "bm25_rank": None,
                "vec_rank": None,
            }
        scores[cid]["rrf_score"] += bm25_weight / (self._rrf_k + rank)
        scores[cid]["bm25_rank"] = rank

    for rank, r in enumerate(vec_results, start=1):
        cid = r["id"]
        if cid not in scores:
            scores[cid] = {
                "rrf_score": 0.0,
                "file_path": r["file_path"],
                "section": r["section"],
                "chunk_text": r["chunk_text"],
                "bm25_rank": None,
                "vec_rank": None,
            }
        scores[cid]["rrf_score"] += vec_weight / (self._rrf_k + rank)
        scores[cid]["vec_rank"] = rank

    fused = sorted(
        scores.values(),
        key=lambda x: x["rrf_score"],
        reverse=True,
    )
    return fused

k 튜닝

k 상수는 상위 순위 결과와 하위 순위 결과에 얼마나 가중치를 부여할지 제어합니다:

  • 낮은 k (예: 10): 상위 순위 결과가 지배적입니다. 1위는 1/11 = 0.091, 10위는 1/20 = 0.050(1.8배 차이)으로 점수가 매겨집니다. 개별 순위 산출기가 최상위 결과를 정확하게 반환한다고 신뢰할 수 있을 때 적합합니다.
  • 기본 k (60): 균형 잡힌 설정입니다. 1위는 1/61 = 0.0164, 10위는 1/70 = 0.0143(1.15배 차이)으로 점수가 매겨집니다. 순위 차이가 압축되어 여러 목록에 동시에 등장하는 것에 더 많은 가중치를 부여합니다.
  • 높은 k (예: 200): 순위 위치보다 양쪽 목록에 모두 등장하는 것이 훨씬 중요해집니다. 1위는 1/201, 10위는 1/210으로 거의 동일합니다. 개별 순위 산출기의 결과가 불안정하지만 목록 간 일치도가 신뢰할 만할 때 사용하세요.

k=60으로 시작하세요. 원래 RRF 논문에서 이 값이 다양한 TREC 데이터셋에서 견고한 성능을 보였습니다. 자신의 쿼리 분포에서 실패 사례를 측정한 후에만 튜닝하세요.

동점 처리

두 청크의 RRF 점수가 동일한 경우(한쪽 목록에서 같은 순위를 가지고 다른 쪽에는 등장하지 않을 때 드물게 발생), 다음 기준으로 동점을 처리합니다:

  1. 하나의 목록에만 등장하는 청크보다 양쪽 목록 모두에 등장하는 청크를 우선합니다
  2. 양쪽 목록 모두에 등장하는 청크 중에서는 합산 순위가 더 낮은 것을 우선합니다
  3. 하나의 목록에만 등장하는 청크 중에서는 해당 목록에서 더 낮은 순위를 가진 것을 우선합니다

전체 검색 파이프라인

이 섹션에서는 쿼리가 입력부터 출력까지 전체 파이프라인을 거치는 과정을 추적합니다: BM25 검색, 벡터 검색, RRF 융합, 토큰 예산 잘림, 컨텍스트 조립까지의 흐름을 다룹니다.

전체 흐름

User query: "PostToolUse hook for context compression"
  │
  ├─ BM25 Search (FTS5)
  │    → MATCH "PostToolUse hook context compression"
  │    → Top 30 results ranked by BM25 score
  │    → 12ms
  │
  ├─ Vector Search (sqlite-vec)
  │    → Embed query with Model2Vec
  │    → KNN k=30 on chunk_vecs
  │    → Top 30 results ranked by cosine distance
  │    → 8ms
  │
  └─ RRF Fusion
       → Merge 60 candidates (may overlap)
       → Score by rank position
       → Top 10 results
       → 3ms
       │
       └─ Token Budget
            → Truncate to max_tokens (default 4000)
            → Estimate at 4 chars per token
            → Return results with metadata
            → <1ms

총 지연 시간: ~23ms — Apple M3 Pro 하드웨어에서 49,746개 청크 데이터베이스 기준입니다.

검색 API

class HybridRetriever:
    def search(self, query, limit=10, max_tokens=4000,
               bm25_weight=1.0, vec_weight=1.0):
        """
        Search the vault using hybrid BM25 + vector retrieval.

        Args:
            query: Search query text
            limit: Maximum results to return
            max_tokens: Token budget for total result text
            bm25_weight: Weight for BM25 results in RRF
            vec_weight: Weight for vector results in RRF

        Returns:
            List of SearchResult with file_path, section,
            chunk_text, rrf_score, bm25_rank, vec_rank
        """
        # BM25 search
        bm25_results = self._bm25_search(query, limit=30)

        # Vector search (if available)
        if self.index.vec_available:
            vec_results = self._vector_search(query, limit=30)
            fused = self._rrf_fuse(
                bm25_results, vec_results,
                bm25_weight, vec_weight,
            )
        else:
            fused = bm25_results  # BM25-only fallback

        # Token budget truncation
        results = []
        token_count = 0
        for r in fused[:limit]:
            chunk_tokens = len(r["chunk_text"]) // 4
            if token_count + chunk_tokens > max_tokens:
                break
            results.append(r)
            token_count += chunk_tokens

        return results

토큰 예산 잘림

max_tokens 매개변수는 AI 도구가 처리할 수 있는 양보다 많은 컨텍스트를 검색기가 반환하지 않도록 방지합니다. 추정치는 토큰당 4자를 사용하며, 영어 산문에 대한 합리적인 근사값입니다. 결과는 탐욕적으로 잘립니다: 순위 순서대로 결과를 추가하다가 예산이 소진되면 중단합니다.

이는 보수적인 전략입니다. 더 정교한 접근 방식이라면 결과별 품질 점수를 고려하여 길고 품질이 낮은 결과보다 짧고 품질이 높은 결과를 우선시할 수 있습니다. 탐욕적 접근 방식은 더 단순하면서도 실제로 잘 작동하는데, RRF 순위가 이미 관련성 순으로 결과를 정렬하기 때문입니다.

데이터베이스 스키마 (전체)

-- Chunk content and metadata
CREATE TABLE chunks (
    id INTEGER PRIMARY KEY,
    file_path TEXT NOT NULL,
    section TEXT NOT NULL,
    chunk_text TEXT NOT NULL,
    heading_context TEXT DEFAULT '',
    mtime_ns INTEGER NOT NULL,
    embedded_at REAL NOT NULL
);

CREATE INDEX idx_chunks_file ON chunks(file_path);
CREATE INDEX idx_chunks_mtime ON chunks(mtime_ns);

-- FTS5 for BM25 search (content-synced to chunks table)
CREATE VIRTUAL TABLE chunks_fts USING fts5(
    chunk_text, section, heading_context,
    content=chunks, content_rowid=id
);

-- sqlite-vec for vector KNN search
CREATE VIRTUAL TABLE chunk_vecs USING vec0(
    id INTEGER PRIMARY KEY,
    embedding float[256]
);

-- Model metadata for compatibility tracking
CREATE TABLE model_meta (
    key TEXT PRIMARY KEY,
    value TEXT
);

우아한 성능 저하 경로

Full pipeline:     BM25 + Vector + RRF    Best results
No sqlite-vec:     BM25 only              Good results (no semantic)
No model download:  BM25 only              Good results (no semantic)
No FTS5:           Vector only             Decent results (no keyword)
No database:       Error                   Prompt user to run indexer

검색기는 초기화 시 사용 가능한 기능을 확인하고 쿼리 전략을 적응적으로 조정합니다. 구성 요소가 누락되면 품질은 저하되지만 오류가 발생하지는 않습니다. 유일한 하드 실패 조건은 데이터베이스 파일 자체가 없는 경우입니다.

운영 통계

16,894개 파일, 49,746개 청크, 83 MB SQLite 데이터베이스, Apple M3 Pro 환경에서 측정한 결과입니다:

지표
총 파일 수 16,894
총 청크 수 49,746
데이터베이스 크기 83 MB
BM25 쿼리 지연 시간 (p50) 12ms
벡터 쿼리 지연 시간 (p50) 8ms
RRF 융합 지연 시간 3ms
전체 검색 지연 시간 (p50) 23ms
전체 재인덱싱 시간 ~4분
증분 재인덱싱 시간 <10초
임베딩 모델 potion-base-8M (256-dim)
BM25 후보 풀 30
벡터 후보 풀 30
기본 결과 제한 10
기본 토큰 예산 4,000 토큰

콘텐츠 해싱과 변경 감지

인덱서는 마지막 인덱스 실행 이후 어떤 파일이 변경되었는지 알아야 합니다. 이 섹션에서는 변경 감지 메커니즘과 해싱 전략을 다룹니다.

파일 수정 시간 비교

인덱서는 chunks 테이블의 모든 청크에 대해 mtime_ns(나노초 단위 파일 수정 시간)를 저장합니다. 증분 실행 시 인덱서는 다음 단계를 수행합니다:

  1. 허용된 폴더 내 모든 .md 파일을 스캔합니다
  2. 파일 시스템에서 각 파일의 mtime_ns를 읽습니다
  3. 데이터베이스에 저장된 mtime_ns와 비교합니다
  4. 세 가지 범주로 분류합니다:
  5. 새 파일: 파일 시스템에는 있지만 데이터베이스에는 없는 경로
  6. 변경된 파일: 양쪽에 모두 존재하지만 mtime_ns가 다른 경우
  7. 삭제된 파일: 데이터베이스에는 있지만 파일 시스템에는 없는 경로
def get_stale_files(self, vault_mtimes):
    """Find files whose mtime changed or are new."""
    stored = dict(self.db.execute(
        "SELECT DISTINCT file_path, mtime_ns FROM chunks"
    ).fetchall())

    stale = []
    for path, mtime in vault_mtimes.items():
        if path not in stored or stored[path] != mtime:
            stale.append(path)
    return stale

def get_deleted_files(self, vault_paths):
    """Find files in database that no longer exist in vault."""
    stored_paths = set(r[0] for r in self.db.execute(
        "SELECT DISTINCT file_path FROM chunks"
    ).fetchall())
    return stored_paths - set(vault_paths)

mtime을 사용하는 이유 — 콘텐츠 해시가 아닌 이유

콘텐츠 해싱(파일 내용의 SHA-256)은 mtime 비교보다 더 신뢰할 수 있습니다 — 파일이 실제로 변경되지 않았지만 수정된 경우(예: git checkout으로 원래 mtime이 복원되는 경우)를 감지할 수 있기 때문입니다. 하지만 해싱은 증분 실행마다 모든 파일을 읽어야 합니다. 16,894개 파일의 내용을 읽는 데는 2~3초가 걸리고, 파일 시스템에서 mtime을 읽는 것은 100ms 미만입니다.

트레이드오프는 다음과 같습니다: mtime 비교는 변경되지 않은 파일에 대해 불필요한 재인덱싱을 간헐적으로 유발하지만(거짓 양성), 실제 변경 사항을 놓치는 일은 없습니다. 거짓 양성의 비용은 실행당 몇 번의 추가 임베딩 호출에 불과합니다. 속도 차이(100ms 대 3초)를 고려하면, 모든 AI 상호작용마다 실행되는 시스템에서 mtime이 실용적인 선택입니다.

삭제 처리

볼트에서 파일이 삭제되면 인덱서는 해당 파일의 모든 청크를 데이터베이스에서 제거합니다:

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 구문은 sqlite-vec v0.1.7부터 기본적으로 작동합니다.26 이전 버전에서는 우회 방법(가상 테이블을 삭제 후 재생성하거나 외부 “활성 ID” 세트를 유지하는 방식)이 필요했습니다. 0.1.7 이전 버전을 사용 중이라면 직접 삭제에 의존하기 전에 업그레이드하세요.

FTS5 콘텐츠 동기화 테이블은 제거된 각 행에 대해 INSERT INTO chunks_fts(chunks_fts, rowid, ...) VALUES('delete', ?, ...)를 통한 명시적 삭제가 필요합니다. 인덱서는 파일 제거 프로세스의 일부로 이를 처리합니다.


증분 인덱싱 vs 전체 재인덱싱

인덱서는 두 가지 모드를 지원해요: 증분(빠름, 일상적 사용)과 전체(느림, 간헐적 사용). 이 섹션에서는 각 모드의 사용 시점, 멱등성 보장, 손상 복구를 다뤄요.

증분 재인덱싱

사용 시점: 노트 편집 후 일상적인 인덱싱. 기본 모드예요.

수행 과정: 1. vault에서 파일 변경 사항 스캔 (mtime 비교) 2. 삭제된 파일의 청크 제거 3. 변경된 파일을 다시 청크 분할하고 재임베딩 4. 새 파일의 청크 삽입 5. FTS5 인덱스 동기화

일반적인 소요 시간: 16,000개 파일 vault에서 하루치 편집 기준 10초 미만.

python index_vault.py --incremental

전체 재인덱싱

사용 시점: - 임베딩 모델 변경 후 (모델 해시 불일치 감지 시) - 스키마 마이그레이션 후 (새 컬럼, 변경된 인덱스) - 데이터베이스 손상 후 (무결성 검사 실패 시) - 증분 인덱싱이 예상치 못한 결과를 낼 때

수행 과정: 1. 기존 데이터 전체 삭제 (청크, 벡터, FTS5 항목) 2. 전체 vault 스캔 3. 모든 파일 청크 분할 4. 모든 청크 임베딩 5. FTS5 인덱스 처음부터 구축

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

python index_vault.py --full

멱등성

두 모드 모두 멱등성을 보장해요: 같은 명령을 두 번 실행해도 동일한 결과가 나와요. 인덱서는 새 청크를 삽입하기 전에 해당 파일의 기존 청크를 삭제하므로, 이미 최신 상태인 데이터베이스에서 증분 인덱싱을 다시 실행하면 변경 사항이 0건이에요. 전체 인덱싱을 다시 실행하면 동일한 데이터베이스가 생성돼요.

손상 복구

SQLite 데이터베이스가 손상된 경우 (쓰기 중 전원 차단, 디스크 오류, 트랜잭션 중 프로세스 강제 종료):

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

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

진실의 원천은 항상 vault 파일이지, 데이터베이스가 아니에요. 데이터베이스는 언제든 재구축할 수 있는 파생 산출물이에요. 이건 핵심적인 설계 속성으로, 데이터베이스를 별도로 백업할 필요가 없어요.

--incremental 플래그

인덱서가 --incremental로 실행될 때:

  1. 모델 해시 확인. 저장된 모델 해시와 현재 모델을 비교해요. 다르면 자동으로 전체 재인덱싱 모드로 전환하고 사용자에게 경고해요.
  2. 파일 스캔. 허용된 폴더를 순회하며 파일 경로와 mtime을 수집해요.
  3. 변경 감지. 저장된 데이터와 비교해요.
  4. 배치 처리. 변경된 파일을 64개씩 배치로 나눠 재청크 분할 및 재임베딩해요.
  5. 진행 보고. 처리된 파일 수와 경과 시간을 출력해요.
  6. 정상 종료. SIGINT를 받으면 현재 파일 처리를 완료한 후 중단해요.

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

개인 노트에는 비밀 정보가 포함돼 있어요: API 키, bearer 토큰, 데이터베이스 연결 문자열, 디버깅 중 붙여넣은 개인 키 등. 자격 증명 필터는 이런 정보가 검색 인덱스에 진입하는 것을 방지해요.

문제점

OAuth 통합을 디버깅하는 노트에 다음과 같은 내용이 포함될 수 있어요:

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

필터링 없이는 JWT와 API 키가 모두 청크 분할되고, 임베딩되어 데이터베이스에 저장돼요. “authentication”을 검색하면 실제 비밀 정보가 포함된 청크가 반환돼요. 더 심각한 건, 검색기가 MCP를 통해 AI 도구에 결과를 전달하면, 비밀 정보가 AI의 컨텍스트 윈도우에 나타나고 잠재적으로 도구의 로그에도 남을 수 있어요.

패턴 기반 필터링

자격 증명 필터는 저장 전 모든 청크에 대해 실행되며, 25개의 벤더별 패턴과 일반 패턴을 매칭해요:

벤더별 패턴:

패턴 예시 정규식
OpenAI API 키 sk-... sk-[a-zA-Z0-9_-]{20,}
Anthropic API 키 sk-ant-api03-... sk-ant-api\d{2}-[a-zA-Z0-9_-]{20,}
GitHub PAT ghp_... gh[ps]_[a-zA-Z0-9]{36,}
AWS 액세스 키 AKIA... AKIA[0-9A-Z]{16}
Stripe 키 sk_live_... [sr]k_(live\|test)_[a-zA-Z0-9]{24,}
Cloudflare 토큰 ... 다양한 패턴

일반 패턴:

패턴 감지 방식
JWT 토큰 eyJ[a-zA-Z0-9_-]+\.eyJ[a-zA-Z0-9_-]+
Bearer 토큰 Bearer\s+[a-zA-Z0-9_\-\.]+
개인 키 -----BEGIN (RSA\|EC\|OPENSSH) PRIVATE KEY-----
고엔트로피 base64 4.5 bits/char 이상의 엔트로피, 40자 이상 문자열
비밀번호 할당 password\s*[:=]\s*["'][^"']+["']

필터 구현

def clean_content(text):
    """Scrub credentials from text before indexing."""
    result = ScanResult(is_clean=True, match_count=0, patterns=[])

    for pattern in CREDENTIAL_PATTERNS:
        matches = pattern.regex.findall(text)
        if matches:
            text = pattern.regex.sub(
                f"[REDACTED:{pattern.name}]", text
            )
            result.is_clean = False
            result.match_count += len(matches)
            result.patterns.append(pattern.name)

    return text, result

핵심 설계 원칙:

  1. 임베딩 전에 필터링. 정제된 텍스트가 임베딩 대상이에요. 벡터 표현에는 자격 증명 패턴이 절대 인코딩되지 않아요. “API key”를 검색하면 API 키 관리를 논의하는 노트가 반환되지, 실제 키가 포함된 노트가 반환되지는 않아요.

  2. 삭제가 아닌 대체. [REDACTED:pattern-name] 토큰은 주변 텍스트의 의미적 맥락을 보존해요. 임베딩은 자격 증명 자체를 인코딩하지 않으면서 “여기에 자격 증명과 유사한 것이 있었다”는 정보를 포착해요.

  3. 값이 아닌 패턴을 로깅. 필터는 매칭된 패턴을 로깅하지만 (예: “Scrubbed 2 credential(s) from oauth-debug.md [jwt, bearer-token]”), 자격 증명 값 자체는 절대 로깅하지 않아요.

경로 기반 제외

.indexignore 파일은 경로 기준의 대략적인 제외 기능을 제공해요. 자격 증명 필터는 인덱싱 대상 파일 내에서 세밀한 스크러빙을 제공해요. 두 가지 모두 필요해요:

  • .indexignore는 민감한 콘텐츠가 포함된 전체 폴더 제외용 (건강 기록, 재무 기록, 경력 문서)
  • 자격 증명 필터는 인덱싱 가능한 콘텐츠에 실수로 포함된 비밀 정보 처리용

데이터 분류

다양한 콘텐츠가 포함된 vault의 경우, 민감도별로 노트를 분류하는 것을 고려하세요:

등급 예시 인덱싱? 필터링?
공개 블로그 초안, 기술 노트
내부 프로젝트 계획, 아키텍처 결정
민감 급여 데이터, 건강 기록 아니오 (.indexignore) 해당 없음
제한 자격 증명, 개인 키 아니오 (.indexignore) 해당 없음

MCP Server 아키텍처

Model Context Protocol (MCP) 서버는 리트리버를 AI 에이전트가 호출할 수 있는 도구로 노출합니다. 이 섹션에서는 서버 설계, 기능 범위, 권한 경계를 다룹니다.

프로토콜 선택: STDIO vs HTTP

MCP는 두 가지 전송 모드를 지원합니다:

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 서비스로 동작합니다. 원격 접근, 다중 클라이언트 구성, 또는 볼트가 공유 서버에 있는 팀 환경에서 유용합니다.

{
  "mcpServers": {
    "obsidian": {
      "url": "http://localhost:3333/mcp"
    }
  }
}

권장 사항: 개인 볼트에는 STDIO를 사용하세요. 더 간단하고, 더 안전하며(네트워크 노출 없음), 서버 수명 주기를 AI 도구가 관리합니다. HTTP는 여러 도구 또는 여러 머신이 동일한 볼트에 동시 접근해야 할 때만 사용하세요.

MCP 사양 변천. 2025년 6월 MCP 사양에서는 OAuth 2.1 인가, 구조화된 도구 출력(타입이 지정된 반환 스키마), 엘리시테이션(서버에서 시작하는 사용자 프롬프트)이 추가되었습니다. 2025년 11월 릴리스에서는 Streamable HTTP가 1급 전송 모드로 도입되었고, .well-known URL 디스커버리를 통한 자동 서버 기능 탐색, 도구가 읽기 전용인지 변경을 일으키는지 선언하는 구조화된 도구 어노테이션, 그리고 SDK 계층 표준화 시스템이 제공되었습니다.1821 다음 사양 릴리스(잠정적으로 2026년 중반)에서는 장시간 실행 작업을 위한 비동기 작업, 의료 및 금융 같은 산업별 프로토콜 확장, 다중 에이전트 워크플로를 위한 에이전트 간 통신 표준이 제안되어 있습니다.21 개인 볼트 서버라면 STDIO가 가장 간단한 경로입니다. Streamable HTTP 전송과 .well-known 디스커버리는 주로 다중 테넌트 라우팅과 로드 밸런싱이 필요한 엔터프라이즈 HTTP 배포에 유리합니다. 전송 방식 선택에 영향을 미치는 업데이트는 MCP 로드맵에서 확인하세요.

기능 설계

MCP 서버는 최소한의 도구 세트를 노출해야 합니다:

search — 주요 도구입니다. hybrid 검색을 실행하고 순위가 매겨진 결과를 반환합니다.

{
  "name": "obsidian_search",
  "description": "Search the Obsidian vault using hybrid BM25 + vector retrieval",
  "parameters": {
    "query": { "type": "string", "description": "Search query" },
    "limit": { "type": "integer", "default": 5 },
    "max_tokens": { "type": "integer", "default": 2000 }
  }
}

read_note — 경로로 특정 노트의 전체 내용을 읽습니다. 에이전트가 검색 결과의 전체 컨텍스트를 확인하려 할 때 유용합니다.

{
  "name": "obsidian_read_note",
  "description": "Read the full content of a note by file path",
  "parameters": {
    "file_path": { "type": "string", "description": "Relative path within vault" }
  }
}

list_notes — 필터(폴더, 태그, 유형, 날짜 범위)에 맞는 노트를 나열합니다. 에이전트가 특정 쿼리 없이 탐색할 때 유용합니다.

{
  "name": "obsidian_list_notes",
  "description": "List notes matching filters",
  "parameters": {
    "folder": { "type": "string", "description": "Folder path within vault" },
    "tag": { "type": "string", "description": "Tag to filter by" },
    "limit": { "type": "integer", "default": 20 }
  }
}

get_context — 검색을 실행하고 결과를 대화에 주입하기 적합한 컨텍스트 블록으로 포맷하는 편의 도구입니다.

{
  "name": "obsidian_get_context",
  "description": "Get formatted context from vault for a topic",
  "parameters": {
    "topic": { "type": "string", "description": "Topic to get context for" },
    "max_tokens": { "type": "integer", "default": 2000 }
  }
}

권한 경계

MCP 서버는 엄격한 경계를 적용해야 합니다:

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

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

  3. 자격 증명 필터링 출력. 데이터베이스에 사전 필터링된 콘텐츠가 포함되어 있더라도, 심층 방어 차원에서 출력 시 자격 증명 필터링을 적용하세요.

  4. 토큰 제한 응답. 모든 도구 응답에 max_tokens를 적용하여 AI 도구가 과도하게 큰 컨텍스트 블록을 수신하는 것을 방지하세요.

오류 처리

MCP 도구는 AI 도구가 복구할 수 있도록 구조화된 오류 메시지를 반환해야 합니다:

def search(self, query, limit=5, max_tokens=2000):
    if not self.db_path.exists():
        return {
            "error": "Index database not found. Run the indexer first.",
            "suggestion": "python index_vault.py --full"
        }

    results = self.retriever.search(query, limit, max_tokens)

    if not results:
        return {
            "results": [],
            "message": f"No results found for '{query}'. Try broader terms."
        }

    return {
        "results": [
            {
                "file_path": r["file_path"],
                "section": r["section"],
                "text": r["chunk_text"],
                "score": round(r["rrf_score"], 4),
            }
            for r in results
        ],
        "count": len(results),
        "query": query,
    }

Claude Code 연동

Claude Code은 Obsidian 검색 시스템의 주요 소비자입니다. 이 섹션에서는 MCP 설정, 훅 연동, 그리고 obsidian_bridge.py 패턴을 다룹니다.

MCP 설정

Obsidian MCP 서버를 ~/.claude/settings.json에 추가하세요:

{
  "mcpServers": {
    "obsidian": {
      "command": "python",
      "args": ["/path/to/obsidian_mcp.py"],
      "env": {
        "VAULT_PATH": "/absolute/path/to/vault",
        "DB_PATH": "/absolute/path/to/vectors.db"
      }
    }
  }
}

설정을 추가한 후 Claude Code을 재시작하세요. MCP 서버가 자식 프로세스로 시작됩니다. 실행 여부를 확인하려면 다음과 같이 입력하세요:

> What tools do you have from the obsidian MCP server?

Claude Code이 사용 가능한 도구(obsidian_search, obsidian_read_note 등)를 목록으로 표시해야 합니다.

훅 연동

훅은 정의된 라이프사이클 시점에서 Claude Code의 동작을 확장합니다. Obsidian 연동과 관련된 훅은 두 가지입니다:

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

#!/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 훅 — 중요한 도구 출력을 볼트에 캡처하여 향후 검색에 활용합니다.

#!/bin/bash
# ~/.claude/hooks/post-tool-use/capture-insight.sh
# Capture significant outputs to vault (selective)

TOOL_NAME="$1"
OUTPUT="$2"

# Only capture substantial outputs
if [ ${#OUTPUT} -gt 500 ]; then
    python /path/to/capture.py --text "$OUTPUT" --source "claude-code-$TOOL_NAME"
fi

obsidian_bridge.py 패턴

브리지 모듈은 훅과 스킬에서 호출할 수 있는 Python API를 제공합니다:

# obsidian_bridge.py
from retriever import HybridRetriever

_retriever = None

def get_retriever():
    global _retriever
    if _retriever is None:
        _retriever = HybridRetriever(
            db_path="/path/to/vectors.db",
            vault_path="/path/to/vault",
        )
    return _retriever

def search_vault(query, limit=5, max_tokens=2000):
    """Search vault and return formatted context."""
    retriever = get_retriever()
    results = retriever.search(query, limit, max_tokens)

    if not results:
        return ""

    lines = ["## Vault Context\n"]
    for r in results:
        lines.append(f"**{r['file_path']}** — {r['section']}")
        lines.append(f"> {r['chunk_text'][:500]}")
        lines.append("")

    return "\n".join(lines)

/capture 스킬

인사이트를 볼트에 다시 캡처하기 위한 Claude Code 스킬입니다:

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

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

커스텀 커맨드 패턴

Claude Code 스킬은 볼트 작업을 이름이 지정된 커맨드로 래핑할 수 있습니다. 실무자들은 볼트를 읽기 소스이자 쓰기 대상으로 활용하는 Obsidian 전용 커맨드 라이브러리를 구축해 왔습니다.

시그널 스캐닝. /scan-intel 커맨드는 외부 소스를 쿼리하고, 발견 항목을 개인 연구 관심사에 대해 점수를 매기며, 적합한 시그널을 frontmatter가 포함된 볼트 노트로 작성합니다:

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

이 커맨드는 설정된 소스(arXiv, HN, RSS)에서 데이터를 가져오고, 점수 모델(관련성, 실행 가능성, 깊이, 권위)을 적용한 후, 통과한 시그널을 주제별 볼트 폴더에 작성합니다. 볼트가 자동화된 인텔리전스 파이프라인의 다운스트림 소비자가 되는 셈입니다.

캡틴 로그. /captains-log 커맨드는 모든 리포지토리의 일일 git 활동을 집계하고, 구조화된 저널 항목을 볼트에 작성하며, 내린 결정, 깨달은 점, 미해결 스레드를 포함합니다:

/captains-log

이 커맨드는 GitHub에서 커밋 히스토리를 가져와 리포지토리별로 그룹화하고, 내러티브 형식의 저널 항목으로 포맷합니다. 시간이 지나면서 일일 로그가 무엇을 배포했고 왜 그랬는지에 대한 검색 가능한 기록을 만들어 줍니다.

Obsidian 캡처. /obsidian-capture 커맨드는 현재 Claude Code 세션에서 얻은 인사이트를 적절한 메타데이터와 함께 볼트에 직접 작성합니다:

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

이 패턴은 모든 볼트 작업으로 확장할 수 있습니다: MOC 생성, 프로젝트 상태 노트 업데이트, 관련 시그널 연결, 또는 축적된 일일 로그로부터 주간 다이제스트 생성 등이 가능합니다.

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

컨텍스트 윈도우 관리

연동 시 Claude Code의 컨텍스트 윈도우를 고려해야 합니다:

  • 쿼리당 주입 컨텍스트를 1,500~2,000 토큰으로 제한하세요. 이보다 많으면 에이전트의 작업 메모리와 경쟁하게 됩니다.
  • 출처 정보를 포함하세요. 에이전트가 소스를 참조할 수 있도록 파일 경로와 섹션 제목을 항상 포함하세요.
  • 청크 텍스트를 잘라내세요. 긴 청크는 완전히 생략하지 말고 ...으로 잘라내는 것이 좋습니다. 처음 300~500자에 핵심 정보가 담겨 있는 경우가 대부분입니다.
  • 모든 도구 호출에 주입하지 마세요. PreToolUse 훅은 호출되는 도구에 따라 선택적으로 컨텍스트를 주입해야 합니다. 읽기 작업에는 볼트 컨텍스트가 필요 없지만, 쓰기와 편집 작업에는 유용합니다.

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
~/.claude/hooks/ 미지원
스킬 ~/.claude/skills/ 미지원
지시문 파일 CLAUDE.md AGENTS.md
승인 모드 --dangerously-skip-permissions suggest / auto-edit / full-auto

핵심 차이점: Codex CLI는 훅을 지원하지 않습니다. 자동 컨텍스트 주입 패턴(PreToolUse 훅)을 사용할 수 없습니다. 대신 AGENTS.md에 작업 시작 전 볼트를 검색하라는 명시적 지시를 포함해야 합니다.


Cursor 및 기타 도구

Cursor와 기타 MCP를 지원하는 AI 도구들은 동일한 Obsidian MCP 서버에 연결할 수 있어요. 이 섹션에서는 주요 도구별 설정 방법을 다룹니다.

Cursor

프로젝트 루트의 .cursor/mcp.json에 다음을 추가하세요:

{
  "mcpServers": {
    "obsidian": {
      "command": "python",
      "args": ["/path/to/obsidian_mcp.py"],
      "env": {
        "VAULT_PATH": "/absolute/path/to/vault",
        "DB_PATH": "/absolute/path/to/vectors.db"
      }
    }
  }
}

Cursor의 .cursorrules 파일에 볼트 활용 지침을 포함할 수 있어요:

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 플러그인) 해당 없음 (내장) Claude Code CLI Obsidian 플러그인 설정
Agent Client (Obsidian 플러그인) 해당 없음 (내장) ACP Obsidian 플러그인 설정

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 통합만큼 깔끔하지는 않지만 범용적으로 작동합니다.


구조화된 노트를 활용한 프롬프트 캐싱

볼트의 구조화된 노트는 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
    },
}

캐시 무효화

캐시 무효화는 두 가지 신호를 기반으로 해요:

  1. TTL 만료. 각 컨텍스트 블록에는 유효 기간이 있어요. 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)

캐시된 블록은 세션 시작 시 로드돼요. 동적 검색 결과는 쿼리별로 남은 예산을 채웁니다. 이 하이브리드 접근 방식은 에이전트에게 자주 필요한 컨텍스트의 기본 베이스라인을 제공하면서도, 특정 쿼리를 위한 예산을 확보해 줘요.

캐싱 전후 토큰 사용량 비교

캐싱 없이: 관련 쿼리마다 볼트 검색이 실행되어 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 훅

도구 출력은 장황할 수 있어요: 스택 트레이스, 파일 목록, 테스트 결과 등. PostToolUse 훅은 이러한 출력이 컨텍스트 윈도우 공간을 차지하기 전에 압축할 수 있어요.

문제

테스트를 실행하는 Bash 도구 호출은 다음과 같은 결과를 반환할 수 있어요:

PASSED tests/test_auth.py::test_login_success
PASSED tests/test_auth.py::test_login_failure
PASSED tests/test_auth.py::test_token_refresh
PASSED tests/test_auth.py::test_session_expiry
... (200 more lines)
FAILED tests/test_api.py::test_rate_limit_exceeded

전체 출력은 5,000 토큰이지만, 핵심 정보는 2줄에 담겨 있어요: 200개 통과, 1개 실패.

훅 구현

#!/bin/bash
# ~/.claude/hooks/post-tool-use/compress-output.sh
# Compress verbose tool outputs to preserve context window

TOOL_NAME="$1"
OUTPUT="$2"
OUTPUT_LEN=${#OUTPUT}

# Only compress large outputs
if [ "$OUTPUT_LEN" -lt 2000 ]; then
    exit 0  # Pass through unchanged
fi

case "$TOOL_NAME" in
    Bash)
        # Compress test output
        if echo "$OUTPUT" | grep -q "PASSED\|FAILED"; then
            PASSED=$(echo "$OUTPUT" | grep -c "PASSED")
            FAILED=$(echo "$OUTPUT" | grep -c "FAILED")
            FAILURES=$(echo "$OUTPUT" | grep "FAILED")
            echo "Tests: $PASSED passed, $FAILED failed"
            if [ "$FAILED" -gt 0 ]; then
                echo "Failures:"
                echo "$FAILURES"
            fi
        fi
        ;;
esac

재귀 트리거 방지

압축 훅이 출력을 생성하면, 가드가 없을 경우 자기 자신을 트리거할 수 있어요:

# Guard against recursive invocation
if [ -n "$COMPRESS_HOOK_ACTIVE" ]; then
    exit 0
fi
export COMPRESS_HOOK_ACTIVE=1

압축 휴리스틱

출력 유형 감지 방법 압축 전략
테스트 결과 PASSED / FAILED 키워드 통과/실패 수 집계, 실패만 표시
파일 목록 명령어에 ls 또는 find 포함 처음 20개 항목 + 총 개수로 축약
스택 트레이스 Traceback 키워드 첫 번째와 마지막 프레임 + 오류 메시지만 유지
Git 상태 modified: / new file: 상태별 개수 요약
빌드 출력 warning: / error: 정보 라인 제거, 경고/오류만 유지

시그널 수집 및 분류 파이프라인

수집 레이어는 볼트에 무엇이 들어올지를 결정해요. 큐레이션 없이는 볼트에 노이즈가 쌓이기만 합니다. 이 섹션에서는 시그널을 도메인 폴더로 라우팅하는 스코어링 파이프라인을 다룹니다.

소스

시그널은 여러 채널에서 들어와요:

  • RSS 피드: 기술 블로그, 보안 권고, 릴리스 노트
  • 북마크: Obsidian Web Clipper 또는 북마클릿을 통해 저장한 브라우저 북마크
  • 뉴스레터: 이메일 뉴스레터의 주요 발췌
  • 수동 캡처: 독서, 대화, 리서치 중 작성한 노트
  • 도구 출력: 훅을 통해 캡처한 중요한 AI 도구 출력
  • iOS 공유 확장: Obsidian의 iOS 앱(2026년 초 업데이트)에는 공유 확장이 포함되어 있어서, Safari, 소셜 네트워크, 기타 앱의 콘텐츠를 Obsidian을 열지 않고도 볼트에 직접 저장할 수 있어요.31 이를 통해 마찰이 적은 모바일 수집 경로가 만들어져요 — Safari에서 기사를 공유하면 스코어링 대기 상태의 볼트 노트로 도착합니다.
  • Obsidian CLI: 셸 스크립트와 훅이 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

지식 그래프 패턴

Obsidian의 wiki-link 그래프는 노트 간의 관계를 인코딩합니다. 이 섹션에서는 링크 의미론, 컨텍스트 확장을 위한 그래프 순회, 그리고 그래프 품질을 저하시키는 안티패턴을 다룹니다.

모든 wiki-link는 그래프에서 방향성 있는 엣지를 생성합니다. Obsidian은 순방향 링크와 backlink를 모두 추적해요:

  • 순방향 링크: 노트 A에 [[Note B]]가 포함되어 있으면 → A가 B로 링크
  • Backlink: 노트 B에서 노트 A가 자신을 참조하고 있음을 표시

그래프는 컨텍스트에 따라 다양한 유형의 관계를 인코딩합니다:

링크 패턴 의미 예시
인라인 링크 “관련됨” “See [[OAuth Token Rotation]] for details”
헤더 링크 “하위 주제를 가짐” ”## Related\n- [[Token Rotation]]\n- [[Session Management]]”
태그형 링크 “분류됨” ”[[type/reference]]”
MOC 링크 “소속됨” 관련 노트를 나열하는 Maps of Content 노트

Maps of Content (MOC)

MOC는 관련 노트를 탐색 가능한 구조로 정리하는 인덱스 노트입니다:

---
title: "Authentication & Security MOC"
type: moc
domain: security
---

## Core Concepts
- [[OAuth 2.0 Overview]]
- [[JWT Token Anatomy]]
- [[Session Management Patterns]]

## Implementation Patterns
- [[OAuth Token Rotation]]
- [[Refresh Token Security]]
- [[PKCE Flow Implementation]]

## Failure Modes
- [[Token Expiry Handling]]
- [[Session Fixation Prevention]]
- [[CSRF Defense Strategies]]

MOC는 두 가지 방식으로 검색에 도움을 줍니다:

  1. 직접 매칭. “authentication overview”를 검색하면 MOC 자체가 매칭되어, 에이전트에게 관련 노트의 큐레이션된 목록을 제공합니다.
  2. 컨텍스트 확장. 특정 노트를 찾은 후, 검색기가 해당 노트가 포함된 MOC가 있는지 확인하고 MOC의 구조를 결과에 포함시켜, 에이전트에게 더 넓은 주제의 맵을 제공할 수 있습니다.

컨텍스트 확장을 위한 그래프 순회

검색기의 향후 개선 방향으로, 상위 결과를 찾은 후 링크를 따라가며 컨텍스트를 확장하는 방법이 있습니다:

def expand_context(results, depth=1):
    """Follow wiki-links from top results to find related context."""
    expanded = set()
    for result in results:
        # Parse wiki-links from chunk text
        links = extract_wiki_links(result["chunk_text"])
        for link_target in links:
            # Resolve link to file path
            target_path = resolve_wiki_link(link_target)
            if target_path and target_path not in expanded:
                expanded.add(target_path)
                # Include target's most relevant chunk
                target_chunks = get_chunks_for_file(target_path)
                # ... rank and include best chunk
    return results + list(expanded_results)

현재 검색기에는 구현되어 있지 않지만, 그래프 구조의 자연스러운 확장을 나타냅니다.

안티패턴

고립된 클러스터. 서로 간에는 링크가 있지만 볼트의 나머지 부분과는 연결이 없는 노트 그룹입니다. Obsidian의 그래프 패널에서 분리된 섬으로 나타납니다. 고립된 클러스터는 MOC가 누락되었거나 도메인 간 링크가 부족하다는 신호예요.

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

링크만 많고 내용이 없는 노트. wiki-link로만 구성되고 산문이 없는 노트입니다. 청커가 임베딩할 텍스트가 없어 인덱싱이 제대로 되지 않아요. 링크된 노트들이 왜 관련되는지 설명하는 최소 한 단락의 컨텍스트를 추가하세요.

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


개발자 워크플로 레시피

볼트 검색과 일상적인 개발 작업을 결합한 실용적인 워크플로입니다.

아침 컨텍스트 로딩

하루를 시작할 때 관련 컨텍스트를 로딩하세요:

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

검색기가 현재 진행 중인 프로젝트에 대한 최근 노트를 반환하여, 어디까지 작업했는지 빠르게 상기시켜 줍니다. 어제의 커밋 메시지를 다시 읽는 것보다 훨씬 효과적이에요.

코딩 중 리서치 캡처

기능을 구현하면서 에디터를 떠나지 않고 인사이트를 캡처하세요:

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

캡처된 인사이트는 즉시 인덱싱되어 향후 검색에 활용할 수 있습니다. 수개월에 걸쳐 이런 마이크로 캡처들이 구현별 지식의 코퍼스를 구축해요.

프로젝트 킥오프

새로운 프로젝트나 기능을 시작할 때:

  1. 볼트 검색: “[기술/패턴]에 대해 내가 알고 있는 것은?”
  2. 상위 5개 결과를 검토하여 이전 결정 사항과 주의점 확인
  3. 해당 도메인에 MOC가 있는지 확인하고, 없으면 생성
  4. 실패 모드 검색: “[기술]의 문제점”

볼트 검색을 활용한 디버깅

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

Search my vault for [error message or symptom]

이전 디버깅 노트에 원인과 해결 방법이 담겨 있는 경우가 많습니다. 프로젝트를 넘나드는 반복적인 문제에 특히 유용해요 — 여러분이 잊어버린 것을 볼트가 기억하고 있으니까요.

코드 리뷰 준비

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개 이상의 노트에서는 다음을 고려하세요: - 더 빠른 임베딩을 위해 배치 크기를 64에서 128로 증가 - 동시 접근을 위한 WAL 모드 사용 (기본값) - 업무 외 시간에 전체 리인덱싱 실행

쿼리 최적화

WAL 모드. SQLite의 Write-Ahead Logging 모드는 인덱서가 쓰기 작업을 수행하는 동안 동시 읽기를 가능하게 합니다:

db.execute("PRAGMA journal_mode=WAL")

MCP 서버가 인덱서의 증분 업데이트 중에 쿼리를 처리할 때 필수적입니다.

커넥션 풀링. MCP 서버는 쿼리마다 새 커넥션을 여는 대신 데이터베이스 커넥션을 재사용해야 합니다. WAL 모드를 사용하는 단일 장기 커넥션으로 동시 읽기를 지원할 수 있어요.

# MCP server initialization
db = sqlite3.connect(DB_PATH, check_same_thread=False)
db.execute("PRAGMA journal_mode=WAL")
db.execute("PRAGMA mmap_size=268435456")  # 256 MB mmap

메모리 매핑 I/O. mmap_size pragma는 SQLite에 데이터베이스 파일에 대해 메모리 매핑 I/O를 사용하도록 지시합니다. 83 MB 데이터베이스의 경우 전체 파일을 메모리에 매핑하면 대부분의 디스크 읽기가 제거돼요.

FTS5 최적화. 전체 리인덱싱 후 다음을 실행하세요:

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

FTS5의 내부 b-tree 세그먼트를 병합하여 이후 검색의 쿼리 지연 시간을 줄여줍니다.

스케일링 벤치마크

Apple M3 Pro, 36 GB RAM, NVMe SSD에서 측정:

작업 500개 노트 5K 노트 15K 노트 50K 노트
BM25 쿼리 2ms 5ms 12ms 25ms
벡터 쿼리 1ms 3ms 8ms 20ms
RRF 퓨전 <1ms <1ms 3ms 5ms
전체 검색 3ms 8ms 23ms 50ms

모든 벤치마크는 데이터베이스 접근, 쿼리 실행, 결과 포맷팅을 포함합니다. MCP STDIO 통신의 네트워크 지연 시간은 1-2ms가 추가돼요.

문제 해결

인덱스 드리프트

증상: 검색 결과가 오래된 내용을 반환하거나 최근 추가한 노트를 찾지 못해요.

원인: 노트를 추가한 후 증분 인덱서가 실행되지 않았거나, 파일의 mtime이 업데이트되지 않은 경우예요 (예: 타임스탬프가 보존된 채 다른 기기에서 동기화된 경우).

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

임베딩(Embedding) 모델 교체

증상: 임베딩 모델을 변경한 후 벡터 검색이 엉뚱한 결과를 반환해요.

원인: 이전 모델의 벡터가 새로운 쿼리 벡터와 비교되고 있어요. 차원이나 벡터 공간의 의미론이 호환되지 않기 때문이에요.

해결: 인덱서가 모델 해시 불일치를 감지하면 자동으로 전체 재인덱싱을 실행해야 해요. 자동으로 실행되지 않으면 데이터베이스를 수동으로 삭제하고 재인덱싱하세요:

rm vectors.db
python index_vault.py --full

FTS5 유지보수

증상: 여러 번의 증분 업데이트 후 FTS5 쿼리가 부정확하거나 불완전한 결과를 반환해요.

원인: 작은 업데이트가 많이 쌓이면 FTS5 내부 세그먼트가 단편화될 수 있어요.

해결: 재구성 및 최적화를 실행하세요:

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

MCP 타임아웃

증상: AI 도구가 MCP 서버 타임아웃을 보고해요.

원인: 첫 번째 쿼리에서 모델 로딩(지연 초기화)이 발생하며 2~5초가 소요돼요. AI 도구의 기본 MCP 타임아웃이 이보다 짧을 수 있어요.

해결: 서버 시작 시 모델을 미리 워밍업하세요:

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

SQLite 파일 잠금

증상: SQLITE_BUSY 또는 SQLITE_LOCKED 오류가 발생해요.

원인: 여러 프로세스가 동시에 데이터베이스에 쓰기를 시도하고 있어요. WAL 모드는 동시 읽기를 허용하지만 쓰기는 하나의 프로세스만 가능해요.

해결: 데이터베이스에 쓰기를 수행하는 프로세스는 인덱서 하나만 되도록 하세요. MCP 서버와 훅은 읽기만 수행해야 해요. 동시 쓰기가 필요하다면 WAL 모드를 사용하고 busy timeout을 설정하세요:

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

sqlite-vec 로드 실패

증상: 벡터 검색이 비활성화되고 검색기가 BM25 전용 모드로 실행돼요.

원인: sqlite-vec 확장이 설치되지 않았거나, 라이브러리 경로에서 찾을 수 없거나, SQLite 버전과 호환되지 않아요.

해결:

# Install via pip
pip install sqlite-vec

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

확장이 정상적으로 로드되는지 확인하세요:

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

대규모 볼트 메모리 문제

증상: 대규모 볼트(50,000개 이상의 노트)를 전체 재인덱싱할 때 메모리 부족 오류가 발생해요.

원인: 임베딩 배치 크기가 너무 크거나, 모든 파일 내용이 동시에 메모리에 로드되고 있어요.

해결: 배치 크기를 줄이고 파일을 증분 방식으로 처리하세요:

BATCH_SIZE = 32  # Reduce from 64

또한 인덱서가 모든 파일을 메모리에 한꺼번에 로드하는 대신, 각 파일을 하나씩 읽고 청킹(chunking)하고 임베딩한 후 다음 파일로 넘어가도록 하세요.


마이그레이션 가이드

Apple Notes에서 마이그레이션

  1. macOS의 “모두 내보내기” 옵션을 사용하거나 apple-notes-liberator 같은 마이그레이션 도구로 Apple Notes를 내보내세요
  2. HTML 내보내기 파일을 markdownify 또는 pandoc를 사용해 마크다운으로 변환하세요
  3. 변환된 파일을 볼트의 00-inbox/ 폴더로 이동하세요
  4. 각 노트를 검토하고 frontmatter를 추가하세요
  5. 노트를 적절한 도메인 폴더로 이동하세요

Notion에서 마이그레이션

  1. Notion에서 내보내기: 설정 → 내보내기 → Markdown & CSV
  2. 내보낸 압축 파일을 볼트의 00-inbox/ 폴더에 압축 해제하세요
  3. Notion 특유의 마크다운 아티팩트를 수정하세요:
  4. Notion은 체크리스트에 - [ ]를 사용해요 — 이는 표준 마크다운이에요
  5. Notion은 속성 테이블을 HTML로 포함해요 — YAML frontmatter로 변환하세요
  6. Notion은 이미지를 상대 경로로 삽입해요 — 이미지를 첨부 파일 폴더로 복사하세요
  7. 표준 frontmatter(type, domain, tags)를 추가하세요
  8. Notion 페이지 링크를 Obsidian wiki-link로 교체하세요

Google Docs에서 마이그레이션

  1. Google Takeout을 사용해 모든 문서를 내보내세요
  2. .docx 파일을 마크다운으로 변환하세요: 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를 추가한 후 폴더별로 정리하세요

일반 마크다운(Obsidian 미사용)에서 마이그레이션

이미 마크다운 파일 디렉터리가 있다면:

  1. 해당 디렉터리를 Obsidian 볼트로 열어요 (Obsidian → 볼트 열기 → 폴더 열기)
  2. 디렉터리가 버전 관리 중이라면 .obsidian/.gitignore에 추가하세요
  3. frontmatter 템플릿을 만들고 기존 파일에 적용하세요
  4. 노트를 읽고 정리하면서 [[wiki-links]]로 연결하기 시작하세요
  5. 인덱서를 바로 실행하세요 — 검색 시스템은 첫날부터 작동해요

다른 검색 시스템에서 마이그레이션

다른 임베딩/검색 시스템에서 마이그레이션하는 경우:

  1. 벡터를 마이그레이션하려고 하지 마세요. 모델마다 호환되지 않는 벡터 공간을 생성해요. 새 모델로 전체 재인덱싱을 실행하세요.
  2. 인덱스가 아닌 콘텐츠를 마이그레이션하세요. 볼트 파일이 원본 데이터예요. 인덱스는 파생된 산출물일 뿐이에요.
  3. 마이그레이션 후 검증하세요. 답을 알고 있는 10~20개의 쿼리를 실행해서 결과가 기대와 일치하는지 확인하세요.

변경 이력

날짜 변경 내용
2026-04-01 Obsidian CLI 섹션 추가 (AI 워크플로를 위한 v1.12 명령어). 에이전트 플러그인 섹션 추가 (Claudian, Agent Client). 볼트 구성을 위한 Bases 코어 플러그인 문서화. 플러그인 수 2,500개 이상으로 업데이트. iOS Share Extension을 인테이크 소스로 추가. 내장 에이전트 플러그인으로 호환성 매트릭스 업데이트.
2026-03-30 MCPVault v0.11.0: list_all_tags 도구, .base/.canvas 지원, @bitbonsai/mcpvault로 이름 변경. Obsidian Desktop v1.12.7이 더 빠른 터미널 상호작용을 위해 CLI 바이너리를 번들로 포함.
2026-03-23 sqlite-vec v0.1.7 안정 버전 문서화: vec0 테이블의 DELETE 지원, 페이지네이션을 위한 KNN 거리 제약 조건. DiskANN 근사 최근접 이웃 인덱스 출시 예정 발표.
2026-03-07 potion-multilingual-128M (101개 언어, 2025년 5월) 임베딩 모델 비교표에 추가. sqlite-vec v0.1.7-alpha.10 (CI/CD 수정, 기능 변경 없음). MCP 사양 및 검색 기법 현행 확인.
2026-03-03 MCP 사양 발전 업데이트 (2025년 11월 출시: Streamable HTTP, .well-known, 도구 주석). Model2Vec 파인튜닝 및 BPE/Unigram 토크나이저 지원 추가. 커뮤니티 MCP 서버 비교표 추가. Smart Connections v4로 업데이트.
2026-03-02 potion-base-32M 및 potion-retrieval-32M 모델 비교표에 추가. 양자화/차원 축소 섹션 추가. MCP 사양 발전 노트 추가.
2026-03-01 최초 공개

참고 문헌


  1. Internet Vin, “22 commands I use with Obsidian and Claude Code,” March 2026, 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. 볼트 전체 재인덱싱 예상 비용: 약 $0.30. 

  5. van Dongen, T. et al. Model2Vec: Turn any Sentence Transformer into a Small Fast Model. arXiv, 2025. 문장 트랜스포머에서 정적 임베딩(embeddings)을 생성하는 증류 접근법을 설명합니다. 

  6. MTEB: Massive Text Embedding Benchmark. potion-base-8M 평균 점수 50.03 대 all-MiniLM-L6-v2 56.09 (89% 성능 유지). 

  7. SQLite FTS5 Extension. FTS5는 BM25 랭킹과 구성 가능한 컬럼 가중치를 갖춘 전문 검색 기능을 제공합니다. 

  8. sqlite-vec: A vector search SQLite extension. SQLite 내에서 KNN 벡터 검색을 위한 vec0 가상 테이블을 제공합니다. 

  9. Robertson, S. and Zaragoza, H. The Probabilistic Relevance Framework: BM25 and Beyond. Foundations and Trends in Information Retrieval, 2009. 

  10. Karpukhin, V. et al. Dense Passage Retrieval for Open-Domain Question Answering. EMNLP, 2020. 밀집 표현이 오픈 도메인 QA에서 BM25를 9-19% 능가합니다. 

  11. Reimers, N. and Gurevych, I. Sentence-BERT: Sentence Embeddings using Siamese BERT-Networks. EMNLP, 2019. 밀집 의미 유사도에 관한 기초 연구입니다. 

  12. Luan, Y. et al. Sparse, Dense, and Attentional Representations for Text Retrieval. TACL, 2021. 하이브리드(hybrid) 검색이 MS MARCO에서 단일 모달리티 접근법을 일관되게 능가합니다. 

  13. SQLite Write-Ahead Logging. 단일 쓰기와 동시 읽기를 위한 WAL 모드입니다. 

  14. Gao, Y. et al. Retrieval-Augmented Generation for Large Language Models: A Survey. arXiv, 2024. RAG 아키텍처와 청킹(chunking) 전략에 대한 서베이입니다. 

  15. Thakur, N. et al. BEIR: A Heterogeneous Benchmark for Zero-shot Evaluation of Information Retrieval Models. NeurIPS, 2021. 

  16. Model2Vec: Distill a Small Fast Model from any Sentence Transformer. Minish Lab, 2024. 

  17. Obsidian Documentation. Obsidian 공식 문서입니다. 

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

  19. 저자의 프로덕션 데이터. 16,894개 파일, 49,746개 청크, 83.56 MB SQLite 데이터베이스, 14개월간 처리된 7,771개 시그널. 쿼리 지연 시간은 time.perf_counter()로 측정되었습니다. 

  20. Model2Vec Potion Models. Minish Lab, 2025. Potion-base-32M (MTEB 52.46), potion-retrieval-32M (MTEB 검색 36.35), v0.5.0+ 양자화/차원 축소 기능. 

  21. Update on the Next MCP Protocol Release. 2025년 11월 릴리스에서 Streamable HTTP 전송, .well-known URL 디스커버리, 구조화된 도구 어노테이션, SDK 티어 표준화가 적용되었습니다. 다음 릴리스는 잠정적으로 2026년 중반 예정이며 비동기 작업, 도메인별 확장, 에이전트 간 통신이 포함됩니다. 

  22. Model2Vec Releases. v0.4.0 (2025년 2월): 학습/미세 조정 지원. v0.5.0 (2025년 4월): 백엔드 재작성, 양자화, 차원 축소. v0.7.0 (2025년 10월): 어휘 양자화, BPE/Unigram 토크나이저 지원. 

  23. Smart Connections for Obsidian. Smart Connections v4: 로컬 우선 AI 임베딩, 초기 인덱싱 후 오프라인에서도 시맨틱 검색이 작동합니다. 

  24. potion-multilingual-128M. Minish Lab, 2025년 5월. 101개 언어를 지원하는 정적 임베딩 모델로 최고 성능의 다국어 정적 임베딩입니다. 다른 potion 모델과 동일한 numpy 전용 의존성입니다. 

  25. MCPVault v0.11.0. 2026년 3월. frontmatter 및 해시태그를 카운트와 함께 스캔하는 새로운 list_all_tags 도구 추가. 점으로 시작하는 폴더 처리 개선, .base.canvas 파일 지원. 패키지명이 npm에서 @bitbonsai/mcpvault로 변경되었습니다. 

  26. sqlite-vec v0.1.7 Release. 2026년 3월 17일. 안정 릴리스: vec0 가상 테이블에 대한 DELETE 지원, 페이지네이션을 위한 KNN 거리 제약, 퍼즈 테스팅 개선. DiskANN 근사 최근접 이웃 인덱싱은 향후 릴리스 예정으로 발표되었습니다. 

  27. Introduction to Bases. v1.9.10에서 도입된 Obsidian 코어 플러그인입니다. frontmatter 속성을 필드로 사용하여 볼트 파일에 대한 데이터베이스 유사 뷰(테이블, 갤러리, 캘린더, 칸반 보드)를 제공합니다. 파일은 .base 형식으로 저장됩니다. 

  28. Obsidian 1.12 Desktop Changelog. 2026년 2월 27일. 터미널 기반 볼트 자동화를 위한 Obsidian CLI를 도입합니다. 검색, 데일리 노트, 템플릿, 속성, 플러그인, 작업, 개발자 도구 명령을 포함합니다. CLI 문서

  29. Claudian. 볼트에 Claude Code를 AI 협업 도구로 내장하는 Obsidian 플러그인입니다. 사이드바 채팅, 컨텍스트 인식 프롬프트, 비전 지원, 슬래시 명령, 권한 모드를 제공합니다. 

  30. Agent Client. Agent Client Protocol (ACP)을 통해 Claude Code, Codex CLI, Gemini CLI에 대한 통합 인터페이스를 제공하는 Obsidian 플러그인입니다. 노트 멘션, 셸 실행, 작업 승인을 지원합니다. 

  31. Obsidian iOS Changelog. 2026년 초 업데이트에는 다른 앱에서 볼트로 직접 콘텐츠를 저장하는 공유 확장, 데일리 노트 및 북마크 위젯 수정, 노트 보기 위젯 새로고침 개선이 포함됩니다. 

VAULT obsidian.md INDEXED