# Filters

> The filter syntax for search, listing, counts and vector queries: equality, one of, not equal, numeric ranges, and how values compare.

A filter keeps only the documents whose `meta` passes every condition. It is a JSON object: each key is a top-level `meta` key, and each value is the condition on it. Filters work the same in text search, listing (an empty `q`), counts, vector search and hybrid queries, over REST and MCP. They're applied before any document is read, so a selective filter makes a query over a big namespace fast and cheap.

```sh title="curl"
curl -s $B/namespaces/support/search -H "Authorization: Bearer $KEY" \
  -d '{"q": "refund", "filter": {"status": ["open", "pending"], "priority": {"gte": 2, "lt": 5}, "team": {"ne": "api"}}}'
```

```python title="Python"
hits = ns.search("refund", filter={"status": ["open", "pending"], "priority": {"gte": 2, "lt": 5}, "team": {"ne": "api"}}).hits
```

```ts title="TypeScript"
const { hits } = await ns.search("refund", { filter: { status: ["open", "pending"], priority: { gte: 2, lt: 5 }, team: { ne: "api" } } });
```

## Conditions

| Filter | Keeps documents where |
|---|---|
| `{"status": "open"}` | `status` equals `"open"` |
| `{"status": {"eq": "open"}}` | The same, spelled out |
| `{"status": ["open", "pending"]}` | `status` is one of the values |
| `{"status": {"in": ["open", "pending"]}}` | The same, spelled out |
| `{"team": {"ne": "api"}}` | `team` isn't `"api"`: a document without `team` passes |
| `{"priority": {"gt": 2}}` | `priority` is a number greater than 2 |
| `{"priority": {"gte": 2, "lt": 5}}` | A number from 2 up to, not including, 5 (`gt`, `gte`, `lt`, `lte` combine) |
| `{"ok": true}` | `ok` is the boolean `true` |

Every key must hold (AND). For an OR across different keys, run two queries or use [SQL](/reference/sql).

## How values compare

- **Exactly.** Strings compare as exact strings: `"Open"` isn't `"open"`. Normalise the values you filter on when you write them.
- **Numbers as numbers.** `3` and `3.0` are equal; the string `"3"` isn't the number `3`.
- **Ranges need numbers**, on both sides. `gt`, `gte`, `lt` and `lte` take a number, and a document whose value isn't a number (or that lacks the key) doesn't pass. Store times as integer seconds since 1970 to filter them by range: `{"ts": {"gte": 1760572800}}`. An ISO date string can't be range-filtered.
- **Booleans** match `true` and `false`.
- **Missing keys.** A document without the key fails every condition except `ne`. To find documents that lack a key, use SQL's `IS NULL`.

## Top-level keys only

A filter reads top-level `meta` keys with scalar values.

- **Nested objects** can't be filtered by their fields: `{"user": {"plan": "pro"}}` is read as an operator named `plan` and refused. Flatten what you filter on: `{"user_plan": "pro"}`.
- **Arrays don't match by element.** A document with `"tags": ["billing", "urgent"]` doesn't pass `{"tags": "urgent"}` (an array equals only the identical array). For tags you filter on, write a boolean each (`"tag_urgent": true`) and filter `{"tag_urgent": true}`.

See [Data model](/guides/data-model) for shaping metadata.

## Where filters go

| Call | Field |
|---|---|
| `POST .../search`, `POST .../count` | `"filter": {...}` in the body |
| `GET .../search`, `GET .../count` | `?filter=` the JSON, URL-encoded |
| Vector and hybrid search (`POST .../search` with `vector`) | `"filter": {...}`: the nearest among the documents that pass |
| MCP `search` and `count` | The `filter` argument |

```sh title="curl"
curl -s -G $B/namespaces/support/search -H "Authorization: Bearer $KEY" \
  --data-urlencode 'q=refund' --data-urlencode 'filter={"status": "open"}'
```

Count estimates (`"exact": false`) don't support filters: use an exact count. Linked namespaces (an index linked by reference) don't support filters either.

## Errors

A filter that can't be read is a 422 `invalid`, saying which key and why:

| Message | Cause |
|---|---|
| `filter must be a JSON object` | The filter isn't an object |
| `filter priority: gte takes a number` | A range operator with a non-number |
| `filter user: unknown operator plan (in, eq, ne, gt, gte, lt, lte)` | An object value whose keys aren't operators: usually a nested object |
