Every search parameter — filters, searchFields, orderBy, highlightFields — is read from the query string of a search request, or from the $parameters array you pass to $_search->search(). This page documents exactly what that layer accepts, what it silently ignores, and where it cannot express what you might expect.

It applies identically to the master index and the facets index — the query builder is shared.

How a parameter name reaches the index

Index field names contain dots (tags.name, fields.brand, language.locale), but PHP rewrites dots in query-parameter names to underscores before any framework code runs. ?fields.brand=nike arrives as the key fields_brand. The framework reverses that substitution to recover the field name.

Two consequences:

1. The dotted and underscored forms are the same request. Write whichever you prefer:

/search/products?fields.brand=nike ← both are /search/products?fields_brand=nike ← identical

2. A field whose own name contains an underscore cannot be filtered. The reversal turns every underscore into a dot, so it cannot tell yours apart from PHP's:

Facet identifierRequest key PHP producesField name the framework looks forResult
brandfields_brandfields.brand✅ matches
shoe_sizefields_shoe_sizefields.shoe.size❌ no such field — filter silently ignored

Underscores are legal in a facet identifier, so nothing stops you creating shoe_size — it simply cannot be filtered afterwards. Use - instead of _ in facet identifiers and layout-area field names: shoe-size works.

Passing a single value

/search/products?fields.brand=nike

Passing several values

There are two syntaxes, and you can mix them freely.

Comma-separated:

/search/products?fields.brand=nike,adidas

Array ([]):

/search/products?fields.brand[]=nike&fields.brand[]=adidas

Both together — each array element is itself comma-split, so this filters by all three values:

/search/products?fields.brand[]=nike,adidas&fields.brand[]=puma → nike, adidas, puma

⚠️ Do not repeat a plain (non-array) parameter

Repeating a parameter without [] does not collect the values. PHP keeps only the last occurrence and discards the rest:

/search/products?fields.brand=nike&fields.brand=adidas → adidas only (nike is lost)

⚠️ Do not mix name= and name[]= for the same field

Mixing the two forms makes PHP discard one of them entirely — whichever came first:

/search/products?fields.brand=nike&fields.brand[]=adidas → adidas only (nike is lost) /search/products?fields.brand[]=adidas&fields.brand=nike → nike only (adidas is lost)

Pick one syntax per field. If you are building the query string in code and might emit a field more than once, always use [].

Worked examples

Given a filter field fields.brand, this is exactly what each request filters by:

RequestFilters by
?fields.brand=nikenike
?fields.brand=nike,adidasnike, adidas
?fields.brand[]=nikenike
?fields.brand[]=nike&fields.brand[]=adidasnike, adidas
?fields.brand[]=nike,adidas&fields.brand[]=pumanike, adidas, puma
?fields.brand=nike&fields.brand=adidasadidas — nike lost
?fields.brand=nike&fields.brand[]=adidasadidas — nike lost
?fields.brand[]=adidas&fields.brand=nikenike — adidas lost
?fields.brand[nike]=nike&fields.brand[]=adidasnike, adidas — array keys are ignored, only values are read
?fields.brand[a][b]=nikenothing — nested arrays are ignored
?fields.brand=nothing — see Empty values
?fields.brand=%20nike%20nike — values are trimmed
?fields.brand=nike,,adidasnike, adidas — empty elements are dropped
?fields.brand=,nothing — no non-empty values remain
?fields.brand=nike,nikenike, nike — duplicates are not removed (harmless; matching is unaffected)

fields.brand is a keyword field. A numeric or date field reads the same syntax and adds one thing on top — a value may be a range bound. See Range filters, which has its own worked table.

Array keys on filters are decoration

For filter fields, only the values matter — a key is ignored:

?fields.brand[whatever]=nike ≡ ?fields.brand[]=nike

