Every SolrCloud tutorial explains that a collection is split into shards and each shard has replicas. Almost none of them explain what the cluster is actually doing during a search — and that gap is where most “why is this slow” and “why are these results wrong” questions live.
Here is the version I wish I’d been given: the fan-out, the two round trips nobody mentions, the response flags that quietly lie to you, and the scoring bug that hides in every tenant-sharded cluster.
Whoever answers the phone does the work
There is no dedicated coordinator node in SolrCloud. When a search request arrives, the node that happens to receive it becomes the aggregator for that request. It asks one replica of every shard, merges the answers, and returns a single response. Your client never sees any of it.
Three shards means three internal requests — whether you asked for 10 rows or 10,000. The aggregator waits for the slowest one, which is why a single sick replica makes the whole collection feel slow.
Two consequences fall out of this immediately. First, a Solr node can answer as long as it reaches at least one replica of every shard — lose the last replica of any single shard and the whole search fails by default. Second, every node is simultaneously a front desk and a floor, which is exactly how a cluster can deadlock. More on that at the end.
A search is two round trips, not one
This is the part the reference guide never quite spells out, and it explains half the parameters on the page.
You asked for the top 10 results. Shard 1 cannot know whether its best document beats shard 3’s best document — it has never seen shard 3’s data. So every shard must return candidates before anyone can decide the real top 10. But shipping 10 full documents from every shard, when 20 of the 30 will be discarded, is wasteful. So Solr splits the work in two.
Phase 1 talks to every shard; Phase 2 fetches the stored fields. The reference guide confirms the two phases directly — it warns that an index can go out of sync “if a commit happens between the first and second phase of the distributed search”, and describes distrib.singlePass as fetching stored fields in the first phase so as to “eliminate the need for making a second request to fetch the stored fields”. The finer detail drawn here — that phase 1 carries ids and sort values, and that phase 2 skips shards with no surviving hit — is implementation behaviour the guide does not spell out; treat it as the mental model, not a quotation.
Which tells you exactly when distrib.singlePass=true is a good idea: when you return a handful of small fields. With large stored fields and ten shards you would ship 100 documents across the network to display 10. Note also that faceting still makes its own refinement round trips, so it never guarantees “one request per shard”.
status: 0 does not mean “complete”
Two independent things can go wrong, and people conflate them constantly. The node might not reach ZooKeeper — so it is working from a possibly stale cluster map. Or a shard might have no reachable replica — so data is genuinely missing from your answer.
Solr reports both in the response header, and here is the trap: all three of these responses carry "status": 0 and an HTTP 200.
| Header | Value | What it actually means |
|---|---|---|
zkConnected | true | Cluster map is current. Fine. |
zkConnected | false | The answering node could not reach ZooKeeper. It answered from its last-known map, which may predate a shard split or move. Results may be stale or incorrect — with no error. |
partialResults | true | At least one shard was unreachable and was skipped. numFound, facet counts and aggregations are all understated. |
With shards.tolerant=true, a dashboard reading “1,284 orders today” can silently become “913 orders today” because one shard was rebooting — no error anywhere. If a number drives a decision, treat partialResults: true as an error in your client, even though Solr calls it a success. And log both flags. They are the difference between “search is fine” and “search has been quietly lying to users for two hours”.
By default shards.tolerant is false and a missing shard fails the request. Setting it to true buys you partial answers; setting it to requireZkConnected goes the other way and refuses to answer at all when ZooKeeper is unreachable. When something is wrong, add shards.info=true; the guide says distributed responses then “include information about the shard”, and in practice that is what lets you see which shard failed or lagged. The exact fields returned are not enumerated in the reference guide, so check them against your own Solr version rather than trusting any blog (including this one).
Comma widens, pipe substitutes
The shards parameter syntax looks fiddly until you learn the two rules behind it. A comma means “and this shard too”. A pipe means “or this copy instead”. Commas add breadth; pipes offer alternative servers for the same slice of data.
shards=shard1 → one shard, random replica of it shards=shard1,shard2 → two shards (comma widened the search) shards=host1/solr/coll,host2/solr/coll → two shards, you named the machines shards=host1/solr/coll|host2/solr/coll → ONE shard, two candidate copies shards=shard1,host2/solr/coll|host3/… → two shards; second one has a shortlist
Count the commas first and you know how many shards you are about to search. The related parameter shards.preference is a multi-column sort over the replicas of each shard — replica.type, replica.location, replica.leader, node.sysprop, written left to right in order of importance. It never excludes anything, so it can never cause a failure; it only changes who gets asked first.
One preference is worth singling out. The default tie-breaker is random, which spreads load beautifully and wrecks your caches: with three replicas, the same query gets computed and cached three separate times. replica.base:stable:hash:sessionId hashes a parameter instead, so the same user keeps landing on the same copy and their cache stays warm.
The scoring bug hiding in every tenant-sharded cluster
This is the subtlest thing in distributed Solr and the one most likely to be quietly ruining your relevance.
When Solr scores a document it weighs each term by how rare it is — inverse document frequency. That is what stops the from outranking photosynthesis. Solr’s default similarity is BM25Similarity (the schema docs state it is used implicitly for any field type without an explicit similarity), and Lucene’s BM25Similarity computes the term weight as idf = ln(1 + (N − n + 0.5) / (n + 0.5)) — N being the number of documents in the index and n how many contain the term. The formula is Lucene’s, not something the Solr reference guide restates.
And there is the problem: in SolrCloud, “the index” means one shard. Both N and n are local. The figures below are a worked illustration, not a benchmark — I invented the corpus, then ran the real formula over it, so the arithmetic is reproducible even though the scenario is made up:
| Shard | Local N | Local n (“photosynthesis”) | IDF used | Effect |
|---|---|---|---|---|
| shard1 — science journals | 1,000,000 | 90,000 | 2.41 | Thinks the term is ordinary. Scores its docs low. |
| shard2 — news articles | 1,000,000 | 600 | 7.42 | Thinks it’s rare. Scores its docs high. |
| shard3 — recipe blogs | 1,000,000 | 400 | 7.82 | Thinks it’s very rare. Scores highest. |
| The truth (whole collection) | 3,000,000 | 91,000 | 3.50 | What every shard should have used. |
IDF assigned to “photosynthesis”, by the shard doing the scoring
The same word, four different weights. A recipe blog that mentions photosynthesis in passing weights the term at 7.82. A peer-reviewed paper on the subject weights it at 2.41 — a 3.2× penalty for being surrounded by relevant neighbours.
Local IDF punishes a document for living on a shard where its subject is well covered. The more expert a shard is about a topic, the lower it scores its own documents for that topic. That is exactly backwards — and it is invisible. No error, no warning, no header flag. Your top results are simply, quietly, from the wrong shard.
Two reassurances before the fix. This changes scores only, never which documents match — numFound, filters and facet counts stay correct, which is precisely why it survives testing for so long. And if you sort by a field rather than by relevance, none of it applies.
Whether you have the problem comes down to routing. Hash routing on a random ID scatters documents evenly, every shard sees the same term distribution, and the default is fine — that is the case it was designed for. But if you shard by tenant, customer, language, source or date, your shards have genuinely different vocabularies and your relevance is skewed. That is worth sitting with, because composite routing by tenant is one of the most commonly recommended SolrCloud patterns.
If a query only touches one shard — via _route_, or shards=shard1, or a single-shard collection — there is no inconsistency within that query, because every score came from the same statistics. Tenant-scoped search gets consistency for free, even though tenant sharding is the riskiest scheme for collection-wide queries.
The fix is to configure a global statsCache in solrconfig.xml, which gathers document and term statistics from every shard before scoring so all shards work from the same numbers:
<statsCache class="org.apache.solr.search.stats.ExactSharedStatsCache"/>
Four implementations ship with Solr. LocalStatsCache is the default and the one you are using if you never configured this. ExactStatsCache is the correctness benchmark, but it costs an extra network round trip on every query — the two-phase dance becomes three. ExactSharedStatsCache gets the same accuracy while reusing stats for repeated terms, which makes it the better production choice for most people. LRUStatsCache bounds the memory for very large vocabularies.
Then trim the waste: add distrib.statsCache=false to request handlers that never score — ID lookups, filter-only queries, field-sorted listings. They would otherwise pay for statistics they never read. And re-check your boosts afterwards: switching to global statistics changes every score in the collection, so anything you tuned previously was tuned against the distortion.
And the failure that looks like a hang
Back to that dual role. Every node serves top-level requests and serves sub-requests from other aggregators, both out of the same HTTP thread pool. Give two nodes one thread each, send each of them a top-level query at the same time, and both threads are occupied — each waiting on a sub-request the other has no thread left to serve. Circular wait. Nothing crashes, nothing logs an error; requests just stop finishing and the cluster looks “slow” rather than broken.
The guide’s own rule: “ensure that the max number of threads serving HTTP requests is greater than the possible number of requests from both top-level clients and other shards.” So size the pool for both kinds of traffic, not just the queries you can see arriving from outside.
The short version
- Whoever gets the request does the coordinating. There is no special node — and that dual role is why deadlock is possible.
- A search is normally two round trips, not one. Almost every performance question traces back to this.
- You need one live replica of every shard. Lose the last one and the whole search fails by default.
status: 0does not mean complete. OnlyzkConnectedandpartialResultstell you that — so log them.- Comma widens, pipe substitutes. That decodes the entire
shardssyntax. shards.preferenceis a sort, not a filter. It can never cause a failure.- Shards score with their own statistics by default. If you shard by tenant, language or source, your relevance is skewed and nothing will tell you.
Where each claim comes from
Everything above is drawn from the Apache Solr Reference Guide. Because a few of the more useful points are behaviour the guide implies rather than states, here is the split, so you can check rather than trust:
| Claim | Status | Source |
|---|---|---|
| Aggregator model; one replica of every shard; needs one live replica per shard | Documented | SolrCloud Distributed Requests |
zkConnected, partialResults, shards.tolerant, shards, shards.preference, _route_, collection, distrib.singlePass, distrib.statsCache, the four statsCache classes, deadlock, ShardHandlerFactory, debug=track | Documented | Same page |
| Two phases exist; phase 2 fetches stored fields | Documented | Same page (distrib.singlePass) + User-Managed Distributed Search, which warns about a commit landing “between the first and second phase” |
| NRT / TLOG / PULL definitions and leader eligibility | Documented | SolrCloud Shards and Indexing |
| BM25 is Solr’s default similarity | Documented | Schema Elements |
The idf formula itself | Lucene, not Solr | org.apache.lucene.search.similarities.BM25Similarity — the class Solr defaults to. The Solr guide does not restate the formula. |
| Phase 1 carries ids + sort values; phase 2 skips shards with no surviving hit | Inferred | Consistent with the documented behaviour, but not stated in the guide. Mental model, not quotation. |
| The photosynthesis shard figures (2.41 / 7.42 / 7.82 / 3.50) | Illustrative | Invented corpus, real formula. Arithmetic is reproducible; the scenario is not a measurement. |
Fields returned by shards.info | Not enumerated | The guide promises only “information about the shard”. Verify against your own version. |
Solr behaviour varies by release — this was checked against the latest reference guide, so confirm anything load-bearing against the version you actually run.

