fix(autoindex): parse PDFs in isolated workers, and use pdf_oxide for more complex PDF parsing - #20
Conversation
Motivation: The previous PDF extractor scanned raw streams and interpreted font character codes without resolving page resources, font encodings, or ToUnicode maps. Real financial reports therefore produced NUL-separated strings, shifted glyphs, and binary-looking content; schema inference correctly refused to elect that corrupted body as semantic_text. Root cause and implementation: Replace the byte scanner with pdf_oxide 0.3.75 and dispatch one document per fresh same-binary worker before Tokio starts. Preserve one-based page and pN-sN section provenance, disclose omitted-page/OCR coverage, normalize only a narrow legacy symbol allowlist, and reject suspicious Unicode or partial parser results instead of falling back to raw bytes. Add versioned parent/worker provenance validation. Containment and DX: Drain stdout/stderr after bounded retained prefixes to prevent pipe deadlock. On Unix, place workers in process groups, apply a 1536 MiB RLIMIT_AS, kill descendants, and reap on timeout/monitor failure. Cap concurrency at four and retained stdout at 32 MiB. Document that this is resource isolation rather than a security sandbox, that non-Unix has no OS memory cap, and that sampling currently fully parses/materializes PDFs before phase B parses again. Add --pdf-workers and --pdf-timeout-secs with actionable help. Measured four-report dry run: INTEL 2019: 279 records, 129/129 text pages, word ratio 0.7276. Walmart 2018: 676 records, 302/304 pages, 0.7126. CVS Health 2022: 617 records, 212/213 pages, 0.7690. PG&E 2017: 618 records, 240/267 pages, 0.7171. Autoindex accepted 4/4 with zero junk, elected body semantic_text at aggregate word ratio 0.77, and completed extraction/inference in 61.567 s with two PDF workers. This was dry-run evidence, not embedding or full indexing throughput. Cost: The same-base no-default release binary grows from 34,160,328 to 39,018,184 bytes: +4,857,856 bytes (+4.633 MiB, 14.22%). The graph gains 62 unique crate names; Cargo.lock changes by +747/-33 lines. Verification: - cargo fmt --check - clippy for autoindex/server, all targets, no default features, warnings denied - xerj-autoindex: 34 passed - same-binary PDF integration: 5 passed - ES-YAML: 1360 passed, 0 failed, 3 skipped Key files: - engine/crates/xerj-autoindex/src/extract/pdf.rs - engine/crates/xerj-autoindex/src/cli.rs - engine/crates/xerj-server/src/main.rs - engine/crates/xerj-server/tests/pdf_worker.rs
PR CI evaluates the PDF worker branch against current main, whose newly added route expressions require wrapping under the current stable rustfmt. Rebase the contribution onto current main and apply only rustfmt's mechanical router layout; no route, parser, or worker behavior changes.
af3d812 to
dedaa85
Compare
|
Merged. One fast-follow to file: |
Summary
Replace autoindex's raw PDF stream scanner with
pdf_oxide0.3.75 and run each PDF in a fresh same-binary worker process.The old extractor searched compressed PDF streams and interpreted font character codes without resolving the object graph, page resources, font encodings, or
/ToUnicodemappings. On real annual reports this silently produced NUL-separated strings, shifted glyphs, and binary-looking content. Downstream inference then correctly refused to electbodyassemantic_text.This change makes PDF extraction fail closed: parsed Unicode page text is retrieval-sized with page/section provenance, suspicious output is rejected, partial parser failures do not create partial indices, and image-only or damaged/encrypted inputs receive actionable repair/decrypt/OCR guidance. There is no raw-byte fallback.
User-facing behavior
xerj autoindex <folder>now launches a fresh hiddenxerj __extract-pdfworker for each PDF. The worker returns a versioned JSON response and the parent validates bothxerj-autoindexandpdf_oxideprovenance.Every accepted PDF record includes:
page;pN-sN;pdf_pages_total;pdf_pages_with_text;pdf_pages_omitted;pdf_ocr_warningwhen pages contain no usable text.New controls:
xerj autoindex --helpdocuments limits, OCR behavior, process isolation, the trusted developer override, parent-memory guidance, and the current double-parse cost.Architecture and safety limits
RLIMIT_AS.The pipe readers continue draining after their retained prefixes overflow, so an oversized response cannot deadlock on a full pipe. This is crash/resource isolation, not a security sandbox: the parser keeps the invoking user's filesystem and network authority.
XERJ_PDF_WORKER_BINremains a trusted developer/test hook. Its response is schema-, extractor-version-, parser-version-, and JSON-validated, but the executable itself runs with user authority.Four-report FinanceBench dry run
Command shape:
The input contained these previously blocking annual reports:
INTEL_2019_10K.pdfWALMART_2018_10K.pdfCVSHEALTH_2022_10K.pdfPG_E_2017_10K.pdfResult:
bodyautomatically elected assemantic_text;This was an autoindex dry run. It measured extraction and schema inference only: it did not write an index, generate embeddings, or finalize segments. The per-file record total is greater than 1,779 because inference caps each file's sample contribution.
The omitted-page count does not claim that every omitted page is scanned; it can include intentional blank pages. The warning deliberately says the page may require OCR.
Quality gate
The parser applies a narrow allowlist of known legacy symbol-font corrections, then rejects page text when more than 0.5% of visible characters are controls, replacement glyphs, or private-use codes, or fewer than 5% are alphanumeric.
On the pinned 368-document FinanceBench PDF corpus this accepted 367 documents and rejected one suspicious document. That is narrow calibration evidence, not a claim that the threshold is universal across all PDFs.
Binary and dependency cost
Both binaries were built from
e6e8e6ac3b017e9c7e03d40e3b9cf4f4e3bee55ewith:cd engine cargo build --release -j 32 -p xerj-server --no-default-featuresThe stripped binary grows 14.22%. The server dependency graph gains 62 unique crate names and the lockfile changes by 747 additions and 33 removals.
pdf_oxideis pinned to=0.3.75, usesdefault-features = falsepluslegacy-crypto, and is licensed MIT OR Apache-2.0.Verification
The subprocess suite covers deterministic output, exact page provenance and protocol versions, corrupt-PDF errors with empty stdout, mixed text/empty-page coverage, non-UTF-8 Unix filenames, stdout and stderr overflow without deadlock, timeout cleanup, descendant cleanup, and process reaping.
Main file pointers
engine/crates/xerj-autoindex/src/extract/pdf.rs: parser, worker protocol, quality gate, limits, cleanup, and provenance.engine/crates/xerj-autoindex/src/cli.rs: PDF controls and discoverability.engine/crates/xerj-autoindex/src/lib.rs: autoindex worker configuration.engine/crates/xerj-server/src/main.rs: pre-runtime hidden worker dispatch.engine/crates/xerj-server/tests/pdf_worker.rs: same-binary process and failure-mode coverage.Remaining limitations and follow-ups