Spaces:
Sleeping
Download app/ingest/README.md from Conradf/nullmap-api: direct link, hf CLI and curl.
- Browser
- Download file 11.5 kB
-
https://huggingface.co/spaces/Conradf/nullmap-api/resolve/main/app/ingest/README.md
- Command line
-
hf download hf://spaces/Conradf/nullmap-api/app/ingest/README.md
-
curl -L -o README.md https://huggingface.co/spaces/Conradf/nullmap-api/resolve/main/app/ingest/README.md
Public S3 ingestion
OpenAlex literature comes exclusively from the anonymous public bucket
s3://openalex/data/parquet/works/. No OpenAlex API key, AWS account or signed URL
is used. ClinicalTrials.gov still uses its separate, keyless API. The snapshot
contains bibliographic metadata, citation references and available abstracts;
it is not a collection of full-text PDFs. The normalizer keeps each work's PubMed
Central ID when present; the API uses it at query time to read open-access full text
from Europe PMC (see fulltext.py). No full text is stored by ingestion.
Run these commands from backend/ on the batch host, such as the sponsor
EC2 machine. Python reads selected Parquet columns over HTTPS, retains matching
rows there, computes embeddings there, and writes to Elasticsearch. Serving
FastAPI there also puts query embedding inference there. OpenAI handles PICO
parsing and bounded evidence extraction through the existing API integration.
The browser only sends queries and receives results; it never scans S3.
Scope and planning
The default hypertension-kidney profile includes the union of those fields:
hypertension topics, the Nephrology subfield, additional renal topics, and
matching terms in titles, reconstructed abstracts and topic names. Exact rules
and the registry query are versioned in scope.py. All years and work
types are retained, including records without abstracts. Nonprimary works are
retained for discovery but excluded from primary-study statistical pools.
Missing abstracts do not imply missing results.
The opt-in hypertension-kidney-pubmed profile is the same topical scope restricted
to PubMed-indexed works (a PMID in ids, or pubmed in indexed_in). Update-date
partitions differ enormously in quality: in a 2026-09-19 check, a 2025-10 part had
0.0% PMIDs and 4.9% DOIs, a 2026-01 part 11.5% and 99.6%, and the identifier-poor
parts contain many records whose title and abstract belong to different works.
Registry linking matches on PMID and full text needs a PMCID, so a corpus built from
such parts cannot link. --largest-first does not avoid them; it selects by bytes.
The profile is recorded in checkpoints and provenance as its own scope. The Parquet
snapshot carries no pmcid in ids; the normalizer recovers it from PMC location URLs.
Completion means all matches under those rules in the supplied snapshot. Incomplete metadata, terminology and source coverage prevent a promise of every relevant study worldwide. The snapshot is a dated release, not a live feed.
.venv/bin/python -m app.bootstrap --plan
Planning is the default when neither --plan nor --run is supplied. It reads
only the manifest and prints its hash, date, part count, physical byte budget
and target index. On 2026-09-19 the public works manifest described 2,446 parts,
724,970,323,127 bytes, dated 2026-06-26.
Partitions are update dates, not topics. Broad topic coverage requires scanning all parts. Column projection can reduce traffic; the physical size budget is conservative, not a measurement of transferred bytes or required output disk space. The 5 GB default refuses a full scan instead of silently importing a partial corpus.
Full build on remote compute
Install dependencies using the root setup guide. Configure
Elasticsearch and models in the remote host's backend/.env. For an auditable
fresh corpus, choose a dedicated index such as
ELASTIC_INDEX=studies-hypertension-kidney-s3. Existing indexes are preserved and
updated by ID; importing does not delete unrelated older records.
Choose an existing persistent mount with room for the retained JSONL and index. Inspect storage first: the sponsor machine's root disk may be small. The code does not format or mount disks. After inspecting the plan, an example run on an adequately provisioned batch host is:
.venv/bin/python -m app.bootstrap --run \
--data-dir /mnt/research/nullmap/2026-06-26 \
--max-snapshot-bytes 750000000000
Replace that directory with the actual persistent mount. Set the budget from the printed manifest and host capacity. Repeat the same command and directory to resume. There is no default year or record cap. The registry condition query includes all statuses and study types and exhausts its cursor. Snapshot records are normalized, weakly classified, embedded, linked across batches and indexed. Bootstrap performs no paid LLM labeling or extraction.
For a bounded smoke test:
.venv/bin/python -m app.bootstrap --plan --max-files 1
.venv/bin/python -m app.bootstrap --run --max-files 1 --max-records 100 \
--registry-limit 20 --data-dir data/s3-smoke
If the selected part exceeds 5 GB, choose an explicit budget after inspecting
its size. A part may contain no scope matches. --max-files, --max-records and
--registry-limit are explicit partial runs. Use a separate directory for the
full run: changing selected files invalidates the scan checkpoint.
To continue after a --max-files N run without rescanning, start a new directory
with --skip-files N (optionally with another --max-files to work in chunks).
Provenance keeps the fingerprint of the whole release. A run that skips parts
never sets complete_snapshot_scope=true, even when every part has been covered
across runs; coverage is then the union of the per-directory checkpoints.
.venv/bin/python -m app.bootstrap --run --skip-registry --skip-files 550 --max-files 30 \
--max-snapshot-bytes 40000000000 --data-dir data/research-s3-continue/p0550-0579
Checkpoints and coverage
Keep each output and its sidecars together:
| Artifact | Purpose |
|---|---|
manifest.json |
Pinned release metadata and part sizes |
openalex.jsonl |
Matched source records and snapshot provenance |
openalex.jsonl.checkpoint.json |
Committed bytes, part, within-part progress and coverage |
ctgov.jsonl and checkpoint |
Registry records, query signature and paging cursor |
*.materialize.json |
Indexed offset, counts, model and target-index signature |
corpus-summary.json |
Stage, declared scope, source completion and index health |
Writes flush before checkpoints advance. Restart rolls back uncommitted JSONL bytes and repeats uncommitted index batches with idempotent IDs. Source, scope, model and target-index signatures prevent incompatible resumes. Capped source files may grow; materialization resumes at its committed offset. A new snapshot release requires a new directory. A remote manifest change detected at scan completion invalidates the completion claim.
complete_snapshot_scope=true means every part in the supplied manifest was
exhausted. A subset manifest establishes coverage only of that subset. The corpus
summary is complete only when the snapshot and registry query both finish.
Limits, interruptions and skipped sources remain explicit. Low disk space stops
the scan with its checkpoint preserved; it does not provision storage.
The importer uses bounded batches and an index-backed linker. It neither loads the whole corpus into RAM nor duplicates all vectors into JSONL. Registry facts override linked paper numbers; canonical links prevent double counting. Metadata-only records are embedded using their available title/text and marked as abstract unavailable in search results.
Reference expansion without API calls
Search expands review citations using already indexed works and their stored vectors, reporting missing references. There are no live OpenAlex lookups. For missing references, collect IDs and run a separate S3 pass into the same Elasticsearch index using a separate output directory:
.venv/bin/python -m app.ingest reference-ids \
--input /mnt/research/nullmap/2026-06-26/openalex.jsonl \
--output /mnt/research/nullmap/missing-references.txt
.venv/bin/python -m app.bootstrap --run --skip-registry \
--work-ids /mnt/research/nullmap/missing-references.txt \
--data-dir /mnt/research/nullmap/reference-backfill \
--max-snapshot-bytes 750000000000
Skip the backfill if the ID file is empty. --work-ids overrides topic filtering
so differently worded references can be recovered. This requires another scan:
S3 has no work-ID search endpoint. Only works in the snapshot can be recovered.
Backfills keep separate provenance and do not claim completion of the main build.
Individual stages and model choices
python -m app.ingest --help exposes snapshot scans, registry fetching,
normalization, classification, embedding, indexing, reference IDs, TEI full-text
extraction, labeling, classifier training and the pagerank pass, which
recomputes within-corpus citation PageRank (references outside the index are
dropped) and bulk-updates the pagerank/authority rank-feature fields used
by retrieval. For example:
.venv/bin/python -m app.ingest snapshot --plan
.venv/bin/python -m app.ingest snapshot --input /data/sample.parquet \
--profile hypertension-kidney --output data/sample.jsonl --resume
.venv/bin/python -m app.ingest fetch ctgov --query 'kidney' --filter '' \
--all --output data/ctgov.jsonl --resume
.venv/bin/python -m app.ingest tei --input /data/tei/ --output data/tei.jsonl
Manifests use { "files": [{ "url": "…", "size_bytes": 123 }] }; the public
meta.content_length form is supported. Local fixtures are supported for tests.
Remote paths accept only unsigned public OpenAlex works URLs. DuckDB installs
its httpfs extension under the output directory on first use.
The tei stage reads GROBID-style TEI XML full text (files or directories of
*.xml) and extracts null-hypothesis evidence with the deterministic, auditable
heuristic in tei.py: a dictionary of statistical terms (confidence
interval, p-value, correlation, effect size, sample size) locates candidate
sentences; the surrounding paragraph is scanned for null confirmations
("no significant difference", non-significant p-values, and confidence intervals
that straddle the null value of 0 for differences/correlations or 1 for
ratios); and the associated numbers are parsed from the body text, tables and
appendix. Each row carries the study fields consumed by statistics.py and
classifier.py (estimate, ci_low/ci_high, p_value, effect_type, n,
result_label, null_score, evidence_span) plus a tei_findings list and a
null_findings subset, each keeping the exact source sentence. Scores are
heuristic, never calibrated probabilities. OpenAlex itself ships bibliographic
Parquet, not TEI; point this stage at a full-text TEI corpus keyed by the same
work identifiers.
The legacy standalone link command loads its input into memory and is suitable
for small slices. Bootstrap uses the bounded index-backed path. For lexical-only
operation use --skip-embeddings and serve with EMBEDDINGS_ENABLED=false.
Keep indexing and query embedding models identical.
The default classifier remains the auditable phrase heuristic. The earlier small LLM-labeled pilot failed evaluation and was not promoted. Labeling and model training remain explicit experiments; see the historical validation record and registry fixture provenance.
Sources: OpenAlex snapshot documentation and the sponsor's OpenAlex README.