<!-- GENERATED FILE β€” do not edit; regenerate with `./do v2-docs` (from core/). -->

v2 Query Operator Reference (Beta)

The complete operator vocabulary of the v2 (Query2) JSON query language β€” where conditions, orderBy, select, query types, and config. v2 is in beta; see the v2 Introduction for context and the v1 reference for the production API.

A query is one JSON object. The same from/where is evidence for a plain query, a prediction, a recommendation, or relation analysis β€” see the Inference guide.

Where conditions

where is an object of field β†’ condition. A bare value means equality:

{ "from": "products", "where": { "category": "books" } }

Equality and tokens

OperatorMeaningExample
(value) / $isEquality. On a Text column: exact equality when the query filters rows, token evidence when it predicts (see below){ "category": "books" }
$exactRaw, non-featurized exact value{ "color": { "$exact": "red" } }
$hasToken / membership. A single token, or an array (matches ANY){ "tags": { "$has": "sale" } } Β· { "tags": { "$has": ["sale","new"] } }
$has: { … }Same-member conjunction on a member-link Set/Array (a set of foreign keys): matches a row that has ONE member whose linked row satisfies all the sub-conditions β€” not "some member is A and some other is B". $exists is an interchangeable spelling, and the same object form works on $refs reverse links{ "items": { "$has": { "a": "fire", "b": "fighter" } } } β€” a basket with one item that is both

A bare value on a Text column: filter or evidence

Available since: v2.9.0. On v2.8.4 and earlier a bare value on a Text column matched by tokens in filters too.

Filtering and inference read a Text value differently, and a where means whichever the query does:

  • In a filter β€” a query that only selects rows (_query/_search, with no ordering or ordered by a stored field) β€” { "name": "BASF Oy" } returns the rows whose name is BASF Oy, character for character. Not "BASF Construction Chemicals Finland Oy", not "Oy BASF", not "basf oy". This holds through a link ({ "vendor.name": "BASF Oy" }) and inside $or/$not, and it is what v1 on a type: "table" has always answered. For the old token match, write $match.
  • As evidence β€” the where of a _predict, _recommend, _match, or a query ordered by $p/$lift/$similarity β€” the value is its tokens, so a description matches by its words and a new wording of a known name still finds it.

$has is not a substitute for equality on a Text column: it is token membership, so { "$has": "BASF Oy" } for a name that is not stored matches rows by their tokens. Use the bare value.

On a member-link's linked Text attribute (items.name where products.name is Text), $has matches by token β€” { "items.name": { "$has": "banana" } } finds baskets with a product named "pirkka banana" or "chiquita banana" (the shared token, not the whole phrase). select ["items.name"] still returns the whole names.

Text columns (type: "Text", with an analyzer) support three matchers:

OperatorMeaningExample
$matchTokenize the input and match rows containing all tokens (AND){ "description": { "$match": "wireless mouse" } }
$searchFull-text search β€” a Lucene-subset query language: bare terms (AND), "quoted phrases", OR, NOT / -term, and ( … ) groups{ "description": { "$search": "(wireless OR bluetooth) mouse -refurbished" } }
$hasA single analyzed token{ "description": { "$has": "wireless" } }

$match is the simple "all words present" matcher; $search is the expressive one when you need phrases, OR, or negation.

Ranges and prefixes

OperatorMeaningExample
$gt $gte $lt $lteComparison β€” numeric columns compare numerically; String columns compare lexicographically (the date-as-string cutoff idiom){ "n": { "$gt": 20 } } Β· { "month": { "$lte": "2024-07" } }
$startsWithPrefix match on a String column{ "sku": { "$startsWith": "FI-" } }
$modModulo (numeric) β€” [divisor, remainder] or {divisor, remainder}{ "n": { "$mod": [3, 1] } }
$numericAdaptive neighbourhood (binned), not an exact match{ "n": { "$numeric": 101 } }

$numeric matches a neighbourhood: Aito bins the column adaptively, so $numeric: 101 draws on nearby values instead of overfitting the single exact row β€” the mechanism that makes a number useful as evidence. A $gt on a non-numeric column fails loud.

Date & timestamp components

Date and Timestamp columns are queried by derived component. A Timestamp (a UTC instant) additionally exposes the sub-day components, computed from the instant at query time β€” raw units, so you bucket them how you like:

OperatorMeaning
$year $month $dayCalendar components
$weekday / $dayOfWeekDay of week (ISO 1=Mon..7=Sun / name)
$dayOfYear1–366
$hour $minuteTime of day (Timestamp only)
$quarter $weekOfMonthQuarter 1–4 / week-of-month 1–5 (Timestamp only)
$matchMatch the date as text
{ "from": "orders", "where": { "created": { "$weekday": 6 } } }
{ "from": "events", "where": { "ts": { "$hour": 9, "$quarter": 3 } } }

Nullability

OperatorMeaningExample
$exists / $definedHas a value (true) or is null/absent (false){ "color": { "$exists": true } }
null equality{ "field": null } is an alias for { "field": { "$exists": false } }{ "color": null }

