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).
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:
{"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-1042findsACME-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:
{"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.
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": trueand"partial": true.nextgoes on from the last hit given. - A count can't be cut short: it fails with
422 query_timeout. "partial": truealso 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.