본문으로 건너뛰기
5분 AI.
GITHUB TRENDING MONTHLY #19 딥다이브 · 2026-10-23 분석

PageIndex 딥다이브
— 벡터 대신 목차를 따라 근거를 찾는 RAG

SDK

빠른 판단

무엇
문서의 계층 목차를 만들고 LLM이 관련 절·원문 페이지를 찾도록 하는 벡터 DB 없는 문서 검색 SDK
이럴 때
긴 보고서·계약서·매뉴얼에서 질문에 맞는 장·절을 탐색하고 페이지 근거와 함께 답하게 할 때
입력
긴 문서와 질의
결과
계층형 목차 색인·선택한 근거 페이지와 문서 기반 답변
긴 보고서를 일정한 크기의 조각으로 나누어 검색하는 대신, 문서의 장·절을 트리로 만들고 LLM이 필요한 페이지를 골라 읽게 하는 Python 프로젝트입니다. PDF의 글자 배치에서 구조를 추출하는 Flash, 목차를 추정·검증하는 classic, 로컬 저장소와 에이전트 도구까지 연결해 살펴봅니다. 저장소 Python 약 47,700줄 중 핵심 패키지는 약 31,200줄입니다. VectifyAI/PageIndex · ⭐39,525 · 포크 3,429 · MIT · 2026-10-23 GitHub 기준 · 소스 해설은 커밋 b3422f9 기준입니다.
목차
  1. 프로젝트 한줄 요약
  2. 왜 주목받는가
  3. 기술 스택 전체 지도
  4. 아키텍처 심화 분석
  5. 디렉토리 구조 해부
  6. 학습 포인트 (기술별)
  7. 하드웨어 / 시스템 요구사항
  8. 직접 해볼 수 있는 실습 과제
  9. 관련 기술 심화 학습 로드맵
  10. 핵심 키워드 사전
  11. 참고 링크

1프로젝트 한줄 요약

문서를 검색 가능한 지도와 실제 페이지로 나누어 다루는 도구

PageIndex는 긴 문서의 계층형 목차를 만들고, 그 목차를 읽는 에이전트가 답의 근거 페이지를 찾도록 돕는 SDK입니다. 핵심 아이디어는 문장의 뜻이 비슷한 조각을 찾는 대신, 질문에 답이 있을 법한 장과 절을 판단한 뒤 원문을 읽게 하는 것입니다.

예를 들어 “작년보다 영업이익률이 낮아진 이유는?”이라는 질문을 받았다고 합시다. “영업이익률”이라는 단어가 반복되는 모든 곳을 읽기보다, 먼저 목차에서 경영 실적과 비용 분석을 고릅니다. 그다음 해당 페이지의 숫자와 설명을 읽고 답합니다. 목차의 요약은 이동 경로를 고르는 안내이고, 답을 뒷받침하는 근거는 실제 본문입니다.

먼저 익힐 용어
RAG — 검색 증강 생성
모델이 기억에만 기대어 답하지 않도록, 질문과 관련된 자료를 찾아 함께 읽히는 방식입니다. “찾기”와 “답하기”를 연결하는 설계이며, 반드시 벡터 데이터베이스가 필요한 것은 아닙니다.
한 문장 비유

책을 잘게 자른 카드 상자보다, 목차와 책갈피를 든 독서 보조원에 가깝습니다

카드 상자는 질문과 비슷한 글이 적힌 카드를 빨리 골라 줍니다. 독서 보조원은 먼저 “이 질문은 어느 장의 내용일까?”를 생각하고, 그 장을 펼칩니다.

PageIndex의 structure가 목차, summary가 안내 문구, start_index와 end_index가 책갈피입니다. 보조원 역할을 맡는 LLM은 목차를 잘못 고를 수도 있으므로, 정답과 근거 페이지를 함께 평가해야 합니다.

1-1. 색인과 질의는 서로 다른 작업입니다

  1. 색인: PDF를 읽고 장·절과 페이지 범위를 추출합니다. 로컬 SDK의 기본 설정에서는 LLM이 구조를 보완하고 요약과 문서 설명도 만듭니다.
  2. 보관: 로컬 모드에서는 트리와 페이지별 텍스트를 JSON으로 나누어 저장합니다.
  3. 질의: 에이전트가 문서 목록과 목차를 보고 필요한 페이지를 요청합니다. 읽은 근거를 바탕으로 답변합니다.
# 구조를 설명하기 위한 작은 예시 — 실제 추출 결과가 아닙니다.
{
  "title": "경영 실적",
  "node_id": "0001",
  "start_index": 12,
  "end_index": 24,
  "summary": "매출 변화와 비용 증가의 원인을 설명합니다.",
  "nodes": [
    {"title": "비용 분석", "node_id": "0002",
     "start_index": 18, "end_index": 24}
  ]
}

PDF 페이지 인덱스는 1부터 시작하며 양 끝을 포함합니다. 위 자식 노드는 18~24쪽, 총 7쪽입니다. 인쇄된 쪽 번호와 PDF 파일의 물리적 페이지 순서는 다를 수 있습니다. 표지와 앞부분의 로마 숫자 쪽 번호가 대표적인 원인입니다.

이 글에서 배울 것
AI 기능을 완성하는 주변 설계까지 읽습니다

트리와 재귀, 문서 레이아웃 분석, 비용 함수, 비동기 작업 의존성, 도구 스키마, 원자적 파일 저장을 한 흐름으로 배웁니다. 새 모델을 학습하는 저장소가 아니라, 모델과 문서 처리 코드를 묶어 검색 기능으로 만드는 사례입니다.

2왜 주목받는가

벡터 없는 검색의 매력과, 비교할 때 지켜야 할 기준

2026-10-23 기준 GitHub Trending Monthly #19, 스타 39,525개입니다. 순위와 스타만으로 유입 원인을 증명할 수는 없습니다. 다만 README의 제안과 구현을 함께 보면 관심을 끌 만한 지점은 분명합니다. 익숙한 RAG 구성에서 벡터 DB를 빼고, 문서 구조와 모델의 판단을 검색의 중심에 놓습니다.

2-1. 긴 문서에서는 “비슷함”과 “답이 있음”이 다릅니다

계약서의 일반 조항과 예외 조항은 비슷한 단어를 쓸 수 있습니다. 질문은 예외의 적용 여부를 묻는데 일반 설명만 가져오면, 문장은 자연스러워도 답은 틀립니다. PageIndex는 장·절의 제목과 상위 문맥을 이용해 관련 부분을 고르는 경로를 제안합니다. 이 문제를 모든 벡터 검색이 해결하지 못한다고 일반화해서는 안 됩니다.

PageIndex의 장점은 검색 경로를 문서 구조로 설명할 수 있다는 것이며, 정답률의 우위는 문서와 질문을 맞춘 평가로 확인해야 합니다. 벡터 RAG도 메타데이터·재순위화·페이지 인용을 붙일 수 있고, PageIndex도 구조 추출이나 에이전트의 판단에서 실패할 수 있습니다.

비교 항목일반적인 벡터 RAG 구성PageIndex 로컬 구현
검색의 주소록조각의 임베딩과 문서 정보제목·요약·페이지 범위를 가진 트리
후보를 고르는 법유사도 검색 후 필터·재순위화LLM이 도구로 목차와 페이지를 읽음
문서 경계고정 길이 또는 의미 단위 분할레이아웃과 제목의 장·절 경계 활용
학습할 장점많은 후보에서 빠르게 좁히는 구조문서 계층과 페이지 근거를 연결하는 구조
주요 비용임베딩·검색 인프라·생성 호출색인 보완·요약·질의 중 도구 반복 호출
어려운 입력파서가 놓친 내용, 검색 누락제목이 약한 문서, 잘못된 트리, 선택 오류
운영 구성선택한 DB와 검색 서비스에 따라 다름로컬 JSON 저장 + 선택한 모델 연결