There is exactly one exception: on a numeric or date field, the four keys gt, gte, lt and lte are read as range operators. On every other field they are decoration like any other key — with one caveat when the value also carries a comma, described in Mixing spellings and forms.

This is not true of searchFields and orderBy, where the key is meaningful (it names the field, and the value carries the weight or direction). See Search fields and Sort fields. highlightFields, like filters, ignores keys.

Range filters (numeric and date fields)

A field whose type is a number or a date can be filtered by a range — one or two bounds — instead of, or as well as, a list of exact values.

Which fields take a range

IndexCore fieldsConfigurable fields
Masterid, layoutId, componentLayoutIds (numbers); created, published, indexed (dates)fields.* indexed from a number, date or datetime layout area
Facetsid, productId, variantId, categoryPath.id, categoryPath.categoryId, price.{code} (numbers); indexed (date)facets of type long, double or date

No other type takes a range. On a keyword, text (.keyword) or boolean field the operator syntax below is simply not recognised: a leading > stays part of the value, and an operator key is decoration. That is deliberate — a keyword field can legitimately hold a value that starts with >, and ?tags.name[gte]=x has always meant a plain filter for x.

The operators

In the valueAs the array keyMeaning
>[gt]greater than
>=[gte]greater than or equal to
<[lt]less than
<=[lte]less than or equal to

The key spelling is matched case-insensitively — [GTE] and [gte] are the same request. In the value spelling the symbol goes directly in front of the value; whitespace after it is trimmed (> 10 is >10), and a URL-encoded symbol (%3E10) is of course the same request.

Two spellings, one meaning

?price.usd=>=10 ≡ ?price.usd[gte]=10 ?price.usd=<100 ≡ ?price.usd[lt]=100 ?published=>=2026-01-01 ≡ ?published[gte]=2026-01-01

Both exist for a reason. The value spelling is what a View's default filter values can store (they are plain strings — see View defaults) and what a template passes most naturally. The key spelling is what the legacy platform's faceted search accepted, so URLs written against it keep their meaning.

Both bounds

A lower and an upper bound on the same field describe one band. Write them comma-separated in one value, as two keys, or as two array elements — all three below mean 10 ≤ price ≤ 100:

?price.usd=>=10,<=100 ?price.usd[gte]=10&price.usd[lte]=100 ?price.usd[]=>=10&price.usd[]=<=100

The comma is the same comma that separates alternative values on a keyword field. What it means depends on what the parts turn out to be: between two exact values it separates alternatives (red,green is red or green); between two bounds it joins the halves of one range (>=10,<=100 is an intersection).

Bounds and exact values together — they intersect

Every filter clause is AND-ed, so a bound and an exact value on the same field intersect:

?price.usd=>10,<=100,77 → price is exactly 77 (and 77 is inside the band) ?price.usd=>10,77,88 → price is 77 or 88 (both are above 10) ?price.usd=>10,<=100,5 → nothing (5 is outside the band)

A range can only ever narrow a list of exact values, never widen it. A contradictory band (?price.usd=>100,<10) matches nothing and is not an error.

One bound per side — the last one wins

A range carries at most one lower bound (> or >=) and one upper bound (< or <=). When a request supplies two on the same side, the later one replaces the earlier — the same rule PHP applies to a repeated plain parameter:

?price.usd=>10,>=20,<=100,<40 → 20 ≤ price < 40

Values

Numbers — an integer or a decimal with a dot as the separator, optionally signed: 10, -5, +5, 49.99, .5. There is no thousands separator: >1,000 is the comma split at work and reads as >1 plus the exact value 000 (see the table below). On a double field such as price.{code} the amount is in major units — 49.99, never 4999.

Dates — any date or date-time PHP can read: 2026-01-01, 2026-01-01 10:30, 2026-01-01T10:30:00+02:00, 1 January 2026. Two rules decide the instant it names:

  • A value without an offset is read in the site's time zone — a bare 2026-01-01 is midnight at the site — and then converted to UTC, so it lines up with the stored UTC values. This is the same reading an admin's scheduled publish date gets: published=>=2026-01-01 finds exactly the records published on or after that local midnight.
  • A value with an offset keeps the offset it states.

