Start here

A bare word is matched across the document, and several are ORed, so the loosest useful query is one you already know how to write. Naming a field narrows it, in the shape field filtering takes almost everywhere else: field:value, with TO ranges and ^ boosts. What you know from Lucene, Elasticsearch or Solr reads the same here. Skip the search index entirely by naming the id. The same parser answers whether a person types the query or an agent sends it over MCP.

termMatch a term; several terms are ORed, so a document matching any one of them is returned. any
*Match every document. any
field:valueMatch a value in one field. text, string, i64, u64, f64, date, boolean, ip, json, facet
id:valueLook up one document by id. Fastest retrieval path. any
Text fields

Exact, approximate
or unfinished

Four ways to match words in order. Phrases need positions, which string fields do not index. These are text fields only.

field:"a b"Exact phrase, terms in order. Text fields only. text
field:"a b"~NPhrase allowing N extra words between terms. text
"a b pre"*Phrase whose last term is a prefix. Two or more terms. text
field:pre*Match every term starting with pre. One term, and the field is required. text, string
Numbers, dates, IPs

Everything ordered
can be bounded

Ranges work on text and string fields as well as the numeric ones, because the term dictionary is ordered. Brackets include the bound, braces exclude it.

field:[low TO high]Range, bounds inclusive. text, string, i64, u64, f64, date, ip
field:{low TO high}Range, bounds exclusive. text, string, i64, u64, f64, date, ip
field:[low TO *]Range with one side unbounded. text, string, i64, u64, f64, date, ip
field:>valueComparison: > < >= <=. i64, u64, f64, date
field: IN [a b c]Match any of several values. text, string, i64, u64, f64, date, boolean
field:/path/to/valueMatch a facet path. facet
More than one clause

Narrow it without
starting again

Operators compose, so a result set is refined by adding a clause rather than rewriting the query. That is what makes the grammar one an agent can generate incrementally.

AND / OR / NOTCombine clauses. Uppercase only. any
+term / -termRequire or exclude a clause. any
(...)Group clauses to control precedence. any
field:value^NWeight a clause's score contribution. text, string, i64, u64, f64, date, boolean, ip, json, facet
At the end of the query

Say what comes back
and how much

Modifiers ride at the end of the query text rather than in a separate parameter, so one string carries the whole request.

return f1,f2Return only these fields, in this order. title:rust return title,author
limit NCap the number of results. limit 0 is count-only: total_hits and no hits, skipping the key-value store, which is the cheapest way to ask how many documents match. title:rust limit 5
offset KSkip the first K results: the page, where limit is the page size. offset + limit is bounded by the same maximum limit is. title:rust limit 10 offset 20
sort field:descOrder by a field. See the sorting rules. title:rust sort year:desc

A modifier counts only where it opens an unbroken run of clauses reaching the end of the query, with query text left in front of it. Anything else is searched for, so find tax return forms is four terms and * limit 10 is how to ask for a bare limit.

A field list needs a comma between names, a limit needs a number, and a sort order must be exactly asc or desc. A clause that does not parse stays in the query rather than being applied in part.

A modifier naming a field the index does not have is reported as a dropped clause, since a projection would otherwise return documents with no fields.

Order

Exact, approximate
or refused

Ask for an order and one of three things happens, decided from the field's fast column before any shard runs.

Exact

The field has a fast column. describe_index lists those as sortable, and that is the whole test.

Approximate

A text or string field without one. The top 2 × limit matches by relevance are ordered alphabetically: a sample, not the first documents in the index, and a different sample on each page. The response says so, carrying _approximate_sort and a _warning.

Refused

Boolean, bytes, ip, json and facet. The request fails with cannot sort by 'FIELD' and the reason, before any shard runs, so no partial answer comes back with it. Retry on a different field.

A fast column is written when the index is built, so sortable cannot be switched on for an index that already holds data. Declare the field fast before writing to it. That works for text, string, and the numeric and date types, and does nothing on the five that are refused above.

Under a numeric or date sort every hit carries a _score of 1.0, because no relevance is computed. Do not read it as a ranking. Ascending unless desc is given.

Sorting is exact on a field with a fast column, and describe_index reports which those are as sortable. A text or string field without one is sorted approximately rather than refused; every other type without one is refused.

A sort the index cannot answer is a refusal, not a degraded result: the request fails with cannot sort by 'FIELD' and the reason: the index has no column of that name, or the type needs a fast column it does not have. It is decided before any shard runs, so no partial answer comes back with it. Boolean, bytes, ip, json and facet fields are the ones this catches in practice. Retry on a different field rather than re-sending.

An approximate sort collects the top 2 × limit matches by relevance and orders those alphabetically, so the result is not the alphabetically first documents in the index and paging through it re-orders a different sample on each page. The response says so: it carries _approximate_sort naming the field, and a _warning.

A field's fast column is written when the index is built, so sortable cannot be turned on for an index that already has data. Declare the field fast before writing to it, which works for text, string and the numeric and date types. It does nothing on a boolean, bytes, ip, json or facet field: no column is built for those, so fast on one reads back as false.

Under a numeric or date sort every hit carries _score of 1.0, because no relevance score is computed. Do not read it as a ranking.

Ascending unless desc is given.

Naming a field

Three things decide
whether a clause lands

How the name is written, whether the field is indexed at all, and, for dates, how much of an instant the literal actually means.

A dot is part of the name

Write k8s.node:worker-1 exactly as it reads. Escaping the dot makes the lookup miss, and paths into a json field are not queryable at all.

Indexed, or invisible

A field in the schema but not indexed cannot be queried. Fields discovered from a document arrive unindexed and stay that way until a schema update promotes them, so check the indexed flag before naming one.

A shadow is the id

Where the source data had its own identifier name, the schema keeps the name and only id keeps the value. Querying that name alone is the fastest retrieval there is, answered from the key-value store without the search index; hits come back under the shadow name.

created:2024-06-15An exact instant: midnight precisely. Matches nothing unless a document sits on that second.
created:[2024-06-15 TO 2024-06-16}A day. Any span is a range, never a bare literal.
created:[2024 TO 2025}A year, the same way.
created:"2024-06-15 12:00:00"A space needs quotes, or the remainder reads as a new clause. Writing the T avoids the question.
created:>9999-01-01Clamped to 2262-04-11, the far bound Tantivy represents, so it matches nothing rather than erroring.
Know the limits

What it will not do
said plainly

A clause that cannot match is dropped and reported rather than silently ignored, so a query that returns nothing tells you which part of it was the problem.

field:*Field-presence tests are not supported for any field type. The clause is dropped and reported. Use a bounded range, or match an explicit value.
pre*A prefix needs a field name; without one the * is dropped and pre is matched as a whole term. Name the field, or OR one clause per field.
field.subfield:valuePaths into a json field are not queryable. A json field is searchable only as unstructured text, so field:value matches any key or value inside it.
field:/regex/Regular expressions are disabled.

Reserved characters, which a backslash makes literal: + ^ ` : { } " ' [ ] ( ) ! \ * and space