skip to content

How does Solr's filterCache store an entry, and why can an fq clause using NOW make it useless?

level: seniorimportance: should knowfreq 48%

answer

  1. what is worth keeping for one filter query
  2. the value is a set of document ids
  3. how big is one bit per document
  4. the key must repeat to ever be reused

basics

~20 s

Each filterCache entry maps one fq to a set of internal Lucene document ids matching it across the whole index, roughly one bit per document when dense. An fq containing NOW resolves to the current millisecond, producing a unique key per request and a permanent cache miss.

solid answer

~50 s

Solr's `filterCache` keys on the filter query and stores the resulting `DocSet` — the set of internal Lucene doc ids matching that filter across the whole index for the current searcher. Dense sets are held as a bitset costing about one bit per document (roughly 6 MB for a 50-million-document index), sparse ones as a sorted int array. Because the ids are searcher-specific, the cache is discarded when a commit opens a new searcher; `autowarmCount` re-executes that many recently used filters against the new searcher, which is part of what makes commits expensive. An `fq` such as `ts:[NOW-1HOUR TO NOW]` is fatal because `NOW` resolves to the current millisecond, so every request has a different key: a hit ratio near zero, plus constant eviction of useful entries. Round the date math — `ts:[NOW-1HOUR/MINUTE TO NOW/MINUTE]` — or mark the filter `{!cache=false}` so it never pollutes the cache.

code

xml · 4 lines
xml
<filterCache class="solr.CaffeineCache"
             size="512"
             initialSize="512"
             autowarmCount="128"/>

go deeper

for a junior

Know that clauses put in fq are cached and do not affect scoring, and that a filter reused across many requests is the kind worth caching.

for a middle

Explain that an entry is a document-id set for the whole index, that it is tied to the current searcher and dies at commit, and why NOW without date-math rounding makes the key unique per request.

for a senior

Do the heap arithmetic out loud — bits per document times cache size — and connect autowarmCount to commit frequency and warming-searcher errors. Know when to reach for cache=false and post filtering instead of caching an expensive or unique filter.

for a principal

Own the interaction between commit cadence, cache warming and node memory as one budget. Decide which filters are shared infrastructure worth caching for everyone and which are per-user constraints that must never enter the key, and encode that in the request handler rather than leaving it to callers.

