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
| Operator | Meaning | Example |
|---|---|---|
(value) / $is | Equality. On a Text column: exact equality when the query filters rows, token evidence when it predicts (see below) | { "category": "books" } |
$exact | Raw, non-featurized exact value | { "color": { "$exact": "red" } } |
$has | Token / 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 isBASF 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 atype: "table"has always answered. For the old token match, write$match. - As evidence β the
whereof 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 search
Text columns (type: "Text", with an analyzer) support three matchers:
| Operator | Meaning | Example |
|---|---|---|
$match | Tokenize the input and match rows containing all tokens (AND) | { "description": { "$match": "wireless mouse" } } |
$search | Full-text search β a Lucene-subset query language: bare terms (AND), "quoted phrases", OR, NOT / -term, and ( β¦ ) groups | { "description": { "$search": "(wireless OR bluetooth) mouse -refurbished" } } |
$has | A 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
| Operator | Meaning | Example |
|---|---|---|
$gt $gte $lt $lte | Comparison β numeric columns compare numerically; String columns compare lexicographically (the date-as-string cutoff idiom) | { "n": { "$gt": 20 } } Β· { "month": { "$lte": "2024-07" } } |
$startsWith | Prefix match on a String column | { "sku": { "$startsWith": "FI-" } } |
$mod | Modulo (numeric) β [divisor, remainder] or {divisor, remainder} | { "n": { "$mod": [3, 1] } } |
$numeric | Adaptive 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:
| Operator | Meaning |
|---|---|
$year $month $day | Calendar components |
$weekday / $dayOfWeek | Day of week (ISO 1=Mon..7=Sun / name) |
$dayOfYear | 1β366 |
$hour $minute | Time of day (Timestamp only) |
$quarter $weekOfMonth | Quarter 1β4 / week-of-month 1β5 (Timestamp only) |
$match | Match the date as text |
{ "from": "orders", "where": { "created": { "$weekday": 6 } } }
{ "from": "events", "where": { "ts": { "$hour": 9, "$quarter": 3 } } }
Nullability
| Operator | Meaning | Example |
|---|---|---|
$exists / $defined | Has a value (true) or is null/absent (false) | { "color": { "$exists": true } } |
| null equality | { "field": null } is an alias for { "field": { "$exists": false } } | { "color": null } |
Fields through a link
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
| Operator | Meaning | Example |
|---|---|---|
$and $or $not | Combine nested where-objects | { "$or": [ { "color": "red" }, { "size": "M" } ] } |
field-scoped $or / $in | Is-any-of: a value list on one field | { "id": { "$or": ["a", "b", "c"] } } Β· { "id": { "$in": [1, 2] } } |
field-scoped $not | Negated equality on one field | { "status": { "$not": "done" } } |
field-scoped $and | Combine field-scoped conditions (e.g. exclude a basket) | { "id": { "$and": [ { "$not": "a" }, { "$not": "b" } ] } } |
$on | Observe a condition only where another holds ([prop, on]) | { "$on": [ { "color": "red" }, { "size": "M" } ] } |
$group | A 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.
| Operator | Meaning | Example |
|---|---|---|
$examine | On 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"] }
| Key | Meaning |
|---|---|
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. |
where | Optional 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. |
having | Optional score floor on $similarity: { "$similarity": { "$gte": t } } (inclusive) or { "$gt": t } (exclusive). |
threshold | Sugar for having: { "$similarity": { "$gte": t } }. |
limit | Top-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 } } } }
| Key | Meaning |
|---|---|
vector | The query vector to code. Or text β embedded via the column's embedder (see Vector search). |
k | Codebook size (number of clusters). |
as | Optional 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 } } } }
| Key | Meaning |
|---|---|
near | The query vector, or text (embedded via the column's embedder). |
k | Neighbours to vote (default 10). |
weight | Blend 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.
| Operator | Meaning | Example |
|---|---|---|
$surprisal | Rows 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.
Inverse links β $refs
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:
| Condition | Meaning | Example |
|---|---|---|
$exists | Has β₯1 referring row | { "$refs.orders.buyer": { "$exists": true } } β users with an order |
$has on a projected attribute | A 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
| Form | Meaning |
|---|---|
"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):
| Column | Meaning |
|---|---|
$value | The candidate value |
feature / field | v1'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 |
$p | Its predicted probability |
$score | The 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:
| Form | Meaning |
|---|---|
"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 form | Example | Reads |
|---|---|---|
| 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)
| Field | Meaning |
|---|---|
predict: "field" | Rank a field's values by probability given where |
where ON the predict field | RESTRICTS 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):
| Endpoint | Meaning |
|---|---|
/_evaluate | Train/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) |
/_estimate | Estimate 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" }) |
/_match | Candidate 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
| Field | Meaning |
|---|---|
config.ai | Inference 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.calibrate | Temperature-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.
| Option | Type | Meaning |
|---|---|---|
preset | string | The inference profile β the same names as the string form. |
design | f | legacy | Scoring stack. f (default) is the redesigned stack; legacy restores the pre-redesign stack and inerts mediation, nameBoost and stage4. |
mediation | boolean | Compose a link-target prediction through a near-deterministic sibling link (e.g. predict acceptor through processor). |
mediationTopMass | number (0,1] | Determinism gate for the sibling edge. |
mediationMinSupport | integer | Minimum mediator support for the composition to fire. |
mediationT | number >0 | Temperature flattening the sibling distribution. |
mediationGamma | number [0,1) | Prior mix-in, so no candidate is floored at 0. |
mediationSiblingBasedOn | boolean | Enrich the mediator stage with the linked table's attributes. |
mediationDirectMix | number [0,1] | Mix the direct expert into the mediated probability. |
mediationMaxCandidates | integer | Cost guard β skip mediation above this candidate count. |
nameBoost | boolean | XβX name-identity boost β evidence text that embeds a candidate's own name. |
nameBoostTheta | number >=0 | Boost strength. |
nameBoostMaxCandidates | integer | Cost guard for the boost. |
nameBoostSelfScaledCap | boolean | Bound 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. |
nameBoostQueryLikelihood | boolean | Score the identity match as a query-likelihood ratio `sum log P(w |
nameBoostQlMu | number >0 | Dirichlet 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. |
nameBoostFadeKappa | number >=0 | Fade 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. |
stage4 | boolean | Per-query redundancy weights (count each nat of evidence once). |
stage4MaxClasses | integer | Cost guard for stage 4. |
stage4ClassIndexMaxRows | integer | Memory 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. |
memo | boolean | Per-state scoring memos. Result-identical either way; for A/B verification. |
hitRelMaxN | integer | Global cap on kept (candidate, known) relations. |
hitRelPerCand | integer | Per-candidate relation floor (0 = the global-cap-only legacy path). |
hitRelAccuracy | number [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. |
hitRelPoolFactor | number >= 0 | Size 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. |
neffTemper | boolean | The n_eff temper (finite-sample overconfidence shrinkage). |
neffTemperMode | count | harmonic | coverage | auto | n_eff estimator. |
neffTemperKappa | number >0 | Temper kappa (ignored in auto). |
neffTemperBoostOnly | boolean | Shrink boosts only, leaving suppressions intact. |
neffRegularityFloor | precision | full | off | Field-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:
| spelling | what 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
fromwith plainrelateβ typed 501 (bit-universe alignment);relate $patternsDOES compose with nestedfrom. basedOn$patterns(conjunction mining as predict evidence) β fails loud; planned.- Arithmetic score composition beyond
$multiplyβ v1's$divide,$subtract,$pow,$normalizeand friends are not ported; only$multiplycomposes ranking values on v2 (letcovers derived fields).
All of these fail loud rather than degrade β a 4xx/501 with the reason,
never a silently wrong 200.