Mining PubMed & PMC literature (NCBI E-utilities)
Search PubMed (citations/abstracts) and PMC (full text) programmatically
with NCBI E-utilities — the stable HTTP interface to Entrez. The core pattern
is two steps: ESearch returns matching record IDs (PMIDs), then EFetch (or
ESummary) downloads the records. The Entrez History server (usehistory=y)
lets you chain the two without re-sending thousands of IDs.
E-utilities are public. No key is required, but a free API key raises your
limit from 3 to 10 requests/second and is strongly recommended for batch work.
When to use
- OpenMed extracted a diagnosis, drug, or gene and you want supporting literature.
- You need abstracts to summarize or to assemble a corpus for biomedical NER.
- You want MeSH-anchored, reproducible searches (date ranges, article types).
For ClinicalTrials.gov use searching-clinicaltrials; this skill is for the
published literature.
Quick start (real E-utilities calls)
Base URL: https://eutils.ncbi.nlm.nih.gov/entrez/eutils/. JSON for ESearch/
ESummary via retmode=json; EFetch returns text or XML (no JSON for PubMed).
import requests, time
BASE = "https://eutils.ncbi.nlm.nih.gov/entrez/eutils"
API_KEY = None # set to your free NCBI key to get 10 req/s instead of 3
def _params(**kw):
if API_KEY:
kw["api_key"] = API_KEY
return kw
def esearch(term: str, retmax: int = 50) -> dict:
"""Find PMIDs; usehistory=y stores them on the Entrez History server."""
r = requests.get(f"{BASE}/esearch.fcgi", params=_params(
db="pubmed", term=term, retmax=retmax,
usehistory="y", retmode="json"), timeout=30)
r.raise_for_status()
res = r.json()["esearchresult"]
return {"count": int(res["count"]), "ids": res["idlist"],
"webenv": res["webenv"], "query_key": res["querykey"]}
def efetch_abstracts(webenv: str, query_key: str, retmax: int = 50) -> str:
"""Pull abstracts by reference to the stored result set (no ID list needed)."""
r = requests.get(f"{BASE}/efetch.fcgi", params=_params(
db="pubmed", WebEnv=webenv, query_key=query_key,
retmax=retmax, rettype="abstract", retmode="text"), timeout=60)
r.raise_for_status()
return r.text
hits = esearch('("type 2 diabetes"[MeSH]) AND metformin AND 2023:2025[pdat]')
print(hits["count"], "papers")
abstracts = efetch_abstracts(hits["webenv"], hits["query_key"])
Equivalent cURL (search then fetch one PMID's abstract):
curl "https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esearch.fcgi?db=pubmed&term=metformin&retmode=json"
curl "https://eutils.ncbi.nlm.nih.gov/entrez/eutils/efetch.fcgi?db=pubmed&id=38000000&rettype=abstract&retmode=text"
ESummary for structured metadata
When you need titles/authors/journal/date as JSON (not the full abstract), use
ESummary — it returns one record per ID:
def esummary(ids: list[str]) -> dict:
r = requests.get(f"{BASE}/esummary.fcgi", params=_params(
db="pubmed", id=",".join(ids), retmode="json"), timeout=30)
r.raise_for_status()
return r.json()["result"] # keyed by PMID: title, pubdate, source, authors…
For PMC full text, repeat with db=pmc and EFetch rettype=""/retmode=xml
(JATS XML). Respect each article's license before redistributing full text.
Workflow
- Build the query. Combine OpenMed-extracted terms with MeSH tags and field
filters:
"<disease>"[MeSH] AND <drug>[tiab] AND 2020:2025[pdat]. Use
[tiab] (title/abstract), [au] (author), [pdat] (publication date).
- ESearch with
usehistory=y to capture WebEnv + query_key and the count.
- Batch-fetch with EFetch/ESummary in pages of ≤ ~200 IDs (or by history),
sleeping to stay under your rate limit.
- Parse abstracts/metadata; store PMID, title, journal, date, abstract text.
- NER the abstracts with
openmed.analyze_text to extract diseases, drugs,
genes, and oncology entities for downstream synthesis.
Hand-off to / from OpenMed
- OpenMed facts → query.
openmed.analyze_text(note) yields Disease,
Pharmaceutical, Genomics, and Oncology entities. Turn the top spans into the
ESearch term (optionally grounded: ICD-10 label, RxNorm ingredient, gene
symbol) to retrieve targeted evidence.
- Abstracts → OpenMed. Feed fetched abstracts straight into
openmed.analyze_text(abstract, model_name="disease_detection_superclinical")
(or a Genomics/Oncology model) to structure the literature into entities for
evidence tables or knowledge-graph edges.
- Queries and abstracts are public literature, not PHI. Still run locally and
never embed patient text in a search term.
Edge cases & gotchas
- Rate limits. 3 req/s without a key, 10 with one — exceed it and NCBI returns
HTTP 429. Add
api_key, throttle, and retry with backoff. NCBI also requests a
tool= and email= parameter identifying your application.
- EFetch has no JSON for PubMed. Use
retmode=text (human-readable) or
retmode=xml (PubMedArticle XML) and parse XML for structured fields.
- History expires.
WebEnv/query_key are session-scoped — fetch promptly
after searching, or re-run ESearch.
- Large result sets. Page with
retstart/retmax (or history) rather than
pulling everything at once; cap total fetches.
- MeSH lag. Very recent articles may not yet be MeSH-indexed — include
[tiab]
term variants so you do not miss them.
- Full-text licensing. PMC full text carries per-article licenses; many are
not redistributable. Store PMIDs/abstracts freely; check the license before
republishing full text.
Standards & references
1---2name: mining-pubmed-literature3description: Searches and fetches PubMed and PMC via NCBI E-utilities (ESearch then EFetch/ESummary) to gather biomedical evidence and build text corpora. Use when the user wants citations for a condition or drug, abstracts to summarize, MeSH-based searches, or a corpus of literature to run NER over. Trigger keywords: PubMed, PMC, NCBI, E-utilities, ESearch, EFetch, ESummary, MeSH, PMID, literature search, abstracts, evidence. Pairs adjacent to OpenMed: fetched abstracts feed openmed.analyze_text for biomedical NER, and OpenMed-extracted diagnoses/drugs/genes become the search terms. E-utilities are public; an optional free API key raises rate limits from 3 to 10 requests/second.4license: Apache-2.05---67# Mining PubMed & PMC literature (NCBI E-utilities)89Search **PubMed** (citations/abstracts) and **PMC** (full text) programmatically10with **NCBI E-utilities** — the stable HTTP interface to Entrez. The core pattern11is two steps: **ESearch** returns matching record IDs (PMIDs), then **EFetch** (or12**ESummary**) downloads the records. The **Entrez History server** (`usehistory=y`)13lets you chain the two without re-sending thousands of IDs.1415E-utilities are public. **No key is required**, but a free API key raises your16limit from **3 to 10 requests/second** and is strongly recommended for batch work.1718## When to use1920- OpenMed extracted a diagnosis, drug, or gene and you want supporting literature.21- You need abstracts to summarize or to assemble a corpus for biomedical NER.22- You want MeSH-anchored, reproducible searches (date ranges, article types).2324For ClinicalTrials.gov use `searching-clinicaltrials`; this skill is for the25published literature.2627## Quick start (real E-utilities calls)2829Base URL: `https://eutils.ncbi.nlm.nih.gov/entrez/eutils/`. JSON for ESearch/30ESummary via `retmode=json`; EFetch returns text or XML (no JSON for PubMed).3132```python33import requests, time3435BASE = "https://eutils.ncbi.nlm.nih.gov/entrez/eutils"36API_KEY = None # set to your free NCBI key to get 10 req/s instead of 33738def _params(**kw):39 if API_KEY:40 kw["api_key"] = API_KEY41 return kw4243def esearch(term: str, retmax: int = 50) -> dict:44 """Find PMIDs; usehistory=y stores them on the Entrez History server."""45 r = requests.get(f"{BASE}/esearch.fcgi", params=_params(46 db="pubmed", term=term, retmax=retmax,47 usehistory="y", retmode="json"), timeout=30)48 r.raise_for_status()49 res = r.json()["esearchresult"]50 return {"count": int(res["count"]), "ids": res["idlist"],51 "webenv": res["webenv"], "query_key": res["querykey"]}5253def efetch_abstracts(webenv: str, query_key: str, retmax: int = 50) -> str:54 """Pull abstracts by reference to the stored result set (no ID list needed)."""55 r = requests.get(f"{BASE}/efetch.fcgi", params=_params(56 db="pubmed", WebEnv=webenv, query_key=query_key,57 retmax=retmax, rettype="abstract", retmode="text"), timeout=60)58 r.raise_for_status()59 return r.text6061hits = esearch('("type 2 diabetes"[MeSH]) AND metformin AND 2023:2025[pdat]')62print(hits["count"], "papers")63abstracts = efetch_abstracts(hits["webenv"], hits["query_key"])64```6566Equivalent cURL (search then fetch one PMID's abstract):6768```bash69curl "https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esearch.fcgi?db=pubmed&term=metformin&retmode=json"70curl "https://eutils.ncbi.nlm.nih.gov/entrez/eutils/efetch.fcgi?db=pubmed&id=38000000&rettype=abstract&retmode=text"71```7273## ESummary for structured metadata7475When you need titles/authors/journal/date as JSON (not the full abstract), use76ESummary — it returns one record per ID:7778```python79def esummary(ids: list[str]) -> dict:80 r = requests.get(f"{BASE}/esummary.fcgi", params=_params(81 db="pubmed", id=",".join(ids), retmode="json"), timeout=30)82 r.raise_for_status()83 return r.json()["result"] # keyed by PMID: title, pubdate, source, authors…84```8586For PMC full text, repeat with `db=pmc` and EFetch `rettype=""`/`retmode=xml`87(JATS XML). Respect each article's license before redistributing full text.8889## Workflow90911. **Build the query.** Combine OpenMed-extracted terms with MeSH tags and field92 filters: `"<disease>"[MeSH] AND <drug>[tiab] AND 2020:2025[pdat]`. Use93 `[tiab]` (title/abstract), `[au]` (author), `[pdat]` (publication date).942. **ESearch with `usehistory=y`** to capture `WebEnv` + `query_key` and the count.953. **Batch-fetch** with EFetch/ESummary in pages of ≤ ~200 IDs (or by history),96 sleeping to stay under your rate limit.974. **Parse** abstracts/metadata; store PMID, title, journal, date, abstract text.985. **NER the abstracts** with `openmed.analyze_text` to extract diseases, drugs,99 genes, and oncology entities for downstream synthesis.100101## Hand-off to / from OpenMed102103- **OpenMed facts → query.** `openmed.analyze_text(note)` yields Disease,104 Pharmaceutical, Genomics, and Oncology entities. Turn the top spans into the105 ESearch `term` (optionally grounded: ICD-10 label, RxNorm ingredient, gene106 symbol) to retrieve targeted evidence.107- **Abstracts → OpenMed.** Feed fetched abstracts straight into108 `openmed.analyze_text(abstract, model_name="disease_detection_superclinical")`109 (or a Genomics/Oncology model) to structure the literature into entities for110 evidence tables or knowledge-graph edges.111- Queries and abstracts are **public** literature, not PHI. Still run locally and112 never embed patient text in a search term.113114## Edge cases & gotchas115116- **Rate limits.** 3 req/s without a key, 10 with one — exceed it and NCBI returns117 HTTP 429. Add `api_key`, throttle, and retry with backoff. NCBI also requests a118 `tool=` and `email=` parameter identifying your application.119- **EFetch has no JSON for PubMed.** Use `retmode=text` (human-readable) or120 `retmode=xml` (PubMedArticle XML) and parse XML for structured fields.121- **History expires.** `WebEnv`/`query_key` are session-scoped — fetch promptly122 after searching, or re-run ESearch.123- **Large result sets.** Page with `retstart`/`retmax` (or history) rather than124 pulling everything at once; cap total fetches.125- **MeSH lag.** Very recent articles may not yet be MeSH-indexed — include `[tiab]`126 term variants so you do not miss them.127- **Full-text licensing.** PMC full text carries per-article licenses; many are128 not redistributable. Store PMIDs/abstracts freely; check the license before129 republishing full text.130131## Standards & references132133- E-utilities In-Depth (parameters & syntax) — https://www.ncbi.nlm.nih.gov/books/NBK25499/134- E-utilities Quick Start — https://www.ncbi.nlm.nih.gov/books/NBK25497/135- General introduction & policies — https://www.ncbi.nlm.nih.gov/books/NBK25501/136- API keys & rate limits — https://support.nlm.nih.gov/kbArticle/?pn=KA-05317137- PubMed search field tags — https://pubmed.ncbi.nlm.nih.gov/help/138- MeSH browser — https://meshb.nlm.nih.gov/