BMJ Cloudflare hackathon

How the journal picker works

A practical tour of the request path, vector data, storage choices, bounded search, caching, and deployment. It starts with what the browser does and works backwards to the offline data build.

The system in one picture

The public site is one Worker and the similarity engine is another. They communicate through a private Cloudflare service binding. The API embeds the manuscript once, compares it with a small derived index, and returns HTML-ready data to the UI.

Public edge

Browser

Submits a title and abstract to the custom domain over HTTPS.

Worker 1

picker-ui

Validates the form, calls the private API, and renders HTML with Hono and htmx.

Worker 2

picker-api

Embeds the query, ranks journal centroids, then searches only the leading journal shards.

Bindings

AI + assets

Workers AI creates the query vector; immutable assets provide derived indexes and map data.

74journal centroid comparisons for every recommendation
32,814source portfolio articles represented by the offline-derived assets
384dimensions in the shared BGE vector space
3 shardsmaximum journal scope used for related-article search per request

Follow one recommendation request

  1. The browser posts the form. htmx sends the title, abstract, and selected matching basis to /picker/analyse.
  2. picker-ui parses and validates it. A missing title is rejected locally. The UI makes exactly one application-service call.
  3. The service binding invokes picker-api. This is Worker-to-Worker communication, not a fetch to a public API hostname.
  4. Workers AI produces one BGE embedding. The same 384-number query vector is reused for journal ranking, related articles, and map placement.
  5. The API scans 74 journal centroids. Each centroid is the normalized mean of the journal's existing article vectors.
  6. The API opens only the top three article shards. It ranks articles inside those journals and returns the closest eight.
  7. The UI renders the result. Suggested journals, the map, related articles, and optional feedback arrive as server-rendered HTML.
The important scaling boundary: the request never constructs author analytics, scans all authors, or compares all 32,814 articles with every journal. The full corpus is used during offline builds; the live path uses compact, precomputed structures.

Why the vector architecture is split

  1. R2 source datasetsarticle metadata, full text, and the full BGE vector snapshot
  2. Local verified mirrorordered IDs are checked against the row-major float matrix
  3. Derived build74 centroids, 74 article shards, and one PCA map
  4. Worker static assetsimmutable, versioned together with picker-api

Offline work is allowed to be broad

Rebuilding indexes can read the whole portfolio because it runs deliberately, outside a user request. It validates that the article ID at row N still belongs to vector row N; a mismatch stops the build.

Online work must be bounded

Journal ranking always has 74 comparisons. Related-article ranking is deliberately scoped by the first result: only the three best journal shards are loaded. That makes the extra work explainable and prevents a feature request from quietly restoring a full-portfolio scan.

One model space

The query, article vectors, centroids, and map all use @cf/baai/bge-small-en-v1.5. Older SPECTER map coordinates are not reused because numbers from different embedding models are not comparable.

What the journal map means

The map starts with the same 74 BGE centroids used for ranking. Principal component analysis (PCA) projects those 384-dimensional points into two dimensions during the offline build. Nearest-neighbour lines reveal local relationships; numbered circles identify the current recommendations; a diamond places the submitted article.

Useful for

Seeing neighbouring journal communities, broad disciplines, and where a manuscript sits relative to the portfolio.

Not useful for

Reading exact similarity from pixel distance. A 2D projection necessarily discards information from the original 384 dimensions.

What persists where

PlaceWhat lives thereOn the live request path?
R2Canonical article metadata, full text, and full vector datasets with catalogs and checksums.No. It is the published source and rebuild origin.
picker-api static assetsJournal centroids, per-journal article vector shards, sidecars, and PCA map parameters.Yes, but only through the Worker's private ASSETS binding.
picker-ui static assetsPico CSS, htmx, and this architecture page.Yes.
D1 (optional)Picker activity and agree/disagree feedback when configured.Only for logging and admin views; never required for recommendations.
Local filesRebuild mirror, scripts, tests, and generated deployment assets.No.

D1 is Cloudflare's managed SQL database with SQLite semantics; it is different from putting a writable .sqlite3 file inside a Worker. The Worker's deployed filesystem is not used as mutable application storage.

Caching, without mystery

What the application caches itself

The tiny centroid matrix and map snapshot are parsed once per warm Worker isolate. Article shards use a six-entry least-recently-used cache, so repeated queries in similar fields avoid reparsing while a long-lived isolate cannot gradually retain the full corpus.

What happens on a cold start

A new isolate reads the required immutable assets again. Correctness never depends on a cache being warm; a miss causes bounded extra I/O, not a portfolio-wide analytics rebuild.

What is deliberately not cached

Submitted manuscript text and query vectors are not written into a vector cache or R2 object by the recommendation API. Optional D1 activity logging is a separate feature and can be disabled.

How optional features fail

If related-article assets or map data cannot load, the API still returns the core journal ranking. If D1 is unavailable, matching still works and only activity recording is skipped.

Deployment topology

ComponentExposureBindings
picker-uijournal-picker.bmj-cloudflare-hackathon.comPICKER_API, ASSETS, optional D1 and admin secrets
picker-apiPrivate; workers.dev disabledAI, ASSETS
R2 data hostsPublic dataset domainsPublished catalogs and objects; not application RPC endpoints

The custom domain belongs to picker-ui. Cloudflare manages the hostname routing and certificate. The UI calls the API using the configured service binding, so the backend does not need a public DNS record.

Rebuild, test, deploy

The account target is 3984c21be244d542dcd9f617b249db50. Credentials remain in local ignored files or Wrangler's authenticated profile; they do not belong in source or this page.

# Rebuild the bounded derived assets
python3 scripts/build_journal_centroids.py
python3 scripts/build_related_article_shards.py
python3 scripts/build_journal_map.py

# Validate and test
python3 scripts/validate_hackathon_assets.py
(cd workers/picker-api && npm test && npm run check && npm run dry-run)
(cd workers/picker-ui && npm test && npm run check && npm run dry-run)

# Deploy backend first, then the public UI
(cd workers/picker-api && npx wrangler deploy)
(cd workers/picker-ui && npx wrangler deploy)

Healthy means: the picker loads, one real request returns journals, related articles link to metadata, the map renders, and /architecture remains reachable.

Cloudflare concepts used here