---
title: kebab v0.30.0 release notes (draft)
created: 2026-06-24
status: draft
release_trigger:
  - 신규 config `[ingest.chunking] max_chunk_tokens` — 인터페이스 추가 (pre-1.0 minor)
  - markdown 청커 동작 변경 `md-heading-v1` → `md-heading-v2` (거대 청크 분할 → 검색 hit 분할) — 도그푸딩 트리거
---

# kebab v0.30.0 — md-heading-v2: 거대 청크가 임베딩을 깨지 않도록

v0.29.0(provenance 출처 필터) 후속 minor release. markdown 청커가 **하나의 거대
블록이 임베더 컨텍스트를 초과해 임베딩을 통째로 실패시키던** 문제를 해소한다.
긴 로그·스택트레이스·표가 통으로 들어간 문서(예: jira 이슈)를 색인할 때 그
문서가 검색에서 통째로 사라지던 회귀를 막는다. **일반 사용자는 업그레이드 후
다음 `kebab ingest` 한 번이면 끝** — markdown 자산만 1회 자동 재청크된다.

---

## 변경 사실

**1) `md-heading-v2` 청커가 기본값이 됐다.** v1은 코드/테이블 블록을 (크기와
무관하게) 절대 분할하지 않았고, list·paragraph도 블록 하나가 곧 청크 하나였다.
그래서 거대한 단일 블록 — 예컨대 로그 덤프가 1000줄짜리 불릿 리스트로 변환된 것 —
이 임베더의 입력 한도를 넘으면 그 문서 전체의 벡터 색인이 실패했다. v2는 v1과
**모든 출력이 동일**하되, 마지막에 **예산을 넘는 청크만** 줄(`\n`) 경계로,
한 줄이 홀로 예산을 넘으면 UTF-8 문자 경계로 잘라 각 조각이 예산 이하가 되게 한다.

**2) 신규 config `[ingest.chunking] max_chunk_tokens`** (byte/3 토큰, default
**4000**). 이 값을 넘는 청크가 분할 대상이 된다. 4000은 8192-context 임베더(예:
snowflake-arctic-embed2)에 약 2배 안전마진을 두면서 일반적인 코드·문단은 그대로
한 청크로 유지하는 값이다. `kebab config migrate`로 기존 config에 additive 주입.

```toml
[ingest.chunking]
chunker_version = "md-heading-v2"
max_chunk_tokens = 4000
```

**3) 분할 조각의 chunk_id·citation.** 한 블록에서 나온 분할 조각들은 같은
`block_ids`를 공유하므로 chunk_id에 `#seg{i}` 접미사를 붙여 충돌을 막는다. 분할
조각의 citation은 **블록 단위**(원 블록 범위) — sub-line 정밀도는 아니다.
미분할 청크(대다수)는 v0.29.0과 출력·citation이 완전히 동일하다.

## Trade-off

- **거대 블록이 여러 검색 hit로 나뉜다.** 이전엔 그 문서가 (임베딩 실패로)
  벡터 검색에서 아예 안 나왔거나, ollama처럼 조용히 잘린 채 한 hit였다. 이제는
  여러 조각으로 정상 색인되어 각각 hit로 잡힐 수 있다 — 같은 문서가 상위에 여러
  번 보일 수 있다.
- **분할 조각 citation은 블록 단위다.** fenced 코드의 줄 span이 fence를 포함하는
  비대칭 때문에 조각별 정확한 줄 범위를 (span, code)만으로 복원할 수 없어,
  "절대 틀리지 않되 블록 단위"를 택했다. 정밀 sub-line citation이 필요하면 향후
  파서가 content 줄 범위를 직접 제공하는 개선이 필요하다.
- **byte/3 휴리스틱.** 토큰 수는 실제 토크나이저가 아니라 바이트/3 근사다. CJK는
  과대추정(더 안전), 영문 코드는 실토큰과 비슷. strict 임베더에 대해 보수적이다.

## Mitigation

- **결과·CLI·wire 포맷 불변.** `--json` 스키마, exit code, citation 모양 모두
  동일. 내부적으로 거대 블록이 분할될 뿐이다.
- **공유 `ChunkPolicy` 미변경.** `max_chunk_tokens`는 v2 청커의 policy_hash에만
  fold되어 코드·PDF 청커의 cascade를 건드리지 않는다 — 코드/PDF 자산은 재청크되지
  않는다.
- **strict 임베더 호환.** v1 시절 ollama가 조용히 truncate하던 것을 청커가 애초에
  예산 이하로 만들므로, oversize 입력을 truncate가 아닌 거부(`500 too large`)로
  처리하는 백엔드(예: AMD Lemonade)에서도 색인이 깨지지 않는다.
- **이미지 OCR 텍스트도 보호.** 분할 판정은 청크의 실제 임베드 text 크기로 한다 —
  이미지 청크는 내부적으로 token_estimate가 0이지만 그 OCR/캡션 text는 클 수 있어
  (빽빽한 스크린샷), 실제 text 길이로 판정해야 oversize 이미지 OCR도 분할된다.

### Known limitation

PDF는 별도 청커(`pdf-page-v1.1`)를 쓰므로 이 oversize-split이 적용되지 않는다 —
초고밀도 scanned page 한 장의 OCR이 한 청크로 예산을 넘으면 그대로 임베드된다
(일반 PDF는 무관). 후속 작업으로 PDF 청커에도 동일 분할을 넣을 수 있다.

## 업그레이드 절차

1. 새 바이너리로 교체.
2. (선택) `kebab config migrate` — config 파일에 `max_chunk_tokens = 4000`을
   additive 주입(주석·값 보존). 안 해도 기본값으로 동작한다.
3. `kebab ingest` — `chunker_version`이 `md-heading-v1` → `md-heading-v2`로
   바뀌었으므로 **markdown 자산이 1회 자동 재청크**된다(`--force-reingest`
   불필요). 코드/PDF 자산은 영향 없음. embedding은 파생물 캐시(V012) 히트로
   대부분 재계산을 피하지만, 분할된 거대 블록의 새 조각은 새로 임베드된다.
4. 대용량 KB라면 첫 ingest가 markdown 재청크로 평소보다 길어질 수 있다(1회성).

### 도그푸딩 evidence

실험 KB(MongoDB 문서 220 + jira 400, arctic-embed-l-v2 @ Lemonade GPU). v2 전:
620 문서 중 2개(`SERVER-22906`=76189토큰 list, `SERVER-23097`=20643토큰 list)가
임베드 실패. v2 후: **620/620, errors=0**, 전 코퍼스 7114 청크가 모두 ≤ 4000
(초과 0). `SERVER-22906`은 14 청크로, `SERVER-23097`은 5 청크로 분할. "WiredTiger
excessive memory cache size" 질의에서 그동안 색인조차 안 되던 `SERVER-22906`이
**1위 결과(0.977)**로 — "임베드 불가"에서 "최상위 검색 결과"로 전환됐다. 출처
필터(`--trust-min primary` 등)는 정상 유지.