2-2. 다른 제품과는 “어느 층을 맡는가”로 비교합니다

대상주로 맡는 역할PageIndex를 배울 이유
FAISS벡터의 근접 이웃을 찾는 라이브러리거리 계산 대신 문서 계층을 탐색하는 대안 학습
Qdrant벡터 검색을 제공하는 데이터베이스별도 벡터 저장소 없이 문서별 검색을 구성하는 예
LangChain·LlamaIndex모델·검색기·도구를 조합하는 넓은 프레임워크PDF 구조화와 페이지 도구에 집중된 구현 읽기
PDF 전체 입력질문마다 모델에 문서 전체를 전달색인을 재사용하며 선택한 부분만 읽히는 설계

위 표는 역할의 비교입니다. 같은 조건에서 실행한 성능 순위가 아닙니다. 특히 대규모 문서 집합을 검색하는 서비스와 단일 PDF 안의 관련 페이지를 찾는 문제는 크기가 다릅니다. README의 “수백만 문서” 파일 계층은 Cloud 전용으로 소개되며, 로컬 DocStore에 같은 기능이 구현되어 있다는 뜻은 아닙니다.

2-3. Flash와 SDK가 진입 장벽을 낮춥니다

README는 2026년 8월 업데이트로 Flash와 로컬·클라우드 SDK를 소개합니다. 코드에서도 LocalAPI.submit_document()의 기본 모드는 flash입니다. 처음부터 모든 목차를 LLM으로 추정하지 않고 글자 크기·배치·번호 체계에서 구조를 뽑는다는 점이, 기존 classic 경로와 구별됩니다.

또한 같은 클라이언트로 로컬 색인을 쓰다가 클라우드 저장소로 바꿀 수 있고, 에이전트에 검색 도구만 연결할 수도 있습니다. 초보자에게는 “모델 호출 한 번”이 아니라 재사용 가능한 문서 처리 인터페이스를 설계하는 예제라는 의미가 있습니다.

수치를 읽는 법
README 벤치마크를 내 환경의 보장으로 읽지 않습니다

README는 FinanceBench 98.7%와 로컬 색인 약 $0.001/페이지, 9~1,098쪽 문서의 약 13초~4.5분을 보고합니다. 서로 다른 평가의 모델·설정에 따른 보고값입니다. FinanceBench 결과를 현재 기본 SDK의 모든 PDF 질의에 적용할 수 없습니다.

README가 소개하는 OSS 평가는 34개 PDF의 62개 사실 조회 질문을 대상으로 합니다. 문서 종류, 모델, 질문 난도, 인용 평가, 비용 계산 조건을 함께 보아야 합니다. 스타 수는 관심의 크기이며 정확도의 측정값은 아닙니다.

3기술 스택 전체 지도

실제 의존성 선언과 구현 파일로 역할을 구분하기

이 저장소의 중심은 Python 라이브러리와 CLI입니다. package.json, Cargo.toml, go.mod, Dockerfile은 없습니다. 웹 서비스의 화면과 서버 전체를 공개한 형태가 아니므로, 백엔드·프론트엔드·인프라를 같은 비중으로 상상하면 구조를 잘못 읽게 됩니다.

3-1. 백엔드에 해당하는 처리 계층

역할pyproject.toml 선언실제 사용 지점
언어python >=3.10타입 힌트, asyncio, 프로세스 풀
모델 라우팅litellm >=1.105.1,<1.106utils.llm_completion()·llm_acompletion()
모델 SDKopenai >=1.70.0pageindex/local_chat.py의 프로토콜별 모델 연결
에이전트 실행openai-agents >=0.20.0local_chat._openai_agent()
도구 연결mcp >=1.19.0,<3pageindex/mcp_bridge.py·pageindex/integrations/
문자·레이아웃pypdfium2 >=5pageindex/flash/parser_pdfium_charlevel/
페이지 텍스트PyPDF2 >=3.0.0LocalAPI._extract_page_texts()
이미지 처리Pillow >=9.0pageindex/imaging.py의 영역 강조 처리
문자열·정렬regex >=2024.0.0 · sortedcontainers >=2.4.0문자 분류, 정렬된 자료구조를 쓰는 파싱 보조
환경·설정python-dotenv >=1.0.0 · pyyaml >=6.0pageindex/utils.py와 pageindex/config.yaml
HTTP 통신requests >=2.28.0 · urllib3 >=1.26pageindex/cloud_api.py·MCP 통신 보조

버전 범위는 설치된 정확한 버전이 아닙니다. requirements.txt는 PyPDF2==3.0.1, pypdfium2==5.13.0, sortedcontainers==2.4.0을 고정하며, python-dotenv>=1.2.2와 pyyaml>=6.0.3을 선언합니다. 두 의존성 파일의 하한과 고정 여부가 다르고, 저장소에는 전체 전이 의존성을 잠근 lock 파일이 없습니다.

3-2. 선택적 통합과 프론트엔드의 경계

pageindex[claude] 추가 옵션은 claude-agent-sdk >=0.2.166, pageindex[anthropic]은 anthropic >=0.122.0을 선택적으로 연결합니다. [openai] 추가 옵션은 호환성을 위해 비어 있고, 기본 의존성에 관련 SDK가 이미 들어 있습니다.

사용자 접점이 저장소에서 확인되는 것학습할 경계
CLIrun_pageindex.py의 argparsePDF·Markdown 입력과 처리 옵션 검증
Python 호출PageIndexClient·ChatStream저장·질의·스트림을 앱에 연결하는 SDK
노트북cookbook/의 9개 .ipynb사용 예와 평가 절차를 설명하는 실행 문서
웹 UIReact·Vue 등 앱 소스와 JS 빌드 설정 없음README의 호스팅 앱을 로컬 UI로 오해하지 않기
Cloud APICloudAPI의 HTTP 클라이언트서버 내부 OCR·저장 구현은 여기서 확인 불가

의존성 목록은 “이 프로젝트가 무엇을 쓰는가”를 보여 주고, 호출 지점은 “그 도구가 어디까지 책임지는가”를 보여 줍니다. 이름에 API가 들어간 LocalAPI도 웹 서버가 아니라 프로세스 안에서 호출되는 Python 객체입니다.

3-3. 빌드·배포·CI

비유

이 저장소는 도서관 건물 전체보다 “문서 정리 장비와 사서용 도구함”에 가깝습니다. 웹 화면은 별도 앱이 맡을 수 있고, 클라우드 창고는 클라이언트 뒤에 있습니다. 공개된 도구함의 책임 범위를 먼저 파악해야 코드가 단순해집니다.

4아키텍처 심화 분석

네 가지 설계 패턴으로 색인부터 근거 읽기까지 연결하기

4-0. 먼저 전체 흐름을 잡습니다

