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 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 80 built-in Hangul spellings of technical terms (`서치`, `웹`, `코어`,
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
- - **외래어 다리** — 기술 용어의 한글 표기(`서치`·`웹`·`코어`·`마이그레이션` 등 약 80개 내장 표)는 영문 표기도 함께 찾는다.
37
+ - **외래어 다리** — 기술 용어의 한글 표기(`서치`·`웹`·`코어`·`마이그레이션` 등 약 90개 내장 표)는 영문 표기도 함께 찾는다.
38
38
  `하이브리드 서치`가 `hybrid-search` 노트를, `리액트 웹 코어`가 `react-web-core`를 찾는다. 질의 쪽만 바뀌므로 재색인은 필요 없다.
39
39
  - **문자 체계가 섞인 토큰 분리** — `SKILL.md계약의핵심`은 `skill` AND `md` AND `계약의핵심`으로 나눈다.
40
40
  - **긴 질의 완화** — 세 단어 이상 질의가 AND 로 한 건도 안 맞으면 같은 단어를 OR 로 다시 찾는다. 자연어 문장 질의가 0건이 되지 않는다.
@@ -1,4 +1,4 @@
1
1
  {
2
- "commit": "ed12243",
3
- "builtAt": "2026-10-02T04:28:17.608Z"
2
+ "commit": "6698cd5",
3
+ "builtAt": "2026-10-09T05:15:35.019Z"
4
4
  }
package/dist/hangul.js CHANGED
@@ -65,24 +65,34 @@ export function sharesHangulBigram(a, b) {
65
65
  return false;
66
66
  }
67
67
  /**
68
- * Drop the excluded clause of a Korean negation query and keep what follows the
69
- * last marker (`tf-idf 말고 순위 공식` → `순위 공식`), so retrieval and rerank
70
- * never see the concept the user ruled out. Returns the query unchanged when a
71
- * side would be empty or hold only `-term` exclusions, a `"` phrase is present, or
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
- export function stripHangulNegation(query) {
73
+ function splitHangulNegation(query) {
75
74
  if (query.includes('"') || !containsHangul(query))
76
- return query;
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 query;
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 query;
85
- return kept.join(" ");
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
- const weight = i === 0 ? 2.0 : 1.0;
4608
- return meta.relaxed ? weight * RELAXED_LIST_WEIGHT : weight;
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.7",
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",