PDF에서 평문 텍스트를 반환하지 말라: RAG가 필요로 하는 관계형 형태

발행: (2026년 6월 12일 AM 01:30 GMT+9)
10 분 소요

Source: Towards Data Science

Enterprise Document Intelligence 시리즈의 한 brick(구성 요소)으로, 파싱, 질문 파싱, 검색, 생성 네 가지 brick을 조합해 엔터프라이즈 RAG 시스템을 구축합니다. 파싱이 가장 먼저이며, 이번 글은 그 두 파트 중 두 번째 파트입니다. 이전 파트에서는 PDF를 line_df 로 변환했으며, 이는 페이지의 텍스트 한 줄당 하나의 행을 의미합니다. 이번 파트에서는 모델의 나머지 부분을 다룹니다: 파서가 생성해야 하는 전체 테이블 집합, 각 테이블이 담고 있는 내용, 그리고 테이블 간 연결 방식. 예를 들어 14페이지에 있는 표는 열 구조를 유지하고, 갱신 비용은 해당 라벨에 연결된 상태로 남습니다. 나머지 세 brick과 마지막에 강조된 답변은 모두 이 테이블들을 읽으며, 원본 PDF를 직접 읽지는 않습니다.

이 글이 시리즈 내에서 차지하는 위치: 파트 II(네 개의 brick) 안의 파싱 brick 중 데이터‑모델 절반인 Article 5 – 이미지 출처: 저자

RAG 튜토리얼은 모두 text = extract_text(pdf) 로 시작합니다. 이 한 줄이 PDF 문제의 시작점이 됩니다.

RAG 파이프라인을 구축합니다. 몇 개의 깔끔한 문서에서는 잘 동작합니다. 그런데 고객이 30페이지짜리 실제 계약서를 보내옵니다. 그 안에 14페이지에 Schedule of Charges 표가 있습니다. 사용자는 “갱신 비용이 얼마인가요?” 라고 묻지만, 모델은 잘못된 숫자를 반환합니다.

팀은 “모델이 표를 읽을 수 없다”고 말합니다.

사실 모델은 표를 잘 읽습니다. 문제는 업스트림에 있습니다. 파서는 표 셀을 하나씩 순회하면서 모든 셀을 하나의 긴 문자열로 합쳐 버렸습니다. 열 구조가 사라지고, 라벨과 금액 사이의 연결 고리도 사라진 것이죠. 모델은 이제 “갱신 비용”이 어느 숫자인지 추측해야 합니다. 가끔 맞히기도 하지만, 대부분 틀립니다.

같은 네 개의 행을 셀 단위로 이어 붙여 만든 하나의 청크. EUR 200 일시불, 연체료 75: 어떤 라벨과 어떤 금액이 짝을 이루나요? – 이미지 출처: 저자

파서는 실패하지 않았습니다. 요청한 대로 결과를 제공했을 뿐입니다. 잘못된 것을 요청한 것이죠.

좋은 PDF 파서는 텍스트만 추출하지 않습니다. 문서를 관계형 테이블 집합으로 모델링합니다. PDF 하나를 입력받아, 종류별로 하나씩 테이블을 출력합니다(현재는 7~8개, 필요에 따라 추가될 예정).

  • toc_df: 저자가 작성한 섹션 목록
  • page_dfline_df: 본문. 모든 페이지, 모든 줄.
  • image_df: 모든 페이지의 모든 그림.
  • span_df: 굵게, 기울임, 색상, 글꼴 크기 등. 모든 줄의 모든 스팬.
  • object_registry: 모든 그림 캡션, 표 캡션, 부록 등.
  • cross_ref_df: 모든 “Figure 2를 보세요”, “Table 4를 보세요”, “Annex B를 보세요” 등.
  • parsing_summary: PDF가 디지털 원본인지, 스캔본인지, 혹은 혼합형인지 알려주고, OCR 품질이 좋은지 나쁜지 알려줍니다.

검색 단계는 이 테이블들을 읽고, 생성 단계도 이 테이블들을 읽으며, 하이라이팅도 이 테이블들을 읽습니다. PDF는 한 번만 열면 됩니다. 그 이후로는 오직 테이블만 다루면 됩니다.

