| Title: | Retrieve and Audit Scholarly Metadata from Multiple Sources |
|---|---|
| Description: | Retrieves and normalizes scholarly article metadata and abstracts from the 'Crossref' REST API <https://www.crossref.org/documentation/retrieve-metadata/rest-api/>, the 'OpenAlex' API <https://help.openalex.org/> and the 'Scopus' Search API <https://dev.elsevier.com/sc_apis.html>. Provides bibliographic searches, cursor pagination, publication date and author filtering, abstract retrieval by Digital Object Identifier (DOI), cached batch enrichment, merging of duplicate records, metadata-quality audits, reproducible search snapshots, local archives with integrity verification, and export to comma-separated values (CSV), 'BibTeX' and 'RIS' citation files. Network requests support configurable timeouts and test callbacks. 'Scopus' searches require an 'Elsevier' API key; 'Crossref' queries do not. 'OpenAlex' supports limited keyless searches and authenticated pagination. |
| Authors: | Diogo Ribeiro [aut, cre, cph] (ORCID: <https://orcid.org/0009-0001-2022-7072>, affiliation: Faculty of Media Arts and Design of the Technical University of Porto) |
| Maintainer: | Diogo Ribeiro <[email protected]> |
| License: | MIT + file LICENSE |
| Version: | 0.3.0 |
| Built: | 2026-11-08 20:45:30 UTC |
| Source: | https://github.com/cran/abstractExtractR |
Appends a SearchSnapshot to a new consecutively numbered run directory.
The SearchPlan must match the archived plan, and completed timestamps must
be nondecreasing. RDS and JSON evidence are written to a temporary sibling
folder before the new run is published. Prior observations are unchanged.
All existing runs have their checksums and JSON search-plan metadata
inspected, but only the latest archived RDS is deserialized to validate
chronology and plan equality. Call read_search_archive() to validate
every historical RDS and generate the full SearchHistory. Cooperative
writer locking is held during the operation; do not force-unlock an
active process.
append_search_archive(snapshot, directory)append_search_archive(snapshot, directory)
snapshot |
New SearchSnapshot or trusted RDS file path. |
directory |
Existing archive directory. |
New run directory path invisibly.
plan <- new_search_plan("example", sources = "crossref") mock <- function(url, headers, params, timeout) { list(message = list(items = list())) } snapshot <- run_search_plan(plan, requester = mock) path <- tempfile("research-archive-") create_search_archive(snapshot, path) append_search_archive(snapshot, path) unlink(path, recursive = TRUE)plan <- new_search_plan("example", sources = "crossref") mock <- function(url, headers, params, timeout) { list(message = list(items = list())) } snapshot <- run_search_plan(plan, requester = mock) path <- tempfile("research-archive-") create_search_archive(snapshot, path) append_search_archive(snapshot, path) unlink(path, recursive = TRUE)
Create a validated S3 Article record. Optional metadata is stored as NULL when absent, while authors are stored in source order as a character vector.
Article( title = NULL, abstract = NULL, publication_date = NULL, authors = character(), doi = NULL )Article( title = NULL, abstract = NULL, publication_date = NULL, authors = character(), doi = NULL )
title, abstract, publication_date, doi
|
Optional scalar strings or NULL. |
authors |
Character vector of author names in source order. |
A validated list with class Article.
article <- Article( title = "Example paper", abstract = "Example abstract", authors = c("Ada Lovelace", "Grace Hopper"), doi = "10.1000/example" ) article$titlearticle <- Article( title = "Example paper", abstract = "Example abstract", authors = c("Ada Lovelace", "Grace Hopper"), doi = "10.1000/example" ) article$title
Uses canonical DOI identity and archive manifest indexes to find observed runs, loading only the matching RDS files. Tracks title, publication date, authors, abstract presence and SHA-256 fingerprint, source attribution, and metadata changes between successive observations. A missing DOI in an intervening run is described as an observation gap, not a retraction.
article_metadata_timeline( directory, doi, include_abstract = FALSE, on_unindexed = c("read", "error"), verify_integrity = TRUE )article_metadata_timeline( directory, doi, include_abstract = FALSE, on_unindexed = c("read", "error"), verify_integrity = TRUE )
directory |
Existing research archive directory. |
doi |
A single valid DOI or doi.org resolver URL. |
include_abstract |
Logical; include full abstract text when TRUE. |
on_unindexed |
Either "read" (default) or "error" for older manifests. |
verify_integrity |
Verify archive checksums, TRUE by default. |
By default full abstract text is excluded. SHA-256 fingerprints indicate exact textual changes, not their significance or scientific validity. Provider failures and varying coverage limit interpretation.
List with doi, observations, changes, gaps, summary and integrity_checked. Timestamps are search-observation times.
plan <- new_search_plan("example", sources = "crossref") mock <- function(url, headers, params, timeout) { list(message = list(items = list( list(title = list("Study"), DOI = "10.1000/example") ))) } snap <- run_search_plan(plan, requester = mock) path <- tempfile("doi-history-") create_search_archive(snap, path) article_metadata_timeline(path, "10.1000/example")$summary unlink(path, recursive = TRUE)plan <- new_search_plan("example", sources = "crossref") mock <- function(url, headers, params, timeout) { list(message = list(items = list( list(title = list("Study"), DOI = "10.1000/example") ))) } snap <- run_search_plan(plan, requester = mock) path <- tempfile("doi-history-") create_search_archive(snap, path) article_metadata_timeline(path, "10.1000/example")$summary unlink(path, recursive = TRUE)
Returns UTF-8 BibTeX using one article entry per record. Titles, author names, DOIs and abstracts are escaped for LaTeX; missing fields are omitted. Citation keys derive from DOIs where possible and are made unique. Classical BibTeX may need LaTeX accent macros for non-ASCII characters; this function intentionally preserves the original Unicode characters.
articles_to_bibtex(articles, keys = NULL)articles_to_bibtex(articles, keys = NULL)
articles |
List of |
keys |
Optional unique vector of BibTeX citation identifiers. |
A length-one character string containing BibTeX text, or an empty string for zero articles.
articles_to_bibtex(list(Article( title = "An example", authors = "Ada Lovelace", doi = "10.1000/example" )))articles_to_bibtex(list(Article( title = "An example", authors = "Ada Lovelace", doi = "10.1000/example" )))
Converts normalized records from Scopus or Crossref into a rectangular representation. Missing scalar fields and missing authors become NA, and authors are joined in source order with a configurable separator. Embedded XML markup in abstracts is preserved.
articles_to_data_frame(articles, author_separator = "; ")articles_to_data_frame(articles, author_separator = "; ")
articles |
List of |
author_separator |
Scalar character separator for the authors column. |
A base R data.frame containing title, abstract, publication_date, authors, and doi, in that order. Empty input produces zero rows with those same character columns.
x <- list(Article(title = "Paper", authors = c("Ada", "Grace"), doi = "10.1000/example")) articles_to_data_frame(x)x <- list(Article(title = "Paper", authors = c("Ada", "Grace"), doi = "10.1000/example")) articles_to_data_frame(x)
Produces RIS records with TY/ER delimiters, one AU tag per author, and optional TI, PY, DA, DO, and AB tags. The format uses CRLF line endings, including after the final ER tag.
articles_to_ris(articles)articles_to_ris(articles)
articles |
List of |
A length-one character string containing RIS records, or an empty string for zero articles.
articles_to_ris(list(Article(title = "Paper", authors = "Ada")))articles_to_ris(list(Article(title = "Paper", authors = "Ada")))
Computes weighted completeness (fields present) and weighted validity (fields present and passing structural validation), plus a DOI duplicate indicator. This is a metadata audit, not a measure of scientific reliability, publication quality, or factual correctness. No HTTP requests are made.
assess_article_quality( articles, weights = c(title = 1, abstract = 1, publication_date = 1, authors = 1, doi = 1) )assess_article_quality( articles, weights = c(title = 1, abstract = 1, publication_date = 1, authors = 1, doi = 1) )
articles |
List of Article records, including an empty list. |
weights |
Named numeric vector with exactly the five field names; finite nonnegative values, at least one positive. |
The default weights are equal across title, abstract, publication_date, authors, and doi. Scores are normalized to the closed interval from 0 to 1. Blank values are missing; malformed DOI and dates are present but invalid. DOI duplication is reported separately and does not affect scores.
A list with records (one row per article, scores and flags), summary (per-field counts and rates), issues (field-level missing/invalid diagnostics), and weights (normalized scoring weights).
papers <- list(Article(title = "A paper", doi = "10.1000/example")) assess_article_quality(papers)$recordspapers <- list(Article(title = "A paper", doi = "10.1000/example")) assess_article_quality(papers)$records
Load a bounded tail of completed runs and compare consecutive observations. This does not load or checksum earlier runs and is not a full archive audit.
compare_recent_archive_runs(directory, last_n = 2L)compare_recent_archive_runs(directory, last_n = 2L)
directory |
Existing research archive. |
last_n |
Number of final runs to compare, at least two and no larger than the current archive length. |
A SearchHistory as produced by compare_search_history().
plan <- new_search_plan("topic", sources = "crossref") mock <- function(url, headers, params, timeout) { list(message = list(items = list())) } snap <- run_search_plan(plan, requester = mock) path <- tempfile("archive-") create_search_archive(snap, path) append_search_archive(snap, path) compare_recent_archive_runs(path, last_n = 2L)$transitions unlink(path, recursive = TRUE)plan <- new_search_plan("topic", sources = "crossref") mock <- function(url, headers, params, timeout) { list(message = list(items = list())) } snap <- run_search_plan(plan, requester = mock) path <- tempfile("archive-") create_search_archive(snap, path) append_search_archive(snap, path) compare_recent_archive_runs(path, last_n = 2L)$transitions unlink(path, recursive = TRUE)
Load and validate exactly the two requested observations, then perform
the existing DOI-based comparison. Other archive RDS objects are not
deserialized or checked. Use read_search_archive() for a full audit.
compare_search_archive_runs(directory, before, after = "latest")compare_search_archive_runs(directory, before, after = "latest")
directory |
Existing research archive path. |
before |
One-based index or run_NNNNNN name of the earlier run. |
after |
One-based index, run_NNNNNN name, or "latest". |
The result of compare_search_snapshots().
plan <- new_search_plan("topic", sources = "crossref") mock <- function(url, headers, params, timeout) { list(message = list(items = list())) } snap <- run_search_plan(plan, requester = mock) path <- tempfile("archive-") create_search_archive(snap, path) append_search_archive(snap, path) compare_search_archive_runs(path, 1L, "latest")$summary unlink(path, recursive = TRUE)plan <- new_search_plan("topic", sources = "crossref") mock <- function(url, headers, params, timeout) { list(message = list(items = list())) } snap <- run_search_plan(plan, requester = mock) path <- tempfile("archive-") create_search_archive(snap, path) append_search_archive(snap, path) compare_search_archive_runs(path, 1L, "latest")$summary unlink(path, recursive = TRUE)
Compare successive SearchSnapshot observations using canonical DOI
equality. Each transition reports newly observed and disappeared DOIs,
modified metadata fields, changed source provenance and records that cannot
be matched because they have no recognizable DOI.
compare_search_history(snapshots, labels = NULL)compare_search_history(snapshots, labels = NULL)
snapshots |
An ordered list of at least two SearchSnapshot objects or
trusted RDS paths from |
labels |
Optional unique nonempty names for the runs. Defaults to list names, or run_001, run_002, etc. when unnamed. |
Alongside the change events, each run records the provider retrieval counts and mean metadata completeness/structural-validity scores. These are properties of retrieved metadata, not measures of scientific quality.
Search settings and provider completeness are assessed for each transition. A disappeared DOI does not establish withdrawal, retraction or deletion. Settings and provider coverage may explain observed differences.
A SearchHistory containing schema_version, runs, providers, transitions, events, and unmatched tables.
plan <- new_search_plan("example", sources = "crossref") mock <- function(url, headers, params, timeout) { list(message = list(items = list())) } snapshot <- run_search_plan(plan, requester = mock) compare_search_history(list(snapshot, snapshot))$transitionsplan <- new_search_plan("example", sources = "crossref") mock <- function(url, headers, params, timeout) { list(message = list(items = list())) } snapshot <- run_search_plan(plan, requester = mock) compare_search_history(list(snapshot, snapshot))$transitions
Uses only normalized DOI identity to compare scholarly Article records. Missing-DOI records are listed separately as unmatched, not paired by title or author. Reports added and removed DOIs and field-level changes for title, abstract, publication_date, authors and DOI.
compare_search_snapshots(before, after)compare_search_snapshots(before, after)
before, after
|
Valid SearchSnapshot objects (or trusted RDS paths returned by save_search_snapshot()). |
Provenance changes compare provider-name sets, not row positions or counts. Comparison is descriptive: disappearance does not imply retraction, since query settings, coverage, availability and provider failures can differ.
List containing summary, added, removed, modified, provenance, unmatched, and comparability diagnostics.
plan <- new_search_plan("test", sources = "crossref") mock <- function(url, headers, params, timeout) { list(message = list(items = list())) } snap <- run_search_plan(plan, requester = mock) compare_search_snapshots(snap, snap)$summaryplan <- new_search_plan("test", sources = "crossref") mock <- function(url, headers, params, timeout) { list(message = list(items = list())) } snap <- run_search_plan(plan, requester = mock) compare_search_snapshots(snap, snap)$summary
Initializes a new local research collection with a first SearchSnapshot observation. The archive contains separate run directories with RDS evidence and credential-free JSON manifests, and does not call APIs. Files are staged in a temporary sibling directory before publication.
create_search_archive(snapshot, directory)create_search_archive(snapshot, directory)
snapshot |
Valid SearchSnapshot or a trusted RDS snapshot path. |
directory |
Path to a new archive directory; parent must exist. |
Existing directories are never intentionally overwritten. Archive files are not cryptographically protected against later manual modification. Do not read untrusted RDS files.
Archive directory path invisibly.
plan <- new_search_plan("example", sources = "crossref") mock <- function(url, headers, params, timeout) { list(message = list(items = list())) } snapshot <- run_search_plan(plan, requester = mock) path <- tempfile("research-archive-") create_search_archive(snapshot, path) unlink(path, recursive = TRUE)plan <- new_search_plan("example", sources = "crossref") mock <- function(url, headers, params, timeout) { list(message = list(items = list())) } snapshot <- run_search_plan(plan, requester = mock) path <- tempfile("research-archive-") create_search_archive(snapshot, path) unlink(path, recursive = TRUE)
Combines Scopus and Crossref Article records by normalized DOI, preserving first-occurrence order and list names. Records with missing or malformed DOIs remain separate, even when titles match.
deduplicate_articles( articles, on_conflict = c("keep_first", "warn", "error"), report = FALSE )deduplicate_articles( articles, on_conflict = c("keep_first", "warn", "error"), report = FALSE )
articles |
List of Article records, such as the concatenation of normalized Scopus and Crossref results. |
on_conflict |
One of "keep_first" (default), "warn" or "error". |
report |
If TRUE, return an audit report as well as the articles. |
The first record has field priority. Later duplicates fill missing titles, abstracts and authors, and may refine compatible partial dates (2024 vs. 2024-06-03). Differing populated values are reported, not silently mixed. Returned valid DOI fields are normalized; input records remain unchanged.
Reconciled list of Article records, or a list of three components when report is TRUE: articles, mapping and conflicts. The mapping includes input_index, output_index, original_doi, canonical_doi, status. Status is unique, duplicate or unkeyed. Conflicts identify which fields were retained and ignored, without losing the original input.
scopus <- list(Article(title = "Study", doi = "10.1000/Example")) crossref <- list(Article( abstract = "Recovered", doi = "https://doi.org/10.1000/example" )) deduplicate_articles(c(scopus, crossref), report = TRUE)scopus <- list(Article(title = "Study", doi = "10.1000/Example")) crossref <- list(Article( abstract = "Recovered", doi = "https://doi.org/10.1000/example" )) deduplicate_articles(c(scopus, crossref), report = TRUE)
Enrich normalized Scopus Article() records using batch DOI retrieval.
Only missing or whitespace-only abstracts are fetched by default.
Records, order, and list names are preserved, and duplicate DOI lookups
are avoided. Crossref JATS/XML markup is preserved unmodified.
enrich_articles_with_crossref( articles, overwrite = FALSE, on_error = c("stop", "warn", "skip"), timeout = .DEFAULT_TIMEOUT, requester = NULL, cache = NULL, mailto = NULL, max_tries = 3L, min_interval = 0.25, sleep = Sys.sleep )enrich_articles_with_crossref( articles, overwrite = FALSE, on_error = c("stop", "warn", "skip"), timeout = .DEFAULT_TIMEOUT, requester = NULL, cache = NULL, mailto = NULL, max_tries = 3L, min_interval = 0.25, sleep = Sys.sleep )
articles |
List of Article objects. |
overwrite |
If TRUE, replace existing abstracts only when Crossref supplies a non-blank string. |
on_error |
Either "stop", "warn" or "skip" for Crossref failures. |
timeout |
Positive HTTP request timeout in seconds. |
requester |
Optional HTTP callback taking url, headers, params and timeout. NULL (default) enables bounded httr2 HTTP retries. |
cache |
Optional reusable environment from |
mailto |
Optional Crossref contact email. |
max_tries |
Total number of HTTP attempts (1 to 10); with custom requester callbacks, retries are controlled by that callback. |
min_interval |
Delay in seconds between distinct uncached DOI lookups. |
sleep |
Injectable delay function, defaults to |
Updated list of Article records with existing metadata preserved.
articles <- list(Article(title = "Example", doi = "10.1000/example")) mock <- function(url, headers, params, timeout) { list(message = list(abstract = "Example abstract")) } enrich_articles_with_crossref(articles, requester = mock)articles <- list(Article(title = "Example", doi = "10.1000/example")) mock <- function(url, headers, params, timeout) { list(message = list(abstract = "Example abstract")) } enrich_articles_with_crossref(articles, requester = mock)
Writes one file in the selected format. CSV uses UTF-8 text, column names, and base R's standard CSV quoting rules. By default CSV fields beginning with potential spreadsheet formula operators are prefixed with a literal apostrophe for safer opening in spreadsheet applications. This protection affects CSV output only, not the original article records or data frame.
export_articles( articles, file, format = c("csv", "bibtex", "ris"), author_separator = "; ", keys = NULL, sanitize_csv = TRUE )export_articles( articles, file, format = c("csv", "bibtex", "ris"), author_separator = "; ", keys = NULL, sanitize_csv = TRUE )
articles |
List of |
file |
Nonempty target file path. Existing files are replaced. |
format |
Either "csv", "bibtex", or "ris". |
author_separator |
Scalar authors separator used in CSV only. |
keys |
Optional BibTeX citation keys; ignored for other formats. |
sanitize_csv |
Logical. Protect cells beginning with =, +, -, or @. |
RIS files are written with CRLF line endings as prescribed by RIS. BibTeX is written as UTF-8, so classic BibTeX may need extra encoding conversion for accented characters.
The file path invisibly.
example <- list(Article(title = "Example", doi = "10.1000/example")) target <- tempfile(fileext = ".bib") export_articles(example, target, format = "bibtex") unlink(target)example <- list(Article(title = "Example", doi = "10.1000/example")) target <- tempfile(fileext = ".bib") export_articles(example, target, format = "bibtex") unlink(target)
Request successive Scopus pages until no next cursor is returned. A repeated cursor is rejected to prevent infinite loops.
fetch_all_from_scopus( query, api_key, start_date = NULL, end_date = NULL, author = NULL, timeout = .DEFAULT_TIMEOUT, requester = .http_get_json )fetch_all_from_scopus( query, api_key, start_date = NULL, end_date = NULL, author = NULL, timeout = .DEFAULT_TIMEOUT, requester = .http_get_json )
query |
Non-empty Scopus search expression. |
api_key |
Non-empty Elsevier API key. Do not store keys in source code. |
start_date, end_date
|
Optional ISO date bounds (YYYY-MM-DD). Both must be supplied together. |
author |
Optional non-empty author expression appended to the query. |
timeout |
Positive request timeout in seconds. |
requester |
Optional HTTP callback accepting url, headers, params, and timeout. Primarily useful for deterministic tests. |
A list of raw Scopus entry objects, in page order.
mock <- function(url, headers, params, timeout) { list("search-results" = list( entry = list(list("dc:title" = "Example")), cursor = list() )) } entries <- fetch_all_from_scopus( "forecasting", "example-key", requester = mock ) length(entries)mock <- function(url, headers, params, timeout) { list("search-results" = list( entry = list(list("dc:title" = "Example")), cursor = list() )) } entries <- fetch_all_from_scopus( "forecasting", "example-key", requester = mock ) length(entries)
Search the public Crossref /works endpoint without an API key.
Uses the supported query.bibliographic parameter, which searches
bibliographic metadata (not full article text). Crossref can omit abstracts.
fetch_from_crossref( query, rows = 20L, start_date = NULL, end_date = NULL, work_type = NULL, cursor = NULL, mailto = NULL, timeout = .DEFAULT_TIMEOUT, requester = .http_get_json )fetch_from_crossref( query, rows = 20L, start_date = NULL, end_date = NULL, work_type = NULL, cursor = NULL, mailto = NULL, timeout = .DEFAULT_TIMEOUT, requester = .http_get_json )
query |
Non-empty bibliographic query, such as a paper title or topic. |
rows |
Number of records requested for the page, from 1 to 1000. |
start_date, end_date
|
Optional inclusive publication date bounds, in
|
work_type |
Optional Crossref work type such as "journal-article". |
cursor |
Optional Crossref cursor; use "*" for the first cursor page. |
mailto |
Optional contact email for Crossref's polite API pool. |
timeout |
Positive HTTP timeout in seconds. |
requester |
Optional callback accepting url, headers, params, timeout. Defaults to the package HTTP transport. Useful for offline tests. |
Raw Crossref response as a nested R list.
mock <- function(url, headers, params, timeout) { list(message = list(items = list(list(title = list("Example"))))) } fetch_from_crossref("forecasting", requester = mock)mock <- function(url, headers, params, timeout) { list(message = list(items = list(list(title = list("Example"))))) } fetch_from_crossref("forecasting", requester = mock)
Search the Works endpoint with the current search and
per-page parameters. Filters use OpenAlex work types and
publication dates. Basic, keyless queries are supported; a free
API key is necessary for cursor paging after the first page.
fetch_from_openalex( query, api_key = NULL, per_page = 25L, page = NULL, cursor = NULL, start_date = NULL, end_date = NULL, work_type = NULL, timeout = .DEFAULT_TIMEOUT, requester = .http_get_json )fetch_from_openalex( query, api_key = NULL, per_page = 25L, page = NULL, cursor = NULL, start_date = NULL, end_date = NULL, work_type = NULL, timeout = .DEFAULT_TIMEOUT, requester = .http_get_json )
query |
Non-empty OpenAlex works search expression. |
api_key |
Optional OpenAlex API key; required for multiple cursor pages. |
per_page |
Number of results requested, between 1 and 100. |
page |
Optional one-based basic page; cannot combine with cursor. |
cursor |
Optional cursor. Use "*" to start cursor pagination. |
start_date, end_date
|
Optional inclusive YYYY-MM-DD publication dates. |
work_type |
Optional OpenAlex type such as "article" or "review". |
timeout |
Positive request timeout in seconds. |
requester |
Injectable callback taking url, headers, params, timeout. |
Credentials, if provided, are sent in the Authorization header rather than the query string. The requester callback supports offline testing.
Raw OpenAlex response as a nested list.
mock <- function(url, headers, params, timeout) { list(meta = list(count = 0), results = list()) } fetch_from_openalex("gaussian processes", requester = mock)mock <- function(url, headers, params, timeout) { list(meta = list(count = 0), results = list()) } fetch_from_openalex("gaussian processes", requester = mock)
Query the Scopus Search API. The function returns the raw JSON response as nested lists and does not normalize individual article records.
fetch_from_scopus( query, api_key, max_results = .MAX_PAGE_SIZE, timeout = .DEFAULT_TIMEOUT, requester = .http_get_json )fetch_from_scopus( query, api_key, max_results = .MAX_PAGE_SIZE, timeout = .DEFAULT_TIMEOUT, requester = .http_get_json )
query |
Non-empty Scopus search expression. |
api_key |
Non-empty Elsevier API key. Do not store keys in source code. |
max_results |
Integer page size, between 1 and 25. |
timeout |
Positive request timeout in seconds. |
requester |
Optional HTTP callback accepting url, headers, params, and timeout. Primarily useful for deterministic tests. |
Raw Scopus JSON object decoded into an R list.
mock <- function(url, headers, params, timeout) { list("search-results" = list(entry = list(list("dc:title" = "Example")))) } fetch_from_scopus("forecasting", "example-key", requester = mock)mock <- function(url, headers, params, timeout) { list("search-results" = list(entry = list(list("dc:title" = "Example")))) } fetch_from_scopus("forecasting", "example-key", requester = mock)
Looks up a DOI via the Crossref works endpoint. Abstracts can include JATS/XML markup, which is returned without modification. Crossref does not have abstracts for every indexed article.
get_abstract_from_doi( doi, timeout = .DEFAULT_TIMEOUT, requester = .http_get_json, mailto = NULL )get_abstract_from_doi( doi, timeout = .DEFAULT_TIMEOUT, requester = .http_get_json, mailto = NULL )
doi |
Non-empty DOI, without the doi.org URL prefix. |
timeout |
Positive request timeout in seconds. |
requester |
Optional HTTP callback accepting url, headers, params, and timeout. Primarily useful for deterministic tests. |
mailto |
Optional contact email for Crossref's polite API pool. |
An abstract string as supplied by Crossref, or NULL if absent.
mock <- function(url, headers, params, timeout) { list(message = list(abstract = "Example abstract")) } get_abstract_from_doi("10.1000/example", requester = mock)mock <- function(url, headers, params, timeout) { list(message = list(abstract = "Example abstract")) } get_abstract_from_doi("10.1000/example", requester = mock)
Looks up each distinct DOI at most once, preserving the input order. Both present and absent abstracts are cached, so repeated queries can be avoided by passing the same cache to multiple calls.
get_abstracts_from_dois( dois, cache = NULL, mailto = NULL, max_tries = 3L, min_interval = 0.25, on_error = c("stop", "warn", "skip"), timeout = .DEFAULT_TIMEOUT, requester = NULL, sleep = Sys.sleep )get_abstracts_from_dois( dois, cache = NULL, mailto = NULL, max_tries = 3L, min_interval = 0.25, on_error = c("stop", "warn", "skip"), timeout = .DEFAULT_TIMEOUT, requester = NULL, sleep = Sys.sleep )
dois |
Character vector of DOIs. Missing or blank values yield NULL without a request. |
cache |
Optional environment, such as |
mailto |
Optional email address identifying the caller to Crossref. |
max_tries |
Integer 1 to 10, including the initial HTTP request. |
min_interval |
Nonnegative minimum delay in seconds between distinct uncached DOI lookups. Defaults to 0.25 (four requests per second). |
on_error |
One of "stop", "warn", or "skip". Failed lookups are not cached across calls. |
timeout |
Positive HTTP request timeout in seconds. |
requester |
Optional callback accepting url, headers, params, timeout. NULL uses httr2 with retry policy; callbacks bypass retries. |
sleep |
Function used for delays; defaults to |
Requests are sequential and spaced with min_interval seconds between uncached lookups. With the default HTTP requester, httr2 retries transient status codes (408, 429, 500, 502, 503, 504) and connection failures, honoring Retry-After or exponential backoff. Injected request callbacks are called directly without automatic HTTP retries.
Named list with one entry per input DOI in input order. Entries contain a character string, or NULL for missing/failed abstracts. Names retain the original DOI values, including duplicates.
mock <- function(url, headers, params, timeout) { list(message = list(abstract = "Sample abstract")) } get_abstracts_from_dois(c("10.1000/example", "10.1000/example"), requester = mock, min_interval = 0)mock <- function(url, headers, params, timeout) { list(message = list(abstract = "Sample abstract")) } get_abstracts_from_dois(c("10.1000/example", "10.1000/example"), requester = mock, min_interval = 0)
Read the small JSON manifests to inspect run dates, article counts, provider-record counts, and completeness across the archive. Only the manifests are decoded; snapshots and the full SearchHistory are not. Run ordering and plan consistency are validated.
list_search_archive_runs(directory, verify_integrity = TRUE)list_search_archive_runs(directory, verify_integrity = TRUE)
directory |
Existing SearchArchive directory. |
verify_integrity |
Logical flag. TRUE (default) verifies available MD5/SHA-256 records and refuses invalid hashes. FALSE checks metadata, filenames and integrity-policy presence without reading RDS contents. |
With verify_integrity = TRUE (default), checksums for all runs are verified against the RDS and manifest files without deserializing RDS. Set this to FALSE for a faster metadata-only listing. An unchecked run is labeled "unchecked", not "verified"; missing integrity records on protected runs are rejected in either mode. The fast mode does not establish that manifests agree with the contents of the RDS files.
Use read_search_archive() for comprehensive evidence validation and
full chronological history; use read_search_archive_run() to load one
observation with its checksum and manifest verification.
A data.frame with index, run, started_at, finished_at, articles, provider_records, complete, and integrity for each archived observation.
plan <- new_search_plan("example", sources = "crossref") mock <- function(url, headers, params, timeout) { list(message = list(items = list())) } path <- tempfile("search-archive-") create_search_archive(run_search_plan(plan, requester = mock), path) list_search_archive_runs(path) unlink(path, recursive = TRUE)plan <- new_search_plan("example", sources = "crossref") mock <- function(url, headers, params, timeout) { list(message = list(items = list())) } path <- tempfile("search-archive-") create_search_archive(run_search_plan(plan, requester = mock), path) list_search_archive_runs(path) unlink(path, recursive = TRUE)
Reusing the cache across calls avoids requesting the same DOI repeatedly. Both found and missing abstracts are cached. Network failures are not persisted in the cache.
new_crossref_cache()new_crossref_cache()
An environment suitable for the cache argument of
get_abstracts_from_dois() and enrich_articles_with_crossref().
cache <- new_crossref_cache() is.environment(cache)cache <- new_crossref_cache() is.environment(cache)
Capture the arguments of search_articles() without any credentials or
executable request callbacks. A plan can be reused to run a new search;
results are retrieved only when run_search_plan() is called.
new_search_plan( query, sources = c("crossref", "openalex"), max_results_per_source = 25L, page_size = 25L, start_date = NULL, end_date = NULL, crossref_work_type = NULL, openalex_work_type = NULL, scopus_author = NULL, on_error = c("stop", "warn", "skip"), on_conflict = c("keep_first", "warn", "error"), timeout = .DEFAULT_TIMEOUT, max_tries = 3L, min_interval = 0.25 )new_search_plan( query, sources = c("crossref", "openalex"), max_results_per_source = 25L, page_size = 25L, start_date = NULL, end_date = NULL, crossref_work_type = NULL, openalex_work_type = NULL, scopus_author = NULL, on_error = c("stop", "warn", "skip"), on_conflict = c("keep_first", "warn", "error"), timeout = .DEFAULT_TIMEOUT, max_tries = 3L, min_interval = 0.25 )
query |
Nonempty scholarly search query. |
sources |
Unique provider names, in precedence order. |
max_results_per_source |
Maximum results per provider (1 to 500). |
page_size |
Maximum results per page (1 to 25). |
start_date, end_date
|
Optional YYYY-MM-DD publication date limits. |
crossref_work_type |
Optional Crossref work type. |
openalex_work_type |
Optional OpenAlex work type. |
scopus_author |
Optional Scopus author expression. |
on_error |
Provider failure policy: stop, warn, or skip. |
on_conflict |
Reconciliation policy: keep_first, warn, or error. |
timeout |
HTTP timeout in seconds, positive and finite. |
max_tries |
Retry attempts for the built-in transport (1 to 10). |
min_interval |
Nonnegative request spacing in seconds. |
A validated SearchPlan list containing no credentials.
plan <- new_search_plan("causal inference", sources = "crossref") plan$queryplan <- new_search_plan("causal inference", sources = "crossref") plan$query
Accepts bare DOIs, doi: and urn:doi: identifiers and doi.org resolver URLs. URI representations are decoded once; bare percent signs are preserved. Matching is case-insensitive only for ASCII A-Z per DOI rules. The function performs no network calls and does not verify DOI registration.
normalize_doi(dois)normalize_doi(dois)
dois |
Character vector of DOI values, including NA when missing. |
Character vector of canonical DOI strings, NA for invalid values.
normalize_doi(c(" DOI:10.1000/Example ", "https://doi.org/10.1000/example"))normalize_doi(c(" DOI:10.1000/Example ", "https://doi.org/10.1000/example"))
Convert Crossref metadata into the same Article() format as Scopus.
Incomplete publication dates retain their original precision (e.g.
"2024" or "2024-08"). Non-record entries are ignored.
process_crossref_entries(entries)process_crossref_entries(entries)
entries |
A list of Crossref works metadata objects. |
A list of Article records.
works <- list(list(title = list("Example"), DOI = "10.1000/example")) process_crossref_entries(works)works <- list(list(title = list("Example"), DOI = "10.1000/example")) process_crossref_entries(works)
Normalize a Crossref works search response
process_crossref_response(payload)process_crossref_response(payload)
payload |
A Crossref response containing |
A list of normalized Article records.
payload <- list(message = list(items = list( list(title = list("Example"), DOI = "10.1000/example") ))) process_crossref_response(payload)payload <- list(message = list(items = list( list(title = list("Example"), DOI = "10.1000/example") ))) process_crossref_response(payload)
Converts titles, inverted-index abstracts, dates, authorship display names and DOI resolver URLs into the package's standard Article representation. Missing fields remain NULL. A present malformed abstract index fails visibly to avoid silently returning truncated or invented text.
process_openalex_entries(entries)process_openalex_entries(entries)
entries |
List of OpenAlex works objects. |
List of normalized Article records, in source order.
works <- list(list( title = "Example", doi = "https://doi.org/10.1000/example", authorships = list(list(author = list(display_name = "Ada"))) )) process_openalex_entries(works)works <- list(list( title = "Example", doi = "https://doi.org/10.1000/example", authorships = list(list(author = list(display_name = "Ada"))) )) process_openalex_entries(works)
Normalize an OpenAlex search response
process_openalex_response(payload)process_openalex_response(payload)
payload |
Raw OpenAlex response including a results array. |
List of Article records.
process_openalex_response(list(results = list( list(title = "A work", publication_date = "2025-01-02") )))process_openalex_response(list(results = list( list(title = "A work", publication_date = "2025-01-02") )))
Converts selected Dublin Core and PRISM fields into validated Article objects. Malformed entry and author objects are ignored.
process_scopus_entries(entries)process_scopus_entries(entries)
entries |
List of raw Scopus entry objects. |
A list of Article objects.
entries <- list(list("dc:title" = "Example", "prism:doi" = "10.1000/example")) process_scopus_entries(entries)entries <- list(list("dc:title" = "Example", "prism:doi" = "10.1000/example")) process_scopus_entries(entries)
Normalize a complete Scopus search response
process_scopus_response(payload)process_scopus_response(payload)
payload |
Raw Scopus JSON object containing search-results.entry. |
A list of Article objects.
payload <- list("search-results" = list( entry = list(list("dc:title" = "Example")) )) process_scopus_response(payload)payload <- list("search-results" = list( entry = list(list("dc:title" = "Example")) )) process_scopus_response(payload)
Reads each run's trusted RDS evidence and checks the accompanying JSON manifest for matching timestamps, query, sources and article count. Rejects missing or nonsequential run folders and nonchronological results. An archive with at least two runs includes an automatically generated SearchHistory for all successive observations.
read_search_archive(directory)read_search_archive(directory)
directory |
Existing research archive directory. |
A SearchArchive containing directory, inventory, snapshots, and history (NULL when the archive contains only its initial observation).
plan <- new_search_plan("example", sources = "crossref") mock <- function(url, headers, params, timeout) { list(message = list(items = list())) } snapshot <- run_search_plan(plan, requester = mock) path <- tempfile("research-archive-") create_search_archive(snapshot, path) read_search_archive(path)$inventory unlink(path, recursive = TRUE)plan <- new_search_plan("example", sources = "crossref") mock <- function(url, headers, params, timeout) { list(message = list(items = list())) } snapshot <- run_search_plan(plan, requester = mock) path <- tempfile("research-archive-") create_search_archive(snapshot, path) read_search_archive(path)$inventory unlink(path, recursive = TRUE)
Load and validate one run instead of deserializing the entire archive.
The selected run's checksum and manifest are validated as with
read_search_archive(). Other runs are not checked; use
list_search_archive_runs() to audit their checksums or
read_search_archive() to load and validate all evidence.
read_search_archive_run(directory, run = "latest")read_search_archive_run(directory, run = "latest")
directory |
Existing SearchArchive directory. |
run |
"latest" (default), a positive one-based integer run index, or a canonical name such as "run_000002". |
A validated SearchSnapshot for the selected run.
plan <- new_search_plan("example", sources = "crossref") mock <- function(url, headers, params, timeout) { list(message = list(items = list())) } path <- tempfile("search-archive-") snapshot <- run_search_plan(plan, requester = mock) create_search_archive(snapshot, path) identical(read_search_archive_run(path), snapshot) unlink(path, recursive = TRUE)plan <- new_search_plan("example", sources = "crossref") mock <- function(url, headers, params, timeout) { list(message = list(items = list())) } path <- tempfile("search-archive-") snapshot <- run_search_plan(plan, requester = mock) create_search_archive(snapshot, path) identical(read_search_archive_run(path), snapshot) unlink(path, recursive = TRUE)
Reloads an RDS search snapshot, validating its schema version and evidence structure. Use only snapshot files from trusted sources. RDS is an R-native serialized format, not a safe interchange format for untrusted files.
read_search_snapshot(file)read_search_snapshot(file)
file |
Path to an existing RDS search snapshot. |
A validated SearchSnapshot preserving articles and provenance.
plan <- new_search_plan("test", sources = "crossref") mock <- function(url, headers, params, timeout) { list(message = list(items = list())) } snap <- run_search_plan(plan, requester = mock) path <- tempfile(fileext = ".rds") save_search_snapshot(snap, path) restored <- read_search_snapshot(path) unlink(path) restored$context$completeplan <- new_search_plan("test", sources = "crossref") mock <- function(url, headers, params, timeout) { list(message = list(items = list())) } snap <- run_search_plan(plan, requester = mock) path <- tempfile(fileext = ".rds") save_search_snapshot(snap, path) restored <- read_search_snapshot(path) unlink(path) restored$context$complete
OpenAlex represents available abstracts as tokens mapped to their zero-based positions. This function sorts those positions and joins the tokens with spaces. Missing or empty indexes yield NULL. Malformed positions, duplicate positions, or gaps cause an explicit error rather than silently fabricating text.
reconstruct_openalex_abstract(index)reconstruct_openalex_abstract(index)
index |
Named list of tokens mapped to integer position vectors, as returned in abstract_inverted_index, or NULL. |
A character abstract or NULL when no index is supplied.
reconstruct_openalex_abstract(list( evidence = 2L, Research = 0L, needs = 1L ))reconstruct_openalex_abstract(list( evidence = 2L, Research = 0L, needs = 1L ))
By default no files are changed. With dry_run = FALSE, remove only hidden staging directories created by this package. If a writer lock remains, deletion is refused unless force_unlock = TRUE; that option is dangerous if any writer is still active.
recover_search_archive(directory, dry_run = TRUE, force_unlock = FALSE)recover_search_archive(directory, dry_run = TRUE, force_unlock = FALSE)
directory |
Existing search archive directory. |
dry_run |
TRUE (default) for inspection only. |
force_unlock |
Explicitly remove a writer lock when dry_run is FALSE. |
All staging paths are validated before any lock removal or deletion. Completed archive runs are never removed. Suspected checksum corruption must be investigated using a trusted backup instead of this function.
List with pending, locked, removed, and unlocked fields.
plan <- new_search_plan("example", sources = "crossref") mock <- function(url, headers, params, timeout) { list(message = list(items = list())) } path <- tempfile("archive-recovery-") create_search_archive(run_search_plan(plan, requester = mock), path) recover_search_archive(path)$pending unlink(path, recursive = TRUE)plan <- new_search_plan("example", sources = "crossref") mock <- function(url, headers, params, timeout) { list(message = list(items = list())) } path <- tempfile("archive-recovery-") create_search_archive(run_search_plan(plan, requester = mock), path) recover_search_archive(path)$pending unlink(path, recursive = TRUE)
Executes the SearchPlan from the most recent archived observation, using credentials and HTTP request callbacks supplied at invocation time. The new observation is appended only after search and snapshot validation. Provider errors obey the SearchPlan policy. This function does not create timers, recurring jobs, or background processes.
refresh_search_archive( directory, scopus_api_key = NULL, openalex_api_key = NULL, crossref_mailto = NULL, requester = NULL, sleep = Sys.sleep, clock = Sys.time, return_mode = c("full", "recent") )refresh_search_archive( directory, scopus_api_key = NULL, openalex_api_key = NULL, crossref_mailto = NULL, requester = NULL, sleep = Sys.sleep, clock = Sys.time, return_mode = c("full", "recent") )
directory |
Existing search archive. |
scopus_api_key |
Optional runtime Scopus API key. |
openalex_api_key |
Optional runtime OpenAlex API key. |
crossref_mailto |
Optional runtime contact email for Crossref. |
requester |
Optional HTTP requester callback. |
sleep |
Injectable request-spacing function. |
clock |
Injectable zero-argument time function. |
return_mode |
"full" (default) preserves the original SearchArchive result, loading all snapshots and building a full SearchHistory after the append. "recent" returns a SearchArchiveRefresh containing the archive inventory, previous and new snapshots, and their two-run comparison. It does not deserialize older historical observations. |
A complete SearchArchive by default, or SearchArchiveRefresh with directory, inventory, previous, snapshot and comparison when return_mode is "recent".
plan <- new_search_plan("example", sources = "crossref") mock <- function(url, headers, params, timeout) { list(message = list(items = list())) } snapshot <- run_search_plan(plan, requester = mock) path <- tempfile("research-archive-") create_search_archive(snapshot, path) updated <- refresh_search_archive(path, requester = mock) updated$inventory unlink(path, recursive = TRUE)plan <- new_search_plan("example", sources = "crossref") mock <- function(url, headers, params, timeout) { list(message = list(items = list())) } snapshot <- run_search_plan(plan, requester = mock) path <- tempfile("research-archive-") create_search_archive(snapshot, path) updated <- refresh_search_archive(path, requester = mock) updated$inventory unlink(path, recursive = TRUE)
Execute the SearchPlan contained in an earlier SearchSnapshot, compare the results with that observation and write a self-contained report directory. This is an explicit, synchronous operation; it does not install a scheduler or contact providers outside the requested search.
refresh_search_snapshot( previous, directory, scopus_api_key = NULL, openalex_api_key = NULL, crossref_mailto = NULL, include_values = FALSE, requester = NULL, sleep = Sys.sleep, clock = Sys.time )refresh_search_snapshot( previous, directory, scopus_api_key = NULL, openalex_api_key = NULL, crossref_mailto = NULL, include_values = FALSE, requester = NULL, sleep = Sys.sleep, clock = Sys.time )
previous |
An existing SearchSnapshot or a trusted RDS path produced
by |
directory |
Destination directory, which must not already exist. The parent directory must exist. |
scopus_api_key |
Optional Elsevier API key supplied at execution. |
openalex_api_key |
Optional OpenAlex API key supplied at execution. |
crossref_mailto |
Optional Crossref contact email. |
include_values |
If TRUE, include changed metadata text in CSV events; defaults to FALSE to exclude potentially long abstracts. |
requester |
Optional HTTP callback taking url, headers, params, timeout. Useful for deterministic offline tests. |
sleep |
Injectable delay callback (default |
clock |
Injectable zero-argument time callback (default |
The output contains the previous and current snapshots in RDS format, credential-free JSON manifests for both, and a CSV drift report. All files are staged in a temporary sibling directory and published only when every write succeeds. Existing output directories are never intentionally overwritten. The previous snapshot or its source RDS file is unchanged.
If the plan allows partial provider failures, the new snapshot still captures them; its comparison is marked incomparable when either run is incomplete. A missing DOI in the new results does not establish article withdrawal or retraction.
API keys and Crossref contact details are supplied only at execution time, and are excluded from the plan and generated manifests. The full RDS files intentionally preserve article text and provider error evidence: store them privately. Changed field values (including abstracts) are omitted from the CSV report unless include_values is TRUE.
A SearchRefresh list containing directory, snapshot and history. The saved files are named before.rds, after.rds, before-manifest.json, after-manifest.json, and report/ (CSV drift tables and interpretation notes).
plan <- new_search_plan("test", sources = "crossref") mock <- function(url, headers, params, timeout) { list(message = list(items = list())) } before <- run_search_plan(plan, requester = mock) path <- tempfile("search-refresh-") refreshed <- refresh_search_snapshot( before, path, requester = mock, sleep = function(...) NULL ) refreshed$history$transitions unlink(path, recursive = TRUE)plan <- new_search_plan("test", sources = "crossref") mock <- function(url, headers, params, timeout) { list(message = list(items = list())) } before <- run_search_plan(plan, requester = mock) path <- tempfile("search-refresh-") refreshed <- refresh_search_snapshot( before, path, requester = mock, sleep = function(...) NULL ) refreshed$history$transitions unlink(path, recursive = TRUE)
Executes search_articles() using a validated SearchPlan and saves the
resulting normalized articles, provider counts and errors, DOI provenance
and field-level conflicts. Records UTC start/end timestamps and runtime
version information. Credentials are accepted at execution time only:
values of API keys and Crossref contact addresses are not saved.
run_search_plan( plan, scopus_api_key = NULL, openalex_api_key = NULL, crossref_mailto = NULL, requester = NULL, sleep = Sys.sleep, clock = Sys.time )run_search_plan( plan, scopus_api_key = NULL, openalex_api_key = NULL, crossref_mailto = NULL, requester = NULL, sleep = Sys.sleep, clock = Sys.time )
plan |
A SearchPlan from |
scopus_api_key |
Optional Elsevier API key. |
openalex_api_key |
Optional OpenAlex API key. |
crossref_mailto |
Optional Crossref contact address. |
requester |
Optional shared HTTP callback for offline tests. |
sleep |
Optional delay function, by default |
clock |
Zero-argument function returning the current time, by default
|
A snapshot is an observation of one search run, not a claim that a provider
will return the same results later. Use save_search_snapshot() to persist
the complete result for reproducible downstream analysis.
A SearchSnapshot with schema_version, plan, started_at,
finished_at, context (runtime information and completion status),
and result (the five search_articles() output tables/objects).
plan <- new_search_plan("forecasting", sources = "crossref") mock <- function(url, headers, params, timeout) { list(message = list(items = list( list(title = list("Example"), DOI = "10.1000/example") ))) } snap <- run_search_plan(plan, requester = mock, sleep = function(...) NULL) snap$result$sourcesplan <- new_search_plan("forecasting", sources = "crossref") mock <- function(url, headers, params, timeout) { list(message = list(items = list( list(title = list("Example"), DOI = "10.1000/example") ))) } snap <- run_search_plan(plan, requester = mock, sleep = function(...) NULL) snap$result$sources
Writes the full SearchSnapshot to an RDS file, using a temporary file in the same directory before renaming. Existing files are never intentionally overwritten. RDS preserves Article classes and empty table schemas. No network calls or additional R packages are required.
save_search_snapshot(snapshot, file)save_search_snapshot(snapshot, file)
snapshot |
A valid SearchSnapshot from |
file |
Nonempty path to a new RDS file. The parent directory must already exist. Existing files cause an error. |
The file path invisibly.
plan <- new_search_plan("test", sources = "crossref") mock <- function(url, headers, params, timeout) { list(message = list(items = list())) } snap <- run_search_plan(plan, requester = mock) path <- tempfile(fileext = ".rds") save_search_snapshot(snap, path) unlink(path)plan <- new_search_plan("test", sources = "crossref") mock <- function(url, headers, params, timeout) { list(message = list(items = list())) } snap <- run_search_plan(plan, requester = mock) path <- tempfile(fileext = ".rds") save_search_snapshot(snap, path) unlink(path)
Fetches bounded result sets from the selected providers, normalizes them to Article records, and reconciles exact DOI matches. Each provider is queried in the requested order. Where populated fields disagree, the earlier provider's value is kept and the disagreement is reported.
search_articles( query, sources = c("crossref", "openalex"), max_results_per_source = 25L, page_size = 25L, start_date = NULL, end_date = NULL, scopus_api_key = NULL, openalex_api_key = NULL, crossref_mailto = NULL, crossref_work_type = NULL, openalex_work_type = NULL, scopus_author = NULL, on_error = c("stop", "warn", "skip"), on_conflict = c("keep_first", "warn", "error"), timeout = .DEFAULT_TIMEOUT, max_tries = 3L, min_interval = 0.25, requester = NULL, sleep = Sys.sleep )search_articles( query, sources = c("crossref", "openalex"), max_results_per_source = 25L, page_size = 25L, start_date = NULL, end_date = NULL, scopus_api_key = NULL, openalex_api_key = NULL, crossref_mailto = NULL, crossref_work_type = NULL, openalex_work_type = NULL, scopus_author = NULL, on_error = c("stop", "warn", "skip"), on_conflict = c("keep_first", "warn", "error"), timeout = .DEFAULT_TIMEOUT, max_tries = 3L, min_interval = 0.25, requester = NULL, sleep = Sys.sleep )
query |
Non-empty scholarly search expression. |
sources |
Non-empty vector containing unique elements of "crossref", "openalex", and "scopus". Order determines metadata precedence. |
max_results_per_source |
Maximum works fetched from each source, between 1 and 500. This is not a global result limit. |
page_size |
Maximum works per request (1 to 25); 25 respects the Scopus API page-size restriction. |
start_date, end_date
|
Optional YYYY-MM-DD publication date bounds. Scopus searches require either both or neither. |
scopus_api_key |
Optional Elsevier API key; required for Scopus. |
openalex_api_key |
Optional OpenAlex API key; required when OpenAlex needs more than one cursor page. |
crossref_mailto |
Optional contact email for Crossref. |
crossref_work_type |
Optional Crossref type such as "journal-article". |
openalex_work_type |
Optional OpenAlex type such as "article". |
scopus_author |
Optional Scopus author expression. |
on_error |
Provider failure policy: "stop" (default), "warn" or "skip". A failed source is recorded in the results for warn/skip. |
on_conflict |
How to handle populated fields differing for one DOI: "keep_first" (default), "warn", or "error". |
timeout |
Positive timeout per HTTP request in seconds. |
max_tries |
Attempts for the built-in Crossref/OpenAlex retry transport (1 to 10). Scopus uses its standard request transport. |
min_interval |
Nonnegative delay between requests for each source. |
requester |
Optional shared HTTP callback receiving url, headers, params, timeout. Useful for deterministic network-free testing. |
sleep |
Injectable delay function, default Sys.sleep. |
The result includes a row for each retrieved record in provenance, showing its original source, position, canonical DOI, reconciliation status and final output index. DOI-free articles remain separate. No fuzzy matching or unsupported source-to-source field inferences are performed.
By default Crossref and OpenAlex are selected because they allow small queries without credentials. Scopus requires scopus_api_key. OpenAlex requires openalex_api_key for more than one cursor page. Source-specific type filters are separate because the provider taxonomies differ.
List with articles (reconciled Article records), provenance (input-to-output mapping with source labels), conflicts (disagreements found during reconciliation), sources (one status row per requested provider), and errors (provider-level failure messages).
mock <- function(url, headers, params, timeout) { if (grepl("crossref", url, fixed = TRUE)) { return(list(message = list(items = list( list(title = list("Example"), DOI = "10.1000/a") )))) } list(results = list( list(title = "Example", doi = "https://doi.org/10.1000/a") ), meta = list()) } result <- search_articles("example", requester = mock, min_interval = 0) result$provenancemock <- function(url, headers, params, timeout) { if (grepl("crossref", url, fixed = TRUE)) { return(list(message = list(items = list( list(title = list("Example"), DOI = "10.1000/a") )))) } list(results = list( list(title = "Example", doi = "https://doi.org/10.1000/a") ), meta = list()) } result <- search_articles("example", requester = mock, min_interval = 0) result$provenance
Search Crossref bibliographic metadata and normalize the matching records.
Uses cursor pagination to retrieve up to max_results; unlike a
search that retrieves the entire Crossref index, this limit is finite.
search_crossref( query, max_results = 50L, page_size = 50L, start_date = NULL, end_date = NULL, work_type = NULL, mailto = NULL, timeout = .DEFAULT_TIMEOUT, max_tries = 3L, min_interval = 0.25, requester = NULL, sleep = Sys.sleep )search_crossref( query, max_results = 50L, page_size = 50L, start_date = NULL, end_date = NULL, work_type = NULL, mailto = NULL, timeout = .DEFAULT_TIMEOUT, max_tries = 3L, min_interval = 0.25, requester = NULL, sleep = Sys.sleep )
query |
Non-empty bibliographic query, such as a paper title or topic. |
max_results |
Maximum normalized records to return. Defaults to 50. |
page_size |
Page size from 1 to 1000; defaults to 50. |
start_date, end_date
|
Optional inclusive publication date bounds, in
|
work_type |
Optional Crossref work type such as "journal-article". |
mailto |
Optional contact email for Crossref's polite API pool. |
timeout |
Positive HTTP timeout in seconds. |
max_tries |
Maximum HTTP attempts per page, 1 to 10, when using the default transport. Injected callback requesters bypass automatic retries. |
min_interval |
Nonnegative delay between page requests in seconds. |
requester |
Optional request callback; defaults to NULL, meaning bounded httr2 transient-error retries. |
sleep |
Injectable delay callback for offline tests. |
The cursor endpoint is paged until the requested number of works is collected, a short page is received, or no next cursor is available. Repeated cursors cause an error rather than an infinite loop. Existing Crossref abstracts are preserved as supplied, including JATS/XML.
A list of normalized Article records in result order.
mock <- function(url, headers, params, timeout) { list(message = list(items = list( list(title = list("Example paper"), DOI = "10.1000/example") ))) } papers <- search_crossref("gaussian processes", requester = mock, min_interval = 0) papers[[1L]]$titlemock <- function(url, headers, params, timeout) { list(message = list(items = list( list(title = list("Example paper"), DOI = "10.1000/example") ))) } papers <- search_crossref("gaussian processes", requester = mock, min_interval = 0) papers[[1L]]$title
Search OpenAlex with finite result limits and cursor pagination. For multi-page searches an OpenAlex API key is required; single-page searches can use the smaller keyless budget. Cursor loops and malformed pages raise errors. Request spacing avoids bursts of API traffic.
search_openalex( query, api_key = NULL, max_results = 25L, page_size = 25L, start_date = NULL, end_date = NULL, work_type = NULL, timeout = .DEFAULT_TIMEOUT, max_tries = 3L, min_interval = 0.25, requester = NULL, sleep = Sys.sleep )search_openalex( query, api_key = NULL, max_results = 25L, page_size = 25L, start_date = NULL, end_date = NULL, work_type = NULL, timeout = .DEFAULT_TIMEOUT, max_tries = 3L, min_interval = 0.25, requester = NULL, sleep = Sys.sleep )
query |
Non-empty OpenAlex works search expression. |
api_key |
Optional OpenAlex API key; required for multiple cursor pages. |
max_results |
Maximum normalized results to return, at least 1. |
page_size |
Requested works per cursor page, between 1 and 100. |
start_date, end_date
|
Optional inclusive YYYY-MM-DD publication dates. |
work_type |
Optional OpenAlex type such as "article" or "review". |
timeout |
Positive request timeout in seconds. |
max_tries |
Retry attempts for transient failures using httr2. |
min_interval |
Delay in seconds between page requests. |
requester |
Optional callback; NULL enables retry-capable httr2. |
sleep |
Injectable delay function for offline tests. |
List of normalized Article records.
mock <- function(url, headers, params, timeout) { list(results = list(list(title = "Example")), meta = list()) } search_openalex("forecasting", requester = mock, min_interval = 0)mock <- function(url, headers, params, timeout) { list(results = list(list(title = "Example")), meta = list()) } search_openalex("forecasting", requester = mock, min_interval = 0)
Creates a small, credential-free summary of one validated SearchSnapshot: the search plan, UTC execution times, runtime versions, provider status counts, DOI coverage, and metadata reconciliation counts. Original article abstracts and provider error messages are excluded.
search_snapshot_manifest(snapshot)search_snapshot_manifest(snapshot)
snapshot |
SearchSnapshot from run_search_plan(). |
A manifest describes retrieved evidence, not a reproducible guarantee that external providers will return the same results in a later search. For a complete record, preserve the source SearchSnapshot RDS as well.
A SearchManifest list with schema, plan, context, counts, and provider summary.
plan <- new_search_plan("topic", sources = "crossref") mock <- function(url, headers, params, timeout) { list(message = list(items = list())) } snapshot <- run_search_plan(plan, requester = mock) search_snapshot_manifest(snapshot)$countsplan <- new_search_plan("topic", sources = "crossref") mock <- function(url, headers, params, timeout) { list(message = list(items = list())) } snapshot <- run_search_plan(plan, requester = mock) search_snapshot_manifest(snapshot)$counts
Locate normalized DOIs across all archived search runs using compact JSON manifest indexes rather than loading every RDS snapshot. The result records which run first and last observed each DOI, its position in that run, and the contributing provider names. Dates are search-observation timestamps, not publication, withdrawal or retraction dates.
trace_archive_dois( directory, dois, on_unindexed = c("read", "error"), verify_integrity = TRUE )trace_archive_dois( directory, dois, on_unindexed = c("read", "error"), verify_integrity = TRUE )
directory |
Existing research archive directory. |
dois |
Character vector of one or more valid DOI representations. Equivalent DOIs in the input are deduplicated after normalization. |
on_unindexed |
Either "read" (default) or "error" for older manifests. |
verify_integrity |
Logical. TRUE (default) verifies all stored file checksums; FALSE performs a faster metadata-only scan. |
Manifests created before DOI indexes are supported: when on_unindexed is "read" (default), only those older RDS snapshots are loaded and validated to recover their DOI positions. Use "error" to forbid fallback reads.
The archive inventory is checked for chronological consistency. By default all archived RDS/manifest file checksums are verified without deserializing indexed runs. Setting verify_integrity = FALSE skips those checks for indexed runs and labels the returned result as unverified. Legacy RDS fallback always performs its available integrity checks.
No network queries are performed. Records without valid DOIs cannot be traced using this function.
An ArchiveDOITrace list with occurrences (one row per matching archive run), summary (one row per requested DOI, including absent identifiers), legacy_runs (runs requiring RDS fallback), and the integrity_checked flag.
plan <- new_search_plan("example", sources = "crossref") mock <- function(url, headers, params, timeout) { list(message = list(items = list( list(title = list("Paper"), DOI = "10.1000/example") ))) } snapshot <- run_search_plan(plan, requester = mock) path <- tempfile("archive-") create_search_archive(snapshot, path) trace_archive_dois(path, "https://doi.org/10.1000/example")$summary unlink(path, recursive = TRUE)plan <- new_search_plan("example", sources = "crossref") mock <- function(url, headers, params, timeout) { list(message = list(items = list( list(title = list("Paper"), DOI = "10.1000/example") ))) } snapshot <- run_search_plan(plan, requester = mock) path <- tempfile("archive-") create_search_archive(snapshot, path) trace_archive_dois(path, "https://doi.org/10.1000/example")$summary unlink(path, recursive = TRUE)
Inspects every run without deserializing RDS. New runs are verified with SHA-256, while older MD5 runs remain readable and are labeled verified_md5. Pre-checksum archives receive legacy_unverified status. A protected run that loses its integrity.json is reported as invalid, not as legacy.
verify_search_archive(directory)verify_search_archive(directory)
directory |
Existing local archive directory. |
The archive root policy is local and unsigned: checksums do not establish authenticity against deliberate manipulation of both data and metadata.
List of runs (run, integrity, issue), pending staging paths, and a logical locked flag.
plan <- new_search_plan("example", sources = "crossref") mock <- function(url, headers, params, timeout) { list(message = list(items = list())) } path <- tempfile("archive-audit-") create_search_archive(run_search_plan(plan, requester = mock), path) verify_search_archive(path)$runs unlink(path, recursive = TRUE)plan <- new_search_plan("example", sources = "crossref") mock <- function(url, headers, params, timeout) { list(message = list(items = list())) } path <- tempfile("archive-audit-") create_search_archive(run_search_plan(plan, requester = mock), path) verify_search_archive(path)$runs unlink(path, recursive = TRUE)
Write six UTF-8 CSV tables (runs, providers, transitions, events, unmatched) and a short README describing interpretation limits. Provider error texts, API keys, and contact addresses are not included. By default, metadata before/after values are suppressed in the event table; opt in using include_values = TRUE. CSV cells are sanitized against spreadsheet formula injection. A new directory is required and existing reports are never intentionally overwritten.
write_search_history_report(history, directory, include_values = FALSE)write_search_history_report(history, directory, include_values = FALSE)
history |
A SearchHistory created by |
directory |
Path to a new report directory, within an existing parent. |
include_values |
Whether to include changed field values (which may include full abstracts) in events.csv. Defaults to FALSE. |
The report directory path invisibly.
plan <- new_search_plan("example", sources = "crossref") mock <- function(url, headers, params, timeout) { list(message = list(items = list())) } snapshot <- run_search_plan(plan, requester = mock) history <- compare_search_history(list(snapshot, snapshot)) directory <- tempfile("search-history-") write_search_history_report(history, directory) unlink(directory, recursive = TRUE)plan <- new_search_plan("example", sources = "crossref") mock <- function(url, headers, params, timeout) { list(message = list(items = list())) } snapshot <- run_search_plan(plan, requester = mock) history <- compare_search_history(list(snapshot, snapshot)) directory <- tempfile("search-history-") write_search_history_report(history, directory) unlink(directory, recursive = TRUE)
Writes a small, human-readable UTF-8 JSON summary of a SearchSnapshot. The manifest intentionally excludes API keys, contact emails, article abstracts, and raw error messages. A temporary file is written in the destination directory before renaming; existing files are not intentionally overwritten.
write_search_manifest(snapshot, file)write_search_manifest(snapshot, file)
snapshot |
Valid SearchSnapshot. |
file |
Path to a new JSON file. Its parent directory must exist. |
File path invisibly.
plan <- new_search_plan("example", sources = "crossref") mock <- function(url, headers, params, timeout) { list(message = list(items = list())) } snapshot <- run_search_plan(plan, requester = mock) target <- tempfile(fileext = ".json") write_search_manifest(snapshot, target) unlink(target)plan <- new_search_plan("example", sources = "crossref") mock <- function(url, headers, params, timeout) { list(message = list(items = list())) } snapshot <- run_search_plan(plan, requester = mock) target <- tempfile(fileext = ".json") write_search_manifest(snapshot, target) unlink(target)