Query basics & parameters

This section documents the current Solr line (10.0; 9.10.x the maintained 9.x branch), written and verified against the Apache Solr Reference Guide. No specific patch version is pinned. Some capabilities (the Solr Operator on Kubernetes, the package-manager ecosystem, Learning To Rank model training, and expert plugin development) are linked, not documented in depth.

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

This section’s bibliography lists the reference material consulted while preparing these pages.

Every read that is not a RealTime Get (see Indexing & updates) goes through the /select request handler: a single HTTP call carrying a q and a grab-bag of other parameters, answered with a fixed response envelope. This page covers the request/response shape itself, the small set of parameters almost every query sets (q, fq, rows, start, fl, sort, wt, defType, debugQuery, omitHeader), the distinction between q and fq and the filter cache that makes fq cheap to reuse, the response writers wt selects between, document transformers and pseudo-fields inside fl, and the two ways to page through results. The syntax q itself accepts — Lucene, DisMax, eDisMax, and the rest — is Query parsers; how matches are ranked is Relevance & scoring.

The /select request handler

/select is a SearchHandler registered (like every request handler) in solrconfig.xml, with a defaults block that supplies parameters a request does not override — typically echoParams, wt=json, and a default rows. A request supplies the rest as URL query parameters (GET) or as a form-encoded body (POST); both are equivalent, and POST is the only option once the parameter list gets too long for a URL.

curl --get "http://localhost:8983/solr/books/select" \
  --data-urlencode 'q=title:darkness' \
  --data-urlencode 'fq=status:published' \
  --data-urlencode 'fl=id,title,score' \
  --data-urlencode 'sort=score desc' \
  --data-urlencode 'rows=10'

The response is always the same shape: a responseHeader (status, QTime, and the echoed parameters, unless omitHeader suppresses it) followed by a response block carrying numFound, start, maxScore, and the matched docs:

{
  "responseHeader": { "status": 0, "QTime": 3 },
  "response": {
    "numFound": 42,
    "start": 0,
    "maxScore": 3.14,
    "docs": [
      { "id": "1", "title": "The Left Hand of Darkness", "score": 3.14 }
    ]
  }
}

Extra top-level sections appear only when the request asked for them — facet_counts when faceting (Faceting), highlighting when highlighting (Highlighting), debug when debugQuery=true, and so on; /select with a bare q returns only responseHeader and response. See Query Syntax and Parsing for how the handler assembles a request into a Lucene query before executing it.

Common query parameters

Parameter Meaning

q

The main query — parsed by whichever query parser defType selects. This is the only parameter that computes relevance and can populate score.

fq

One or more filter queries, ANDed with q and with each other. Matches are yes/no, contribute no score, and are cached independently of q — see the next section.

rows

How many documents to return in this response (default 10).

start

How many matching documents to skip before the first one returned (default 0) — the OFFSET half of start/rows pagination, covered below.

fl

Which fields (and pseudo-fields, functions, transformers, and aliases) come back per document — covered in its own section below.

sort

One or more <field> asc|desc clauses, comma-separated, applied in order as tiebreakers. score desc sorts by relevance; omitting sort entirely defaults to relevance order when q is a scoring query.

wt

Which response writer formats the output (json by default) — see below.

defType

Which query parser parses q for this request — lucene (the default) unless the request sets it to dismax, edismax, or another registered parser. See Query parsers.

debugQuery

debugQuery=true is a boolean shorthand for debug=all: it adds a debug block to the response with the parsed query, per-clause explain output, and timing — useful while developing a query, expensive enough to leave off in production.

omitHeader

omitHeader=true drops responseHeader from the response entirely. Leave it in place on any request that might return partial results (timeAllowed and friends) — that is where partialResults surfaces.

See Common Query Parameters for the complete list, including request-cancellation (canCancel, queryUUID), resource-limiting (timeAllowed, cpuAllowed, memAllowed, maxHitsAllowed), and segmentTerminateEarly.

q vs. fq, and the filter cache

q and fq both restrict which documents match, but they answer different questions and Solr treats them very differently underneath:

  • q answers "how well does this document match, and in what order should matches come back" — it is scored, and its result is specific to this exact query string plus whatever else affects score, so it is rarely worth caching as a unit.

  • fq answers a plain yes/no — "is status:published true for this document" — with no score attached. Because that answer does not depend on q at all, Solr caches each distinct filter query independently in the filter cache as a bitset of matching internal document IDs, and reuses it across every subsequent request carrying the identical fq string, whatever q and sort are doing on top of it.

