--- 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` 등)는 정상 유지.