Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
34251ba
Add PageIndex Flash for ultra fast LLM-free tree extraction
rejojer Jul 27, 2026
77e838c
Sort dicts imports in title scoring
rejojer Jul 27, 2026
c724370
Correct output schema in README
rejojer Jul 27, 2026
2cee169
Sync PageIndex Flash from private branch
rejojer Jul 30, 2026
0b66491
Sync PageIndex Flash from private branch
rejojer Jul 30, 2026
d70db2a
Merge remote-tracking branch 'origin/main' into feat/pageindex-flash
rejojer Jul 31, 2026
9c74471
Add tree optimization (merge & expand)
rejojer Jul 31, 2026
205e4e7
Sync PageIndex Flash from private branch
rejojer Jul 31, 2026
910250c
Add recursive node summaries for Flash
rejojer Jul 31, 2026
e55f92d
Sync PageIndex Flash from private branch
rejojer Jul 31, 2026
ff4727d
Trim README
rejojer Jul 31, 2026
59c9725
Update README
rejojer Jul 31, 2026
6f1fe1d
Merge remote-tracking branch 'origin/main' into feat/pageindex-flash
rejojer Jul 31, 2026
2f59820
Sync PageIndex Flash from private branch
rejojer Jul 31, 2026
62e5a56
Sync PageIndex Flash from private branch
rejojer Jul 31, 2026
3670d49
Define summary constants
rejojer Jul 31, 2026
83f8e5d
Raise summary concurrency to 64
rejojer Jul 31, 2026
58c12e5
Sync PageIndex Flash from private branch
rejojer Aug 1, 2026
93691d2
Parse summary replies without extract_json
rejojer Aug 1, 2026
1273697
Rework the optimize and summary model flags
rejojer Aug 1, 2026
5303b52
Merge main into the release branch
rejojer Aug 2, 2026
71ce87c
Sync PageIndex Flash from private branch
rejojer Aug 2, 2026
203002c
Sync PageIndex Flash from private branch
rejojer Aug 2, 2026
518b26a
Sync PageIndex Flash from private branch
rejojer Aug 2, 2026
4a2e13b
Sync PageIndex Flash from private branch
rejojer Aug 2, 2026
189ec1a
Sync PageIndex Flash from private branch
rejojer Aug 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 21 additions & 33 deletions pageindex/flash/README.md
Original file line number Diff line number Diff line change
@@ -1,22 +1,32 @@
# PageIndex Flash

Builds a PageIndex tree structure from a PDF using layout statistics alone.
No LLM, no API key, no OCR, no network. Runs in seconds, fully offline.
Builds the PageIndex tree structure from a PDF using layout statistics without
an LLM. Augmenting the tree with summaries and refining it for retrieval needs
an LLM.

## Usage

### Python

```python
from pageindex.flash import page_index_flash

tree = page_index_flash("paper.pdf")
tree = page_index_flash("paper.pdf") # with node summaries
tree = page_index_flash("paper.pdf", summary=False) # tree structure only, no LLM
tree = page_index_flash("paper.pdf", optimize=True) # with node summaries + refined tree
```

Takes a file path or an `io.BytesIO` stream and returns the tree as a dict.
Summaries are on by default and need an LLM API key.

### Command line

```bash
python3 run_pageindex.py --pdf_path document.pdf --flash
python3 run_pageindex.py --pdf_path document.pdf --flash --optimize
```

Accepts a path (`str` or `pathlib.Path`) or an `io.BytesIO` stream. Raises on a
missing, non-PDF, encrypted, empty, or unreadable file.
Writes the tree to `results/<name>_structure_flash.json`.

## Output

Expand All @@ -28,32 +38,22 @@ missing, non-PDF, encrypted, empty, or unreadable file.
{
"title": str,
"node_id": str, # 4-digit, zero-padded
"start_index": int,
"start_index": int, # 1-based, inclusive
"end_index": int,
"key_items": [str], # with --optimize: titles of merged-away subsections
"nodes": [...], # absent on leaf nodes
"summary": str,
"key_items": [str], # optimize only: titles of subsections merged away
"nodes": [...], # absent on leaves
}
],
}
```

Page indexes are 1-based. `nodes` nests the same shape recursively. Without
`--optimize` the extracted tree is returned as-is.

## Benchmark

Nine PDFs, each run end to end with tree optimization: PDF parse, layout
outline, merge, LLM expand, then a summary for every node.

![Time against document length](assets/time_vs_pages.png)

![Cost against document length](assets/cost_vs_pages.png)

Both scale close to linearly with length, at 218 s and $0.85 per 1,000 pages.
Two qualifiers. Cost follows node count a little more closely than page count,
$0.0007 to $0.0016 per node, so a densely structured document costs more than
its length suggests. And wall clock flattens past roughly 700 pages, where
summary concurrency rather than length becomes the limit.
<img src="assets/time_vs_pages.png" alt="Time against document length" width="50%">

| Document | Pages | Input tokens | Output tokens |
|---|---:|---:|---:|
Expand All @@ -68,16 +68,4 @@ summary concurrency rather than length becomes the limit.
| Machine Learning: A Probabilistic Perspective | 1,098 | 1,587,265 | 646,958 |
| **Total** | **2,985** | **3,751,599** | **1,392,588** |

Measured with `gpt-5.6-luna` at $0.20 / $1.20 per million input / output tokens,
priced cold with no prompt-cache discount.

## Limits

- Scanned PDFs without embedded text are not supported.
- Encrypted PDFs need preprocessing first.
- Headings drawn as vector paths, or very decorative layouts, can be missed.
- Titles are taken from the document text as-is.

## Dependencies

`pypdfium2`, `PyPDF2`, `regex`, `sortedcontainers`.
Measured with `gpt-5.6-luna`.
Binary file removed pageindex/flash/assets/cost_vs_pages.png
Binary file not shown.
Loading