|
| 1 | +--- |
| 2 | +layout: integration |
| 3 | +name: oxidize-pdf |
| 4 | +description: Convert PDFs into Haystack Documents with a fast Rust engine and element-disjoint RAG chunking; accepts paths and ByteStreams |
| 5 | +authors: |
| 6 | + - name: Santiago Fernández Muñoz |
| 7 | + socials: |
| 8 | + github: bzsanti |
| 9 | +pypi: https://pypi.org/project/haystack-oxidize-pdf |
| 10 | +repo: https://github.com/bzsanti/oxidize-pdf-integrations/tree/main/haystack |
| 11 | +type: Data Ingestion |
| 12 | +report_issue: https://github.com/bzsanti/oxidize-pdf-integrations/issues |
| 13 | +version: Haystack 2.0 |
| 14 | +toc: true |
| 15 | +--- |
| 16 | +### **Table of Contents** |
| 17 | +- [Overview](#overview) |
| 18 | +- [Installation](#installation) |
| 19 | +- [Usage](#usage) |
| 20 | +- [License](#license) |
| 21 | + |
| 22 | +## Overview |
| 23 | + |
| 24 | +[`haystack-oxidize-pdf`](https://pypi.org/project/haystack-oxidize-pdf/) is a Haystack |
| 25 | +converter backed by [oxidize-pdf](https://github.com/bzsanti/oxidize-python), a |
| 26 | +Rust-powered PDF engine with first-class RAG primitives. The parser runs natively (no |
| 27 | +system dependencies — it ships as a wheel for Linux, macOS and Windows) and exposes |
| 28 | +element-disjoint semantic chunking, so PDFs become retrieval-ready `Document` objects |
| 29 | +without any post-processing. |
| 30 | + |
| 31 | +The chunking contract is enforced by regression tests: no chunk's text is a substring of |
| 32 | +another's, and every source element appears in exactly one chunk. This guarantees no |
| 33 | +duplicated context leaks into a vector store during ingestion. |
| 34 | + |
| 35 | +## Installation |
| 36 | + |
| 37 | +```bash |
| 38 | +pip install haystack-oxidize-pdf |
| 39 | +``` |
| 40 | + |
| 41 | +The package depends on `haystack-ai>=2.0,<3` and `oxidize-pdf>=0.4.3`. |
| 42 | + |
| 43 | +## Usage |
| 44 | + |
| 45 | +### Components |
| 46 | + |
| 47 | +This integration introduces `OxidizePdfConverter`, a `@component` that reads PDF sources |
| 48 | +and outputs Haystack `Document` objects. Sources may be file paths (`str` / |
| 49 | +`pathlib.Path`) or `ByteStream` instances, interchangeably. |
| 50 | + |
| 51 | +The converter has a single `__init__` argument, `mode`, controlling how each source is |
| 52 | +turned into documents: |
| 53 | + |
| 54 | +- `mode="rag"` (default): one `Document` per semantic chunk produced by oxidize-pdf's |
| 55 | + chunker. Per-chunk metadata exposes `chunk_index` (0-based, resets per source), |
| 56 | + `page_numbers` (1-indexed), `element_types`, `heading_context`, and `token_estimate`. |
| 57 | +- `mode="pages"`: one `Document` per page (plain text); metadata carries `page_number` |
| 58 | + (1-indexed). |
| 59 | +- `mode="markdown"`: a single `Document` per source containing the whole PDF as markdown; |
| 60 | + no `page_number` is emitted. |
| 61 | + |
| 62 | +### Use the Converter standalone |
| 63 | + |
| 64 | +```python |
| 65 | +from haystack_oxidize_pdf import OxidizePdfConverter |
| 66 | + |
| 67 | +converter = OxidizePdfConverter() # mode="rag" by default |
| 68 | +result = converter.run(sources=["paper.pdf"]) |
| 69 | + |
| 70 | +for doc in result["documents"]: |
| 71 | + print(doc.meta["chunk_index"], doc.meta["heading_context"]) |
| 72 | + print(doc.content[:200]) |
| 73 | +``` |
| 74 | + |
| 75 | +### Use it in a Pipeline |
| 76 | + |
| 77 | +```python |
| 78 | +from haystack import Pipeline |
| 79 | +from haystack_oxidize_pdf import OxidizePdfConverter |
| 80 | + |
| 81 | +pipeline = Pipeline() |
| 82 | +pipeline.add_component("converter", OxidizePdfConverter(mode="rag")) |
| 83 | +# ...add an embedder, a document writer, etc. |
| 84 | + |
| 85 | +result = pipeline.run({"converter": {"sources": ["paper.pdf"]}}) |
| 86 | +documents = result["converter"]["documents"] |
| 87 | +``` |
| 88 | + |
| 89 | +### ByteStream input |
| 90 | + |
| 91 | +The converter accepts `ByteStream` objects natively (via oxidize-pdf's |
| 92 | +`PdfReader.from_bytes`), so PDFs that never touch disk — uploads, objects fetched from |
| 93 | +blob storage — can be ingested directly. `ByteStream.meta` is merged into each output |
| 94 | +`Document.meta`: |
| 95 | + |
| 96 | +```python |
| 97 | +from haystack.dataclasses import ByteStream |
| 98 | +from haystack_oxidize_pdf import OxidizePdfConverter |
| 99 | + |
| 100 | +with open("paper.pdf", "rb") as f: |
| 101 | + stream = ByteStream( |
| 102 | + data=f.read(), |
| 103 | + mime_type="application/pdf", |
| 104 | + meta={"upstream_origin": "s3://bucket/key"}, |
| 105 | + ) |
| 106 | + |
| 107 | +docs = OxidizePdfConverter().run(sources=[stream])["documents"] |
| 108 | +# each doc.meta carries upstream_origin == "s3://bucket/key" |
| 109 | +``` |
| 110 | + |
| 111 | +### Batch sources with per-source metadata |
| 112 | + |
| 113 | +`meta` may be a single dict (broadcast to every output document) or a list of dicts (one |
| 114 | +per source, lengths must match): |
| 115 | + |
| 116 | +```python |
| 117 | +docs = OxidizePdfConverter(mode="markdown").run( |
| 118 | + sources=["doc-a.pdf", "doc-b.pdf"], |
| 119 | + meta=[{"tag": "first"}, {"tag": "second"}], |
| 120 | +)["documents"] |
| 121 | +# docs[0].meta["tag"] == "first"; docs[1].meta["tag"] == "second" |
| 122 | +``` |
| 123 | + |
| 124 | +Caller-supplied `meta` overrides base file-level fields (`file_path`, `file_name`, |
| 125 | +`total_pages`, `pdf_version`), but per-document fields (`chunk_index`, `page_numbers`, |
| 126 | +`page_number`) are applied last and are never overwritten. |
| 127 | + |
| 128 | +## License |
| 129 | + |
| 130 | +`haystack-oxidize-pdf` is distributed under the terms of the |
| 131 | +[MIT license](https://github.com/bzsanti/oxidize-pdf-integrations/blob/main/LICENSE). |
0 commit comments