[색인: 기본 로컬 경로] PDF 파일 | v PageIndexClient.submit_document() | v LocalAPI.submit_document() |-- PyPDF2: 페이지별 원문 텍스트 추출 -----------+ | | +-- Flash: PDFium 문자·좌표·폰트 분석 | | | v | 레이아웃 목차 + 내장 북마크 | | | v | merge(규칙) / expand <--- 색인 모델 | | | v | 노드 요약 + 문서 설명 <--- 색인 모델 | | | +-------------------+--------------------+ v DocStore tree.json / pages.json / doc.json [질의: 자기 모델을 쓰는 경로] 질문 + 문서 범위 | v pageindex/local_chat.py의 에이전트 <------> 질의 모델 | +-- get_document_structure(): 제목·요약·페이지 +-- get_page_content(): 고른 페이지의 실제 텍스트 | v 근거를 읽은 답변 + 선택한 인용 형식 [Cloud 문서를 선택한 경우] PageIndexClient --> CloudAPI --> PageIndex 서비스 자기 모델의 에이전트 --> MCP bridge --> 클라우드 도구

색인 모델과 질의 모델은 역할이 다릅니다. 전자는 트리 보완·요약·설명을 맡고, 후자는 질문에 따라 읽을 곳을 판단합니다. index="cloud"로 문서 저장소를 바꾸어도 chat=...을 지정하면 자기 모델의 에이전트로 읽을 수 있습니다. 두 선택을 별도로 다루는 것이 SDK 설계의 핵심입니다.

전체 흐름에서 가장 중요한 계약은 “트리의 각 노드가 실제로 읽을 수 있는 페이지 범위를 가리킨다”는 것입니다. 제목과 요약이 아무리 그럴듯해도 주소가 틀리면 검색이 실패합니다.

4-1. 패턴 ① 단계형 파이프라인 — 이전 단계의 결과를 다음 단계의 근거로

pageindex/flash/main.py의 extract_toc()는 긴 PDF를 한 번의 프롬프트로 처리하지 않습니다. parse_charlevel_meta_parallel()에서 문자와 좌표를 얻고, process_page()로 페이지를 구성한 뒤 문서 전체 통계와 블록을 만듭니다.

  1. 문자와 페이지: PDFium이 글자·폰트·영역·회전을 읽습니다.
  2. 줄과 블록: 열 구분과 줄 간격 등을 반영해 읽기 순서를 구성합니다.
  3. 잡음 분류: 머리말·꼬리말·워터마크·목차성 페이지를 표시합니다.
  4. 제목 후보: 본문 스타일과 다른 제목, 번호 체계, 문서 제목을 판별합니다.
  5. 목차 조립: assemble_outline()과 유효성 검사로 계층을 만듭니다.
  6. 북마크 결합: 신뢰할 만한 내장 목차를 틀로 삼거나 검출 결과와 섞습니다.

순서는 기능입니다. 반복되는 머리말을 먼저 분류하지 않으면 큰 글씨가 모두 장 제목으로 올라갈 수 있습니다. 페이지의 회전과 좌표계를 맞추지 않으면 열 순서가 바뀔 수 있습니다. LLM을 쓰지 않는 부분에도 많은 문서 처리 지식이 들어 있습니다.

코드를 읽는 열쇠
휴리스틱 — 자주 맞는 관찰을 규칙으로 만든 것
“다른 글자보다 크고 위아래 간격이 넓으면 제목일 가능성이 높다” 같은 규칙입니다. 정답을 보장하는 수학 공식은 아닙니다. 여러 규칙과 검증을 쌓아 실제 문서의 예외를 다룹니다.

구조를 찾지 못했을 때도 상태를 구분합니다

toc_source뜻관리 경로의 처리
detected레이아웃에서 구조 검출요약·최적화 대상으로 진행
bookmarks내장 목차 사용읽을 수 있는 본문 유무에 따라 보완
hybrid북마크와 검출 결과 결합결합된 구조를 반환
pages계층 없이 페이지당 노드 하나10개 초과이면 로컬 SDK·CLI가 거부
unreadable읽을 수 있는 텍스트 계층 없음OCR 선행이 필요하다는 오류

거부 기준은 pageindex/flash/api.py의 flash_rejection_reason()에 모여 있습니다. FLAT_TREE_MAX_NODES = 10은 계층을 못 찾은 평면 트리의 기준입니다. 모든 PDF를 10쪽으로 제한하는 설정이 아닙니다. 저수준 page_index_flash()는 이런 결과를 반환할 수 있고, 로컬 SDK·CLI가 이 공통 정책을 적용해 실패를 알립니다.

# pageindex/flash/api.py의 공개 함수 사용 예시
from pageindex.flash import page_index_flash

raw = page_index_flash(
    "report.pdf",
    summary=False,
    optimize=False,
)
print(raw["toc_source"])
print(raw["structure"])

이 조합은 구조 추출에 LLM을 쓰지 않습니다. summary=False만 지정하고 최적화를 기본값으로 두면 expand가 모델을 호출할 수 있습니다. 반면 일반 로컬 SDK의 _index_flash()는 요약을 켜고 문서 설명도 생성하므로, 위 저수준 호출과 동작이 다릅니다.

classic 경로는 “목차 추정 → 검증 → 교정”으로 읽습니다

pageindex/page_index_classic.py의 tree_parser()는 목차 존재와 쪽 번호를 확인합니다. meta_processor()는 번호 있는 목차, 번호 없는 목차, 목차 없이 추정하는 경로를 선택합니다. verify_toc()로 제목이 해당 페이지에 나타나는지 검사합니다.

검사 결과가 완전하면 채택하고, 일치 비율이 0.6보다 높으면서 오류가 있으면 fix_incorrect_toc_with_retries()를 최대 3회 시도합니다. 낮으면 더 약한 전제로 다시 처리합니다. 이 비율은 목차 위치 검사 결과이며 사용자 질문의 정답률이 아닙니다.

process_large_node_recursively()의 재분할 조건도 그대로 읽어야 합니다. 잎 노드이고, 페이지 인덱스 차이가 설정값보다 크며, 토큰 수가 설정값 이상일 때 나눕니다. 다만 상위 분할과 같은 페이지 범위를 다시 나누는 경우는 제외합니다. 페이지 조건과 토큰 조건 사이에는 AND가 있습니다. 기본 페이지 차이 설정값 10과 토큰 설정값 20,000을 각각 독립적인 강제 상한으로 해석하면 코드와 달라집니다.

4-2. 패턴 ② 비용 모델 — 더 깊은 트리가 항상 더 좋은 것은 아닙니다

pageindex/tree_optimize.py는 트리를 읽는 비용을 단순한 페이지 단위로 모델링합니다. S(node)는 해당 범위를 그대로 읽는 비용, ROUTING_COST = 1은 목차에서 경로를 고르는 비용입니다. tree_cost()는 자식 경로 중 가장 비싼 것을 재귀적으로 계산합니다.

# tree_cost()의 논리를 설명용으로 줄인 식입니다.
잎 노드 비용 = 그 노드가 덮는 페이지 수
부모 비용 = 경로 선택 비용 + max(각 자식 비용, 자식이 안 덮는 페이지 수)

# 가상의 12쪽 구간을 4쪽짜리 자식 세 개로 나눌 수 있다면
그대로 읽기 = 12
한 단계 내려가기 = 1 + max(4, 4, 4) = 5

# 1쪽 안에서 제목만 여러 개로 나누었다면
그대로 읽기 = 1
한 단계 내려가기 = 1 + 1 = 2

두 번째 경우는 제목이 많아져도 읽을 페이지가 줄지 않습니다. merge()는 span <= cost일 때 자식을 접습니다. 사라진 하위 제목은 도입부 노드 제목을 제외하고 key_items에 남깁니다. 페이지 범위가 완전히 같은 형제 잎 노드들을 합치는 merge_same_page()도 있습니다.

