---
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