이 글에서는 각 테이블을 상세히 살펴보고, parse_pdf 를 두 개의 매우 다른 PDF에 적용해 같은 컬럼이 두 경우 모두를 포괄한다는 것을 보여줍니다. 이전 글(“Beyond extract_text: the two layers of a PDF that drive RAG quality”)에서는 업스트림 측면, 즉 파서가 먼저 읽는 선언적 신호와 라인에 번호를 매기기 전에 수행되는 페이지‑레벨 분류에 대해 다루었습니다.

각 테이블이 생성되는 과정: line_df, parsing_summary, toc_df, image_df는 파싱 단계에서 바로 생성되고, page_df, span_df, object_registry, cross_ref_dfline_df 로부터 파생됩니다 – 이미지 출처: 저자

1. 엔터티당 하나의 테이블

우리가 추출한 모든 내용은 문서 모델의 엔터티당 하나의 테이블과 파싱 요약을 포함하는 딕셔너리 형태로 반환됩니다.

_df 라는 명명 규칙은 이름만 보고도 그 세분화 정도를 알 수 있게 해줍니다. 이 글 상단의 다이어그램은 각 테이블이 어떻게 생성되는지 보여줍니다. 네 개는 파싱 단계에서 바로 생성됩니다: line_df(텍스트 라인), parsing_summary(문서‑레벨 종합), toc_df(네이티브 목차, doc.get_toc 사용), image_df(page.get_image_info 사용). 나머지 네 개는 line_df 로부터 파생됩니다: page_df는 페이지별로 집계하고, span_df, object_registry, cross_ref_df는 라인에서 추출됩니다. 테이블들이 어떻게 서로 결합되는지는 섹션 2에서 별도로 다룹니다.

1.1. toc_df: 목차

목차는 엔터프라이즈 문서 어디에나 존재합니다. 계약서, 보고서, 정책, 직원 매뉴얼, 규제 제출물 등 거의 모든 문서는 선언된 섹션 구조를 가지고 있으며, 이 구조는 검색 엔진에 제공할 수 있는 가장 저렴한 의미론적 신호입니다.

하지만 항상 네이티브인 것은 아닙니다. 때로는 단순히 서체(굵은 제목, 번호 매긴 섹션, 들여쓰기된 하위 제목)만으로 존재하고, line_dfspan_df를 이용해 재구성해야 합니다.

여기서는 네이티브 경우(디지털 원본 LaTeX, Word, InDesign 내보내기에서 일반적)를 중점적으로 다룹니다. 북마크가 없을 때 타이포그래피 기반으로 목차를 재구성하는 방법은 별도의 주제로, 적응형 파서를 통해 스케치하고 추후 전용 글에서 완전하게 다룹니다.

parent_idxbreadcrumb이 포함된 선언된 목차; 네이티브 북마크가 없을 경우 비어 있음 – 이미지 출처: 저자

구축 방법: build_toc_df(doc)doc.get_toc(simple=False)(북마크당 하나의 엔트리와 목적지 딕셔너리 포함)를 호출하고, 결과를 순회하면서 parent_idx, breadcrumb, end_page, start_y 를 계산합니다. 예시로 Attention 논문에 적용하면 섹션 1.2에 이미 보여준 22개의 엔트리를 얻을 수 있습니다(세 단계 헤딩, 네이티브 북마크, 재구성 필요 없음).

암묵적인 end_page 규칙: 목차는 섹션이 시작되는 위치는 표시하지만, 끝나는 위치는 거의 표시하지 않습니다. build_toc_df 는 각 행에 대해 end_page 를 다음 동일 레벨 혹은 상위 레벨 엔트리의 start_page 로 설정하고, 마지막 섹션은 total_pages 를 사용합니다. 예를 들어 Attention 논문의 Conclusionstart_page=10, end_page=15 로 표시됩니다. 문서 전체가 15페이지이므로 마지막 섹션은 문서 끝까지 차지합니다. 이 규칙은 설계상 한 페이지 겹침을 유지합니다(end_page 가 후속 섹션의 start_page 와 동일, successor.start_page - 1 아님). 이는 생성 brick 의 다음 페이지 미리보기(섹션

0 조회
Back to Blog

관련 글

더 보기 »

BI는 죽었다, BI 만세

in data, I’ve watched the same pattern repeat itself again and again: - A big tech company hits a technical or process limitation. - They solve it internally wi...