jaysnote
11분

[LLM Wiki 15] QMD 소스 레벨 분석: 로컬 Markdown 검색 엔진의 내부 구조

QMD GitHub README와 소스를 기준으로 collection, SQLite store, FTS/BM25, sqlite-vec, query expansion, RRF, local reranker, MCP server가 어떻게 연결되는지 분석한다.

QMD 질의 파이프라인

QMD는 Markdown 문서를 대상으로 동작하는 로컬 검색 엔진이다. README만 보면 “BM25와 vector search, LLM rerank를 섞은 on-device search engine” 정도로 이해할 수 있다. 그런데 소스를 보면 역할이 더 분명해진다.

QMD는 LLM Wiki 전체를 구현한 제품이라기보다, LLM Wiki에 붙일 수 있는 로컬 검색 계층에 가깝다.

  • Markdown/Obsidian Vault는 지식 원본이다.
  • QMD는 그 파일을 색인하고 검색한다.
  • MCP server는 LLM agent가 QMD를 호출할 수 있게 한다.
  • Main LLM은 QMD가 찾아준 근거를 읽고 답을 만든다.

즉 QMD의 핵심 가치는 “내 로컬 Markdown 지식을 LLM이 잘 검색할 수 있는 형태로 바꿔주는 것”이다.

한 줄로 이해하기

QMD는 로컬 Markdown 폴더를 읽어서 SQLite에 색인하고, 검색 시 BM25, vector search, query expansion, RRF, rerank를 조합해 좋은 근거 문서를 찾아주는 엔진이다.

여기서 중요한 점은 QMD가 단순히 embedding 검색만 하는 도구가 아니라는 것이다. 실제 구현은 다음 세 가지를 함께 쓴다.

  • 정확한 단어 매칭: SQLite FTS5 기반 BM25
  • 의미 기반 검색: sqlite-vec 기반 vector search
  • 후보 재정렬: GGUF reranker 모델 기반 rerank

그래서 “정확한 키워드”, “의미가 비슷한 표현”, “질문과 후보의 실제 관련성”을 단계별로 다룬다.

소스 컴포넌트 구조

QMD 소스 컴포넌트 맵

소스 기준으로 보면 QMD는 크게 다섯 덩어리로 나뉜다.

첫째, CLI 계층이다. src/cli/qmd.ts가 사용자의 명령을 받는다. qmd collection, qmd context, qmd index, qmd embed, qmd search, qmd query, qmd mcp 같은 명령이 여기서 연결된다.

둘째, Store 계층이다. src/store.ts가 QMD의 핵심 엔진이다. SQLite 스키마 생성, 문서 저장, FTS 색인, vector 색인, hybrid query, query expansion, RRF, rerank blending이 대부분 이 파일에 모여 있다.

셋째, LLM 계층이다. src/llm.ts는 local GGUF 모델을 불러온다. embedding 모델, reranker 모델, query expansion 모델을 node-llama-cpp로 실행한다. 기본 모델도 코드에 지정되어 있다.

넷째, MCP 계층이다. src/mcp/server.ts는 QMD를 외부 LLM agent가 쓸 수 있는 도구로 노출한다. 주요 도구는 query, get, multi_get, status다.

다섯째, collection/config 계층이다. src/collections.ts와 설정 파일은 어떤 Markdown 폴더를 어떤 collection으로 다룰지 정의한다. 이 설정이 SQLite의 store_collections와 연결된다.

이 구조를 LLM Wiki에 대입하면 다음처럼 볼 수 있다.

  • CLI: 사람이 직접 실행하는 관리 도구
  • Store: 검색 엔진 본체
  • LLM: 로컬 모델 실행 계층
  • MCP: agent 연결 인터페이스
  • Collection: 지식 저장소 단위

색인 Flow

QMD 색인 파이프라인

QMD에서 색인은 Markdown 파일을 검색 가능한 데이터베이스로 바꾸는 과정이다.

먼저 사용자는 collection을 만든다. collection은 검색 대상 Markdown 폴더를 뜻한다. Obsidian으로 보면 Vault 전체나 Vault 안의 특정 폴더를 collection으로 볼 수 있다.

그 다음 qmd index가 파일을 읽는다. Markdown 본문, title, path, metadata가 SQLite에 저장된다. 소스에서는 문서 테이블, content 테이블, FTS 테이블, vector 테이블이 분리되어 있다.

검색용 색인은 두 갈래로 만들어진다.

  • FTS 색인: 단어 기반 검색을 위한 documents_fts
  • Vector 색인: embedding 기반 검색을 위한 vectors_vec

FTS는 BM25 검색에 쓰이고, vector 색인은 의미 검색에 쓰인다. 같은 Markdown 문서가 두 종류의 검색 구조로 들어가는 셈이다.

