# Searching

A search names a query language, a query, and optionally filters, sorting, paging and the fields to
return. WarpSearch runs it over one consistent view of the collection and returns ranked hits,
plus any facets and metrics you asked for, in one response. This page covers the request and
response. The languages themselves are in [Query languages](query-languages.md), analytics in
[Facets and aggregations](facets-and-aggregations.md), and vectors in
[Vector search](vector-search.md) and [Hybrid search](hybrid-search.md).

## A first search

```bash
curl -X POST https://api.warpsearch.io/v1/collections/books/search \
  -H "Authorization: Bearer $WARPSEARCH_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "language": "lucene",
    "query": "title:dune",
    "filters": ["year:[1960 TO 1980]", "genre:scifi"],
    "sort": ["year desc"],
    "fields": ["title", "year"],
    "limit": 10
  }'
```

```json
{
  "took_ms": 3,
  "collection": "books",
  "total": 2,
  "offset": 0,
  "hits": [
    {"id": "b2", "fields": {"title": "Dune Messiah", "year": 1969}},
    {"id": "b1", "fields": {"title": "Dune", "year": 1965}}
  ]
}
```

The request needs a viewer role or higher. The collection can be in the path
(`/v1/collections/{c}/search`) or in the body (`POST /v1/search`, where SQL takes it from `FROM`).

## Request fields

| Field | Meaning |
|---|---|
| `language` | Required. `sql`, `es`, `lucene`, `solr`, `logql`, `grafana` or `luxir` (the native syntax). See [Query languages](query-languages.md). |
| `query` | The query. A string for most languages, a JSON object for `es` and `grafana`. Omitted means match everything. |
| `collection` | The collection, when you use `POST /v1/search`. |
| `filters` | An array of non-scoring restrictions, written in the request's language. They narrow the matches without changing scores. |
| `sort` | An array such as `["year desc", "_score"]`. |
| `offset`, `limit` | Paging. Both count hits. |
| `fields` | Which fields to return for each hit. |
| `facets`, `metrics` | Analytics over all matches. See [Facets and aggregations](facets-and-aggregations.md). |
| `vars` | Values for `$name` placeholders in native queries. Never parsed as syntax. |
| `time_range` | `{"from": ..., "to": ..., "field": ...}`: restrict to a time window. Handy with logs and `grafana`. |
| `time_zone` | Zone for date literals, date math and calendar buckets, for example `America/Denver`. |
| `default_operator` | `OR` (default) or `AND` for bare words in a Lucene-style query. |
| `track_total` | Default `true`. Set `false` to skip counting every match. |
| `scores` | Default `true`. Set `false` to skip returning scores. |
| `include_translation` | Adds the native form of your query to the response. |

Unknown fields in the request are rejected, so a typo is an error, not a silently ignored option.

For quick checks there is also `GET /v1/collections/{c}/search?q=...&limit=...&offset=...&sort=year:desc&fields=title,year`,
which runs `q` as a Lucene query.

## Filters versus query

Put what should rank documents in `query` and what should only include or exclude them in
`filters`. A filter does not affect scores and is cheaper. Typical filters are tenant, category,
status and date range.

```json
{
  "language": "es",
  "query": {"match": {"title": "dune"}},
  "filters": [{"term": {"genre": "scifi"}}, {"range": {"year": {"gte": 1960}}}]
}
```

## Sorting

Sort by a field, by relevance, or by several keys in order: `["genre", "year desc", "_score"]`. A
sort without a direction is ascending for fields and descending for `_score`. Documents missing the
sort value come last in either direction.

Sort on `keyword`, number and date values (the `es` language can also sort by geo distance). A `text` field cannot be sorted by its
whole value, so give it a `keyword` sub-field; see
[Schemas and field types](schemas-and-fields.md).

## Paging

Paging is offset based: `limit` is the page size and `offset` the number of hits to skip.

```json
{"language": "lucene", "query": "title:dune", "limit": 20, "offset": 40}
```

`offset + limit` cannot exceed your plan's result window, and `limit` cannot exceed the plan's
maximum page size; either answers `422 limit_exceeded`. Deep pages cost more than shallow ones
because the engine keeps `offset + limit` candidates. Show a few pages and let people narrow with
filters rather than offering page 500.

> **Not supported:** cursors, `search_after`, scroll and point in time. Use filters (for example on
> an increasing id or date) to walk through a large result set.

## Counting

