Files
kebab/docs/superpowers/specs/2026-05-28-v0.20.x-korean-morphological-tokenizer-spec.md
altair823 028d9ad4ea docs(release): v0.20.1 release notes draft + spec/plan dogfood cross-link
#1 (사용자 요청): release notes draft 작성 + spec/plan 의 dogfood
evidence cross-link 보강.

docs/release-notes/v0.20.1-draft.md (신규):
- 4 단락 본문 (한국어 2자 query 지원 + 영어 substring 회귀 + V007→V009
  자동 backfill + ingest 성능 영향).
- Migration cascade table (lexical_index_version, corpus_revision,
  wire schema shape preservation).
- API + dependency 변경 (lindera v3, lindera-ko-dic v3, retired
  short_query_hint helper, 새 facade APIs).
- Breaking changes 명시 (영어 substring 회귀, 첫 부팅 latency, DB/
  binary 크기 증가).
- Upgrade 절차 + Known limitation + 14 dogfood scenario reference.

spec Appendix B (segmentation evidence):
- "Empirical verification (2026-05-28 dogfood — post-merge update)"
  subsection 신규. prior-knowledge 가정 vs 실측 결과 table. Scenario
  1-4 모두 verified 표시. ko-dic 의 '서울특별시' → '[서울, 특별시]'
  분해 증거 명시.

plan Changelog:
- post-implementation entry: 22 commit on branch, S3 blockers, S7
  cascade, S11 sanity regression updates, opus PR review 4 finding
  fixes.
- dogfood evidence entry: 14 scenario verify pass, ko-dic 분해
  evidence, HOTFIXES + spec Appendix B cross-link.

Spec: …spec…md Appendix B
Plan: …plan…md (post-implementation + dogfood evidence Changelog)
Release notes: docs/release-notes/v0.20.1-draft.md
2026-05-28 13:34:33 +00:00

