Skip to content

feat: PDF full-text ingestion via pdfplumber - #29

Merged
Burton-David merged 1 commit into
mainfrom
feat/pdf-full-text
May 31, 2026
Merged

feat: PDF full-text ingestion via pdfplumber#29
Burton-David merged 1 commit into
mainfrom
feat/pdf-full-text

Conversation

@Burton-David

Copy link
Copy Markdown
Owner

What

Search results carry only title + abstract, so the section-aware chunker and analyze_paper never saw real body text. This adds an optional ingest step that fetches a paper's PDF and stores the extracted body on Paper.full_text — which _embedding_text, SectionAwareChunker, and the FAISS codec already consume (no changes needed there).

New pdf/ package — a PdfFetcher protocol and HttpPdfFetcher:

  • Streams the download under a 25 MiB cap; rejects non-PDFs by content-type and %PDF magic bytes (catches HTML landing pages).
  • Extracts page text with pdfplumber in a worker thread, capped at 500k chars — well above the analyzer's 60k prompt cap because the chunker wants the whole body, not just the head.
  • Treats <200 extracted chars as scanned/image-only and declines (OCR is out of scope).
  • A separate PdfTextCache keyed by paper.id (no TTL, atomic tmp+rename) avoids re-downloading; an empty cache entry records "no extractable text".
  • Never raises for a PDF-side problem — returns None, and ingest proceeds on title + abstract.

WiringLibraryService gains an optional pdf_fetcher; ingest_paper and bulk_ingest fill full_text best-effort (bulk bounded to 4 concurrent downloads). The server auto-enables it when pdfplumber is importable, with RESEARCH_MCP_DISABLE_PDF=1 to opt out (mirrors RESEARCH_MCP_DISABLE_PUBMED).

Dependencies — pdfplumber (MIT, on pdfminer.six) is the [pdf] extra. pyMuPDF was rejected: it's AGPL, incompatible with this MIT project. reportlab (dev) builds real fixture PDFs so tests exercise genuine extraction offline; CI installs [pdf] so the real-extraction path is covered.

Closes #4.

Test plan

  • ruff check src tests / mypy src — clean
  • pytest -q --strict-markers — 552 passed, 14 skipped
  • tests/unit/test_pdf_fetcher.py (reportlab fixture + httpx MockTransport): extract, disk-cache hit, HTTP error, HTML landing page, non-PDF magic bytes, byte cap, scanned→None (+cached), char cap, missing-pdfplumber→None
  • tests/unit/test_library_service.py: ingest fills full_text; graceful-degrade when the fetcher returns None or raises; no-fetcher leaves it unset; no-refetch when full_text already set; bulk fills each
  • Verified the real path against arXiv: fetched arxiv:1706.03762, extracted 35,539 chars containing "attention" / "transformer" / "bleu" / "encoder"

Search results carry only title + abstract, so the section-aware chunker and
analyze_paper never saw real body text. This adds an optional ingest step that
fetches a paper's PDF and stores the extracted body on Paper.full_text — which
_embedding_text, the chunker, and the codec already consume.

New pdf/ package: a PdfFetcher protocol and HttpPdfFetcher, which streams the
download under a 25 MiB cap, rejects non-PDFs by content-type and %PDF magic
bytes, extracts page text with pdfplumber in a worker thread (capped at 500k
chars — well above the analyzer's 60k prompt cap because the chunker wants the
whole body), and treats <200 extracted chars as scanned/image-only. A separate
PdfTextCache keyed by paper id (no TTL) avoids re-downloading; an empty cache
entry records 'no extractable text'. The fetcher never raises for a PDF-side
problem — it returns None and ingest proceeds on title + abstract.

LibraryService gains an optional pdf_fetcher; ingest_paper and bulk_ingest fill
full_text best-effort (bulk bounded to 4 concurrent downloads). The server
auto-enables it when pdfplumber is importable, with RESEARCH_MCP_DISABLE_PDF=1
to opt out. pdfplumber (MIT) ships as the [pdf] extra; reportlab (dev) builds
real fixture PDFs so the tests exercise genuine extraction offline.

Closes #4.
@Burton-David
Burton-David merged commit 34bdbbd into main May 31, 2026
1 check passed
@Burton-David
Burton-David deleted the feat/pdf-full-text branch May 31, 2026 16:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

PDF full-text ingestion + analysis

1 participant