`total` is the exact number of matches by default, which means evaluating every matching
document. When you do not display a total, set `"track_total": false` and the query can skip
documents that cannot reach the top results. That makes broad queries on large collections much
faster and cheaper. To count only, use the count endpoint:

```bash
curl -X POST https://api.warpsearch.io/v1/collections/books/count \
  -H "Authorization: Bearer $WARPSEARCH_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"language": "lucene", "query": "genre:scifi"}'
# {"count": 1204, "took_ms": 2}
```

> **Coming soon:** a bounded total hit count (`10000+`) that is cheaper than an exact one.

## Choosing fields

`fields` limits what comes back, which keeps responses small and cheap. Without it you get every
retrievable field, except vectors and geo points. Name a vector field to get it back. Hits that
lack a requested field simply omit that key.

> **Coming soon:** a stored `_source` copy of each document, and highlighting.

> **Not supported:** highlighting and snippets today, suggesters and autocomplete, and
> more-like-this.

## What each query type does

| You write | It does |
|---|---|
| A word (`dune`) on a `text` field | Matches the word after analysis (case folded, stemmed for English fields) and scores with BM25. |
| A phrase (`"way of kings"`) | Matches the words in order. A slop (`"way kings"~2`) allows words in between. |
| A value on a `keyword` field | Matches the whole value exactly. |
| A prefix, wildcard, regex or fuzzy term | Matches indexed terms. Wildcard and regex depend on your plan. Fuzzy allows up to 2 edits. |
| A range (`year:[1960 TO 1980]`) | Compares numbers, dates or terms. Dates accept date math such as `now-30d`. |
| A `knn` clause | Finds nearest vectors; see [Vector search](vector-search.md). |
| Geo distance or box | Matches points within a radius or rectangle, through the Elasticsearch and Solr languages. |

Geo queries are available in the `es` language (`geo_distance`, `geo_bounding_box`, and sorting by
`_geo_distance`) and in `solr` (`{!geofilt}`, `{!bbox}`, `geodist()`).

> **Coming soon:** polygon geo queries.

## Dates and time zones

Dates in queries are interpreted in UTC unless the request sets `time_zone`. A partial date
matches its whole window: `2026-06` is all of June. With a zone set, `now/d` starts at local
midnight and calendar buckets follow local days across daylight-saving changes. Documents are
always ingested in UTC, so write an explicit `Z` or offset when the exact instant matters.

## Warnings

A request can succeed with `warnings`, an array of strings. They tell you when WarpSearch
approximated something instead of rejecting it, for example a score approximation in an
Elasticsearch `multi_match` or an analyzer that fell back to `standard`. Read them while you are
building. Compatible endpoints also send them in a `Warning: 299` response header.

## Result shape

| Field | Meaning |
|---|---|
| `took_ms` | Time spent on the search. |
| `total` | Number of matches (exact unless `track_total` is `false`). |
| `hits` | The page: each hit has `id`, `score` and `fields`. |
| `facets`, `metrics` | Present when requested. |
| `translation` | Present with `include_translation`. |
| `warnings` | Approximations and adjustments. |

Pass `?format=es` on `POST /v1/collections/{c}/search` to get an Elasticsearch-shaped response
instead.

## Errors

| Status | Code | Meaning |
|---|---|---|
| `400` | `query_error` | The query could not be parsed. The `detail` says where. |
| `400` | `unsupported_feature` | The query uses something WarpSearch does not offer. |
| `400` | `invalid_field` | A field name does not exist or is not allowed. |
| `403` | `forbidden` | Your key lacks the role on this collection. |
| `404` | `not_found` | No such collection, or you have no role on it. |
| `422` | `limit_exceeded` | A plan limit such as page size, result window, clauses or facets. |
| `429` | `rate_limited` | Slow down; honor `Retry-After`. |
| `503` | `warming_up` | The collection is loading. Retry after `Retry-After`. |
| `504` | `timeout` | The request ran past your plan's time limit. Narrow it with filters. |

Full list and handling advice in [Limits and errors](limits-and-errors.md).

## Tips

- Search-box input should go through a language that tolerates punctuation. In `luxir`, bind the
  text to a variable (`"vars": {"q": "..."}`) so a stray quote or `AND` cannot break the query.
- Narrow with filters before you widen with `limit`.
- Return only the fields you show, and keep page sizes small. Response bytes are billed beyond
  your allowance; see [Pricing](pricing.md#keeping-response-size-down).
- Use `track_total: false` whenever you do not show a count.
