Skip to content

Guides

Filters and projection

How a filterable field narrows a query, how filter values are typed and encoded, and how to ask for stored fields back with projection.


Two schema options decide what a field can do at query time. indexed makes a field searchable text. filterable makes it comparable as a whole value. stored makes it readable back from a hit. A field can be all three, but as schemas explains, writing indexed: true turns the other two off unless you write them out.

This page covers the query side of filterable and stored.

A filter is a term on a field that is not indexed

There is no separate filter syntax to learn. A filterable-but-not-indexed field used as a term in either grammar becomes an exact filter:

Written Lowers to
team:storage EqualityFilter
price:[1.0 TO *] NumericRangeFilter
published:[2024-01-01 TO *] TimeRangeFilter
[team="storage"] (CQP) EqualityFilter
zsh
ysearch search articles 'body:"object storage" AND category:engineering'

explain --index shows the split between the lowered query and its filters, which is the way to confirm a clause landed where you expected:

zsh
ysearch explain 'body:"object storage" AND category:engineering' --index articles

The output's filters: block reads none or one field = value line each.

Ranges

Ranges apply to numeric and timestamp fields only.

Query Refusal
a range on an indexed field field "price" is indexed; ranges apply to filterable fields
a range on a string field a range over a string field is not supported

A timestamp bound is RFC 3339, YYYY-MM-DD, or unix milliseconds. Bracket style decides inclusivity: [1 TO 20] includes both bounds, {1 TO 20} excludes both, [1 TO 20} mixes, and * is unbounded on that side.

zsh
ysearch search articles 'title:incident AND published:[2026-01-01 TO *]'
ysearch search articles 'price:[1 TO 20}'

--filter

--filter field=value adds an equality filter directly, without going through the query string. It is repeatable and it is equality only — there is no range form of the flag. The value is everything after the first =, so a value containing = needs no escaping:

zsh
ysearch search articles 'body:cache' \
  --filter category=engineering --filter site=example.test

The value is typed by the field, and a value the field's type cannot read is refused before anything is sent:

value "x" is not an integer
value "x" is not a boolean (true|false|1|0)
value "x" is not a timestamp (RFC 3339, YYYY-MM-DD, or unix millis)

A malformed flag is an invocation error and exits 2: --filter must be field=value, got "x".

Typed values on the wire

A filter value carries the field's type, encoded as the attribute lane stores it:

Field type Encoding
STRING raw UTF-8
INT64 8-byte big-endian
TIMESTAMP 8-byte big-endian
DOUBLE IEEE-754 bits
BOOL one byte
BYTES never filterable

This is why a filter compares whole values rather than analyzed tokens, and why a regular expression on a filterable field is refused: there is no dictionary of terms to expand against.

Filters are a conjunction

Every filter on a request is required. A filter in an optional (Should) or negated (MustNot) position is refused, and the refusal names the spelling that works:

Position Refusal Write instead
optional an optional filter is not supported +field:value
negated a negated filter is not supported exclude with a term on an indexed field

A query of only filters and exclusions is a match-all with those applied, which is a legitimate way to page through a slice of an index:

zsh
ysearch search articles 'category:engineering' --keys --top-k 1000

Exclusion is a term, not a filter, and contributes nothing to scoring:

zsh
ysearch search articles 'status -beta'

Projection: getting the document back

A hit is an ID and a score. --fields asks the server for stored values, returned as one document frame per result frame and joined by position:

zsh
ysearch search articles 'body:cache' --fields title,site,url
ysearch search articles 'body:cache' --fields '*'

* requests every stored field, increasing projection work and response size.

Three rules govern projection:

  1. A name that is not a stored field is INVALID_ARGUMENT listing the stored fields. An index with no stored fields refuses projection outright.
  2. search always projects the key field alongside what you asked for, so every hit carries its key and mutation version — then hides the key from the columns that already show it.
  3. A document a particular segment cannot answer — a format-v1 segment, or one built before the field became stored — comes back unavailable rather than failing the search. Missing stored values do not change the retrieval path or its reported exactness class. The response header's projection_unavailable_segments names the segment.

Rule 3 is the one that shows up after a schema evolution. Turning stored on mints a new schema version, but the segments built under the old version do not gain the lane retroactively; they answer unavailable for values that were not stored. Re-ingest the source documents under the updated schema to make those values available.

Reading the table

The table output prints KEY, SCORE to four decimals, VERSION, then one column per projected field. A repeated value is joined with , and an absent one is —.

Collapse groups hits by key in first-appearance order and shows the one with the greatest mutation_version; the version cell then reads … (n versions). --no-collapse prints every hit in rank order instead.

zsh
ysearch search articles 'body:cache' --fields title,url --no-collapse

A keyless document — the form inherited from yolosearch's first milestone, with no key field in the schema — has no key to collapse on. It renders as id:<high><low> in hex and stands alone.

Result shapes and their cost

Three flags choose what the server has to produce, and the difference is large on a big result set:

Flag Returns Cost
--fields a,b hydrated stored values pays for stored-field reads
--fields '*' every stored field the most expensive form
--keys logical schema keys, one per line projects only the schema key
--ids 128-bit public IDs as lowercase hex, one per line requests neither scores nor stored documents — the cheapest large-result path
zsh
ysearch search articles cache --fields title,url --top-k 100
ysearch search articles cache --keys --top-k 100
ysearch search articles cache --ids --top-k 100

--keys and --ids are line-oriented streaming modes built for large result sets; use them when a downstream process is going to fetch the documents itself.

JSON output

--json prints {"header", "hits":[…], "trailer"} — the protojson header and trailer, and every hit, never collapsed:

json
{"id":{"high":"…","low":"…"},"score":0.0,"key":"…","mutation_version":"…","fields":{}}

The id halves are decimal strings in JSON where the table shows them in hex. Because JSON output is never collapsed, a client consuming it has to do its own collapse by key if it wants one live version per key — or rely on the generation's liveness object, which already excluded superseded ordinals before top-K. See ingest for what liveness means.

Reaching a form the grammars do not spell

--request FILE reads a protojson SearchRequest instead of a query string. --top-k and --fields still override what the file says, and --top-k 0 keeps the file's value. That is the way to a PrefixTermQuery, which neither grammar spells:

zsh
ysearch search articles --request prefix-query.json --fields title,url

A positional query beside --request is an invocation error; so is neither.

Validation errors

Message Cause
field "price" is indexed; ranges apply to filterable fields the field is text, not an attribute
a range over a string field is not supported ranges are numeric and timestamp only
an optional filter is not supported write +field:value
a negated filter is not supported exclude with a term on an indexed field
--filter must be field=value, got "x" an invocation error; exits 2
INVALID_ARGUMENT listing the stored fields --fields named something not stored
FAILED_PRECONDITION a segment lacks the attribute lane a filter needs

Filters need the attribute lane, and phrases need the positions lane. Both arrived with segment format 2; a format-v1 segment refuses each with FAILED_PRECONDITION naming the segment.

Next