QMD가 흥미로운 부분은 context도 만든다는 점이다. README와 CLI를 보면 qmd context는 collection 구조를 사람이 읽기 좋은 Markdown 형태로 출력한다. 이것은 LLM agent에게 “어떤 collection이 있고 어떤 폴더 구조를 검색할 수 있는지” 알려주는 안내판 역할을 한다.

질의 Flow

사용자가 qmd query 또는 MCP query를 호출하면 단순히 vector search 한 번으로 끝나지 않는다. 소스의 hybridQuery() 흐름을 보면 다음 단계가 이어진다.

  1. 먼저 원 질문으로 BM25 probe를 한다.
  2. 검색 신호가 충분히 강하면 query expansion을 생략할 수 있다.
  3. 필요하면 로컬 LLM으로 검색용 query를 확장한다.
  4. 확장 query는 lexical, vector, HyDE 성격으로 나뉜다.
  5. lexical query는 FTS/BM25로, vector/HyDE query는 vector search로 보낸다.
  6. 여러 검색 결과를 RRF로 합친다.
  7. 문서를 chunk로 나누고 가장 관련 있는 chunk를 고른다.
  8. reranker가 query와 chunk를 다시 비교해 점수를 낸다.
  9. RRF 순위와 rerank 점수를 섞어 최종 결과를 만든다.

이 흐름의 장점은 검색 실패를 한 방식에 맡기지 않는다는 점이다. 정확한 단어가 있으면 BM25가 잡고, 표현이 달라도 의미가 비슷하면 vector search가 잡고, 마지막에는 reranker가 질문과 후보를 다시 대조한다.

README 그림보다 실제 구현이 더 정교한 지점

README의 아키텍처 그림은 전체 구조를 잘 보여준다. 하지만 소스를 보면 실제 검색 품질을 위해 들어간 세부 장치가 더 있다.

첫째, 원 질문을 더 강하게 본다. RRF에서 original query 기반 FTS와 vector 결과는 expansion query보다 더 높은 가중치를 받는다. 사용자의 원래 의도를 query expansion이 덮어쓰지 않게 하는 장치다.

둘째, 항상 expansion을 하지 않는다. BM25 probe에서 강한 신호가 잡히고 별도 intent가 없으면 expansion을 생략할 수 있다. 정확한 키워드 질문에서는 불필요한 확장이 오히려 노이즈가 될 수 있기 때문이다.

셋째, 전체 문서가 아니라 chunk를 rerank한다. 긴 Markdown 전체를 reranker에 넣으면 비용도 커지고 관련 없는 문단이 섞일 수 있다. QMD는 후보 문서에서 관련 chunk를 고른 뒤 rerank한다.

넷째, rerank 결과를 그대로 쓰지 않고 RRF 순위와 섞는다. 상위 후보일수록 기존 검색 순위를 더 믿고, 뒤쪽 후보일수록 reranker 점수의 영향이 커지도록 position-aware blending을 한다.

다섯째, rerank 결과를 캐시한다. query, model, chunk text 기준으로 점수를 저장해 같은 비교를 반복하지 않게 한다.

이런 세부 구현 때문에 QMD는 “BM25 + vector + rerank”라는 말보다 실제로는 더 조심스럽게 설계된 hybrid retrieval engine에 가깝다.

주요 개념 정리

BM25는 단어 기반 검색 점수다. 검색어가 문서에 얼마나 잘 등장하는지, 너무 흔한 단어인지, 문서 길이는 어떤지 등을 고려한다. 제품명, 사람 이름, 날짜, 에러 코드처럼 글자가 중요한 검색에 강하다.

Vector search는 문장이나 문서를 embedding 벡터로 바꿔 의미가 가까운 문서를 찾는 방식이다. “비용 부담”과 “too expensive”처럼 표현은 다르지만 의미가 가까운 문서를 찾을 수 있다.

HyDE는 Hypothetical Document Embeddings의 줄임말이다. 질문만 embedding하는 대신, 질문에 대한 가상의 답변 문서나 검색용 문장을 만든 뒤 그것을 vector search에 사용하는 방식이다.

RRF는 Reciprocal Rank Fusion이다. 여러 검색 결과 목록을 하나로 합칠 때 쓰는 방식이다. 각 검색기의 점수 스케일이 달라도 순위 기반으로 합칠 수 있어 BM25와 vector search를 섞기 좋다.

Rerank는 1차 검색 후보를 다시 읽고 질문에 더 맞는 순서로 재정렬하는 단계다. QMD에서는 로컬 reranker GGUF 모델이 query와 chunk를 함께 보고 관련성 점수를 낸다.

GGUF는 로컬 LLM 실행에 많이 쓰이는 모델 파일 포맷이다. QMD는 embedding, rerank, query expansion 모델을 GGUF로 내려받아 node-llama-cpp를 통해 실행한다.