expand()는 큰 잎 노드에 모델이 제안한 하위 절을 붙일지 판단합니다. TRIGGER_PAGES = 5를 넘는 구간이 후보이며, 비용 감소와 이득 비율뿐 아니라 read_after < read_before라는 읽기 토큰 추정 조건도 통과해야 합니다. 모델이 제안했다고 그대로 수용하지 않습니다.

optimize()는 기본 최대 3라운드로 병합과 확장을 진행합니다. 확정한 노드는 frozen 집합으로 관리하여 접었다가 다시 펴는 반복을 막습니다. 반환되는 탐색 복잡도 수치는 설계한 비용 함수의 값이며, 실제 응답 시간이나 정확도를 보증하지 않습니다.

비유

서랍에 분류표를 더 붙인다고 늘 빨리 찾는 것은 아닙니다. 종이 한 장을 찾는데 서랍 세 겹을 여는 것보다 종이 한 장을 바로 읽는 편이 낫습니다. 이 코드는 “분류의 정교함”에 비용을 매겨 지나친 계층을 줄입니다.

4-3. 패턴 ③ 의존성 스케줄러 — 자식부터 요약하고 확정된 부분부터 시작합니다

pageindex/utils.py의 SummaryScheduler는 짧은 잎 노드에는 원문을 그대로 쓰고, 나머지 잎은 원문에서 요약합니다. 부모는 자식의 요약과 도입부를 합쳐 요약합니다. mark_final()은 “이 노드의 자식 구성이 더는 바뀌지 않는다”는 신호입니다. _visit()은 그 신호와 자식 작업을 기다립니다.

[장 전체의 요약] ^ 두 자식 요약을 기다림 / \ [절 A의 요약] [절 B의 요약] ^ ^ 원문 A 원문 B 시간 흐름: A 확정 --> A 요약 시작 B 확정 --> B 요약 시작 --> 둘 다 완료 --> 부모 요약

Flash의 _optimize_and_summarize()는 최적화기의 on_final에 scheduler.mark_final을 연결합니다. 문서 전체 최적화가 끝날 때까지 기다리지 않고, 바뀌지 않을 부분부터 요약합니다. 독립 작업의 병렬성과 부모·자식 순서 제약을 동시에 다루는 예입니다.

finish()는 확정 후 자식이 바뀌었는지, 확정 신호를 받지 못한 노드가 있는지 검사합니다. 신호가 빠지면 끝없이 기다릴 수 있으므로 오류를 발생시킵니다. 일부 요약 실패에는 대체 요약을 쓰지만, 시도한 요약 모델 호출이 모두 실패하면 예외를 발생시킵니다.

초보자가 여기서 배울 것은 asyncio.gather() 문법 이상입니다. “무엇이 동시에 가능하고, 무엇을 기다려야 하는가”를 데이터의 의존성으로 표현해야 합니다. 요약 모델에 많은 요청을 한꺼번에 보내는 것만으로는 올바른 병렬 처리가 되지 않습니다.

4-4. 패턴 ④ 퍼사드와 어댑터 — 쓰는 쪽의 인터페이스를 일정하게 유지합니다

PageIndexClient는 사용자가 보는 공통 창구인 퍼사드입니다. 생성자에서 문서 저장소를 LocalAPI 또는 CloudAPI로 선택하고, 공개 메서드는 self._api에 위임합니다. 질의 모델의 선택은 별도로 보관합니다.

utils.unify_tree()는 로컬의 start_index와 Cloud의 page_index, 부모 요약의 summary와 prefix_summary를 공통 필드로 맞춥니다. 노드를 마음대로 옮겨 새 트리를 만드는 함수가 아니라, 호출자가 다뤄야 하는 형식 차이를 줄이는 경계입니다.

로컬 읽기 도구책임구현
browse_documents문서 이름·설명을 보고 대상 찾기agent_tools._browse_documents()
get_document상태와 문서 정보 확인agent_tools._get_document()
get_document_structure본문 없이 제목·요약·범위 읽기agent_tools._get_document_structure()
get_page_content선택한 페이지 원문 읽기agent_tools._get_page_content()

이 도구들은 agent_tools._tool_specs()에서 스키마와 호출 함수를 갖습니다. pageindex/integrations/openai_agents.py의 build_mcp_server()는 같은 기능을 프레임워크가 이해하는 도구로 바꿉니다. Cloud는 MCP 서버의 실제 도구 목록을 받아 중계하므로, 로컬의 네 도구와 항상 같다고 가정할 수 없습니다.

검색 경로도 고정된 이진 탐색 알고리즘이 아닙니다. 에이전트가 구조를 읽고 get_page_content()를 부르는 도구 사용 과정입니다. 로컬 도구의 문자 예산은 100_000 × 0.95 = 95_000으로 계산하며, 큰 구조는 part로 나누고 페이지 응답에는 반환·누락 범위를 알립니다. 이는 토큰 수 제한과는 다릅니다.

저장소 경계에서도 작은 계약을 쌓습니다

DocStore.save_document()는 tree.json과 pages.json을 먼저 쓰고 doc.json을 마지막에 씁니다. 읽는 쪽은 doc.json이 있는 문서를 대상으로 삼습니다. 파일 하나는 _write_json_atomic()에서 임시 파일 → flush() → fsync() → os.replace() 순서로 교체합니다.

여러 파일 전체가 DB 트랜잭션처럼 한 번에 커밋되는 것은 아닙니다. manifest.json은 목록 조회용 캐시이고, lock()은 POSIX의 fcntl을 사용합니다. Windows에서 같은 잠금 보장을 가정하지 않도록 구현의 분기를 읽어야 합니다.

또한 Flash의 구조와 저장용 페이지 텍스트는 서로 다른 PDF 파서에서 나옵니다. LocalAPI._check_page_bounds()가 범위를 검사하지만, 이것만으로 두 파서가 글자와 읽기 순서까지 똑같이 복원했다고 보장하지는 않습니다. 구조·본문·근거를 함께 확인하는 평가가 필요한 이유입니다.

5디렉토리 구조 해부

처음부터 전부 읽기보다 진입점과 각 파일의 역할부터 찾기
PageIndex/ ├── README.md 프로젝트 설명과 사용 예 ├── pyproject.toml 패키지·의존성·추가 옵션 ├── requirements.txt 직접 설치와 CI용 의존성 ├── run_pageindex.py PDF·Markdown 색인 CLI ├── pageindex/ │ ├── __init__.py 공개 API와 지연 임포트 │ ├── client.py 공통 SDK 진입점과 입력 검증 │ ├── types.py 색인·질의 설정 타입 │ ├── local_api.py 로컬 색인·문서 조회 │ ├── local_store.py JSON 저장과 목록 캐시 │ ├── cloud_api.py Cloud HTTP 클라이언트 │ ├── local_chat.py 모델·도구·응답 프로토콜 연결 │ ├── chat_stream.py 스트림과 응답 객체 │ ├── agent_tools.py 문서 도구 계약과 실행 │ ├── mcp_bridge.py Cloud MCP 통신 중계 │ ├── page_index_classic.py LLM 기반 목차 구성과 검증 │ ├── page_index_md.py Markdown 제목을 트리로 변환 │ ├── tree_optimize.py 병합·확장·비용 계산 │ ├── utils.py 요약 스케줄러·설정·공용 처리 │ ├── config.yaml classic 설정과 모델 설정 안내 │ ├── integrations/ 에이전트 프레임워크 어댑터 │ └── flash/ │ ├── api.py 공개 색인 함수와 거부 정책 │ ├── main.py 파이프라인 조립 │ ├── parser_pdfium_charlevel/ 문자·폰트·좌표 복원 │ ├── parser_pdfium_parallel.py 프로세스 기반 페이지 처리 │ ├── clustering/ · blocks/ 줄·블록 군집화 │ ├── columns/ · phases/ 다단 배치와 페이지 처리 │ ├── classification/ · labels/ 잡음·본문·캡션 분류 │ ├── heading_detection/ · title/ 제목 후보와 문서 제목 │ ├── outline_assembly/ · outline/ 목차 조립·정리 │ ├── model/ · stats/ · tokens/ 자료형·통계·문자 처리 │ ├── embedded_toc.py PDF 내장 목차 결합 │ └── data/ 글리프·문자·용어 사전 JSON ├── tests/ 회귀 테스트와 PDF 생성 보조 ├── examples/ │ ├── documents/ 예제 PDF 9개 │ └── results/ 저장된 구조 JSON 예제 ├── cookbook/ 9개 노트북과 Claude 사용 예 ├── assets/ SVG 도식과 PNG 차트 └── .github/ 테스트·배포·의존성·보안 워크플로

