28.4k 문서 도그푸딩이 드러낸 다섯 결함(#228–#232)을 전부 처리한 릴리스. 다섯 건 모두 단위 테스트가 잡지 못하는 종류였다 — 규모에서만 드러나는 성능 붕괴, 조용히 사라지는 내용, 멈춘 것처럼 보이는 진행 표시. minor 트리거는 두 가지다. #232 의 parser_version cascade (pdf-text-v1 → pdf-text-v2, 기존 색인 PDF 재처리 유발)와 신규 config 키 ([ingest.pdf.ocr] render_library / render_dpi). 나머지 셋은 additive wire 변경 + 관측성 개선이라 자체로는 patch 급이다. #232 스캔 PDF 를 페이지 렌더링으로 OCR — CCITTFax/JBIG2/Flate/JPX 가 조용히 버려지던 것을 pdfium 런타임 로드로 커버. 없으면 DCTDecode 폴백 + 왜 건너뛰었는지 명시. 단일 바이너리 유지. 한국어 CCITT 스캔 CER 12.6~15.7% (이 릴리스 이전엔 본문 0자). #229 chunks_fts 삭제를 rowid 조회로 (V016) — 문서 200건 삭제 1590초 → 2.0초. 검색 결과 불변, 재색인 불필요. #230 Lance fragment 압축 + 삭제 배치화 — 2.8만 문서 색인 속도가 분당 32~36건으로 평탄 유지(이전 30.7 → 4.3 단조 하락), 15 GB → 2.8 GB. #228 삭제 sweep 을 진행바·ndjson 로그에 노출 — 무표시 구간이 hang 으로 오인되던 것. sweep 이 취소 플래그도 보게 했다. #231 derivation cache 배칭 + 계측 — 가설은 재현되지 않았다(히트 139초 vs 우회 1180초). 이슈 스스로 예측한 대로 #229 가 해소했다. 도그푸딩 evidence 는 tasks/HOTFIXES.md 2026-08-16 / 2026-08-17 항목. 사용자 영향 설명은 docs/release-notes/v0.33.0-draft.md. 곁들여, `cargo clippy --workspace --all-targets -- -D warnings` 가 main 에서 실패하고 있던 것을 고쳤다(툴체인 상승으로 새 lint 둘이 기존 코드에 걸림). 방치하면 새 위반이 그 그늘에 숨는다는 것을 실제로 겪었다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017c9JwQq8ZkGvYjpKXMiDhF
9.8 KiB
v0.33.0 — 28.4k 문서 도그푸딩이 드러낸 다섯 결함
v0.32.0 이후 실제 코퍼스(나무위키 18,282 + Apache Jira 10,146 = 28,427 문서 / 600,808 청크)로 도그푸딩하면서 등록한 이슈 #228~#232 를 전부 처리한 릴리스. 다섯 건 모두 단위 테스트가 잡지 못하는 종류였다 — 규모에서만 드러나는 성능 붕괴, 조용히 사라지는 내용, 멈춘 것처럼 보이는 진행 표시.
minor bump 트리거는 두 가지다. #232 의 parser_version cascade(pdf-text-v1 → pdf-text-v2, 기존 색인 PDF 재처리 유발)와 신규 config 키([ingest.pdf.ocr] render_library / render_dpi). 나머지 셋은 additive wire 변경 + 관측성 개선이라 자체로는 patch 급이다.
실측 evidence 는 전부 tasks/HOTFIXES.md 의 2026-08-16 / 2026-08-17 dated entry 에 있다.
1. 스캔 PDF 가 인코딩에 따라 통째로 버려지던 문제 (#232) — minor
변경 사실. 스캔 페이지를 래스터화하는 방식이 "페이지에 박힌 JPEG 을 그대로 꺼내기" 에서 "페이지를 렌더링하기" 로 바뀌었다. 예전에는 /Filter 가 정확히 DCTDecode 인 이미지 하나뿐인 페이지만 OCR 됐다. 실제 스캔본에서 흔한 CCITTFaxDecode·JBIG2Decode·FlateDecode·JPXDecode, 필터 체인, 그리고 "배경 + 마스크" 로 나뉜 구조는 전부 걸러졌다.
걸러진 페이지는 조용히 사라졌다. 색인은 "성공" 으로 끝나고, 검색이 안 되는 시점에야 알게 되며, 그때 원인이 PDF 인코더라는 걸 역추적할 방법이 없었다.
trade-off. 렌더러(pdfium)는 공유 라이브러리로만 배포되고 정적 빌드가 없다. 바이너리에 넣으면 kebab 의 단일 실행 파일 성질이 깨지므로 런타임에 찾아 쓰는 방식으로 붙였다. 즉 스캔본 커버리지가 환경에 따라 달라진다 — 라이브러리가 있으면 전부, 없으면 예전과 같이 DCTDecode 페이지만 처리한다.
mitigation. 없을 때 조용히 넘어가지 않는다.
- 페이지마다 이유를 찍는다:
⊘ OCR page 5 건너뜀 — 래스터 없음 — 이 페이지의 인코딩은 페이지 렌더러 없이 읽을 수 없다 - run 요약에 건수가 나온다:
scanned 1 new 1 … ocr-skipped 1 --json에ingest_report.ocr_skipped_pages와pdf_ocr_finished.failure_reason(no_renderer/render_error/unopenable_pdf/ocr_error)kebab doctor의pdf_render가 지금 어느 쪽인지 보고한다
upgrade 절차. 스캔 PDF 를 쓰지 않으면 할 일이 없다. 쓴다면:
libpdfium을 로더가 찾는 곳에 두거나[ingest.pdf.ocr] render_library = "/경로/libpdfium.so"로 지정한다. 공개 빌드는 bblanchon/pdfium-binaries 에 있고 리눅스 x64 기준 7.4 MB 다.kebab doctor로pdf_render가 경로를 보고하는지 확인한다.- 재색인은 자동이다.
parser_version이pdf-text-v2로 올라가서 다음kebab ingest가 기존 PDF 를 다시 처리한다 —--force-reingest를 떠올릴 필요가 없다.
해상도 노브. render_dpi (기본 300) 가 렌더 해상도 요청이고, max_pixels 가 실제 상한이다. PDF 기본값 max_pixels = 2048 이면 A4 는 175 DPI 언저리에서 잘린다. 더 선명하게 읽히고 싶으면 max_pixels 를 올려야 하는데(A4 300 DPI 는 약 3500), 그만큼 큰 이미지를 OCR 엔진이 받는다. 엔진 자체도 256~4096 으로 다시 조인다.
도그푸딩. 정답 텍스트를 아는 한국어 스캔 합성 픽스처(CCITT 3건, 22쪽)를 config 기본 모델 qwen2.5vl:3b 로 읽혔다.
| 문서 | 페이지 | 문자 오류율 |
|---|---|---|
| 브렌다(메이즈 러너 시리즈) | 8 | 15.65% |
| 비행대 스토리 | 8 | 12.55% |
| 구 아놀리엔 | 6 | 15.08% |
22쪽 전부 OCR 성공, 건너뜀 0. 이 릴리스 이전에는 세 문서 모두 본문이 0자였으므로 비교 대상 오류율은 100% 다.
엔진 선택 주의. 범용 멀티모달 모델은 OCR 엔진이 아니다. 같은 픽스처를 gemma3:4b 로 읽혔더니 래스터는 정상인데 출력이 원문과 무관한 환각이었고("이 문서에 스무어라가 포함되어 있습니다"), 해상도가 올라가자 밀집 한국어 페이지에서 180초 타임아웃까지 났다. config 기본값이 qwen2.5vl:3b 인 이유가 이것이다.
2. 어휘 인덱스 삭제가 코퍼스 전체 스캔이던 문제 (#229) — patch (동작 불변)
변경 사실. chunks_fts 의 삭제 트리거가 행을 찾는 기준이 chunk_id 에서 rowid 로 바뀌었다 (마이그레이션 V016, 자동).
chunk_id 는 FTS5 에서 색인되지 않는 컬럼이라 조건을 만족할 색인이 없었고, 청크 한 건 삭제가 인덱스 전체를 훑었다. 문서 삭제·수정 등 chunk 를 지우는 모든 경로가 이 비용을 냈고, 코퍼스가 커질수록 증분 재색인이 느려지는 형태였다.
trade-off. 없다. 컬럼 구성과 토크나이저가 그대로라 검색 결과가 바뀌지 않는다. 실제 KB 에서 네 질의의 상위 20건을 점수와 스니펫까지 해시로 비교했고 마이그레이션 전후가 동일했다.
mitigation / upgrade. 다음 실행 때 자동 적용된다. 60만 청크 기준 32초가 걸리고 재색인은 필요 없다 — chunks 와 임베딩은 손대지 않는다.
도그푸딩. 문서 28,427건 / 청크 600,808개 KB 에서 문서 200건 삭제:
| 소요 | |
|---|---|
| 이전 | 1,590초 |
| 이후 | 2.0초 |
약 800배다.
3. LanceDB 메타데이터가 제곱으로 불어나던 문제 (#230) — patch (동작 불변)
변경 사실. 벡터 스토어에 압축 정책이 생겼고, 삭제가 파일 단위에서 배치 단위로 바뀌었다.
압축이 없던 동안 매 쓰기가 fragment 와 "지금까지의 모든 fragment 를 나열한" manifest 를 남겼고, 메타데이터가 실데이터보다 빠르게 커졌다. 문서 1.1만 건에서 2.5배였던 비율이 1.7만 건에서 7.2배였다.
trade-off. 압축이 도는 순간에는 I/O 를 쓴다. 실측으로 15 GB 를 1.7 GB 로 되돌리는 데 153초였다.
mitigation / upgrade. 자동이다. 별도 조치가 없다.
도그푸딩. 2.8만 문서를 넣는 동안 색인 속도가 분당 32~36건으로 평평하게 유지됐다. 이전에는 30.7건에서 4.3건까지 단조롭게 떨어졌다. LanceDB 크기는 문서가 1.7배 늘었는데도 15 GB 에서 2.8 GB 로 줄었다. 문서 364건 삭제가 만드는 커밋은 364회에서 33회가 됐다.
kebab doctor 에 vector_store 정보성 체크가 생겨 fragment·버전 수를 보여준다(종료 코드 영향 없음).
4. 삭제 구간이 멈춘 것처럼 보이던 문제 (#228) — patch (관측성)
변경 사실. 파일이 지워진 문서를 정리하는 구간(sweep)이 진행 표시와 로그에 나온다.
이 구간은 신호를 하나도 내지 않았다. 진행바는 색인 총계를 표시한 채 멈춰 보이고 ndjson 로그는 0바이트로 남았다. 프로세스는 정상 동작 중인데 밖에서는 hang 과 구별할 수 없었고, 실제로 세 번 연속 Ctrl-C 로 죽인 기록이 이슈에 남아 있다.
trade-off. 없다. --json 에 이벤트 세 종(sweep_started / sweep_progress / sweep_completed)과 ndjson 로그에 세 종(purge / purge_failed / sweep_summary)이 추가된다. 기존 소비자는 모르는 종류를 무시하므로 영향이 없다.
mitigation / upgrade. 자동이다.
곁들여, sweep 이 취소 플래그를 보게 했다. 첫 Ctrl-C 가 "현재 파일까지만 하고 중단한다" 고 안내하는데 이 구간에서는 사실이 아니었다 — 유일한 수단인 두 번째 Ctrl-C 는 강제 종료라 버퍼에 쌓인 삭제가 고아로 남았다. 스캔 결과가 0건일 때 취소가 "완료" 로 보고되던 것도 함께 고쳤다.
5. 임베딩 캐시 (#231) — patch, 그리고 가설이 재현되지 않았다
이슈는 "캐시가 히트하는데도 우회하고 전량 재임베딩하는 편이 더 빠르다" 고 보고하면서 실측 표를 비워 뒀다. 그 표를 채웠는데 재현되지 않는다.
문서 792건 / 청크 16,379개를 강제 재처리한 결과, 캐시 히트 경로가 139초, 캐시 우회가 1,180초로 히트가 8.5배 빠르다. 새로 넣은 계측으로 뜯어보면 캐시 경로 전체가 run 의 1.5% 이하다.
다만 보고가 틀렸다고 단정하지 않는다. 보고 이후 #229 와 #230 이 머지됐고, 이슈 본문 스스로 #229 와 같은 뮤텍스를 공유해 서로를 증폭한다고 적어 뒀다. 그러니 "#229 이후에는 재현되지 않는다" 로 읽는 것이 맞다.
변경 사실. 캐시 조회를 배치로 접고(문서당 왕복 21회 → 1회), 삽입을 한 트랜잭션으로 묶고, 구문 캐시를 쓴다. 다만 벽시계 차이는 측정 오차 안이다 — 이 코퍼스에서는 체감이 없다.
실질적인 산출물은 계측 쪽이다. --json 의 asset_timings 에 cache_hit / cache_miss / cache_ms 가 실린다. 이전에는 stderr 로만 흘러 run 이 끝나면 사라졌고, "내 코퍼스에서 캐시가 이득인가" 를 확인할 방법이 없었다. TTY 에서는 ⏱ … embed 75ms · cache 21/21 0ms · store 36ms 로 보인다.
trade-off / upgrade. 없다. 자동이다.
그 밖
cargo clippy --workspace --all-targets -- -D warnings가 main 에서 실패하고 있었다(툴체인 상승으로 새 lint 두 개가 기존 코드에 걸림). 고쳤다. 방치하면 새 위반이 그 그늘에 숨는다는 것을 실제로 겪었다.kebab doctor에 체크 세 개가 추가됐다:pdf_render(정보성),vector_store(정보성),fts_shadow(어긋나면 exit 3 — 이 상태에서는 문서 삭제가 엉뚱한 인덱스 행을 지운다).