ko-qmd 2.8.3-ko.7 → 2.8.3-ko.8
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 +19 -0
- package/README.en.md +1 -1
- package/README.md +1 -1
- package/dist/cli/build-info.json +2 -2
- package/dist/hangul.js +31 -10
- package/dist/store.d.ts +0 -8
- package/dist/store.js +9 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [2.8.3-ko.8] - 2026-10-09
|
|
6
|
+
|
|
7
|
+
- `structuredSearch` (MCP `searches`, SDK `search({ queries })`) no longer lets a relaxed
|
|
8
|
+
(any-word) first lex list keep the 2x positional boost: it now counts 0.25 instead of 1.0.
|
|
9
|
+
Even halved, a relaxed rank 1 still tied a vector rank 1 against a lex rank 4 (`hmu-05`
|
|
10
|
+
probe), so the decoy sharing a few question words kept rank 1 on the `rerank:false` path.
|
|
11
|
+
On the ko-vault seam form, hard `hybrid_r1` goes 0.4111 → 0.4444 with `full_r1` unchanged at
|
|
12
|
+
0.6111; one easy query (`sem-08`) trades hybrid rank 1 for 2 while its `full` rank 1 and all
|
|
13
|
+
recall@5 lines hold. Non-first relaxed lists still count half, as before.
|
|
14
|
+
- Hangul loanword table gains 11 entries mined from real query-log traffic (`지연` →
|
|
15
|
+
`latency`, `병렬` → `parallel`, `게이트` → `gate`, `가드` → `guard`, `패널` → `panel`,
|
|
16
|
+
`팔레트` → `palette`, `하네스` → `harness`, `옵시디언` → `obsidian`, `백링크` → `backlink`,
|
|
17
|
+
`포즈` → `pause`, `루프` → `loop`). Native words from the same logs (`색인`·`질의`·`요약`·`문서`
|
|
18
|
+
…) stay unbridged. Query-side only, no re-index; ko-vault bench lines are unchanged.
|
|
19
|
+
- Negation routing stays strip-only by measurement: demoting X with lex `-term`s was tried
|
|
20
|
+
and reverted — the expected note often discusses X itself (`bm25-ranking.md` mentions TF-IDF),
|
|
21
|
+
so NOT removed the answer with the decoy (`hybrid_r1` 0.4111 → 0.3778). `stripHangulNegation`
|
|
22
|
+
now shares its parsing through an internal split helper; behavior is unchanged.
|
|
23
|
+
|
|
5
24
|
## [2.8.3-ko.7] - 2026-10-02
|
|
6
25
|
|
|
7
26
|
The query path is now observable and bounded. Every query-log row breaks its latency into
|
package/README.en.md
CHANGED
|
@@ -38,7 +38,7 @@ flowchart LR
|
|
|
38
38
|
(`검색을` → `검색`), a two-particle chain (`청킹에서의` → `청킹`), verb and nominalizing endings
|
|
39
39
|
(`토큰화하는` → `토큰화`, `검색하기` → `검색`), `한`/`된` endings (`필요한` → `필요`) and the 하↔해
|
|
40
40
|
contraction. Quoted phrases, Hanja and kana are left as written.
|
|
41
|
-
- **Loanword bridge** — about
|
|
41
|
+
- **Loanword bridge** — about 90 built-in Hangul spellings of technical terms (`서치`, `웹`, `코어`,
|
|
42
42
|
`마이그레이션`, …) also search their English spelling, so `하이브리드 서치` finds a `hybrid-search` note.
|
|
43
43
|
Only the query changes; no re-index is needed.
|
|
44
44
|
- **Mixed-script token split** — `SKILL.md계약의핵심` becomes `skill` AND `md` AND `계약의핵심`.
|
package/README.md
CHANGED
|
@@ -34,7 +34,7 @@ flowchart LR
|
|
|
34
34
|
- **조사·어미 제거** — 따옴표 없는 한글 단어는 어간도 매칭한다. 끝 조사 하나(`검색을` → `검색`), 두 조사 연쇄(`청킹에서의` → `청킹`),
|
|
35
35
|
동사·명사화 어미(`토큰화하는` → `토큰화`, `검색하기` → `검색`), `한`/`된` 어미(`필요한` → `필요`), 하↔해 축약(`더하` ↔ `더해`).
|
|
36
36
|
따옴표 구문·한자·가나는 그대로 둔다.
|
|
37
|
-
- **외래어 다리** — 기술 용어의 한글 표기(`서치`·`웹`·`코어`·`마이그레이션` 등 약
|
|
37
|
+
- **외래어 다리** — 기술 용어의 한글 표기(`서치`·`웹`·`코어`·`마이그레이션` 등 약 90개 내장 표)는 영문 표기도 함께 찾는다.
|
|
38
38
|
`하이브리드 서치`가 `hybrid-search` 노트를, `리액트 웹 코어`가 `react-web-core`를 찾는다. 질의 쪽만 바뀌므로 재색인은 필요 없다.
|
|
39
39
|
- **문자 체계가 섞인 토큰 분리** — `SKILL.md계약의핵심`은 `skill` AND `md` AND `계약의핵심`으로 나눈다.
|
|
40
40
|
- **긴 질의 완화** — 세 단어 이상 질의가 AND 로 한 건도 안 맞으면 같은 단어를 OR 로 다시 찾는다. 자연어 문장 질의가 0건이 되지 않는다.
|
package/dist/cli/build-info.json
CHANGED
package/dist/hangul.js
CHANGED
|
@@ -65,24 +65,34 @@ export function sharesHangulBigram(a, b) {
|
|
|
65
65
|
return false;
|
|
66
66
|
}
|
|
67
67
|
/**
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
* no marker stands as its own token — a marker glued to X (`이거말고`) is not split.
|
|
68
|
+
* Split a "X 말고 Y" query at its last exclusion marker into the excluded (X)
|
|
69
|
+
* and kept (Y) token lists. Null when there is no rewrite: a `"` phrase is
|
|
70
|
+
* present, no Hangul, no standalone marker, an empty side, or a Y of only
|
|
71
|
+
* `-term` exclusions (lex needs a positive term to match).
|
|
73
72
|
*/
|
|
74
|
-
|
|
73
|
+
function splitHangulNegation(query) {
|
|
75
74
|
if (query.includes('"') || !containsHangul(query))
|
|
76
|
-
return
|
|
75
|
+
return null;
|
|
77
76
|
const tokens = query.trim().split(/\s+/);
|
|
78
77
|
const last = tokens.findLastIndex((token) => NEGATION_MARKERS.has(token));
|
|
79
78
|
if (last < 1 || last === tokens.length - 1)
|
|
80
|
-
return
|
|
79
|
+
return null;
|
|
81
80
|
const kept = tokens.slice(last + 1);
|
|
82
81
|
// A Y of only `-term` exclusions leaves lex no positive term to match.
|
|
83
82
|
if (kept.every((token) => token.startsWith("-")))
|
|
84
|
-
return
|
|
85
|
-
return
|
|
83
|
+
return null;
|
|
84
|
+
return { excluded: tokens.slice(0, last), kept };
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Drop the excluded clause of a Korean negation query and keep what follows the
|
|
88
|
+
* last marker (`tf-idf 말고 순위 공식` → `순위 공식`), so retrieval and rerank
|
|
89
|
+
* never see the concept the user ruled out. Returns the query unchanged when a
|
|
90
|
+
* side would be empty or hold only `-term` exclusions, a `"` phrase is present, or
|
|
91
|
+
* no marker stands as its own token — a marker glued to X (`이거말고`) is not split.
|
|
92
|
+
*/
|
|
93
|
+
export function stripHangulNegation(query) {
|
|
94
|
+
const split = splitHangulNegation(query);
|
|
95
|
+
return split ? split.kept.join(" ") : query;
|
|
86
96
|
}
|
|
87
97
|
/**
|
|
88
98
|
* Strip one trailing particle (or 기) from a pure-Hangul word when the stem
|
|
@@ -173,7 +183,9 @@ export function hangulTermQuery(term) {
|
|
|
173
183
|
* ASCII words separated by single spaces so they are safe inside an FTS5 phrase.
|
|
174
184
|
*/
|
|
175
185
|
const LOANWORDS = {
|
|
186
|
+
"가드": ["guard"],
|
|
176
187
|
"가이드": ["guide"],
|
|
188
|
+
"게이트": ["gate"],
|
|
177
189
|
"골든": ["golden"],
|
|
178
190
|
"그래프": ["graph"],
|
|
179
191
|
"노트": ["note"],
|
|
@@ -194,14 +206,17 @@ const LOANWORDS = {
|
|
|
194
206
|
"리트리벌": ["retrieval"],
|
|
195
207
|
"리피티션": ["repetition"],
|
|
196
208
|
"린트": ["lint"],
|
|
209
|
+
"루프": ["loop"],
|
|
197
210
|
"마이그레이션": ["migration"],
|
|
198
211
|
"메타데이터": ["metadata"],
|
|
199
212
|
"모델": ["model"],
|
|
200
213
|
"미팅": ["meeting"],
|
|
201
214
|
"바이그램": ["bigram"],
|
|
215
|
+
"백링크": ["backlink"],
|
|
202
216
|
"벡터": ["vector"],
|
|
203
217
|
"벤치": ["bench"],
|
|
204
218
|
"벤치마크": ["benchmark"],
|
|
219
|
+
"병렬": ["parallel"],
|
|
205
220
|
"서버": ["server"],
|
|
206
221
|
"서치": ["search"],
|
|
207
222
|
"세션": ["session"],
|
|
@@ -215,6 +230,7 @@ const LOANWORDS = {
|
|
|
215
230
|
"엔그램": ["ngram", "n gram"],
|
|
216
231
|
"오토메이션": ["automation"],
|
|
217
232
|
"온리": ["only"],
|
|
233
|
+
"옵시디언": ["obsidian"],
|
|
218
234
|
"워크플로": ["workflow"],
|
|
219
235
|
"워크플로우": ["workflow"],
|
|
220
236
|
"웹": ["web"],
|
|
@@ -227,6 +243,7 @@ const LOANWORDS = {
|
|
|
227
243
|
"인제스트": ["ingest"],
|
|
228
244
|
"인코더": ["encoder"],
|
|
229
245
|
"임베딩": ["embedding"],
|
|
246
|
+
"지연": ["latency"],
|
|
230
247
|
"청크": ["chunk"],
|
|
231
248
|
"청킹": ["chunking"],
|
|
232
249
|
"캐시": ["cache"],
|
|
@@ -242,6 +259,9 @@ const LOANWORDS = {
|
|
|
242
259
|
"토픽": ["topic"],
|
|
243
260
|
"트래킹": ["tracking"],
|
|
244
261
|
"파이프라인": ["pipeline"],
|
|
262
|
+
"패널": ["panel"],
|
|
263
|
+
"팔레트": ["palette"],
|
|
264
|
+
"포즈": ["pause"],
|
|
245
265
|
"퓨전": ["fusion"],
|
|
246
266
|
"프로버넌스": ["provenance"],
|
|
247
267
|
"프로비넌스": ["provenance"],
|
|
@@ -250,6 +270,7 @@ const LOANWORDS = {
|
|
|
250
270
|
"프롬프트": ["prompt"],
|
|
251
271
|
"플러그인": ["plugin"],
|
|
252
272
|
"필터": ["filter"],
|
|
273
|
+
"하네스": ["harness"],
|
|
253
274
|
"하이브리드": ["hybrid"],
|
|
254
275
|
"훅": ["hook"],
|
|
255
276
|
};
|
package/dist/store.d.ts
CHANGED
|
@@ -1080,14 +1080,6 @@ export type RankedListMeta = {
|
|
|
1080
1080
|
* original vector boost.
|
|
1081
1081
|
*/
|
|
1082
1082
|
export declare function getHybridRrfWeights(rankedListMeta: RankedListMeta[]): number[];
|
|
1083
|
-
/**
|
|
1084
|
-
* RRF list weights for structuredSearch: the first list gets 2x (the caller ordered by
|
|
1085
|
-
* importance), the rest 1x.
|
|
1086
|
-
*
|
|
1087
|
-
* ko-qmd: a relaxed FTS list counts half here too, as in getHybridRrfWeights. At a flat 2.0 a
|
|
1088
|
-
* lex line that only matched some of a question's words outranked the vector list, and a decoy
|
|
1089
|
-
* sharing a few of those words took rank 1 (test/fixtures/ko-vault/BASELINE.md 2026-09-27).
|
|
1090
|
-
*/
|
|
1091
1083
|
export declare function getStructuredRrfWeights(rankedListMeta: RankedListMeta[]): number[];
|
|
1092
1084
|
/**
|
|
1093
1085
|
* Blend a reranker score with a linear retrieval-position tiebreak.
|
package/dist/store.js
CHANGED
|
@@ -4601,11 +4601,15 @@ export function getHybridRrfWeights(rankedListMeta) {
|
|
|
4601
4601
|
* ko-qmd: a relaxed FTS list counts half here too, as in getHybridRrfWeights. At a flat 2.0 a
|
|
4602
4602
|
* lex line that only matched some of a question's words outranked the vector list, and a decoy
|
|
4603
4603
|
* sharing a few of those words took rank 1 (test/fixtures/ko-vault/BASELINE.md 2026-09-27).
|
|
4604
|
+
* ko-qmd: a relaxed first list forfeits the positional boost and halves again (0.25) — even
|
|
4605
|
+
* halved, its rank 1 still tied a vector rank 1 with a lex rank 4 (hmu-05, 2026-10-09 probe).
|
|
4604
4606
|
*/
|
|
4607
|
+
const RELAXED_FIRST_LIST_WEIGHT = 0.25;
|
|
4605
4608
|
export function getStructuredRrfWeights(rankedListMeta) {
|
|
4606
4609
|
return rankedListMeta.map((meta, i) => {
|
|
4607
|
-
|
|
4608
|
-
|
|
4610
|
+
if (meta.relaxed)
|
|
4611
|
+
return i === 0 ? RELAXED_FIRST_LIST_WEIGHT : RELAXED_LIST_WEIGHT;
|
|
4612
|
+
return i === 0 ? 2.0 : 1.0;
|
|
4609
4613
|
});
|
|
4610
4614
|
}
|
|
4611
4615
|
/**
|
|
@@ -4760,6 +4764,9 @@ const rerankDocuments = (candidates, docChunkMap) => candidates.flatMap((cand) =
|
|
|
4760
4764
|
*/
|
|
4761
4765
|
export async function hybridQuery(store, rawQuery, options) {
|
|
4762
4766
|
// ko-qmd: "X 말고 Y" names X to exclude — every list and the reranker see only Y.
|
|
4767
|
+
// (Tried demoting X with lex -terms: falsified 2026-10-09 — the expected note
|
|
4768
|
+
// often discusses X itself, e.g. bm25-ranking.md mentions TF-IDF, so NOT
|
|
4769
|
+
// removes the answer along with the decoy.)
|
|
4763
4770
|
const query = stripHangulNegation(rawQuery);
|
|
4764
4771
|
const limit = options?.limit ?? 10;
|
|
4765
4772
|
const minScore = options?.minScore ?? 0;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ko-qmd",
|
|
3
|
-
"version": "2.8.3-ko.
|
|
3
|
+
"version": "2.8.3-ko.8",
|
|
4
4
|
"description": "Local hybrid search for Korean Markdown notes and Obsidian vaults — BM25 with Korean particle stripping, Hangul bigram FTS, Qwen3 vector search and LLM reranking, as CLI, MCP server and SDK",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|