트리는 주요 파일과 폴더를 묶어 표시한 것입니다. pageindex/flash/만 74개 Python 파일이므로, 이곳을 첫 페이지부터 읽기 시작하면 전체 목적을 잃기 쉽습니다. 먼저 공개 API와 자료 모양을 익히고 내부 파싱으로 내려가는 편이 좋습니다.

읽는 순서출발 파일답할 질문
① 입력과 출력run_pageindex.py·pageindex/flash/README.md무엇을 넣으면 어떤 JSON이 나오는가?
② 실제 저장pageindex/local_api.py·pageindex/local_store.py노드와 원문은 어디서 만나고 언제 저장되는가?
③ 구조 추출pageindex/flash/api.py·pageindex/flash/main.py레이아웃에서 목차까지 어떤 단계를 거치는가?
④ 비용과 요약pageindex/tree_optimize.py·pageindex/utils.py무엇을 합치고 언제 요약을 시작하는가?
⑤ 질의 연결pageindex/agent_tools.py·pageindex/local_chat.py질문이 어떻게 페이지 읽기 도구 호출이 되는가?

5-1. 줄 수로 보는 무게중심

영역파일 수·줄 수해석
Python 전체112개 · 47,697줄패키지·테스트·CLI·보조 스크립트 포함
핵심 패키지96개 · 31,236줄pageindex/ 전체
Flash74개 · 17,486줄패키지 줄 수에 포함된 하위 영역
테스트 영역14개 · 16,215줄테스트 모듈 12개와 보조 파일 포함
프레임워크 통합4개 · 562줄패키지 줄 수에 포함
기타 언어·설정YAML 242줄 · TOML 70줄JS·TS·Rust·Go 소스는 없음

줄 수는 커밋 b3422f9의 파일에서 개행 수를 센 값이며 빈 줄과 주석을 포함합니다. .git/과 노트북 JSON은 Python 합계에서 제외했습니다. 9개 노트북의 코드 셀은 별도로 약 1,159줄입니다. Flash와 통합 코드는 패키지의 부분집합이므로 표의 행들을 모두 더하면 중복 계산됩니다.

테스트 영역은 전체 Python 줄 수의 약 34%입니다. 비중 자체가 품질의 증명은 아니지만, 실제 PDF 문자 복원과 트리 구조의 예외를 회귀 사례로 남기는 프로젝트라는 점은 배울 만합니다.

6학습 포인트 (기술별)

각 기술을 코드와 작은 실험으로 연결하기

6-1. Python 자료구조 — 재귀와 스택을 같은 문제에 적용합니다

page_index_md.build_tree_from_nodes()는 제목 수준을 스택에 저장합니다. 새 제목과 같거나 더 깊은 수준은 꺼내고, 남은 마지막 제목을 부모로 삼습니다. “제목 목록”이라는 평면 데이터를 “부모와 자식”으로 바꾸는 간단한 입문 예제입니다.

# build_tree_from_nodes()의 핵심 반복을 설명용으로 축약했습니다.
while stack and stack[-1][1] >= current_level:
    stack.pop()

if stack:
    parent_node, _ = stack[-1]
    parent_node["nodes"].append(tree_node)
else:
    root_nodes.append(tree_node)

stack.append((tree_node, current_level))

작은 실험: 제목 수준을 1, 2, 3, 2, 1로 써 놓고 매 줄마다 스택을 종이에 그려 보세요. 중간에 1, 3처럼 수준이 건너뛰면 어떤 부모가 선택되는지도 확인합니다. 정규식으로 제목을 찾는 것과 계층을 조립하는 것은 별도 문제입니다.

6-2. 문서 파싱 — 보이는 글자와 저장된 문자는 다를 수 있습니다

PDF는 문단 트리가 아니라 글자를 그리는 위치·폰트·코드를 저장할 수 있습니다. pageindex/flash/parser_pdfium_charlevel/은 문자 코드와 Unicode 연결, 글자 폭, 겹친 글자, 방향을 다룹니다. “텍스트 추출”을 문자열 하나 얻는 작업으로만 보면 놓치는 영역입니다.

작은 실험: 한 단 PDF와 두 단 PDF에서 추출한 줄 순서를 원문과 대조해 보세요. tests/test_flash_extraction.py의 글자 폭·아포스트로피·오른쪽에서 왼쪽으로 쓰는 문자 사례를 읽고, 잘못된 공백 하나가 제목 검출에 주는 영향을 적습니다.

6-3. 동시성 — CPU 작업과 네트워크 대기를 나눕니다

pageindex/flash/parser_pdfium_parallel.py는 PDFium의 FFI가 스레드 안전하지 않고 핸들이 프로세스에 속한다는 전제로 ProcessPoolExecutor를 사용합니다. 반면 모델 요청은 대기 시간이 커서 asyncio로 겹칩니다. “병렬 처리”라는 말 아래 서로 다른 도구가 쓰입니다.

작은 실험: 파서는 입력 페이지 수가 기본 64 미만이면 순차 경로를 탑니다. workers=1의 경로, 자동 작업자 수, 실패 시 순차 처리로 돌아가는 분기를 따라 그려 보세요. 모델 동시성 숫자와 CPU 작업자 숫자를 같은 설정으로 생각하지 않는 연습입니다.

6-4. API 설계 — 타입 힌트와 실행 시 검증을 구분합니다

pageindex/types.py의 TypedDict는 편집기와 정적 분석 도구에 설정 모양을 알려 줍니다. 실제 잘못된 값의 거부는 pageindex/client.py의 _resolve_index_slot()과 생성자 검증이 맡습니다. 타입 선언만으로 런타임 입력이 안전해지지는 않습니다.

작은 실험: index={"mode": "local", "api_key": "..."}가 왜 충돌하는지 설명해 보세요. 문서 저장소를 고르는 값과 모델 이름을 고르는 값이 섞일 때, 조용히 잘못된 모드로 실행하는 대신 오류를 내는 이유를 찾아봅니다.

6-5. 파일 저장 — 원자적 교체와 다중 파일 트랜잭션은 다릅니다

local_store._write_json_atomic()은 중간까지 쓰인 JSON이 정상 파일을 덮어쓰지 않도록 임시 파일을 교체합니다. 하지만 저장 과정에 파일이 세 개 있다고 해서 세 개가 한꺼번에 원자적으로 바뀌는 것은 아닙니다.

작은 실험: tree.json 저장 후 프로세스가 멈춘 경우와 doc.json 저장 후 목록 캐시 갱신이 실패한 경우를 나눠 보세요. list_metas()가 어떤 문서를 보이게 하는지 읽고, “완료 표시”와 “조회 캐시”의 차이를 설명합니다.