Every operator above also applies to a field reached through a link by its dotted path β€” { "part.price": { "$gte": 10 } } filters orders by the linked part's price, exactly as { "price": { "$gte": 10 } } would filter parts. The operator vocabulary is a property of the field's type, not of how you reached it: a bare value against a linked Set/Array column is membership ({ "part.tags": "steel" } ≑ { "part.tags": { "$has": "steel" } }, as on the column itself), and a column value operation selects through the link too ("select": ["part.desc.$tokenCount"] is the parts column's count).

When the link itself is null (a nullable link with no target on that row), the path has no value on that row: no operator matches it, $not does not select it, select projects it as null, and { "<link>.<field>": { "$exists": false } } is how you select those rows β€” the same rules as a nullable column read directly.

Available since: v2.8.4. On v2.8.3 and earlier the comparison, prefix, modulo and $search operators were refused on a linked path (Field part.price does not support operations); a bare value, $or or $in against a linked Set/Array column matched nothing; link.field.$op was refused; and a field reached through a null link read the linked table's first row instead of null.

Logical combinators

OperatorMeaningExample
$and $or $notCombine nested where-objects{ "$or": [ { "color": "red" }, { "size": "M" } ] }
field-scoped $or / $inIs-any-of: a value list on one field{ "id": { "$or": ["a", "b", "c"] } } Β· { "id": { "$in": [1, 2] } }
field-scoped $notNegated equality on one field{ "status": { "$not": "done" } }
field-scoped $andCombine field-scoped conditions (e.g. exclude a basket){ "id": { "$and": [ { "$not": "a" }, { "$not": "b" } ] } }
$onObserve a condition only where another holds ([prop, on]){ "$on": [ { "color": "red" }, { "size": "M" } ] }
$groupA correlated theme that votes by mutual corroboration (compression), not OR β€” for predictions where a set of tokens should count as one decorrelated signal{ "$group": [ { "tag": "a" }, { "tag": "b" } ] }

Combinators work on both collection tables and legacy table worlds.

Empty clause lists. A client that builds a filter list β€” { "id": { "$and": cart.map(...) } } β€” sends an empty list whenever there is nothing to filter on. Both are accepted, and each means what it says. An empty $and is a conjunction of no conditions, so it constrains nothing: it is ignored and the rest of the where still applies. An empty $or/$in is a disjunction with no branch that can match, so it selects nothing β€” zero hits, not every row. If you meant "no filter", omit the term rather than sending an empty $or.

Available since: v2.8.2. On v2.8.1 and earlier an empty clause list on any of these fails with a 501 and the message empty.reduceLeft.

Reference a stored row β€” $examine

Condition a prediction on the attributes of a linked entity you only hold a reference to: Aito looks the row up and expands the named attributes into evidence (equivalent to writing link.attr: <value> by hand). It works even for an entity with no rows of its own β€” the prediction generalizes through the attributes, not the unseen id. Requires basedOn; collections only.

OperatorMeaningExample
$examineOn a link field: reference a stored row by at (its target key β€” bare or { "id": … }) and condition on its basedOn attributes{ "account": { "$examine": { "at": "acme", "basedOn": ["industry"] } } }

Vector similarity

$nearest ranks rows by similarity to a query vector on a Vector column. It is a single where proposition that produces its own ordered, scored result set β€” the nearest rows first, each carrying its score as $similarity.

{ "from": "products",
  "where": { "$nearest": {
    "near":  { "embedding": [0.12, -0.03, 0.88] },
    "where": { "category": "shoes" },
    "having":{ "$similarity": { "$gte": 0.8 } },
    "limit": 10 } },
  "select": ["id", "name", "$similarity"] }
KeyMeaning
near{ <vector-column>: [query vector] } β€” the column to score and the query vector. Its length must equal the column's dimensions; a mismatch (or a non-finite component) is rejected.
whereOptional candidate filter β€” any where-object. Scoring runs only over the survivors, so limit is the top-k among matches, never a globally-nearest row the filter should have excluded.
havingOptional score floor on $similarity: { "$similarity": { "$gte": t } } (inclusive) or { "$gt": t } (exclusive).
thresholdSugar for having: { "$similarity": { "$gte": t } }.
limitTop-k cap β€” return at most this many nearest rows.

$similarity is the per-row score, exposed as a selectable field; rows come back ordered by it descending (an implicit orderBy $similarity). Its meaning follows the column's similarity setting: for cosine it is the cosine similarity in [-1, 1] (vectors are unit-normalised on ingest, so scoring is an exact dot product); for dotProduct it is the raw inner product. Rows whose vector is null are skipped, not scored.

$nearest is exact nearest-neighbour (a full scan of the candidates), not an approximate index β€” results are exact and reproducible, with ties broken by row order.

Vectors as prediction evidence β€” $cluster / $semantic

Beyond search, a Vector column can drive prediction. On a predict query, two operators turn similarity into evidence.

$cluster quantizes a vector into a categorical code (k-means), so it becomes an ordinary discrete feature the model reasons over:

{ "from": "tickets", "predict": "category",
  "where": { "embedding": { "$cluster": { "vector": [0.1, 0.9, 0.2], "k": 64 } } } }
KeyMeaning
vectorThe query vector to code. Or text β€” embedded via the column's embedder (see Vector search).
kCodebook size (number of clusters).
asOptional name for the derived code column (default <column>_cluster).

The vector is coded against the column's codebook and the condition rewrites to a plain equality on the code column β€” so the vector becomes ordinary categorical evidence. A single full-vector cluster recovers most of the exact-kNN lift.

$semantic blends the k nearest neighbours' predict-field values into the prediction β€” semantic-kNN late fusion, the continuous-similarity form:

{ "from": "tickets", "predict": "category",
  "where": { "embedding": { "$semantic": { "near": [0.1, 0.9, 0.2], "k": 20, "weight": 0.5 } } } }
KeyMeaning
nearThe query vector, or text (embedded via the column's embedder).
kNeighbours to vote (default 10).
weightBlend weight in [0, 1]: 0 = the token/naive-Bayes baseline only, 1 = pure kNN vote, 0.5 (default) = an even blend.

The k nearest rows vote their predict field, similarity-weighted, and the vote is fused with the ordinary prediction. $knn/$nn do the same with feature similarity instead of vector similarity (see below).

Text introspection

On a text field you can also read its analysis: $size (row count), $tokenCount (distinct tokens), $distinctCount (distinct values). Select them with a field.$op token β€” e.g. "select": ["note.$tokenCount"]. The value is a property of the whole column, so every returned row carries the same number (a per-row constant); pair it with "limit": 1 to read it once.

Unusual values (experimental)

Available since: v2.9.0. Applies only to an x-knowledge column.

OperatorMeaningExample
$surprisalRows whose value costs at least N bits per token under the column's learned grammar β€” the values least like the rest of the column{ "body": { "$surprisal": 5.0 } }

Only an x-knowledge column answers $surprisal; on any other column it is refused by name. It needs one learned grammar, so on a collection written in several batches it is refused until the collection is optimized. Higher N selects fewer, stranger values β€” a useful N depends on the column, so start high and lower it.

A dotted path follows a forward link β€” from a row to the row it points at (order.buyer resolves the order's user, and buyer.name reads that user's name). $refs follows a link the other way: from a row to the rows that point at it. $refs.<table>.<fk> is the set of <table> rows whose link <fk> targets this row β€” e.g. on a user, $refs.orders.buyer is that user's orders (the orders whose buyer is this user). Append an attribute to project it over that set: $refs.orders.buyer.product is the products of those orders.

It is an ordinary field β€” you query, select, and predict over it with the same operators a stored member-set uses. The query is identical whether the relation is stored on the row or discovered via the back-link; nothing is copied onto the referring row.

Link resolution is lazy, and a link's cross-table enrichment is optional. A link's target is resolved at query time, not validated when the schema is created β€” so you may declare a link before its target table exists, and a link whose target is missing, renamed, or deleted does not raise an error. Its linked columns and its cross-table prediction signal are simply absent for that query, and the result degrades gracefully to what the row itself carries. (This is the long-standing rep1 behaviour, preserved by design.) Cross-table priors additionally require the target to be a rep2 collection; a link to a rep1 (type: "table") target resolves as absent the same way. So if a predict seems to ignore a linked signal, confirm the target table exists and is a rep2 collection.

Where β€” over the referring set:

ConditionMeaningExample
$existsHas β‰₯1 referring row{ "$refs.orders.buyer": { "$exists": true } } β€” users with an order
$has on a projected attributeA referring row whose attribute matches{ "$refs.orders.buyer.product": { "$has": "gold" } } β€” users with a "gold" order
$exists/$has with an object (same-member filter)Restrict the referring rows before the existence check β€” ALL conditions hold on the SAME referring row; a condition col may itself be a forward link (item.tier β€” one more hop){ "$refs.orders.buyer": { "$exists": { "item.tier": "premium" } } }
<link>.$refs.… (forward-then-reverse)Resolve this row's forward link first, then its inverse links β€” two hops{ "sender.$refs.edges.subject": { "$exists": true } } β€” emails whose sender has an edge

Row-level $and of two $refs conditions intersects, e.g. a user with both a gold order and a web order:

{ "from": "users",
  "where": { "$and": [
    { "$refs.orders.buyer.product": { "$has": "gold" } },
    { "$refs.orders.buyer.channel": { "$has": "web" } } ] } }

Select β€” project the attribute over each row's referring set (an array per row):

{ "from": "users", "select": ["name", { "products": "$refs.orders.buyer.product" }] }
// β†’ { "name": "Alice", "products": ["gold", "silver"] }, …

Predict β€” use a $refs condition as where evidence to predict a field from the back-linked rows, without materialising anything onto the row:

{ "from": "users",
  "where": { "$refs.orders.buyer.product": { "$has": "gold" } },
  "predict": "segment" }

A same-member conjunction β€” one referring row that matches several attributes at once (an order that is gold and on web) β€” is the object form: { "$refs.orders.buyer": { "$exists": { "product": "gold", "channel": "web" } } }. Contrast the row-level $and above, which matches any gold order and any (possibly different) web order.

Beta: the basedOn keyword does not yet accept a $refs path; the where-evidence form above covers predicting from inverse links.

Ordering β€” orderBy

FormMeaning
"price" / { "field": "price", "desc": true }Sort by a field
{ "$asc": "price" } / { "$desc": "price" }Direction shorthands for the same (also work on "$p"/"$f"/"$lift", e.g. { "$asc": "$f" })
"$p"Rank by prediction probability
"$f"Rank get candidates by their frequency (count under the where)
"$lift"Rank get candidates by lift (how much the where raises each value's probability)
{ "$p": { "$context": <where> } }Rank candidates by P(condition in the from/context table)
{ "$sum": { "$context": <field> } } / { "$mean": { "$context": <field> } }Rank get candidates by a per-candidate aggregate of a context field β€” "which candidate contributes most to the goal". The same body the select side takes, so a column you can name you can also order by. Booleans count as 0/1, so { "$sum": { "$context": "purchase" } } ranks by purchases per candidate. Higher first unless "desc": false.
{ "$similarity": { <textField>: <text> } }Rank rows by the BM25 relevance of the field against the text, emitted as a probability lift exp(thetaΒ·bm25) β€” 1.0 (neutral) for a no-match β€” so it is directly multipliable with a $p (a raw 0 would annihilate a $multiply). theta defaults to 0.33 (the tf-idf lift scale); override with "theta": <n>. On a get over a link, the field may be a linked text column. $sameness is an accepted alias.
{ "$bm25": { <textField>: <text> } }Same, but the raw BM25 score (0.0 for a no-match) rather than the lift. Ranking of a single $similarity/$bm25 orderBy is identical (the lift is monotone); the difference is the emitted value's scale and its composition behaviour.
{ "$vectorSimilarity": { <vectorField>: [<vector>] } }Rank rows by vector cosine against the query vector, emitted as a probability lift exp(thetaΒ·cos) β€” 1.0 (neutral) for an orthogonal or vector-less row β€” so it composes multiplicatively with a $p/$similarity (the probabilistic hybrid-search blend, ["$p", "$similarity", "$vectorSimilarity"]). theta defaults to 1.0. Only usable inside $multiply; the query vector must be supplied explicitly (text-embedding a near value is a follow-up).
{ "$vectorCosine": { <vectorField>: [<vector>] } }Same, but the raw cosine (0.0 for a vector-less row) rather than the lift. A single-term ranking is identical (the lift is monotone).
{ "$vectorIdf": { <vectorField>: [<vector>] } }Self-calibrating vector lift (1/p)^ΞΈ, where p is the row's cosine tail probability over the examined rows (the fraction at least as similar to the query) β€” a vector IDF, βˆ’log p. This is BM25's (1/p)^0.33 form applied to a similarity condition: the model-specific cosine scale is absorbed into p, so one ΞΈ (default 0.87) transfers across embedding models with no per-model tuning. Preferred over $vectorSimilarity for composing with $p/$similarity.
{ "$multiply": [ … ] }Rank by the per-row product of parts: the string "$similarity" (lift) or "$bm25" (raw) referencing the query's $match where-term, a { "$similarity": {field: text} } / { "$bm25": {field: text} } object, a { "$vectorSimilarity": {field: [vec]} } / { "$vectorCosine": {field: [vec]} } vector term, or a { "$p": { "$context": … } } part. ["$p", "$similarity", "$vectorSimilarity"] composes probability Γ— text-similarity Γ— vector-similarity.
{ "from": "impressions", "get": "product",
  "orderBy": { "$p": { "$context": { "purchase": true } } },
  "select": ["$value", "$p"] }

Projection β€” select

Plain column names, computed value-expression columns (arithmetic over fields β€” see Derived fields β†’ let), plus the predictive columns below. On a get over a link, the link TARGET's columns are selectable too β€” each candidate resolves them on its target row (get: "product" + select: ["name", "price"] returns each candidate product's own attributes):

ColumnMeaning
$valueThe candidate value
feature / fieldv1's names for the ranked candidate and the predicted field, accepted as select aliases on any ranked-value query (_predict / _recommend / _match), so a v1 body works unchanged over v2. feature is $value under its old name; field is the request's own target. Asked of a query that ranks nothing they fail loud, like any unknown column
$pIts predicted probability
$scoreThe value used by orderBy β€” for a predict/recommend ranking, equal to $p

A ranked-candidate query also accepts a top-level having filter on the per-candidate frequency: "having": { "$f": { "$gte": 1 } } keeps only candidate values that actually co-occur with the where. This is the server-side tenant-scoping idiom β€” where supplies evidence, not an output domain, so a tenant-scoped predict otherwise ranks every tenant's values with the foreign ones in the tail at $f: 0. Applied after ranking (no renormalisation), before offset/limit; only $f with one of $gte/$gt/$lte/$lt is accepted, anything else is a loud 400. | $f | The candidate's frequency β€” count of rows where the predicted field equals this value, under the where | | $lift | The candidate's lift β€” how much the where evidence raises its probability over its base rate | | $why | The explanation factor tree (see the Inference guide). The parameterized form { "$why": { "highlight": { "posPreTag": "<b>", "posPostTag": "</b>" } } } additionally marks the matched evidence tokens in the explanation with your tags | | $highlight | On a search query: the matched field value with the query's tokens marked with tags β€” the search-hit form of $why's highlight. On a predict/recommend of a link field: the candidate's OWN attribute values with the evidence-carrying parts marked β€” an attribute whose prior lift is above 1 gets the positive tags, below 1 the negative tags (see the Inference guide). The parametric form { "$highlight": { "posPreTag": "<mark>", "posPostTag": "</mark>", "negPreTag": …, "negPostTag": …, "encoder": … } } sets the markup (SQL: startSel / stopSel on search(..)) | | $matches | On a search query: the matched tokens and their positions in the field value | | { alias: "source" } | Rename a column in the output β€” { "demandScore": "$score" } | | { alias: <value-expression> } | A computed column β€” { "margin": { "$subtract": ["revenue", "cost"] } }. Any let value-expression (arithmetic over fields/constants; see Derived fields); evaluated the same way as let, so select and let share one vocabulary | | { alias: { "$p": { "$context": <where> } } } | A named per-candidate contextual probability column | | { "$sum" \| "$mean": { "$context": <field> } } | Per-candidate aggregate of a context-table field over the candidate's where-filtered rows (bare, or wrapped in an alias) β€” e.g. a per-product conversion rate { "$mean": { "$context": "purchase" } } | | $count / $sum:f / $avg:f / $min:f / $max:f | Relational aggregate over the whole where selection β€” collapses all matching rows to ONE hit (SQL count(*), sum(f), …), ignoring offset/limit. Skips null cells; null over zero rows. Distinct from the per-candidate { "$sum": { "$context": … } } above; mixing an aggregate with a plain field is rejected (SQL would need a GROUP BY) | | field.$predictions | A per-row predicted column: for each selected SOURCE row, the ranked distribution [ { "value": …, "p": … }, … ] over the field β€” computed from the row's OTHER fields, with the predicted field held out. Collections only; default top-N. (No leave-one-out; use _evaluate for an unbiased estimate) | | field.$prediction | The argmax of the above β€” the single most likely value | | { alias: { "$predict": "field", "limit": n, "basedOn": [ … ] } } | Object form of a predicted column β€” cap the distribution (limit) and/or generalize a predicted link's candidates by their attributes (basedOn) |

Selecting an unsupported computed column fails loud (a 4xx), never a silent empty value. Aggregates and $f require candidates whose rows live in the from table itself β€” a dotted-path get (e.g. get: "context.week") is a known limitation (returns zeros; tracked on the gap matrix).

Derived fields β€” let

let defines inline computed fields from value-expressions, usable in select and orderBy. It is the join-free way to compute derived columns β€” margin, ratios, and the like β€” without a CTE or self-join:

{ "from": "sales",
  "let": { "margin":    { "$subtract": ["revenue", "cost"] },
           "marginPct": { "$divide": [ { "$subtract": ["revenue", "cost"] }, "revenue" ] } },
  "select": ["id", "margin", "marginPct"], "orderBy": "margin" }

A value-expression is one of:

FormMeaning
"revenue" / { "$field": "revenue" }A field's value
123 / { "$const": 2.0 }A constant
{ "$sum": [a, b, …] }Sum of the parts
{ "$subtract": [a, b] }a βˆ’ b
{ "$multiply": [a, b, …] }Product of the parts
{ "$divide": [a, b] }a / b
{ "$pow": [base, exp] }base raised to exp (element-wise)
{ "$normalize": expr }Scale so the column's values sum to 1.0 across the queried rows (a cross-row transform β€” e.g. a revenue share)
{ "$length": "field" }The per-row element count of an array/set field β€” a stored member-link set's size, or a $refs projection's count (graph degree / neighbour count). E.g. { "degree": { "$length": "$refs.edges.subject.relation" } }. Distinct from $count (count(*), which collapses the whole selection to one hit); non-array fields are rejected loudly
{ "$coalesce": [a, b, …] }The first defined part per row (SQL coalesce) β€” e.g. a NULL-safe fallback { "n": { "$coalesce": [ "note", { "$const": "n/a" } ] } }
{ "$cast": { "expr": e, "as": "int|bigint|decimal|text|boolean" } }Coerce e to a type (SQL CAST(e AS type)); an uncoercible value is undefined for that row
{ "$upper": e } / { "$lower": e }Per-row string case fold (SQL upper/lower)
{ "$charLength": e }Per-row string character length (SQL length(str)); distinct from $length (array element count)
{ "$dateTrunc": { "unit": u, "expr": e } }Truncate a timestamp DOWN to unit β€” year, quarter, month, week (ISO, Monday), day, hour, minute, second (SQL date_trunc(u, ts)). Calendar-correct in UTC; the result is a timestamp, so every instant in the bucket shares one value
{ "$concat": [a, b, …] }Per-row string concatenation (SQL concat / ||); an undefined part is treated as ''

Parts nest arbitrarily. A missing or non-numeric part, or division by zero, makes that row's value undefined.

Beta: let fields are usable in select, orderBy, and β€” for equality and membership β€” in where: where: { "margin": 6 } keeps the rows whose computed margin is 6, and where: { "margin": { "$in": [3, 6] } } keeps those whose margin is 3 or 6 (a same-field $or means the same). The field still projects. The condition is evaluated per row against the current selection and AND-composes with normal column filters (where: { "margin": 6, "revenue": 10 }). Still in progress (rejected with a clear error, never silently ignored): a range/operator on a let field, a let field mixed with other conditions under $or or inside $not, and using one as prediction evidence (e.g. basedOn: { "$numeric": "margin" }). For those, filter or predict on the source column(s) instead.

from β€” collections, views, and inline relations (Beta)

from names what a query reads. Besides a plain collection it accepts a view (a derived relation saved in the schema) or an inline relation expression (an ad-hoc join or union for a single query). All are queried, filtered, projected, and predicted exactly like a collection β€” a view is just the saved, materialised form of the same relation operators.

from formExampleReads
collection"orders"one collection
view"search_index"a saved view β€” union-merge or link-join
inline link-join{ "from": "orders", "join": { "table": "products", "on": { "$=": ["product", "id"] } } }a base foreign-key column exposed as a navigable link for this query; joined columns navigate via dotted paths (product.name), no content copied
inline union{ "union": ["appLogs", "backendLogs"] }several collections stacked into one relation (heterogeneous schemas unify column-by-column; lazy, no copy)
projected union{ "union": [ { "from": "customers", "select": { "id": "id", "content": { "$text": ["name", "notes"] } } }, … ] }each source mapped into one common schema β€” "col" renames a column, { "$const": v } a literal, { "$multiply": [...] } an arithmetic column (same vocabulary as let), and { "$text": [...] } concatenates Text fields into one analyzer-backed content column so a single $match/$search spans every source

Views are created and refreshed in the schema β€” see Schema Design β†’ Views for the full treatment (both kinds, worked examples, _refresh semantics), and Relationships, Links & Joins for how link-joins behave in a query. A view is a materialised snapshot β€” rebuild it with POST /api/v2/schema/{view}/_refresh after its sources change; an inline $text union is tokenised per query, so for a hot cross-collection search prefer a persisted union view (built once).

Query types (top level)

FieldMeaning
predict: "field"Rank a field's values by probability given where
where ON the predict fieldRESTRICTS the candidates β€” {"sku": {"$or": ["A","B"]}} with predict: "sku" ranks only A and B, renormalised. It is not evidence about the answer, so it never argues for its own contents. Restricting also cuts the work: cost scales with the candidate domain. Use <field>.<column> to restrict by a LINKED row's attribute instead.
recommend: "linkField" + goal: {…}Rank link candidates by P(goal)
relate: ["field", …]Return statistical relations (lift, info-gain), not rows. An array or set field relates one relation per MEMBER ({"tags": {"$has": "fruit"}}), not per whole value β€” on a direct field and through a link alike. A text field on the queried table relates per TOKEN; a text field reached through a link relates whole values unless you ask for tokens with "product.name.$feature", which relates one relation per token as the column's own analyzer produces it (English stems included, so "Sparkling" relates as sparkl). "field.$feature" works in the field array and in $props, and is refused on a field with no members or tokens. A text column carries BOTH indexes and either can be named explicitly: "vendor.$distinct" relates its whole values, "vendor.$token" its tokens (see Which index a text column relates). Relations with zero information gain are omitted, so a token exactly independent of the condition does not appear.
search: { "field": …, "text": … }Sugar: text match + rank by relevance
get: "field"Open a field's distinct values as candidates
basedOn: ["field", …]Project linked-table fields in as cross-table evidence. Without it, a link-target predict projects every String/Text field of the linked table that shares its name with a where field (payments.vendor Γ— invoices.vendor): the Xβ†’X analogy, whose regularity is learned per field pair, so a value seen twice inherits what the pair knows. $why lists each attribute with the lift it carried. On a recommend, candidates are also scored THROUGH these attributes: a where value's evidence about the goal pools over every candidate carrying the same attribute value, so a query word reaches products no past row paired it with (available since v2.9.0).
predict: "field.$feature"Non-exclusive (multi-label) predict of a member/array field: each member value is scored independently by P(field has value | where) β€” for tags, baskets, categories. The prediction target determines the mode: predict X is exclusive (candidates are distinct whole values), predict X.$feature is per-member. The v1-style exclusiveness flag is deprecated β€” if given, it must agree with the target or the request is rejected

Endpoints: /api/v2/_query is the unified entry; /_predict, /_search, /_recommend, /_relate are aliases that take the same body; /_batch runs a JSON array of queries. Also on v2, engine-dispatched (collections run natively; rep1 tables reuse the v1 pipeline):

EndpointMeaning
/_evaluateTrain/test-split quality measurement β€” see the Evaluation guide. Supports test conditions, { "$index": n } row selection, sampling selectors ($sample β€” incl. the {n, of, seed} form β€” and $hash with $mod, in test and testSource.where), testSource {from, where, limit}, $get bindings, operator conditions (e.g. a $lte month cutoff), and per-query config (inference profile + train-fitted calibration)
/_estimateEstimate of a numeric field given the where. Default: knn (v1's AdjustedKNN), so a model-less request matches the v1 engine on identical data. model: "regression" (pure transformation-pipeline regression) is the other v1 estimator; both knn and regression require plain-equality where conditions. model: "mean" is the posterior-mean (probability-weighted mean over the predicted candidates, weightedAverage why) and accepts the full where operator vocabulary
/_aggregate$sum / $mean (+ variance stats) / $min / $max / $f over the where-matching rows β€” array form (["price.$mean"], keys = the spec strings) or aliased object form ({ "conversion": "purchase.$mean" })
/_matchCandidate values of the match field ranked by calibrated probability given the where evidence, generalizing to evidence never seen verbatim (lowered to predict: <field>). Honours select / $why.

Config

FieldMeaning
config.aiInference profile preset β€” group (v2) (Group re-expression only, the default), high (And + Group), v1/and (legacy And-only), or fast/flat (plain naive Bayes); unknown names fail loud. (v1 and v2 name the V1/V2 engine defaults; And+Group is the opt-in high profile β€” it adds cost without improving accuracy on the corpora.) Also accepts an object form that additionally sets scoring options for this query β€” see below.
config.calibrateTemperature-calibrate probabilities once per DB state

Per-query scoring options β€” the config.ai object form

config.ai accepts either a preset name or an object. The object form selects the same preset (preset) and, in addition, overrides individual scoring options for that one request:

{ "from": "invoices", "predict": "Processor",
  "where": { "Description": "monthly cloud hosting" },
  "config": { "ai": { "preset": "v2", "nameBoost": false, "mediationT": 2.0 } } }

Options are resolved in layers β€” built-in defaults, then the deployment's ops settings, then this query β€” so an object naming one option changes exactly that option and leaves the rest at their defaults. Nothing is global: two concurrent requests can score with different options without affecting each other. Unknown keys, unknown values and out-of-range numbers are rejected with a 400 naming the option (never silently ignored). A query with no config.ai is scored exactly as before.

The three cost guards (mediationMaxCandidates, nameBoostMaxCandidates, stage4MaxClasses) bound work that grows quadratically with the candidate count; the memory guard (stage4ClassIndexMaxRows) bounds a transient per-state index, and hitRelAccuracy bounds how much of the relation-discovery pool is upgraded from sampled to exact statistics. All four behave the same way: a query may LOWER one β€” making itself cheaper β€” but a request to raise it above the deployment's configured ceiling is rejected with a 400 rather than silently clamped.

OptionTypeMeaning
presetstringThe inference profile β€” the same names as the string form.
designf | legacyScoring stack. f (default) is the redesigned stack; legacy restores the pre-redesign stack and inerts mediation, nameBoost and stage4.
mediationbooleanCompose a link-target prediction through a near-deterministic sibling link (e.g. predict acceptor through processor).
mediationTopMassnumber (0,1]Determinism gate for the sibling edge.
mediationMinSupportintegerMinimum mediator support for the composition to fire.
mediationTnumber >0Temperature flattening the sibling distribution.
mediationGammanumber [0,1)Prior mix-in, so no candidate is floored at 0.
mediationSiblingBasedOnbooleanEnrich the mediator stage with the linked table's attributes.
mediationDirectMixnumber [0,1]Mix the direct expert into the mediated probability.
mediationMaxCandidatesintegerCost guard β€” skip mediation above this candidate count.
nameBoostbooleanX→X name-identity boost — evidence text that embeds a candidate's own name.
nameBoostThetanumber >=0Boost strength.
nameBoostMaxCandidatesintegerCost guard for the boost.
nameBoostSelfScaledCapbooleanBound the boost by how much the match IDENTIFIES the candidate (its sum log(K/df)) rather than by a constant. Default on; nameBoostMaxLogLift then acts as a floor.
nameBoostQueryLikelihoodbooleanScore the identity match as a query-likelihood ratio `sum log P(w
nameBoostQlMunumber >0Dirichlet smoothing mass for nameBoostQueryLikelihood, in pseudo-tokens (default 0.5). Identities are short β€” a catalogue name is ~5 tokens β€” so document-retrieval values (~2000) would drown the name entirely.
nameBoostFadeKappanumber >=0Fade the name-identity boost with the candidate's own history: the analogy is a prior worth this many rows (default 16; 0 = no fade), weighted 1/(1+n/kappa) for a candidate with n training rows, so a candidate the history does not describe keeps the whole boost and a well-known one keeps little. Not applied under basedOn.
stage4booleanPer-query redundancy weights (count each nat of evidence once).
stage4MaxClassesintegerCost guard for stage 4.
stage4ClassIndexMaxRowsintegerMemory guard — row ceiling for stage 4's row→class index (1 byte/row); above it counting falls back to a per-class scan. Results are identical either way.
memobooleanPer-state scoring memos. Result-identical either way; for A/B verification.
hitRelMaxNintegerGlobal cap on kept (candidate, known) relations.
hitRelPerCandintegerPer-candidate relation floor (0 = the global-cap-only legacy path).
hitRelAccuracynumber [0,1]How much of the relation-discovery pool is upgraded from sampled to exact statistics. 1 (default) upgrades all; lower is cheaper on large tables and may reorder marginal relations.
hitRelPoolFactornumber >= 0Size of the relation-discovery candidate pool, as 8 + maxN * factor. 2 (default) leaves the upgrade room to re-rank into the kept set; lower discovers from a smaller pool and does fewer exact upgrades.
neffTemperbooleanThe n_eff temper (finite-sample overconfidence shrinkage).
neffTemperModecount | harmonic | coverage | auton_eff estimator.
neffTemperKappanumber >0Temper kappa (ignored in auto).
neffTemperBoostOnlybooleanShrink boosts only, leaving suppressions intact.
neffRegularityFloorprecision | full | offField-regularity floor mode.

The same object form is accepted by /_evaluate (evaluate.config.ai), so an A/B of two scoring configurations is two measured runs, not a restart.

Feature-based nearest rows β€” $knn / $nn

Rank rows by feature similarity to an exemplar (text fields score by BM25). This is a where operator (the feature analog of the vector $nearest / $semantic above), not a config setting:

{ "from": "msgs", "where": { "$knn": { "near": { "text": "sim card" } } }, "limit": 5 }
{ "from": "msgs", "where": { "$nn": { "near": { "text": "sim card" }, "threshold": 0.5 } } }

$nn keeps only rows scoring above threshold (default 0.0). The v1 array form { "$nn": [ { "text": "…" } ] } is accepted with a single exemplar. $similarity is selectable on the results.

On a predict query, $knn/$nn become feature-kNN classification β€” the feature analog of $semantic: the k feature-nearest neighbours vote the predict field, blended with the ordinary prediction.

{ "from": "tickets", "predict": "agent",
  "where": { "$knn": { "near": { "text": "password reset" }, "k": 2, "weight": 0.5 } } }

k (neighbours to vote, default 10) and weight (blend in [0, 1], default 0.5) apply only in this predict context; as a plain search operator $knn/$nn page by the query's limit.

Population restriction β€” nested from

from accepts a restriction object instead of a table name:

{ "from": { "from": "invoices", "where": { "customer_id": "CUST-0000" }, "limit": 50000 },
  "where": { "gl_code": "1600" },
  "predict": "approver" }

Rows failing the inner where are masked out of the examined table β€” the population the query sees. This is different from adding the filter to the outer where: where is evidence (it conditions the prediction and contributes lift), while the nested from restricts the row population, so base rates, support counts and candidates all compute over the subset (the multi-tenancy idiom). The optional inner limit caps the restricted population to its first N rows. One restriction level; a restriction matching no rows, deeper nesting, and unsupported combinations (relate, bare search, $nearest/$knn, config.calibrate) all fail loud.

One entity's properties β€” relate: {"$props": …}

The field-array form ranks EVERY value of the listed fields against the where. When the question is about one entity β€” "for each property value of THIS product, what does it do to purchase?" β€” name the propositions instead:

{ "from": "impressions",
  "where": { "purchase": true },
  "relate": { "$props": { "product.category": "100", "product.name": "Pirkka banana" } } }

Each named proposition becomes one hit, related to the where. The statistics are the field-array form's: scoping decides which relations come back, never their values. It also lifts the enumeration's ranking cap, so a value that would sit past the top of a population-wide ranking is still returned β€” the reason this is a form rather than client-side filtering.

v1's nested-entity spelling {"product": {"category": "100"}} is accepted as the same request, flattened onto the link paths (product.category). A FLAT relate object keeps its own meaning (relate every field to that condition); an object mixing the two is refused. $props takes plain field-value propositions β€” a combinator ($or, $not) names a set, not a relation, and is refused; so is a value no row carries. On a Text column a named proposition is the WHOLE value β€” "Pirkka banana", not "banana" β€” because that is the proposition a filter takes; name product.name.$token to relate one word instead (next section).

Which index a text column relates β€” .$distinct / .$token

Available since: v2.9.2. On v2.9.1 and earlier a text column was reachable through its tokens only, and neither suffix was recognised.

A text column has TWO indexes: its distinct whole VALUES and its analyzed TOKENS. relate, $patterns and $props can name either, by suffixing the field:

spellingwhat it enumerates
"vendor"the default: tokens on the queried table, and on $patterns BOTH indexes ranked together
"vendor.$distinct"whole values only β€” {"vendor": "Kauko Oy"}
"vendor.$token"tokens only β€” {"vendor": {"$has": "kauko"}}
"vendor.$feature"unchanged: the column's MEMBERS, which on a text column are its tokens

Asking a column for an index it does not have is refused by name ($token on a String column names the column and its type), never answered with an empty relation.

Both indexes matter because they are different rows. Kauko Group Oy contains every token of Kauko Oy, so $has kauko AND $has oy covers both companies while {"vendor": "Kauko Oy"} covers one. A mined rule displayed as one company but supported by the other's invoices is a wrong number in a 200 β€” which is why $patterns offers the whole value alongside the tokens. A rule that pins a column to a whole value also drops that column's own token clauses from the same rule: they select nothing the value did not, and the rule reads as one company rather than as a list of its words. Members of a set column are never pruned this way β€” basket $has A AND basket $has B is the finding.

Itemset mining β€” relate: {"$patterns": …}

The market-basket primitive: which sets of a field's values co-occur more than chance. Candidates are the fields' MEMBERS β€” array/set elements, analyzed text tokens, or scalar values β€” plus, on a TEXT column, its whole values as well (see the section above) β€” mined greedily into variable-size itemsets (pairs seed, grow while the MDL gain holds, up to 6 items, support β‰₯ 2), then each itemset is scored against the where condition:

{ "from": { "from": "invoices", "where": { "customer_id": "CUST-0000" } },
  "where": { "gl_code": "1600" },
  "relate": { "$patterns": ["vendor", "category", "amount_band"] } }

Each hit carries related (the $and itemset), condition, lift, info, f (the itemset's support count) and n. Without a where, itemsets rank by their own support. The {"$related": {"relate": [...], "k": n, "to": {…}}} wrapper bounds the candidate set (k, default 32) and adds a narrowing target. Nested from composes (the multi-tenancy shape above): support counts and n are computed over the restricted population.

Not yet supported

  • Nested from with plain relate β€” typed 501 (bit-universe alignment); relate $patterns DOES compose with nested from.
  • basedOn $patterns (conjunction mining as predict evidence) β€” fails loud; planned.
  • Arithmetic score composition beyond $multiply β€” v1's $divide, $subtract, $pow, $normalize and friends are not ported; only $multiply composes ranking values on v2 (let covers derived fields).

All of these fail loud rather than degrade β€” a 4xx/501 with the reason, never a silently wrong 200.