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 |
|---|---|
|
The main query — parsed by whichever query parser |
|
One or more filter queries, ANDed with |
|
How many documents to return in this response (default |
|
How many matching documents to skip before the first one returned (default |
|
Which fields (and pseudo-fields, functions, transformers, and aliases) come back per document — covered in its own section below. |
|
One or more |
|
Which response writer formats the output ( |
|
Which query parser parses |
|
|
|
|
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:
-
qanswers "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. -
fqanswers a plain yes/no — "isstatus:publishedtrue for this document" — with no score attached. Because that answer does not depend onqat 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 identicalfqstring, whateverqandsortare 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. Acache=falsefilter withcost>=100that implements Lucene’sPostFilterinterface 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 andq, 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_fieldrenames 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
fland 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 withfl=id,title,[transformerName]and configured with space-separated parameters ([explain style=nl]). Common transformers:[explain](a scoring explanation per document, formattedtext/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 withchildFilter),[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.