That reuse is why a stable, repeated condition — status:published, tenant_id:42, a date-range that only ticks over once an hour — belongs in fq and not folded into q as a +status:published clause: a cache hit skips scoring and re-execution altogether, where the same condition inside q gets re-evaluated (and re-scored) on every single request.

curl --get "http://localhost:8983/solr/books/select" \
  --data-urlencode 'q=title:darkness' \
  --data-urlencode 'fq=status:published' \
  --data-urlencode 'fq=published_at:[2020-01-01T00:00:00Z TO *]'

Execution order, cache, and cost

Solr does not necessarily run every fq before q, or in the order they were written — each filter (cached or not) is scheduled by its own cost, cheapest first, and the intersection of all filters plus the main query is what actually gets scored. Two local params on an individual fq change that scheduling:

  • \{!cache=false} skips the filter cache for that one filter — appropriate for a filter that is effectively unique per request (a per-user ACL check, a value with huge cardinality) where caching it would only evict more useful entries without ever being reused.

  • \{!cost=N} gives Solr a relative cost estimate. A cache=false filter with cost>=100 that implements Lucene’s PostFilter interface becomes a post-filter: instead of running against the whole index up front, it is evaluated only against documents that already survived every other filter and q, which is exactly the right trade-off for a filter that is expensive per-document (a geospatial or script-based check) but cheap once the candidate set is already small.

# Skip the filter cache for this per-request ACL filter, and defer it until after
# the cheaper filters have already cut the candidate set down (cost 100 => post-filter).
curl --get "http://localhost:8983/solr/books/select" \
  --data-urlencode 'q=title:darkness' \
  --data-urlencode 'fq=status:published' \
  --data-urlencode 'fq={!cache=false cost=100}owner_id:42'

The filter cache itself is sized and configured in solrconfig.xml like any other Solr cache (Configuration & caches covers sizing and autowarming); a cold cache after a commit or restart pays the full cost of rebuilding each fq bitset again on first use. See Common Query Parameters for the authoritative fq/cache/cost reference.

Response writers (wt)

wt selects the format Solr serializes the response into. json is the default; other built-in writers include xml, javabin (Solr’s own compact binary format, what SolrJ uses on the wire by default), csv, geojson for spatial results, cbor, and smile (a JSON-compatible binary format). Pick a writer based on the consumer — json for anything reading the response directly, javabin when talking to Solr from SolrJ, csv for a flat export into spreadsheet-shaped tooling.

curl --get "http://localhost:8983/solr/books/select" \
  --data-urlencode 'q=*:*' \
  --data-urlencode 'wt=csv' \
  --data-urlencode 'fl=id,title'
# https://solr.apache.org/guide/solr/latest/query-guide/response-writers.html
// SolrJ always speaks javabin to the server regardless of the wt a raw HTTP client would ask for.
try (SolrClient client = new Http2SolrClient.Builder("http://localhost:8983/solr").build()) {
    SolrQuery query = new SolrQuery("title:darkness")
            .addFilterQuery("status:published")
            .setFields("id", "title", "score")
            .setSort("score", SolrQuery.ORDER.desc)
            .setRows(10);
    QueryResponse response = client.query("books", query);
    SolrDocumentList docs = response.getResults();   // numFound, start, maxScore, and the docs
}

See Response Writers for the full list and per-writer configuration (e.g. XSLT transforms, CSV column/separator overrides).

Document transformers and field-list pseudo-fields

fl is not limited to literal schema field names. Three extra things can appear in it, freely mixed:

  • Aliases — display_name:actual_field renames a field in the output without changing the schema, handy when a downstream consumer expects a different key than the index uses internally.

  • Functions — any function query can appear in fl and is evaluated per document, e.g. fl=id,title,mult(price,qty).

  • Document transformers — a [name] token that adds or reshapes information about each document beyond its stored field values, invoked with fl=id,title,[transformerName] and configured with space-separated parameters ([explain style=nl]). Common transformers: [explain] (a scoring explanation per document, formatted text/html/nl), [shard] (which shard this hit came from in a distributed search), [docid] (Lucene’s internal document id, mainly for debugging), [child] (nested child documents, filterable with childFilter), [subquery] (a separate, per-document query for join-like lookups), and [value] (a synthetic constant field, e.g. fl=source:[value v=catalog-a]).

