| Title: | Client for a Self-Hosted 'researchD' Literature Surveillance Server |
|---|---|
| Description: | An R client for 'researchD', a self-hosted literature and signal surveillance server. Configures surveillance questions and reads stored literature records, signals, source health and service heartbeats through a token-authenticated REST API. Returns typed 'tibble' objects, including source provenance where supported by the server. Collection, classification, source access and delivery are managed by the server. |
| Authors: | Raban Heller [aut, cre, cph] (ORCID: <https://orcid.org/0000-0001-8006-9742>) |
| Maintainer: | Raban Heller <[email protected]> |
| License: | MIT + file LICENSE |
| Version: | 0.1.2 |
| Built: | 2026-11-03 18:16:59 UTC |
| Source: | https://github.com/CTTIR/researchR |
A question is a list of concepts, AND-joined at query time; the terms and
MeSH headings within one concept are OR-joined. So a two-concept question
(semaglutide OR "GLP-1") AND (MASH OR steatohepatitis) is two calls to
this function.
rr_concept(name, terms = character(0), mesh = character(0))rr_concept(name, terms = character(0), mesh = character(0))
name |
Human-readable concept name, e.g. |
terms |
Character vector of free-text synonyms. |
mesh |
Character vector of MeSH headings. |
A list of class researchr_concept.
rr_concept("drug", terms = c("semaglutide", "GLP-1"))rr_concept("drug", terms = c("semaglutide", "GLP-1"))
Creates the connection object every other function in this package takes as
its first argument. Nothing is sent at connection time unless check = TRUE. The default checks the server immediately; use check = FALSE
to construct an offline connection.
rr_connect( base_url = Sys.getenv("RESEARCHR_URL"), token = Sys.getenv("RESEARCHR_TOKEN"), timeout = 60, check = TRUE )rr_connect( base_url = Sys.getenv("RESEARCHR_URL"), token = Sys.getenv("RESEARCHR_TOKEN"), timeout = 60, check = TRUE )
base_url |
Base URL of the researchD web service, e.g.
|
token |
API bearer token. Defaults to the |
timeout |
Request timeout in seconds. |
check |
Whether to contact the server and verify the token now. |
An object of class researchr_conn.
researchD exposes two disjoint authentication surfaces. The browser
dashboard is behind a password plus a TOTP second factor; the /api/v1
surface this package uses is behind a bearer token. A dashboard session is
deliberately not accepted as an API token, and vice versa.
Set the token on the server in its .env as API_TOKEN, and locally as
the RESEARCHR_TOKEN environment variable (in ~/.Renviron, so it never
reaches your scripts or your history). If the server has no API_TOKEN
set, the whole surface answers 503 rather than serving unauthenticated —
this package reports that as a researchR_disabled condition.
A checked connection requires valid API v1 metadata. Malformed responses
raise researchR_protocol_error. Request errors redact the bearer token
and submitted source keys; do not include credentials in diagnostic reports.
conn <- rr_connect() rr_questions(conn)conn <- rr_connect() rr_questions(conn)
Deleting a question cascades: its concepts, stored records, signals and signal configuration go with it. This cannot be undone.
rr_delete_question(conn, question_id)rr_delete_question(conn, question_id)
conn |
A |
question_id |
Question id. |
Invisibly, TRUE.
## Not run: rr_delete_question(rr_connect(), "obsolete_question") ## End(Not run)## Not run: rr_delete_question(rr_connect(), "obsolete_question") ## End(Not run)
The feeds are open — no token — so these URLs can go straight into Zotero (My Library, right-click, New Feed, From URL) or any reader.
rr_feed_url( conn, question_id, study_type = NULL, all = FALSE, preprints = FALSE, signals = FALSE )rr_feed_url( conn, question_id, study_type = NULL, all = FALSE, preprints = FALSE, signals = FALSE )
conn |
A |
question_id |
Question id. |
study_type |
Optional study type, or signal type when |
all |
If |
preprints |
If |
signals |
If |
A single URL string.
conn <- structure(list(base_url = "https://feeds.example.org"), class = "researchr_conn") rr_feed_url(conn, "glp1_mash", study_type = "rct")conn <- structure(list(base_url = "https://feeds.example.org"), class = "researchr_conn") rr_feed_url(conn, "glp1_mash", study_type = "rct")
Hits the unauthenticated /healthz endpoint, so this answers "is the web
service running" independently of whether the token is any good.
rr_health(conn)rr_health(conn)
conn |
A |
TRUE if the server answered ok, otherwise FALSE.
rr_health(rr_connect(check = FALSE))rr_health(rr_connect(check = FALSE))
rr_source_health() answers "is this source still returning anything".
This answers the prior question: "did the engine and the scheduler run at
all". The distinction matters because a crashed engine writes no source
heartbeats either, so without this the two failures look identical — and
both look like a quiet week in the literature.
rr_heartbeat(conn)rr_heartbeat(conn)
conn |
A |
A tibble with one row per service: when it last ran, when it last
ran successfully, a short detail string, the most recent error, and a
stale flag.
rr_heartbeat(rr_connect())rr_heartbeat(rr_connect())
Returns the study-type taxonomy, signal types, confidence tiers and source list the server is actually using. Worth calling after a server upgrade: it is the authoritative vocabulary, so validating against it is how the client avoids drifting from the server's classifier.
rr_meta(conn)rr_meta(conn)
conn |
A |
A named list.
rr_meta(rr_connect())$study_typesrr_meta(rr_connect())$study_types
Fetch one question by id
rr_question(conn, question_id)rr_question(conn, question_id)
conn |
A |
question_id |
Question id (the slug). |
A one-row tibble, as in rr_questions().
rr_question(rr_connect(), "glp1_mash")rr_question(rr_connect(), "glp1_mash")
List the surveillance questions
rr_questions(conn)rr_questions(conn)
conn |
A |
A tibble, one row per question. The list-valued fields
(concepts, sources, email_to, study_filter, delivery,
api_key_present) come back as list columns.
API keys are never returned by the server: api_key_present reports
only which sources have a key stored, never the key itself.
rr_questions(rr_connect())rr_questions(rr_connect())
Pulls what researchD has already found and stored — this never triggers a fetch from the upstream databases, which is the pipeline's own job on its cron schedule.
rr_records( conn, question_id, study_type = NULL, is_preprint = NULL, since = NULL, until = NULL, min_score = NULL, emailed = NULL, apply_threshold = FALSE, n_max = Inf, page_size = 200 )rr_records( conn, question_id, study_type = NULL, is_preprint = NULL, since = NULL, until = NULL, min_score = NULL, emailed = NULL, apply_threshold = FALSE, n_max = Inf, page_size = 200 )
conn |
A |
question_id |
Question id. |
study_type |
Optional single study type. Must be one of
|
is_preprint |
Optional logical: preprints only, or non-preprints only. |
since, until
|
Optional publication-date bounds. |
min_score |
Optional score floor. |
emailed |
Optional logical: filter on whether the record has already gone out in a digest. |
apply_threshold |
Apply the question's own digest threshold. |
n_max |
Maximum records to return. |
page_size |
Records per request. |
Results are paginated by the server; this function follows the pages for
you up to n_max. Offset pagination is not a database snapshot. If the
result count changes or a record repeats during paging, the request stops
with researchR_pagination_changed; retry after collection has finished.
Malformed responses or structured metadata raise researchR_protocol_error
rather than being silently replaced by empty results.
A tibble of records, one row each, with pub_date as Date and
first_seen as UTC POSIXct. provenance is a list column of source
contributors; it is empty for servers that do not expose provenance.
Scoring orders results without deleting below-threshold records.
apply_threshold = TRUE
reproduces what the curated feed and the digest email show for questions
in digest_mode = "threshold"; the default FALSE gives you the complete
stored set. RSS feeds, including all.xml, have a server-side item limit.
Configured server retention can remove old records.
conn <- rr_connect() rr_records(conn, "glp1_mash", study_type = "rct", since = Sys.Date() - 90)conn <- rr_connect() rr_records(conn, "glp1_mash", study_type = "rct", since = Sys.Date() - 90)
Update a question's signals configuration
rr_set_signal_config( conn, question_id, sources = NULL, min_confidence = NULL, instant_alerts = NULL, feed_categories = NULL, scrape_targets = NULL, api_keys = NULL )rr_set_signal_config( conn, question_id, sources = NULL, min_confidence = NULL, instant_alerts = NULL, feed_categories = NULL, scrape_targets = NULL, api_keys = NULL )
conn |
A |
question_id |
Question id. |
sources |
Named list of signal-source toggles. |
min_confidence |
Minimum confidence for the signals digest and the
default signals feed. One of |
instant_alerts |
If |
feed_categories |
Named list of curated-feed category toggles. |
scrape_targets |
List of scrape target definitions. Optional list names are ignored. |
api_keys |
Named list of plaintext signal API keys. Write-only, as
with |
Invisibly, the configuration as the server now holds it.
## Not run: rr_set_signal_config(rr_connect(), "glp1_mash", min_confidence = "reported", instant_alerts = TRUE) ## End(Not run)## Not run: rr_set_signal_config(rr_connect(), "glp1_mash", min_confidence = "reported", instant_alerts = TRUE) ## End(Not run)
Read a question's signals configuration
rr_signal_config(conn, question_id)rr_signal_config(conn, question_id)
conn |
A |
question_id |
Question id. |
A named list: enabled signal sources, minimum confidence, whether instant alerts are on, enabled feed categories, scrape targets, and which signal API keys are stored (never the keys themselves).
rr_signal_config(rr_connect(), "glp1_mash")rr_signal_config(rr_connect(), "glp1_mash")
The signals track carries non-publication intelligence — grant awards, trial registrations and results postings, regulatory decisions, SEC filings, patents, press and investor-relations items. It runs off the same questions as the literature track but is a deliberately separate stream, because these objects are earlier and less reliable than papers.
rr_signals( conn, question_id, signal_type = NULL, min_confidence = NULL, since = NULL, n_max = Inf, page_size = 200 )rr_signals( conn, question_id, signal_type = NULL, min_confidence = NULL, since = NULL, n_max = Inf, page_size = 200 )
conn |
A |
question_id |
Question id. |
signal_type |
Optional single signal type; see
|
min_confidence |
Optional minimum confidence tier. |
since |
Optional lower bound on |
n_max |
Maximum signals to return. |
page_size |
Signals per request. |
A tibble of signals. extra is parsed from JSON into a list
column of type-specific fields. Malformed extra JSON raises
researchR_protocol_error; absent or null metadata becomes an empty list.
Every signal carries a confidence: "authoritative" (registries,
regulators, SEC), "reported" (official but preliminary, e.g. a topline
readout), or "unverified" (scraped press or IR — a lead, not a fact).
min_confidence is a floor over that ordering, not an equality test:
"reported" returns reported and authoritative.
rr_signals(rr_connect(), "glp1_mash", min_confidence = "reported")rr_signals(rr_connect(), "glp1_mash", min_confidence = "reported")
A surveillance system's worst failure is a source that quietly stopped returning anything, because the symptom — fewer results — looks exactly like a quiet week in the literature. Every pipeline run writes a heartbeat row per source; this reports the last successful fetch, the last one that actually returned rows, and the error count.
rr_source_health(conn, stale_days = 3)rr_source_health(conn, stale_days = 3)
conn |
A |
stale_days |
A source with no successful fetch in this many days is
flagged |
Watch last_nonempty as much as stale: a source can be answering
happily while its query has silently stopped matching anything.
A tibble, one row per source and track.
rr_source_health(rr_connect())rr_source_health(rr_connect())
Upserts by question_id: an existing question with that id is updated in
place, otherwise a new one is created. Concepts are replaced wholesale, so
pass the full set you want.
rr_upsert_question( conn, question_id, label, concepts, sources = list(), api_keys = list(), study_filter = character(0), from_days_back = 7L, enabled = TRUE, digest_mode = c("all", "threshold"), min_score = 0, delivery = "file", email_enabled = FALSE, email_cron = "0 7 * * 1", email_to = character(0) )rr_upsert_question( conn, question_id, label, concepts, sources = list(), api_keys = list(), study_filter = character(0), from_days_back = 7L, enabled = TRUE, digest_mode = c("all", "threshold"), min_score = 0, delivery = "file", email_enabled = FALSE, email_cron = "0 7 * * 1", email_to = character(0) )
conn |
A |
question_id |
Question id (a slug, e.g. |
label |
Human-readable label. |
concepts |
List of |
sources |
Named list of per-source toggles, e.g.
|
api_keys |
Named list of plaintext per-source API keys. Write-only. |
study_filter |
Character vector of publication types to restrict to. Empty means maximum recall. |
from_days_back |
Lookback window, in days. |
enabled |
Whether the question is fetched at all. |
digest_mode |
|
min_score |
Score cutoff used when |
delivery |
Character vector of delivery channels, e.g.
|
email_enabled |
Whether this question emails a digest. |
email_cron |
Cron expression for this question's own email cadence. |
email_to |
Character vector of recipient addresses. |
Invisibly, the question as the server now holds it.
api_keys takes plaintext keys and is write-only: the server encrypts
them at rest and never returns them. Leave a source out to keep its stored
key untouched; pass the literal "__CLEAR__" to delete one.
## Not run: rr_upsert_question( rr_connect(), "glp1_mash", "GLP-1 receptor agonists in MASH", concepts = list( rr_concept("drug", c("semaglutide", "tirzepatide", "GLP-1")), rr_concept("disease", c("MASH", "NASH", "steatohepatitis"))), sources = list(pubmed = TRUE, europepmc = TRUE), email_enabled = TRUE, email_cron = "0 7 * * 1", email_to = "[email protected]") ## End(Not run)## Not run: rr_upsert_question( rr_connect(), "glp1_mash", "GLP-1 receptor agonists in MASH", concepts = list( rr_concept("drug", c("semaglutide", "tirzepatide", "GLP-1")), rr_concept("disease", c("MASH", "NASH", "steatohepatitis"))), sources = list(pubmed = TRUE, europepmc = TRUE), email_enabled = TRUE, email_cron = "0 7 * * 1", email_to = "[email protected]") ## End(Not run)