<!-- GENERATED FILE โ€” do not edit. Source: core/core/src/main/scala/com/knowledgegarden/episto/core/query2/docs/V2QueryDocs.scala; 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 (text fields tokenize){ "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

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 components

Date columns are stored ISO-8601 and queried by component:

OperatorMeaning
$year $month $dayCalendar components
$weekday / $dayOfWeekDay of week
$dayOfYear1โ€“366
$matchMatch the date as text
{ "from": "orders", "where": { "created": { "$weekday": 6 } } }

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 }

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.

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.

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)
{ "$similarity": { <textField>: <text> } }Rank rows by the BM25 relevance of the field against the text (the search scorer โ€” real scores, not an approximation). On a get over a link, the field may be a linked text column: candidates score by their link target's BM25. $sameness is an accepted alias.
{ "$multiply": [ โ€ฆ ] }Rank by the per-row product of parts: the string "$similarity" (references the query's $match where-term), a { "$similarity": {field: text} } object, or a { "$p": { "$context": โ€ฆ } } part
{ "from": "impressions", "get": "product",
  "orderBy": { "$p": { "$context": { "click": 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
$pIts predicted probability
$scoreThe value used by orderBy โ€” for a predict/recommend ranking, equal to $p
$fThe candidate's frequency โ€” count of rows where the predicted field equals this value, under the where
$liftThe candidate's lift โ€” how much the where evidence raises its probability over its base rate
$whyThe 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
$highlightOn a search query: the matched field value with the query's tokens marked with tags โ€” the search-hit form of $why's highlight
$matchesOn 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:fRelational 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)

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)

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 and orderBy today. Using a let field as where or prediction evidence (e.g. basedOn: { "$numeric": "margin" }) is in progress.

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
recommend: "linkField" + goal: {โ€ฆ}Rank link candidates by P(goal)
relate: ["field", โ€ฆ]Return statistical relations (lift, info-gain), not rows
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
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: posterior-mean (probability-weighted mean over the predicted candidates, weightedAverage why). model: "regression" (pure transformation-pipeline regression) and model: "knn" (K-NN adjusted) run the v1 estimation algorithms; both require plain-equality where conditions
/_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" })
/_matchA field's distinct values ranked by frequency under the where (lowered to get + orderBy: "$f")

Config

FieldMeaning
config.aiInference profile preset โ€” v2/high (And + Group re-expression, the default), group (Group only), v1/and (legacy And), or fast/flat (plain naive Bayes); unknown names fail loud
config.calibrateTemperature-calibrate probabilities once per DB state

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.

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 โ€” 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.