Elasticsearch date math passes through untouched and is evaluated by Elasticsearch in UTC: now, now-7d, now+1M, now-1M/d (rounded to the start of the UTC day). The units are yMwdhHms, case-sensitive — now-7D is not "seven days" and is refused (see below). Anchored math (2026-01-01||+1M) is also passed through as it stands.

Without an operator a date is an exact value, and a date-only exact value matches the whole day it names (in UTC): ?created=2026-12-12 is every record created on that day. Anything like "since", "before" or "between" is a range.

Unreadable bounds fail the request

A bound Elasticsearch cannot read for the field's type is not dropped and not turned into an empty result. It is sent as it stands, Elasticsearch refuses the query, and the request fails with an error response — the API returns an error, the frontend renders its error response with an empty result set. This is different from an unknown field, which is ignored, and from an exact value no document carries, which is a normal empty result.

?price.usd=>abc error — not a number ?price.usd=> error — an empty bound (nothing after the symbol) is not an empty value ?published=>= error — same, on a date ?published=>now-7D error — "D" is not a date-math unit (use "d") ?published=>=yesterday fine — PHP reads it; midnight at the site

The reasoning: Elasticsearch coerces filter values to the field's type, so junk can only ever be a hard error, never a silently wrong filter — and inventing a value in its place would hide the caller's mistake.

Mixing spellings and forms — what actually happens

The two spellings, the comma, and PHP's own array rules can be combined, and most combinations do what you would expect. The ones that do not follow from three rules:

  1. The value's symbol beats a contradicting key. ?price.usd[gt]=<100 means price < 100, not gt: "<100". The symbol is the more deliberate of the two — a key is often just how a form or template happened to build the URL — and leaving it on the value would make the bound unreadable.
  2. A comma together with an operator key collapses to the last part, on every field type. With ?f[gte]=a,b every comma-separated part lands in the same gte slot, so b overwrites a. This is not a supported spelling; it is documented so the outcome is not a surprise. The array-key form exists for legacy URLs, which never combined the two.
  3. PHP's scalar-versus-array rule still applies. Mixing f= and f[]= for one field, or repeating f=, loses values exactly as on a keyword field — a bound is not special here.

price.usd below is a double core field on the facets index; fields.brand is a keyword facet.

RequestFilters byWhy
?price.usd=>=10,<=10010 ≤ price ≤ 100two bounds, comma-joined
?price.usd[gte]=10&price.usd[lte]=10010 ≤ price ≤ 100two keys on one array parameter
?price.usd[gte]=10&price.usd[]=<=10010 ≤ price ≤ 100array elements mix the key and value spellings freely
?price.usd[gte]=10&price.usd[]=77price = 77 (and ≥ 10)a key-form bound and an exact value intersect
?price.usd[whatever]=>10,<=10010 < price ≤ 100a key that is not an operator is decoration; the comma splits as usual
?price.usd=>10,<=100,77price = 77bounds and exact values intersect
?price.usd=>10,>=20price ≥ 20two bounds on one side: the last wins
?price.usd=>100,<10nothinga contradictory band — empty result, not an error
?price.usd[gt]=<100price < 100rule 1: the value's symbol beats the key
?price.usd[gte]=>10price > 10rule 1 — the key is ignored once the value carries a symbol
?price.usd[lte]=>=10price ≥ 10rule 1, even when the symbol contradicts the key's side
?price.usd[GTE]=10price ≥ 10key matching ignores case
?price.usd[gte]=10,77price ≥ 77 — 10 is lostrule 2: a comma with an operator key keeps the last part only
?price.usd[gte]=>1,<10price < 10 — >1 is lostrule 2 keeps <10; then rule 1 lets its symbol beat the gte key
?price.usd=>=10&price.usd[lte]=100price ≤ 100 — the lower bound is lostrule 3: f= and f[]= mixed — whichever came first is discarded
?price.usd[gte]=10&price.usd=<=100price ≤ 100 — lostrule 3, other direction
?price.usd=>=10&price.usd=<=100price ≤ 100 — lostrule 3: a repeated plain parameter keeps the last
?price.usd[gte]=10&price.usd[gte]=20price ≥ 20the same key twice: PHP keeps the last
?price.usd=> 10price > 10whitespace after the symbol is trimmed
?price.usd=%3E10price > 10URL-encoded >
?price.usd=>1,000nothingno thousands separators: >1 and exactly 0
?price.usd=>abcerror responseElasticsearch cannot read the bound
?price.usd=>error responsean empty bound is not an empty value
?price.usd=see Empty valuesan empty value, dropped or fail-closed per the view
?fields.brand=>nikethe literal >nikekeyword: no operator syntax
?fields.brand[gte]=nikenikekeyword: the key is decoration
?fields.brand[gte]=nike,adidasadidas — nike is lostrule 2 applies to every type — the operator-looking key collapses the comma list
?fields.brand[]=nike,adidasnike, adidasno operator-looking key, so the comma separates alternatives as usual

