chunks_fts 삭제가 FTS5 전체 스캔 — chunk 삭제 비용이 O(인덱스 전체) #229
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
증상
chunk 삭제가 코퍼스 크기에 비례해 느려진다. chunk 51,900행 기준 문서 purge 가 초당 6건에 묶이고, 프로파일상 거의 전부 디스크 read 대기다. 문서 5,834건 삭제에 약 16분이 걸린다.
원인
chunks_fts는 contentless FTS5 테이블이고chunk_id가UNINDEXED다 (migrations/V002__fts.sql:19-25):그런데 삭제 트리거는 그
chunk_id로 행을 찾는다 (migrations/V002__fts.sql:31-38):FTS5 는
UNINDEXED컬럼에 인덱스를 만들지 않는다. 따라서WHERE chunk_id = ?는 사용할 수 있는 인덱스가 없고 FTS5 테이블 전체 스캔으로 떨어진다. chunk 1건 삭제마다 5만여 행의 content btree 를 훑는다 → chunk 삭제가 O(전체 인덱스).스택 샘플 (
sample <pid>, 4초, 메인 스레드 100%):영향 범위
chunks에서 DELETE 가 나가는 모든 경로:sweep_deleted_files(crates/kebab-app/src/lib.rs:2038) — 삭제된 파일 정리reset --orphans-only(crates/kebab-app/src/reset.rs:execute_orphans_only)purge_orphan_at_workspace_path경로,crates/kebab-app/src/lib.rs:~1990)즉 정상적인 증분 재색인에서도 문서가 수정될 때마다 비용을 낸다. 코퍼스가 커질수록 재색인이 느려지는 형태.
제안
external-content FTS5 로 전환해 삭제를 rowid 기준으로 바꾼다:
검토할 항목:
rebuild가 필요하다 (INSERT INTO chunks_fts(chunks_fts) VALUES('rebuild')). 51,900행 기준 일회성 비용.chunk_id/doc_id를 FTS 테이블에서 빼고 검색 시chunks와 rowid join 으로 가져와야 한다. 검색 경로(crates/kebab-search)의 컬럼 참조를 함께 고쳐야 함.rebuild_chunks_fts(crates/kebab-store-sqlite/src/fts.rs:40) 의 DELETE + INSERT 방식도 external-content 규약('rebuild'커맨드)으로 교체.migrations/V002__fts.sql:1-6) spec 쪽도 같이 갱신해야 한다. HOTFIXES dated entry 필요.더 작은 변경으로 버티려면
chunk_id → fts rowid매핑 테이블을 두는 방법이 있으나, 정합성 유지 부담이 external-content 전환보다 크다.PR #235 로 닫는다. 다만 이슈가 제안한 external-content 전환은 택하지 않았으므로 근거를 남긴다.
원인은 이슈가 지목한 그대로였다. 삭제 트리거가
chunk_id로 행을 찾는데 그 컬럼은 FTS5 에서 색인되지 않으므로, 삭제 한 건이 색인 전체를 훑는다.해법으로는
chunks_fts의 rowid 를chunks.rowid와 일치시키고 삭제를 rowid 로 하는 쪽을 택했다 (migrations/V016__fts_rowid_delete.sql). external-content 를 택하지 않은 이유는 V009 트리거가 색인하는 값이tokenized_korean_text || ' ' || text라서chunks의 어느 컬럼과도 일치하지 않기 때문이다. 그 길로 가려면 생성 컬럼을 새로 만들고, FTS 테이블에서chunk_id/doc_id를 빼고, 검색 경로의 컬럼 참조를 rowid join 으로 바꿔야 한다. 삭제 비용은 rowid 정렬만으로 같은 복잡도로 내려가므로, 본문 그림자(chunks_fts_content, 실측 550 MB) 회수는 별 건으로 남긴다.실측은 실제 KB 사본(문서 28,427건 / chunk 600,808건)에서 문서 200건 삭제 기준 1590.1초 → 2.0초다. 삭제 후 남은
chunks와chunks_fts행 수가 양쪽 다 595,741 로 같고, 네 질의의 상위 20건을 chunk_id·bm25 점수·snippet 까지 해시로 비교했을 때 마이그레이션 전후가 동일했다. 그래서corpus_revision은 올리지 않는다.이슈 본문의 사실관계 두 가지를 정정해 둔다. 지목된 마이그레이션은 V002 가 아니라 V009 다 — V007 이 trigram 으로, V009 가 unicode61 + 한국어 형태소 컬럼으로 각각
chunks_fts를 다시 만들었다. 그리고chunks_fts는 contentless 가 아니다.chunks_fts_content가 실재하고, 그 550 MB 가 위에서 말한 그림자다.부수적으로
rebuild_chunks_fts의 잠복 결함도 잡았다. V009 가 색인하는 한국어 형태소 접두를 빠뜨리고 원문만 넣고 있어서, 재구축을 한 번 돌리면 2자 한국어 질의가 다음 재색인 때까지 안 맞았다.리뷰에서 나온 지적 하나를 함께 반영했다. rowid 로 삭제하면 정렬이 어긋난 순간 남의 문서 shadow 행을 조용히 지운다 — 실패 양상이 '느림'에서 '조용한 오삭제'로 바뀐다.
kebab doctor에fts_shadow점검을 넣어 이 불변식을 눈에 보이게 했다. 참고로 초안에서 VACUUM 을 위험으로 적었으나, 실측해 보니 rowid 를 다시 매기지 않았다(실제 KB 사본과 합성 DB 양쪽에서 불일치 0, sqlite 3.53.4). 남는 실제 경로는 앞으로chunks를 테이블 재작성 방식으로 바꾸는 마이그레이션이며, V016 주석에 울타리를 박아 뒀다.