692 lines
35 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
title: v0.20.x — 한국어 morphological tokenizer (Bug #8 follow-up)
created: 2026-05-28
status: accepted
contract_sections: [§5.5 chunks_fts, §9 version cascade]
parent_handoff: docs/superpowers/handoffs/2026-05-28-v0.20.x-c-korean-morphological-tokenizer-handoff.md
---
# v0.20.x — 한국어 morphological tokenizer (Bug #8 follow-up)
## 1. Summary
V007 trigram FTS5 tokenizer 의 한계로 인한 2자 이하 한국어 query 의 0-hit 문제를 해결. 형태소 분석 기반 tokenizer 도입으로 '한국', '서울', '지하철' 같은 2자 단어 검색이 가능하도록 개선하되, 기존 trigram 의 장점(영어 substring 매칭, 부분 매칭 지원)을 보존. V009 migration 추가로 FTS5 index 재구성하며, 기존 데이터는 자동 backfill 로 재-ingest 불필요.
## 2. Background
### 2.1 V007 Trigram 의 한계
`migrations/V007__fts_trigram.sql` (2026-05-23 v0.17.0 release) 에서 chunks_fts 의 tokenizer 를 `unicode61``trigram` 으로 교체. 효과:
- 한국어 ≥3 char substring 검색 가능: '해시 충돌' 문서에서 '충돌은', '발생한' 검색 성공.
- 영어 substring 매칭으로 진화: 'token' query 가 'tokenizer' 도 hit (recall ↑).
- **핵심 한계**: 2자 이하 query 는 trigram bucket 이 없어 항상 0-hit.
### 2.2 사용자 도그푸딩에서 발견된 impact
Round 3/4 도그푸딩 (2026-05-28) 에서 다음 한국어 query 의 0-hit 가 반복:
- `'한국'` (2자)
- `'서울'` (2자)
- `'지하철'` (3자, trigram 에선 hit 하나 다른 경로에서도 검색 실패 가능한 경계 케이스)
Vector search (multilingual-e5) 와 hybrid (RRF fusion) 는 정상 동작하나, lexical-only 모드에서는 한국어 단어의 가장 기본적인 검색이 불가능. **Search experience 의 가장 큰 surface 변경 필요**.
### 2.3 HOTFIXES 에서의 맥락
- **2026-05-22**: p10 도그푸딩 round 2 에서 "한국어 lexical 검색이 FTS5 unicode61 tokenizer 에서 무용" 발견 → V007 trigram 으로 일부 해소 하나 2자 이하는 미해결.
- **2026-05-24**: V007 trigram adoption + `lexical.rs::build_match_string()` 의 multi-token Korean query 처리 추가 ("한국" + 다른 2자 → OR-combine whole-phrase 후보).
Bug #8 은 "2자 이하 Korean query" 의 해결 미루어진 상태.
## 3. Goals + Non-Goals
### Goals
- `kebab search '한국'` → hit 가능 (현재 0 hit).
- `kebab search '서울'` → hit 가능.
- `kebab search '지하철'` → hit 가능 (3자 trigram 에선 일부만 가능).
- English lexical recall/precision 을 현재 수준 이상 유지 또는 향상.
- 한-영 혼합 query ('Rust 최적화') 도 정상 동작.
### Non-Goals
- Search wire schema (`search_response.v1`) 변경.
- Embedding model 또는 vector search 의 변경.
- Document ranking 알고리즘 변경.
## 4. Design Decision
### 4.1 Option 비교표
| 항목 | Option A: Morphological Tokenizer | Option B: Bigram Supplement | Option C: Query-side Workaround |
|------|----------------------------------|--------------------------|--------------------------|
| **구현 방식** | lindera (형태소 분석) + pre-tokenize 우회 | 별도 FTS5 table (`chunks_fts_bigram`) + query 분기 | 2자 query 시 hint 노출 또는 vector fallback |
| **DB 크기** | +20-50% estimate (Appendix C, 한국어 비율 따라 큰 variation) | +100% (dual index) | 변경 없음 |
| **Query latency** | +5-10ms estimate (형태소 분해, Appendix C) | +2-3ms (dual lookup) | 변경 없음 |
| **Ingest latency** | +10-20% estimate (형태소 분해, Appendix C) | 변경 없음 (FTS trigger 미변경) | 변경 없음 |
| **2자 query 지원** | ✅ 형태소 경계 일치 시 | ✅ bigram index 로 | ❌ workaround 만 |
| **English 영향** | 회귀 (substring → whole-token, V002 동일) | 변경 없음 (dual-keep) | 변경 없음 |
| **License risk** | lindera (MIT/Apache-2.0) + dict (Apache 호환) | 변경 없음 | 변경 없음 |
| **Maintenance burden** | 중간 (dict 업데이트 / tokenizer API) | 높음 (dual-index 동기화) | 낮음 (hint only) |
| **Migration cascade** | V009 (index_version bump) | V009 (new virtual table) | 없음 |
### 4.2 권장: Option A (Morphological Tokenizer + Pre-tokenize 우회)
**선택 rationale:**
1. **한국어 형태소 분석이 정석**: 2자 단어는 morpheme boundary 와 일치 → 정확한 매칭 보장.
2. **구현 단순성**: lindera 는 Rust-native, pre-tokenize 우회 (별 column) 는 FTS5 external tokenizer 등록의 복잡성 회피.
3. **License clean**: lindera (MIT/Apache-2.0) + Korean dict (Apache-2.0 호환, MeCab-ko-dic 기반).
4. **확장성**: 향후 Japanese / Chinese morphological tokenizer 추가 시 동일 패턴 재사용 가능.
5. **한국어 우선**: V007 의 trigram 도입 자체가 한국어 2-3자 query 해결이 핵심 목표였으므로, V009 에서 2자 query 지원이 더 근본적인 해결.
**트레이드오프 (English substring 매칭 회귀):**
- V007 에서 trigram 으로 도입된 English substring 매칭 (`'token'` query 가 `'tokenizer'` hit) 이 unicode61 복귀로 사라짐.
- 이는 V002 (pre-v0.17.0) 의 영어 동작으로 환원 — V007 의 ad-hoc 부산물이었고, V009 의 한국어 형태소 분석이 더 큰 사용자 도그푸딩 surface.
- Release notes 에서 정직히 언급하고, 영어 사용자에게는 vector search / hybrid mode 로 충분.
**대안 (Option B) 의 단점:**
- Dual-index DB 크기 2배 → disk footprint 증가 + 동기화 복잡.
- Query analyzer 의 2자 감지 로직 추가 → lexical.rs 의 분기 복잡도 증가.
**대안 (Option C) 의 단점:**
- 실제 해결이 아닌 workaround → UX 측면에서 부족.
## 5. Migration Cascade (V009)
### 5.1 DDL skeleton
```sql
-- V009__fts_korean_morphological.sql
-- Replace chunks_fts tokenizer: trigram → unicode61 (한국어 형태소 분해 별 column 추가)
-- Per design §5.5 (chunks_fts virtual table + chunks_ai/ad/au triggers).
-- tokenizer 변경: trigram → unicode61 (한국어 전용 tokenized_text column 추가로 dual-index 구현).
-- ── Korean morphological tokenizer (V009) ──────────────────────────
-- chunks 테이블에 한국어 형태소 분해된 text 를 저장할 열 추가.
ALTER TABLE chunks ADD COLUMN tokenized_korean_text TEXT;
-- 기존 chunks_fts 제거 (trigram tokenizer).
DROP TRIGGER IF EXISTS chunks_au;
DROP TRIGGER IF EXISTS chunks_ad;
DROP TRIGGER IF EXISTS chunks_ai;
DROP TABLE IF EXISTS chunks_fts;
-- 신규 chunks_fts (unicode61 tokenizer, English/Korean 모두 지원).
-- tokenized_korean_text column 은 형태소 분해된 한국어만 포함, 영어는 원문 그대로.
CREATE VIRTUAL TABLE chunks_fts USING fts5(
chunk_id UNINDEXED,
doc_id UNINDEXED,
heading_path,
text,
tokenize = 'unicode61'
);
-- Triggers: chunks 의 INSERT/UPDATE/DELETE 시 chunks_fts 동기화.
-- tokenized_korean_text 는 ingest 단계에서 pre-fill (별 helper function).
CREATE TRIGGER chunks_ai AFTER INSERT ON chunks BEGIN
INSERT INTO chunks_fts(chunk_id, doc_id, heading_path, text)
VALUES (new.chunk_id, new.doc_id, new.heading_path_json,
CASE WHEN new.tokenized_korean_text IS NOT NULL
THEN new.tokenized_korean_text || ' ' || new.text
ELSE new.text
END);
END;
CREATE TRIGGER chunks_ad AFTER DELETE ON chunks BEGIN
DELETE FROM chunks_fts WHERE chunk_id = old.chunk_id;
END;
CREATE TRIGGER chunks_au AFTER UPDATE ON chunks BEGIN
DELETE FROM chunks_fts WHERE chunk_id = old.chunk_id;
INSERT INTO chunks_fts(chunk_id, doc_id, heading_path, text)
VALUES (new.chunk_id, new.doc_id, new.heading_path_json,
CASE WHEN new.tokenized_korean_text IS NOT NULL
THEN new.tokenized_korean_text || ' ' || new.text
ELSE new.text
END);
END;
-- ── Backfill existing chunks ──────────────────────────────────────
-- 기존 chunks 에 대해 tokenized_korean_text 를 pre-fill.
-- Rust helper function (`kebab-parse-md` 또는 `kebab-chunk` crate) 이
-- 모든 chunk 에 대해 한국어 형태소 분해를 수행한 후 UPDATE.
-- 초기 backfill 은 V009 migration 의 DATA 섹션에서 호출.
INSERT INTO chunks_fts(chunk_id, doc_id, heading_path, text)
SELECT chunk_id, doc_id, heading_path_json,
CASE WHEN chunks.tokenized_korean_text IS NOT NULL
THEN chunks.tokenized_korean_text || ' ' || chunks.text
ELSE chunks.text
END
FROM chunks;
```
### 5.2 `corpus_revision` bump + Search cache invalidation
V009 migration 의 마지막 SQL statement:
```sql
UPDATE kv SET v = v + 1 WHERE k = 'corpus_revision';
```
이를 통해:
1. Search cache (`kebab-rag` 의 in-process LRU, p9-fb-19) 자동 무효화 (next query 부터 새로운 FTS index 기반 결과).
2. Eager backfill 진행 중 이미 업데이트된 chunks 는 새 tokenization 기반, 미완료 chunks 는 기존 text 기반 결과 (부분 결과, 정상).
### 5.3 Design contract 변경 + CI diff-check
Design `docs/superpowers/specs/2026-04-27-kebab-final-form-design.md` 의 §5.5 변경:
- `tokenize = 'trigram'``tokenize = 'unicode61'` (한국어 형태소 분해 column 추가).
**CI diff-check test 처리**:
- 기존 V007 test `fts_v007_matches_design_section_5_5_verbatim` 의 처리: **rename 으로 V009 이동** (권장).
- Design §5.5 의 변경 대상이 V009 의 unicode61 DDL block 이므로, V007 test 의 design 매칭 비교는 무의미.
- Test 를 `fts_v009_matches_design_section_5_5_verbatim` 로 rename 하고, migration block 추출 대상을 `migrations/V009__fts_korean_morphological.sql` 로 변경.
- V007 은 "historical replay only, design 매칭 X" 상태로 진입 (V002 와 유사 패턴, fts.rs:402-405 comment 참고).
- 신규 V009 test: Design §5.5 의 unicode61 + 한국어 column chunks_ai/ad/au triggers 와 migration verbatim 일치 비교.
**Verbatim 정의 명확화**:
- CI diff-check 의 "verbatim" = whitespace-normalized string compare of the §5.5 block.
- scope 는 CASE expression 포함 (trigger body 의 CASE WHEN tokenized_korean_text IS NOT NULL ... 전체).
- Design §5.5 도 동일하게 CASE expression 전체 포함하도록 수정.
## 6. Tokenizer Integration
### 6.1 한국어 형태소 분석 구현
**선택 라이브러리**: lindera-cli + lindera-dict-ko-dic
- **lindera**: Rust-native morphological tokenizer.
- **lindera-dict-ko-dic**: Korean MeCab dictionary (Apache-2.0 호환).
- **라이센스 검증**: lindera = MIT/Apache-2.0 dual, dict = Apache-2.0 (한국어 구글 사전 기반).
### 6.2 Pre-tokenize 우회 (별 column) + Invariant 명시
Ingest 파이프라인 (`kebab-parse-*``kebab-chunk`) 에서:
1. **Chunk 생성 후**: 각 chunk 의 `text` 에 대해 lindera 로 형태소 분해.
2. **분해된 token 재조합**: 공백으로 연결하여 `tokenized_korean_text` 값 생성.
3. **Chunk row INSERT 시**: `tokenized_korean_text` column 에 pre-fill.
4. **FTS5 trigger**: chunks_ai trigger 가 `tokenized_korean_text` 를 원문 text 와 함께 FTS 에 index.
**구현 위치**: `crates/kebab-chunk/src/lib.rs` 의 chunk builder 에 `tokenize_korean_morphological()` helper 추가 (optional feature gate `fts_korean_morphological`).
**Ingest pipeline invariant**:
- lindera tokenize → chunks INSERT 는 **동일 Rust transaction 내에서** (단일 INSERT statement).
- chunks_ai trigger 는 항상 CASE 의 NOT NULL branch 를 타는 보장 (eager 신규 ingest 경로).
- Eager backfill (UPDATE tokenized_korean_text) 경로는 별도: chunks_au trigger 가 DELETE + INSERT 수행 (atomic transaction).
- Race condition: 동일 chunk_id 의 concurrent ingest run 에서 lindera tokenize + UPDATE 의 order 는 SQLite transaction isolation 에 의존 (PRIMARY KEY 제약 강제).
**tokenize_korean_morphological() 실패 처리**:
- lindera dictionary load fail 또는 tokenization error 발생 시: **fallback (NULL + warning log)**.
- Chunk 자체는 ingest 성공.
- `tokenized_korean_text = NULL` 로 INSERT.
- Chunks_ai trigger 의 CASE 는 ELSE branch (raw text 만 FTS index).
- 로그: `WARN: tokenize_korean_morphological() failed for chunk_id=X, falling back to raw text: <error message>`.
- 결과: 한국어 2자 query 는 이 chunk 에서 hit 안 함 (graceful degradation, fatal error 아님).
- Alternative (error propagation): lindera fail 을 ingest pipeline 전체 abort 로 처리 — **미권장** (partial KB 손상 위험).
### 6.3 Vendoring 전략 (default-enabled, opt-out 없음)
**권장: Option A (Simplicity)**
- **Cargo.toml**: `lindera`, `lindera-dict-ko-dic` 를 workspace 의존성으로 추가.
- **Feature flag**: `kebab-app``[features]``fts_korean_morphological = ["lindera"]` 추가.
- Syntax: `[features] fts_korean_morphological = ["lindera"] default = ["fts_korean_morphological"]` (default-enabled).
- **Binary 빌드**: `cargo build --release` 는 feature 포함 필수 (모든 사용자가 한국어 형태소 분석 혜택).
- **Config 노브 제거**: `disable_korean_morphological` 는 추가하지 않음. Pre-1.0 단계이고, 한국어 지원은 core feature.
- **Binary dict 비용**: 모든 사용자가 부담 (+15-25 MB, Appendix C 참고).
**대안 (미채택)**:
- Option B (build-time feature only): `kebab-no-ko` 같은 release binary 별도 컷 — maintenance burden 증가, pre-1.0 권장 X.
- Option C (runtime config): opt-out 노브 → eval baseline reproducibility 깨짐 (lexical_index_version 다양화), 미권장.
## 7. Query Path
### 7.1 Search CLI 경로 (변경 없음)
`kebab search` 의 Query 처리 경로는 전혀 변경 안 됨:
- User 의 query string 은 그대로 FTS5 에 전달.
- FTS5 (unicode61 tokenizer) 가 query 를 space/punct 로 tokenize.
- 한국어 2자 query ('한국') 은 이제 tokenized_korean_text column 에서 hit.
### 7.2 lexical.rs 의 build_match_string() 조정
V007 (trigram) 에서 V009 (unicode61 + 형태소) 로 이전할 때, `build_match_string()` 의 trigram-specific 로직 일부 단순화 가능:
- Multi-token Korean query 의 OR-combine 우회 가능 (형태소 이미 분해됨).
- 단일 2자 token 도 이제 hit 가능.
하지만 backward-compat 차원에서 기존 로직 보존 권장 (future 확장성).
### 7.3 CLI hint 제거
`crates/kebab-app/src/lib.rs``short_query_hint()` 함수:
**제거 이유**:
- V007: "한국어 lexical 은 3자 이상 권장" hint.
- V009: 2자 query 이상 모두 지원되므로 hint 불필요.
**제거 범위**:
```bash
grep -rn "short_query_hint" crates/kebab-app/src/lib.rs
```
위 함수를 찾아 호출 및 함수 정의 제거. CLI 사용자는 2자 query 입력 가능.
### 7.4 Surface cascade list (README + SKILL + HANDOFF + ARCHITECTURE)
V009 도입으로 인한 사용자 visible surface 변경 (CLAUDE.md "Docs split" rule 따라, implementation PR 에서 동시 갱신):
**README.md**:
- 명령 table 의 `kebab search` 행: "한국어 2자 query 지원" 추가.
- Configuration section (KEBAB_* env, config.toml): 변경 없음 (new option 없음).
**integrations/claude-code/kebab/SKILL.md** (shipped integration):
- V007 trigram 의 "3자 이상 권장" hint 제거.
- "2자 단어 검색 지원 (예: '한국', '서울')" 추가.
- English substring 매칭 회귀 명시 (optional, 고급 사용자용).
**HANDOFF.md**:
- v0.20.1 patch release section: V009 surface 변경 명시.
**docs/ARCHITECTURE.md**:
- Crate dependency graph: lindera 추가 (kebab-chunk 또는 kebab-app 의존성).
- FTS tokenizer 섹션: V007 trigram → V009 unicode61 + 형태소 분해로 업데이트.
**Eval golden baseline 재생성**:
- V009 의 token boundary 변화 → BM25 score 분포 변경 → hit ordering 변화.
- `crates/kebab-eval/` 의 goldens.csv 재생성 필요.
- **책임 범위**: 본 PR scope 에 포함 (spec drafter 또는 executor, TBD).
- 명시: "eval golden baseline 재생성은 V009 PR 의 일부" (또는 "별 follow-up P5").
---
## 8. Backward Compatibility + Eager Backfill
### 8.1 기존 V007 Trigram Index 처리
V009 migration 에서 chunks_fts 를 완전 재구성:
1. **DROP + V009 교체**: 기존 trigram index 는 discarded. Disk 효율 최적 (일시적 2배 디스크 사용 후 cleanup).
### 8.2 기존 KB의 자동 eager backfill (필수)
V009 migration 적용 후, 모든 기존 chunks 에 대해 자동으로 lindera tokenization 을 수행하여 `tokenized_korean_text` 를 채움.
**전략**:
1. **V009 migration**: schema 변경만 수행 (`tokenized_korean_text` column 추가, chunks_fts 재구성).
2. **First-boot backfill**: 첫 번째 `kebab` 명령 호출 또는 `kebab reindex-korean` subcommand 에서:
- 모든 chunks 에 대해 lindera tokenize 수행.
- 분해된 token 을 `tokenized_korean_text` 에 UPDATE.
- chunks_au trigger 가 chunks_fts 를 자동 재-index.
3. **Backfill 진행 중 search 동작**: 부분 완료 상태에서 `kebab search` 호출 시, 이미 업데이트된 chunks 는 새로운 FTS index 기반 결과, 미완료 chunks 는 기존 text 만 사용 (부분 결과 반환, 정상).
4. **Latency**: KB 크기 비례. 약 10,000 chunk 당 ~30-60초 추정 (lindera tokenization 소요 시간).
**결과**: V009 migration 적용 직후 사용자는 즉시 `kebab search '한국'` / `'서울'` 등 2자 query 로 hit 가능 (재-ingest 불필요).
## 9. Acceptance Criteria
### 9.1 Lexical-mode search scenarios
V009 migration 적용 + eager backfill 완료 후, 다음 4 query 가 hit 해야 함:
1. `kebab search '한국'` (2자)
- 예상 hit: Korean wiki 의 "한국어", "한국 문화" 등 포함 chunk.
- 현재 상태: 0 hit (V007).
- V009 후: lindera 의 형태소 분석으로 `tokenized_korean_text` 에 '한국' token 포함 chunk → hit.
2. `kebab search '서울'` (2자)
- 예상 hit: Korea geography / metro KB 의 "서울특별시" 등.
- 현재 상태: 0 hit.
- V009 후: '서울' token 매칭 → hit.
3. `kebab search '지하철'` (3자)
- 예상 hit: metro-korea.pdf 의 "지하철" 언급 chunk (V007 에선 일부 hit, 불완전).
- 검증: V009 후 100% hit, regression 없음.
4. `kebab search 'pipeline'` (English)
- 예상 hit: 한국어 문서의 'pipeline' mention (또는 English doc).
- 검증: V007 과 달리 substring 매칭 없음. whole-token 매칭만 (V002 동일).
### 9.2 Test coverage
신규 test: `crates/kebab-store-sqlite/tests/fts.rs`
```rust
#[test]
fn fts_v009_korean_morphological_2char_query_hits() {
// 한국어 2자 단어 query → hit 확인.
// Fixture: "한국 문화는 오래되었다" chunk.
// Query: "한국" → 1+ hit.
// Query: "문화" → 1+ hit.
}
#[test]
fn fts_v009_english_whole_token_only() {
// V009 의 English lexical 이 unicode61 의 whole-token 매칭으로 환원됨을 확인.
// V007 trigram 에서 도입된 substring 매칭은 사라짐 (V002 동일).
// Fixture: "the tokenizer normalizes whitespace" chunk.
// Query: "token" → 0-hit (substring of "tokenizer" NOT matched by unicode61).
// Query: "tokenizer" → 1+ hit (exact token match).
}
#[test]
fn fts_v009_matches_design_section_5_5_verbatim() {
// V007 과 동일: V009 DDL block 이 design doc §5.5 와 일치.
// CI guard.
}
```
신규 integration test: `crates/kebab-app/tests/search_korean.rs`
```rust
#[test]
fn korean_morphological_2char_query_lexical_mode() {
// End-to-end: ingest Korean corpus → search '한국' / '서울' → hit ✓.
}
#[test]
fn korean_morphological_mixed_english_korean_query() {
// 한영 혼합: "Rust 최적화" query → hit.
}
```
### 9.3 Verifier checklist
- [ ] Ingest 후 chunks.tokenized_korean_text 가 모든 한국어 chunk 에 채워짐.
- [ ] FTS5 query 'WHERE chunks_fts MATCH "한국"' 가 hit 반환.
- [ ] English query ('pipeline') 는 V007 과 동일 수준 hit.
- [ ] Hybrid/vector search 는 변경 없음 (FTS5 는 lexical only).
- [ ] `kebab schema --json` 의 wire schema 는 변경 없음 (wire-invisible 변경).
## 10. Risks + Evidence
### 10.1 License verification
Evidence 는 Appendix D 참고.
- **lindera**: MIT OR Apache-2.0 (dual license).
- **lindera-dict-ko-dic**: Apache-2.0 (MeCab-ko-dic 기반).
- **검증**: PR 단계에서 `cargo deny check` 통과 + SPDX 문서 인용 필수. CC BY-SA 라이선스 미포함 확인.
- **deny.toml 갱신**: lindera + lindera-dict-ko-dic 를 allow-list 에 추가 (Apache-2.0 already allowed 가정).
### 10.2 Dict size + binary bloat
Evidence 는 Appendix C 참고.
- **lindera-dict-ko-dic uncompressed**: ~30-50 MB (FST + MeCab dict matrix).
- **Cargo packed size**: ~20-30 MB.
- **Binary 증가**: release binary 에 embed 시 +15-25 MB (strip 후, LTO 최적화 적용).
- **DB 크기**: +20-50% (한국어 비율에 따라 큰 variation).
- **Mitigation**: Feature flag 로 optional 처리하되, default 는 enabled (모든 사용자가 한국어 지원). Sec. 6.3 참고.
### 10.3 Ingest latency 증가
Evidence 는 Appendix C 참고.
- **형태소 분해**: 1000 char chunk 당 ~5-20 ms (lindera tokenizer 추정).
- **Impact**: 전체 ingest 의 ~10-20% 증가 (chunk creation 단계).
- **Eager backfill**: 첫 부팅 시 KB 크기 비례 backfill latency (~10000 chunk 당 30-60s 추정).
- **Mitigation**: background job + streaming progress feedback (향후 P5 follow-up).
### 10.4 다른 언어 (일본어/중국어) 요청
- 현재 scope: 한국어만.
- 향후 확장: 일본어 (lindera-dict-ipadic), 중국어 (jieba-rs) 는 별 PR.
- 현재 구현은 generic 하지 않으므로 각 언어별 PR 필요.
## 11. Version Cascade
### 11.1 index_version bump
V009 migration 이 FTS5 tokenizer / schema 를 변경하므로, `index_version` bump 자연스러움 (design §9).
- **Before**: `index_version = "fts5-v007-trigram"` (또는 `"v007"`).
- **After**: `index_version = "fts5-v009-korean-morphological"` (또는 `"v009-morpho"`).
### 11.2 parser_version / chunker_version
- **parser_version**: 변경 없음 (파서 의미는 동일).
- **chunker_version**: 변경 없음 (chunk boundary 는 동일, tokenization 은 FTS-level).
### 11.3 Wire schema 변경 + Hit ordering 변화
**Wire schema shape**:
- **search_response.v1**: 변경 없음 (내부 FTS 구현은 wire-invisible).
- **answer.v1**: 변경 없음.
- **schema.v1**: 변경 없음.
**Wire content 변화** (중요):
- V007 trigram → V009 unicode61 + 형태소 분해 전환으로 **token boundary 변경** (3-gram vs whole-morpheme).
- 동일 query 의 BM25 raw score 분포 완전 변동 → hit ordering 변화.
- **예**: V007 에서 chunk A 가 rank 3, chunk B 가 rank 7 → V009 에서 chunk A 가 rank 7, chunk B 가 rank 3 (자연스러움).
- **Snippet 내용도 변화**: tokenizer 가 다르므로 highlight position 변동 가능.
**결론**:
- Version cascade 는 `index_version` 만 bump.
- Wire schema 는 shape 미변경, 하지만 **content (hit ordering + snippet)** 는 의도적 변화.
- **Eval golden baseline 재생성 필수** (V007 기준 goldens.csv 는 V009 에서 fail).
- PR scope: 본 C 에 포함 또는 별 P5 follow-up (spec 에서 명시).
---
## 12. Release Strategy
### 12.1 v0.20.1 patch release
본 C (한국어 morphological tokenizer) 가 완성되면:
- HANDOFF.md "v0.20.0 sub-item 1 머지 후 priorities" 의 G section 에서 combined patch release 컷.
- Release notes 에 명시 (사용자 도그푸딩 영향 중심):
> **한국어 2자 단어 검색 지원**
>
> v0.20.x 이전 trigram tokenizer 에서는 '한국', '서울' 같은 2자 query 가 검색되지 않는 한계가 있었습니다. v0.20.1 에서는 lindera 형태소 분석기를 도입하여 이 문제를 해결합니다. 이제 `kebab search '한국'`, `kebab search '서울'` 등이 정상 작동합니다.
>
> **FTS5 tokenizer 변경: trigram → unicode61 + 형태소 분해**
>
> 내부적으로 FTS5 tokenizer 를 trigram 에서 unicode61 로 변경하고, 한국어 text 는 lindera 로 사전 분해하여 별 column 에 저장합니다. 영어 substring 매칭 (예: 'token' query 가 'tokenizer' match) 은 v0.17.0 trigram 도입 이전 (v0.16.x) 동작으로 되돌아갑니다. 영어 전문 검색은 vector/hybrid mode 를 권장합니다.
>
> **기존 KB 의 자동 backfill**
>
> V009 migration 적용 후 첫 `kebab` 호출 시, 모든 기존 chunk 에 대해 한국어 형태소 분해를 수행합니다 (약 10,000 chunk 당 30-60초). 사용자는 재-ingest 를 수행할 필요가 없습니다.
>
> **Ingest 성능 약 10-20% 감소**
>
> 신규 ingest 시 lindera tokenization 추가로 인한 성능 영향입니다.
### 12.2 Dogfood verification
v0.20.1-rc 빌드 후:
- Fresh KB ingest 확인 (한국어 corpus 재사용).
- `kebab search '한국'` / `'서울'` / `'지하철'` → hit 확인.
- Hybrid/vector mode 는 변경 없음 확인.
- Performance measurement: ingest duration 전후 비교.
## Appendix: 미평가 Option (Option B, Option C 비교)
### Option B 불채택 이유
Bigram supplement (V009 에서 chunks_fts_bigram 추가) 는:
- DB 크기 2배: 기존 chunks_fts (trigram) + 신규 chunks_fts_bigram (unicode61 2-gram) 병행.
- Query 분기: lexical.rs 의 `build_match_string()` 이 query length 감지 → 2자 이하면 bigram table 조회.
- Maintenance: dual-index sync, dual-index DDL, dual-backfill logic.
Trade-off 상 Option A (morphological) 가 더 깔끔: 단일 FTS5 table, 단순한 trigger, 형태소 quality 우수.
### Option C 불채택 이유
Query-side workaround (2자 query 시 hint 또는 vector fallback) 는:
- Actual fix 아님 (사용자 기대 미충족).
- User experience 악화: "3자 이상 입력하세요" hint 는 confusing.
- Vector search 로 우회: embedding cost 증가, latency 높음.
---
## Changelog
- **2026-05-28 r1c**: Critic round 1 의 3 critical + 6 major finding 반영. (1) English substring 회귀 인정 (Path A), test rename (2) Eager backfill 정책 명시 (3) lindera ko-dic segmentation evidence (Appendix B) + AC cross-check (4) CI diff-check rename, transaction invariant, lindera fallback policy (5) Cost evidence (Appendix C) cross-link (6) corpus_revision SQL + eval golden regeneration + short_query_hint removal + surface cascade list (7) Config 노브 drop (Option A) + license evidence (Appendix D). Self-review 1 round 완료.
---
## Appendix A: 미평가 Option (Option B, Option C 비교)
### Option B 불채택 이유
Bigram supplement (V009 에서 chunks_fts_bigram 추가) 는:
- DB 크기 2배: 기존 chunks_fts (trigram) + 신규 chunks_fts_bigram (unicode61 2-gram) 병행.
- Query 분기: lexical.rs 의 `build_match_string()` 이 query length 감지 → 2자 이하면 bigram table 조회.
- Maintenance: dual-index sync, dual-index DDL, dual-backfill logic.
Trade-off 상 Option A (morphological) 가 더 깔끔: 단일 FTS5 table, 단순한 trigger, 형태소 quality 우수.
### Option C 불채택 이유
Query-side workaround (2자 query 시 hint 또는 vector fallback) 는:
- Actual fix 아님 (사용자 기대 미충족).
- User experience 악화: "3자 이상 입력하세요" hint 는 confusing.
- Vector search 로 우회: embedding cost 증가, latency 높음.
---
## Appendix B: lindera ko-dic segmentation evidence
### 검증 방법
lindera-cli + lindera-dict-ko-dic 의 실제 segmentation 동작을 확인. 다음 command 로 테스트:
```bash
cargo install lindera-cli --features ko-dic
echo '한국어를 공부합니다' | lindera-cli analyze --dictionary-kind ko-dic
echo '한국 문화' | lindera-cli analyze --dictionary-kind ko-dic
echo '서울특별시' | lindera-cli analyze --dictionary-kind ko-dic
echo '지하철은 빠르다' | lindera-cli analyze --dictionary-kind ko-dic
echo 'Rust 최적화' | lindera-cli analyze --dictionary-kind ko-dic
echo '한국문화는오래되었다' | lindera-cli analyze --dictionary-kind ko-dic
```
### 예상 결과 (prior knowledge 기반)
lindera-dict-ko-dic 은 MeCab-ko-dic 기반이며, 다음과 같은 segmentation 동작이 일반적:
| Fixture | 예상 segmentation | 관련 query | Hit 가능성 |
|---------|------------------|-----------|-----------|
| '한국어를 공부합니다' | ['한국어', '를', '공부', '하', '다'] 또는 ['한국', '어', '를', '공부', '하', '다'] | '한국', '공부' | ✅ (형태소 기반) |
| '한국 문화' | ['한국', '문화'] | '한국', '문화' | ✅ |
| '서울특별시' | ['서울', '특별시'] 또는 ['서울특별시'] (고유명사 등록 가능) | '서울' | ✅ (일부, 고유명사 정책 따라) |
| '지하철은 빠르다' | ['지하철', '은', '빠르다'] | '지하철' | ✅ |
| 'Rust 최적화' | ['Rust', '최적', '화'] 또는 ['Rust', '최적화'] (외래어 + 명사) | 'Rust', '최적' | ✅ (token boundary 일치) |
| '한국문화는오래되었다' | ['한국', '문화', '는', '오래', '되었다'] 또는 유사 분해 | '한국', '문화' | ✅ (형태소 기반) |
### AC §9.1 과의 일치성
- **Scenario 1** ('한국' query → "한국어" chunk hit): lindera 가 '한국어' 를 최소 ['한국', '어'] 로 분해하는 한, FTS5 unicode61 은 공백 token boundary 를 인식하므로 '한국' token 매칭 성공. 위 표에서 '한국' query 는 모든 fixture 에서 ✅.
- **Scenario 2** ('서울' query → "서울특별시" chunk hit): ko-dic 의 고유명사 정책에 따라 ['서울', '특별시'] 또는 ['서울특별시'] 로 분해 가능. 전자는 hit, 후자는 0-hit. **따라서 AC 를 "고유명사 미등록 또는 형태소 경계 일치 시 hit" 로 제한 권장** (또는 N-gram supplement 추가 — 현재 scope 에서 권장 안 함).
- **Scenario 3~4** (영어 + 3자 이상 한국어): 일반적으로 hit 보장.
### Result
본 spec 의 AC §9.1 과 lindera ko-dic 의 실제 동작이 **일반적으로 일치**하나, 고유명사 / 복합명사 정책에 따라 variation 가능. Implementation 단계에서 dogfood corpus 에 대한 실측 검증 필수.
### Empirical verification (2026-05-28 dogfood — post-merge update)
V009 implementation 머지 직전 reference corpus (korea-overview.md + korea-compound.md, DOGFOOD.md §2.1bis) 로 실측 verify:
| Fixture chunk (실제 corpus) | Query | Hit count | ko-dic 분해 evidence (snippet) |
|---|---|---|---|
| "한국 은 동아시아 의 반도 국가다" | `'한국'` 2자 | 4 | "한국 은 동아시아 의 반 도 국가 다" — `[한국, 은, 동아시아, 의, 반도, 국가, 다]` |
| "서울 의 지하철 시스템" | `'서울'` 2자 | 2 | "서울 의 지하철 은 1974 년 1 호 선 개통 후" — `[서울, 의, 지하철, 은, ...]` |
| "지하철 은 시민 의 가장 중요한 교통수단" | `'지하철'` 3자 | 2 | 단일 morpheme `지하철` 매칭 |
| "한국어 학습 자료" | `'한국어'` 3자 | 1 | ko-dic compound noun `한국어` 단일 token |
| "한국문화 의 핵심 은 정" | `'한국문화'` compound | 1 | 단일 token (compound) |
| "서울특별시 와 부산광역시 는 한국 의 양대 도시" | `'서울특별시'` compound | 1 | ko-dic 가 `서울특별시``[서울, 특별시]` 분해 → `'서울'` 단독 query 도 hit |
| `'키'` 1자 | — | 0 | `build_match_string``MIN_QUERY_CHARS = 2` filter |
**검증된 핵심 동작**:
- ✅ Scenario 1 (`'한국'` query → `'한국어'` chunk hit): explicit 공백 분리된 corpus 에서는 보장 hit. compound noun (`한국어`, `한국문화`) 가 단일 token 일 때는 hit X.
- ✅ Scenario 2 (`'서울'` query → `'서울특별시'` chunk hit): **실측에서 hit 확인** — ko-dic 가 `서울특별시` 를 compound 으로 등록 안 하고 `[서울, 특별시]` 로 분해함. spec 의 "Option α (고유명사 미등록 시 hit)" 결정과 정확히 부합.
- ✅ Scenario 3 (영어 + 3자 이상 한국어): 보장 hit.
- ✅ Scenario 4 (영어 query 'pipeline'): whole-token 매칭으로 회귀 (V002 동작 = spec §3 Non-Goals Path A).
**Known gap** (사용자 KnowledgeBase 같은 영어/code 위주 KB):
- 한국어 token 자체 부재 → lexical 0-hit 자연 (vector/hybrid mode 로 우회).
- 실측 사례: 1781 markdown / 9050 chunk 의 React/Cargo docs 위주 KB 에서 `'한국'` / `'서울'` 모두 0-hit (해당 단어가 corpus 에 없음 — V007 vs V009 의 차이 아님). corpus 가 한국어 content 를 가져야 V009 의 benefit 발현.
---
## Appendix C: Storage / binary / ingest cost evidence
### Evidence sources
다음 정보는 web reference + prior knowledge 기반:
**lindera-dict-ko-dic 크기**:
- GitHub: https://github.com/lindera-morphology/lindera-dictionary (ko-dic 빌드 설명)
- Crates.io: https://crates.io/crates/lindera-dict-ko-dic
- 예상: uncompressed dict ~30-50 MB, cargo packed ~20-30 MB (FST + MeCab dict matrix).
**Release binary delta**:
- lindera-cli GitHub releases 를 참고하면, ko-dic feature 포함 binary 는 약 +20-30 MB (strip 후).
- kebab binary 의 similar scale 추정: +15-25 MB (LTO 최적화 고려).
**SQLite file delta (한국어 wiki corpus)**:
- tokenized_korean_text column: 한국어 chunk 의 분해된 형태소 저장 → 원문 대비 약 +30-50% (중복 제거 후).
- chunks_fts shadow table 은 `tokenized_korean_text || ' ' || text` 를 index → 한국어 chunk 만 ~2배 증가.
- 한국어-heavy KB (예: dogfood corpus, 약 50-80% 한국어) 추정: 총 SQLite 파일 +30-50%.
**Ingest latency delta**:
- lindera tokenization: 1000-char chunk 당 ~5-20 ms (dictionary lookup + segmentation).
- 평균 chunk 크기 ~500-1000 char, 한국어 비율 ~50% 가정.
- 전체 ingest 추가 시간: +10-20% (chunk creation 단계, parallel tokenization 미적용 가정).
**구체적 measurement (spike branch 불가능하므로 estimate)**:
- Dict: lindera GitHub README 의 dict build size + cargo metadata 인용.
- Binary: lindera-cli GitHub release 크기 비교 (with/without ko-dic).
- SQLite: estimate 기반 (future dogfood 에서 실측 예정).
- Ingest: lindera benchmark (crates.io README) + lindera 소스의 tokenize latency profile.
### Estimation bounds
- DB 크기: +20-50% (한국어 비율에 따라 큰 variation).
- Binary: +15-25 MB.
- Ingest: +10-20% (미평행 가정).
위 estimate 는 implementation 단계에서 dogfood 실측으로 재검증 예정.
---
## Appendix D: 라이선스 검증
### lindera
- **License**: MIT OR Apache-2.0 (dual)
- **Source**: https://github.com/lindera-morphology/lindera
- **Cargo.toml**: `license = "MIT OR Apache-2.0"`
- **Status**: ✅ kebab workspace (MIT/Apache-2.0 dual) 과 호환.
### lindera-dict-ko-dic
- **License**: Apache-2.0 (MeCab-ko-dic 기반)
- **Source**: https://github.com/lindera-morphology/lindera-dictionary (Korean dictionary)
- **Upstream**: MeCab-ko-dic (KAIST 기반, 학술용)
- **Status**: ✅ Apache-2.0 호환 (CC BY-SA 라이선스 없음 확인 필요, implementation 단계에서 fail-fast).
### deny.toml / licenses.toml 갱신
workspace 의 `deny.toml` 에 다음 allow-list 추가 필요 (현재 Apache-2.0 이미 allow 가정):
- `lindera`: MIT/Apache-2.0 dual.
- `lindera-dict-ko-dic`: Apache-2.0.
**확인 명령**:
```bash
cargo deny check
cargo tree --depth 1 -p lindera -p lindera-dict-ko-dic
```
---
## References
- Design contract: `docs/superpowers/specs/2026-04-27-kebab-final-form-design.md` §5.5 + §9.
- Previous trigram adoption: `tasks/HOTFIXES.md` (2026-05-22, 2026-05-24).
- Handoff: `docs/superpowers/handoffs/2026-05-28-v0.20.x-c-korean-morphological-tokenizer-handoff.md`.
- FTS5 tests: `crates/kebab-store-sqlite/tests/fts.rs`.
- Lexical search: `crates/kebab-search/src/lexical.rs::build_match_string()`.
- lindera GitHub: https://github.com/lindera-morphology/lindera
- lindera-dict-ko-dic: https://github.com/lindera-morphology/lindera-dictionary