The safe habits that avoid every lossy row: use one spelling per field, never put a comma in an operator-keyed value, and when a field appears more than once always use [].

What cannot be passed

A value that contains a comma. Commas are always separators, with no escaping mechanism. A brand literally named Smith, Jones cannot be filtered — the request splits it into Smith and Jones:

?fields.brand=Smith, Jones → filters by "Smith" and "Jones", not "Smith, Jones"

If your data contains commas in filterable values, index a comma-free variant (a slug) as the facet and keep the display form in Labels. The same rule is why a number cannot carry a thousands separator.

A "between" spelling. There is no a..b, a-b or [between] form. Write two bounds: >=a,<=b.

Nested structures. Only a flat list of scalars is read. ?field[a][b]=x contributes nothing.

Unknown fields. A parameter that does not name an available filter field is ignored, with no error. This is deliberate — an unmapped field sent to Elasticsearch would fail the whole request — but it does mean a typo produces more results rather than an error. If a filter appears to do nothing, check the spelling and that it is listed in the view.

Fields the view does not allow. A filter field marked not changeable in the view ignores the request value; the view's configured value stays in force.

Reserved parameter names. These names are consumed as query settings before filtering happens, so a field of the same name can never be filtered:

Always reservedReserved only when q is present
q, limit, page, orderBy, highlightFieldsqueryType, minimumShouldMatch, tieBreaker, fuzzy, searchFields

The second column is conditional because those settings are only read when there is a full-text phrase to apply them to. A field named fuzzy would therefore filter on a request without q and stop filtering on one with q — avoid these names for facets and layout-area fields entirely.

Empty values

An empty value (?field=, or ?field[]=) collects no values at all. What happens then depends on the field's Ignore empty setting in the view:

  • Ignore empty on (the default) — the filter is dropped from the query. ?fields.brand= behaves as if fields.brand had not been sent. This is what you want when a search form submits every input whether or not the user filled it in.
  • Ignore empty off — the filter is kept with an empty value list, which matches no documents at all. The search returns an empty result.

Note. "Ignore empty off" does not mean "find records where this field is empty" — there is currently no way to express that. If you need it, leave Ignore empty on and do not send the parameter.

An operator with nothing after it — ?price.usd=> — is not an empty value. It is an empty bound, and it fails the request; see Unreadable bounds fail the request.

A request value replaces the view's default

When a view configures default Values for a filter field and the request sends that same field, the request value replaces the default outright — it is not added to it. That holds for bounds too: a view default of <=100 on price.usd is gone the moment the request sends any price.usd value, bound or not. Sending the field empty (with Ignore empty on) removes the filter entirely, including the view's default.

