Conversation
cthoyt's answer on pubmed-downloader#16 was that J_Entrez.txt is missing lots of information and the package reads the serfiles instead. Measuring that: he is right about the file, but the information it lacks is not the information we export, and the two fields we do export exist in PubMed's own form only in J_Entrez.txt. The decisive number is that J_Entrez's JournalTitle *is* esummary.fulljournalname, byte for byte -- 27/27 on a sample drawn from the journals where the two sources disagree, against 0/27 for the catalog. Rebuilding the Entrez rendering from the catalog tops out at 99.22%: the rest needs NCBI's transliteration table, 250-character truncation and its punctuation rules. Written to be shareable outside the repo, since it is also the answer to the upstream PR. Sample sizes and extrapolations are flagged as such, and the CatfilePlus evaluation is included so nobody has to redo it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
journal.start_year / end_year / active have been NULL columns since the
schema was written -- J_Entrez.txt does not carry them. The serial catalog
does, so load_journals now reads both: J_Entrez defines the row set and the
titles, the catalog fills the three year columns. A real load takes 40,384
of 42,056 journals (96.0%) from NULL to a publication year.
We enumerate NLM's listing ourselves rather than calling upstream's
ensure_serfile_catalog(), which passes skip_prefix="serfilebase" and so
only ever fetches the ~1 MB monthly deltas -- records changed since Dec
2019, not the catalog. The ~150k records live in the baseline it excludes.
Fetching the deltas as well as the baseline is not incidental: they supply
429 of the 902 journals the baseline lacks.
Three things that are easy to get wrong, each pinned by a test:
- ~1,600 catalog years are MARC wildcards (19uu, 199u, uuuu). int()
raises on those, so only \d{4} counts as a year.
- 9999 is the "still publishing" sentinel, not a year, and a *missing*
end year means unknown rather than ceased -- so it leaves active NULL.
- Later files win, so the baseline must be read before the deltas.
The years are an enrichment; the export reads title and abbreviation. A
~450 MB download failing must not cost us the dimension, so the fetch
degrades to NULL years with a warning. conftest defaults every test to
"no catalog files" so the suite stays offline.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
AGENTS.md gets the decision (why not one source) and the two traps that bite; the evidence stays in docs/journal-catalog.md, the same split as slurm/README.md vs the #SBATCH headers. FUTURE.md's "revert the custom journal parser once upstream fixes process_journal_overview" entry is gone: upstream's answer to pubmed-downloader#16 was that the overview file is the wrong source for what its Journal model holds, so the PR is moot and the parser stays. Replaced with the one thing we measured and deliberately deferred -- exposing serfile's 4,763 ValidYN="N" ISSNs as resolver aliases, which needs a `valid` column on journal_issn first. Its *valid* ISSNs are 99.3% identical to J_Entrez's, so there is nothing else to gain there. Also notes for #31 that journal.title is byte-identical to what esummary and efetch serve, which makes a hard journal check more viable than "different source" suggests. The `journals` step now pulls ~890 MB on a first run (NLM posts occasional bulk re-releases as monthly deltas -- 2026's May and June files are 169 MB and 249 MB). Streaming them costs ~5 s at ~50 MiB RSS, so 02-journals.sbatch keeps its 8G header; only the outbound-host and scratch notes change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The 902 figure is the baseline's gap; the monthly deltas close 429 of it, so the article-volume estimate and the "already covered" line both needed scaling to the 473 that actually end up with NULL years. Also corrects "a ninth of the download" -- that compared baselines, but a full year of serfile is ~890 MB against CatfilePlus's 5.08 GB. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
An earlier commit here removed FUTURE.md's entry on the grounds that upstream's "the package reads the serfiles instead" made the PR irrelevant. That reads the measurement backwards. If J_Entrez.txt were a thin subset of the catalog, process_journal_overview() would be a vestige and the PR would be polishing it. But it is not a subset: fulljournalname and IsoAbbr exist nowhere in CatalogRecord, and no reconstruction from the catalog reaches them. So that function is the only library path to two fields the catalog cannot supply, its input has no publication years and never will, and therefore start_year/end_year must be optional -- the same load-then-enrich shape this pipeline now uses. The model itself says as much: every other field the overview file cannot supply already defaults (abbreviation_medline, abbreviation_iso, synonyms, active). start_year/end_year are the only Optional-annotated fields without a default, which is an oversight pattern, not a design statement. Recorded with the caveat for adopting it: Journal.active defaults to True, wrong for the 13,012 journals serfile marks ceased. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
pystow's ensure() skips by file name, so a republished serfile would have kept its stale bytes indefinitely -- the same hazard AGENTS.md already records for upstream's ensure(). Re-fetching unconditionally is not an option at ~890 MB a run. NLM publishes no .md5 sidecars for these files (checked: 302, and none in the directory listing), so download.py's checksum machinery does not transfer. The server does return a stable ETag, which does the same job for a few hundred bytes: each cached file now carries an .etag sidecar, and one HEAD per file per run decides whether to force a refetch. A failed HEAD returns None and leaves the cached copy alone, so a network blip costs nothing and a first download is not treated as a refetch. Verified against live data: a warm run re-downloads none of the 10 catalog files and still loads 42,056 journals. conftest now stubs _serfile_urls rather than _ensure_serfile, so the suite stays offline while _ensure_serfile itself remains testable. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
NLM publishes zero-byte files: serfile.20240903.xml is served with Content-Length: 0 (Last-Modified Sep 2024), and lxml raises XMLSyntaxError on an empty document even with recover=True. Hit while measuring the catalog, not hypothesised. _parse_serfile had no per-file guard, so that exception would have propagated to _journal_years, whose blanket `except Exception` is there to survive a failed *download*. One bad file in the current year would therefore have cost every journal its publication years -- silently, since the fallback is exactly the NULL columns this feature exists to fill. Now a file that will not parse logs a warning and the rest still contribute, which is what upstream's _parse_catalog does. Pinned by a test with a genuinely zero-byte fixture. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
I wrote into four files that upstream's ensure_serfile_catalog(), by
skipping serfilebase*, "only ever fetches the monthly deltas -- records
changed since Dec 2019, not the catalog". Measured, the consequence does
not follow, so it comes out.
Downloading all 83 NLMXML update files (2.52 GiB) and diffing against
serfilebase.2026.xml:
83 deltas, no baseline 151,974 distinct ids (misses 10)
baseline + 2026 deltas (ours) 151,615 (misses 369)
2026 baseline alone 150,942 (misses 1,042)
Only 10 of 150,942 baseline records never appear in a delta, because NLM
re-releases the whole catalog through the monthly files over time -- 2026's
May and June deltas are 169 MB and 249 MB against the usual ~1 MB. Upstream's
coverage is effectively complete, and marginally better than ours.
What survives is a cost argument, not a correctness one: 2.52 GiB across 83
files against 872 MiB across 10, and 761,624 records yielded for 151,974
distinct journals -- 80% superseded duplicates, one record 24 times. That
still justifies anchoring on the baseline, so no behaviour changes here.
Also recorded, because it is why the wrong claim survived review: the
_parse_catalog `and False` cache bug is real in the installed 0.0.14 and
already fixed on main, so it must not be reported -- check the installed
version against main before writing any of this up. The Journal year fields
go the other way, still int | None with no default on main, so
pubmed-downloader#16 remains live.
Our own accepted limit is written down too: anchoring on the newest baseline
drops 369 records NLM has since removed, 11 of them J_Entrez journals
(0.03%), which lose their publication years.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This was referenced Sep 4, 2026
Open
Tracked as #50, which has room for the open questions the FUTURE.md entry did not: whether anything consumes journal_issn at all today, whether Babel's resolver would use cancelled identifiers, and whether the new column should be a boolean or a status string. 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.
journal.start_year/end_year/activehave been NULL columns since the schema was written, becauseJ_Entrez.txtdoes not carry them. NLM's serial catalog (serfile) does, soload_journalsnow reads both sources: J_Entrez defines the row set and the titles, the catalog fills the three year columns. A real load takes 40,384 of 42,056 journals (96.0%) from NULL to a publication year.No issues are closed by this. It grew out of pubmed-downloader#16, where upstream's position is that
J_Entrez.txtis the wrong source and the package reads the serfiles instead — so the first job was working out whether to switch sources or add one.Why two sources rather than one
Switching wholesale would have been a regression, and the measurement is the reason to keep both.
J_Entrez.JournalTitleis byte-for-byte identical toesummary.fulljournalname. On the 8,259 journals (20% of the 41k overlap) where the two sources disagree, sampling 50 at random and fetching one article each:esummaryfulljournalnameefetch<Journal><Title>The disagreements are systematic, not noise — 3,747 diacritics (NCBI ASCII-folds), 1,554 corporate suffixes (
Academy of management review. Academy of Management), ~2,958 qualifier disambiguators (Ethos (Berkeley, Calif.)). Rebuilding the Entrez rendering from the catalog tops out at 99.22%; the residual needs NCBI's transliteration table, ~250-character truncation and its punctuation rules.docs/journal-catalog.mdis the full write-up, written to be shareable outside this repo since it is also the answer to the upstream PR. It covers the CatfilePlus evaluation too (5.08 GB, MARCXML-only since 2024, same catalog titles, ~1% marginal coverage) so nobody redoes it.What it produces
That last row is the design in one assertion: J_Entrez's title with the Entrez disambiguator intact, next to the catalog's years.
Notes for review
ensure_serfile_catalog(), which passesskip_prefix="serfilebase"and takes all 83 monthly deltas instead. That is a cost argument, not a coverage one — measured, its 83 files carry 151,974 distinct records against the baseline's 150,942, because NLM re-releases the whole catalog through the deltas over time. Anchoring on the baseline is 872 MiB across 10 files rather than 2.52 GiB across 83, and avoids the 80% duplicate records (761,624 yielded for 151,974 distinct journals, one appearing 24×) that a caller would otherwise have to dedupe. Fetching the deltas as well as the baseline is not incidental either: they supply 429 of the 902 journals the baseline lacks.19uu,uuuu) thatint()raises on;9999is the "still publishing" sentinel and a missing end year means unknown rather than ceased (soactivestays NULL, not false); later files must win over the baseline.serfile.20240903.xmlis served withContent-Length: 0, anditerparseraises on it even withrecover=True— so_parse_serfileskips a file it cannot parse rather than letting one bad file cost every journal its years. And anchoring on the newest baseline drops 369 records NLM has since removed, of which 11 are J_Entrez journals (0.03%) that lose their years; accepted rather than spending 2.52 GiB and a dedupe pass on eleven journals.PublicationFirstYearelement, but only 96.0% yield a parseable year. The gap is the wildcards.journal_name/journal_abbrevstill come from J_Entrez, which is the point of not switching.cli.updatealready takes toward the journal step.conftestdefaults every test to "no catalog files" so the suite stays offline.journalsnow pulls ~890 MB on a first run. NLM posts occasional bulk re-releases as monthly deltas — 2026's May and June files are 169 MB and 249 MB against the usual ~1 MB. Streaming them costs ~5 s at ~50 MiB RSS, so02-journals.sbatchkeeps its 8G header; only its host and scratch notes changed.ensure()skips by file name, which would have let a republished file keep stale bytes indefinitely — the hazardAGENTS.mdalready records for upstream'sensure()— and re-fetching ~890 MB every run is not an option. NLM publishes no.md5sidecars for serfile (checked: 302, and none in the listing), sodownload.py's checksum machinery does not transfer; the server's stableETagdoes the same job for a few hundred bytes. Each cached file carries an.etagsidecar and one HEAD per run decides. A failed HEAD leaves the cached copy alone. Verified live: a warm run re-downloads none of the 10 files.What it deliberately does not do
journal_issn. Filtered toValidYN="Y", serfile's ISSNs are 99.3% identical to J_Entrez's (+143 / −156) — an earlier count of "+4,300" was an artifact of includingValidYN="N"records. The genuinely new content is the 4,763 cancelled ISSNs, useful as resolver aliases but needing avalidcolumn first so a consumer cannot mistake them for current.journal.abbreviation_iso.IsoAbbrequalsMedAbbrin 42,056 / 42,056 records, so theCOALESCEinexport.pyis a distinction without a difference — but deleting a column is a Parquet schema change for downstream consumers, so it is its own decision. Filed as Consider dropping journal.abbreviation_iso, which duplicates abbreviation_medline #48._parse_journal_overview. It stays; we depend on nothing upstream. This work is nonetheless the argument for pubmed-downloader#16 rather than against it:process_journal_overview()cannot return a single record (Journal.start_year/end_yearare annotatedint | Nonewith no default, so pydantic makes them required, andJ_Entrez.txthas no such key), and sincefulljournalname/IsoAbbrexist nowhere inCatalogRecord, that function is the only library path to them and its input can never supply years. If the PR lands we delete ~35 lines; if it does not, nothing here changes.FUTURE.mdrecords both, plus the caveat thatJournal.activedefaults toTrue— wrong for the 13,012 journals serfile marks ceased..//ReferenceListscope bug pair in upstream's_extract_article.Before merging
Nothing is blocking. 247 tests pass, and the pipeline was run end to end against live NLM data (numbers above), including a warm re-run to confirm the ETag check prevents a refetch. Deferred work is recorded in
FUTURE.mdand listed under "deliberately does not do" above; #48 is filed for theabbreviation_isoquestion.How the conclusion changed during the work — kept for anyone tracing why the docs argue what they argue; the durable versions are in
docs/journal-catalog.mdandAGENTS.md.The branch is named
switch-catalog-to-serfilebecause a wholesale switch is what was originally asked for. Theesummarycomparison is what redirected it to a two-source design; the branch name was left alone rather than rewritten.Two numbers moved after the first full run against live data, and both are corrected above and in the doc:
An intermediate commit removed
FUTURE.md's pubmed-downloader#16 entry on the grounds that upstream's "the package reads the serfiles instead" made the PR moot. That read the measurement backwards and was reverted in0a79557: the finding thatJ_Entrez.txtis not a subset of the catalog is precisely what justifies makingJournal's year fields optional.An earlier draft of the analysis also claimed serfile added ~4,300 ISSNs. That was an artifact of not filtering on
ValidYN, and the doc now says so explicitly so the mistake is not repeated.