MCP는 Model Context Protocol이다. QMD 입장에서는 CLI 도구를 넘어, Claude나 다른 agent가 “검색 도구”처럼 QMD를 호출할 수 있게 하는 연결 규격이다.

MCP로 붙으면 무엇이 달라지나

CLI에서 QMD를 쓰면 사람이 직접 검색한다. MCP로 붙이면 LLM agent가 직접 검색한다.

MCP server는 query, get, multi_get, status 같은 도구를 제공한다. agent는 먼저 query로 관련 문서를 찾고, 필요하면 get이나 multi_get으로 원문을 가져온다. status는 현재 collection과 인덱스 상태를 확인하는 데 쓴다.

README에서 HTTP MCP server를 권장하는 이유도 실용적이다. 매번 stdio 프로세스를 새로 띄우면 로컬 모델 로딩 비용이 반복될 수 있다. HTTP server로 띄워두면 모델을 메모리나 VRAM에 유지한 채 여러 요청을 처리하기 쉽다.

LLM Wiki 관점에서는 이 부분이 중요하다. 지식 저장소가 아무리 좋아도 LLM이 안정적으로 호출할 수 있는 도구 형태가 아니면 워크플로우에 붙이기 어렵다. QMD의 MCP 계층은 로컬 Markdown 지식을 agent의 검색 도구로 바꿔주는 접점이다.

QMD와 LLM Wiki의 차이

QMD는 LLM Wiki의 한 구성 요소로 볼 수 있지만, LLM Wiki 전체를 대신하지는 않는다.

LLM Wiki는 보통 다음 요소를 함께 포함한다.

  • 지식 작성: Markdown, Obsidian, Git
  • 구조화: 링크, 태그, properties, 폴더 규칙
  • 검색: BM25, vector search, rerank
  • 실행: agent, MCP, workflow
  • 운영: 권한, 배포, 변경 이력, 품질 관리

QMD가 강하게 담당하는 부분은 검색과 agent 연결이다. Markdown을 작성하거나 지식 그래프를 사람이 보는 UI, 팀 권한 관리, 승인 워크플로우, 문서 생명주기 관리는 QMD의 중심 범위가 아니다.

따라서 “QMD가 LLM Wiki를 구현했나?”라고 묻는다면 답은 이렇게 정리하는 편이 정확하다.

QMD는 LLM Wiki의 검색 엔진 계층을 꽤 잘 구현한 도구다. 하지만 LLM Wiki 전체 시스템을 구현한 것은 아니다. Obsidian, Git, MCP agent, 운영 규칙과 함께 붙였을 때 LLM Wiki 아키텍처의 중요한 부품이 된다.

한계와 주의점

첫째, 1차 후보에 못 들어온 문서는 rerank가 살릴 수 없다. Rerank는 후보를 다시 줄 세우는 단계이지, 전체 문서를 새로 찾는 단계가 아니다. 그래서 BM25와 vector search의 후보 생성 품질이 여전히 중요하다.

둘째, 로컬 모델 품질과 리소스의 영향을 받는다. GGUF 모델을 로컬에서 돌리기 때문에 다운로드, 메모리, VRAM, CPU/GPU 성능에 따라 속도와 품질이 달라진다.

셋째, Markdown 품질이 검색 품질을 좌우한다. 제목, 섹션, 링크, 용어 정리가 엉망이면 아무리 좋은 검색 엔진을 붙여도 결과 품질이 흔들린다.

넷째, QMD는 검색 엔진이지 지식 운영 체계가 아니다. 어떤 문서를 언제 갱신할지, 신뢰할 수 있는 문서와 낡은 문서를 어떻게 구분할지, 팀 문서 권한을 어떻게 나눌지는 별도로 설계해야 한다.

정리

QMD의 소스를 보면 설계 방향이 명확하다. “로컬 Markdown을 그냥 embedding해서 찾자”가 아니라, 사람이 쓴 문서를 agent가 안정적으로 검색할 수 있게 여러 검색 방식을 조합한다.

좋은 점은 현실적이라는 것이다. 정확한 단어는 BM25로 잡고, 의미 유사도는 vector search로 잡고, 섞인 후보는 RRF로 합치고, 마지막 품질은 reranker로 보정한다. 그리고 MCP로 agent가 호출할 수 있게 만든다.

LLM Wiki를 만들 때 QMD는 매우 적합한 검색 계층이다. 다만 QMD만으로 전체 LLM Wiki가 완성되는 것은 아니다. Obsidian 같은 작성 환경, Git 기반 변경 관리, 문서 구조 규칙, agent workflow를 함께 붙였을 때 QMD의 가치가 가장 잘 드러난다.

관련 글

← 목록으로