6-6. 테스트와 신뢰 경계 — 모델 출력도 검증할 입력입니다

tests/test_page_index.py는 LLM이 목차 항목 순서를 바꾸거나 실제 없는 99쪽을 반환하는 사례를 모킹합니다. tests/test_tree_format.py는 도입부 노드, 페이지 범위, 요약 의존성을 검사합니다. 모델이 그럴듯한 JSON을 반환하는 상황까지 실패 사례로 다룹니다.

page_index_classic._secure_doc_text()는 문서 내용을 데이터 경계로 감싸고, 알려진 주입 문구를 정리합니다. _validate_physical_indices()는 모델이 만든 페이지 주소를 검증합니다. 이런 방어가 모든 프롬프트 주입을 해결하는 것은 아니며, 모델 안내문과 결정적인 값 검사를 구분해 읽어야 합니다.

이 저장소에서 배울 가장 재사용하기 좋은 습관은 모델의 제안을 그대로 믿지 않고, 구조·범위·상태라는 프로그램의 계약으로 검증하는 것입니다.

학습 결과물
소스 파일마다 “입력·출력·실패” 세 칸을 적어 보세요

예를 들어 Flash의 입력은 PDF, 출력은 트리와 출처 상태, 실패는 읽을 텍스트 없음이나 구조 추출 실패입니다. 이 세 칸을 적을 수 있으면 함수 이름을 외우는 단계를 넘어 시스템의 경계를 이해한 것입니다.

7하드웨어 / 시스템 요구사항

GPU보다 입력 PDF의 상태와 모델 호출 조건을 먼저 확인하기

저장소는 공식 최소 CPU·RAM·디스크 용량을 수치로 명시하지 않습니다. 다음 표에서 Python 버전과 처리 방식은 소스에서 확인되는 사실이고, 학습용 장비는 시작을 위한 제안입니다. 성능 보장이나 대형 문서 처리 한계로 읽으면 안 됩니다.

항목확인된 요구·동작학습용 출발점
Python>=3.10 선언, CI는 3.10·3.13선언 범위 안의 별도 가상환경
운영체제CI 실행기는 ubuntu-latestmacOS·Windows는 의존성 설치와 잠금 차이 확인
CPUPDF 파싱에 CPU와 프로세스 풀 사용4코어급으로 작은 PDF부터 시작
메모리문자·블록·페이지 텍스트와 작업자별 문서 보유8GB 이상을 출발점으로 관찰, 큰 PDF는 사용량 측정
GPUPyTorch·CUDA·로컬 모델 가중치 의존성 없음원격 모델을 쓰면 이 패키지용 GPU 불필요
디스크기본 .pageindex/에 JSON 저장PDF와 추출 텍스트·색인·여유 공간 확보
네트워크기본 요약·확장·질의는 모델 연결 필요자격 증명·모델 접근·요청 제한 확인
PDF 조건로컬 SDK는 PDF, 텍스트 계층 필요선택·복사 가능한 짧은 PDF 사용

7-1. 메모리는 페이지 수에만 비례하지 않습니다

수식·폰트·글자가 빽빽한 100쪽과 제목이 큰 슬라이드 100쪽은 파서가 만드는 객체 수가 다릅니다. 병렬 파서는 기본적으로 CPU 수보다 하나 적은 작업자를 상한으로 삼고, 각 작업자가 문서를 엽니다. 빠른 CPU가 메모리 사용까지 줄여 주지는 않습니다.

작은 문서는 기본 64쪽 기준으로 순차 처리합니다. 저수준 extract_toc(..., workers=1)는 작업자를 제한할 수 있지만, 모든 SDK·CLI 설정에 같은 인자가 노출되어 있는 것은 아닙니다. 먼저 자신이 호출하는 층의 옵션을 확인해야 합니다.

7-2. “로컬”은 문서 저장과 에이전트 실행의 위치를 뜻합니다

로컬 모드라도 원격 모델을 연결하면 요약할 본문과 질의에 필요한 문서 내용이 그 모델 제공자에게 전송될 수 있습니다. 로컬 추론 서버를 별도로 연결하는 경우에는 그 모델의 하드웨어 요구사항을 따로 계산해야 합니다. PageIndex 자체가 로컬 모델 실행기를 제공하는 것은 아닙니다.

로컬의 get_ocr()라는 메서드 이름도 주의해야 합니다. LocalAPI는 PyPDF2로 추출해 저장한 텍스트를 반환합니다. 로컬 코드에 스캔 이미지를 문자로 읽는 OCR 엔진이 내장되어 있다는 뜻이 아닙니다. Cloud의 OCR·이미지 이해는 README와 서비스의 기능 설명입니다.

7-3. 동시성과 비용을 작게 시작합니다

요약과 확장은 별도 호출 묶음으로 겹칠 수 있습니다. 기본 상한 64와 32를 합치면 동시에 최대 96개의 색인 모델 호출이 가능하므로, 계정의 요청 제한에 맞춰 줄이는 편이 좋습니다. 예를 들어 summary_concurrency=2는 각 묶음의 상한을 조정하며, 전체 호출이 반드시 2개 이하라는 뜻은 아닙니다.

전체 비용에는 최초 색인과 이후 질의 중의 목차 읽기·페이지 읽기·추가 질문·답변 생성이 포함됩니다. submit_document()의 로컬 반환값에는 usage가 들어 있습니다. 평가에서는 정답률과 함께 입력·출력 토큰, 지연 시간, 도구 호출 횟수를 남기세요.

처음 실행할 때의 순서
기존 결과 읽기 → 모델 없는 구조 추출 → 제한된 모델 호출

먼저 예제 JSON으로 트리 모양을 익히면 비용이 들지 않습니다. 다음으로 저수준 Flash에서 요약과 최적화를 모두 끄고 구조를 살펴본 뒤, 작은 문서에 모델 호출을 붙이면 파서와 모델의 문제를 구별하기 쉽습니다.

8직접 해볼 수 있는 실습 과제

난이도별 다섯 과제 — 결과를 눈으로 확인할 기준까지 만들기

아래 명령과 코드는 독자가 별도의 학습 환경에서 사용할 예시입니다. 저장된 결과를 읽는 과제부터 시작하고, 모델을 연결하는 단계에서는 사용할 모델과 호출 비용을 직접 정합니다. 파일 경로는 저장소 루트를 기준으로 합니다.

과제 1

목차 JSON을 텍스트로 펼치기 입문

목표: 재귀 순회와 페이지 범위를 익힙니다. examples/results/earthmover_structure.json을 읽어 들여 제목 앞에 깊이만큼 들여쓰기를 붙이세요.

완료 기준: 부모·자식 관계가 보이고, 각 노드에 시작·끝 페이지가 출력됩니다. nodes가 없는 잎에서도 오류가 나지 않아야 합니다.

# 표준 라이브러리로 저장된 결과만 읽는 예시입니다.
import json
from pathlib import Path

path = Path("examples/results/earthmover_structure.json")
doc = json.loads(path.read_text(encoding="utf-8"))

def walk(nodes, depth=0):
    for node in nodes:
        print("  " * depth, node["title"],
              node.get("start_index"), node.get("end_index"))
        walk(node.get("nodes", []), depth + 1)

walk(doc["structure"])
과제 2

Markdown 제목에서 트리 만들기 입문

목표: 모델 없이도 문서 구조를 얻을 수 있음을 이해합니다. 제목 여섯 개를 가진 짧은 Markdown을 만들고, 제목 수준을 손으로 트리로 그립니다.

