요약
- 한국어 Obsidian vault(1106 docs) + Claude Sonnet 환경의 30-쿼리 파일럿에서, Claude + grep/Read이 Claude + qmd MCP보다 모든 면에서 동률 이상: 정확도 96.7% vs 93.3%, 속도 2.1× 빠름, 비용 14% 저렴.
- 그러나 retrieval-only 벤치(qmd bench)에서는 하이브리드(BM25+벡터)가 BM25-only를 압도 (recall 79% vs 32%). 즉 "qmd 자체는 좋은 검색 엔진"이지만, Claude가 똑똑하면 grep만으로 같은 결과에 도달한다.
- 결론: 도구의 가치는 사용자 능력의 함수다. 약한 검색 클라이언트(LLM이 전략을 못 짜는 경우)에는 qmd가 의미 있고, 강한 검색 클라이언트(Sonnet+)에는 grep 베이스라인이 이미 충분하다.
본문
실험 설계
- vault:
~/Documents/Obsidian1106 markdown (Zettelkasten + 회사 프로젝트 + 자료 노트, 한국어 위주, 영어 기술 용어 혼합) - 인덱싱: qmd v2.1.0,
embeddinggemma-300M-Q8_0(default), 1448 chunks / 14.2 MB DB / 40s 임베딩 시간 - eval set: 30 쿼리, 카테고리 5개(
exact / semantic / topical / cross-domain / alias) +unanswerable4개 + 랜덤 샘플 5개 - 두 조건:
- A (grep): Claude
--allowed-tools "Bash,Glob,Grep,Read"— rg/find/Read만 허용 - B (qmd): Claude
--allowed-tools "mcp__qmd__*,Read"+ qmd MCP stdio
- A (grep): Claude
- 동일 prompt,
--max-budget-usd 0.50캡,--model sonnet, 60 세션 ($4.09 총비용)
Retrieval-only 결과 (qmd bench)
| backend | P@k | Recall | MRR | F1 | Avg latency |
|---|---|---|---|---|---|
| bm25 | 0.333 | 0.322 | 0.333 | 0.327 | 1ms |
| vector | 0.589 | 0.675 | 0.600 | 0.598 | 25ms |
| hybrid ★ | 0.633 | 0.786 | 0.596 | 0.633 | 3288ms |
| full (w/ rerank) | 0.589 | 0.797 | 0.595 | 0.597 | 7982ms |
핵심:
- BM25 단독은 한국어에서 약함 — SQLite FTS5가 한국어 형태소 분석기 없음.
- Vector(embeddinggemma)는 60% F1로 BM25를 압도. multilingual 약점이 있어도 한국어 의미 검색에 충분.
- Hybrid(BM25+vector+RRF)가 sweet spot.
- Rerank는 비용 대비 효과 없음 — full pipeline이 hybrid보다 점수도 낮고 2.4× 느림. cross-encoder가 한국어에서 잘못 판단하는 케이스가 보임.
Agent-in-loop 결과
| 메트릭 | grep | qmd | Δ |
|---|---|---|---|
| hit rate | 96.7% | 93.3% | grep +3.4%p |
| avg turns | 2.87 | 3.70 | grep -0.83 |
| avg duration | 10.8s | 23.0s | grep 2.1× 빠름 |
| avg cost | $0.063 | $0.073 | grep -14% |
| avg input tokens | 57,297 | 81,516 | grep -30% |
McNemar paired exact p = 1.0 — 정확도 차이는 통계적 noise (n=30 너무 작음, codex 리뷰가 미리 경고).
paired 분포:
- both hit: 27 / grep only: 2 / qmd only: 1 / neither: 0
왜 Claude+grep이 이기는가
- Sonnet은 충분히 똑똑하다 — 한국어 패러프레이즈를 자체적으로 keyword set으로 변환해 grep. 동의어 재시도까지 자율적으로 함.
- Obsidian zettel 파일명이 descriptive — "비잔틴 장군 문제는 악의적인 노드가 포함됐을 때를 가정한 분산 합의 문제.md" 같은 한 문장 제목 → 키워드 grep만으로 충분.
- qmd MCP는 콜드 스타트 부담 — stdio transport는 매 세션마다 query-expansion 1.7B + reranker 0.6B 모델 로드. 단일 호출이 빨라야 하는 시나리오에 비효율.
- qmd는 더 큰 컨텍스트 반환 — snippets + scores 포함 → Claude가 후속 Read를 더 함 → 토큰 +30%.
qmd가 의미 있는 1 케이스 (n=30 중)
exact-kafka 쿼리(Kafka 구조):
- grep: 더 길고 구체적인 zettel을 답으로 골라 정답
kafka.md놓침 - qmd: BM25가 짧은 정확 매칭
kafka.md를 1순위로 정확히 골라냄
→ 짧은 영문 식별자 키워드 + 정확한 매치를 원할 때 qmd BM25가 가끔 유리.
qmd v2.1.0의 제약 (별도 발견)
dist/store.js가 search-time query embedding 모델로 embeddinggemma를 하드코딩. QMD_EMBED_MODEL 환경변수는 qmd embed에만 적용됨. 인덱스를 Qwen3/bge-m3 등으로 빌드하면 dimension mismatch로 SIGABRT.
→ qmd의 한국어 성능 천장이 embeddinggemma-300M의 multilingual 능력에 묶임. bge-m3 같은 한국어 강자 모델 swap이 무의미.
일반화 한계 (codex 리뷰 반영)
- n=30은 파일럿 — 통계적 결론에는 100
200개 필요. 카테고리 813개당으로 확대해야 차이 신뢰구간 수렴. - 카테고리 분포는 의도 표본 — 실제 사용 분포가 아님. 실제 검색 실패 로그 + 랜덤 + unanswerable 비중 조정 필요.
- 벤치/에이전트 prompt가 confounder를 다 통제하지 못함 — Claude의 grep 전략이 자율적이라 베이스라인 강도가 모델 능력에 의존.
- HTTP daemon mode 미측정 — stdio는 콜드 스타트 매번. HTTP daemon이면 warm 후 1~2초 단축 예상되나, 그래도 grep <100ms와 비교 못함.
- vault가 1106 docs로 작음 — qmd가 빛을 발하는 영역은 grep으로 후보 좁히기 어려운 큰 코퍼스(>10k docs).
함의
도구는 사용자 능력의 함수다. 평가는 도구의 절대 성능이 아니라 "이 사용자가 이 도구로 얻는 추가 가치"로 측정해야 한다.
- 자기 vault < 5000 docs + Claude Sonnet 이상 → qmd 도입 효용 의문
- 자기 vault > 10,000 docs 또는 약한 grep 클라이언트(작은 모델, 비대화형) → qmd 가치 있을 가능성
- 측정 없이 "RAG가 좋다"고 도구 도입 X. 본인 코퍼스로 grep + 지능형 클라이언트 베이스라인 먼저 측정.
참고
- 실험 코드:
~/Projects/my/my-obsidian-viewer/scripts/qmd-eval/ - fixture: 30개 쿼리 (5 카테고리 + unanswerable + random)
- 원본 qmd zettel: QMD는 BM25, 벡터, LLM 리랭킹을 로컬 SQLite에서 결합한다
- qmd repo: https://github.com/tobi/qmd
- qmd v2.1.0 search-model 하드코딩 발견:
dist/store.js:24DEFAULT_EMBED_MODEL = "embeddinggemma"