This repository keeps the scholar-data database up to date. It does two kinds of work:
- Ingest new datasets - turn raw DataCite/EMDB exports into
Dataset(+ author/identifier) rows and a search index entry. - Enrich existing datasets - layer in topics and citations for datasets that are already in the database, without touching anything else.
All of the root-level scripts read their input from subfolders of
~/Downloads and are non-destructive (insert/upsert/update only) unless
noted otherwise. The initial/ folder holds the original one-time,
full-rebuild scripts (they TRUNCATE before reloading) - see
initial/ (legacy, destructive) before running
anything in there.
You will need the following installed on your system:
- Python 3.8+
- Pip
-
Create a local virtual environment and activate it:
python -m venv .venv #or py -m venv .venv source .venv/bin/activate # linux .venv\Scripts\activate # windows
-
Install the dependencies:
pip install -r requirements.txt
-
Add your environment variables. An example is provided at
.env.examplecp .env.example .env
Make sure to update the values in
.envto match your local setup. Scripts that talk to Postgres needDATABASE_URL; the Meilisearch indexer needsSEARCH_API_URL/SEARCH_API_KEY. -
Format the code:
poe format_with_isort poe format_with_black
You can also run
poe formatto run both commands at once. -
Check the code quality:
poe typecheck poe pylint poe flake8
You can also run
poe lintto run all three commands at once.
Run these in order whenever you have a new batch of raw DataCite/EMDB records to add.
-
format-raw-data.pyInput:Downloads/slim-records/datacite-slim-records/*.ndjsonandDownloads/slim-records/emdb-slim-records/*.ndjson(raw exports). Cleans/normalizes each record into aDataset-ready shape (identifiers, authors, subjects, etc.) and assigns sequential dataset ids. Output:Downloads/database/dataset/*.ndjson(directory is wiped first).⚠️ The starting dataset id (starting_dataset_idinmain()) is hardcoded - bump it toMAX(Dataset.id) + 1before each run or new ids will collide with existing ones. -
fill-database-dataset.pyInput:Downloads/database/dataset/*.ndjson(output of step 1). Bulk-inserts (COPY) intoDataset,DatasetAuthor, andDatasetIdentifier.⚠️ Also has hardcoded starting ids (STARTING_AUTHOR_ID,STARTING_IDENTIFIER_ID) that must be bumped to the current max id + 1 before each run. -
build-meilisearch-datasets-index.pyInput: sameDownloads/database/dataset/*.ndjson. Pushesid/identifier/title/publishedAt/subjects/authors into the existing Meilisearchdatasetindex.
After new datasets exist in the database, re-run
pull-identifier-datasetid-map.py so the enrichment
scripts below can resolve the new DOIs.
These scripts resolve a DOI-style identifier (e.g.
10.57451/lhd.a.nb4a_fraction.75456.1) to the internal integer Dataset.id
and then insert/update - they never truncate a table.
Everything in this section depends on an identifier -> datasetId map.
-
pull-identifier-datasetid-map.py(preferred) - queries the liveDatasettable and dumps{datasetId, identifier, identifierType}toDownloads/pulled-database/dataset-id-identifier/*.ndjson(directory is wiped first). This is the mapmerge-citations.pyandfill-database-topics.pyboth load. Re-run it after adding new datasets so the map includes them. -
build-identifier-datasetid-map.py(legacy fallback) - builds the same kind of map without hitting the database, from the locally processed dataset files inDownloads/database/datasetandDownloads/database-old/dataset(i.e. the output offormat-raw-data.py). Output:Downloads/database/identifier_to_id_map/*.ndjson. Only useful if you don't have DB access when building the map; preferpull-identifier-datasetid-map.pyotherwise.
fill-database-topics.pyInput:Downloads/topics-split/*.ndjson, one topic per line:{"dataset_id": "<doi>", "topic_id", "topic_name", "score", "source", "subfield_id", "subfield_name", "field_id", "field_name", "domain_id", "domain_name"}. Resolvesdataset_idvia the identifier map (fromDownloads/pulled-database/dataset-id-identifier), keeps the highest-scoring topic per dataset, and upserts intoDatasetTopic(ON CONFLICT ("datasetId") DO UPDATE). Requirespull-identifier-datasetid-map.pyto have been run first.
merge-citations.pyInput:Downloads/citations.ndjson, one citation per line:{"dataset_id"/"doi", "citation_link", "source": [...], "citation_weight", "citation_date"}. Resolves identifiers via the same map, merges sources (OR) and the largercitationWeightinto existing(datasetId, citationLink)rows, and inserts new ones (manually assigning ids, then resyncing theCitationid sequence). Unmatched identifiers are written toDownloads/pulled-database/citation-merge-report/unmatched.json. Supports--dry-runto preview changes without writing.
pull-db-for-d-index.pyDumps everythinginitial/generate-d-index-files.pyneeds to recompute the d-index -Datasetjoined withFujiScore,Citation,Mention,NormalizationFactor, andDatasetTopic- intoDownloads/pulled-database/datasets/*.ndjson(directory wiped first).
This is a one-off/custom-tier task (not part of the regular ingest/enrich flow): instead of running FUJI on every one of the ~70M+ datasets, it checks whether a publisher's FUJI scores are consistent enough across a sample to extrapolate that score to the rest of the publisher's datasets.
-
fuji-score-extrapolation-candidates.pyReads the dump frompull-db-for-d-index.py(Downloads/pulled-database/datasets), reservoir-samples low-scoring DOI identifiers per publisher, writesDownloads/pulled-database/fuji-extrapolation-test/fuji-extrapolation-candidates.json, and queues the sampled dataset ids intoFujiJobso a FUJI worker scores them. -
fuji-score-extrapolation-analysis.pyCompares each sampled dataset's pre-sample ("before") score against its freshly computed ("after")FujiScore, and reports per publisher whether scores are consistent enough to extrapolate that score to the rest of the publisher's datasets. Output:Downloads/pulled-database/fuji-extrapolation-test/fuji-extrapolation-analysis.json.(The FUJI scores themselves come from a separate Dockerized FUJI worker that picks up the queued
FujiJobrows and writes straight toFujiScore- that worker isn't one of the scripts in this repo.) -
fuji-score-extrapolation-update.pyReads the analysis file above and splits publishers into consistent (a single agreed "after" score) and inconsistent (no agreement - takes the median ofafterUniqueScoresinstead) groups. Streams the dataset dump (Downloads/pulled-database/datasets) and stages an update for every dataset under a consistent publisher (to that publisher's score), and for datasets under an inconsistent publisher whose existing FUJI score is below 14 (to the publisher's median). Output:Downloads/pulled-database/fuji-scores/*.ndjson(directory wiped first;FujiScore-shaped rows -datasetId,score,evaluationDate,metricVersion,softwareVersion- 10k records per file), ready forfill-database-fuji.pyto import. -
fuji-score-extrapolation-analysis-script-output.pyReads the same analysis file and prints a TypeScriptHARDCODED_REPOSITORY_ID_SCORESmap (publisherId -> score) for every consistent publisher, sorted by dataset count, plus a smallEXTRA_SCORESdict for manually-set overrides (e.g.emdb) - for pasting into application code that wants to skip FUJI entirely for known publishers. -
fill-database-fuji.pyInput:Downloads/pulled-database/fuji-scores/*.ndjson(output offuji-score-extrapolation-update.py). Streams the staging files single-pass and upserts intoFujiScore(ON CONFLICT ("datasetId") DO UPDATE). The similarly-namedinitial/fill-database-fuji.pyis the original, now-superseded version kept for reference.
Scripts under initial/ were used for the original full database build
and TRUNCATE the tables they touch before reloading from scratch (e.g.
fill-database-fuji.py, fill-database-citation.py,
fill-database-d-index.py, fill-database-topics-old.py,
format-topics.py, generate-d-index-files.py, and the original
fill-database-dataset.py/format-raw-data.py). Don't run
them against a live database unless you intend a full rebuild - use the
root-level scripts above for incremental additions instead. They're kept
here for reference, and initial/identifier_mapping.py is still imported by
build-identifier-datasetid-map.py.