Query Markup Documents (QMD)

Intro

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

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

검색의 종류와 Semantic Search

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

%%{init: {
  'theme': 'base',
  'themeVariables': {
    'fontFamily': '-apple-system, SF Pro Text, system-ui, sans-serif',
    'fontSize': '14px',
    'primaryColor': '#ffffff',
    'primaryTextColor': '#1a1a1a',
    'primaryBorderColor': '#1a1a1a',
    'lineColor': '#1a1a1a',
    'secondaryColor': '#f5f5f5',
    'tertiaryColor': '#fafafa'
  }
}}%%
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을 사용한다는 것은 다음과 같다.

%%{init: {
  'theme': 'base',
  'themeVariables': {
    'fontFamily': '-apple-system, SF Pro Text, system-ui, sans-serif',
    'fontSize': '14px',
    'primaryColor': '#ffffff',
    'primaryTextColor': '#1a1a1a',
    'primaryBorderColor': '#1a1a1a',
    'lineColor': '#1a1a1a',
    'secondaryColor': '#f5f5f5',
    'tertiaryColor': '#fafafa'
  }
}}%%
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로 확인할 수 있다.

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가 의미 기반으로 검색하게 해주는 좋은 도구다. 한 번 사용해보는 것을 추천한다.