Solr overview and tuning
BOA's Solr integration is per-site and INI-driven. The Solr-side core
lifecycle is controlled entirely by three variables in each site's
boa_site_control.ini; there is no per-platform Solr state
(boa_platform_control.ini Solr keys are ignored). Solr 9 is BOA PRO only.
The authoritative INI contract lives in this page; the daemon that enforces it is documented in Core lifecycle.
The per-site INI contract
In <platform-root>/sites/<domain>/modules/boa_site_control.ini:
solr_integration_module = search_api_solr9
solr_update_config = NO
solr_custom_config = NO
manage_solr_config.sh reads these on every four-minute pass and reconciles the
site's Solr core against them. A missing site INI is seeded from
/data/conf/default.boa_site_control.ini.
solr_integration_module
Selects the integration module and therefore the core type and Solr version. The
search_api_solr / search_api_solr7 / search_api_solr9 labels all resolve
under the shared search_api_solr config tree, but the deployed config family is a
per-Drupal-version subdirectory of it, not one flat family: search_api_solr9 on a
D10 platform is served from solr9_drupal10, while search_api_solr /
search_api_solr7 map to the solr7_drupalN families (D7 gets solr7_drupal7,
passed to create_core -d at core creation). Core version is switched by which
Solr daemon the label targets (Solr 7 on port 9077, Solr 9 on port 9099).
| Value | Core created | Notes |
|---|---|---|
search_api_solr9 |
Solr 9 (port 9099) | current; D8+ |
search_api_solr7 |
Solr 7 (port 9077) | D8+; legacy |
search_api_solr |
Solr 7 (port 9077) | alias of the D8+ search_api_solr family |
apachesolr |
Solr 4 (port 8099) | D6/D7 only; deprecated |
| (commented out / unrecognised) | core deleted, cleanly, within 15 minutes | solr delete is issued and solr.php removed |
When the value is removed or no longer names a recognised module, the daemon
issues solr delete against every version's data dir for the computed core ID
(and its _Old_/_Legacy_ aliases) and removes the site's solr.php. This is
the explicit deletion path (BOA cleanly deletes the core within 15 minutes),
distinct from the age-based orphan archival in
Core lifecycle.
solr_update_config
Controls auto-refresh of schema.xml + solrconfig.xml when the integration
module version changes:
| Value | Effect |
|---|---|
YES |
re-deploy the shipped config family (/data/conf/solr/...) on version change |
NO (default) |
leave the core's config in place until manually refreshed |
Ignored when solr_custom_config = YES.
solr_custom_config
| Value | Effect |
|---|---|
YES |
read operator config from sites/<domain>/files/solr/; the daemon skips re-deploying schema.xml/solrconfig.xml |
NO (default) |
use the BOA-shipped config family for the chosen module |
solr_custom_config = YES locks the core's config across upgrades (intentional
immutability). The transient-NO pattern is the supported way to pick up a
Drupal-generated config without permanent lock-in: set NO, let one pass deploy
the new config, then restore YES. Other lifecycle behaviour (deletion on INI
removal, solr.php refresh, optimisation) still runs under YES.
solr.php
Once a core exists the daemon writes sites/<domain>/solr.php with the core's
connection details. It is not used to connect to the core — Drupal does not
include it; it is information only, for reading off the credentials when you
configure the search_api_solr server in the site. It is removed when the core is
deleted.
Core continuity across site operations
The Solr core is keyed to the site (oct.<user>.<domain>), not the platform:
- Migration between platforms — the core stays attached. After migration,
Verify the site;
solr_integration_moduleandsolr.phpare preserved; no re-index needed, provided the deployedschema.xml/solrconfig.xmlremain compatible with the integration-module version in the codebase. - Cloning — the clone gets its own fresh core (unique
solr.php, empty index) within a few passes. The clone's Drupal config must be pointed at its new Solr server before indexing. Cron is not enabled on the cloned site by default, which prevents the clone from overwriting the original site's index. - Cross-host migration (
xoct/xcopytransfer,xmasssync/cutover) — Solr data (/opt/solr4,/var/solr7/data,/var/solr9/data) is rsynced data-only into the target's existing Solr setup, following the target's own/var/solrNlayout (including a/var/solrNsymlink onto attached storage). Each Solr transfer is pre-checked for disk space — during the migration DRY run and again at--live: an index that will not fit at its target destination printsDENY, marks the runNOT_CLEAN(so--liveis refused until resolved), and that Solr transfer is skipped. See Cross-host migration for the full DRY/--liveworkflow.
Tuning and diagnostics
A Solr-enabled host accumulates orphan cores (deleted/renamed/cloned-site cores left on disk) and high-write cores stress the JVM heap. The reconciliation daemon automates orphan archival and ratio-driven optimisation; the operator-facing work is diagnosis and high-write core configuration.
Read the GC log
tail -f /var/solr9/logs/solr_gc.log # Solr 7: /var/solr7/logs/solr_gc.log
Healthy: ParNew (young gen) collecting normally; Old gen declining or stable; CMS cycles infrequent.
Orphan-core accumulation (the key indicator): Old gen identical before/after on every cycle, CMS firing every ~2 s and freeing nothing:
GC(N) Old: 1642215K->1642215K(2759348K)
GC(N) Old: 1642215K->1642215K(2759348K)
Each loaded core holds live heap; the Old gen flatlines. Resolution: let the orphan-cleanup pass archive stale cores (or force it — see Core lifecycle), then restart Solr to release the held references. Archiving a large backlog of orphan cores typically drops Old gen by hundreds of MB and ends the continuous CMS spin.
Overlapping onDeckSearchers in /var/solr9/logs/solr.log:
PERFORMANCE WARNING: Overlapping onDeckSearchers=2
Searchers created faster than they warm — per-document commits firing too quickly. Fix via high-write tuning below.
Normal post-restart: brief ~2 s CMS cycling that burns off startup allocation with Old gen decreasing sweep over sweep is not a leak; if Metaspace is also decreasing, classloader cleanup is working.
Index optimisation (ratio-driven, 12 h throttle)
Optimisation is automatic and per deleted-doc ratio — no fixed cadence and no
per-core list. _optimize_solr_cores runs via the throttle wrapper
_run_optimize_if_due (sentinel /var/backups/solr/.optimize_last_run.pid, at
most once per 12 h, _OPTIMIZE_INTERVAL_HOURS=12). For each registered core it
reads numDocs/maxDoc from the admin STATUS API, computes the deleted-doc
percentage, and decides:
| Deleted-doc ratio | Action |
|---|---|
< 20% (_OPTIMIZE_DEL_PCT_THRESHOLD=20) |
skip — Lucene's merge policy keeps up |
>= 20% |
expungeDeletes — merges only segments over the delete threshold |
>= 30% (_OPTIMIZE_FULL_THRESHOLD=30) |
full optimize maxSegments=1 |
A protected core (conf/.protected.conf) never gets the forced full merge even at
>= 30% — it may carry its own tuned merge policy, so it gets expungeDeletes
only. Every update uses waitFlush=false: the request returns immediately and the
merge continues in the background, so run time stays bounded regardless of index
size.
# What the daemon issues internally (Solr 9; 9077 for Solr 7):
curl 'http://127.0.0.1:9099/solr/<core>/update?expungeDeletes=true&waitFlush=false'
curl 'http://127.0.0.1:9099/solr/<core>/update?optimize=true&maxSegments=1&waitFlush=false'
Low-write and read-only cores naturally fall under the threshold and are skipped; there is no manual weekly schedule.
High-write core tuning
For sites used as an API layer with continuous index churn, stock
search_api_solr config underperforms. Apply these in the core's
conf/solrconfig.xml, alongside a conf/.protected.conf sentinel so BOA does not
overwrite the file on the next config sync. Highest-impact first:
ramBufferSizeMB32 → 256 — fewer flushes, fewer tiny segments, less merge work.- Switch to
TieredMergePolicy(deletesPctAllowed=20) — handles high delete ratios far better than the defaultLogByteSizeMergePolicy; keeps the delete ratio self-healing. infoStream→false— stop logging every merge (floodssolr.logon a high-write core).
Commit/searcher tunables that specifically cure overlapping onDeckSearchers:
<autoCommit>
<maxTime>300000</maxTime> <!-- 5 min; app issues its own commits -->
<openSearcher>false</openSearcher> <!-- don't reopen searcher on hard commit -->
</autoCommit>
<autoSoftCommit>
<maxTime>10000</maxTime> <!-- 10s near-realtime; drop the maxDocs trigger -->
</autoSoftCommit>
<maxWarmingSearchers>4</maxWarmingSearchers> <!-- default 2 → 4 -->
Hard commits no longer open searchers, soft commit is time-only (no per-document
maxDocs churn), and more warming-searcher headroom keeps frequent soft commits
from exhausting the pool. Apply via the solr_custom_config = YES route so the
config survives upgrades.
Core health checks
# Per-core stats via Solr admin
curl -s 'http://127.0.0.1:9099/solr/<core>/admin/luke?wt=json&numTerms=0' | jq
# Core status (the CoreAdmin STATUS action the daemon's own health check uses)
curl -s 'http://127.0.0.1:9099/solr/admin/cores?action=STATUS&wt=json' | jq '.status | keys'
# Java process memory
ps -o pid,vsz,rss,cmd -p $(pgrep -f solr) | awk '{print $1, $2/1024 " MB VSZ", $3/1024 " MB RSS"}'
D7 apachesolr_attachments Java path
When apachesolr_attachments cannot find a Java binary, pin it in
sites/<domain>/local.settings.php:
$conf['apachesolr_attachments_java'] = '/usr/bin/java11 -Xms32m -Xmx64m';
/usr/bin/java11 exists on every Solr 4/7-capable host (the Solr 4 path runs
under Java 11).
Related
- Solr 9 modules — the deployed
.modset and how modules load. - Core lifecycle — the reconciliation daemon, orphan archival, and forced runs.
- Reference appendix —
solr_integration_module,solr_update_config,solr_custom_config.