curl --get "http://localhost:8983/solr/books/select" \
  --data-urlencode 'q=title:darkness' \
  --data-urlencode 'fl=id,name:title,score,[explain style=nl]'

See Document Transformers for the full transformer catalogue, and Common Query Parameters for fl’s syntax (globs, `+/,-separated lists, and the special */score pseudo-fields).

Pagination

start / rows

start and rows are the direct analogue of OFFSET/LIMIT: Solr computes the full ranked list up to start + rows, discards the first start documents, and returns the rest. That means cost grows with the offset — page 500 at rows=20 still has to rank and hold the first 10,000 documents before it can return the last page’s worth — so start/rows is the right tool for shallow paging (a UI’s "next page" through the first several hundred results) and the wrong one for walking an entire result set.

curl --get "http://localhost:8983/solr/books/select" \
  --data-urlencode 'q=*:*' \
  --data-urlencode 'sort=id asc' \
  --data-urlencode 'start=20' \
  --data-urlencode 'rows=10'

cursorMark deep paging

For paging through a large or unbounded result set, replace start with cursorMark: the first request passes cursorMark=, and each response carries a nextCursorMark to send back verbatim as the next request’s cursorMark. Unlike start, a cursor encodes a *relative position in the sort order rather than an absolute offset, so each page costs roughly the same as the first regardless of how deep the walk has gone.

Two requirements make this safe: sort must include the uniqueKey field (asc or desc) as the final tiebreaker, so that documents sharing every other sort value still resolve to one deterministic order; and start must be 0 or omitted — cursorMark and start are mutually exclusive. Avoid a sort clause built from NOW or another value that recomputes differently on every request, since that changes the ordering the cursor is walking mid-walk.

# First page.
curl --get "http://localhost:8983/solr/books/select" \
  --data-urlencode 'q=*:*' \
  --data-urlencode 'sort=published_at desc, id asc' \
  --data-urlencode 'rows=100' \
  --data-urlencode 'cursorMark=*'

# Each response includes "nextCursorMark": "<opaque-token>" -- feed it back as cursorMark.
# A response whose nextCursorMark equals the cursorMark just sent means the walk is done.
curl --get "http://localhost:8983/solr/books/select" \
  --data-urlencode 'q=*:*' \
  --data-urlencode 'sort=published_at desc, id asc' \
  --data-urlencode 'rows=100' \
  --data-urlencode 'cursorMark=<opaque-token-from-previous-response>'

cursorMark holds no server-side session state — the token alone is enough to resume — so it scales to many concurrent pagers the same way Elasticsearch’s search_after does. See Pagination of Results for the full constraints, including SolrCloud caveats around score-based sorts across replicas. See Pagination: Offset vs. Keyset for how this same keyset pattern is expressed in SQL, MongoDB, Couchbase, Elasticsearch, GraphQL and Spring Data.

/export for a full unranked export

cursorMark still pays scoring and sort-buffer overhead for a full-index walk. When the goal is exporting an entire (or very large) sorted result set rather than paging a UI, the /export handler is built for exactly that: it streams fully sorted results starting within milliseconds and keeps streaming until the whole set has gone out, rather than materializing rows documents into memory at a time. It requires a sort and an fl, both restricted to single-valued fields with docValues enabled — there is no relevance scoring involved — and it is the mechanism Streaming expressions & SQL builds its search() stream on top of.

curl --get "http://localhost:8983/solr/books/export" \
  --data-urlencode 'q=*:*' \
  --data-urlencode 'sort=id asc' \
  --data-urlencode 'fl=id,title,published_at'
# https://solr.apache.org/guide/solr/latest/query-guide/exporting-result-sets.html

Reach for start/rows for ordinary shallow paging, cursorMark for deep paging still driven by an application walking pages, and /export for a one-shot bulk extraction of an entire matching set.

Continue with Query parsers for the syntax q itself accepts, Relevance & scoring for how matches are ranked, or Faceting for aggregating over a result set rather than just paging through it.