Skip to content

Speed up and simplify Solr data loading; fix /status for standalone - #278

Merged
gaurav merged 32 commits into
mainfrom
speed-up-loads
Jul 23, 2026
Merged

Speed up and simplify Solr data loading; fix /status for standalone#278
gaurav merged 32 commits into
mainfrom
speed-up-loads

Conversation

@gaurav

@gaurav gaurav commented Jul 16, 2026

Copy link
Copy Markdown
Collaborator

What & why

Building a NameRes instance loads ~130 GB of Babel synonyms into Solr and ships a backup a worker restores. The old pipeline had three problems: a slow serial load (per-file hard commit + sleep 60), a Solr schema defined twice (loader and the Helm restore script) that had already drifted, and ZooKeeper running for what is a one-shard, one-node job.

This PR reworks the whole flow around a self-contained standalone core: the backup is a whole Solr core — config + schema + index — so restoring it is just "untar into the Solr home and start Solr". No ZooKeeper, no collection creation, no restore-time schema setup. This supersedes #249.

Key changes

  • Single source of truth for the schema: new checked-in configset at data-loading/configsets/name_lookup/conf — two files, ~220 lines, written for NameRes rather than copied from Solr's _default. Field types are carried over verbatim so analysis behaviour is unchanged, the malformed types field is fixed, the index buffer is larger, autoSoftCommit is off and queryResultCache is grown. Everything in it is a decision we made, so there is nothing to reconcile against upstream on a Solr upgrade; the copy it replaced was 44 files, 70 field types and 69 dynamic fields, of which NameRes uses 6 field types and 13 fields (42 of the files were language stopword lists, which Babel synonyms can never use). The schema is now immutable (mutable=false), and schemaless mode is off in solrconfig.xml rather than in configoverlay.json, which Solr's Config API rewrites — so no file in the configset is one Solr can rewrite behind us. An unexpected Babel key is now a hard unknown field error instead of a silently invented field.
  • Fast, guarded load: setup-and-load-solr.sh streams files to Solr in parallel with a single deferred commit (no per-file commit, no sleeps). Because a parallel load of a huge dataset is scary, it counts input documents before loading and compares the increase in Solr's count afterward, with curl --fail on every upload — a drop or a bad file aborts with a non-zero exit. The count is the load-bearing half: Solr answers 200 and indexes nothing for a file of plain text, so --fail alone would let it through.
  • Standalone everywhere: loader Dockerfile, docker-compose.yml, and CI drop cloud mode; Solr bumped to 9.10. The Makefile creates the core from the configset, optimizes the index, and tars the core into snapshot.backup.tar.gz (with pigz when available — single-threaded gzip on an already-compressed 130 GB index was hours for a few percent).
  • /status fixed for standalone: it looked up the SolrCloud core name name_lookup_shard1_replica_n1, so under standalone it reported Expected core not found. with no document counts — quietly, since the endpoint still answers 200 and the Helm probes only check that. The core now comes from SOLR_CORE (default name_lookup), with a fallback to the single core present so old cloud-built backups keep working.
  • Version pinning by role: the builder names an exact release tarball (SOLR_VERSION=9.10.0) and is the floor for anything serving the backup; the servers (docker-compose, CI, Helm chart) track the 9.10 line, so they pick up patch releases and stay at or above the builder automatically.
  • Deleted: setup_solr.sh, solr-restore/ and configoverlay.json (all obsolete).
  • Load sized to the container, not the node: parallelism and pigz now come from the cgroup CPU quota rather than nproc, which inside a pod reports the node's cores — 8 CPUs of quota on a 64-core node used to mean 64 throttled uploads. The loading heap drops from 220G (solr -m commits both -Xms and -Xmx) to 31G, since indexing wants page cache rather than heap, and ramBufferSizeMB rises to 2G — the buffer is a budget shared across indexing threads, so at 32 parallel uploads that is ~64 MB per segment instead of ~16 MB, and four times fewer segments to merge.
  • Kubernetes loading job reworked: the pod goes to 32 CPUs / 128Gi, and /var/solr is sized at 600Gi (the optimize writes the new segment before dropping the old ones, so it peaks at ~2x the finished index). Moving the index onto a node-local NVMe ephemeral volume is the change that would actually speed this up — it takes the whole write load of the indexing run and the optimize, and it is the one volume we could afford to lose — but it is blocked on Enable nvme-ephemeral volumes in the data-loading namespace #280 (the namespace cannot create nvme-ephemeral volumes), so both volumes are persistent PVCs today and both need deleting by hand. Makefile stamp files now live on the volume whose state they describe, so they disappear with the index if that volume is ever made ephemeral. New data-loading/kubernetes/README.md covers the volume split, sizing, the Grafana panels that answer “would more of this help”, and every knob.
  • CI image builds cached: the data-loading image was rebuilt from scratch every run — apt upgrade, seven apt installs and a ~250MB Solr tarball — taking ~25 minutes. It now uses the GitHub Actions cache.
  • Helm chart (in translator-devops, edited copy under data/name-lookup/): standalone Solr, idempotent download.sh, restore.sh gutted to blocklist-only, heap lowered for page cache.

