Apache Solr performance improvements
On this page
Once you have Apache Solr up and running and serving real queries, the next question is always the same: how do I make it faster? The honest answer is that most Solr slowness is self-inflicted — a schema that indexes everything, a commit on every document, or a JVM heap so large it starves the operating-system cache that Lucene quietly depends on. This post walks through the changes that actually move the needle, roughly in the order I reach for them.
Before changing anything, remember the golden rule: measure first. Solr ships with a lot of visibility — the admin UI’s Plugins / Stats page, per-core cache statistics, and debugQuery=true to see where a query spends its time. Tune against numbers, not hunches.
# Start with the schema
Performance work starts in schema.xml, because the schema decides how much work Lucene does per document and per query.
- Index only what you search on; store only what you display. A field with
indexed="true" stored="false"is searchable but adds nothing to the stored document you retrieve. Flip each field’sindexedandstoreddeliberately — every stored field you don’t display is wasted I/O on every hit. - Pick the narrowest field type that works. A
stringfield is a single, un-analyzed token — perfect for IDs, codes, and exact-match facets. Reserve the heaviertext_general(tokenizing, lowercasing, stemming) for fields you actually run full-text search against. - Use
docValuesfor anything you sort, facet, or group on. DocValues store the field in a column-oriented form that’s far cheaper for these operations than building the field cache on the heap — and it keeps that data off the JVM heap and in the OS cache where it belongs. - Turn off what you don’t need.
omitNorms="true"on fields you never score by length, andomitTermFreqAndPositions="true"on fields you never phrase-search, both shrink the index and speed indexing. - Be careful with
copyField. It’s convenient for building a single searchabletextfield, but everycopyFieldre-analyzes and re-indexes content. Copy only what you’ll query.
# Speed up indexing
Slow indexing is usually a batching-and-commit problem, not a hardware problem.
Batch your documents. Sending one document per HTTP request is the classic mistake. Use SolrJ’s ConcurrentUpdateSolrClient (or batch a few hundred to a few thousand docs per add) so you amortize network and analysis overhead.
Stop committing on every document. A commit reopens the searcher and is expensive. Let Solr manage commits for you with auto-commit settings in solrconfig.xml, and understand the two kinds:
- A hard commit flushes to disk and (optionally) opens a new searcher — it makes data durable.
- A soft commit makes documents visible to search without a full disk flush — it’s how you get near-real-time search cheaply.
flowchart LR
ADD["Batch of docs<br/>(add via SolrJ)"] --> RAM["In-memory buffer<br/>(ramBufferSizeMB)"]
RAM -->|"soft commit<br/>(visible, not durable)"| SEARCH["New searcher<br/>docs searchable"]
RAM -->|"hard commit<br/>(flush to disk)"| DISK["Segments on disk<br/>durable"]
DISK --> MERGE["Segment merge<br/>(TieredMergePolicy)"]
classDef in fill:#2563eb,color:#fff,stroke:#1e40af;
classDef soft fill:#059669,color:#fff,stroke:#065f46;
classDef hard fill:#d97706,color:#fff,stroke:#92400e;
class ADD,RAM in;
class SEARCH soft;
class DISK,MERGE hard;A sane starting point is a periodic hard commit with openSearcher=false for durability, plus a shorter soft-commit interval for visibility:
<autoCommit>
<maxTime>60000</maxTime> <!-- flush to disk every 60s -->
<openSearcher>false</openSearcher>
</autoCommit>
<autoSoftCommit>
<maxTime>2000</maxTime> <!-- make docs visible every 2s -->
</autoSoftCommit>Give the indexer room to work. Raise ramBufferSizeMB so Lucene builds larger in-memory segments before flushing (fewer, bigger segments mean less merging). Leave segment merging to the default TieredMergePolicy — and resist the urge to run optimize (forceMerge) routinely. On a large index, optimize rewrites the entire thing; it’s a rare maintenance operation, not a performance tactic.
When doing a big one-off load, disable auto-soft-commit and cache autowarming for the duration, index everything, then do a single hard commit at the end. You avoid reopening the searcher hundreds of times mid-load.
# Make queries fast
On the read side, Solr’s caches are the biggest lever — when you use them well.
flowchart TD
Q["Incoming query"] --> QRC["queryResultCache<br/>(ordered doc IDs per query)"]
Q --> FC["filterCache<br/>(cached fq bitsets)"]
QRC --> DC["documentCache<br/>(stored fields)"]
FC --> DC
DC --> RESP["Response"]
OS["OS filesystem cache<br/>(Lucene segments via MMapDirectory)"] -.-> RESP
classDef cache fill:#2563eb,color:#fff,stroke:#1e40af;
classDef os fill:#059669,color:#fff,stroke:#065f46;
classDef io fill:#475569,color:#fff,stroke:#334155;
class QRC,FC,DC cache;
class OS os;
class Q,RESP io;- Use filter queries (
fq) for reusable constraints. Anything that isn’t part of relevance scoring — a status flag, a category, a date range — belongs infq, notq. Filter queries are cached independently in thefilterCacheas bitsets and reused across queries, sofq=category:booksis computed once and shared by every request that uses it. - Size the caches to your workload and watch the hit ratio. In the admin stats, a
filterCacheorqueryResultCachewith a low hit ratio and lots of evictions is telling you it’s too small; a huge cache that’s mostly cold is wasting heap. Tune thesizeinsolrconfig.xmlagainst real numbers. - Return only the fields you need.
fl=id,title,pricebeatsfl=*. Fewer stored fields per hit means less I/O and smaller responses. - Avoid deep paging with
start.start=100000&rows=10forces Solr to score and rank 100,010 documents to hand you ten. For deep pagination, usecursorMark— it pages by a sort cursor instead of an offset and stays flat as you go deeper. - Facet with the right method. Facet on
docValuesfields, and pick the method deliberately:facet.method=fc(field cache/docValues) is usually best for many-valued fields, whileenumsuits fields with a small number of distinct terms. - Watch out for expensive queries. Leading wildcards (
*term), unbounded fuzzy searches, and huge boolean expansions are slow by nature.debugQuery=truewill show you the timing breakdown so you can see which clause is the culprit.
Warm the caches before traffic hits. Configure firstSearcher and newSearcher event listeners with a handful of representative queries so a freshly opened searcher comes up warm instead of cold-starting on your users.
# Tune the JVM and let the OS help
This is the counter-intuitive one. Lucene memory-maps its index files (MMapDirectory) and leans heavily on the operating-system filesystem cache to keep hot segments in RAM. That has a direct consequence:
A giant heap is not a performance win for Solr. Size the heap to what your caches and requests actually need, and leave the rest of physical memory free for the OS to cache index files. Over-allocate the heap and you starve the very cache Lucene relies on — and you pay for it with longer GC pauses on top.
Practical guidance:
- Set
-Xmsand-Xmxto the same value to avoid heap resizing pauses, and keep that value only as large as your cache sizing justifies. - Use a low-pause collector (in this era, CMS or G1) and actually look at GC logs — long stop-the-world pauses show up as periodic query latency spikes.
- Put the index on SSDs. Search is I/O-bound, and the difference between spinning disk and SSD for random reads is dramatic.
# Scale out when a single node isn’t enough
When one machine can’t hold the index in cache or keep up with query volume, SolrCloud is the answer: shard to split a large index across nodes (each shard searches a slice in parallel) and replicate each shard to add query throughput and failover. Sharding helps when the index is too big for one box; replication helps when you have more queries than one box can serve. They solve different problems — reach for the one that matches your bottleneck.
# A quick checklist
-
indexed/stored/docValuesset deliberately per field - Narrowest field type that works;
omitNormswhere scoring by length isn’t needed - Batched indexing; auto-commit + auto-soft-commit configured, no per-doc commits
-
optimizereserved for rare maintenance, not routine use - Reusable constraints moved into
fq; cache hit ratios monitored -
fllimited to displayed fields;cursorMarkfor deep paging - Heap sized modestly; plenty of RAM left for the OS filesystem cache; index on SSD
- Warming queries configured for new searchers
Solr performance tuning is rarely one heroic change — it’s a series of small, measured adjustments across the schema, the indexing pipeline, the query patterns, and the JVM. Start by measuring, change one thing at a time, and let the admin stats tell you whether it worked.
# References
Click to show appreciation
Discussion