Guides Markdown

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.

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

B=https://api.skimmerdb.com/v1
curl -s $B/namespaces/support/search -H "Authorization: Bearer $KEY" \
  -d '{"q": "acme-1042", "filter": {"status": "open"}}'
res = ns.search("acme-1042", filter={"status": "open"})
for hit in res.hits:
    print(hit.id, hit.line)
const res = await ns.search("acme-1042", { filter: { status: "open" } });
for (const hit of res.hits) console.log(hit.id, hit.line);

The answer:

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 pattern, case-sensitive unless it starts with (?i).
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}'
res = ns.search("E[0-9]{3}", regex=True, filter={"priority": {"gte": 1, "lt": 4}}, limit=50)
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.

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"]}
FormMeaning
"k": valueequal
"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.

Listing

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

curl -s $B/namespaces/support/search -H "Authorization: Bearer $KEY" \
  -d '{"q": "", "filter": {"status": ["open", "pending"]}, "limit": 100}'
res = ns.search("", filter={"status": ["open", "pending"]}, limit=100)
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.

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_
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.

curl -s $B/namespaces/support/count -H "Authorization: Bearer $KEY" \
  -d '{"q": "refund", "filter": {"status": "open"}}'
# {"count": 20, "exact": true, ...}
n = ns.count("refund", filter={"status": "open"})
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.

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.

curl -s $B/namespaces/support/warm -H "Authorization: Bearer $KEY" \
  -d '{"queries": ["refund", "ERR-404"]}'
ns.warm(["refund", "ERR-404"])
await ns.warm(["refund", "ERR-404"]);

More on what makes queries fast: Performance and cost.