ko-qmd 2.8.3-ko.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/CHANGELOG.md +1937 -0
  2. package/LICENSE +21 -0
  3. package/README.en.md +184 -0
  4. package/README.md +292 -0
  5. package/bin/qmd +192 -0
  6. package/dist/ast.d.ts +65 -0
  7. package/dist/ast.js +51 -0
  8. package/dist/bench/bench.d.ts +41 -0
  9. package/dist/bench/bench.js +12 -0
  10. package/dist/bench/score.d.ts +33 -0
  11. package/dist/bench/score.js +1 -0
  12. package/dist/bench/types.d.ts +101 -0
  13. package/dist/bench/types.js +1 -0
  14. package/dist/cli/build-info.json +4 -0
  15. package/dist/cli/embed-lock.d.ts +24 -0
  16. package/dist/cli/embed-lock.js +2 -0
  17. package/dist/cli/formatter.d.ts +120 -0
  18. package/dist/cli/formatter.js +60 -0
  19. package/dist/cli/mcp-pid.d.ts +33 -0
  20. package/dist/cli/mcp-pid.js +1 -0
  21. package/dist/cli/qmd.d.ts +101 -0
  22. package/dist/cli/qmd.js +165 -0
  23. package/dist/cli/version.d.ts +42 -0
  24. package/dist/cli/version.js +1 -0
  25. package/dist/collections.d.ts +162 -0
  26. package/dist/collections.js +1 -0
  27. package/dist/db.d.ts +63 -0
  28. package/dist/db.js +2 -0
  29. package/dist/filters.d.ts +46 -0
  30. package/dist/filters.js +1 -0
  31. package/dist/grep.d.ts +85 -0
  32. package/dist/grep.js +9 -0
  33. package/dist/hangul.d.ts +102 -0
  34. package/dist/hangul.js +1 -0
  35. package/dist/index.d.ts +270 -0
  36. package/dist/index.js +1 -0
  37. package/dist/llm.d.ts +584 -0
  38. package/dist/llm.js +40 -0
  39. package/dist/maintenance.d.ts +33 -0
  40. package/dist/maintenance.js +1 -0
  41. package/dist/mcp/daemon-health.d.ts +17 -0
  42. package/dist/mcp/daemon-health.js +1 -0
  43. package/dist/mcp/origin-guard.d.ts +67 -0
  44. package/dist/mcp/origin-guard.js +1 -0
  45. package/dist/mcp/server.d.ts +131 -0
  46. package/dist/mcp/server.js +35 -0
  47. package/dist/memory.d.ts +74 -0
  48. package/dist/memory.js +13 -0
  49. package/dist/metadata-filter.d.ts +74 -0
  50. package/dist/metadata-filter.js +1 -0
  51. package/dist/metadata-store.d.ts +45 -0
  52. package/dist/metadata-store.js +68 -0
  53. package/dist/metadata.d.ts +61 -0
  54. package/dist/metadata.js +1 -0
  55. package/dist/paths.d.ts +1 -0
  56. package/dist/paths.js +1 -0
  57. package/dist/query-log.d.ts +156 -0
  58. package/dist/query-log.js +2 -0
  59. package/dist/refresh.d.ts +60 -0
  60. package/dist/refresh.js +1 -0
  61. package/dist/rerank-input.d.ts +46 -0
  62. package/dist/rerank-input.js +5 -0
  63. package/dist/store.d.ts +1267 -0
  64. package/dist/store.js +421 -0
  65. package/dist/trust.d.ts +152 -0
  66. package/dist/trust.js +2 -0
  67. package/package.json +124 -0
  68. package/scripts/build.mjs +130 -0
  69. package/scripts/check-package-grammars.mjs +29 -0
  70. package/scripts/package-smoke.mjs +65 -0
  71. package/scripts/test-all.mjs +42 -0
  72. package/skills/qmd/SKILL.md +333 -0
  73. package/skills/qmd/references/mcp-setup.md +122 -0
  74. package/skills/release/SKILL.md +141 -0
  75. package/skills/release/scripts/install-hooks.sh +38 -0
  76. package/skills/release/scripts/release-context.sh +129 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024-2026 Tobi Lutke
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.en.md ADDED
@@ -0,0 +1,184 @@
1
+ # ko-qmd — local hybrid search for Korean Markdown notes
2
+
3
+ [![npm](https://img.shields.io/npm/v/ko-qmd?label=npm%20ko-qmd)](https://www.npmjs.com/package/ko-qmd)
4
+ [![license: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
5
+
6
+ [한국어](README.md) · **English**
7
+
8
+ **ko-qmd is an on-device search engine for Korean Markdown notes — Obsidian vaults, wikis, meeting
9
+ notes and docs folders — that finds them by keyword or by natural-language question.**
10
+ It combines BM25 full-text search, vector search and LLM reranking, and matches Korean query words
11
+ by their stem even when a particle or verb ending is attached. Every model runs locally through
12
+ [node-llama-cpp](https://github.com/withcatai/node-llama-cpp) (GGUF, no network calls), so your notes
13
+ never leave the machine. AI agents such as Claude Code and Claude Desktop use it through the CLI, an
14
+ MCP server, a REST endpoint or the TypeScript SDK.
15
+ It is the Korean distribution of [tobi/qmd](https://github.com/tobi/qmd) (MIT).
16
+
17
+ - Package: `ko-qmd` on npm. The binary and MCP server keep the upstream name, `qmd`
18
+ - Version: `<upstream version>-ko.N` (e.g. `2.8.3-ko.5`)
19
+ - Full reference (every CLI option, MCP tool parameters, SDK API, scoring): [docs/REFERENCE.md](docs/REFERENCE.md)
20
+ - Changes: [CHANGELOG.md](CHANGELOG.md) · [FAQ](#faq)
21
+
22
+ ```mermaid
23
+ flowchart LR
24
+ Q[Query] --> H[Korean handling<br/>particle/ending strip · loanword bridge]
25
+ H --> FTS[BM25<br/>FTS5 + syllable bigrams]
26
+ Q --> VS[Vector search<br/>Qwen3-Embedding]
27
+ FTS --> RRF[RRF fusion]
28
+ VS --> RRF
29
+ RRF --> RR[Rerank<br/>Qwen3-Reranker]
30
+ RR --> OUT[Results]
31
+ ```
32
+
33
+ ## What differs from upstream qmd
34
+
35
+ ### Korean keyword search (lex)
36
+
37
+ - **Particle and ending stripping** — an unquoted Hangul word also matches its stem: one trailing particle
38
+ (`검색을` → `검색`), a two-particle chain (`청킹에서의` → `청킹`), verb and nominalizing endings
39
+ (`토큰화하는` → `토큰화`, `검색하기` → `검색`), `한`/`된` endings (`필요한` → `필요`) and the 하↔해
40
+ contraction. Quoted phrases, Hanja and kana are left as written.
41
+ - **Loanword bridge** — about 90 built-in Hangul spellings of technical terms (`서치`, `웹`, `코어`,
42
+ `마이그레이션`, …) also search their English spelling, so `하이브리드 서치` finds a `hybrid-search` note.
43
+ Only the query changes; no re-index is needed.
44
+ - **Mixed-script token split** — `SKILL.md계약의핵심` becomes `skill` AND `md` AND `계약의핵심`.
45
+ - **Long-query relaxation** — a query of three or more words that matches nothing with AND is retried with
46
+ OR, so a natural-language sentence never returns zero hits.
47
+ - **Syllable-bigram index** — Hangul runs are indexed as syllable bigrams too, so a word inside a
48
+ compound written without spaces still matches.
49
+ - **Negation strip** — "X 말고 Y" ("Y, not X") searches for Y alone.
50
+
51
+ ### Models and chunking
52
+
53
+ | Role | Default model | Upstream |
54
+ |---|---|---|
55
+ | Embedding | `Qwen3-Embedding-0.6B` (Q8_0) | embeddinggemma-300M |
56
+ | Rerank | `Qwen3-Reranker-0.6B` (Q8_0) | same |
57
+ | Query expansion | `qmd-query-expansion-1.7B` | same |
58
+
59
+ - Korean documents are chunked by their real token budget: Hangul costs far more tokens per character
60
+ than English (measured on Qwen3-Embedding: 1.64 chars/token for Korean, 6.08 for English). English
61
+ chunk boundaries match upstream.
62
+ - Hybrid RRF weights are tuned for Korean, and a query containing Hangul skips LLM query expansion on
63
+ the hybrid path (English queries are still expanded).
64
+ - The rerank blend lets the reranker actually change the top result, and the reranker is shown the
65
+ chunk that best matches the query's Hangul syllable bigrams, led by the note title.
66
+
67
+ ### Scope, tools and daemon
68
+
69
+ - **Path and time scope** — CLI `--path` / `--since` / `--until`, the same fields over REST and MCP,
70
+ `scope` in the SDK. Ranking happens inside the filtered corpus.
71
+ - **`qmd grep`** — exact string or regex match over indexed bodies, for spellings the tokenizer never
72
+ produced.
73
+ - **Daemon** — re-indexes changed files before a search (once per collection every 30 s), optional query
74
+ log (`QMD_QUERY_LOG=1`), raw `vec_score` / `fts_score` with `rerank:false`, and a per-document
75
+ rerank token cap (`QMD_RERANK_MAX_DOC_TOKENS`, or `rerankMaxDocTokens` per request).
76
+
77
+ ## Install
78
+
79
+ ```sh
80
+ npm install -g ko-qmd # or pin: ko-qmd@2.8.3-ko.5
81
+ ```
82
+
83
+ - It cannot sit beside upstream `@tobilu/qmd` (same binary name): `npm uninstall -g @tobilu/qmd` first.
84
+ - Requires Node.js 22+ or Bun; on macOS, Homebrew SQLite (for extension loading). Models download to
85
+ `~/.cache/qmd/models/` on first use.
86
+
87
+ ## Quick start
88
+
89
+ ```sh
90
+ qmd collection add ~/notes --name notes # register and index a folder
91
+ qmd context add qmd://notes "Personal dev notes" # description returned with results
92
+ qmd embed # vector embeddings (first time, then changes only)
93
+
94
+ qmd query "하이브리드 서치 설계 결정" # fusion + rerank (recommended)
95
+ qmd search "리랭크" # BM25 only (fast, no model)
96
+ qmd vsearch "검색 지연을 줄인 방법" # vector only
97
+ qmd grep "QMD_QUERY_LOG" # exact body match
98
+ qmd get "#abc123" # fetch by docid
99
+ ```
100
+
101
+ Common options: `-c <collection>` · `-n <count>` · `--min-score <score>` · `--full` ·
102
+ `--intent "<what you are after>"` · `--no-rerank` · `--format json|csv|md|xml|files`.
103
+
104
+ ## Use from AI agents
105
+
106
+ ```sh
107
+ qmd mcp # stdio MCP server (Claude Desktop, Claude Code, …)
108
+ qmd mcp --http # HTTP on port 8181 — models load once, sessions share them
109
+ qmd mcp --http --daemon # background; stop with qmd mcp stop
110
+ ```
111
+
112
+ The HTTP daemon serves `POST /mcp` (MCP Streamable HTTP), `POST /query` (REST) and `GET /health`.
113
+
114
+ ```sh
115
+ curl -s http://127.0.0.1:8181/query -H 'Content-Type: application/json' -d '{
116
+ "searches": [{"type": "vec", "query": "리랭크 지연"}, {"type": "lex", "query": "리랭크 지연"}],
117
+ "collections": ["notes"], "limit": 10
118
+ }'
119
+ ```
120
+
121
+ ```ts
122
+ const { createStore } = await import('ko-qmd') // ESM only — require() does not work
123
+
124
+ const store = await createStore({ dbPath: './index.sqlite', configPath: `${process.env.HOME}/.config/qmd/index.yml` })
125
+ const results = await store.search({
126
+ queries: [{ type: 'vec', query: '리랭크 지연' }, { type: 'lex', query: '리랭크 지연' }],
127
+ collection: 'notes', limit: 10,
128
+ })
129
+ await store.close()
130
+ ```
131
+
132
+ Tool list, request fields and SDK API: [docs/REFERENCE.md](docs/REFERENCE.md). Search guidance for
133
+ agents, including a section on Korean queries: [skills/qmd/SKILL.md](skills/qmd/SKILL.md).
134
+
135
+ ## Benchmark
136
+
137
+ `bash scripts/bench-ko.sh` runs the `test/fixtures/ko-vault/` fixture (97 documents, 123 queries).
138
+ On its first 52 queries, bm25 recall@5 went from 0.6250 on upstream 2.8.3 to 0.9519 in this
139
+ distribution. Every run's numbers are in [BASELINE.md](test/fixtures/ko-vault/BASELINE.md).
140
+
141
+ ## FAQ
142
+
143
+ ### What is ko-qmd?
144
+ A CLI, MCP server and SDK that search Korean Markdown notes on your own machine. It adds Korean
145
+ particle and ending handling, a Hangul syllable-bigram index, a Korean-friendly default embedding
146
+ model (Qwen3-Embedding) and tuned fusion weights on top of upstream qmd.
147
+
148
+ ### Why not use upstream qmd for Korean?
149
+ Upstream BM25 treats a word with a particle attached (`검색을`, `청킹에서의`) as a different token from
150
+ its stem, so it misses notes that say `검색`. On the ko-vault benchmark (52 queries) bm25 recall@5 is
151
+ 0.6250 upstream and 0.9519 here.
152
+
153
+ ### Can it search an Obsidian vault?
154
+ Yes. Register the vault folder with `qmd collection add <vault path> --name <name>` and it indexes the
155
+ `*.md` files inside. It only reads your files.
156
+
157
+ ### Does it need internet access or an API key?
158
+ No. The embedding, rerank and query-expansion models download once on first use and run offline after
159
+ that.
160
+
161
+ ### How do I connect it to Claude Code or another agent?
162
+ Start the MCP server with `qmd mcp` (stdio) or `qmd mcp --http --daemon` (HTTP, shared models). See
163
+ [Use from AI agents](#use-from-ai-agents).
164
+
165
+ ### Does it work for English notes too?
166
+ Yes. English queries and documents behave as upstream: same chunk boundaries, same query expansion.
167
+ Korean and English notes can live in one index.
168
+
169
+ ## Development
170
+
171
+ ```sh
172
+ bun install
173
+ bun src/cli/qmd.ts <command> # run from source
174
+ bun run lint && bun run test:types
175
+ npx vitest run --reporter=verbose test/
176
+ npm run build # dist/ — never `bun build --compile` (it breaks sqlite-vec)
177
+ ```
178
+
179
+ `develop` is the working branch (PRs go there); `main` is the release branch. Release, dogfood and
180
+ upstream-sync procedures are documented in Korean in [README.md](README.md#개발).
181
+
182
+ ## License
183
+
184
+ MIT. Original copyright Tobi Lutke ([LICENSE](LICENSE)); this distribution uses the same license.
package/README.md ADDED
@@ -0,0 +1,292 @@
1
+ # ko-qmd — 한국어 마크다운 노트를 위한 로컬 하이브리드 검색 엔진
2
+
3
+ [![npm](https://img.shields.io/npm/v/ko-qmd?label=npm%20ko-qmd)](https://www.npmjs.com/package/ko-qmd)
4
+ [![license: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
5
+
6
+ **한국어** · [English](README.en.md)
7
+
8
+ **ko-qmd 는 한국어 마크다운 노트(Obsidian 볼트·위키·회의록·문서 폴더)를 키워드와 자연어로 찾는 로컬 검색 엔진이다.**
9
+ BM25 전문 검색, 벡터 검색, LLM 리랭크를 섞어 쓰고, 조사·어미가 붙은 한국어 질의도 어간으로 찾는다.
10
+ 모든 모델이 [node-llama-cpp](https://github.com/withcatai/node-llama-cpp) 로 내 컴퓨터에서 돌아 노트가 밖으로 나가지 않는다(GGUF, 네트워크 호출 없음).
11
+ AI 에이전트(Claude Code·Claude Desktop 등)는 CLI·MCP 서버·REST·TypeScript SDK 로 쓴다.
12
+ [tobi/qmd](https://github.com/tobi/qmd)(MIT)의 한국어 배포판이다.
13
+
14
+ - 패키지: `ko-qmd` (npm). 실행 파일과 MCP 서버 이름은 업스트림과 같은 `qmd`
15
+ - 버전: `<업스트림 버전>-ko.N` (예: `2.8.3-ko.5`)
16
+ - 영어 레퍼런스(전체 CLI 옵션·MCP 툴 파라미터·SDK API·점수 계산): [docs/REFERENCE.md](docs/REFERENCE.md)
17
+ - 변경 이력: [CHANGELOG.md](CHANGELOG.md) · 자주 묻는 질문: [FAQ](#자주-묻는-질문)
18
+
19
+ ```mermaid
20
+ flowchart LR
21
+ Q[질의] --> H[한글 처리<br/>조사·어미 제거 · 외래어 다리]
22
+ H --> FTS[BM25<br/>FTS5 + 음절 bigram]
23
+ Q --> VS[벡터 검색<br/>Qwen3-Embedding]
24
+ FTS --> RRF[RRF 융합]
25
+ VS --> RRF
26
+ RRF --> RR[리랭크<br/>Qwen3-Reranker]
27
+ RR --> OUT[결과]
28
+ ```
29
+
30
+ ## 업스트림과 다른 점
31
+
32
+ ### 한국어 키워드 검색 (lex)
33
+
34
+ - **조사·어미 제거** — 따옴표 없는 한글 단어는 어간도 매칭한다. 끝 조사 하나(`검색을` → `검색`), 두 조사 연쇄(`청킹에서의` → `청킹`),
35
+ 동사·명사화 어미(`토큰화하는` → `토큰화`, `검색하기` → `검색`), `한`/`된` 어미(`필요한` → `필요`), 하↔해 축약(`더하` ↔ `더해`).
36
+ 따옴표 구문·한자·가나는 그대로 둔다.
37
+ - **외래어 다리** — 기술 용어의 한글 표기(`서치`·`웹`·`코어`·`마이그레이션` 등 약 90개 내장 표)는 영문 표기도 함께 찾는다.
38
+ `하이브리드 서치`가 `hybrid-search` 노트를, `리액트 웹 코어`가 `react-web-core`를 찾는다. 질의 쪽만 바뀌므로 재색인은 필요 없다.
39
+ - **문자 체계가 섞인 토큰 분리** — `SKILL.md계약의핵심`은 `skill` AND `md` AND `계약의핵심`으로 나눈다.
40
+ - **긴 질의 완화** — 세 단어 이상 질의가 AND 로 한 건도 안 맞으면 같은 단어를 OR 로 다시 찾는다. 자연어 문장 질의가 0건이 되지 않는다.
41
+ - **음절 bigram 색인** — 한글 구간의 음절 bigram 을 FTS 필드에 덧붙인다. 붙여 쓴 복합어 안의 단어도 걸린다.
42
+ 기존 인덱스는 처음 열 때 FTS 를 한 번 다시 만든다.
43
+ - **부정 절 제거** — "X 말고 Y" 꼴 질의는 Y 만 찾는다(`말고`·`빼고`·`제외하고`·`제외한`).
44
+
45
+ ### 기본 모델과 청킹
46
+
47
+ | 역할 | 기본 모델 | 업스트림 |
48
+ |---|---|---|
49
+ | 임베딩 | `Qwen3-Embedding-0.6B` (Q8_0) | embeddinggemma-300M |
50
+ | 리랭크 | `Qwen3-Reranker-0.6B` (Q8_0) | 같음 |
51
+ | 질의 확장 | `qmd-query-expansion-1.7B` | 같음 |
52
+
53
+ - 한국어 문서는 실제 토큰 예산으로 청킹한다. 한글은 글자당 토큰이 영어보다 훨씬 많다(Qwen3-Embedding 실측 ko 1.64 / en 6.08 chars/token).
54
+ 그래서 청크 크기는 한글·기타 문자의 조화 평균으로 잡는다. 영어 문서의 청크 경계는 업스트림과 같다.
55
+ - 하이브리드 RRF 가중치를 한국어에 맞게 조정했다. 벡터 목록은 절반, OR 로 완화한 lex 목록은 다시 절반, 확장 질의 목록은 0.75 로 센다.
56
+ - `qmd query`·MCP plain `query`·SDK `search({ query })` 의 하이브리드 경로에서 한글이 한 글자라도 든 질의는 LLM 질의
57
+ 확장을 하지 않는다(명시적 `expand:` 포함, `qmd vsearch` 는 아직 확장한다). 확장 모델은 한국어 질문에 영어 문장 틀과 중국어를 섞어 썼고,
58
+ 그 샘플링만으로 어려운 질의 1위가 실행마다 30개 중 5개까지 바뀌었다. 끄자 어려운 질의 1위는 줄지 않았고 hybrid 지연은
59
+ 질의당 약 1초에서 수십 ms 로 줄었다(BASELINE.md 2026-09-26). 영어 질의는 그대로 확장한다.
60
+ - 리랭커가 실제로 1위를 바꿀 수 있게 점수 블렌드를 고쳤다. 검색 순위 항은 블렌드의 10% 인 선형 감쇠다.
61
+ 이전 블렌드에서는 리랭크 점수와 상관없이 검색 1위가 그대로 남았다.
62
+ - 리랭커에는 질의의 한글 음절 bigram 이 가장 많이 겹치는 청크를 보내고, 청크에 노트 제목이 없으면 제목을 앞에 붙인다.
63
+
64
+ ### 검색 범위와 도구
65
+
66
+ - **경로·시간 범위** — CLI `--path`·`--since`·`--until`, REST·MCP `path`·`since`·`until`, SDK `scope`.
67
+ 걸러진 코퍼스 안에서 순위를 매긴다(전역 top-K 를 뽑은 뒤 거르는 방식이 아니다). metadata 필터(`filter`)와 같이 주면 AND 다.
68
+ - **`qmd grep`** — 색인된 본문을 문자열이나 정규식으로 정확히 찾는다(순위 없음). 토크나이저가 못 만든 한국어 표기를 찾을 때 쓴다.
69
+ - **`limit` 과 검색 폭 분리** — `limit` 은 돌려줄 개수만 정한다. 검색은 최소 20 문서를 본다.
70
+
71
+ ### 데몬(HTTP MCP 서버) 기능
72
+
73
+ - **색인 자동 갱신** — 검색 전에 대상 컬렉션을 확인하고(컬렉션당 30초에 한 번) 바뀐 파일을 다시 색인한다.
74
+ 데몬이 도는 중에 쓴 노트가 `qmd update` 없이 다음 검색에서 키워드로 잡힌다. 벡터는 `qmd embed` 뒤에 반영된다.
75
+ - **질의 로그** — `QMD_QUERY_LOG=1` 이면 REST `/query` 와 HTTP MCP `query` 호출마다 `~/.cache/qmd/queries-YYYY-MM.jsonl` 에 한 줄을 남긴다
76
+ (결과 경로·점수·소요 시간, 스니펫 없음). 헤더 `X-QMD-Tag`·`X-QMD-Qid`·`X-QMD-Role`·`X-QMD-No-Log` 로 행에 표시를 단다.
77
+ 계약: [docs/REFERENCE.md § Query log](docs/REFERENCE.md#query-log-ko-qmd).
78
+ - **원시 점수** — `rerank:false` 응답의 결과마다 `vec_score`(코사인 유사도)·`fts_score`(정규화 BM25)가 붙는다.
79
+ 그 경로의 `score` 는 1/순위라 임계값을 걸 수 없기 때문이다.
80
+ - **모델 상주** — `QMD_LLM_IDLE_TIMEOUT_MS=0` 이면 모델을 내리지 않는다. 기본은 5분 idle 뒤 내린다.
81
+ - **리랭크 입력 cap** — 리랭커에 보내는 문서당 토큰을 자른다. 데몬 전체는 `QMD_RERANK_MAX_DOC_TOKENS`,
82
+ 요청 하나는 REST `rerankMaxDocTokens` 로 건다(요청 값이 우선). KB 골드셋에서 128토큰 cap 은 품질을 지키면서
83
+ 콜드 p90 을 3.6s → 0.9s 로 줄였다(Apple M3 Max, 후보 15).
84
+
85
+ ## 설치
86
+
87
+ ```sh
88
+ npm install -g ko-qmd # 또는 버전 핀: ko-qmd@2.8.3-ko.5
89
+ ```
90
+
91
+ - 업스트림 `@tobilu/qmd` 와 실행 파일 이름이 같아 함께 둘 수 없다. `npm uninstall -g @tobilu/qmd` 뒤 설치한다.
92
+ - 버전이 prerelease(`-ko.N`)라 `^`·`~` 범위는 같은 `major.minor.patch` 의 `-ko.*` 안에서만 움직인다. 앱에서는 정확한 버전으로 핀한다.
93
+ - 미발행 커밋은 `npm i github:tak-bro/ko-qmd#<sha>` 로 받는다. `prepare` 가 `tsc` 로 `dist/` 를 만든다. 설치에는 node 와 tsc 만 필요하다.
94
+ - 요구 사항: Node.js 22+ 또는 Bun, macOS 는 Homebrew SQLite(확장 로드용). 모델은 첫 사용 때 `~/.cache/qmd/models/` 로 내려받는다.
95
+
96
+ ## 빠른 시작
97
+
98
+ ```sh
99
+ qmd collection add ~/notes --name notes # 컬렉션 등록 + 색인
100
+ qmd context add qmd://notes "개인 개발 노트" # 검색 결과와 함께 돌려줄 설명
101
+ qmd embed # 벡터 임베딩 (처음 한 번, 이후 변경분만)
102
+
103
+ qmd query "하이브리드 서치 설계 결정" # 융합 + 리랭크 (권장, 한글 질의는 확장하지 않는다)
104
+ qmd search "리랭크" # BM25 만 (빠름, 모델 없음)
105
+ qmd vsearch "검색 지연을 줄인 방법" # 벡터만
106
+ qmd grep "QMD_QUERY_LOG" # 본문 정확 일치
107
+
108
+ qmd get "#abc123" # docid 로 문서 가져오기
109
+ qmd get notes/plan.md:100:40 # 100행부터 40행
110
+ ```
111
+
112
+ 자주 쓰는 옵션: `-c <컬렉션>` · `-n <개수>` · `--min-score <점수>` · `--full` · `--intent "<찾는 것>"` · `--no-rerank` ·
113
+ `--format json|csv|md|xml|files` · `--path`·`--since`·`--until`.
114
+ 전체 명령은 `qmd --help` 와 [docs/REFERENCE.md § Usage](docs/REFERENCE.md#usage).
115
+
116
+ 컬렉션·모델 설정은 `~/.config/qmd/index.yml` 에 둔다. 프로젝트별 인덱스는 `qmd init` 으로 `.qmd/` 를 만든다.
117
+ 기본 임베딩을 바꾸면 벡터가 호환되지 않으므로 `qmd embed -f` 로 전부 다시 임베딩한다.
118
+
119
+ ## 에이전트에서 쓰기
120
+
121
+ ### MCP 서버
122
+
123
+ ```sh
124
+ qmd mcp # stdio (Claude Desktop·Claude Code 등)
125
+ qmd mcp --http # HTTP, 기본 포트 8181 — 모델을 한 번만 올리고 여러 세션이 공유
126
+ qmd mcp --http --daemon # 백그라운드, 끄기는 qmd mcp stop
127
+ ```
128
+
129
+ HTTP 데몬은 `POST /mcp`(MCP Streamable HTTP), `POST /query`(REST), `GET /health` 를 연다.
130
+ 툴 목록과 파라미터는 [docs/REFERENCE.md § MCP Server](docs/REFERENCE.md#mcp-server).
131
+ 에이전트용 검색 지침은 스킬 `skills/qmd/SKILL.md` 에 있다. 한국어 질의 절이 따로 있다: `lex:` 에는 어간·영문 용어, `vec:` 에는 한국어 패러프레이즈.
132
+
133
+ ### REST `/query`
134
+
135
+ ```sh
136
+ curl -s http://127.0.0.1:8181/query -H 'Content-Type: application/json' -d '{
137
+ "searches": [{"type": "vec", "query": "리랭크 지연"}, {"type": "lex", "query": "리랭크 지연"}],
138
+ "collections": ["notes"], "limit": 10,
139
+ "rerank": true, "candidateLimit": 15, "rerankMaxDocTokens": 128
140
+ }'
141
+ ```
142
+
143
+ | 필드 | 뜻 |
144
+ |---|---|
145
+ | `searches` | `{type: lex\|vec\|hyde, query}` 목록 (필수). 질의 확장은 하지 않는다 |
146
+ | `collections` · `limit` · `minScore` | 범위·개수·최소 점수 |
147
+ | `rerank` · `candidateLimit` | 리랭크 여부(기본 켜짐)·리랭크할 후보 수(기본 40) |
148
+ | `rerankMaxDocTokens` | 이 요청의 리랭크 문서당 토큰 cap. 양의 정수만 받고 나머지는 무시 |
149
+ | `path` · `since` · `until` · `filter` · `intent` | 경로·시간 범위, metadata 필터, 의도 힌트 |
150
+
151
+ ### SDK
152
+
153
+ ```ts
154
+ const { createStore } = await import('ko-qmd') // ESM 전용 — require() 는 안 된다
155
+
156
+ const store = await createStore({ dbPath: './index.sqlite', configPath: `${process.env.HOME}/.config/qmd/index.yml` })
157
+ const results = await store.search({
158
+ queries: [{ type: 'vec', query: '리랭크 지연' }, { type: 'lex', query: '리랭크 지연' }],
159
+ collection: 'notes', limit: 10, rerank: true, candidateLimit: 15, rerankMaxDocTokens: 128,
160
+ })
161
+ await store.close()
162
+ ```
163
+
164
+ API 전체: [docs/REFERENCE.md § SDK / Library Usage](docs/REFERENCE.md#sdk--library-usage).
165
+
166
+ ## 환경 변수
167
+
168
+ | 변수 | 효과 |
169
+ |---|---|
170
+ | `QMD_QUERY_LOG` | `1`/`true`/`yes` 면 HTTP 데몬이 검색마다 질의 로그를 남긴다 |
171
+ | `QMD_LLM_IDLE_TIMEOUT_MS` | 모델을 내리기까지 idle ms (`0` = 내리지 않음) |
172
+ | `QMD_LLM_LOAD_TIMEOUT_MS` | 모델 로드 대기의 상한 ms (기본 120000, `0` = 무한 대기). 넘으면 그 단계 없이 답하고 로드는 백그라운드에서 계속된다 |
173
+ | `QMD_RERANK_CASCADE` | `0`/`false`/`off`/`no` 면 리랭크 cascade를 끄고 후보 전체를 한 번에 채점한다 |
174
+ | `QMD_RERANK_MAX_DOC_TOKENS` | 리랭커에 보내는 문서당 토큰 cap (요청의 `rerankMaxDocTokens` 가 우선) |
175
+ | `QMD_RERANK_CONTEXT_SIZE` · `QMD_EMBED_CONTEXT_SIZE` · `QMD_EXPAND_CONTEXT_SIZE` | 역할별 context 크기 |
176
+ | `QMD_EMBED_PARALLELISM` | 병렬 context 수 (높으면 RAM/VRAM 고갈) |
177
+ | `QMD_LLAMA_GPU` · `QMD_FORCE_CPU` | GPU 백엔드 선택·끄기 |
178
+ | `QMD_CONFIG_DIR` | 설정 디렉터리 (XDG_CONFIG_HOME 보다 우선) |
179
+ | `QMD_EDITOR_URI` | 터미널 결과의 에디터 링크 템플릿 |
180
+
181
+ 설정된 값과 그 영향은 `qmd doctor` 가 보여 준다.
182
+
183
+ ## 아키텍처
184
+
185
+ - SQLite FTS5(BM25) + 한글 음절 bigram, sqlite-vec(벡터), RRF 융합, 리랭크 블렌드
186
+ - 청킹: 약 900토큰·15% 겹침, 마크다운 제목을 경계로 선호. 코드 파일은 `--chunk-strategy auto` 로 tree-sitter AST 경계(TS·JS·Python·Go·Rust)
187
+ - 인덱스: `~/.cache/qmd/index.sqlite`. LLM 결과 캐시(`llm_cache`)도 여기에 있다
188
+ - 점수 계산과 백엔드별 해석: [docs/REFERENCE.md § Score Normalization & Fusion](docs/REFERENCE.md#score-normalization--fusion)
189
+
190
+ ## 개발
191
+
192
+ ```sh
193
+ bun install
194
+ bun src/cli/qmd.ts <명령> # 소스에서 실행
195
+ bun run lint # oxlint
196
+ bun run test:types # tsc (tsconfig.build.json)
197
+ npx vitest run --reporter=verbose test/
198
+ bun test --preload ./src/test-preload.ts test/
199
+ npm run build # dist/ — `bun build --compile` 은 쓰지 않는다(sqlite-vec 이 깨진다)
200
+ ```
201
+
202
+ - 브랜치: `develop` 이 작업 브랜치(PR 은 여기로), `main` 은 배포 브랜치다. 릴리스는 `/release <version>`(→ `v*` 태그 → `publish.yml` 이 npm 발행).
203
+ 절차와 CHANGELOG 규칙: [skills/release/SKILL.md](skills/release/SKILL.md).
204
+ - CI 는 업스트림 매트릭스에 `windows-latest` 와 Electron 스모크 잡을 더했다.
205
+
206
+ ### dogfood — 작업 트리 빌드를 이 머신 데몬에 올리기
207
+
208
+ ```sh
209
+ bash scripts/dogfood.sh # 빌드 → bench-ko 게이트 → npm pack → npm i -g → 데몬 재시작 → 모델 워밍 → 스모크
210
+ bash scripts/dogfood.sh --restore # 발행본(DOGFOOD_PIN_FILE 의 ko-qmd@<버전>, 없으면 latest)으로 되돌림
211
+ bash scripts/dogfood.sh --check "RESULT bm25_r5=…" # 게이트 판정만
212
+ ```
213
+
214
+ - 게이트: `bench-ko.sh` 의 `bm25_r5` 가 [BASELINE.md](test/fixtures/ko-vault/BASELINE.md) 의 마지막 `RESULT` 줄보다 낮으면 설치 전에 멈춘다(exit 3).
215
+ 베이스라인에 `RESULT-HARD` 줄이 있으면 어려운 유형 게이트도 돈다: 벤치를 돌린 form(`form=`, 없으면 plain)이
216
+ 그 줄과 같아야 하고, `full_r1` 이 그 줄의 값에서 그 줄의 `tol=` 을 뺀 값보다 낮으면 마찬가지로 멈춘다.
217
+ seam form(`bench-ko.sh` 기본값 — 질의마다 `lex:`·`vec:` 두 줄, LLM 질의 확장 없음)이면 `hybrid_r1` 도 같은 식으로 본다.
218
+ seam form 은 같은 커밋에서 매번 같은 값이 나와 기준줄이 `tol=0` 이다. plain form 의 `hybrid_r1` 은 확장 샘플링만으로
219
+ 질의 5개만큼 흔들려서 게이트하지 않는다(BASELINE.md 2026-09-26).
220
+ - `npm link` 가 아니라 pack 설치다. 링크하면 데몬이 작업 트리의 `dist/` 를 서빙해 빌드 중에 깨질 수 있다.
221
+ - 머신 배선 env: `DOGFOOD_LABEL`(launchd 라벨, 기본 `io.github.ko-qmd.daemon`) · `DOGFOOD_URL`(기본 `http://127.0.0.1:8181`) ·
222
+ `DOGFOOD_SMOKE` · `DOGFOOD_PIN_FILE`. 성공하면 `~/.cache/qmd/dogfood-deployed` 에 `<시각> <커밋>` 을 쓴다.
223
+
224
+ ### ko-vault 벤치
225
+
226
+ `bash scripts/bench-ko.sh` — 픽스처 `test/fixtures/ko-vault/`(문서 97: 위키 28·디스트랙터 69·질의 123), 임베딩은 Qwen3-Embedding-0.6B-Q8_0 고정.
227
+ 기본은 seam form(REST/MCP 호출과 같은 `lex:`+`vec:` 모양, 확장 없음)이고 `KO_FORM=plain` 이면 질의를 그대로 넘겨 확장 경로를 잰다.
228
+ run별 수치는 [BASELINE.md](test/fixtures/ko-vault/BASELINE.md). 업스트림 2.8.3 의 bm25_r5 0.6250 에서 시작해,
229
+ Qwen3-Embedding 기본값·질의 52 에서 bm25_r5 0.9519 · vector_r5 1.0000 · full_r5 1.0000 이다.
230
+ 질의 63 은 외래어 표기 질의 11건을 더한 셋이고, 그 11건의 수치는 BASELINE.md 2026-09-23 절에 있다. 질의 93 은 어려운 유형
231
+ (sem-hard 12·multi 8·neg 10) 30건을 더한 셋이고, 첫 수치는 BASELINE.md 2026-09-24 절에 있다. 질의 123 은 easy 15건
232
+ (exact 5·alias 4·semantic 3·topical 2·cross-domain 1, 저커버리지 토픽과 topics/ 입구 보강)과 hard 15건
233
+ (sem-hard 5·multi 5·neg 5)을 더한 셋이다. hard 45 기준의 RESULT 참조선은 BASELINE.md 2026-10-10 절에 있다.
234
+
235
+ ### Electron 내장
236
+
237
+ 앱은 SDK 를 워커(utilityProcess)에서 쓴다. CI `electron-smoke` 잡이 Electron 39(ABI 140)로 `better-sqlite3` 를 리빌드하고
238
+ `createStore` → `searchLex('검색')` 을 확인한다.
239
+
240
+ - **동적 `import()` 만 된다.** `dist/` 에 top-level await 가 있어 `require()` 는 `ERR_REQUIRE_ASYNC_MODULE` 로 실패한다.
241
+ `XDG_CACHE_HOME` 같은 env 는 모듈 로드 시점에 읽히므로, 워커 진입점에서 env 를 설정한 뒤 `await import('ko-qmd')` 한다.
242
+ - **`asarUnpack`**: `node_modules/@node-llama-cpp/**` · `node_modules/sqlite-vec-*/**` · `node_modules/better-sqlite3/build/**`
243
+ - **pnpm `onlyBuiltDependencies`**: `better-sqlite3`, `node-llama-cpp`
244
+ - `better-sqlite3` 는 Electron ABI 로 리빌드한다(`@electron/rebuild` 또는 electron-builder `install-app-deps`).
245
+ 리빌드된 모듈은 시스템 Node(vitest)에서 `NODE_MODULE_VERSION` 불일치로 로드되지 않는다.
246
+
247
+ ### 업스트림 동기화
248
+
249
+ 업스트림은 `upstream` 원격으로만 따라간다(GitHub 포크가 아니다). `develop` 에서 `chore/upstream-sync-*` 브랜치를 따고,
250
+ 업스트림의 first-parent 머지 지점을 하나씩 `--no-ff` 로 머지한다.
251
+
252
+ ```sh
253
+ git fetch upstream --tags
254
+ git log --first-parent --oneline develop..upstream/main # 머지할 지점 목록
255
+ bash scripts/bench-ko.sh # 머지 전 기준값
256
+ git merge --no-ff <지점> # 지점마다 반복, 충돌은 이 머지 커밋에서 해결
257
+ pnpm install --lockfile-only # package.json 이 바뀌었으면 — 업스트림은 bun.lock 만 갱신한다
258
+ bun run lint && bun run test:types && bun run test:unit && bash scripts/bench-ko.sh
259
+ ```
260
+
261
+ - 벤치는 bm25 recall@5·MRR 을 질의별로 머지 전과 비교한다. 기본 seam form 은 실행마다 같은 값이 나와서, 움직였다면
262
+ 동기화가 바꾼 것이다. `KO_FORM=plain` 의 hybrid 는 질의 확장 샘플링만으로도 흔들린다.
263
+ - PR 은 squash 가 아니라 **머지 커밋**으로 들인다. 업스트림 커밋이 조상으로 남아야 다음 동기화가 새 커밋만 본다.
264
+ 같은 이유로 이 브랜치는 squash·rebase 하지 않고, `develop`·`main` 을 upstream 위로 rebase 하지 않는다.
265
+ - `pnpm install --frozen-lockfile --lockfile-only` 가 통과해야 한다. 낡은 `pnpm-lock.yaml` 은 GitHub 설치(`prepare`)와 릴리스 태그의 pre-push 검사를 막는다.
266
+ - 업스트림 태그 위에 있을 때만 `v<업스트림 버전>-ko.N` 태그를 쓴다. `main` 스냅샷이면 버전은 마지막 태그 기준이다.
267
+
268
+ ## 자주 묻는 질문
269
+
270
+ ### ko-qmd 는 무엇인가?
271
+ 한국어 마크다운 노트를 로컬에서 검색하는 CLI·MCP 서버·SDK 다. 업스트림 qmd 에 한국어 조사·어미 처리, 한글 음절 bigram 색인,
272
+ 한국어에 맞춘 기본 임베딩(Qwen3-Embedding)과 융합 가중치를 더했다.
273
+
274
+ ### 업스트림 qmd 로 한국어를 검색하면 무엇이 안 되나?
275
+ 업스트림의 BM25 는 `검색을`·`청킹에서의` 처럼 조사가 붙은 단어를 다른 토큰으로 본다. 그래서 `검색` 이라고 적은 노트를 놓친다.
276
+ ko-vault 벤치(질의 52)에서 bm25 recall@5 는 업스트림 2.8.3 의 0.6250 에서 이 배포판의 0.9519 로 올랐다([BASELINE.md](test/fixtures/ko-vault/BASELINE.md)).
277
+
278
+ ### Obsidian 볼트를 그대로 검색할 수 있나?
279
+ 그렇다. 볼트 폴더를 `qmd collection add <볼트 경로> --name <이름>` 으로 등록하면 그 안의 `*.md` 를 색인한다. 볼트 파일은 읽기만 한다.
280
+
281
+ ### 인터넷 연결이나 API 키가 필요한가?
282
+ 아니다. 임베딩·리랭크·질의 확장 모델은 처음 쓸 때 한 번 내려받고(`~/.cache/qmd/models/`), 그 뒤로는 오프라인에서 돈다.
283
+
284
+ ### Claude Code 같은 AI 에이전트에 붙이려면?
285
+ `qmd mcp`(stdio) 또는 `qmd mcp --http --daemon`(HTTP, 여러 세션이 모델 공유)으로 MCP 서버를 띄운다. [에이전트에서 쓰기](#에이전트에서-쓰기) 참고.
286
+
287
+ ### 영어 노트도 되나?
288
+ 된다. 영어 질의와 영어 문서의 청크 경계·질의 확장은 업스트림과 같게 동작한다. 한국어와 영어가 섞인 노트도 한 인덱스에서 찾는다.
289
+
290
+ ## 라이선스
291
+
292
+ MIT. 원저작권은 Tobi Lutke([LICENSE](LICENSE))에게 있고, 이 배포판도 같은 라이선스를 따른다.