Skip to content

perf(pmid): derive abstract from the single article-XML fetch#58

Merged
caufieldjh merged 1 commit into
mainfrom
feat/abstract-from-article-xml
Jul 9, 2026
Merged

perf(pmid): derive abstract from the single article-XML fetch#58
caufieldjh merged 1 commit into
mainfrom
feat/abstract-from-article-xml

Conversation

@caufieldjh

Copy link
Copy Markdown
Contributor

Follow-up to #57, addressing the review's deferred note 2 ("still two efetch calls per PMID").

What changed

PMIDSource.fetch() used to make two NCBI efetch calls per PMID:

  1. rettype="abstract", retmode="text" → the abstract
  2. rettype="xml" → the article XML (MeSH terms + publication types)

The article XML already contains the abstract, so this parses it from there and drops the second call (and its rate_limit_delay sleep). One fewer round-trip to NCBI per PMID.

  • New _parse_abstract(soup) reconstructs the abstract prose from Abstract/AbstractText, preserving structured-section labels (e.g. METHODS:, RESULTS:), mirroring how PubMed renders structured abstracts as text. Replaces the now-removed _fetch_abstract.

Behavior note (why this was deferred, and why it's safe)

content is now the abstract prose only, without the citation-header boilerplate (title/authors/journal/PMID/DOI) that the old text-mode fetch prepended. This is safe — and arguably cleaner:

  • Supporting-text validation (supporting_text_validator.py) fuzzy-matches claim text against abstract / full-text prose, never against citation headers. Verified no code or test relies on header text in content; the real/e2e validation tests use cached fixtures, not live fetches.
  • A record with no abstract now correctly yields no content (content_type "unavailable"/"summary") instead of a header-only stub that was misleadingly tagged abstract_only.

Live verification

lookup PMID:33301246 now reconstructs the structured abstract from a single efetch:

BACKGROUND: Severe acute respiratory syndrome coronavirus 2 ...

METHODS: In an ongoing multinational, placebo-controlled ...

Testing

New unit tests: structured / unstructured / absent / empty abstract parsing, plus a fetch() test asserting exactly one Entrez.efetch call backs all three fields (abstract, MeSH, publication types). Doctests on _parse_abstract.

Full just test green: 625 passed, mypy + ruff clean.

🤖 Generated with Claude Code

Addresses the deferred note 2 from PR #57 review. Previously fetch()
made two efetch calls per PMID: one for the abstract (rettype=abstract,
text) and one for the article XML (MeSH terms + publication types). The
article XML already contains the abstract, so parse it from there and
drop the second call along with its rate-limit sleep.

- New _parse_abstract reconstructs prose from Abstract/AbstractText,
  preserving structured-section labels (e.g. "METHODS:"); replaces the
  now-removed _fetch_abstract.
- content is now the abstract prose only, without the citation-header
  boilerplate the text fetch used to prepend. Supporting-text validation
  matches claims against abstract/full-text prose, not headers, so this
  is neutral-to-cleaner; a record with no abstract now correctly yields
  no content instead of a header-only stub.

Tests: structured/unstructured/absent/empty abstract parsing, plus a
fetch() test asserting exactly one efetch call backs all three fields.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@claude

claude Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Claude finished @caufieldjh's task in 1m 30s —— View job


Review: perf(pmid): derive abstract from the single article-XML fetch

  • Read PR diff
  • Review pmid.py changes (abstract parsing, single fetch)
  • Review test coverage
  • Post review feedback

Nice, focused change. It cleanly resolves note 2 from #57 — dropping one efetch round-trip and one rate_limit_delay sleep per PMID by reconstructing the abstract from the article XML that was already being fetched for MeSH terms and publication types. The behavior-change reasoning in the PR body is sound, and I confirmed there are no dangling references to the removed _fetch_abstract in the source tree (only historical mentions in docs/).

Strengths

  • _parse_abstract correctly targets <Abstract> (not <OtherAbstract>), preserves structured-section Labels, and skips empty nodes.
  • Test coverage is good: structured / unstructured / absent / empty, plus the test_fetch_uses_single_efetch assertion that mock_efetch.call_count == 1 — that call-count assertion is exactly what pins the perf win in place. Doctests double as docs. 👍
  • Removing the header-only stub so a record with no abstract yields unavailable/summary instead of a misleading abstract_only is a genuine correctness improvement.

Minor observations (non-blocking)

  1. Structured abstracts that use NlmCategory without Label (pmid.py:229). Some PubMed records label sections only via NlmCategory="METHODS" and omit the human-readable Label attribute. For those, the current code produces unlabelled prose (sections still joined, just no METHODS: prefix). Not a correctness bug for fuzzy matching — the prose is preserved — but if you want to mirror PubMed's text rendering more faithfully, consider falling back to NlmCategory when Label is absent:

    label = node.get("Label") or node.get("NlmCategory")

    (PubMed suppresses the UNLABELLED category, so you'd want to guard against emitting UNLABELLED:.)

  2. Records with only <OtherAbstract> (author-manuscript / non-English abstracts). soup.find("Abstract") returns None for these, so content is now empty where the old text-mode efetch might have surfaced that text. Likely rare and arguably fine to skip, but worth a mention as a behavioral nuance since it's a subtle difference from the old path.

  3. Inline markup spacing (node.get_text(), pmid.py:226). get_text() concatenates nested <sub>/<sup>/<i> content with no separator (e.g. H<sub>2</sub>OH2O, usually desirable; but adjacent inline tags without whitespace can merge words). Minor and consistent with _parse_mesh_terms; only flagging for awareness.

None of these block merging — the change is correct and well-tested as written. If you'd like, I'm happy to add the NlmCategory fallback (item 1) in a follow-up.

I was not able to run just test in this review pass, but the CI on this PR should cover the 625-test suite the description references.
feat/abstract-from-article-xml

@caufieldjh caufieldjh merged commit e07f338 into main Jul 9, 2026
7 checks passed
@caufieldjh caufieldjh deleted the feat/abstract-from-article-xml branch July 9, 2026 19:13
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.

1 participant