For AI agents: this page is also available as Markdown at /docs/query-languages.md. Every docs page is: add .md to its URL. llms.txt lists them all.
Query languages
Every search names a language, and WarpSearch translates it into one native query that runs on
the same engine. Use whichever language your team, your tools or your existing queries already
speak. You can run any of them through the REST API, and the Elasticsearch, OpenSearch and Solr
languages are also served by drop-in endpoints for existing clients.
language |
What you send | Typical use |
|---|---|---|
sql |
One SELECT over one collection |
Analysts, BI, quick questions |
lucene |
Lucene query_string syntax |
Search boxes, Kibana and Grafana habits |
es |
An Elasticsearch or OpenSearch _search body |
Migrations, rich queries, aggregations, kNN |
solr |
Solr request parameters or a JSON Request body | Solr migrations |
logql |
A Loki LogQL query | Log search and log metrics |
grafana |
A Grafana Elasticsearch data source target | Dashboards |
luxir |
WarpSearch's native expression syntax | Application-authored queries, with safe variables |
The value luxir is the native language's name in the API. Filters, facets, metrics, sorting and
paging in a request work the same way whatever the language; see Searching and
Facets and aggregations.
#Sending a query
curl -X POST https://api.warpsearch.io/v1/search \
-H "Authorization: Bearer $WARPSEARCH_API_KEY" \
-H "Content-Type: application/json" \
-d '{"language": "sql", "query": "SELECT title, year FROM books WHERE year > 1960 ORDER BY year DESC LIMIT 5"}'
POST /v1/search takes the collection from the body's collection field or, for SQL, from FROM.
POST /v1/collections/{c}/search puts it in the path. The query is a string, except for es and
grafana, where it is a JSON object.
#See the translation
POST /v1/translate takes the same body and returns the native form without running it. The
response includes the native query, any warnings, and a replay: a request in the native
language that you can send to /v1/search to skip translation. Adding "include_translation": true
to a normal search puts the same information in its response.
This is the quickest way to learn what an unfamiliar language did with your query, and to check for approximations before you tune relevance.
#Rules that apply to every language
- Fields are the collection's fields, as listed by
GET /v1/collections/{c}.textfields are analyzed,keyword(string) fields match whole values. - A query is translated exactly when the engine can express it. When only the scores would
differ, you get the closest form and a warning. Otherwise the request fails with
400 unsupported_feature. Nothing is silently ignored. - Warnings are in the response's
warningsarray, and on compatible endpoints in aWarning: 299header. - Regex and wildcard queries depend on your plan and answer
403 forbiddenwhen it does not include them. See Limits and errors.
#SQL
One SELECT over one collection.
SELECT title, year FROM books
WHERE year BETWEEN 1960 AND 1980 AND genre IN ('scifi', 'fantasy')
AND MATCH(title) AGAINST('dune') AND author LIKE 'Frank%'
ORDER BY year DESC LIMIT 10 OFFSET 20
| Supported | Notes |
|---|---|
=, !=, <, <=, >, >=, IN, BETWEEN, LIKE, REGEXP, IS [NOT] NULL, AND, OR, NOT |
On a text field = is a phrase match, on a keyword field it is exact. |
Full text: MATCH(col) AGAINST('words'), match_query(col, 'words'), match_phrase(col, 'exact words'), query_string('lucene syntax') |
|
Dates: ISO-8601 strings, NOW() - INTERVAL 7 DAY |
|
COUNT(*), COUNT(col), AVG, SUM, MIN, MAX |
|
GROUP BY a, b, HAVING COUNT(*) >= n, ORDER BY <aggregate>, SELECT DISTINCT col |
Becomes nested terms facets. LIMIT caps the number of groups. HAVING supports COUNT(*) with > and >= only. |
Not supported: joins, subqueries,
UNION, window functions.
#Lucene
Lucene query_string syntax, as typed into Kibana or Grafana. Bare terms search the collection's
default fields.
{"language": "lucene",
"query": "title:dune AND year:[1960 TO 1980] -status:draft author:herb* name:smith~1 \"exact phrase\"~2 _exists_:isbn price:>=10"}
? and * become wildcards, a lone trailing * becomes a prefix query, and /re/ is a regular
expression. Use GET /v1/collections/{c}/search?q=... for a one-line version.
#Elasticsearch and OpenSearch Query DSL
Send an _search body as the query object.
{"language": "es",
"query": {"query": {"bool": {"must": [{"match": {"title": "dune"}}],
"filter": [{"range": {"year": {"gte": 1960}}}]}},
"aggs": {"genres": {"terms": {"field": "genre", "size": 5}}},
"size": 10}}
| Area | Supported |
|---|---|
| Full text | match, match_phrase, match_phrase_prefix (one word), match_bool_prefix, multi_match, combined_fields, query_string, simple_query_string |
| Term-level | term, terms, ids, exists, range, prefix, wildcard, regexp, fuzzy |
| Compound | bool, constant_score, dis_max, boosting, function_score (weight, field value factor, decays, random score), post_filter |
| Geo | geo_distance, geo_bounding_box, _geo_distance sort |
| Vectors | Top-level knn, the knn query and the OpenSearch knn query, alone or combined with text for hybrid search. num_candidates is capped at 10000. See Vector search and Hybrid search. |
| Other | sort, from/size, _source, track_total_hits, min_score, collapse with inner_hits |
| Aggregations | terms, histogram, date_histogram, range, date_range, filters, filter, global, top_hits, avg, sum, min, max, value_count, stats, extended_stats, cardinality, percentiles, percentile_ranks |
Some scores are approximations: best_fields and dis_max sum their clauses instead of taking the
maximum plus a tie breaker, and cross_fields does not blend terms. The same documents match, and
you get a warning.
Not supported:
nested,has_childandhas_parent,more_like_this,percolate, scripts,search_after,scroll, point in time,suggest,rescoreandruntime_mappings. These answer400 unsupported_feature.highlight,explainandtimeoutare accepted with a warning and have no effect.
Coming soon: highlighting and polygon geo queries.
Add ?format=es to /v1/collections/{c}/search for an Elasticsearch-shaped response, or use the
compatible endpoints with an existing client.
#Solr
Solr request parameters as a query string, or a JSON Request API object.
{"language": "solr",
"collection": "books",
"query": "q=dune messiah&defType=edismax&qf=title^2 body&mm=2&fq={!tag=g}genre:scifi&facet=true&facet.field={!ex=g}genre"}
Supported: the standard, dismax and edismax parsers, local params and $param references, fq
with tags and exclusions, function queries and bf/bq, sorting (including geodist()),
{!geofilt} and {!bbox}, {!knn}, classic and JSON faceting (field, range, query, interval,
pivot), stats, and grouping by one field. Solr-shaped responses come from
/compat/solr/{collection}/select; see Solr compatibility.
Not supported: joins and block join,
collapse,cursorMark, spellcheck, more-like-this, term statistics functions, atomic updates and delete by query.
#LogQL
Grafana Loki's query language. Labels are fields, and line filters search the collection's message field.
{"language": "logql",
"collection": "logs",
"query": "sum by (host) (count_over_time({app=\"api\"} |= \"timeout\" [5m]))",
"time_range": {"from": "2026-10-20T00:00:00Z", "to": "2026-10-20T06:00:00Z"},
"step": "1m"}
Metric queries come back as a date histogram, one series per group. Set the window with
time_range and the bucket width with step.
| Supported | Notes |
|---|---|
Stream selectors on labels, such as {app="api", level=~"warn|error"} |
|
Line filters |=, !=, |~ |
Match words and phrases in the message field, not raw substrings. |
json and logfmt stages |
No-ops, because fields are already indexed. |
count_over_time, rate, sum by, avg_over_time with unwrap |
rate returns counts per bucket. |
Not supported:
without(),bytes_*functions and binary operations between metric queries.
The Loki push and query HTTP APIs themselves are not offered; see Loki and LogQL.
#Grafana
The Grafana Elasticsearch data source target model: query, metrics and bucketAggs, usually
with time_range. A date_histogram with interval: auto picks a round width for about 100
buckets.
#Native syntax
The native language is for queries your application writes. It has real AND/OR/NOT
precedence, and $name variables that are always values and never syntax, so user input cannot
change the shape of a query.
{"language": "luxir",
"query": "title:$q AND year:>=1960 AND status:=live",
"vars": {"q": "dune messiah"},
"limit": 10}
| Form | Meaning |
|---|---|
title:dune |
Word match, analyzed like the field. |
title:"dune messiah"~2 |
Phrase, with slop. |
status:=live, status:=(a, b) |
Exact whole value, one or any of several. |
year:[1960 TO 1970], year:>=1965 |
Range. { } excludes an endpoint, * leaves it open. |
created:>=NOW/DAY-30DAYS |
Date math. |
title:mess*, author:Herbet~1, year:* |
Prefix, fuzzy (up to 2 edits), field exists. |
+title:dune -tag:beta, title:dune^2 |
Required, excluded, boost. |
wildcard(du*, field=title), regex(dune|kings, field=title) |
Wildcard and regular expression, whole term. |
simple_query($input, fields=[title, body]) |
Forgiving search-box syntax that never fails to parse. |
There is no default field: name one. ? and mid-word * are ordinary characters, and the pattern
queries above are functions.
#Compatible endpoints
Keep your client and point it at WarpSearch with the same API key:
| Client | Base URL |
|---|---|
| Elasticsearch | https://api.warpsearch.io/compat/es |
| OpenSearch | https://api.warpsearch.io/compat/opensearch |
| Solr | https://api.warpsearch.io/compat/solr/{collection} |
See Elasticsearch compatibility, OpenSearch compatibility
and Solr compatibility. Agents can run all of the languages above through the MCP
search and translate_query tools; see Guide for AI agents.