# Search and filters

> Exact text and regex search over documents, narrowed by metadata filters, with cursors, exact counts and estimates.

Search finds documents whose `text` contains what you ask for, exactly: a literal string,
ignoring case, or an RE2 regex. Filters on `meta` narrow it. Results come newest first, one per
document, with the matched text and the line around it. Counts are exact.

## Search

`POST /v1/namespaces/{name}/search` with `{"q", "regex", "filter", "limit", "cursor", "scan"}`,
or `GET` with the same query parameters (`filter` as JSON).

```sh title="curl"
B=https://api.skimmerdb.com/v1
curl -s $B/namespaces/support/search -H "Authorization: Bearer $KEY" \
  -d '{"q": "acme-1042", "filter": {"status": "open"}}'
```

```python title="Python"
res = ns.search("acme-1042", filter={"status": "open"})
for hit in res.hits:
    print(hit.id, hit.line)
```

```ts title="TypeScript"
const res = await ns.search("acme-1042", { filter: { status: "open" } });
for (const hit of res.hits) console.log(hit.id, hit.line);
```

The answer:

```json title="Answer"
{"namespace": "support", "hits": [{"id": "t-1", "match": "ACME-1042", "line": "Refund requested for order ACME-1042",
  "meta": {"priority": 3, "status": "open"}, "written_at": "2026-10-05T01:30:12Z"}],
 "next": "", "docs": 2, "took_ms": 0.41, "partial": false, "temperature": "warm",
 "store_gets": 0, "store_bytes": 0, "cpu_ms": 0.12, "timed_out": false}
```

Each hit has the document's `id`, the text that matched (`match`), the `line` it's on, the
document's `meta` and when it was written. Not the whole text: fetch that with
`GET .../docs/{id}` when you need it.

## Text and regex

- **Plain text** (the default) is matched literally, ignoring case. `acme-1042` finds
  `ACME-1042`. No tokenising, stemming or ranking: a document matches or it doesn't.
- **Regex** with `"regex": true`: an [RE2](https://github.com/google/re2/wiki/Syntax) pattern,
  case-sensitive unless it starts with `(?i)`.

```sh title="curl"
curl -s $B/namespaces/support/search -H "Authorization: Bearer $KEY" \
  -d '{"q": "E[0-9]{3}", "regex": true, "filter": {"priority": {"gte": 1, "lt": 4}}, "limit": 50}'
```

```python title="Python"
res = ns.search("E[0-9]{3}", regex=True, filter={"priority": {"gte": 1, "lt": 4}}, limit=50)
```

```ts title="TypeScript"
const res = await ns.search("E[0-9]{3}", { regex: true, filter: { priority: { gte: 1, lt: 4 } }, limit: 50 });
```

`q` searches `text` only, never ids or metadata. See [Data model](/guides/data-model).

### Regexes the index can't narrow

The index finds candidates from a regex's literals (3 or more characters) and from character
classes it can look up, like `[a-z][0-9]{2}[a-z]{3}` or `G-[A-Z0-9]{10}`, so those don't read
every document. A pattern with neither, like `.*[0-9]$`, has to check every document. On a
namespace of more than 100,000 documents (or about 100 MB of them) such a query is refused
with `422 scan_refused`, unless it says `"scan": true`; then it runs within the plan's time
limit.

## Filters

Every key in `filter` must hold:

```json
{"status": "open", "priority": {"gte": 2, "lt": 5}, "team": {"ne": "api"}, "region": ["eu", "us"]}
```

| Form | Meaning |
|---|---|
| `"k": value` | equal |
| `"k": [a, b]` or `{"in": [a, b]}` | one of |
| `{"ne": value}` | not equal (a missing key counts as not equal) |
| `{"gt" \| "gte" \| "lt" \| "lte": number}` | a numeric range |

Filters apply to top-level metadata keys. Numbers compare as numbers; strings exactly. Filters
prune the search before any document is read, so a selective filter over a big namespace is
fast. All the rules: [Filters](/reference/filters).

## Listing

An empty `q` lists documents, newest first, narrowed by the filter:

```sh title="curl"
curl -s $B/namespaces/support/search -H "Authorization: Bearer $KEY" \
  -d '{"q": "", "filter": {"status": ["open", "pending"]}, "limit": 100}'
```

```python title="Python"
res = ns.search("", filter={"status": ["open", "pending"]}, limit=100)
```

```ts title="TypeScript"
const res = await ns.search("", { filter: { status: ["open", "pending"] }, limit: 100 });
```

## Pages and cursors

`limit` is 20 by default, at most 1,000. When there are more hits, the answer's `next` is a
cursor: pass it back as `cursor` with the same query for the next page. `""` means no more.

```python title="Python"
cursor = None
while True:
    res = ns.search("timeout", cursor=cursor, limit=200)
    for hit in res.hits:
        ...
    if not res.next_:          # Python names the field next_
        break
    cursor = res.next_
```

```ts title="TypeScript"
let cursor: string | undefined;
do {
  const res = await ns.search("timeout", { cursor, limit: 200 });
  for (const hit of res.hits) { /* ... */ }
  cursor = res.next || undefined;
} while (cursor);
```

Cursors name where a document was first written, so they stay valid while the namespace is
compacted: pages go on where they left off. Documents written since appear on a new search.

## Count

`POST /v1/namespaces/{name}/count` with `{"q", "regex", "filter"}` (or `GET`). Exact by
default: every visible document that matches.

```sh title="curl"
curl -s $B/namespaces/support/count -H "Authorization: Bearer $KEY" \
  -d '{"q": "refund", "filter": {"status": "open"}}'
# {"count": 20, "exact": true, ...}
```

```python title="Python"
n = ns.count("refund", filter={"status": "open"})
```

```ts title="TypeScript"
const n = await ns.count("refund", { filter: { status: "open" } });
```

On big namespaces, `"exact": false` estimates fast from a spread sample of the candidates
(exact when there are few; no filters). `"sample": 1000` sets how many are checked (default
2,000), and `lo`..`hi` is a 95% interval for `count`.

For grouped counts (by status, by day, by user), use [SQL](/guides/analytics).

## Time limits and partial answers

A search, count or vector query runs at most the plan's time limit: 10 seconds on free, 30 on
pro.

- Past it, a **search** answers what it found so far, with `"timed_out": true` and
  `"partial": true`. `next` goes on from the last hit given.
- A **count** can't be cut short: it fails with `422 query_timeout`.
- `"partial": true` also means object storage failed for some reads, so hits may be missing.
  Treat a partial answer as incomplete, never as "there are no more".

`cpu_ms` is the CPU the query used (metered, not billed).

## Warm a namespace

`POST /v1/namespaces/{name}/warm` with the searches you're about to run opens the namespace
(if it isn't) and fetches what those searches need, so an agent about to work on a namespace
gets warm answers from its first query. It answers the `temperature` before warming, what it
read and how long it took, and counts as a query.

```sh title="curl"
curl -s $B/namespaces/support/warm -H "Authorization: Bearer $KEY" \
  -d '{"queries": ["refund", "ERR-404"]}'
```

```python title="Python"
ns.warm(["refund", "ERR-404"])
```

```ts title="TypeScript"
await ns.warm(["refund", "ERR-404"]);
```

More on what makes queries fast: [Performance and cost](/guides/performance).
