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

35 KiB
Raw Blame History

title, created, status, contract_sections, parent_handoff
title created status contract_sections parent_handoff
v0.20.x — 한국어 morphological tokenizer (Bug 2026-05-28 accepted
§5.5 chunks_fts
§9 version cascade
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 를 unicode61trigram 으로 교체. 효과:

  • 한국어 ≥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

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

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.rsshort_query_hint() 함수:

제거 이유:

  • V007: "한국어 lexical 은 3자 이상 권장" hint.
  • V009: 2자 query 이상 모두 지원되므로 hint 불필요.

제거 범위:

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

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

#[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 로 테스트:

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_stringMIN_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 크기:

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

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.

확인 명령:

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