• altair823 released this 2026-08-16 21:23:27 +00:00 | 0 commits to main since this release

    v0.33.0 — 28.4k 문서 도그푸딩이 드러낸 다섯 결함

    v0.32.0 이후 실제 코퍼스(나무위키 18,282 + Apache Jira 10,146 = 28,427 문서 / 600,808 청크)로 도그푸딩하면서 등록한 이슈 #228~#232 를 전부 처리한 릴리스. 다섯 건 모두 단위 테스트가 잡지 못하는 종류였다 — 규모에서만 드러나는 성능 붕괴, 조용히 사라지는 내용, 멈춘 것처럼 보이는 진행 표시.

    minor bump 트리거는 두 가지다. #232parser_version cascade(pdf-text-v1pdf-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
    • --jsoningest_report.ocr_skipped_pagespdf_ocr_finished.failure_reason (no_renderer / render_error / unopenable_pdf / ocr_error)
    • kebab doctorpdf_render 가 지금 어느 쪽인지 보고한다

    upgrade 절차. 스캔 PDF 를 쓰지 않으면 할 일이 없다. 쓴다면:

    1. libpdfium 을 로더가 찾는 곳에 두거나 [ingest.pdf.ocr] render_library = "/경로/libpdfium.so" 로 지정한다. 공개 빌드는 bblanchon/pdfium-binaries 에 있고 리눅스 x64 기준 7.4 MB 다.
    2. kebab doctorpdf_render 가 경로를 보고하는지 확인한다.
    3. 재색인은 자동이다. parser_versionpdf-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 doctorvector_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회), 삽입을 한 트랜잭션으로 묶고, 구문 캐시를 쓴다. 다만 벽시계 차이는 측정 오차 안이다 — 이 코퍼스에서는 체감이 없다.

    실질적인 산출물은 계측 쪽이다. --jsonasset_timingscache_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 — 이 상태에서는 문서 삭제가 엉뚱한 인덱스 행을 지운다).
    Downloads