SDK
b3422f9 기준입니다.PageIndex는 긴 문서의 계층형 목차를 만들고, 그 목차를 읽는 에이전트가 답의 근거 페이지를 찾도록 돕는 SDK입니다. 핵심 아이디어는 문장의 뜻이 비슷한 조각을 찾는 대신, 질문에 답이 있을 법한 장과 절을 판단한 뒤 원문을 읽게 하는 것입니다.
예를 들어 “작년보다 영업이익률이 낮아진 이유는?”이라는 질문을 받았다고 합시다. “영업이익률”이라는 단어가 반복되는 모든 곳을 읽기보다, 먼저 목차에서 경영 실적과 비용 분석을 고릅니다. 그다음 해당 페이지의 숫자와 설명을 읽고 답합니다. 목차의 요약은 이동 경로를 고르는 안내이고, 답을 뒷받침하는 근거는 실제 본문입니다.
카드 상자는 질문과 비슷한 글이 적힌 카드를 빨리 골라 줍니다. 독서 보조원은 먼저 “이 질문은 어느 장의 내용일까?”를 생각하고, 그 장을 펼칩니다.
PageIndex의 structure가 목차, summary가 안내 문구, start_index와 end_index가 책갈피입니다. 보조원 역할을 맡는 LLM은 목차를 잘못 고를 수도 있으므로, 정답과 근거 페이지를 함께 평가해야 합니다.
# 구조를 설명하기 위한 작은 예시 — 실제 추출 결과가 아닙니다.
{
"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 파일의 물리적 페이지 순서는 다를 수 있습니다. 표지와 앞부분의 로마 숫자 쪽 번호가 대표적인 원인입니다.
트리와 재귀, 문서 레이아웃 분석, 비용 함수, 비동기 작업 의존성, 도구 스키마, 원자적 파일 저장을 한 흐름으로 배웁니다. 새 모델을 학습하는 저장소가 아니라, 모델과 문서 처리 코드를 묶어 검색 기능으로 만드는 사례입니다.
2026-10-23 기준 GitHub Trending Monthly #19, 스타 39,525개입니다. 순위와 스타만으로 유입 원인을 증명할 수는 없습니다. 다만 README의 제안과 구현을 함께 보면 관심을 끌 만한 지점은 분명합니다. 익숙한 RAG 구성에서 벡터 DB를 빼고, 문서 구조와 모델의 판단을 검색의 중심에 놓습니다.
계약서의 일반 조항과 예외 조항은 비슷한 단어를 쓸 수 있습니다. 질문은 예외의 적용 여부를 묻는데 일반 설명만 가져오면, 문장은 자연스러워도 답은 틀립니다. PageIndex는 장·절의 제목과 상위 문맥을 이용해 관련 부분을 고르는 경로를 제안합니다. 이 문제를 모든 벡터 검색이 해결하지 못한다고 일반화해서는 안 됩니다.
PageIndex의 장점은 검색 경로를 문서 구조로 설명할 수 있다는 것이며, 정답률의 우위는 문서와 질문을 맞춘 평가로 확인해야 합니다. 벡터 RAG도 메타데이터·재순위화·페이지 인용을 붙일 수 있고, PageIndex도 구조 추출이나 에이전트의 판단에서 실패할 수 있습니다.
| 비교 항목 | 일반적인 벡터 RAG 구성 | PageIndex 로컬 구현 |
|---|---|---|
| 검색의 주소록 | 조각의 임베딩과 문서 정보 | 제목·요약·페이지 범위를 가진 트리 |
| 후보를 고르는 법 | 유사도 검색 후 필터·재순위화 | LLM이 도구로 목차와 페이지를 읽음 |
| 문서 경계 | 고정 길이 또는 의미 단위 분할 | 레이아웃과 제목의 장·절 경계 활용 |
| 학습할 장점 | 많은 후보에서 빠르게 좁히는 구조 | 문서 계층과 페이지 근거를 연결하는 구조 |
| 주요 비용 | 임베딩·검색 인프라·생성 호출 | 색인 보완·요약·질의 중 도구 반복 호출 |
| 어려운 입력 | 파서가 놓친 내용, 검색 누락 | 제목이 약한 문서, 잘못된 트리, 선택 오류 |
| 운영 구성 | 선택한 DB와 검색 서비스에 따라 다름 | 로컬 JSON 저장 + 선택한 모델 연결 |
| 대상 | 주로 맡는 역할 | PageIndex를 배울 이유 |
|---|---|---|
| FAISS | 벡터의 근접 이웃을 찾는 라이브러리 | 거리 계산 대신 문서 계층을 탐색하는 대안 학습 |
| Qdrant | 벡터 검색을 제공하는 데이터베이스 | 별도 벡터 저장소 없이 문서별 검색을 구성하는 예 |
| LangChain·LlamaIndex | 모델·검색기·도구를 조합하는 넓은 프레임워크 | PDF 구조화와 페이지 도구에 집중된 구현 읽기 |
| PDF 전체 입력 | 질문마다 모델에 문서 전체를 전달 | 색인을 재사용하며 선택한 부분만 읽히는 설계 |
위 표는 역할의 비교입니다. 같은 조건에서 실행한 성능 순위가 아닙니다. 특히 대규모 문서 집합을 검색하는 서비스와 단일 PDF 안의 관련 페이지를 찾는 문제는 크기가 다릅니다. README의 “수백만 문서” 파일 계층은 Cloud 전용으로 소개되며, 로컬 DocStore에 같은 기능이 구현되어 있다는 뜻은 아닙니다.
README는 2026년 8월 업데이트로 Flash와 로컬·클라우드 SDK를 소개합니다. 코드에서도 LocalAPI.submit_document()의 기본 모드는 flash입니다. 처음부터 모든 목차를 LLM으로 추정하지 않고 글자 크기·배치·번호 체계에서 구조를 뽑는다는 점이, 기존 classic 경로와 구별됩니다.
또한 같은 클라이언트로 로컬 색인을 쓰다가 클라우드 저장소로 바꿀 수 있고, 에이전트에 검색 도구만 연결할 수도 있습니다. 초보자에게는 “모델 호출 한 번”이 아니라 재사용 가능한 문서 처리 인터페이스를 설계하는 예제라는 의미가 있습니다.
README는 FinanceBench 98.7%와 로컬 색인 약 $0.001/페이지, 9~1,098쪽 문서의 약 13초~4.5분을 보고합니다. 서로 다른 평가의 모델·설정에 따른 보고값입니다. FinanceBench 결과를 현재 기본 SDK의 모든 PDF 질의에 적용할 수 없습니다.
README가 소개하는 OSS 평가는 34개 PDF의 62개 사실 조회 질문을 대상으로 합니다. 문서 종류, 모델, 질문 난도, 인용 평가, 비용 계산 조건을 함께 보아야 합니다. 스타 수는 관심의 크기이며 정확도의 측정값은 아닙니다.
이 저장소의 중심은 Python 라이브러리와 CLI입니다. package.json, Cargo.toml, go.mod, Dockerfile은 없습니다. 웹 서비스의 화면과 서버 전체를 공개한 형태가 아니므로, 백엔드·프론트엔드·인프라를 같은 비중으로 상상하면 구조를 잘못 읽게 됩니다.
| 역할 | pyproject.toml 선언 | 실제 사용 지점 |
|---|---|---|
| 언어 | python >=3.10 | 타입 힌트, asyncio, 프로세스 풀 |
| 모델 라우팅 | litellm >=1.105.1,<1.106 | utils.llm_completion()·llm_acompletion() |
| 모델 SDK | openai >=1.70.0 | pageindex/local_chat.py의 프로토콜별 모델 연결 |
| 에이전트 실행 | openai-agents >=0.20.0 | local_chat._openai_agent() |
| 도구 연결 | mcp >=1.19.0,<3 | pageindex/mcp_bridge.py·pageindex/integrations/ |
| 문자·레이아웃 | pypdfium2 >=5 | pageindex/flash/parser_pdfium_charlevel/ |
| 페이지 텍스트 | PyPDF2 >=3.0.0 | LocalAPI._extract_page_texts() |
| 이미지 처리 | Pillow >=9.0 | pageindex/imaging.py의 영역 강조 처리 |
| 문자열·정렬 | regex >=2024.0.0 · sortedcontainers >=2.4.0 | 문자 분류, 정렬된 자료구조를 쓰는 파싱 보조 |
| 환경·설정 | python-dotenv >=1.0.0 · pyyaml >=6.0 | pageindex/utils.py와 pageindex/config.yaml |
| HTTP 통신 | requests >=2.28.0 · urllib3 >=1.26 | pageindex/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 파일이 없습니다.
pageindex[claude] 추가 옵션은 claude-agent-sdk >=0.2.166, pageindex[anthropic]은 anthropic >=0.122.0을 선택적으로 연결합니다. [openai] 추가 옵션은 호환성을 위해 비어 있고, 기본 의존성에 관련 SDK가 이미 들어 있습니다.
| 사용자 접점 | 이 저장소에서 확인되는 것 | 학습할 경계 |
|---|---|---|
| CLI | run_pageindex.py의 argparse | PDF·Markdown 입력과 처리 옵션 검증 |
| Python 호출 | PageIndexClient·ChatStream | 저장·질의·스트림을 앱에 연결하는 SDK |
| 노트북 | cookbook/의 9개 .ipynb | 사용 예와 평가 절차를 설명하는 실행 문서 |
| 웹 UI | React·Vue 등 앱 소스와 JS 빌드 설정 없음 | README의 호스팅 앱을 로컬 UI로 오해하지 않기 |
| Cloud API | CloudAPI의 HTTP 클라이언트 | 서버 내부 OCR·저장 구현은 여기서 확인 불가 |
의존성 목록은 “이 프로젝트가 무엇을 쓰는가”를 보여 주고, 호출 지점은 “그 도구가 어디까지 책임지는가”를 보여 줍니다. 이름에 API가 들어간 LocalAPI도 웹 서버가 아니라 프로세스 안에서 호출되는 Python 객체입니다.
poetry-core와 poetry.core.masonry.api를 사용합니다. pageindex/config.yaml과 Flash의 JSON 사전도 배포물에 포함합니다..github/workflows/tests.yml은 Ubuntu에서 Python 3.10·3.13, 에이전트 프레임워크 유무를 조합합니다. PDFium 4 계열 호환용 조합도 하나 더 둡니다.gate 잡이 always()로 실행되어 테스트 결과가 success인지 검사합니다. 취소된 테스트를 통과로 취급하지 않으려는 설계입니다..github/workflows/publish.yml은 v* 태그에서 버전을 얻어 pyproject.toml에 주입한 후 빌드합니다. PyPI 발행에는 OIDC 신뢰 게시를 씁니다.moderate입니다. CodeQL의 languages: actions는 워크플로 검사를 뜻하며, Python 전체 분석이라고 읽으면 안 됩니다.이 저장소는 도서관 건물 전체보다 “문서 정리 장비와 사서용 도구함”에 가깝습니다. 웹 화면은 별도 앱이 맡을 수 있고, 클라우드 창고는 클라이언트 뒤에 있습니다. 공개된 도구함의 책임 범위를 먼저 파악해야 코드가 단순해집니다.
색인 모델과 질의 모델은 역할이 다릅니다. 전자는 트리 보완·요약·설명을 맡고, 후자는 질문에 따라 읽을 곳을 판단합니다. index="cloud"로 문서 저장소를 바꾸어도 chat=...을 지정하면 자기 모델의 에이전트로 읽을 수 있습니다. 두 선택을 별도로 다루는 것이 SDK 설계의 핵심입니다.
전체 흐름에서 가장 중요한 계약은 “트리의 각 노드가 실제로 읽을 수 있는 페이지 범위를 가리킨다”는 것입니다. 제목과 요약이 아무리 그럴듯해도 주소가 틀리면 검색이 실패합니다.
pageindex/flash/main.py의 extract_toc()는 긴 PDF를 한 번의 프롬프트로 처리하지 않습니다. parse_charlevel_meta_parallel()에서 문자와 좌표를 얻고, process_page()로 페이지를 구성한 뒤 문서 전체 통계와 블록을 만듭니다.
assemble_outline()과 유효성 검사로 계층을 만듭니다.순서는 기능입니다. 반복되는 머리말을 먼저 분류하지 않으면 큰 글씨가 모두 장 제목으로 올라갈 수 있습니다. 페이지의 회전과 좌표계를 맞추지 않으면 열 순서가 바뀔 수 있습니다. 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()는 요약을 켜고 문서 설명도 생성하므로, 위 저수준 호출과 동작이 다릅니다.
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을 각각 독립적인 강제 상한으로 해석하면 코드와 달라집니다.
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 집합으로 관리하여 접었다가 다시 펴는 반복을 막습니다. 반환되는 탐색 복잡도 수치는 설계한 비용 함수의 값이며, 실제 응답 시간이나 정확도를 보증하지 않습니다.
서랍에 분류표를 더 붙인다고 늘 빨리 찾는 것은 아닙니다. 종이 한 장을 찾는데 서랍 세 겹을 여는 것보다 종이 한 장을 바로 읽는 편이 낫습니다. 이 코드는 “분류의 정교함”에 비용을 매겨 지나친 계층을 줄입니다.
pageindex/utils.py의 SummaryScheduler는 짧은 잎 노드에는 원문을 그대로 쓰고, 나머지 잎은 원문에서 요약합니다. 부모는 자식의 요약과 도입부를 합쳐 요약합니다. mark_final()은 “이 노드의 자식 구성이 더는 바뀌지 않는다”는 신호입니다. _visit()은 그 신호와 자식 작업을 기다립니다.
Flash의 _optimize_and_summarize()는 최적화기의 on_final에 scheduler.mark_final을 연결합니다. 문서 전체 최적화가 끝날 때까지 기다리지 않고, 바뀌지 않을 부분부터 요약합니다. 독립 작업의 병렬성과 부모·자식 순서 제약을 동시에 다루는 예입니다.
SUMMARY_CONCURRENCY = 64: 요약 모델 호출의 기본 동시성입니다.EXPAND_CONCURRENCY = 32: 확장 모델 호출의 기본 상한입니다.SUMMARY_MAX_WORDS = 150: 프롬프트로 요청하는 요약의 단어 수 상한입니다. 글자 수나 응답 토큰 강제 상한이 아닙니다.SUMMARY_INTRO_MAX_PAGES = 3: 부모 요약에 넣는 도입부 페이지 수의 기본 상한입니다.finish()는 확정 후 자식이 바뀌었는지, 확정 신호를 받지 못한 노드가 있는지 검사합니다. 신호가 빠지면 끝없이 기다릴 수 있으므로 오류를 발생시킵니다. 일부 요약 실패에는 대체 요약을 쓰지만, 시도한 요약 모델 호출이 모두 실패하면 예외를 발생시킵니다.
초보자가 여기서 배울 것은 asyncio.gather() 문법 이상입니다. “무엇이 동시에 가능하고, 무엇을 기다려야 하는가”를 데이터의 의존성으로 표현해야 합니다. 요약 모델에 많은 요청을 한꺼번에 보내는 것만으로는 올바른 병렬 처리가 되지 않습니다.
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()가 범위를 검사하지만, 이것만으로 두 파서가 글자와 읽기 순서까지 똑같이 복원했다고 보장하지는 않습니다. 구조·본문·근거를 함께 확인하는 평가가 필요한 이유입니다.
트리는 주요 파일과 폴더를 묶어 표시한 것입니다. 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 | 질문이 어떻게 페이지 읽기 도구 호출이 되는가? |
| 영역 | 파일 수·줄 수 | 해석 |
|---|---|---|
| Python 전체 | 112개 · 47,697줄 | 패키지·테스트·CLI·보조 스크립트 포함 |
| 핵심 패키지 | 96개 · 31,236줄 | pageindex/ 전체 |
| Flash | 74개 · 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 문자 복원과 트리 구조의 예외를 회귀 사례로 남기는 프로젝트라는 점은 배울 만합니다.
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처럼 수준이 건너뛰면 어떤 부모가 선택되는지도 확인합니다. 정규식으로 제목을 찾는 것과 계층을 조립하는 것은 별도 문제입니다.
PDF는 문단 트리가 아니라 글자를 그리는 위치·폰트·코드를 저장할 수 있습니다. pageindex/flash/parser_pdfium_charlevel/은 문자 코드와 Unicode 연결, 글자 폭, 겹친 글자, 방향을 다룹니다. “텍스트 추출”을 문자열 하나 얻는 작업으로만 보면 놓치는 영역입니다.
작은 실험: 한 단 PDF와 두 단 PDF에서 추출한 줄 순서를 원문과 대조해 보세요. tests/test_flash_extraction.py의 글자 폭·아포스트로피·오른쪽에서 왼쪽으로 쓰는 문자 사례를 읽고, 잘못된 공백 하나가 제목 검출에 주는 영향을 적습니다.
pageindex/flash/parser_pdfium_parallel.py는 PDFium의 FFI가 스레드 안전하지 않고 핸들이 프로세스에 속한다는 전제로 ProcessPoolExecutor를 사용합니다. 반면 모델 요청은 대기 시간이 커서 asyncio로 겹칩니다. “병렬 처리”라는 말 아래 서로 다른 도구가 쓰입니다.
작은 실험: 파서는 입력 페이지 수가 기본 64 미만이면 순차 경로를 탑니다. workers=1의 경로, 자동 작업자 수, 실패 시 순차 처리로 돌아가는 분기를 따라 그려 보세요. 모델 동시성 숫자와 CPU 작업자 숫자를 같은 설정으로 생각하지 않는 연습입니다.
pageindex/types.py의 TypedDict는 편집기와 정적 분석 도구에 설정 모양을 알려 줍니다. 실제 잘못된 값의 거부는 pageindex/client.py의 _resolve_index_slot()과 생성자 검증이 맡습니다. 타입 선언만으로 런타임 입력이 안전해지지는 않습니다.
작은 실험: index={"mode": "local", "api_key": "..."}가 왜 충돌하는지 설명해 보세요. 문서 저장소를 고르는 값과 모델 이름을 고르는 값이 섞일 때, 조용히 잘못된 모드로 실행하는 대신 오류를 내는 이유를 찾아봅니다.
local_store._write_json_atomic()은 중간까지 쓰인 JSON이 정상 파일을 덮어쓰지 않도록 임시 파일을 교체합니다. 하지만 저장 과정에 파일이 세 개 있다고 해서 세 개가 한꺼번에 원자적으로 바뀌는 것은 아닙니다.
작은 실험: tree.json 저장 후 프로세스가 멈춘 경우와 doc.json 저장 후 목록 캐시 갱신이 실패한 경우를 나눠 보세요. list_metas()가 어떤 문서를 보이게 하는지 읽고, “완료 표시”와 “조회 캐시”의 차이를 설명합니다.
tests/test_page_index.py는 LLM이 목차 항목 순서를 바꾸거나 실제 없는 99쪽을 반환하는 사례를 모킹합니다. tests/test_tree_format.py는 도입부 노드, 페이지 범위, 요약 의존성을 검사합니다. 모델이 그럴듯한 JSON을 반환하는 상황까지 실패 사례로 다룹니다.
page_index_classic._secure_doc_text()는 문서 내용을 데이터 경계로 감싸고, 알려진 주입 문구를 정리합니다. _validate_physical_indices()는 모델이 만든 페이지 주소를 검증합니다. 이런 방어가 모든 프롬프트 주입을 해결하는 것은 아니며, 모델 안내문과 결정적인 값 검사를 구분해 읽어야 합니다.
이 저장소에서 배울 가장 재사용하기 좋은 습관은 모델의 제안을 그대로 믿지 않고, 구조·범위·상태라는 프로그램의 계약으로 검증하는 것입니다.
예를 들어 Flash의 입력은 PDF, 출력은 트리와 출처 상태, 실패는 읽을 텍스트 없음이나 구조 추출 실패입니다. 이 세 칸을 적을 수 있으면 함수 이름을 외우는 단계를 넘어 시스템의 경계를 이해한 것입니다.
저장소는 공식 최소 CPU·RAM·디스크 용량을 수치로 명시하지 않습니다. 다음 표에서 Python 버전과 처리 방식은 소스에서 확인되는 사실이고, 학습용 장비는 시작을 위한 제안입니다. 성능 보장이나 대형 문서 처리 한계로 읽으면 안 됩니다.
| 항목 | 확인된 요구·동작 | 학습용 출발점 |
|---|---|---|
| Python | >=3.10 선언, CI는 3.10·3.13 | 선언 범위 안의 별도 가상환경 |
| 운영체제 | CI 실행기는 ubuntu-latest | macOS·Windows는 의존성 설치와 잠금 차이 확인 |
| CPU | PDF 파싱에 CPU와 프로세스 풀 사용 | 4코어급으로 작은 PDF부터 시작 |
| 메모리 | 문자·블록·페이지 텍스트와 작업자별 문서 보유 | 8GB 이상을 출발점으로 관찰, 큰 PDF는 사용량 측정 |
| GPU | PyTorch·CUDA·로컬 모델 가중치 의존성 없음 | 원격 모델을 쓰면 이 패키지용 GPU 불필요 |
| 디스크 | 기본 .pageindex/에 JSON 저장 | PDF와 추출 텍스트·색인·여유 공간 확보 |
| 네트워크 | 기본 요약·확장·질의는 모델 연결 필요 | 자격 증명·모델 접근·요청 제한 확인 |
| PDF 조건 | 로컬 SDK는 PDF, 텍스트 계층 필요 | 선택·복사 가능한 짧은 PDF 사용 |
수식·폰트·글자가 빽빽한 100쪽과 제목이 큰 슬라이드 100쪽은 파서가 만드는 객체 수가 다릅니다. 병렬 파서는 기본적으로 CPU 수보다 하나 적은 작업자를 상한으로 삼고, 각 작업자가 문서를 엽니다. 빠른 CPU가 메모리 사용까지 줄여 주지는 않습니다.
작은 문서는 기본 64쪽 기준으로 순차 처리합니다. 저수준 extract_toc(..., workers=1)는 작업자를 제한할 수 있지만, 모든 SDK·CLI 설정에 같은 인자가 노출되어 있는 것은 아닙니다. 먼저 자신이 호출하는 층의 옵션을 확인해야 합니다.
로컬 모드라도 원격 모델을 연결하면 요약할 본문과 질의에 필요한 문서 내용이 그 모델 제공자에게 전송될 수 있습니다. 로컬 추론 서버를 별도로 연결하는 경우에는 그 모델의 하드웨어 요구사항을 따로 계산해야 합니다. PageIndex 자체가 로컬 모델 실행기를 제공하는 것은 아닙니다.
로컬의 get_ocr()라는 메서드 이름도 주의해야 합니다. LocalAPI는 PyPDF2로 추출해 저장한 텍스트를 반환합니다. 로컬 코드에 스캔 이미지를 문자로 읽는 OCR 엔진이 내장되어 있다는 뜻이 아닙니다. Cloud의 OCR·이미지 이해는 README와 서비스의 기능 설명입니다.
요약과 확장은 별도 호출 묶음으로 겹칠 수 있습니다. 기본 상한 64와 32를 합치면 동시에 최대 96개의 색인 모델 호출이 가능하므로, 계정의 요청 제한에 맞춰 줄이는 편이 좋습니다. 예를 들어 summary_concurrency=2는 각 묶음의 상한을 조정하며, 전체 호출이 반드시 2개 이하라는 뜻은 아닙니다.
전체 비용에는 최초 색인과 이후 질의 중의 목차 읽기·페이지 읽기·추가 질문·답변 생성이 포함됩니다. submit_document()의 로컬 반환값에는 usage가 들어 있습니다. 평가에서는 정답률과 함께 입력·출력 토큰, 지연 시간, 도구 호출 횟수를 남기세요.
먼저 예제 JSON으로 트리 모양을 익히면 비용이 들지 않습니다. 다음으로 저수준 Flash에서 요약과 최적화를 모두 끄고 구조를 살펴본 뒤, 작은 문서에 모델 호출을 붙이면 파서와 모델의 문제를 구별하기 쉽습니다.
아래 명령과 코드는 독자가 별도의 학습 환경에서 사용할 예시입니다. 저장된 결과를 읽는 과제부터 시작하고, 모델을 연결하는 단계에서는 사용할 모델과 호출 비용을 직접 정합니다. 파일 경로는 저장소 루트를 기준으로 합니다.
목표: 재귀 순회와 페이지 범위를 익힙니다. 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"])
목표: 모델 없이도 문서 구조를 얻을 수 있음을 이해합니다. 제목 여섯 개를 가진 짧은 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 함수의 경로입니다.
목표: 레이아웃·북마크·평면 대체 결과를 구별합니다. 짧은 텍스트 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쪽 초과 평면 트리는 성공적인 계층 추출로 분류하지 않습니다.
목표: 답의 문장뿐 아니라 검색의 성공을 평가합니다. 한 문서에서 사실 조회 질문 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은 예시에서 정한 환경 변수 이름입니다. 인용 태그가 붙었다는 사실과 그 페이지가 주장을 뒷받침한다는 사실은 다르므로, 마지막에는 원문으로 확인합니다.
목표: 모델의 제안과 프로그램의 채택 결정을 분리합니다. 12쪽 노드 아래 4쪽 노드 세 개를 제안하는 가짜 응답과, 1쪽 노드 아래 여러 제목을 제안하는 응답을 만듭니다.
읽을 코드: tree_cost(), expand_read_tokens(), SummaryScheduler.mark_final()·finish(), tests/test_client.py.
완료 기준: 비용이 같은 경우 병합하는 이유, 읽기 토큰이 줄지 않으면 확장을 거절하는 이유를 설명합니다. 확정 신호를 빼거나 확정 후 자식을 바꾸면 왜 실패해야 하는지도 재현하세요. 모델 응답은 모킹하여 비용 함수와 스케줄러 자체를 관찰합니다.
실습의 최종 결과물은 “답이 나오는 데모”를 넘어, 어떤 입력에서 왜 맞고 왜 실패하는지 설명하는 작은 평가표입니다.
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/ | 질문·정답·근거·비용 평가표 검색 실패와 읽기 실패를 구분하기 |
직접 만든 평가 세트에 단순 키워드 검색, 벡터 검색, 목차 기반 검색을 붙여 보세요. 동일한 PDF 추출 결과와 같은 질문을 쓰고, 검색 단계와 생성 단계를 따로 기록합니다. 더 좋은 모델을 쓴 효과를 검색기의 효과로 오해하지 않도록 조건을 맞춥니다.
로드맵의 도착점은 특정 검색 방식을 지지하는 것이 아니라, 내 문서에서 근거를 더 잘 찾는 설계를 측정하고 선택하는 능력입니다.
내비게이션을 공부한다면 먼저 지도를 읽고, 다음으로 길 찾기 비용을 계산하고, 마지막에 실제 이동 시간을 재야 합니다. PageIndex에서도 트리 모양, 비용 함수, 실제 질의 결과는 서로 다른 세 단계입니다.
| 용어 | 쉬운 설명 |
|---|---|
| 색인 | 나중에 빨리 찾도록 미리 만들어 둔 주소록입니다. 여기서는 장·절과 페이지 범위를 가진 트리가 중심입니다. |
| RAG | 자료를 찾아 모델에 읽힌 뒤 답을 만드는 방식입니다. 찾는 방법은 벡터·키워드·트리 등으로 달라질 수 있습니다. |
| 임베딩 | 텍스트 등의 특징을 숫자 목록으로 표현한 것입니다. 숫자 사이의 가까움을 검색에 사용할 수 있습니다. |
| 벡터 없는 검색 | 이 검색 경로에서 벡터 색인과 임베딩 유사도에 의존하지 않는다는 뜻입니다. 모델 내부의 숫자 연산이 없다는 뜻은 아닙니다. |
| 추론 기반 검색 | 모델이 질문과 문서 구조를 해석해 읽을 곳을 고르는 방식입니다. 올바른 경로 선택이 자동 보장되지는 않습니다. |
| 청크 | 처리를 위해 나눈 텍스트 단위입니다. PageIndex도 구간과 페이지를 다루며, README의 “청킹 없음”은 고정 크기 조각 색인을 중심으로 삼지 않는다는 맥락입니다. |
| 트리·노드·잎 | 트리는 계층 전체, 노드는 항목 하나, 잎은 자식이 없는 끝 항목입니다. |
| 물리적 페이지 | PDF 안에서 실제로 몇 번째 페이지인지입니다. 종이에 인쇄된 쪽 번호와 다를 수 있습니다. |
| 텍스트 계층 | PDF 안에 문자로 들어 있는 정보입니다. 페이지 사진만 있으면 별도 OCR이 필요합니다. |
| OCR | 이미지 속 글자를 문자로 읽는 기술입니다. 텍스트 계층을 추출하는 작업과 구별합니다. |
| 북마크 | PDF에 내장된 목차 정보입니다. 화면에 인쇄된 목차 페이지와는 별도 자료입니다. |
| 휴리스틱 | 문서의 모양과 반복 패턴에 근거한 경험적 규칙입니다. 예외와 검증이 함께 필요합니다. |
| 병합·확장 | 트리의 세부 항목을 접거나 큰 구간을 하위 절로 나누는 작업입니다. |
| 잔여 페이지 | 부모 범위 안에서 자식들이 덮지 않은 페이지입니다. 비용 계산에서 누락하면 안 됩니다. |
| 비용 모델 | 어떤 선택이 싼지 비교하려고 만든 계산 규칙입니다. 실제 시간·요금과 같은 값은 아닐 수 있습니다. |
| 토큰 | 모델이 텍스트를 다루는 단위입니다. 글자 수·단어 수·JSON 문자 예산과 일치하지 않습니다. |
| 비동기·동시성 | 기다리는 동안 다른 일을 진행하는 방식과, 동시에 진행 중인 작업의 수입니다. |
| 프로세스 풀 | 여러 실행 프로세스가 작업을 나눠 맡는 구조입니다. PDFium 파서는 스레드 대신 이를 사용합니다. |
| 퍼사드·어댑터 | 퍼사드는 여러 기능의 공통 창구, 어댑터는 서로 다른 인터페이스를 맞춰 주는 연결 부품입니다. |
| 도구 호출·MCP | 모델이 정해진 입력 형식으로 기능을 요청하는 것이 도구 호출입니다. MCP는 도구와 자료를 연결하는 표준 프로토콜입니다. |
| 원자적 교체 | 파일이 반쯤 쓰인 상태로 보이지 않게 새 파일로 바꾸는 기법입니다. 여러 파일 전체의 트랜잭션과는 구별합니다. |
| 회귀 테스트·모킹 | 고친 문제가 다시 생기지 않는지 확인하는 테스트와, 외부 응답을 가짜 값으로 바꾸어 조건을 통제하는 방법입니다. |
| 인용·근거 적중 | 인용은 출처 주소의 표현이고, 근거 적중은 실제로 그곳에 답을 뒷받침할 내용이 있는지의 평가입니다. |
| 항목 | 상태 |
|---|---|
| 저장소·선정 | 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 |
| 최근 push | 2026-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 기준 |
| 설명 | 커밋 b3422f9에서 확인한 범위 |
|---|---|
| 벡터 DB·청킹 없음 | 벡터 색인 대신 트리를 사용합니다. 구현에는 페이지·노드 구간, 큰 노드 분할, 모델 입력 묶음이 있습니다. 텍스트를 어떤 단위로도 나누지 않는다는 뜻은 아닙니다. |
| LLM 없이 구조 추출 | Flash의 최초 레이아웃 추출에 해당합니다. 기본 요약·확장·문서 설명은 모델을 사용합니다. classic은 구조 구성에도 모델을 씁니다. |
| 메타데이터 Cloud 전용 | README 비교표와 달리 LocalAPI.submit_document(metadata=...)도 JSON을 검사·저장하며 조회 결과로 반환합니다. 메타데이터 스키마와 Cloud의 필터·검색 기능까지 같다는 뜻은 아닙니다. |
| 설명 가능한 검색 | 트리와 페이지 읽기 경로를 살펴볼 수 있습니다. 모델의 판단이 항상 옳거나 인용이 자동으로 검증된다는 뜻은 아닙니다. |
① 스캔·표·그림을 구분하세요. 로컬 SDK는 텍스트 PDF 중심이고 OCR 엔진을 포함하지 않습니다. 읽을 수 있는 글자가 있어도 표의 셀 관계나 그림의 의미까지 복원했다는 보장은 없습니다. 빈 본문이나 부실한 계층은 정상 검색 결과와 구별해야 합니다.
② 로컬 저장은 완전한 오프라인 실행과 다릅니다. 기본 색인 보완과 질의에는 모델이 필요합니다. 문서 내용의 전송 범위, 자격 증명, 동시성, 요금을 선택한 연결 방식에 맞춰 확인해야 합니다.
③ 요약과 인용을 원문으로 검증하세요. 잘못된 제목·페이지 범위·요약이 다음 선택을 어긋나게 할 수 있습니다. README의 정답률과 비용은 특정 평가의 보고값이며, 모든 문서와 모델에 적용되는 보장값이 아닙니다.
④ 공개 SDK와 Cloud 서버의 범위가 다릅니다. Cloud OCR·이미지 이해·파일 계층의 내부 구현은 이 저장소만으로 검증할 수 없습니다. 로컬 JSON 저장의 잠금·복구 특성도 다중 사용자 DB와 같다고 가정하면 안 됩니다.