Speed up and simplify Solr data loading; fix /status for standalone - #278
Merged
Conversation
… 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>
gaurav
commented
Jul 23, 2026
Contributor
There was a problem hiding this comment.
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.
/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>
…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>
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>
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.
Contributor
There was a problem hiding this comment.
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/queryis configured withindent=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]+'
}
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
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 malformedtypesfield is fixed, the index buffer is larger,autoSoftCommitis off andqueryResultCacheis 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 insolrconfig.xmlrather than inconfigoverlay.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 hardunknown fielderror instead of a silently invented field.setup-and-load-solr.shstreams 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, withcurl --failon 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--failalone would let it through.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 intosnapshot.backup.tar.gz(withpigzwhen available — single-threaded gzip on an already-compressed 130 GB index was hours for a few percent)./statusfixed for standalone: it looked up the SolrCloud core namename_lookup_shard1_replica_n1, so under standalone it reportedExpected 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 fromSOLR_CORE(defaultname_lookup), with a fallback to the single core present so old cloud-built backups keep working.SOLR_VERSION=9.10.0) and is the floor for anything serving the backup; the servers (docker-compose, CI, Helm chart) track the9.10line, so they pick up patch releases and stay at or above the builder automatically.setup_solr.sh,solr-restore/andconfigoverlay.json(all obsolete).pigznow come from the cgroup CPU quota rather thannproc, 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 -mcommits both-Xmsand-Xmx) to 31G, since indexing wants page cache rather than heap, andramBufferSizeMBrises 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./var/solris 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 createnvme-ephemeralvolumes), 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. Newdata-loading/kubernetes/README.mdcovers the volume split, sizing, the Grafana panels that answer “would more of this help”, and every knob.apt upgrade, sevenapt installs and a ~250MB Solr tarball — taking ~25 minutes. It now uses the GitHub Actions cache.translator-devops, edited copy underdata/name-lookup/): standalone Solr, idempotentdownload.sh,restore.shgutted 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 (includingtest_status, which the standalone switch initially broke).uuidprocessor and/querysurvived the trim), an unknown field is rejected with a 400, a Schema API write is refused withschema is not editable, and highlighting still returns spans.LOAD_APPEND=1to override).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.doneon a restarted container restarts Solr instead of failing — without re-runningsolr createon 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
pull_request:trigger from.github/workflows/release-nameres-loading.yml(c563ded). It only existed to publish apr-278image so a load could run against this branch without merging.nameres-loading.k8s.yamltoimage: …:latest(b6458cc). Caveat carried over: the publish workflow only tagslateston a published release, solatestbecomes 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-278image 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 reviewernote).
Note for reviewers
The
data/name-lookup/Helm chart is gitignored here (it's a working copy); its edits live intranslator-devops. One caveat found while bringing the load up: the Solr image tag must be quoted (tag: "9.10") — unquoted, YAML parses9.10as the float9.1and pullssolr:9.1, which cannot init a 9.10-built core (it fails with_version_ … not searchable). That fix still needs applying intranslator-devops.Point
dataUrlat a backup built by the new loader — old backups won't restore under the new flow.🤖 Generated with Claude Code