읽을 코드: extract_nodes_from_markdown()과 build_tree_from_nodes(). 코드 울타리 안의 제목은 건너뛰고, 한 줄 전체의 굵은 글씨는 수준 1로 처리하는 분기를 찾아보세요.

완료 기준: 1 → 2 → 3 → 2 → 1 수준 변화와 코드 블록 안의 가짜 제목을 구별합니다. 이 파서는 완전한 Markdown 문법 전체를 구현한 것이 아니라는 점도 설명해야 합니다.

# 별도 학습 파일 lesson.md를 대상으로 하는 CLI 예시입니다.
python3 run_pageindex.py --md_path lesson.md \
  --if-add-node-id yes --if-add-node-summary no \
  --if-add-doc-description no --if-add-node-text yes

Markdown 경로의 노드는 line_num을 사용합니다. PDF의 start_index·end_index와 단위를 혼동하지 마세요. 또한 로컬 SDK의 submit_document()는 PDF만 받으므로, 이 실습은 별도 CLI·Markdown 함수의 경로입니다.

과제 3

Flash의 구조 출처 비교하기 중급

목표: 레이아웃·북마크·평면 대체 결과를 구별합니다. 짧은 텍스트 PDF 두 개를 준비해 북마크 사용 여부를 바꾸고, toc_source와 노드 수·범위를 비교하세요.

완료 기준: 제목이 원문과 맞는지, 앞부분이 Preface로 덮이는지, 장 도입부가 누락되지 않는지 기록합니다. 북마크 옵션을 켰다고 결과가 반드시 bookmarks로 바뀌는 것은 아닙니다.

# 둘 다 요약과 모델 확장을 끈 CLI 예시입니다.
python3 run_pageindex.py --mode flash --pdf_path report.pdf \
  --no-summary --optimize off --no-embedded-toc

python3 run_pageindex.py --mode flash --pdf_path report.pdf \
  --no-summary --optimize off --embedded-toc

CLI는 두 실행 모두 results/report_structure.json에 저장하므로, 첫 결과를 별도 이름으로 보관한 뒤 비교합니다. 스캔본이 실패했다면 구조 옵션을 바꾸기 전에 텍스트 계층 유무부터 확인하세요. 10쪽 초과 평면 트리는 성공적인 계층 추출로 분류하지 않습니다.

과제 4

근거 페이지가 맞는 질의 평가 만들기 중급

목표: 답의 문장뿐 아니라 검색의 성공을 평가합니다. 한 문서에서 사실 조회 질문 5개를 만들고, 정답·정답 페이지·허용 오차를 먼저 적어 둡니다.

완료 기준: 정답 일치, 근거 페이지 적중, 모델이 실제 읽은 페이지, 총 토큰과 시간을 분리해 기록합니다. 요약만 보고 맞힌 듯한 답과 원문 근거를 읽은 답을 구별하세요.

# 사용할 모델 식별자는 학습 환경의 설정값으로 받습니다.
import os
from pageindex import PageIndexClient

client = PageIndexClient(
    index={
        "model": os.environ["INDEX_MODEL"],
        "storage_path": "./practice-store",
        "summary_concurrency": 2,
    },
    chat=os.environ["CHAT_MODEL"],
)
submitted = client.submit_document("report.pdf")
doc_id = submitted["doc_id"]
print(submitted["usage"])
print(client.get_document_structure(doc_id))
print(client.chat(
    "비용 증가의 원인을 원문에서 찾아 설명해 주세요.",
    doc_id=doc_id,
    citations=True,
))

모델 제공자의 자격 증명은 환경에 별도로 설정합니다. 위 코드의 INDEX_MODEL과 CHAT_MODEL은 예시에서 정한 환경 변수 이름입니다. 인용 태그가 붙었다는 사실과 그 페이지가 주장을 뒷받침한다는 사실은 다르므로, 마지막에는 원문으로 확인합니다.

과제 5

비용 함수와 비동기 계약을 작은 입력으로 재현하기 고급

목표: 모델의 제안과 프로그램의 채택 결정을 분리합니다. 12쪽 노드 아래 4쪽 노드 세 개를 제안하는 가짜 응답과, 1쪽 노드 아래 여러 제목을 제안하는 응답을 만듭니다.

읽을 코드: tree_cost(), expand_read_tokens(), SummaryScheduler.mark_final()·finish(), tests/test_client.py.

완료 기준: 비용이 같은 경우 병합하는 이유, 읽기 토큰이 줄지 않으면 확장을 거절하는 이유를 설명합니다. 확정 신호를 빼거나 확정 후 자식을 바꾸면 왜 실패해야 하는지도 재현하세요. 모델 응답은 모킹하여 비용 함수와 스케줄러 자체를 관찰합니다.

실습의 최종 결과물은 “답이 나오는 데모”를 넘어, 어떤 입력에서 왜 맞고 왜 실패하는지 설명하는 작은 평가표입니다.

9관련 기술 심화 학습 로드맵

5주 동안 자료구조에서 평가 가능한 RAG까지

Python의 함수·리스트·딕셔너리에 익숙하다는 가정의 학습 순서입니다. 주차는 진도 기준이며 필요한 시간은 경험에 따라 다릅니다. 처음부터 모든 PDF 예외를 이해하려 하지 말고, 매주 설명 가능한 결과물을 하나씩 만듭니다.

주차배울 개념·읽을 코드만들 결과물과 통과 기준
1주차JSON, 트리, 재귀, 스택
pageindex/page_index_md.py·examples/results/
제목 출력기와 손으로 그린 트리
깊이·부모·잎의 의미를 설명하기
2주차PDF 좌표, 글자·줄·블록, 휴리스틱
pageindex/flash/main.py·pageindex/flash/phases/
짧은 PDF 두 개의 구조 비교표
페이지 주소와 제목을 원문으로 검증하기
3주차재귀 비용 함수, 병합·확장, 입력 검증
pageindex/tree_optimize.py·tests/test_tree_format.py
가상 트리의 비용 계산 노트
복잡한 트리가 손해인 반례를 만들기
4주차비동기 의존성, 동시성 제한, 저장 일관성
SummaryScheduler·pageindex/local_store.py
부모·자식 작업의 시간 흐름도
확정 신호와 파일 완료 표시를 설명하기
5주차도구 스키마, 어댑터, 근거 평가
pageindex/agent_tools.py·pageindex/integrations/
질문·정답·근거·비용 평가표
검색 실패와 읽기 실패를 구분하기

9-1. 그다음에는 같은 데이터로 대안을 비교합니다

직접 만든 평가 세트에 단순 키워드 검색, 벡터 검색, 목차 기반 검색을 붙여 보세요. 동일한 PDF 추출 결과와 같은 질문을 쓰고, 검색 단계와 생성 단계를 따로 기록합니다. 더 좋은 모델을 쓴 효과를 검색기의 효과로 오해하지 않도록 조건을 맞춥니다.

로드맵의 도착점은 특정 검색 방식을 지지하는 것이 아니라, 내 문서에서 근거를 더 잘 찾는 설계를 측정하고 선택하는 능력입니다.

학습 비유

내비게이션을 공부한다면 먼저 지도를 읽고, 다음으로 길 찾기 비용을 계산하고, 마지막에 실제 이동 시간을 재야 합니다. PageIndex에서도 트리 모양, 비용 함수, 실제 질의 결과는 서로 다른 세 단계입니다.

10핵심 키워드 사전

