A filter is a JSON object matched against each document in a collection. Bare fields test equality; field values wrapping $-prefixed operators express richer predicates. Multiple fields in one filter are implicitly ANDed, and the empty filter {} matches every document.
You run a filter with find:
omgdb find app.omgdb users '{"age":{"$gte":30}}'
The filter is compiled and validated into an internal AST before matching, so structurally invalid filters and unknown operators are rejected up front with precise, structured errors an agent can repair from (see Errors and self-repair). For how filters choose an index or fall back to a scan, see indexes; to inspect the plan or debug a no-results query, see introspection.
The find command
omgdb find <store> <collection> [<filter>] [--limit N] [--skip N] [--sort '<spec>']
[--project '<spec>'] [--after-id '<json>'] [--after-sort-key '<json>']
| Argument / flag | Meaning |
|---|---|
<store> | Path to the store directory (e.g. app.omgdb). |
<collection> | Collection (namespace) name. |
<filter> | MongoDB-style filter as a JSON string. Defaults to {} (match all). |
--limit N | Print at most N matching documents. |
--skip N | Skip the first N matching documents before printing. |
--sort '<spec>' | Sort JSON ({"field":1,"other":-1}), applied before skip/limit. Sorts are deterministic, with _id tie-breaks. |
--project '<spec>' | Projection JSON; see Projection. |
--after-id '<json>' | Stateless pagination in _id order: only print matches whose _id sorts after this value. Cannot be combined with --sort. |
--after-sort-key '<json>' | Stateless pagination through a sorted scan: resume after this sort key. Requires --sort. |
Each match prints as one line of canonical JSON. --after-id and --after-sort-key are mutually exclusive; for persistent cursors and the full pagination workflow see the CLI reference.
Comparison operators
| Operator | Description | Example |
|---|---|---|
$eq | Field equals the operand. This is the implicit operator for a bare value: {"name":"ana"} compiles to $eq. | {"name":{"$eq":"ana"}} |
$ne | Field does not equal the operand. Also matches a missing field. | {"age":{"$ne":25}} |
$gt | Field is greater than the operand under the total value order. | {"age":{"$gt":20}} |
$gte | Field is greater than or equal to the operand. | {"age":{"$gte":30}} |
$lt | Field is less than the operand. | {"age":{"$lt":30}} |
$lte | Field is less than or equal to the operand. | {"age":{"$lte":65}} |
$in | Field equals any value in the operand array. | {"role":{"$in":["admin","root"]}} |
$nin | Field equals none of the values in the operand array. Also matches a missing field. | {"role":{"$nin":["user"]}} |
$in and $nin require an array operand; anything else is a compile error. Combining bounds on one field intersects them — {"age":{"$gt":20,"$lt":40}} matches ages strictly between 20 and 40.
{"name":"ana"}
{"age":{"$gte":30}}
{"age":{"$gt":20,"$lt":40}}
{"age":{"$ne":25}}
{"role":{"$in":["admin","root"]}}
{"role":{"$nin":["user"]}}
Note:
$eq,$ne,$in, and$ninuse value equality, whereNaNnever equals itself. The ordering operators ($gt/$gte/$lt/$lte) use the total order, which placesNaNconsistently. Equality and ordering can therefore disagree onNaN.
Element operators
| Operator | Description | Example |
|---|---|---|
$exists | Matches on field presence (true) or absence (false). | {"age":{"$exists":true}} |
$type | Field’s type matches a type name, a numeric BSON code, or any entry in an array of those. | {"age":{"$type":"long"}} |
$exists requires a boolean operand and only tests whether the (dotted) path resolves — it does not inspect the value’s type.
{"age":{"$exists":true}}
{"missing":{"$exists":false}}
{"age":{"$type":"long"}}
{"age":{"$type":"number"}}
{"v":{"$type":["bool","long"]}}
{"name":{"$type":2}}
Type names come from the value’s BSON-style type. Note that integers report as long and floats as double:
| Value | $type name | BSON code |
|---|---|---|
null | null | 10 |
| boolean | bool | 8 |
integer (i64) | long | 18 |
float (f64) | double | 1 |
| string | string | 2 |
| binary data | binData | 5 |
| array | array | 4 |
| object / sub-document | object | 3 |
| ObjectId | objectId | 7 |
| date / timestamp | date | 9 |
The alias number matches both long and double. An unknown type name is a compile error with a did-you-mean suggestion (strnig suggests string), and int/decimal — types OMGDB never stores — are redirected explicitly: $type: OMGDB stores no `int` values (integers are `long`); use `long`, `double`, or `number` . That redirect matters to an agent: a naive {"$type":"int"} would otherwise compile into a predicate that can never match.
Logical operators
| Operator | Description | Example |
|---|---|---|
$and | All listed sub-filters must match. | {"$and":[{"a":5},{"b":"x"}]} |
$or | At least one sub-filter must match. | {"$or":[{"a":1},{"b":"x"}]} |
$nor | None of the listed sub-filters may match. | {"$nor":[{"a":1},{"b":"y"}]} |
$not | Negates a field-level operator expression. | {"a":{"$not":{"$gt":10}}} |
$and, $or, and $nor each require an array of filter objects. Because every filter is already an implicit AND of its fields, $and is only needed when you want multiple conditions on the same field path that cannot be expressed in one object.
{"$and":[{"a":5},{"b":"x"}]}
{"$or":[{"a":1},{"b":"x"}]}
{"$nor":[{"a":1},{"b":"y"}]}
{"a":{"$not":{"$gt":10}}}
$not is a field-level operator: it wraps an operator-expression object (every key starting with $) and matches when that inner expression does not. It cannot wrap a bare scalar — {"a":{"$not":5}} is rejected as malformed.
Note: The planner accelerates equality, scalar ranges, and
$in(planned as a union of equality buckets) on indexed top-level fields, including compound-index shapes. Predicates nested inside a top-level$or,$nor, or$notare generally not accelerated and fall back to a scan; partial indexes add some nuance. See indexes for the exact eligibility rules —omgdb explainalways tells you the plan for a concrete query.
Evaluation operators
| Operator | Description | Example |
|---|---|---|
$regex | String field matches the regular expression. | {"name":{"$regex":"^Ada"}} |
$mod | Integer (or truncated float) field modulo divisor equals remainder. | {"n":{"$mod":[4,2]}} |
$expr | An aggregation-style expression over the whole document is truthy. | {"$expr":{"$gt":["$spent","$budget"]}} |
$regex uses Rust’s regex crate and is unanchored, so it is a substring match unless you anchor the pattern with ^ / $. An invalid pattern is a compile error. Flags are supplied with a sibling $options string in the same field object:
| Flag | Effect |
|---|---|
i | Case-insensitive |
m | Multi-line (^/$ match at line boundaries) |
s | Dot matches newline (dotall) |
x | Ignore whitespace (extended / verbose mode) |
{"name":{"$regex":"^Ada"}}
{"name":{"$regex":"^ada","$options":"i"}}
{"tags":{"$regex":"rust","$options":"i"}}
$mod requires a two-element [divisor, remainder] array of finite whole numbers (4 and 4.0 both work; 1.5 does not) with a non-zero divisor ([0,1] is rejected). It applies to integer fields directly and to float fields by truncating toward zero; non-numeric fields simply do not match.
{"n":{"$mod":[4,2]}}
Note:
$optionsis only a modifier for$regex, read from the same field object. A standalone$options(with no$regex) is rejected at compile time with$options requires $regex, an unsupported flag names itself (unsupported $options flag `q` (supported: i, m, s, x)), and a non-string$optionsvalue is a compile error.
$expr
$expr lifts the aggregation expression engine into a filter, which is how you compare one field against another — something the plain operators cannot express:
{"$expr":{"$gt":["$spent","$budget"]}}
{"dept":"a","$expr":{"$gt":["$spent","$budget"]}}
{"$expr":{"$eq":[{"$toUpper":"$dept"},"A"]}}
$expr appears as a top-level filter key and may sit alongside ordinary field conditions. The expression supports the documented expression operators plus the $$ROOT and $$CURRENT system variables (any other $$-variable is a compile error), and unknown expression operators get did-you-mean suggestions like everything else.
Note:
$expris evaluated per document and is never index-accelerated — an$expr-only filter is always a full scan. Other predicates in the same filter can still use an index; the$exprpart is then applied as a re-filter on the candidates.
Array operators
| Operator | Description | Example |
|---|---|---|
$size | Field is an array of exactly the given length. | {"tags":{"$size":3}} |
$all | Field is an array containing every listed value. | {"tags":{"$all":["a","c"]}} |
$elemMatch | Field is an array with at least one element satisfying the criteria. | {"items":{"$elemMatch":{"qty":{"$gt":5}}}} |
$size requires a non-negative integer and matches only when the field is an array. $all requires an array operand and tests plain element membership, again only on array fields.
$elemMatch requires an object operand and has two forms, chosen by the operand’s shape:
- Document form — the operand is a sub-filter (with plain field keys), matched against each object element:
{"items":{"$elemMatch":{"qty":{"$gt":5}}}}. - Scalar form — the operand is an operator expression (every key starts with
$), matched against each scalar element:{"tags":{"$elemMatch":{"$eq":"b"}}}.
{"tags":{"$size":3}}
{"tags":{"$all":["a","c"]}}
{"items":{"$elemMatch":{"qty":{"$gt":5}}}}
{"tags":{"$elemMatch":{"$eq":"b"}}}
Limitation:
$alldoes not support the nested$elemMatch-inside-$allform — it only checks plain element membership. An$elemMatchoperand mixing$-keys and plain keys is treated as the document sub-filter form.
Dotted-path traversal
A field key is split on . into path segments. Each segment descends into an embedded document by key, or into an array by numeric index:
{"addr.city":"athens"}
{"items.0.sku":"x"}
addr.city reads the city field of the addr sub-document; items.0.sku reads the sku field of the first array element. Descending through a scalar or a missing key resolves to nothing, so the predicate fails to match — unless the operator tolerates absence ($exists:false, $ne, $nin, $not).
Note: Secondary indexes cover top-level fields, so a dotted-path predicate like
{"addr.city":"athens"}always scans. (Partial-index filter predicates may use dotted paths — see indexes.)
Array-contains semantics
A scalar predicate on an array field matches if any element satisfies it (MongoDB’s multikey semantics). This applies to equality, the comparison operators, $in, and $regex:
{"tags":"rag"}
{"tags":{"$in":["db"]}}
{"tags":{"$regex":"rust","$options":"i"}}
{"tags":"rag"} matches a document whose tags array contains "rag". {"age":{"$gt":20}} matches if any element of an array-valued age field exceeds 20. $size, $all, and $elemMatch, by contrast, operate on the array as a whole and require the field itself to be an array.
Implicit AND
Multiple top-level fields in a filter are ANDed together — all conditions must hold:
{"role":"admin","age":{"$gte":18}}
This matches documents where role equals "admin" and age is at least 18. The empty filter {} is an AND of zero conditions and therefore matches every document.
Value ordering
The comparison operators (and range queries and sorting) use a single total order across all value types. Numbers compare numerically across i64/f64; values of different types compare by a fixed type rank:
null < number < string < object < array < bytes < bool < date < objectId
Arrays compare element-wise then by length; objects compare by (key, value) pairs in order then by length. NaN is ordered consistently (via total_cmp), which is why the ordering operators stay total even though $eq treats NaN as unequal to itself.
Projection
Pass --project with a JSON spec to control which fields each result includes. A value of 1 (or true) includes a field; 0 (or false) excludes it; and three array operators shape projected array fields.
omgdb find app.omgdb users '{"role":"admin"}' --project '{"name":1}'
{"name":1} // include name (and _id by default)
{"name":1,"_id":0} // include name, suppress _id
{"age":0} // exclude age, keep everything else
{"addr.city":1} // dotted path: include a nested field
{"comments":{"$slice":-2}} // keep the last two array elements
{"comments":{"$slice":[1,3]}} // skip 1, keep 3
{"items":{"$elemMatch":{"qty":{"$gt":5}}}} // first element matching the sub-filter
{"items.$":1} // first element the query matched
{"name":1,"age":0} // ERROR: cannot mix inclusion and exclusion
Rules:
_idis kept by default and does not set inclusion/exclusion mode; suppress it explicitly with"_id":0.- Inclusion and exclusion modes cannot be mixed (aside from
_id). Any value other than an integer, boolean, or a$slice/$elemMatchobject is rejected. - Dotted paths project nested object fields in both modes.
$slicetakes an integer (positive keeps the head, negative keeps the tail) or[skip, limit]with a positive limit.$elemMatchreturns only the first array element matching its sub-filter.- Positional
$({"items.$":1}) is query-bound: it returns the first array element matched by the filter’s condition on that array path. It requires such a condition, at most one positional projection may appear, it must end the path, and ambiguous bindings are rejected rather than guessed (use query$elemMatchfor multi-field element conditions).
Errors and self-repair
Filters are validated at compile time. Two error kinds are surfaced:
- Malformed query — a structurally invalid operand, e.g.
$inwithout an array,$existswithout a boolean,$modwith a zero divisor,$optionswithout$regex, or an invalid$regexpattern. The message names the exact problem. - Unknown query operator — an unrecognised
$-prefixed token. The message echoes the exact bad token and, when a close match exists, adds a “did you mean” suggestion (nearest known operator within edit distance 2).
{"age":{"$gtee":1}}
// error: unknown query operator: $gtee (did you mean `$gte`?)
The recognised operators are: $eq, $ne, $gt, $gte, $lt, $lte, $in, $nin, $exists, $type, $not, $and, $or, $nor, $expr, $size, $all, $mod, $elemMatch, $regex.
To debug a query that returns nothing, omgdb diagnose reports the per-predicate selectivity (how many documents each top-level field condition matches alone, plus the observed value range for a condition that matches nothing), and omgdb explain describes whether the query will use an index or a full scan. See introspection and indexes. For multi-stage transformations beyond filtering, see aggregation.