Validation

CI now runs the whole thing end to end, against a Solr loaded by the new pipeline:

  • pytest: 28/28 pass (including test_status, which the standalone switch initially broke).
  • Configset verified against a real Solr: core creates from the two files, load succeeds (so the uuid processor and /query survived the trim), an unknown field is rejected with a 400, a Schema API write is refused with schema is not editable, and highlighting still returns spans.
  • The loader must abort on a file that does not load — the guard is what stands between us and a silently half-loaded index, so it is tested rather than assumed.
  • The loader must refuse a core that already has documents. Loading assigns fresh UUIDs, so a re-run over the same files doubles the index, and the count guard cannot see it — the delta is still exactly right. This is the one way the loader can return a corrupt index while reporting success, so it is a hard error (LOAD_APPEND=1 to override).
  • Full backup roundtrip in CI: tar the core → drop it into a fresh Solr → 89 documents served with zero schema setup and no chown. The roundtrip deliberately chowns the source core to uid 1000 first, standing in for the loading image, so it proves the tarball's ownership stamping rather than hiding it behind a fixup.

Also checked by hand: an unexpected field is rejected with a 400 (schemaless really is off), filenames with spaces and blank lines are counted correctly, and make data/backup.done on a restarted container restarts Solr instead of failing — without re-running solr create on a core that already exists.

Also confirmed the failure the ownership fix prevents: a backup tarred without it restores into a core that will not load, with SolrException: /var/solr/data/name_lookup/data/index/write.lock — Solr takes a write lock on the core it serves, and the loading image's uid (1000) is not the serving image's (8983).

Issues

Closes #238, closes #185, closes #256, closes #266. Addresses helxplatform/translator-devops#609. Progresses #265 (heap knob + smaller index; final heap/GC tuning and #272 still need load testing). Out of scope: #267, running natively on SLURM/HPC.

Pre-merge cleanup — done

  • Deleted the pull_request: trigger from .github/workflows/release-nameres-loading.yml (c563ded). It only existed to publish a pr-278 image so a load could run against this branch without merging.
  • Reverted nameres-loading.k8s.yaml to image: …:latest (b6458cc). Caveat carried over: the publish workflow only tags latest on a published release, so latest becomes this pipeline only once a release is cut — run the next load after this is merged and released.

The load that already ran — done

A full Babel 2026jul22 load (~111Gi index, ~331M docs) was built with the pr-278
image and is now restored and serving on the cluster, so the workarounds this section
used to list are spent. That run predated the review fixes, so the backup's ownership
was stamped by hand — the merged Makefile now does it automatically with tar --owner.
The load ran to completion (no partial-load restart, no stray files to skip). Two
problems surfaced during it, both now fixed: the backup's uid ownership (in this
branch) and the serving chart's Solr image tag being read as 9.1 (see the reviewer
note).

Note for reviewers