A view's default values are stored as plain strings, so a default bound is always written in the value spelling — >=10, <100 — never as a key.

How filters combine

  • Several exact values for the same field are OR-ed — a document matching any one of them is a hit.
  • A range and exact values on the same field are AND-ed — the exact values must also fall inside the band.
  • Different fields are AND-ed — a document must match every filtered field.
?fields.brand=nike,adidas&fields.color=black&price.usd=<100 → (brand = nike OR adidas) AND (color = black) AND (price < 100)

There is no cross-field OR, and no negation. minimumShouldMatch affects only the full-text query, never filters.

Casing

Whether a filter value must match the stored casing depends on the field:

FieldCasing
Master index — any .keyword filter field (name.keyword, tags.name.keyword, author.displayName.keyword, …)Case- and accent-insensitive
Master index — language.codeExact — it is a bare keyword, not a .keyword subfield, so it carries no folding. ?language.code=EN will not match a stored en.
Facets index — language.code, language.localeCase-insensitive
Facets index — other core fields (name.keyword, categoryPath.*)Exact
Facets index — a facet with Case Sensitive on (default)Exact
Facets index — a facet with Case Sensitive offCase- and accent-insensitive
Range-operator keys ([gte], [GTE]) and date-math unitsKeys: case-insensitive. Date-math units: case-sensitive (d is days, D is refused)

The master index has no case-sensitive string filters except language.code, because every other string field there is a text field whose automatic .keyword copy is folded. The facets index is the reverse: exact by default, folded only where you ask for it (or on the two language fields). Numbers and dates have no casing.

See Case Sensitive for what turning it off also does to the values you get back.

Boolean filter fields

A boolean facet accepts the usual spellings, in any casing:

ValueInterpreted as
1, true, yes, ontrue
0, false, no, offfalse
anything elsedropped
?fields.in-stock=1 ≡ ?fields.in-stock=true ≡ ?fields.in-stock=YES

Values that are not a recognised boolean are dropped. If none of the supplied values is one, the filter is kept with no values and matches no documents — ?fields.in-stock=banana returns an empty result, not unfiltered results. That is the same fail-closed answer an empty value gets when Ignore empty is off, and it means a mistyped boolean parameter never silently widens a result set.

Booleans take no range operators — ?fields.in-stock=>0 is the unreadable value >0, dropped like banana.

Passing parameters from a template

$_search->search() takes the same names as an associative array, and skips the query-string layer entirely — so the dot/underscore substitution and the scalar-versus-[] pitfalls do not apply. Pass a string or an array:

{$response = $_search->search('products', [ 'q' => 'shoes', 'fields.brand' => ['nike', 'adidas'], 'fields.color' => 'black', 'limit' => 20, 'page' => 1, ])}

Comma-splitting still applies to string values, so 'fields.brand' => 'nike,adidas' is equivalent to the array form above.

Range bounds take either spelling. The array's keys are read exactly as URL array keys are, so an operator-keyed array is the natural form here — and because a PHP array cannot repeat a key, the "last bound per side" and "comma in an operator-keyed value" cases from the table above simply cannot arise:

{* value spelling — a list of bounds and/or exact values *} {$response = $_search->search('products', [ 'price.usd' => ['>=10', '<=100'], 'published' => '>=now-1M', ])} {* key spelling — the operator is the array key *} {$response = $_search->search('products', [ 'price.usd' => ['gte' => 10, 'lte' => 100], 'indexed' => ['gte' => '2026-01-01', 'lt' => '2026-02-01'], ])} {* one string, comma-split like a URL value *} {$response = $_search->search('products', [ 'price.usd' => '>=10,<=100,77', ])}

Numbers may be passed as PHP numbers or as strings. Dates must be passed as strings — a \DateTime object is not accepted and breaks the search; format it first ($date->format('c') keeps its offset).