---
title: Query Operators
description: Reference for the OMGDB find query language — comparison, element, logical, evaluation, and array operators, dotted paths, array semantics, and projection.
---

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`:

```sh
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](#errors-and-self-repair)). For how filters choose an index or fall back to a scan, see [indexes](/docs/indexes/); to inspect the plan or debug a no-results query, see [introspection](/docs/introspection/).

## The find command

```text
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](#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](/docs/cli/).

## 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](#value-ordering). | `{"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.

```json
{"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 `$nin` use value equality, where `NaN` never equals itself. The ordering operators (`$gt`/`$gte`/`$lt`/`$lte`) use the total order, which places `NaN` consistently. Equality and ordering can therefore disagree on `NaN`.

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

```json
{"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.

```json
{"$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 `$not` are generally not accelerated and fall back to a scan; partial indexes add some nuance. See [indexes](/docs/indexes/) for the exact eligibility rules — `omgdb explain` always 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) |

```json
{"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.

```json
{"n":{"$mod":[4,2]}}
```

> **Note:** `$options` is 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 `$options` value is a compile error.

### $expr

`$expr` lifts the [aggregation expression engine](/docs/aggregation/) into a filter, which is how you compare one field against another — something the plain operators cannot express:

```json
{"$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:** `$expr` is 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 `$expr` part 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"}}}`.

```json
{"tags":{"$size":3}}
{"tags":{"$all":["a","c"]}}
{"items":{"$elemMatch":{"qty":{"$gt":5}}}}
{"tags":{"$elemMatch":{"$eq":"b"}}}
```

> **Limitation:** `$all` does not support the nested `$elemMatch`-inside-`$all` form — it only checks plain element membership. An `$elemMatch` operand 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:

```json
{"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](/docs/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`:

```json
{"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:

```json
{"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:

```text
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.

```sh
omgdb find app.omgdb users '{"role":"admin"}' --project '{"name":1}'
```

```json
{"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:

- `_id` is 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`/`$elemMatch` object is rejected.
- **Dotted paths** project nested object fields in both modes.
- **`$slice`** takes an integer (positive keeps the head, negative keeps the tail) or `[skip, limit]` with a positive limit.
- **`$elemMatch`** returns 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 `$elemMatch` for 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. `$in` without an array, `$exists` without a boolean, `$mod` with a zero divisor, `$options` without `$regex`, or an invalid `$regex` pattern. 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).

```text
{"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](/docs/introspection/) and [indexes](/docs/indexes/). For multi-stage transformations beyond filtering, see [aggregation](/docs/aggregation/).