The data/name-lookup/ Helm chart is gitignored here (it's a working copy); its edits live in translator-devops. One caveat found while bringing the load up: the Solr image tag must be quoted (tag: "9.10") — unquoted, YAML parses 9.10 as the float 9.1 and pulls solr:9.1, which cannot init a 9.10-built core (it fails with _version_ … not searchable). That fix still needs applying in translator-devops.

Point dataUrl at a backup built by the new loader — old backups won't restore under the new flow.

🤖 Generated with Claude Code

gaurav and others added 3 commits July 16, 2026 10:24
… guard

The Solr schema was defined twice -- in setup_solr.sh at load time and again in
the Helm restore script at serve time -- and had already drifted. Make the
checked-in configset (configsets/name_lookup/conf) the single source of truth,
generated from a real Solr so behaviour is unchanged, with the malformed `types`
field fixed, a larger index buffer, autoSoftCommit off, and a grown
queryResultCache (closes #266).

Rewrite setup-and-load-solr.sh to stream files to Solr in parallel with a single
deferred commit (no per-file commit, no sleeps), and guard against dropped data
by counting input documents before the load and comparing against Solr's count
afterward, with curl --fail on every upload. The core is now created from the
configset (see the Makefile / CI), so this script no longer sets up the schema
and setup_solr.sh is deleted.

Convert the test fixture to JSON-lines to match the Babel production format so CI
exercises the line-counting guard.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Run Solr in standalone mode everywhere (no ZooKeeper for a one-node job): drop
-cloud from the loader Dockerfile and -DzkRun from docker-compose and CI. The
Makefile now creates the core from the checked-in configset, loads in parallel,
optimizes the index before export (closes #256), and tars the whole core --
config, schema and index -- into a self-contained snapshot.backup.tar.gz.

Restoring is now just "untar into the Solr home and start Solr", so the old
replication-backup targets and the solr-restore/ helper are removed. CI creates
the core from the configset and runs the parallel loader end to end.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Rewrite data-loading/README.md around the self-contained standalone-core flow,
with an "Options considered" section explaining why we chose it over the status
quo and PR #249, and the list of issues it closes. Update the test-run commands
in CLAUDE.md to create the core from the configset first.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Comment thread .github/workflows/tester.yml Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR reworks NameRes’s Solr data-loading and backup pipeline around a self-contained standalone Solr core backup (config + schema + index) to simplify restore, remove cloud/ZooKeeper dependencies, and substantially speed up bulk loading.

Changes:

  • Replace serial per-file commit + sleep loading with a parallel streaming loader guarded by a pre/post document-count check.
  • Switch local docker-compose and CI Solr to standalone mode and standardize on Solr 9.10.0, creating the core from a checked-in configset.
  • Add a checked-in Solr configset (schema + config + language resources) and remove obsolete restore/schema-setup scripts.

Reviewed changes

Copilot reviewed 56 out of 57 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
tests/create_test_synonyms.py Write test synonym data in JSON-lines format to match the production loader’s expectations.
solr-restore/restore.sh Removed obsolete restore script (standalone core restore no longer uses replication restore + schema setup).
solr-restore/README.md Removed obsolete documentation for the deleted restore flow.
docker-compose.yml Bump Solr image to 9.10.0 and document standalone core auto-discovery on startup.
data-loading/setup-and-load-solr.sh Parallel streaming loader with deferred commit and doc-count verification guard.
data-loading/setup_solr.sh Removed schema/field creation via Schema API (schema now lives in configset).
data-loading/README.md Updated documentation for standalone core backups, parallel loading, and simplified restore.
data-loading/Makefile End-to-end pipeline: download/split/load/optimize/tar a self-contained core backup in standalone mode.
data-loading/Dockerfile Copy in configset and run Solr standalone (--user-managed) for loader container flow.
data-loading/configsets/name_lookup/conf/solrconfig.xml Checked-in Solr core configuration tuned for bulk load + serving, used as schema/config source of truth.
data-loading/configsets/name_lookup/conf/managed-schema.xml Checked-in managed schema for the name_lookup core (single source of truth).
data-loading/configsets/name_lookup/conf/configoverlay.json Disable Solr autoCreateFields via config overlay (ensures schema consistency).
data-loading/configsets/name_lookup/conf/synonyms.txt Add Solr synonym resource file referenced by analyzers in the configset.
data-loading/configsets/name_lookup/conf/stopwords.txt Add Solr stopwords resource file referenced by analyzers in the configset.
data-loading/configsets/name_lookup/conf/protwords.txt Add Solr protected-words resource file referenced by analyzers in the configset.
data-loading/configsets/name_lookup/conf/lang/userdict_ja.txt Add Japanese tokenizer user dictionary resource used by default analyzers.
data-loading/configsets/name_lookup/conf/lang/stoptags_ja.txt Add Japanese POS stop-tag list resource for analyzer chains.
data-loading/configsets/name_lookup/conf/lang/stopwords_ar.txt Add Arabic stopwords resource file.
data-loading/configsets/name_lookup/conf/lang/stopwords_bg.txt Add Bulgarian stopwords resource file.
data-loading/configsets/name_lookup/conf/lang/stopwords_ca.txt Add Catalan stopwords resource file.
data-loading/configsets/name_lookup/conf/lang/stopwords_cz.txt Add Czech stopwords resource file.
data-loading/configsets/name_lookup/conf/lang/stopwords_da.txt Add Danish stopwords resource file.
data-loading/configsets/name_lookup/conf/lang/stopwords_de.txt Add German stopwords resource file.
data-loading/configsets/name_lookup/conf/lang/stopwords_el.txt Add Greek stopwords resource file.
data-loading/configsets/name_lookup/conf/lang/stopwords_en.txt Add English stopwords resource file.
data-loading/configsets/name_lookup/conf/lang/stopwords_es.txt Add Spanish stopwords resource file.
data-loading/configsets/name_lookup/conf/lang/stopwords_eu.txt Add Basque stopwords resource file.
data-loading/configsets/name_lookup/conf/lang/stopwords_fa.txt Add Persian stopwords resource file.
data-loading/configsets/name_lookup/conf/lang/stopwords_fi.txt Add Finnish stopwords resource file.
data-loading/configsets/name_lookup/conf/lang/stopwords_fr.txt Add French stopwords resource file.
data-loading/configsets/name_lookup/conf/lang/stopwords_ga.txt Add Irish stopwords resource file.
data-loading/configsets/name_lookup/conf/lang/stopwords_gl.txt Add Galician stopwords resource file.
data-loading/configsets/name_lookup/conf/lang/stopwords_hi.txt Add Hindi stopwords resource file.
data-loading/configsets/name_lookup/conf/lang/stopwords_hu.txt Add Hungarian stopwords resource file.
data-loading/configsets/name_lookup/conf/lang/stopwords_hy.txt Add Armenian stopwords resource file.
data-loading/configsets/name_lookup/conf/lang/stopwords_id.txt Add Indonesian stopwords resource file.
data-loading/configsets/name_lookup/conf/lang/stopwords_it.txt Add Italian stopwords resource file.
data-loading/configsets/name_lookup/conf/lang/stopwords_ja.txt Add Japanese stopwords resource file.
data-loading/configsets/name_lookup/conf/lang/stopwords_lv.txt Add Latvian stopwords resource file.
data-loading/configsets/name_lookup/conf/lang/stopwords_nl.txt Add Dutch stopwords resource file.
data-loading/configsets/name_lookup/conf/lang/stopwords_no.txt Add Norwegian stopwords resource file.
data-loading/configsets/name_lookup/conf/lang/stopwords_pt.txt Add Portuguese stopwords resource file.
data-loading/configsets/name_lookup/conf/lang/stopwords_ro.txt Add Romanian stopwords resource file.
data-loading/configsets/name_lookup/conf/lang/stopwords_ru.txt Add Russian stopwords resource file.
data-loading/configsets/name_lookup/conf/lang/stopwords_sv.txt Add Swedish stopwords resource file.
data-loading/configsets/name_lookup/conf/lang/stopwords_th.txt Add Thai stopwords resource file.
data-loading/configsets/name_lookup/conf/lang/stopwords_tr.txt Add Turkish stopwords resource file.
data-loading/configsets/name_lookup/conf/lang/stemdict_nl.txt Add Dutch stemmer override dictionary resource.
data-loading/configsets/name_lookup/conf/lang/hyphenations_ga.txt Add Irish hyphenations resource.
data-loading/configsets/name_lookup/conf/lang/contractions_ca.txt Add Catalan contraction list resource for elision filter.
data-loading/configsets/name_lookup/conf/lang/contractions_fr.txt Add French contraction list resource for elision filter.
data-loading/configsets/name_lookup/conf/lang/contractions_ga.txt Add Irish contraction list resource for elision filter.
data-loading/configsets/name_lookup/conf/lang/contractions_it.txt Add Italian contraction list resource for elision filter.
CLAUDE.md Update repository guidance to reflect standalone core creation and new loader script usage.
.github/workflows/tester.yml Run Solr 9.10.0 standalone in CI and create the core from the checked-in configset before tests.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread data-loading/Makefile Outdated
Comment thread data-loading/setup-and-load-solr.sh Outdated
gaurav and others added 6 commits July 23, 2026 03:40
/status looked for a core called name_lookup_shard1_replica_n1, which is the
name SolrCloud gives it. In standalone mode the core is just name_lookup, so
/status reported "Expected core not found." with no document counts -- and
because the endpoint still answers 200, the Helm probes stayed green while it
did. tests/test_status.py caught this.

The core name now comes from SOLR_CORE (default name_lookup) and is used for
the query URLs too, so there is one place to change it. If that name is not
present but Solr reports exactly one core, we report on that one, so backups
built by the old cloud pipeline keep working.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…names

Three problems with the guard, in rough order of how much they would hurt:

- It compared the input line count against Solr's *total* document count. Every
  load assigns fresh UUIDs, so loading is additive: a second load against a
  non-empty core would double the data and then blame the count. Compare the
  increase instead, and say so when the core is not empty.
- The wait for Solr was unbounded, so a Solr that never starts hung the build
  (six hours in CI) instead of failing it.
- The glob was expanded with default IFS, so a path containing a space split
  into two non-existent files, which nullglob then dropped -- the count would
  quietly come up short. Empty IFS during expansion fixes it.

Blank lines are no longer counted as documents, since Solr ignores them.

Also corrected a comment: --fail does not catch malformed JSON. Solr answers
200 and indexes nothing for a file of plain text; only the count notices.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
data/solr.pid is now an order-only prerequisite of core.done and backup.done.
Optimizing needs a running Solr, and the stamp files outlive a restarted
container while the server does not, so `make data/backup.done` after a restart
used to curl into nothing. Order-only, because a restarted Solr must not make
the core look stale: re-running `solr create` on an existing core is an error.

The tarball is compressed with pigz when it is available (added to the image).
A 130 GB Lucene index is already largely compressed and gzip is single-threaded,
so this was hours of CPU for a few percent. Dropped tar -v, which wrote a line
per index file into the build log, and added pipefail so a tar failure is not
hidden by a successful gzip.

Uncompressing now uses find -exec, so rerunning the download step after a
partial run is a no-op rather than a "no such file" failure, and the wait for
the PID file sleeps and gives up rather than spinning a CPU forever.

Solr 9 wants Java 11 at a minimum but ships and tests on 17 (the official
9.10 image runs 17), so the loader image uses 17 as well.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
configoverlay.json carried one setting, update.autoCreateFields=false. That
file is written by Solr's Config API, so shipping it in the configset invites
exactly the drift this configset exists to prevent. Set the default on the
add-unknown-fields chain in solrconfig.xml instead -- same behaviour, verified
by posting a document with an unknown field and getting a 400.

Also dropped size/initialSize from queryResultCache: as the comment two lines
above it in the same file says, both are ignored once maxRamMB is set.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The guard is what stands between us and a silently half-loaded 130 GB index,
and the backup is the actual product of data-loading/, but neither was tested
anywhere automated. Two steps, a few seconds each on the 89-document test core:

- the loader must exit non-zero on a file that does not load
- tar the core, drop it into a fresh Solr, and serve it: 89 documents with no
  collection creation and no schema setup

The wait for Solr is also bounded now, and dumps the container log when it
gives up, rather than hanging the job until the six-hour timeout.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Pin by role. The builder has to name an exact release tarball, so
SOLR_VERSION stays at 9.10.0, and it is the floor for anything that serves the
backup. The servers -- docker-compose, CI, the Helm chart -- now track 9.10, so
they pick up patch releases (any 9.10.x reads a 9.10.x index) and stay at or
above the builder without anyone remembering to bump them.

Deployment.md still told people to run solr-restore/restore.sh, which this
branch deletes. Restoring is now "extract the backup into ./data/solr and start
Solr". Fixed the mount description while there: ./data/solr is mounted at
/var/solr/data, the Solr home, which is why the core is found at all.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@gaurav gaurav changed the title Speed up and simplify Solr data loading with self-contained backups Speed up and simplify Solr data loading; fix /status for standalone Jul 23, 2026
gaurav and others added 11 commits July 23, 2026 04:15
…heap

Two settings were being chosen from numbers that do not describe the container
they run in.

Parallelism came from getconf _NPROCESSORS_ONLN, and pigz defaulted to one
thread per core. Both report the *node's* CPU count inside a container: in the
loading pod, 8 CPUs' worth of quota on a 64-core node meant 64 parallel uploads
and 64 compression threads, all throttled. available-cpus.sh reads the cgroup
quota instead (v2 and v1, falling back to the node count when unlimited), and
both the loader and pigz now use it, so raising the pod's cpu limit is picked up
without a second setting to remember.

SOLR_MEM was 220G, which `solr -m` turns into -Xms220G -Xmx220G -- committed,
not a ceiling. A bulk load does not want that: Lucene buffers into
ramBufferSizeMB (512 MB) and streams merges through the OS page cache, so the
heap was taking memory away from the only thing that would have used it. It is
now 31G, which also stays under the compressed-oops threshold, and the serving
side already measured 11-13Gi of RSS against 111Gi of page cache for the same
index.

The tunables use ?= so a pod spec can override them from the environment.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The pod asked for 220G/256G of memory and 6-8 CPUs, which was the right shape
for the old serial loader and the old 220G heap: almost all of the memory went
to a heap that could not use it, while the parallel loader is short of the CPU
it now knows how to use. It asks for 96Gi and 16 CPUs instead, with requests ==
limits so a multi-hour load is not evicted partway through.

The PVC comments described the pipeline as it was in 2022-2023. The Solr volume
needs 2-3x the finished index because optimize writes the new segment before
dropping the old ones, and the data volume no longer stages an uncompressed
~119G copy of the backup, since it is tarred straight out of the Solr volume.

Added a README covering what actually costs time in a load (CPU while indexing,
disk while merging and optimizing), how each resource should be sized, which
Grafana panels answer "would more of this help", and what every knob does. Also
replaced the keep-alive loop with sleep infinity and said why the pod overrides
the image's entrypoint.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
data/core.done and data/setup.done recorded facts about the Solr volume ("the
core exists", "it has been loaded") but lived on the data volume. That was
harmless while both were persistent PVCs with the same lifetime. It stops being
harmless the moment the Solr volume becomes ephemeral: a recreated pod would
find an empty index and a pair of stamps swearing it was loaded, skip straight
to optimizing a core that no longer exists, and fail somewhere confusing.

The three stamps that describe the Solr volume now live on it, so they vanish
with it and make sees the truth. data/synonyms/done and data/backup.done stay
where they are, because they describe the data volume.

Logs move the other way, from the Solr volume to data/logs, since they are most
useful exactly when the run has died and the Solr volume has gone with it. That
includes Solr's own logs, via SOLR_LOGS_DIR.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The index is the volume that wants speed -- it takes the whole write load of the
indexing run and then the read-and-write of the optimize -- and it is also the
one volume we can afford to lose, since once the tarball exists the index is
worthless. That makes it an exact fit for nvme-ephemeral: created with the pod,
destroyed with it.

data/ stays on a persistent PVC. It holds the ~130G download and the backup
tarball, which is the only thing the whole exercise produces; that does not
belong on a volume that dies with its pod. So deleting the pod now throws away
the fast, rebuildable half and keeps the slow, irreplaceable half.

fsGroup 1000 matches the image's nru user, without which the volume arrives
root-owned and Solr cannot write to it.

The old Solr PVC is kept as a documented fallback for when no node has enough
local NVMe, since 400Gi of local disk is a real scheduling constraint in a way
that network storage is not. README.md covers the split, the fallback, and what
survives a pod restart.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The namespace has roughly 90 CPUs and 1000Gi spare, so 16/96Gi was leaving most
of the available speed unused for a job that runs once and finishes.

Stopping at 32 rather than taking the lot: the request has to fit on a single
node, alongside the NVMe volume, and namespace quota says nothing about whether
any one node can do that. Raising it further is a matter of checking node
capacity -- the loader picks up the new CPU count on its own.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The buffer is a budget shared across indexing threads, so what it sets in
practice is how large a segment grows before it is flushed: roughly the budget
divided by the number of concurrent uploads. At 512 MB and 32 parallel uploads
that is ~16 MB a segment; at 2 GB it is ~64 MB, so four times fewer segments for
the merges to chew through afterwards.

Safe at both ends. It is a budget rather than a reservation, so serving pods
(which never index) allocate nothing from it, and 2 GB is a small fraction of
the 31G load heap and well under Lucene's 1945 MB per-thread hard limit.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Every run rebuilt the image from nothing: apt upgrade, seven apt installs and a
~250MB Solr tarball fetched through an Apache mirror redirect. That is the
difference between the 2-3 minute runs in the history and the 25 minute ones --
any Dockerfile edit near the top invalidates everything after it, and there was
no cache to fall back on.

cache-from/cache-to with the GitHub Actions backend fixes that; mode=max keeps
intermediate layers, so editing the COPY lines at the bottom of the Dockerfile
no longer re-downloads Solr. Buildx is already set up with the docker-container
driver, which the gha backend needs.

Also dropped the get_version step: it used ::set-output, which is deprecated,
to compute a value nothing referenced.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The publish workflow tags `latest` only on a published release, so `latest` is
still the pre-PR image: old Makefile, old loader script, 220G heap, no
available-cpus.sh. All of those live inside the image rather than in this repo's
working copy, so a correct pod spec would have run the old pipeline anyway.

pr-278 is what the pull_request build publishes. Revert to latest after release.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Makefile, the loader and the configset are all baked into the data-loading
image, so the `image:` tag decides which version of the pipeline runs and editing
a checkout does nothing. That is easy to miss, and the cost of missing it is an
eight-hour load of the wrong thing, so the README now says it and gives three
commands that tell you what you actually have.

Also notes the escape hatch: the configset is only read when the core is created,
so editing solrconfig.xml inside the pod before `make all` still counts.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
gaurav and others added 3 commits July 23, 2026 05:37
The configset was captured wholesale from a running Solr, so it carried the
whole of Solr's _default: 44 files, 70 field types and 69 dynamic fields, where
NameRes uses 6 field types and 13 fields. 42 of those files were language
stopword and contraction lists that could never have done anything here -- Babel
synonyms carry no language, and neither of our field types stems, removes
stopwords or applies synonyms, so nothing referenced them but the stock text_*
types we never query.

That left most of the file pinned without having been chosen, with no story for
what to do when Solr moves. Both files are now written for NameRes instead of
copied: 1593 lines to 218, 44 files to 2, and every element carries a comment
saying why it is there. What remains is our data model plus a handful of tuning
decisions, which are ours to own whatever Solr does. Upgrades are now "bump the
image and let CI create a core from this", which is a check we already run.

Two behaviours tightened along the way:

- The schema is immutable: schemaFactory declares mutable=false, so the Schema
  API refuses writes and a live core cannot drift from this file. Undeclared,
  Solr defaults to a mutable managed schema, which is the trap that made us
  delete configoverlay.json. The one thing that touches a live index is the Helm
  blocklist, which deletes by query and needs no schema change.
- An unexpected Babel key is a hard error rather than an invented field, since
  there are no dynamic fields and no schemaless chain.

Kept deliberately, both easy to delete by accident: the named uuid update
processor, which is what gives every document its id, and the /query handler,
which the load guard and the CI roundtrip use to count documents.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The build fetched Solr through https://www.apache.org/dyn/closer.lua, which
redirects to a randomly chosen Apache mirror. Mirrors range from fast to barely
moving, and ADD-from-a-URL has no timeout, no retry and no resume, so a bad draw
hung the build until the job gave up -- 25 minutes on one run, still going at 18
on another, against 2-3 minutes historically. Same Dockerfile every time; the
only variable was which mirror answered. Nothing verified the download either.

Copying /opt/solr out of the official image removes the whole failure mode: it
arrives over a CDN, the daemon verifies it by digest, and it caches like any
other layer. The JDK comes with it, so the openjdk-17-jre install goes too, and
there is no 250MB tarball to extract. The Solr that builds a backup is now
bit-for-bit the Solr that serves it.

The stage is pinned to linux/amd64 to match the RENCI base image, which is
published for amd64 only. Without that, a build on an arm64 machine resolves
this stage to arm64 while the final stage falls back to amd64, and every java
call fails with "No such file or directory" -- found the hard way.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Building the image locally and running `make all` inside it -- which nothing had
done before, since CI exercises the loader against a stock Solr rather than
through the Makefile -- turned up two failures, both fatal to a real load.

`solr create -d configsets/name_lookup` cannot work: a *relative* -d is resolved
against Solr's own server/solr/configsets directory, not the working directory,
so core creation died with "Can't find resource 'solrconfig.xml'". CONFIGSET is
now absolute via $(CURDIR).

The start target waited for `solr status` to print a PID. In a container Solr
reports "No Solr nodes are running" while serving happily on 8983 -- the stock
solr image does the same, so this is not something about our image -- and the
loop ran to its limit and failed. Nothing ever used that PID: stop-solr stops
Solr by port, and the file is only a make stamp. It now waits for the admin
endpoint to answer, which is the actual question being asked, and the stamp is
renamed solr.started rather than solr.pid because it no longer holds a PID.

Verified in the built image: make all loads 89 documents, make data/backup.done
optimizes and writes a tarball through pigz, and that tarball restores into a
stock Solr and serves all 89 with /status reporting ok.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The namespace cannot create nvme-ephemeral generic ephemeral volumes, so the
loading pod could not be scheduled. Issue #280 asks for that; the NVMe version is
preserved on nvme-ephemeral-loading and will come back as its own PR rather than
blocking a load that is otherwise ready to run.

Sized up from 400Gi to 600Gi while reverting. The 400Gi figure was 3x the ~127Gi
index measured for Babel 2025nov4, but the Makefile now points at 2026jul22 and
releases grow. optimize=true writes the new single segment before dropping the
old ones, so peak usage is 2-3x the finished index, and at a 180Gi index 3x is
540Gi. Running out of space happens during the optimize, which is the last step
of a multi-hour load, and takes the whole load with it -- cheap insurance.

fsGroup stays, because a freshly provisioned PVC is root-owned and Solr runs as
nru, but it now uses fsGroupChangePolicy: OnRootMismatch. The default chowns
every file on every mount, which on a volume holding a ~130G index is minutes of
pod startup for no reason.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 21 out of 22 changed files in this pull request and generated 4 comments.

Comment thread data-loading/configsets/name_lookup/conf/solrconfig.xml Outdated
Comment thread data-loading/README.md Outdated
Comment thread .github/workflows/release-nameres-loading.yml
Comment thread data-loading/Makefile Outdated
gaurav added 2 commits July 23, 2026 07:04
The two that would have bitten in production:

Backup ownership. Solr writes to the core it serves -- it takes a write
lock and refuses to load the core if it cannot. The loading image runs as
nru (1000) and every Solr that serves the backup runs as solr (8983), so a
restored core was owned by a user that does not exist on the serving side.
Verified against a real restore: without this the core fails with

  SolrException: /var/solr/data/name_lookup/data/index/write.lock

at the end of a multi-hour restore. CI papered over it with a chown, and
Deployment.md did not mention it at all. tar now stamps BACKUP_UID/GID onto
every member, which fixes every restore path at once instead of asking each
one to remember. The CI roundtrip now chowns the source core to 1000 first
-- standing in for the loading image -- and drops the chown on the restore
side, so it exercises the fix rather than hiding it. tlog is excluded while
we are here: the optimize commits, so there is nothing to replay.

Additive loads. Loading assigns fresh UUIDs, so re-running a load over the
same files indexes everything twice, and the document-count guard cannot
see it because the delta is still exactly right. That is the one way the
loader can hand back a corrupt index while reporting success, so a
non-empty core is now a hard error (LOAD_APPEND=1 to override). It fires in
the two cases that happen: a load re-run after one died partway through,
and make re-running the load because a stamp file went missing. Both
READMEs claimed a dead pod "resumes exactly where it left off"; resumption
is per step and the load is one step, so they now say to clear the core.

Smaller fixes:

- /status used core['startTime'] where every field around it uses .get().
- `make clean` ran `mkdir data` on a directory that still existed.
- wget now accepts only *.txt/*.txt.gz. Everything in data/synonyms is
  loaded, so a stray robots.txt became a file the loader tried to index --
  caught by the count guard, but only after a multi-hour load.
- The split loop swallowed a failed split that was not the last one.
- The loading image's entrypoint still asked for a 64G heap.

Doc drift: ramBufferSizeMB was described as 512 MB (it is 2 GB),
SOLR_VERSION as 9.10.0 (it is 9.10.1), LOAD_PARALLELISM as defaulting to
the CPU count (it is the cgroup limit, which is the entire point), and the
useColdSearcher comment promised warming that nothing performs.

Checked against a real Solr 9.10: core creates from the configset, 89 docs
load, the malformed-file and non-empty-core guards both abort, the backup
round-trips into a fresh Solr with no chown and serves queries, and
pytest is 28/28.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 21 out of 22 changed files in this pull request and generated 1 comment.

Comments suppressed due to low confidence (1)

data-loading/setup-and-load-solr.sh:42

  • solr_count() parses Solr’s JSON response with a regex that assumes no whitespace after "numFound":. Since /query is configured with indent=true, the JSON writer may legally emit whitespace (e.g. "numFound": 123), which would make this return an empty count and abort an otherwise successful load. Make the match tolerate whitespace and also surface curl errors for easier debugging.
solr_count() {
  curl -sf "${SOLR_SERVER}/solr/${CORE}/query?q=*:*&rows=0" \
    | grep -oE '"numFound":[0-9]+' | head -1 | grep -oE '[0-9]+'
}

Comment thread .github/workflows/tester.yml
gaurav and others added 5 commits July 23, 2026 15:04
The measured index for Babel 2026jul22 (the release SYNONYMS_URL loads) is
~111Gi -- smaller than 2025nov4's ~127Gi, because it has fewer proteins. That
falsifies the "newer releases are bigger" claim the 600Gi sizing rested on, so
reword both places to note release size moves both ways and the headroom is for
a possibly-larger future release. No size change: 600Gi stays comfortably above
2-3x either figure.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
maxRamMB is a CaffeineCache feature. CaffeineCache is Solr's default cache
today, so this is a no-op now, but if a later release changed that default to
an impl without RAM accounting, maxRamMB would be silently ignored and the
cache would grow unbounded. Pin the class so the configset's intent survives a
Solr upgrade. Verified the core still creates from the configset on solr:9.10.

Addresses a Copilot review comment on #278.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The load guard and the CI backup-roundtrip check both scrape numFound out of
/query with a regex that assumed "numFound":<digits> with no space. Solr 9.10
emits exactly that today (verified: "numFound":1), so this is defensive, but
/query runs with indent=true and a JSON-writer change to "numFound": 1 would
turn the guard into a false failure. Allow optional whitespace after the colon;
head -1 still skips the sibling numFoundExact key.

Addresses two Copilot review comments on #278.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The per-setting checks (ls available-cpus.sh, grep ramBufferSizeMB/SOLR_MEM)
were a crutch for the unversioned transition image; with released version tags
the image tag itself identifies the pipeline. Drop them, keep the durable facts
(pipeline is baked into the image, `latest` is the previous pipeline until a
release is cut, files are editable in the pod before make), and point at the
org.opencontainers.image.revision label as the reliable tag-to-commit check.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@gaurav
gaurav merged commit 6fc21a5 into main Jul 23, 2026
1 check passed
@github-project-automation github-project-automation Bot moved this from Backlog to Done in NameRes sprints Jul 23, 2026
@gaurav
gaurav deleted the speed-up-loads branch July 23, 2026 19:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

2 participants