Query Markup Documents (QMD)

Contents
Vittore Carpaccio, "St. Augustine in His Study", c. 1502. Scuola di San Giorgio degli Schiavoni, Venice.
Vittore Carpaccio, "St. Augustine in His Study", c. 1502. Scuola di San Giorgio degli Schiavoni, Venice.

Intro

노트 관리가 단순히 읽고 쓰고 잘 찾기만 하면 된다지만, 사실 각 과정은 단순하다고 할 수 없을 만큼 복잡하다. 특히 읽기와 쓰기는 교육 과정에서 중점적으로 배우기에 그 중요성을 알고 어느 정도 익숙하다고 말할 수 있지만 찾는 것은 그렇지 않다. 이 글에서는 노트를 의미 기반으로 찾게 해주는 도구인 QMD 를 소개한다. 개인적으로 옵시디언과 함께 잘 활용 중인 툴이다.

참고로 QMD 는 로컬 기반으로 동작하는데 컴퓨터 사양이 어느 정도는 뒷받침되어야 한다.

파일 검색 방식은 크게 두 가지로 나눌 수 있다.

graph TD
    A[파일 검색 방식] --> B[Lexical Search<br/>#40;Keyword search#41;]
    A --> C[Semantic Search]
  • Lexical search 검색어가 포함된 것을 검색한다. 우리에게 직관적으로 떠오르는 바로 그 검색 방식. BM25 가 대표적이다.
  • Semantic search 검색어와 의미가 비슷한 것을 검색한다. 의미로 검색한다는 것엔 여러 방식이 있겠지만 Personal Knowledge Management 에서 주로 다루는 방식은 embedding model 을 사용한 방식이라고 보면 된다.

Embedding model 을 사용한다는 것은 다음과 같다.

graph LR
    A[📄📄📄<br/>노트들] --> B[🧠<br/>Embedding Model]
    C[🔍<br/>검색어] --> B
    B --> D["[0.2, -0.5, ...] × N<br/>벡터 집합"]
    B --> E["[0.3, -0.4, ...]<br/>쿼리 벡터"]
    D --> M(( ))
    E --> M
    M -- cosine similarity --> F[📄<br/>유사한 노트]

    classDef plain fill:none,stroke:none
    class A,B,C,F,M plain

노트를 임베딩하면 벡터로 바뀐다. 좋은 임베딩 모델은 의미가 비슷한 것은 비슷한 벡터로, 다른 것은 다른 벡터로 바꾼다. 그 후 검색어가 들어오면 검색어 역시 벡터로 바꿔서 코사인 유사도로 가장 유사한 노트를 찾아낸다.

QMD

QMD 란

옵시디언 노트를 의미 기반으로 찾고 싶다는 생각은 예전부터 있었고, 사실 이는 Smart Connections 커뮤니티 플러그인으로 가능했다. 그러나 어느 시점부터 embedding model 지정이 유료화되면서, 유료 사용이 아닌 한 Smart Connections 는 엉망이 되었다. 무료로 쓸 수 있게 지정된 embedding model 의 한국어 성능이 매우 떨어지기 때문이다. 대안을 찾던 중 마침 QMD 가 등장했다.

QMD 는 Shopify 창업자 Tobi Lütke 가 만든 로컬 마크다운 검색 도구다. CLI 형태로 동작하며, 마크다운 노트를 로컬에서 인덱싱 (토큰화나 임베딩) 하여 검색 가능하게 한다. 검색 방식도 다양하다. BM25, 벡터 검색, 그리고 LLM 과 BM25 + 벡터 검색을 조합한 하이브리드 검색까지 지원한다. 이 모든 과정이 로컬 모델을 기반으로 이루어진다.

사실 임베딩 정도야 openai 나 voyage ai 의 embedding model 을 사용하고 싶긴 했다. 비용이 들긴 하지만 임베딩은 비용이 그리 많이 들지도 않고 로컬 PC 성능도 타지 않으니까. 어찌되었든 로컬에서만 작동한다는 것은 보안이 중요한 회사 등에서 사용시 장점이 될 수 있다.

또한 QMD 는 마크다운에 특화되어 있기에 마크다운 친화적으로 청킹을 한다. 그러니까 문서 내용이 길어지면 쪼개가면서 임베딩을 진행한다. 그런데 정 없이 (?) 딱 정해진 토큰 수에서 문서를 자르면 문장 중간에 내용이 분리되는 문제가 있을 수 있다. 따라서 헤딩 등의 마크다운 요소들을 사용하여 마크다운 구조 내에서 청킹을 진행한다.

QMD 로는 Semantic Search 만 사용할 것

참고로 QMD 의 Lexical search 방식인 BM25 를 그대로 쓰면 한글의 경우 잘 맞지 않는다. 예를 들어서 안보위기 라는 이름의 노트가 있으면, 안보 로는 찾아지는데 위기 로는 찾아지지 않는다. 한국어의 경우 영어와 달리 띄어쓰기 없이 쓰는 경우가 있기 때문이다. 토큰화 과정 중 발생하는 문제인데 이를 해결하기 위한 kQMD 같은 프로젝트도 있다. (참고로 이 문제는 obsidian 플러그인인 omnisearch 에서도 발생한다.) 어쩌면 앞으로 QMD 에서 이 부분을 해결한 방식으로 지원할 수도 있겠지만.

상황이 이렇기에 나의 경우 lexical search 는 QMD 가 아닌 AI Agent 가 알아서 파일을 검색하도록 맡긴다. 즉 이 경우, claude code 가 Grep 이나 Glob 을 사용하게 된다. 의미 기반 검색이 필요할 경우에만 QMD 의 semantic search 를 사용한다. 이런 식의 하이브리드 검색이 어쩌면 비효율적으로 보일 수도 있겠으나, 당장은 동작하기에 그대로 사용한다. 사실 PKM 에서 노트는 정말 많아봐야 1 만개다.

QMD 사용하기

설치

qmd 설치는 https://github.com/tobi/qmd 를 참고하면 된다. Windows 사용자의 경우 조금 골치 아플 수도 있다.

Collection 추가하기

QMD 는 collection 이라는 단위로 마크다운 노트 모음을 관리한다. 따라서 QMD 에 옵시디언 볼트를 사용하려면 볼트 폴더를 collection 으로 추가할 필요가 있다. 터미널에서 옵시디언 볼트 폴더로 접근해서 qmd collection add . --name pkm 을 입력하여 볼트 폴더를 pkm 이란 이름의 콜렉션으로 추가한다.

임베딩 모델을 변경할 것

기본적으로 지정된 임베딩 모델은 embeddinggemma-300M-Q8_0 인데 이는 한국어 성능이 떨어지는 문제가 있다. 따라서 임베딩 모델을 바꿀 필요가 있다. Qwen3 임베딩 모델을 사용하면 되는데 다음과 같이 하면 된다.

export QMD_EMBED_MODEL="hf:Qwen/Qwen3-Embedding-0.6B-GGUF/Qwen3-Embedding-0.6B-Q8_0.gguf"
qmd embed -f

export 로 환경 변수를 변경하기 때문에 ~/.zshrc 등 사용하는 쉘이 실행될 때 자동으로 임베딩 모델을 지정하게 등록할 필요가 있다. 또한 embedding model 이 변경되었기 때문에 기존에 embeddinggemma 로 임베딩을 했다면 강제로 qmd embed -f 로 임베딩을 다시 진행할 필요가 있다. (무슨 말인지 모르겠다면 AI Agent 에게 요청하면 해준다.)

또한 추가된 collection 에 파일들이 변경되었다고 자동으로 임베딩이 업데이트되는 건 아니다. 따라서 cronjob 으로 collection 업데이트 후 임베딩을 해줄 필요가 있다. 내 경우 다음 명령어를 새벽 2 시 정도에 돌도록 설정했다. 파일 변경 시마다 임베딩까지 굳이 할 필요는 없어 보인다.

qmd update && qmd embed

잘 되었는지는 qmd status 로 확인할 수 있다.

로컬 임베딩 모델이므로 아무래도 성능이 의심될 수 있다. 관련 해서는 Qwen3-Embedding 0.6B 임베딩 성능 에서 다룬다.

Openclaw 의 메모리 엔진으로 QMD 사용하기

또한 openclaw 사용자의 경우 memory engine 을 qmd 로 사용할 수 있다. openclaw 는 대화 시에 몇 가지 대화 맥락을 memory 에 저장한다. 이는 마크다운 파일로 기록된다.

그런데 이를 의미 검색을 통해서 갖고 올 필요가 있다. 솔직히 대화 내용이 아직 적을 때는 필요 없다. 극단적으로 이 파일들을 통째로 AI 에게 넘길 수 있으니까. 그런데 내용 개수가 많아지면 필요한 것만 갖고 올 필요가 있고, 그럴 경우 의미를 바탕으로 갖고 올 필요가 있다. 이때 qmd 를 사용하면 되고 그 방법은 https://docs.openclaw.ai/concepts/memory-qmd 에 작성되어 있다. 성공적으로 연동한다면 qmd status 명령 시에 콜렉션으로 memory-root-main memory-alt-main memory-dir-main 이 추가된 것을 확인할 수 있다.

AI Agent 가 옵시디언 볼트의 QMD 사용하게 하기

QMD 는 MCP 를 지원한다. 따라서 QMD 로 임베딩한 내 노트를 claude code 등이 사용하게끔 하려면 이를 MCP 로 연동시켜줄 수 있다. 예컨대, 클로드 데스크탑의 경우 다음 설정으로 MCP 연동이 가능하다.

{
  "mcpServers": {
    "qmd": {
      "command": "qmd",
      "args": ["mcp"]
    }
  }
}

마무리

본격적인 로컬 LLM 의 시대가 오기는 좀 멀어도 임베딩 모델에 한해서는 빠른 시일 내로 로컬 PC 에서 동작시켜도 만족스러운 수준에 도달하지 않을까 싶다. QMD 는 과금이나 귀찮은 API 설정 필요 없이 내 옵시디언 볼트를 AI Agent 가 의미 기반으로 검색하게 해주는 좋은 도구다. 한 번 사용해보는 것을 추천한다.

Graph
Graph