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.
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"}}}'hits = ns.search("refund", filter={"status": ["open", "pending"], "priority": {"gte": 2, "lt": 5}, "team": {"ne": "api"}}).hitsconst { 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.
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.
3and3.0are equal; the string"3"isn't the number3. - Ranges need numbers, on both sides.
gt,gte,ltandltetake 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
trueandfalse. - Missing keys. A document without the key fails every condition except
ne. To find documents that lack a key, use SQL'sIS 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 namedplanand 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 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 |
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 |