RAG에서 문서의 섹션을 따르는 청크를 원한다면 PDF를 Markdown으로 변환하세요. 각 제목이 분할 지점이 되고, 청크 위에 있는 제목들이 메타데이터로 함께 전달됩니다. 인덱싱하기 전에 제목, 표, OCR 텍스트를 확인한 다음, 답을 이미 알고 있는 질문으로 검색을 테스트하세요.

이 가이드는 정책, 매뉴얼, 계약서, 연구 자료 같은 비공개 문서를 대상으로 검색 기능을 만드는 개발자를 위한 것입니다. 사용할 임베딩 모델과 벡터 저장소는 이미 정해 두었다고 가정합니다.

연보라색 리본으로 묶은 한 더미를 포함해, 노트북 옆에 놓인 줄 쳐진 색인 카드 더미와 종이 조각들.
문서의 섹션 구조를 따르는 청크.

제목 기준으로 나누면 청크가 더 좋은 이유

고정 크기 분할기는 텍스트를 정해진 길이로 어디서든 자릅니다. 그러면 청크가 섹션 중간에서 시작해, 무엇에 관한 내용인지 알려 주는 제목을 잃을 수 있습니다. "요청은 분당 100회로 제한됩니다"라는 문장은 "Sandbox" 아래에서는 하나의 의미이고 "Production" 아래에서는 다른 의미입니다. 제목이 없으면 검색기도 모델도 어느 쪽인지 알 수 없습니다.

Markdown은 문서의 개요를 #, ##, ###로 시작하는 줄로 유지합니다. 제목 기반 분할기는 그 줄에서 자르므로 청크마다 섹션 하나를 담고, 청크 위에 있는 제목들, 즉 섹션 경로를 기록합니다. LangChain의 Markdown 분할 가이드는 같은 개념을 설명합니다. 텍스트를 그 텍스트가 속한 제목과 함께 유지하는 것입니다.

섹션마다 길이가 다르므로 긴 섹션은 크기를 기준으로 한 번 더 나누어야 합니다. 섹션 경로는 그 섹션의 모든 조각에 적용됩니다.

PDF를 변환하고 제목 확인하기

MD Factory에서 PDF가 있는 폴더를 추가하세요. 각 Markdown 파일은 PDF 옆에 같은 이름으로 저장되고 이미지는 그 옆의 폴더에 들어갑니다. 스캔한 PDF는 PC에서 OCR을 거칩니다. 폴더에 있는 Word 파일과 다른 형식도 같은 방식으로 변환됩니다.

분할 코드를 작성하기 전에 결과를 몇 개 열어 보세요:

  • 장, 섹션, 하위 섹션이 #, ##, ###로 나오는지 확인하세요. PDF는 제목을 더 크거나 굵은 글꼴로만 표시하는 경우가 많아서, 어떤 변환기든 섹션이 어디서 시작하고 어떻게 중첩되는지 추정해야 합니다. 수준이 틀린 곳은 고치세요.
  • Word 파일에서는 작성자가 Word의 제목 스타일을 사용했다면 제목이 그대로 옮겨집니다. 굵거나 크기만 큰 텍스트는 일반 단락으로 남습니다.
  • 본문 중간에 들어간 머리글, 바닥글, 페이지 번호가 있는지 찾아보세요.
  • 파일 옆의 검토 필요를 클릭해 확인할 내용을 보세요. PDF의 경우 OCR이 텍스트 일부를 읽지 못했거나 텍스트를 전혀 찾지 못한 페이지가 목록에 포함될 수 있습니다.

인덱싱하기 전에 Markdown 파일의 문제를 고치세요. 모든 청크가 그 문제를 물려받습니다.

코드로 제목 기준 분할하기

LangChain의 MarkdownHeaderTextSplitter는 나열한 제목 수준에서 나누고 각 제목을 청크의 메타데이터에 넣습니다. 각 섹션 안에서는 두 번째 분할기가 크기를 제한합니다:

from pathlib import Path
from langchain_text_splitters import (
    MarkdownHeaderTextSplitter,
    RecursiveCharacterTextSplitter,
)

corpus = Path("corpus")
by_heading = MarkdownHeaderTextSplitter(
    [("#", "h1"), ("##", "h2"), ("###", "h3")], strip_headers=False
)
by_size = RecursiveCharacterTextSplitter(chunk_size=1500, chunk_overlap=150)

chunks = []
for path in sorted(corpus.rglob("*.md")):
    sections = by_heading.split_text(path.read_text(encoding="utf-8"))
    for chunk in by_size.split_documents(sections):
        chunk.metadata["source"] = path.relative_to(corpus).as_posix()
        chunk.metadata["section"] = " > ".join(
            chunk.metadata[h] for h in ("h1", "h2", "h3") if h in chunk.metadata
        )
        chunks.append(chunk)

여기서 청크 크기와 겹침은 출발점일 뿐이니 임베딩 모델에 맞게 조정하세요. 임베딩할 텍스트에도 섹션 이름이 들어가도록 제목 줄을 청크 텍스트에 남기기 위해, 예제에서는 다음 값을 사용합니다: strip_headers=False.

LlamaIndex의 MarkdownNodeParser도 같은 방식으로 제목 기준 분할을 합니다. SimpleDirectoryReader로 파일을 불러오면 각 노드에는 파일 이름도 담깁니다:

