Product: papers.highsignal.app
Multi-source academic-paper data platform on ClickHouse. 488k papers across arxiv, OpenReview, bioRxiv, medRxiv — with semantic search, citation graph PageRank, peer-review aggregations, MLX/spaCy auto-tagging, and HighSignal-style insight surfaces (sleepers, hot now, papers-like-this, authors-by-tag).
Stack: ClickHouse 24.10 (Dockerized) · FastAPI · Astro 5 + React + Tailwind
- shadcn/ui · sentence-transformers · MLX (Qwen2.5-3B-4bit) · spaCy v2.
- 488,491 papers ingested, ~1.05M paper→paper edges, full-corpus PageRank computed, all papers embedded (all-MiniLM-L6-v2, 384-dim) and clustered into 64 semantic clusters.
- Runtime is ClickHouse-only. Postgres remains as an optional
dependency for legacy CLI commands (e.g.
ingest,download-pdfs) but is not required for the API, the frontend, or any current pipeline. - The Astro frontend is fully wired to the FastAPI backend. See
DEPLOY.md. - GitHub: https://github.com/High-Signal-App/research-papers
If you have the researchpapers_data_*.tar.gz dump in hand, this is the
fastest path to a running system. Needs Docker + uv.
git clone https://github.com/High-Signal-App/research-papers
cd researchPapers
./scripts/deploy.sh /path/to/researchpapers_data_*.tar.gz
# CH on :8123, FastAPI on :8000
# Frontend (separate terminal)
cd web && npm install && npm run dev # http://127.0.0.1:4321For LAN/CDN deployments, see DEPLOY.md.
A full rebuild takes hours. Outline:
docker compose up -d clickhouse # CH on :8123
uv sync # python deps
uv run papers select-top --n 400000 # OpenAlex top-N CS metadata
uv run papers ingest-openreview # NeurIPS/ICLR + reviews
uv run papers ingest-biorxiv # bioRxiv + medRxiv
uv run papers backfill-references # paper→paper edges
uv run papers refresh-metadata # arxiv API fixups for top papers
uv run papers pagerank-full # writes paper_scores_v2
uv run papers embed # all-MiniLM-L6-v2 (~17 min)
uv run papers cluster-embeddings # MiniBatchKMeans, 64 clusters
uv run papers spacy-tag-v2 # noun-chunk tags (CPU)
uv run papers mlx-tag-v3 --shards 3 # premium subset, MLX on Apple Silicon
uv run papers export-ch # write web/public/data/*.json
uv run papers api-serve --host 0.0.0.0 --port 8000 ┌──────────────────────┐
arxiv API ──┐ │ ClickHouse (papers) │
OpenAlex ───┼──► ingesters ───► │ papers │
OpenReview ─┤ │ paper_tags │
bioRxiv ────┘ │ references_paper │
│ citation_history │
│ paper_embeddings │
│ paper_clusters │
│ paper_metadata_v2 │ ◄── overlay
│ paper_scores_v2 │ ◄── overlay
└──────────┬───────────┘
│
┌─────────────────┼─────────────────┐
▼ ▼ ▼
ch_exports.py pagerank_full.py refresh_metadata.py
(JSON for FE) (scipy.sparse) (arxiv API title fix)
│ │
▼ ▼
web/public/data/*.json paper_metadata_v2
│
▼
Astro 5 + React (web/)
▲
│
FastAPI (src/researchpapers/api.py)
/search /papers /sleepers /hot /similar
/semantic-search /tags /authors /reviews
All under the FastAPI server (uv run papers api-serve):
| Endpoint | What |
|---|---|
GET /healthz, GET /stats |
health + corpus stats |
GET /search?q=... |
full-text-ish search via CH LIKE |
GET /papers/{paper_id} |
canonical paper detail (joined w/ overlays) |
GET /semantic-search?q=... |
encodes q via MiniLM, cosineDistance over paper_embeddings |
GET /sleepers |
papers with late citation spikes |
GET /hot |
recent papers gaining attention |
GET /similar/{paper_id} |
nearest neighbours by embedding |
GET /tags/top-rated |
tag → mean rating cross-join (uses OpenReview scores) |
GET /tags/{tag} |
drilldown: papers under a tag |
GET /authors/by-tag/{tag} |
top authors per topic |
GET /authors/by-id/{openalex_id} |
full author profile |
GET /reviews/top-rated |
best-reviewed OpenReview papers |
GET /rag/status |
server-side Knowledgebase RAG wiring status, without exposing secrets |
POST /rag/query |
cited answers over the seeded research-papers-cs-cited1000-all Knowledgebase domain |
The website includes a "Research answer API" panel. In local same-host mode it
calls FastAPI POST /rag/query; on Cloudflare Pages it calls the Pages Function
at /api/rag/query. Both paths keep the Knowledgebase service key on the server.
The live path defaults to semantic retrieval plus synthesized cited answers
through Knowledgebase/free-ai. The Pages Function also has a bundled-data
fallback over web/public/data/*.json, so the public demo API returns cited
answers before the Knowledgebase secret is configured. FastAPI returns 503
without the key because it does not serve the static website export.
Required runtime env for the full Knowledgebase RAG path:
export RAG_SERVICE_KEY="<service-key>"
export RAG_SERVICE_URL="https://knowledgebase.sarthakagrawal927.workers.dev"
export RAG_DOMAIN="research-papers-cs-cited1000-all"Seed the demo domain from high-citation OpenAlex Computer Science papers with local BGE embeddings:
RAG_SERVICE_KEY="<service-key>" uv run python scripts/seed_openalex_cs_rag.py \
--live \
--vector-ingest \
--local-embedding-backend sentence-transformers \
--domain research-papers-cs-cited1000-all \
--state data/openalex-cs-cited1000-all-bge-st-kb-seed-state.json \
--openalex-filter 'cited_by_count:>999,is_retracted:false,primary_topic.field.id:17' \
--max-records 11116This seeds the current cited_by_count > 999 OpenAlex Computer Science slice:
11,116 primary-CS OpenAlex works as of June 24, 2026. Query-time embedding
still runs through Knowledgebase/Cloudflare; ingestion embeddings are generated
locally.
The frontend also ships /paths, a static reading-path catalog for deciding
what to read next. It includes source-attributed researcher buckets such as the
public Sutskever/Carmack mirror, Karpathy-style LLM systems, LeCun world-model
work, and CHAI/Russell-aligned safety reading. The catalog is copyright-safe by
design: it stores bibliographic metadata, source links, and original short notes
only; it does not host PDFs, copy abstracts, or reproduce long excerpts.
Once ./scripts/deploy.sh has the stack running (FastAPI on :8000, CH on
:8123), here is how to actually pull insights out of the corpus.
# Corpus stats — confirms data shape: 488k papers across 4 sources, ~1.05M
# paper→paper edges, ~478k embedded.
curl -s http://127.0.0.1:8000/stats | jq
# Substring search across titles + authors.
curl -s "http://127.0.0.1:8000/search?q=attention+is+all+you+need&limit=5" | jq
# Canonical detail for one paper (with corrected title from paper_metadata_v2
# and PageRank from paper_scores_v2 already merged in).
curl -s http://127.0.0.1:8000/papers/arxiv:1706.03762 | jq
# Semantic search — encodes the query through MiniLM and ranks by cosine
# distance against all 478k embeddings.
curl -s "http://127.0.0.1:8000/semantic-search?q=mixture+of+experts+routing&limit=10" | jq
# "Sleepers" — papers that were quiet for years then woke up. Useful for
# finding under-cited fundamentals that suddenly mattered.
curl -s "http://127.0.0.1:8000/sleepers?limit=20" | jq
# "Hot" — recent papers gaining citations fast. The freshness signal.
curl -s "http://127.0.0.1:8000/hot?limit=20" | jq
# "Similar" — nearest neighbours by embedding for a given paper.
curl -s "http://127.0.0.1:8000/similar/arxiv:1706.03762?limit=10" | jq
# Tag → mean OpenReview rating. The "what topics get accepted at top venues"
# signal. Comes from a cross-join of paper_tags × openreview_reviews.
curl -s "http://127.0.0.1:8000/tags/top-rated?limit=20" | jq
# Drilldown: all papers under a single tag, ordered by PageRank.
curl -s "http://127.0.0.1:8000/tags/transformers?limit=20" | jq
# Author profile by OpenAlex ID (only populated for top-2000 refreshed papers).
curl -s http://127.0.0.1:8000/authors/by-id/A5024211345 | jqThe API only exposes the curated insights; for ad-hoc questions, talk to CH
directly. Connect with docker exec -it researchpapers_ch clickhouse-client --user papers --password papers -d papers, or hit HTTP on :8123.
-- Source breakdown
SELECT source, count() FROM papers GROUP BY source ORDER BY count() DESC;
-- arxiv 399902 │ biorxiv 64829 │ openreview 30327 │ medrxiv 17332
-- Canonical view of one paper, with title/year corrections applied.
SELECT
p.paper_id,
effective_year(p.source, p.arxiv_id, p.submitted_date) AS year,
p.citation_count,
coalesce(nullIf(m.title, ''), p.title) AS title
FROM papers p
LEFT JOIN paper_metadata_v2 m USING (paper_id)
WHERE p.paper_id = 'arxiv:1706.03762';
-- arxiv:1706.03762 │ 2017 │ 6551 │ Attention Is All You Need
-- Top 10 by full-corpus PageRank.
SELECT
s.paper_id,
round(s.pagerank, 6) AS pr,
coalesce(nullIf(m.title, ''), p.title) AS title
FROM paper_scores_v2 s
LEFT JOIN papers p USING (paper_id)
LEFT JOIN paper_metadata_v2 m USING (paper_id)
ORDER BY s.pagerank DESC LIMIT 10;
-- Top arxiv 2024 papers by citations (year corrected via UDF).
SELECT
p.paper_id, p.citation_count,
coalesce(nullIf(m.title, ''), p.title) AS title
FROM papers p LEFT JOIN paper_metadata_v2 m USING (paper_id)
WHERE p.source = 'arxiv'
AND effective_year(p.source, p.arxiv_id, p.submitted_date) = 2024
ORDER BY p.citation_count DESC LIMIT 10;
-- Nearest neighbours of any paper, by embedding.
WITH q AS (SELECT embedding FROM paper_embeddings WHERE paper_id = 'arxiv:1706.03762' LIMIT 1)
SELECT e.paper_id, round(cosineDistance(e.embedding, (SELECT embedding FROM q)), 4) AS d,
coalesce(nullIf(m.title, ''), p.title) AS title
FROM paper_embeddings e
LEFT JOIN papers p USING (paper_id)
LEFT JOIN paper_metadata_v2 m USING (paper_id)
WHERE e.paper_id != 'arxiv:1706.03762'
ORDER BY d ASC LIMIT 10;
-- Most common spaCy noun-chunk tags across the corpus.
SELECT arrayJoin(tags) AS tag, count() AS c
FROM paper_tags WHERE tagger = 'spacy_v2'
GROUP BY tag ORDER BY c DESC LIMIT 20;
-- language models │ machine learning │ deep learning │ LLMs │ ...
-- All papers in semantic cluster N (0..63), top-cited first.
SELECT p.paper_id, p.citation_count,
coalesce(nullIf(m.title, ''), p.title) AS title
FROM paper_clusters c
JOIN papers p USING (paper_id)
LEFT JOIN paper_metadata_v2 m USING (paper_id)
WHERE c.cluster_id = 17
ORDER BY p.citation_count DESC LIMIT 20;cd web && npm install && npm run dev (then http://127.0.0.1:4321) is the
visual entry point. Each table is a React island bound to either a static
JSON in web/public/data/ (built by papers export-ch) or a live FastAPI
endpoint (search, semantic search, similar). The /digest page is the
HighSignal-style summary. The /paths page is a static curated reading-path
surface with interactive filters, ordered paper briefs, provenance notes,
and JSON/BibTeX/RIS/Markdown export controls.
The dashboard's tables (sleepers, hot, top-authors, communities, etc.) read
from web/public/data/*.json. To regenerate after new ingestion or a
re-tag run:
uv run papers export-ch # rewrites web/public/data/*.json from CH
cd web && npm run build # rebuild the static bundleThe live endpoints (search, semantic-search, similar, sleepers, hot, etc.) always read from CH directly — no rebuild needed.
One command (16 GB M1 Pro, RAM-aware):
docker compose up -d clickhouse
uv run papers warm-update --build-web # overlays + exports + static build
uv run papers api-serve --lean # API without resident ML modelsuv run papers api-serve --lean # default: low-RAM API (~400 MB saved)
uv run papers warm-update # sequential overlay refresh
uv run papers export-ch # write JSON files for the static FE
uv run papers refresh-metadata # pull arxiv API + OpenAlex into paper_metadata_v2
uv run papers enrich-citations # Semantic Scholar counts → citation_overlay_v2
uv run papers refresh-abstracts # fix contaminated arxiv abstracts → abstract_overlay_v2
uv run papers build-author-graph # authors_v2 + paper_authorships_v2
uv run papers pagerank-full # full-corpus PageRank → paper_scores_v2
uv run papers embed # all-MiniLM-L6-v2 embed → paper_embeddings
uv run papers cluster-embeddings # MiniBatchKMeans → paper_clusters
uv run papers snapshot-citations # append to citation_history
uv run papers spacy-tag-v2 # POS-only noun-chunk tagger (arxiv)
uv run papers spacy-tag-source --source openreview
uv run papers mlx-tag-v3 --shards 3 # grouped-prompt MLX, premium subset
uv run papers status # progress / row countsFull list: uv run papers --help.
Some primary data has known defects: OpenAlex returns latest revision dates for arxiv preprints (so "Attention Is All You Need" shows 2025 instead of 2017), and a handful of arxiv IDs have cross-contaminated titles/abstracts in OpenAlex's index. We patch around these with overlay tables and two ClickHouse UDFs:
paper_metadata_v2(ReplacingMergeTree) — corrected titles + author OpenAlex IDs pulled from the arxiv API. Populated bypapers refresh-metadata. Joined in viaLEFT JOIN ... ON paper_idwithCOALESCE(nullIf(m.title, ''), p.title).citation_overlay_v2(ReplacingMergeTree) — Semantic ScholarcitationCountfor top papers. Populated bypapers enrich-citations. Preferred over OpenAlex counts in hot/sleepers/search when present. API exposescitation_sourceprovenance (semantic_scholar|openalex_refresh|openalex).abstract_overlay_v2(ReplacingMergeTree) — authoritative arxiv abstracts for contaminated records. Populated bypapers refresh-abstracts. Search and paper detail use corrected text; pass--reembedto refresh semantic-search embeddings for corrected papers.authors_v2+paper_authorships_v2— canonical author identities (OpenAlex IDs + inferred community/cluster buckets). Built bypapers build-author-graph. API:/authors/v2/{id},/coauthors,/authors/resolve?name=....paper_scores_v2(ReplacingMergeTree) — full-corpus PageRank, written bypapers pagerank-full. Used becausepapers.pagerank_scorecan't beALTER UPDATEd cheaply (partition key onsubmitted_date).effective_year(source, arxiv_id, submitted_date)— parses the YYMM prefix of arxiv IDs (2410.xxxxx→ 2024) and only falls back totoYear(submitted_date)for non-arxiv sources or malformed IDs. Defined inclickhouse/init/02_functions.sqlso it survives container restarts;deploy.shre-applies it after restores.effective_date(source, arxiv_id, submitted_date)— same idea, returns aDatefor chart axes.
Heavy jobs stream from ClickHouse in chunks and wait for free RAM between steps. Defaults are tuned for an M1 Pro 16 GB machine with other apps open:
| Setting | Default | Why |
|---|---|---|
API --lean |
on | semantic search uses a one-shot encoder subprocess |
embed --batch-size |
64 | was 256 |
spacy-tag-v2 batch |
3000 papers, max 2 workers | was 25000 / up to 8 workers |
cluster-embeddings |
chunked partial_fit | was load-all-478k into RAM |
pagerank-full |
streamed edge reads | was load-all edges at once |
Use papers warm-update to run overlay jobs sequentially without peaking
multiple model loads. For MLX tagging, keep --total-shards high and run one
shard at a time.
src/researchpapers/
api.py FastAPI server (all read endpoints)
cli.py typer entrypoint (uv run papers ...)
ch_db.py ClickHouse connection helper
ch_exports.py CH → JSON exports for the static frontend
exporter.py legacy multi-source exporter (CH-only)
refresh_metadata.py arxiv API + OpenAlex → paper_metadata_v2
semantic_scholar_enrichment.py S2 citation counts → citation_overlay_v2
arxiv_abstract_refresh.py arxiv abstract fixups → abstract_overlay_v2
author_graph.py canonical authors + coauthor graph
overlays.py shared overlay SQL + table DDL
ram.py RAM waits + M1 16 GB profile
warm_update.py one-command overlay refresh
encode_query.py one-shot query encoder (CLI `papers encode-query`); API loads the encoder in-process lazily
pagerank_full.py scipy.sparse PageRank → paper_scores_v2
embed.py sentence-transformers → paper_embeddings
cluster_embeddings.py MiniBatchKMeans → paper_clusters
mlx_tag_v3.py MLX grouped-prompt tagger (Apple Silicon)
noun_tag_v2.py spaCy v2 POS-only noun-chunk tagger
llm_tag.py LM Studio / Ollama / MLX HTTP tagger
openalex.py OpenAlex client + top-cited fetcher
openreview_ingest.py NeurIPS/ICLR API + reviews
biorxiv_ingest.py bioRxiv + medRxiv + chemRxiv
citation_history.py snapshots → citation_history
highsignal_report.py sample HighSignal-style report
watcher.py auto re-analytics loop
clickhouse/init/
01_schema.sql papers, paper_tags, references_paper, ...
02_functions.sql effective_year / effective_date UDFs
03_overlays.sql citation/abstract/author overlay tables
migrations/ legacy Postgres migrations (kept for cold restore)
scripts/
deploy.sh unpack dump, start CH, apply UDFs, serve API
dump_data.sh tar CH volume + web/public/data/ exports
migrate_pg_to_ch.py one-shot Postgres → ClickHouse migrator
web/
src/pages/index.astro dashboard with React islands
src/pages/digest.astro HighSignal-style digest page
src/pages/paths.astro curated reading paths shell
src/components/ shadcn/ui + TanStack tables + charts + reading path catalog
src/data/reading-paths.ts repo-local reading-path content
public/data/*.json exported aggregations
public/api-config.js runtime API base override
DEPLOY.md three deployment shapes (host / LAN / CDN)
- OpenAlex citation undercount is mitigated for top papers via
papers enrich-citations→citation_overlay_v2. Full-corpus S2 backfill remains out of scope. - Cross-contaminated OpenAlex abstracts are mitigated via
papers refresh-abstracts→abstract_overlay_v2. Run with--reembedafter large refreshes to update semantic-search vectors. - Author disambiguation is expanded via
papers build-author-graphintoauthors_v2. Inferred buckets (community/cluster) cover high-citation papers without OpenAlex IDs; common surnames may still collide. - No Vercel/CF deploy yet. The static FE builds clean (see
DEPLOY.md) but hasn't been pushed to a CDN — the user prefers same-host deploy unless going public. - OrbStack 2.1.3 + macOS 26 instability. Apple
Virtualization.framework occasionally kills the OrbStack VM backend
silently. Workaround:
~/.orbstack/bin/orb startfrom the CLI (the GUI doesn't always re-spawn). Linux Docker daemons are fine.
.env is optional. CONTACT_EMAIL is read by the polite-scraping headers;
POSTGRES_URL is only needed for the few legacy CLIs that still touch
Postgres (ingest, download-pdfs, extract-urls). See .env.example.