Export DOIs and PMCIDs as an identifiers field - #7
Open
gaurav wants to merge 6 commits into
Open
Conversation
`_extract_article` collects `Article.xrefs` with `.//ArticleIdList/ArticleId` under `PubmedData`. `.//` matches at any depth, and `PubmedData/ReferenceList/Reference` carries an `ArticleIdList` of its own, so every cited reference's DOI and PMCID was stored as an identifier of the citing article. One real record (PMID:41136637) contributed 426 foreign DOIs alongside its own. parse.py already exists to correct and extend that parser, so re-extract xrefs from the direct child `PubmedData/ArticleIdList/ArticleId` — the path Babel reads — and overwrite `article.xrefs` before rows are built. The article's own PMID stays excluded, as upstream had it. This has polluted `article_id` since the first load, so an existing database needs `load --force` to clean it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The JSON export emitted only `id: "PMID:<n>"`, so nothing downstream could resolve a publication by DOI or PMCID. Babel derives those mappings by re-parsing the PubMed XML itself; since pubmed2db is meant to replace that download, the export has to carry them. The data was already in the `article_id` table, so this derives the field at export with a CTE joined on (pmid, source_file) — the same key the abstract CTE uses, which is what keeps a superseded version's DOI out of the export. No schema change and no reload to backfill. Prefixes match Babel's src/prefixes.py exactly: `PMID`, lowercase `doi`, and `PMC` (PubMed's PMCID values already start with `PMC`, hence `PMC:PMC6423490`). `DOI:` or a de-doubled PMCID would silently fail to join against the Babel compendium. Values keep PubMed's own case, so consumers must match case-insensitively. Only DOIs and PMCIDs are promoted; `pii`/`mid` stay in `article_id` and the Parquet export. validate gains the matching cross-check: `efetch_documents` rebuilds the CURIEs from the same ArticleIdList using the exporter's own prefix map, and `check_fields` compares them as a set, since it is the one list-valued field and the string path cannot handle it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adds the validate-side tests the previous commits lacked: that an identifier disagreeing with Entrez is actually flagged, and that `_identifiers` reads the article's own ArticleIdList rather than its cited references' — the mirror of the parse-side regression test. Both fail if the behaviour is reverted. `test_field_validation_matches` already guarded the set comparison (the export sorts, efetch returns document order), so note that rather than adding a redundant test. Auditing the other upstream XPaths turned up a second instance of the same wrong-scope bug: `cites_pubmed_ids` searches MedlineCitation for `.//ReferenceList/Reference`, but ReferenceList is a child of PubmedData, a sibling — so `reference_citation` is silently always empty (0 rows from 14,201 real articles whose records carry hundreds of references). Documented in CLAUDE.md and FUTURE.md; not fixed here, since it is a separate table from this branch's subject. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Both branches independently found and fixed the same bug: cited references' DOIs being attributed to the citing article. initial-implementation's parse._article_ids and this branch's parse._xrefs anchor to the same PubmedData/ArticleIdList/ArticleId path and drop the same `pubmed` self-ID. Keeps _article_ids and drops _xrefs, since initial-implementation also fixes the sibling bug this branch documented but deferred -- cites_pubmed_ids never matching, leaving reference_citation empty. Carries over what _xrefs had and _article_ids lacked: the "path Babel reads" note and the PMID:41136637 evidence. This branch's contribution is unchanged and is now purely export-side: the identifiers CURIE array, the validate cross-check, and the 0002 fixture covering a version-specific PMCID and pii exclusion. test_xrefs_exclude_cited_references is rewritten against article_ids and renamed to match. Also applies the latest-snapshot restriction to the new `ids` CTE. It had the same shape as `abs` before 04a4ffc: aggregating the whole version history and discarding the superseded rows in the join. CLAUDE.md's three overlapping upstream sections are consolidated into one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Takes the removal of the citation-graph table. This branch's FUTURE.md entry asked whether the table was wanted at full scale; that is now decided, so the open question is replaced by the decision and the re-enabling instructions parked in parse._cited_pmids. Nothing on the export side changes: the identifiers field reads article_id, not reference_citation. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
3 tasks
Now that PR #2 has merged PR #1, this branch picks both up and its diff collapses back to the export-side work it is actually about. Conflict resolutions, all additive rather than either/or: - export.py: _LATEST_METADATA_SQL selects both `la.medline_date` (for the pub_year backfill) and `ids.identifiers`, and _document unpacks all twelve. Verified the two coexist: PMID 1003 exports pub_year "1998" recovered from "1998 Spring" with identifiers ["PMID:1003"], while PMID 1001 keeps its DOI and PMCID alongside a real pub_year. - validate.py: keeps ID_PREFIXES and adds _year_from_medline_date, so both of the exporter's normalizations are applied to the efetch side. This branch had independently bumped EXPECTED_FIELDS' placeholder row to a hardcoded 10; the derived version supersedes it, which is the point -- `identifiers` and `medline_date` each widened that row once already. - FUTURE.md/CLAUDE.md: both sides' notes kept. The ELocationID follow-up and the identifiers design note are unique to this branch; the completed MedlineDate entry and the efetch-is-a-rendering warning come from the other. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds DOIs and PMCIDs to the JSON export as an
identifiersarray of CURIEs, andfixes an upstream parser bug that surfaced while doing it. Closes #4
{ "id": "PMID:16954148", "identifiers": ["PMID:16954148", "PMC:PMC1904490", "doi:10.1242/jcs.03153"], "journal_name": "Journal of cell science", "...": "..." }What changed
identifiersis derived at export, not stored. The DOIs and PMCIDs werealready being loaded into the
article_idtable, so this adds a CTE joined on(pmid, source_file)— the same key the abstract CTE uses, which is what keeps asuperseded version's DOI out of the export. No schema change and no reload needed
to backfill.
Prefixes match Babel's
src/prefixes.pyexactly:PMID, lowercasedoi, andPMC. PubMed's PMCID values already begin withPMC, hence the doubledPMC:PMC6423490.DOI:or a de-doubledPMC:6423490would silently fail to joinagainst the Babel publication compendium. Values keep PubMed's own case, so
consumers must match case-insensitively — DOIs are case-insensitive per spec
and PubMed is not internally consistent.
Only DOIs and PMCIDs are promoted. Other
ArticleIdtypes (pii,mid, …) stayin the
article_idtable and the Parquet export.validategained the matching cross-check.efetch_documentsrebuilds theCURIEs from the same
ArticleIdListusing the exporter's own prefix map, andcheck_fieldscompares them as a set — it is the one list-valued field, so thestring path cannot handle it.
Upstream bug fixed along the way
pubmed_downloader's_extract_articlebuildsArticle.xrefsfrom.//ArticleIdList/ArticleIdunderPubmedData..//matches at any depth andPubmedData/ReferenceList/Referencecarries anArticleIdListof its own, soevery cited reference's DOI and PMCID was being stored as an identifier of the
citing article. One real record (PMID:41136637) contributed 426 foreign DOIs
alongside its own.
parse._xrefsnow re-extracts from the direct child — the pathBabel reads.
This has silently corrupted
article_idsince the first load. The JSON exportdodged it only because it never read that table; the Parquet export did not.
Important
Existing databases need
pubmed2db load --forceover the corpus to cleanarticle_id. Nothing detects this automatically, because the source filesthemselves have not changed.
Verification
61 tests pass, offline. Each new test was mutation-checked — reverting the
behaviour it covers makes it fail.
Verified end-to-end against a real update file (14,199 records): 96.7% carry a
DOI, 57% a PMCID, and the maximum identifiers on any one record is 3 (it was 427
before the parser fix).
validateagainst live Entrez reports only pre-existingahead-of-print staleness — two sampled records have since been assigned both a
journal issue and a PMCID upstream, which shows up in
volume/issue/pub_monthas well as in
identifiers.Follow-ups
Noted while doing this work; none are started.
reference_citationis always empty. Same wrong-scope bug class as theone fixed here:
cites_pubmed_idssearchesMedlineCitationfor.//ReferenceList/Reference, butReferenceListis a child ofPubmedData, asibling — so it never matches. 0 rows loaded from 14,201 real articles whose
records carry hundreds of references each. The path fix is one line; the real
question is whether the citation graph is wanted at full scale (~444 rows for a
single article). Documented in
FUTURE.md.validate. A DOI rate that collapsedbetween two exports would pass silently in the offline structure check. Roughly
three lines (
pct_with_doi/pct_with_pmc); small enough to fold in here.xrefsbug upstream tocthoyt/pubmed-downloader. The fixthere is anchoring the XPath to the direct child. Tracked in
FUTURE.md.load --forceso itsarticle_idtable drops the cited-reference pollution, then re-run the export.
ELocationIDDOIs are still unread. A DOI can also appear asArticle/ELocationID[@EIdType="doi"], normally duplicatingArticleIdList.Babel ignores it too and
ArticleIdListis authoritative, so this is afallback worth adding only if records turn up with one and not the other.
identifiersfield with Node Annotator. TheDocumentMetadataAPI spec documents nine string fields and carries the
identifier only as the
resultsmap key — it has noidfield and noidentifier field.
idwas already an extension;identifiersis a second one,and the first non-string value in the record.
🤖 Generated with Claude Code