## What an entry actually is The `filterCache` is configured in `solrconfig.xml`: ```xml <filterCache class="solr.CaffeineCache" size="512" initialSize="512" autowarmCount="128"/> ``` The key is the filter query itself; the value is a `DocSet`, an unordered set of internal Lucene document ids matching that filter across the entire index. It carries no scores and no ordering, because a filter contributes nothing to ranking — that is the whole point of putting a clause in `fq` rather than `q`. Solr picks a representation by density. A dense set becomes a `BitDocSet` backed by a fixed bitset of `maxDoc` bits — one bit per document in the index whether it matches or not, so about `maxDoc / 8` bytes. A sparse set becomes a `SortedIntDocSet`, roughly four bytes per matching document. The practical consequence: on a 50-million-document core, a cached dense filter costs about 6 MB, and `size=512` is a promise of up to roughly 3 GB of heap if the entries are dense. Sizing this cache without doing that multiplication is a classic way to OOM a Solr node. ## Searcher lifetime and autowarming Internal doc ids are only meaningful for a specific index view. When a commit opens a new searcher, the old cache is thrown away wholesale. `autowarmCount` tells Solr to re-execute that many of the most recently used filter queries against the new searcher before it serves traffic, so the first users after a commit do not all pay a cold miss. This trades directly against commit frequency. Aggressive autowarming plus frequent hard commits means the machine spends much of its time re-running filters, and if warming takes longer than the interval between commits you can end up with overlapping warming searchers — hence `maxWarmingSearchers` and the familiar error when it is exceeded. Near-real-time deployments usually keep `autowarmCount` low and lean on soft commits. ## The NOW trap Solr resolves `NOW` to the request's current time in milliseconds. So `fq=ts:[NOW-1HOUR TO NOW]` produces a distinct query string, and therefore a distinct cache key, on essentially every request. The hit ratio for that filter is zero, the expensive range set is recomputed each time, and each new entry evicts something that would have been reused. The fix is date-math rounding: ``` fq=ts:[NOW-1HOUR/MINUTE TO NOW/MINUTE] ``` Now the key changes once a minute instead of once a millisecond, and the filter caches usefully. Round as coarsely as the product tolerates — `NOW/DAY` for a "last 30 days" filter is free. The same reasoning applies to any filter containing a per-request unique value: a user id, a session token, a generated geo point. Those belong behind `{!cache=false}`. ## cache=false and post filtering Two local params control participation: ``` fq={!cache=false}some_expensive_filter fq={!frange l=0 cache=false cost=200}sum(rating,votes) ``` `cache=false` skips the cache entirely — right for filters that are cheap to recompute, or unique per request, or so large that caching them evicts everything useful. Adding `cost` on a filter whose parser supports post filtering (such as `{!frange}` or `{!collapse}`) with a cost of 100 or more, together with `cache=false`, promotes it to a **post filter**: instead of building a full DocSet up front, it is evaluated only on documents that already passed the query and the cheaper filters. For an expensive function or an access-control check that matches most documents, that is dramatically cheaper. For cheaper filters, `cost` simply orders their evaluation. ## Sizing and monitoring The numbers to watch, exposed in the admin UI's plugins and stats page and via the metrics endpoints, are the cumulative hit ratio, the eviction count, and the warmup time. A modest hit ratio with high evictions means the working set of distinct filters exceeds `size` — either raise it (after computing the heap cost per entry) or stop caching the unique ones. A hit ratio near zero on a specific filter almost always means a per-request value in the key. High warmup time means `autowarmCount` is fighting your commit rate. ## What else uses it The filterCache is not only for `fq`. Classic faceting with `facet.method=enum` intersects one cached filter per term, which is why that method is only sensible for very low-cardinality fields; `useFilterForSortedQuery` lets the main query use it too. So a change in facet configuration can quietly change filterCache pressure. ## The design rule Put in `fq` what is reusable across users and requests — status flags, tenant ids, coarse date buckets, category constraints — and keep per-request uniqueness out of the key. That single discipline determines whether the filterCache is your best performance feature or dead weight consuming gigabytes of heap.

  • Why is the filterCache emptied on commit, and what does autowarmCount do about it?
    Entries hold internal Lucene document ids, which are only valid for a specific index view, so a new searcher invalidates them all. `autowarmCount` re-executes that many of the most recently used filters against the new searcher before it starts serving, so users after a commit do not all hit a cold cache. The cost is CPU on every commit, and with frequent commits warming can overlap and fall behind.
  • When would you deliberately set cache=false on a filter you use constantly?
    When the filter's document set is huge and would evict many smaller, more reusable entries, or when it is cheap enough to recompute that caching buys nothing, or when it embeds a per-request value so the key never repeats. Pairing `cache=false` with a cost of 100 or more on a parser that supports post filtering, such as `{!frange}` or `{!collapse}`, additionally defers evaluation to only the documents that survived the other clauses.
  • How would you tell from Solr's metrics that the filterCache is misconfigured?
    Look at the cumulative hit ratio, evictions and warmup time in the admin plugins and stats page. A low hit ratio with heavy evictions means the working set of distinct filters exceeds the configured size. A hit ratio near zero usually means a per-request value such as NOW or a user id is in the filter key. Long warmup times mean autowarmCount is too high for the commit rate.
  • What besides fq consumes filterCache entries?
    Classic faceting with `facet.method=enum` intersects one cached filter per distinct term, so it is only viable on very low-cardinality fields, and `useFilterForSortedQuery` lets the main query take an entry too. That means a facet configuration change can silently multiply cache pressure and evict the filters you were relying on.

An entry is a pre-marked checklist of every document that passes a filter. Marking it up is worth it only if the same checklist gets reused; a filter pinned to the current millisecond produces a fresh checklist every time and throws the old ones away.

saying these in an interview costs you the question

  • Thinks the filterCache stores matching documents or their fields
  • Says cached filters survive a commit
  • Sizes the cache without multiplying by index size
  • Believes range queries are never cacheable
  • Puts a per-user id into fq and expects cache hits

context