ko-qmd 2.8.3-ko.4 → 2.8.3-ko.6
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.
- package/CHANGELOG.md +147 -1
- package/README.en.md +184 -0
- package/README.md +218 -1494
- package/dist/bench/bench.d.ts +7 -1
- package/dist/bench/bench.js +44 -0
- package/dist/bench/types.d.ts +32 -11
- package/dist/bench/types.js +2 -1
- package/dist/cli/build-info.json +2 -2
- package/dist/cli/qmd.js +2 -1
- package/dist/hangul.d.ts +11 -1
- package/dist/hangul.js +32 -4
- package/dist/index.d.ts +4 -1
- package/dist/index.js +2 -0
- package/dist/llm.d.ts +5 -0
- package/dist/llm.js +16 -2
- package/dist/mcp/server.js +10 -7
- package/dist/rerank-input.d.ts +31 -0
- package/dist/rerank-input.js +45 -0
- package/dist/store.d.ts +22 -6
- package/dist/store.js +126 -100
- package/package.json +11 -3
- package/README.ko.md +0 -115
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,152 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [2.8.3-ko.6] - 2026-09-29
|
|
6
|
+
|
|
7
|
+
Korean queries are handled better before and during reranking. A query written
|
|
8
|
+
as "X 말고 Y" now searches for Y alone, and a Hangul query skips LLM expansion,
|
|
9
|
+
which steadies hard-query results and cuts hybrid latency from about 1s to tens
|
|
10
|
+
of ms per query. The reranker is now shown the chunk that best matches the
|
|
11
|
+
query's Hangul syllable bigrams, led by the note title: held-out full MRR goes
|
|
12
|
+
0.6589 → 0.7260 on a goldset generated from a real vault. Scoped vector search on
|
|
13
|
+
small indexes runs one KNN (vector stage 250–1300ms → about 25ms). The README
|
|
14
|
+
gains an English edition and an FAQ.
|
|
15
|
+
|
|
16
|
+
- `QMD_RERANK_CACHE=<path>` keeps rerank scores in their own sqlite file instead of the index's
|
|
17
|
+
`llm_cache`, so a store over a rebuilt index reuses them. The key carries the query, the rerank
|
|
18
|
+
model, the doc-token cap and the chunk text, not the scoring code in `src/llm.ts`, so the file is
|
|
19
|
+
safe across index rebuilds with the same reranker code. It is not trimmed to 1000 rows. With it
|
|
20
|
+
set, a second `scripts/bench-ko.sh` run on ko-vault gives the same lines and its `full` backend
|
|
21
|
+
spends 3.7 s reranking instead of 300 s (the whole run drops from 5:17 to 0:56). Unset, nothing
|
|
22
|
+
changes. A fallback result (no rerank context could be created, every score 0.5) is no longer
|
|
23
|
+
cached in either place.
|
|
24
|
+
|
|
25
|
+
- The reranker now sees a better chunk of a long Korean note. The chunk is chosen by the
|
|
26
|
+
two-syllable pairs of the query's Hangul words of three or more syllables, instead of whole
|
|
27
|
+
words, so `임베딩은` still finds a chunk about `임베딩`. Generic two-syllable words such as 방법
|
|
28
|
+
and 경우 no longer steer the choice. A query word with no Hangul (an English term, say) counts double. When the chosen chunk
|
|
29
|
+
does not contain the note title, `# <title>` is put in front of it. An autonomous loop
|
|
30
|
+
(`scripts/autoresearch/`) found this over 28 experiments on a goldset generated from a private
|
|
31
|
+
vault. The goldset's held-out split, which the loop never saw, goes from full MRR 0.6589 to
|
|
32
|
+
0.7260 (32 queries). ko-vault seam and hard lines are unchanged. The bm25, vector and hybrid
|
|
33
|
+
results do not read the rerank input and are identical on both sets (`BASELINE.md`
|
|
34
|
+
2026-09-28). An intent word now counts 1 point in chunk selection (was 0.5), keeping it at half a
|
|
35
|
+
query word now that a non-Hangul query word counts 2; no goldset carries intent, so no bench
|
|
36
|
+
number moves.
|
|
37
|
+
|
|
38
|
+
- The reranker's input (the query string, which chunk of each candidate is sent and the document
|
|
39
|
+
text) is now built in `src/rerank-input.ts` for both `hybridQuery` and `structuredSearch`, which
|
|
40
|
+
had copied the chunk-scoring loop. Results are unchanged.
|
|
41
|
+
|
|
42
|
+
- A Korean query of the form "X 말고 Y" now searches for Y alone. `hybridQuery` (CLI `qmd query`,
|
|
43
|
+
plain MCP `query`) and the `lex`/`vec` lines of `structuredSearch` (MCP `searches`, typed lines)
|
|
44
|
+
drop everything up to the last whitespace-separated 말고, 빼고, 제외하고 or 제외한. The rerank
|
|
45
|
+
query follows. `hyde` lines, `intent` and `qmd search`/`vsearch` stay as written. Before this
|
|
46
|
+
change every list also searched for X, the concept the user ruled out, so a note about X often
|
|
47
|
+
took rank 1. `아닌` and bare `제외` are not markers, because they also state conditions or act as
|
|
48
|
+
nouns. On the ko-vault seam form, the `neg` queries' full MRR goes 0.4500 → 0.6944. Hard
|
|
49
|
+
`full_r1` goes 0.5111 → 0.6111 and hard `full_mrr` 0.7472 → 0.8287. No query outside `neg`
|
|
50
|
+
changed its results, and that run is the gate's new reference (`BASELINE.md` 2026-09-27,
|
|
51
|
+
"a Korean negation clause is stripped before search").
|
|
52
|
+
|
|
53
|
+
- `structuredSearch` (MCP `searches`, SDK `search({ queries })`, typed `lex:`/`vec:` lines) now
|
|
54
|
+
counts a relaxed lex list, one that matched only through the any-word retry, at half its slot's
|
|
55
|
+
weight, as `hybridQuery` already did. At a flat 2.0 for the first list, a lex line that matched
|
|
56
|
+
some of a Korean question's words outranked the vector list, and a decoy sharing those words took
|
|
57
|
+
rank 1. This one weight was the whole gap between the two paths on the Korean ko-vault queries:
|
|
58
|
+
re-fusing each path's lists with the other's weights reproduced its top 10 for 85 of 85 queries.
|
|
59
|
+
On the seam form, hard `hybrid_r1` goes 0.3111 → 0.3778 and `hybrid_r5` 0.8996 → 0.9211, with
|
|
60
|
+
`full_r1` unchanged at 0.5111, and that run is the gate's new reference (`BASELINE.md` 2026-09-27).
|
|
61
|
+
|
|
62
|
+
- A query with any Hangul is no longer expanded by the LLM on the hybrid path (`hybridQuery`). That
|
|
63
|
+
path serves CLI `qmd query`, including an explicit `expand:` line, as well as a plain MCP `query`
|
|
64
|
+
and SDK `search({ query })`; `qmd vsearch` still expands. On the ko-vault goldset the
|
|
65
|
+
expansion model wrapped Korean questions in English sentence templates and stray Chinese, and its
|
|
66
|
+
sampling alone moved hard rank 1 by up to five of 30 queries between runs. Without it, hard
|
|
67
|
+
`hybrid_r1` reads 0.3778 (0.2611–0.3611 with expansion), `full_r1` holds at 0.5111, the easy
|
|
68
|
+
queries lose nothing, and the median hybrid query drops from about 1 s to 20 ms. The Korean
|
|
69
|
+
queries now return the same results on every run. English-only queries are still expanded, and a
|
|
70
|
+
caller who wants variants for a Korean question can pass its own `searches`
|
|
71
|
+
(`BASELINE.md` 2026-09-26).
|
|
72
|
+
|
|
73
|
+
- `scripts/bench-ko.sh` benches the seam form by default. Every one-line goldset query reaches
|
|
74
|
+
`qmd bench` as `lex: <q>` + `vec: <q>`, the shape REST/MCP callers send, which skips LLM query
|
|
75
|
+
expansion. `KO_FORM=plain` keeps the old path, and the `RESULT-HARD` line now ends with
|
|
76
|
+
`form=<seam|plain>`. The `dogfood.sh` gate refuses a bench whose form differs from its baseline
|
|
77
|
+
line's form. On the seam form it gates hard `hybrid_r1` as well as `full_r1`, against a reference
|
|
78
|
+
line with `tol=0`, since the seam form gave the same lines in every run. The reason is the plain
|
|
79
|
+
form's hard wobble, which turned out to be the expansion sampler alone. Replaying recorded
|
|
80
|
+
expansions reproduces every query's top 10, and every query that moved between same-commit runs
|
|
81
|
+
had drawn a different expansion. The draw moved `hybrid_r1` by up to five of 30 queries and once
|
|
82
|
+
put `full_r1` 0.0007 above the old gate's floor (`BASELINE.md` 2026-09-26). The gate also runs
|
|
83
|
+
the bench with any exported `KO_BENCH`/`KO_CORPUS`/`KO_FORM` removed, and `bench-ko.sh` refuses a
|
|
84
|
+
`KO_BENCH` inside `tmp/bench-ko/`, where the run clears its outputs.
|
|
85
|
+
|
|
86
|
+
- `scripts/bench-ko.sh` takes `KO_CORPUS` (a corpus directory holding `wiki/` and
|
|
87
|
+
`distractors/`) and `KO_BENCH` (a goldset) so an A/B runs through the same isolated harness
|
|
88
|
+
and stdout lines. Overrides resolve against the caller's cwd and are checked before the
|
|
89
|
+
previous run's output is removed. The first use settled a question: three everyday-language
|
|
90
|
+
question lines per document, decoys included, did not move hard rank 1 on the REST-shaped
|
|
91
|
+
goldset (hybrid_r1 0.3111 → 0.2778, full_r1 flat at 0.5111; hard hit@5 up 2 of 30). Index-time
|
|
92
|
+
summaries are therefore not being built. The same runs point the `hybrid_r1` wobble at LLM
|
|
93
|
+
query expansion rather than re-embedding (`BASELINE.md` 2026-09-25).
|
|
94
|
+
- ko-vault goldset: 69 near-topic distractor documents and 30 hard queries (`sem-hard` /
|
|
95
|
+
`multi` / `neg`) join the fixture; `bench-ko.sh` indexes both directories (97 files) and
|
|
96
|
+
prints a `RESULT-HARD hybrid_r1=… full_r1=… n=30` line next to `RESULT`. The `dogfood.sh`
|
|
97
|
+
gate now also fails when the hard `full_r1` falls below the baseline minus the `tol=` recorded
|
|
98
|
+
on that baseline line, and fails closed when `RESULT-HARD` or its `tol=` goes missing.
|
|
99
|
+
`full_r1` is gated rather than `hybrid_r1` because it held one value across eight same-commit
|
|
100
|
+
runs, while `hybrid_r1` moved by about 3.5 queries of 30.
|
|
101
|
+
Fixture invariants (no title-term leakage, no excluded concept among expected files, no basename
|
|
102
|
+
collisions) are enforced by `test/ko-bench-fixture.test.ts`.
|
|
103
|
+
|
|
104
|
+
- Collection- or filter-scoped vector search is faster on indexes of up to
|
|
105
|
+
4096 vectors. It now post-filters one sqlite-vec KNN over the whole table,
|
|
106
|
+
which is exact because vec0 KNN is a brute-force scan, instead of computing
|
|
107
|
+
`vec_distance_cosine` row by row. On the KB goldset (1047 of 3389 vectors,
|
|
108
|
+
M3 Max) the vector stage dropped from 250–1300ms to about 25ms, and a cold
|
|
109
|
+
capped query (cap 128, candidateLimit 15) went from p90 2000ms to 906ms with
|
|
110
|
+
identical rankings. Larger indexes keep the exact scan.
|
|
111
|
+
|
|
112
|
+
## [2.8.3-ko.5] - 2026-09-24
|
|
113
|
+
|
|
114
|
+
Rerank gets an input cap. Truncating each document to 128 tokens keeps KB
|
|
115
|
+
goldset quality (Hit@5 0.97 / Recall@5 0.77) while cutting cold rerank p90 from
|
|
116
|
+
3.6s to 0.9s on an M3 Max. The cap can be set daemon-wide
|
|
117
|
+
(`QMD_RERANK_MAX_DOC_TOKENS`) or per request (`rerankMaxDocTokens` on REST
|
|
118
|
+
`/query` and SDK `search()`). The README is now a Korean guide to ko-qmd itself,
|
|
119
|
+
and the English reference lives in `docs/REFERENCE.md`.
|
|
120
|
+
|
|
121
|
+
### Changes
|
|
122
|
+
|
|
123
|
+
- README.md is rewritten in Korean around ko-qmd itself: what differs from
|
|
124
|
+
upstream, install, quick start, MCP/REST/SDK, environment variables and the
|
|
125
|
+
maintenance guide that used to live in `README.ko.md` (now removed). The
|
|
126
|
+
upstream-derived English reference (full CLI, MCP tool parameters, SDK API,
|
|
127
|
+
score fusion) moved to `docs/REFERENCE.md`, with install and import names
|
|
128
|
+
switched to `ko-qmd`.
|
|
129
|
+
- REST `POST /query` and SDK `search()` take `rerankMaxDocTokens`, a
|
|
130
|
+
per-request rerank doc token cap. For that request it overrides the daemon's
|
|
131
|
+
`QMD_RERANK_MAX_DOC_TOKENS`, and it never raises the context budget. A caller
|
|
132
|
+
like the KB seam can then turn a capped rerank on for one machine without
|
|
133
|
+
changing the daemon's env. The rerank cache key follows the effective cap
|
|
134
|
+
(request, else env). Invalid values (non-integer, ≤ 0, strings) are ignored.
|
|
135
|
+
- `QMD_RERANK_MAX_DOC_TOKENS` caps the tokens of each document sent to the
|
|
136
|
+
reranker, below the context budget. Unset keeps today's behaviour. On the KB
|
|
137
|
+
goldset (40 queries, candidateLimit 15), a cap of 128 keeps quality at
|
|
138
|
+
Hit@5 0.97 / Recall@5 0.77 against 0.95 / 0.76 uncapped. It cuts cold p90 from
|
|
139
|
+
3623ms to 897ms on an M3 Max and from 4720ms to 2192ms on another Apple Silicon
|
|
140
|
+
Mac. The cap is part of the rerank cache key, so capped and uncapped scores
|
|
141
|
+
never mix. Listed in `qmd doctor`.
|
|
142
|
+
- `scripts/dogfood.sh` warms the embedding model with one vec query after
|
|
143
|
+
every daemon restart (deploy and `--restore`). A fresh daemon's first vec
|
|
144
|
+
query took 15.5s, so the first KB seam search after a restart hit its 20s
|
|
145
|
+
limit and fell back to rg. A failed warm-up is a warning, not a failed restart.
|
|
146
|
+
- `scripts/dogfood.sh` waits for the *restarted* daemon. `launchctl kickstart -k`
|
|
147
|
+
returns while the old process is still shutting down and answering `/health`,
|
|
148
|
+
so the restart looked done ~9s before the new process listened. A `/health`
|
|
149
|
+
whose `uptime` predates the kickstart is now treated as the old process.
|
|
150
|
+
|
|
5
151
|
## [2.8.3-ko.4] - 2026-09-24
|
|
6
152
|
|
|
7
153
|
Korean documents are now chunked at their real token budget, and Hangul
|
|
@@ -49,7 +195,7 @@ it to `scope`.
|
|
|
49
195
|
- Plain Hangul lex terms that spell a common technical loanword (`서치`, `웹`,
|
|
50
196
|
`코어`, `마이그레이션`, about 80 entries in a built-in table) also match the
|
|
51
197
|
Latin spelling, so `하이브리드 서치` finds a note that only says
|
|
52
|
-
`hybrid-search` and
|
|
198
|
+
`hybrid-search` and `리액트 웹 코어` reaches `react-web-core`. The lookup runs
|
|
53
199
|
on particle-stripped stems too (`서치를`), single-syllable entries (`웹`, `훅`)
|
|
54
200
|
are bridged when they stand alone (not `웹을`), and terms with no entry produce the same FTS5 query as before.
|
|
55
201
|
Query-side only; no re-index. On the ko-vault bench the eleven new loanword
|
package/README.en.md
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# ko-qmd — local hybrid search for Korean Markdown notes
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/ko-qmd)
|
|
4
|
+
[](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 80 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, 93 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.
|