Self-hosted document intelligence for RAG — parse, classify, and split PDF/DOCX/PPTX into searchable, cited, retrieval-ready chunks
Project description
ingestlib
Self-hosted document intelligence for RAG pipelines. One library that takes a raw document — PDF, DOCX, PPTX — and produces searchable, cited, retrieval-ready chunks: the territory of LlamaParse / Reducto / Unstructured.io, running on your own stack.
from ingestlib.services import ingest, retrieve
ingest("finance-10k.pdf") # parse → classify → split → embed → vector store
result = retrieve("what were the total revenues?")
print(result.context) # ranked chunks, each citing doc · page · section
What it does
| Stage | What you get |
|---|---|
| Parse | Layout-aware markdown per page: tables as HTML (merged cells intact), formulas as LaTeX, charts converted to data tables (estimated values marked ~, printed callouts and growth labels captured), figures extracted as PNG crops with captions and AI descriptions — every block traceable to a bounding box on the page |
| Classify | Document-type label (invoice, research_paper, …) — open-ended or constrained to your categories, with confidence and alternatives. Works standalone with no OCR |
| Split | Sections (pages grouped by role: methods, results, …) containing natural chunks — boundaries follow the content, tables never split, each chunk carries a [category › section › heading] breadcrumb in its embedding_text |
| Ingest | The whole pipeline in one call, every stage persisted to S3, vectors upserted, deduplicated by content checksum |
| Retrieve | Question → hybrid search (dense embeddings + lexical sparse, merged) → Jina rerank → hits with scores and citations, plus a prompt-ready context block |
Engines: PaddleOCR-VL-1.6 (0.9B VLM, runs on your GPU) for layout + recognition, Amazon Nova 2 Lite for judgment (chart reading, review, classification, chunk boundaries), Nova multimodal embeddings, Pinecone or Qdrant for vectors (both hybrid dense + sparse), S3 for artifacts. ~$0.002/page in LLM spend.
Quickstart
1. Requirements
- Python 3.12+ and uv
- AWS account with Bedrock access (
us-east-1): Nova 2 Lite + Nova 2 multimodal embeddings - Vector database — Pinecone account (serverless, free tier works; the default) or a Qdrant server (local docker or Qdrant Cloud)
- Jina AI account for reranking (free tier: 100 RPM)
2. Install
git clone https://github.com/LangModule/ingestlib.git
cd ingestlib
uv sync
System dependency — LibreOffice (DOCX/PPTX → PDF conversion):
brew install --cask libreoffice # macOS (binary is `soffice`)
sudo apt install libreoffice-core libreoffice-writer libreoffice-impress # Linux
3. Start the OCR inference server
Parse runs PaddleOCR-VL-1.6 behind an inference server. First launch downloads ~1.8 GB of weights; later launches load from cache in seconds.
# Apple Silicon (Metal GPU)
uv run python -m mlx_vlm.server --port 8111 --model PaddlePaddle/PaddleOCR-VL-1.6
# NVIDIA (then set paddle_vl.backend: vllm-server in config.yaml)
vllm serve PaddlePaddle/PaddleOCR-VL-1.6 --port 8111
The layout model (PP-DocLayoutV3, ~126 MB) auto-downloads on the first parse.
4. Configure
cp .env.example .env # paste your API keys (Jina + Pinecone and/or Qdrant)
cp config.example.yaml config.yaml # your AWS profile, bucket name, vector store choice
aws configure --profile your-aws-profile # Bedrock-enabled credentials
Edit config.yaml: your AWS profile/account, S3 bucket name (globally
unique), vector store choice and index/collection names. The S3 bucket and
the vector indexes/collection are created automatically on first use — no
manual setup.
Config is discovered at call time, never at import: INGESTLIB_CONFIG=/path/to/config.yaml
wins, otherwise the working directory and its parents are searched — so
installed usage works the same as running inside this repo.
5. Run
from ingestlib.services import ingest, retrieve
r = ingest("report.pdf")
print(r.status, r.category, r.chunks, r.durations)
res = retrieve("what does the report conclude?", top_k=5)
for hit in res.hits:
print(hit.rerank_score, hit.citation, hit.chunk.heading)
Using the operations directly
Every operation also works standalone:
from ingestlib.operations import parse, classify, split
result = parse("report.pdf") # ParseResult: pages, regions, figures
print(result.markdown) # whole-document markdown
result.save_images("out/") # extracted figures/charts as PNGs
label = classify("report.pdf") # no OCR needed — native text + embedded images
chunks = split(result, category=label.category)
for c in chunks.chunks:
print(c.token_estimate, c.embedding_text.splitlines()[0])
Persistence and vector access are explicit too:
from ingestlib.storage import artifacts, PineconeStore
doc_id = artifacts.save_parse(result) # S3: source, result.json, page PNGs, crops
artifacts.list_documents() # registry: filename, pages, category, chunks
Architecture
src/ingestlib/
├── services/ ingest · retrieve — the product
├── operations/ parse · classify · split — the tools (each standalone)
├── storage/ artifacts (S3) · base (VectorStore contract) · pinecone · qdrant
├── foundations/ llm (Bedrock Nova, Jina) · ocr (PaddleOCR-VL)
├── utils/ logger · files
└── config.py config.yaml + .env → typed configs
Strict downward dependencies. The VectorStore contract means backends drop
in as connectors — both ship hybrid search: Pinecone (dense + hosted
sparse model, merged client-side) and Qdrant (dense + BM25 with
server-side IDF and RRF fusion; local docker or cloud). Pick one with
vector_store: pinecone | qdrant in config.yaml. Keys for both can sit in
.env — only the selected connector ever builds a client.
Logging
INGESTLIB_LOG_LEVEL=INFO # DEBUG | INFO | WARNING | ERROR (default INFO)
INGESTLIB_LOG_THIRD_PARTY=1 # also show paddlex/httpx/botocore chatter
INGESTLIB_LOG_COLOR=0 # disable colored output
Testing
Tests hit real APIs, never mocks. Pure logic runs always; server-hitting suites are opt-in via env gates.
make test # fast suite (~180 tests, ~90s; e2e groups skip)
make test-parse # parse e2e (needs VL server + Bedrock)
make test-classify # classify e2e (needs Bedrock)
make test-split # split e2e (needs Bedrock)
make test-s3 # artifact store e2e (needs AWS)
make test-pinecone # vector connector e2e (needs Pinecone + Bedrock)
make test-qdrant # vector connector e2e (needs a Qdrant server + Bedrock)
make test-services # full product e2e (needs the entire stack)
make test-all # everything
make eval # retrieval quality eval (see below)
Fixture PDFs live in tests/data/pdf/ — 14 real documents (research papers,
earnings decks, insurance forms, timetables, 10-Ks).
Retrieval quality
Beyond pass/fail tests, evals/ measures retrieval quality: 22 ground-truth
questions over the fixture corpus, run through the real retrieve() flow
under dense/hybrid × rerank on/off, scored by hit@k and MRR. Measured so far:
with reranking, every answer lands in the top 3 hits (hit@3 = 1.00);
hit@1 ranges 0.86–1.00 across runs. Each run saves a timestamped snapshot to
evals/results/, so quality changes are visible over time.
Disk footprint
| Component | Size | Location |
|---|---|---|
| Python deps | ~3 GB | .venv/ |
| PaddleOCR-VL-1.6 weights | ~1.8 GB | ~/.cache/huggingface/hub/ |
| PP-DocLayoutV3 | ~126 MB | ~/.paddlex/official_models/ |
| LibreOffice | ~600 MB | system |
Scope
English documents; PDF / DOCX / PPTX input. Images, charts, and tables inside documents are fully extracted and interpreted; direct image files and handwriting are out of scope by design.
Roadmap
- Hover-highlight review UI (bbox provenance already shipped for it)
- Extract: schema-driven field extraction with source provenance
License
See LICENSE.
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file ingestlib-0.1.0.tar.gz.
File metadata
- Download URL: ingestlib-0.1.0.tar.gz
- Upload date:
- Size: 11.5 MB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.14 {"installer":{"name":"uv","version":"0.11.14","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d906d8924754a8f13536248286be9af07c6fd15c6b9bb371c0c7d658737dfc61
|
|
| MD5 |
95140a5214def372ed35d6dc6b1306ff
|
|
| BLAKE2b-256 |
b91138c667f6c96ea5e2ef2d1629209ea1a681c4c0ba668c4d124f846d9422b3
|
File details
Details for the file ingestlib-0.1.0-py3-none-any.whl.
File metadata
- Download URL: ingestlib-0.1.0-py3-none-any.whl
- Upload date:
- Size: 88.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.14 {"installer":{"name":"uv","version":"0.11.14","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e8e4ffd57f0e37a88ee437a2f7f1da072c1801297aec2aa16f833aa092f8f586
|
|
| MD5 |
d69761dc6d341bfda2276314b8fe525f
|
|
| BLAKE2b-256 |
2e187e4e39bc0cb20e04d726b0f9d7bb36583c7b6b560c6190ec005060825fb5
|