from llama_index.core import SimpleDirectoryReader
from llama_index.core.node_parser import MarkdownNodeParser

documents = SimpleDirectoryReader(
    "corpus", recursive=True, required_exts=[".md"]
).load_data()
nodes = MarkdownNodeParser().get_nodes_from_documents(documents)
print(nodes[0].metadata["file_name"], nodes[0].metadata["header_path"])

header_path 속성은 노드 위에 있는 제목들을 나열합니다. 예: /Employee handbook/Expenses/. 노드 자신의 제목은 노드 텍스트의 첫 줄입니다.

모든 청크에 출처 남기기

각 청크와 함께 최소한 세 가지를 저장하세요:

  • 파일. Markdown 파일은 PDF와 같은 이름이므로 handbook.md에서 나온 청크는 handbook.pdf로 이어집니다. 서로 다른 폴더에 있는 terms.md 두 개는 서로 다른 문서이므로, 컬렉션 폴더 기준의 상대 경로를 저장하세요.
  • 섹션 경로. 답변이 사용한 섹션을 말해 줄 수 있고, 읽는 사람이 해당 구절로 바로 이동할 수 있게 됩니다.
  • 판 또는 날짜. 정책이 바뀌면 두 버전을 모두 제공하지 말고, 이전 파일의 청크를 삭제하고 새 청크를 인덱싱하세요.

접근 권한이 다른 사람들이 같은 인덱스를 조회한다면 각 문서를 읽을 수 있는 사람도 함께 저장하고, 조회 시점에 그 값으로 필터링하세요. 검색기는 질문과 일치하는 청크라면 무엇이든 반환합니다.

MD Factory가 만드는 Markdown에는 페이지 번호가 없습니다. 답변에서 페이지를 인용해야 한다면 원본 PDF에서 해당 구절을 찾아보세요.

표와 OCR 텍스트는 신중하게 다루기

Markdown 표는 제목 행이 맨 위에 한 번만 있습니다. 표가 청크 크기보다 길면 크기 분할기가 표를 자르고, 뒤쪽 조각은 열 이름 없이 도착합니다. LangChain(langchain-text-splitters 1.1.3)으로 테스트했을 때 40행짜리 표를 300자 청크로 나누자 조각이 네 개 나왔고, 제목 행이 들어 있는 것은 첫 번째뿐이었습니다.

해결 방법은 두 가지입니다. 다른 청크보다 길어지더라도 표 하나를 청크 하나에 유지하거나, 모든 조각의 맨 앞에 제목 행을 반복하는 것입니다.

Word와 Excel 파일에서는 병합되거나 복잡한 셀이 단순화되면 MD Factory가 문서를 검토 필요로 표시합니다. PDF 표에는 이런 표시가 없으므로, 몇 개를 골라 원본과 비교하세요. 특히 병합된 제목이 있는 표를 확인하세요.

OCR 텍스트는 별도의 확인이 필요합니다. 잘못 읽힌 숫자, 이름, 코드는 그대로 답변에 들어가고, 청크에서는 이상한 점이 전혀 보이지 않습니다. 몇몇 청크의 수치를 스캔본과 비교하고, 표시된 페이지는 고칠 때까지 인덱스에서 제외하세요. 무엇을 살펴봐야 하는지는 다음 글에 있습니다: 스캔한 PDF의 OCR 확인 가이드.

실제 질문으로 검색 테스트하기

사람들이 실제로 할 질문을 적고, 각 질문마다 답이 들어 있는 파일과 섹션을 적어 두세요. 열두 개에서 스무 개 정도로 시작하세요.

  1. 질문마다 검색기를 실행하고 상위 결과를 확인하세요.
  2. 올바른 섹션이 그중에 있는지 기록하세요.
  3. 놓친 질문마다, 반환된 청크를 반환되었어야 할 청크와 비교하세요. 흔한 원인은 잘못된 제목 수준, 제목과 떨어져 잘린 표, 핵심 용어의 OCR 오류, 답이 묻힐 만큼 긴 섹션입니다.
  4. 청크 크기, 제목 수준, 표 분할 방식처럼 한 가지만 바꾸고 같은 질문을 다시 실행하세요.

질문을 컬렉션 옆의 파일에 보관하고, 문서나 분할 코드가 바뀔 때마다 다시 실행하세요.

MD Factory를 쓸 때와 라이브러리를 쓸 때

MD Factory는 Windows용 데스크톱 앱입니다. 내 PC에서 변환하고 눈으로 확인하려는 비공개 컬렉션에 적합합니다. 폴더를 추가하고, 상태를 보고, 검토가 필요한 파일을 열어 고친 다음, 인덱싱 스크립트가 그 폴더를 가리키게 하세요.

MD Factory는 변환하는 동안 문서를 PC에 둡니다. 하지만 청크를 호스팅형 임베딩 API로 임베딩하거나 호스팅형 모델로 보내면 그 텍스트는 해당 제공업체로 전달됩니다. 문서가 네트워크 밖으로 나가면 안 된다면 로컬 모델을 사용하세요.

문서가 계속 들어오거나, 서버에서 운영되거나, 사람이 지켜보지 않아도 변환되어야 한다면 대신 코드에서 변환 라이브러리를 사용하세요. 선택지는 다음 글을 참고하세요: PDF를 Markdown으로 변환하는 도구 비교.