앞으로 다른 RAG 저장소를 읽을 때도 다시 만날 용어
용어쉬운 설명
색인나중에 빨리 찾도록 미리 만들어 둔 주소록입니다. 여기서는 장·절과 페이지 범위를 가진 트리가 중심입니다.
RAG자료를 찾아 모델에 읽힌 뒤 답을 만드는 방식입니다. 찾는 방법은 벡터·키워드·트리 등으로 달라질 수 있습니다.
임베딩텍스트 등의 특징을 숫자 목록으로 표현한 것입니다. 숫자 사이의 가까움을 검색에 사용할 수 있습니다.
벡터 없는 검색이 검색 경로에서 벡터 색인과 임베딩 유사도에 의존하지 않는다는 뜻입니다. 모델 내부의 숫자 연산이 없다는 뜻은 아닙니다.
추론 기반 검색모델이 질문과 문서 구조를 해석해 읽을 곳을 고르는 방식입니다. 올바른 경로 선택이 자동 보장되지는 않습니다.
청크처리를 위해 나눈 텍스트 단위입니다. PageIndex도 구간과 페이지를 다루며, README의 “청킹 없음”은 고정 크기 조각 색인을 중심으로 삼지 않는다는 맥락입니다.
트리·노드·잎트리는 계층 전체, 노드는 항목 하나, 잎은 자식이 없는 끝 항목입니다.
물리적 페이지PDF 안에서 실제로 몇 번째 페이지인지입니다. 종이에 인쇄된 쪽 번호와 다를 수 있습니다.
텍스트 계층PDF 안에 문자로 들어 있는 정보입니다. 페이지 사진만 있으면 별도 OCR이 필요합니다.
OCR이미지 속 글자를 문자로 읽는 기술입니다. 텍스트 계층을 추출하는 작업과 구별합니다.
북마크PDF에 내장된 목차 정보입니다. 화면에 인쇄된 목차 페이지와는 별도 자료입니다.
휴리스틱문서의 모양과 반복 패턴에 근거한 경험적 규칙입니다. 예외와 검증이 함께 필요합니다.
병합·확장트리의 세부 항목을 접거나 큰 구간을 하위 절로 나누는 작업입니다.
잔여 페이지부모 범위 안에서 자식들이 덮지 않은 페이지입니다. 비용 계산에서 누락하면 안 됩니다.
비용 모델어떤 선택이 싼지 비교하려고 만든 계산 규칙입니다. 실제 시간·요금과 같은 값은 아닐 수 있습니다.
토큰모델이 텍스트를 다루는 단위입니다. 글자 수·단어 수·JSON 문자 예산과 일치하지 않습니다.
비동기·동시성기다리는 동안 다른 일을 진행하는 방식과, 동시에 진행 중인 작업의 수입니다.
프로세스 풀여러 실행 프로세스가 작업을 나눠 맡는 구조입니다. PDFium 파서는 스레드 대신 이를 사용합니다.
퍼사드·어댑터퍼사드는 여러 기능의 공통 창구, 어댑터는 서로 다른 인터페이스를 맞춰 주는 연결 부품입니다.
도구 호출·MCP모델이 정해진 입력 형식으로 기능을 요청하는 것이 도구 호출입니다. MCP는 도구와 자료를 연결하는 표준 프로토콜입니다.
원자적 교체파일이 반쯤 쓰인 상태로 보이지 않게 새 파일로 바꾸는 기법입니다. 여러 파일 전체의 트랜잭션과는 구별합니다.
회귀 테스트·모킹고친 문제가 다시 생기지 않는지 확인하는 테스트와, 외부 응답을 가짜 값으로 바꾸어 조건을 통제하는 방법입니다.
인용·근거 적중인용은 출처 주소의 표현이고, 근거 적중은 실제로 그곳에 답을 뒷받침할 내용이 있는지의 평가입니다.

11참고 링크

공식 문서와 구현을 함께 읽고, 현재 상태와 적용 한계를 확인하기

11-1. 공식 저장소·문서

11-2. 평가와 관련 기술

현재 상태 (2026-10-23 GitHub 기준)

항목상태
저장소·선정VectifyAI/PageIndex · GitHub Trending Monthly #19
스타 / 포크39,525 / 3,429 · 2026-10-23 GitHub 기준
설명·주 언어📑 PageIndex: Document Index for Vectorless, Reasoning-based RAG · Python
생성일2025-04-01
최신 릴리스v0.4.5 · 2026-10-22
최근 push2026-10-23 · GitHub 기준
분석 기준 커밋b3422f9 · “Merge pull request #617 from VectifyAI/cookbook-pageindex-metadata”
커밋 시각 2026-10-22 23:33:11 −0700, 한국 시각 2026-10-23 15:33:11
코드 규모Python 112개 파일 · 47,697줄, 그중 패키지 31,236줄·테스트 영역 16,215줄
YAML 242줄·TOML 70줄, 노트북 9개 별도
패키지 버전 표기pyproject.toml은 0.2.10. 배포 시 태그 버전으로 덮어쓰므로 최신 릴리스 번호와 구분해야 함
실행·지원 범위Python 3.10 이상 선언 · 기본 로컬 PDF 색인은 Flash · Markdown은 별도 함수·CLI
라이선스MIT · LICENSE 기준

11-3. README와 구현을 함께 읽어야 하는 지점

설명커밋 b3422f9에서 확인한 범위
벡터 DB·청킹 없음벡터 색인 대신 트리를 사용합니다. 구현에는 페이지·노드 구간, 큰 노드 분할, 모델 입력 묶음이 있습니다. 텍스트를 어떤 단위로도 나누지 않는다는 뜻은 아닙니다.
LLM 없이 구조 추출Flash의 최초 레이아웃 추출에 해당합니다. 기본 요약·확장·문서 설명은 모델을 사용합니다. classic은 구조 구성에도 모델을 씁니다.
메타데이터 Cloud 전용README 비교표와 달리 LocalAPI.submit_document(metadata=...)도 JSON을 검사·저장하며 조회 결과로 반환합니다. 메타데이터 스키마와 Cloud의 필터·검색 기능까지 같다는 뜻은 아닙니다.
설명 가능한 검색트리와 페이지 읽기 경로를 살펴볼 수 있습니다. 모델의 판단이 항상 옳거나 인용이 자동으로 검증된다는 뜻은 아닙니다.
쓰기 전에 알아둘 한계
입력·비용·평가·서비스 경계를 확인합니다

① 스캔·표·그림을 구분하세요. 로컬 SDK는 텍스트 PDF 중심이고 OCR 엔진을 포함하지 않습니다. 읽을 수 있는 글자가 있어도 표의 셀 관계나 그림의 의미까지 복원했다는 보장은 없습니다. 빈 본문이나 부실한 계층은 정상 검색 결과와 구별해야 합니다.

② 로컬 저장은 완전한 오프라인 실행과 다릅니다. 기본 색인 보완과 질의에는 모델이 필요합니다. 문서 내용의 전송 범위, 자격 증명, 동시성, 요금을 선택한 연결 방식에 맞춰 확인해야 합니다.

③ 요약과 인용을 원문으로 검증하세요. 잘못된 제목·페이지 범위·요약이 다음 선택을 어긋나게 할 수 있습니다. README의 정답률과 비용은 특정 평가의 보고값이며, 모든 문서와 모델에 적용되는 보장값이 아닙니다.

④ 공개 SDK와 Cloud 서버의 범위가 다릅니다. Cloud OCR·이미지 이해·파일 계층의 내부 구현은 이 저장소만으로 검증할 수 없습니다. 로컬 JSON 저장의 잠금·복구 특성도 다중 사용자 DB와 같다고 가정하면 안 됩니다.