Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

JSON-LD Query

JSON-LD Query is Fluree’s native query language, providing a JSON-based interface for querying graph data. It combines the expressiveness of SPARQL with the convenience of JSON, making it easy to integrate with modern applications.

Overview

JSON-LD Query uses JSON-LD syntax to express queries, leveraging @context for IRI expansion and compaction. Queries are structured as JSON objects with familiar clauses like select, where, from, etc.

Basic Query Structure

{
  "@context": {
    "ex": "http://example.org/ns/"
  },
  "select": ["?name", "?age"],
  "where": [
    { "@id": "?person", "ex:name": "?name", "ex:age": "?age" }
  ]
}

Query Clauses

@context

The @context defines namespace mappings for IRI expansion/compaction:

{
  "@context": {
    "ex": "http://example.org/ns/",
    "schema": "http://schema.org/",
    "foaf": "http://xmlns.com/foaf/0.1/"
  }
}

When querying via the CLI, omitting @context causes the ledger’s default context to be injected automatically. The HTTP API defaults this behavior off; pass ?default-context=true to opt in for a request. To opt out explicitly, pass an empty object: "@context": {}. See opting out of the default context.

Note: When using fluree-db-api directly (embedded), @context is not injected automatically. Queries must supply their own context or use full IRIs. Use db_with_default_context() or GraphDb::with_default_context() to opt in.

select

Specifies what to return in results. The shape of select determines the shape of each output row.

Bare variable — one column, each row is the bound value (not wrapped in an array):

{
  "select": "?name"
}

Variable list — each row is [v1, v2, ...]:

{
  "select": ["?name", "?age"]
}

Wildcard — every variable bound in the WHERE clause:

{
  "select": "*"
}

Subject expansion — return a nested JSON-LD object instead of a flat row. The key is either a variable (the WHERE clause binds it to subjects) or an IRI constant (the named subject is expanded directly, no WHERE needed):

{
  "select": { "?person": ["*", { "schema:knows": ["@id", "schema:name"] }] },
  "where": { "@id": "?person", "@type": "schema:Person" }
}
{
  "select": { "ex:alice": ["*"] }
}

The array value is the selection spec — "*" for all forward properties, individual property names ("schema:name"), or nested object forms for sub-selections. Add "depth": N at the query top level to bound auto-expansion of unselected references.

Mixed array — combine flat variables and subject expansions in one row, in any order. Each object is an independent expansion with its own root and selection spec:

{
  "select": [
    "?age",
    { "?person": ["@id", "schema:name"] },
    { "?org": ["@id", "schema:name"] }
  ],
  "where": {
    "@id": "?person",
    "ex:age": "?age",
    "ex:worksFor": "?org"
  }
}

Each row is [age, expanded_person, expanded_org]. When every column is an IRI-constant expansion (no variable dependency anywhere in select), the output is independent of the WHERE solution count: the formatter emits one row regardless of how many solutions the WHERE produced.

S-expression columns — a select item that is a string starting with ( is an S-expression, in two flavors:

Aggregates. Auto-aliased (?count, ?sum, etc.) or with an explicit alias via (as ...):

{
  "select": ["?category", "(count ?product)"],
  "groupBy": ["?category"]
}
{
  "select": ["?category", "(as (count ?product) ?total)"],
  "groupBy": ["?category"]
}

Scalar expressions (COALESCE, IF, arithmetic, string/hash/date functions, …). Always require an explicit alias via (as <expr> ?alias). Mirrors SPARQL SELECT (expr AS ?alias):

{
  "select": ["?p", "(as (coalesce ?titleFr ?titleEn \"untitled\") ?title)"]
}
{
  "select": [
    "?name",
    "(as (coalesce ?email \"no-email\") ?contact)",
    "(as (count ?favNums) ?count)"
  ],
  "groupBy": ["?name", "?contact"]
}

Scalar select expressions desugar to a bind in the WHERE pattern list. If the expression references an aggregate’s output variable (e.g. (as (+ ?count 1) ?adjusted)) the bind runs after aggregation; otherwise it runs before, so the alias is also a valid groupBy key.

The same expression language is shared with bind and filter. The one exception is in / not-in, which require the bracketed-list form and are not accepted in select expressions — rewrite as (or (= ?x 1) (= ?x 2) …) instead.

ask

Tests whether a set of patterns has any solution, returning true or false. No variables are projected. Equivalent to SPARQL ASK. The value of ask is the where clause itself — an array or object of the same patterns accepted by where:

{
  "@context": { "ex": "http://example.org/ns/" },
  "ask": [
    { "@id": "?person", "ex:name": "Alice" }
  ]
}

Single-pattern shorthand (object instead of array):

{
  "@context": { "ex": "http://example.org/ns/" },
  "ask": { "@id": "?person", "ex:name": "Alice" }
}

Returns true if at least one solution exists, false otherwise. Internally, LIMIT 1 is applied for efficiency.

from

Specifies which ledger(s) to query:

Single Ledger:

{
  "from": "mydb:main"
}

Multiple Ledgers:

{
  "from": ["mydb:main", "otherdb:main"]
}

Time Travel:

{
  "from": "mydb:main@t:100"
}
{
  "from": "mydb:main@iso:2024-01-15T10:30:00Z"
}
{
  "from": "mydb:main@commit:bafybeig..."
}

where

The where clause contains query patterns:

Basic Pattern:

{
  "where": [
    { "@id": "?person", "ex:name": "?name" }
  ]
}

Multiple Patterns:

{
  "where": [
    { "@id": "?person", "ex:name": "?name" },
    { "@id": "?person", "ex:age": "?age" }
  ]
}

Type Pattern:

{
  "where": [
    { "@id": "?person", "@type": "ex:User", "ex:name": "?name" }
  ]
}

Pattern Types

Object Patterns

Match triples where subject, predicate, and object are specified:

{
  "@id": "ex:alice",
  "ex:name": "Alice"
}

Variable Patterns

Use variables (starting with ?) to match unknown values:

{
  "@id": "?person",
  "ex:name": "?name"
}

Type Patterns

Match entities by type:

{
  "@id": "?person",
  "@type": "ex:User",
  "ex:name": "?name"
}

Property Join Patterns

Match multiple properties of the same subject:

{
  "@id": "?person",
  "ex:name": "?name",
  "ex:age": "?age",
  "ex:email": "?email"
}

Advanced Patterns

Optional Patterns

Match optional data that may not exist:

{
  "where": [
    { "@id": "?person", "ex:name": "?name" },
    ["optional", { "@id": "?person", "ex:email": "?email" }]
  ]
}

Sibling vs. grouped OPTIONAL — semantics

The two forms below are not equivalent. Each ["optional", ...] array is a single OPTIONAL block in SPARQL terms — every item inside is part of the same conjunctive group, and a row is null-extended only when the group as a whole fails to match. To express two independent left joins, write two sibling arrays.

Sibling OPTIONALs — two independent left joins:

{
  "where": [
    { "@id": "?person", "ex:name": "?name" },
    ["optional", { "@id": "?person", "ex:email": "?email" }],
    ["optional", { "@id": "?person", "ex:phone": "?phone" }]
  ]
}

Equivalent SPARQL:

?person ex:name ?name .
OPTIONAL { ?person ex:email ?email }
OPTIONAL { ?person ex:phone ?phone }

?email and ?phone are independent — a person with only an email keeps ?email bound and gets null for ?phone, and vice versa.

Grouped OPTIONAL — one conjunctive left join:

{
  "where": [
    { "@id": "?person", "ex:name": "?name" },
    ["optional",
     { "@id": "?person", "ex:email": "?email" },
     { "@id": "?person", "ex:phone": "?phone" }
    ]
  ]
}

Equivalent SPARQL:

?person ex:name ?name .
OPTIONAL { ?person ex:email ?email . ?person ex:phone ?phone }

?email and ?phone are bound together — a person who has an email but no phone is null-extended on both variables, because the inner conjunctive group did not match as a whole.

Filters and binds inside OPTIONAL

filter and bind constrain or compute from existing bindings, so they need something to anchor to inside the OPTIONAL block. Any binding-producing pattern qualifies as an anchor — a node-map, values, an earlier bind, a nested optional, or a sub-query. A filter or bind as the very first item in an OPTIONAL array is rejected.

["optional",
  { "@id": "?person", "ex:age": "?age" },
  ["filter", "(> ?age 18)"]
]
["optional",
  ["values", ["?x", [1, 2, 3]]],
  ["filter", "(> ?x 0)"]
]

Union Patterns

Match data from multiple alternative patterns:

{
  "where": [
    ["union",
     { "@id": "?person", "ex:name": "?name" },
     { "@id": "?person", "ex:alias": "?name" }
    ]
  ]
}

Negation Patterns

Restrict results to subjects that do not match a given pattern. Two forms are available; prefer not-exists for the common “subjects without property p” case.

not-exists (canonical)

["not-exists", { ... }] keeps rows for which the inner pattern has no solution under the current bindings:

{
  "where": [
    { "@id": "?file", "@type": "ex:File" },
    ["not-exists", { "@id": "?file", "ex:parent": "?p" }]
  ],
  "select": "?file"
}

When the inner pattern is correlated to the outer only via vars the inner itself produces (here ?file, produced by both the outer triple and the inner triple), and the inner does not reference any outer-only variable (e.g. via a ["filter", ...] mentioning an outer var the inner doesn’t produce), the planner builds the inner solution set once and probes it per outer row — the performant path on large outer streams. If either condition fails, the inner is rebuilt and re-run per outer row (still correct, but linear in the outer cardinality).

["exists", { ... }] is the affirmative dual and uses the same dispatch.

OPTIONAL + (not (bound ?v))

The SPARQL-muscle-memory form OPTIONAL { ... } FILTER (!bound(?v)) is spelled in JSON-LD as an optional followed by a filter using the supported bound function:

{
  "where": [
    { "@id": "?file", "@type": "ex:File" },
    ["optional", { "@id": "?file", "ex:parent": "?p" }],
    ["filter", "(not (bound ?p))"]
  ],
  "select": "?file"
}

The data filter form is equivalent: ["filter", ["not", ["bound", "?p"]]].

When the optional block binds ?v, the filter immediately tests !bound(?v), and ?v is not used after the filter, the planner rewrites this shape to a NOT EXISTS and dispatches it through the same strategy chooser as not-exists. The two forms therefore have the same plan and the same performance.

minus

["minus", { ... }] removes solutions whose shared variables match the inner pattern. Use when you want set-difference semantics rather than negation-as-failure (see SPARQL 1.1 §8.3 for the difference).

Filter expression — supported “is-unbound” idioms

The s-expression parser recognizes not, and, or as AST operators and bound as a function. The following are not parser tokens:

AttemptResult
(not (bound ?p))✅ supported
["not", ["bound", "?p"]]✅ supported (data filter form)
(!bound ?p), (! (bound ?p))❌ parse error — use (not (bound ?p))
(nil? ?p)❌ parse error — use (not (bound ?p))

The “Unknown function” parse error includes a hint pointing at (not (bound ?v)) or ["not-exists", ...] for these common attempts.

SPARQL → JSON-LD translation

SPARQLJSON-LD
FILTER NOT EXISTS { ?s :p ?v }["not-exists", {"@id":"?s","ex:p":"?v"}]
FILTER EXISTS { ?s :p ?v }["exists", {"@id":"?s","ex:p":"?v"}]
OPTIONAL { ?s :p ?v } FILTER(!bound(?v))["optional", {"@id":"?s","ex:p":"?v"}] then ["filter", "(not (bound ?v))"]
?a :p ?b MINUS { ?a :q ?c }{"@id":"?a","ex:p":"?b"} then ["minus", {"@id":"?a","ex:q":"?c"}]

Graph Patterns

Scope patterns to a named graph:

{
  "@context": { "ex": "http://example.org/ns/" },
  "from": "mydb:main",
  "fromNamed": {
    "products": {
      "@id": "mydb:main",
      "@graph": "http://example.org/graphs/products"
    }
  },
  "select": ["?product", "?name"],
  "where": [
    ["graph", "products", { "@id": "?product", "ex:name": "?name" }]
  ]
}

Notes:

  • fromNamed is an object whose keys are dataset-local aliases. Each value is an object with @id (ledger reference) and optional @graph (graph selector IRI).
  • The second element of ["graph", ...] can be a dataset-local alias (recommended) or a graph IRI.
  • The legacy "from-named": [...] array format is still accepted for backward compatibility.
  • For dataset and named-graph configuration details, see docs/query/datasets.md.

Filter Patterns

Apply conditions to filter results:

Single Filter:

{
  "where": [
    { "@id": "?person", "ex:age": "?age" },
    ["filter", "(> ?age 18)"]
  ]
}

Multiple Filters:

{
  "where": [
    { "@id": "?person", "ex:age": "?age", "ex:name": "?name" },
    ["filter", "(> ?age 18)", "(strStarts ?name \"A\")"]
  ]
}

Complex Filters:

{
  "where": [
    { "@id": "?person", "ex:age": "?age", "ex:last": "?last" },
    ["filter", "(and (> ?age 45) (strEnds ?last \"ith\"))"]
  ]
}

Bind Patterns

Compute values and bind to variables:

{
  "where": [
    { "@id": "?person", "ex:age": "?age" },
    ["bind", "?nextAge", "(+ ?age 1)"]
  ]
}

Values Patterns

Provide initial bindings:

{
  "where": [
    ["values", "?name", ["Alice", "Bob", "Carol"]],
    { "@id": "?person", "ex:name": "?name" }
  ]
}

Property Paths

Property paths enable transitive traversal of predicates, following chains of relationships across multiple hops. Define a path alias in @context using @path, then use the alias as a key in WHERE node-maps.

Defining a Path Alias:

Add a term definition with @path to your @context. The value of @path can be a string (SPARQL property path syntax) or an array (S-expression form).

String Form (SPARQL syntax):

{
  "@context": {
    "ex": "http://example.org/",
    "knowsPlus": { "@path": "ex:knows+" }
  },
  "select": ["?who"],
  "where": [
    { "@id": "ex:alice", "knowsPlus": "?who" }
  ]
}

This returns all entities reachable from ex:alice by following one or more ex:knows edges transitively.

Array Form (S-expression):

{
  "@context": {
    "ex": "http://example.org/",
    "knowsPlus": { "@path": ["+", "ex:knows"] }
  },
  "select": ["?who"],
  "where": [
    { "@id": "ex:alice", "knowsPlus": "?who" }
  ]
}

The array form uses the operator as the first element followed by its operands.

Supported Operators:

OperatorString syntaxArray syntaxDescription
One or moreex:p+["+", "ex:p"]Transitive closure (1+ hops)
Zero or moreex:p*["*", "ex:p"]Reflexive transitive closure (0+ hops)
Inverse^ex:p["^", "ex:p"]Traverse predicate in reverse direction
Alternativeex:a|ex:b[“|”, “ex:a”, “ex:b”]Match any of several predicates
Sequenceex:a/ex:b["/", "ex:a", "ex:b"]Follow a chain of predicates (property chain)

Zero-or-more (*) includes the starting node itself in the results (zero hops).

Sequence (/) compiles into a chain of triple patterns joined by internal intermediate variables. Each step must be a simple predicate or an inverse simple predicate (^ex:p). For example, "ex:friend/ex:name" matches paths where subject has a ex:friend whose ex:name is the result.

Parsed but Not Yet Supported:

The following operators are recognized by the parser but currently rejected (not yet supported for execution):

OperatorString syntaxArray syntax
Zero or oneex:p?["?", "ex:p"]

Subject and Object Variables:

Path aliases work with variables on either side:

{
  "@context": {
    "ex": "http://example.org/",
    "knowsPlus": { "@path": "ex:knows+" }
  },
  "select": ["?x", "?y"],
  "where": [
    { "@id": "?x", "knowsPlus": "?y" }
  ]
}

This returns all pairs (?x, ?y) where ?y is transitively reachable from ?x via ex:knows.

Fixed Subject or Object:

You can also fix one end to an IRI:

{
  "@context": {
    "ex": "http://example.org/",
    "knowsPlus": { "@path": "ex:knows+" }
  },
  "select": ["?who"],
  "where": [
    { "@id": "?who", "knowsPlus": { "@id": "ex:bob" } }
  ]
}

This finds all entities that can reach ex:bob through one or more ex:knows hops.

Inverse Example:

Find entities that know ex:bob (traverse ex:knows in reverse):

{
  "@context": {
    "ex": "http://example.org/",
    "knownBy": { "@path": "^ex:knows" }
  },
  "select": ["?who"],
  "where": [
    { "@id": "ex:bob", "knownBy": "?who" }
  ]
}

Alternative Example:

Match entities connected by either ex:knows or ex:likes:

{
  "@context": {
    "ex": "http://example.org/",
    "connected": { "@path": "ex:knows|ex:likes" }
  },
  "select": ["?who"],
  "where": [
    { "@id": "ex:alice", "connected": "?who" }
  ]
}

Inverse can also be applied to complex paths (sequences and alternatives):

  • ^(ex:friend/ex:name) — inverse of a sequence: reverses the step order and inverts each step, producing (^ex:name)/(^ex:friend)
  • ^(ex:name|ex:nick) — inverse of an alternative: distributes the inverse into each branch, producing (^ex:name)|(^ex:nick)
  • Double inverse cancels: ^(^ex:p) simplifies to ex:p

Array form examples:

{ "@path": ["^", ["/", "ex:friend", "ex:name"]] }
{ "@path": ["^", ["|", "ex:name", "ex:nick"]] }

Inverse is supported inside alternative branches (e.g. ex:knows|^ex:knows matches both directions of the ex:knows predicate).

Alternative branches can also be sequence chains. For example, ex:friend/ex:name|ex:colleague/ex:name returns the name of a friend OR the name of a colleague:

{
  "@context": {
    "ex": "http://example.org/",
    "contactName": { "@path": "ex:friend/ex:name|ex:colleague/ex:name" }
  },
  "select": ["?name"],
  "where": [
    { "@id": "ex:alice", "contactName": "?name" }
  ]
}

Branches can freely mix simple predicates, inverse predicates, and sequence chains (e.g. ex:name|ex:friend/ex:name|^ex:colleague).

Alternative uses UNION semantics (bag, not set): when multiple branches match the same (subject, object) pair, duplicate solutions are produced. Use selectDistinct if set semantics are needed.

Sequence (Property Chain) Example:

Follow a chain of predicates. The string form uses / to separate steps:

{
  "@context": {
    "ex": "http://example.org/",
    "friendName": { "@path": "ex:friend/ex:name" }
  },
  "select": ["?person", "?name"],
  "where": [
    { "@id": "?person", "friendName": "?name" }
  ]
}

The array form uses "/" as the operator:

{ "@path": ["/", "ex:friend", "ex:name"] }

Sequence steps can include inverse predicates. For example, "^ex:parent/ex:name" traverses the ex:parent link backwards, then follows ex:name:

{ "@path": "^ex:parent/ex:name" }

Longer chains are supported: "ex:friend/ex:address/ex:city" follows three hops.

Sequence steps can also be alternatives. For example, "ex:friend/(ex:name|ex:nick)" distributes the alternative into a union of chains (ex:friend/ex:name and ex:friend/ex:nick):

{ "@path": "ex:friend/(ex:name|ex:nick)" }

Array form:

{ "@path": ["/", "ex:friend", ["|", "ex:name", "ex:nick"]] }

Multiple alternative steps are supported: "(ex:a|ex:b)/(ex:c|ex:d)" expands to 4 chains. A safety limit of 64 expanded chains is enforced to prevent combinatorial explosion.

Each step must be a simple predicate (ex:p), inverse simple predicate (^ex:p), or an alternative of simple predicates ((ex:a|ex:b)). Transitive (+/*) and nested sequence modifiers are not allowed inside sequence steps.

Rules:

  • @path and @reverse are mutually exclusive on the same term definition (produces an error).
  • @path and @id may coexist on the same term definition; when the alias key appears in a WHERE node-map, the @path definition is used.
  • Cycle detection is built in: transitive traversal terminates when it encounters a node already visited.
  • Variable names starting with ?__ are reserved for internal use (e.g., intermediate join variables generated by sequence paths). These variables will not appear in wildcard (select: "*") output.

Filter Functions

Comparison Functions

Comparison operators accept two or more arguments. With multiple arguments, they chain pairwise: (< ?a ?b ?c) means ?a < ?b AND ?b < ?c.

  • (= ?x ?y ...) - Equality
  • (!= ?x ?y ...) - Inequality
  • (> ?x ?y ...) - Greater than
  • (>= ?x ?y ...) - Greater than or equal
  • (< ?x ?y ...) - Less than
  • (<= ?x ?y ...) - Less than or equal

When comparing incomparable types (e.g., a number and a string):

  • = yields false — values of different types are not equal
  • != yields true — values of different types are not equal
  • <, <=, >, >= raise an error — ordering between incompatible types is undefined

Logical Functions

  • (and ...) - Logical AND
  • (or ...) - Logical OR
  • (not ...) - Logical NOT

String Functions

  • (strStarts ?str ?prefix) - String starts with
  • (strEnds ?str ?suffix) - String ends with
  • (contains ?str ?substr) - String contains
  • (regex ?str ?pattern) - Regular expression match

Numeric Functions

Arithmetic operators accept two or more arguments. With multiple arguments, they fold left: (+ ?x ?y ?z) evaluates as (?x + ?y) + ?z. A single argument returns the value unchanged.

  • (+ ?x ?y ...) - Addition
  • (- ?x ?y ...) - Subtraction
  • (* ?x ?y ...) - Multiplication
  • (/ ?x ?y ...) - Division
  • (- ?x) - Unary negation (single argument)
  • (abs ?x) - Absolute value

Vector Similarity Functions

Used with bind to compute similarity scores between @vector values:

  • (dotProduct ?vec1 ?vec2) - Dot product (inner product)
  • (cosineSimilarity ?vec1 ?vec2) - Cosine similarity (-1 to 1)
  • (euclideanDistance ?vec1 ?vec2) - Euclidean (L2) distance

Function names are case-insensitive. See Vector Search for usage examples.

Type Functions

  • (bound ?var) - Variable is bound. To test for absence, see Negation Patterns(not (bound ?v)) after optional is supported, or use ["not-exists", ...] directly.
  • (isIRI ?x) - Is an IRI
  • (isBlank ?x) - Is a blank node
  • (isLiteral ?x) - Is a literal

Query Modifiers

orderBy

Sort results:

{
  "orderBy": ["?name"]
}

Descending Order:

{
  "orderBy": [["desc", "?age"]]
}

Multiple Sort Keys:

{
  "orderBy": ["?last", ["desc", "?age"]]
}

limit

Limit number of results:

{
  "limit": 10
}

offset

Skip results:

{
  "offset": 20,
  "limit": 10
}

groupBy

Group results:

{
  "select": ["?category", "(count ?product)"],
  "groupBy": ["?category"],
  "where": [
    { "@id": "?product", "ex:category": "?category" }
  ]
}

having

Filter grouped results:

{
  "select": ["?category", "(count ?product)"],
  "groupBy": ["?category"],
  "having": [["filter", "(> (count ?product) 10)"]],
  "where": [
    { "@id": "?product", "ex:category": "?category" }
  ]
}

Aggregation Functions

  • (count ?var) / (count *) — count non-null values; * counts solution rows
  • (count-distinct ?var) — count distinct non-null values
  • (sum ?var) — sum numeric values
  • (avg ?var) — average numeric values
  • (min ?var) / (max ?var) — extremum
  • (median ?var) — median
  • (variance ?var) / (stddev ?var) — population variance / standard deviation
  • (sample ?var) — implementation-defined sample value
  • (groupconcat ?var) / (groupconcat ?var ", ") — concatenate string values, optional separator (defaults to a single space)

Each aggregate auto-aliases to ?<fn-name> (?count, ?sum, …). Use (as (<fn> ?var) ?alias) to choose an explicit alias.

Time Travel Queries

Query historical data using time specifiers in from:

Transaction Number:

{
  "@context": { "ex": "http://example.org/ns/" },
  "from": "ledger:main@t:100",
  "select": ["?name"],
  "where": [
    { "@id": "?person", "ex:name": "?name" }
  ]
}

ISO Timestamp:

{
  "@context": { "ex": "http://example.org/ns/" },
  "from": "ledger:main@iso:2024-01-15T10:30:00Z",
  "select": ["?name"],
  "where": [
    { "@id": "?person", "ex:name": "?name" }
  ]
}

Commit ContentId:

{
  "@context": { "ex": "http://example.org/ns/" },
  "from": "ledger:main@commit:bafybeig...",
  "select": ["?name"],
  "where": [
    { "@id": "?person", "ex:name": "?name" }
  ]
}

Multiple Ledgers at Different Times:

{
  "@context": { "ex": "http://example.org/ns/" },
  "from": ["ledger1:main@t:100", "ledger2:main@t:200"],
  "select": ["?data"],
  "where": [
    { "@id": "?entity", "ex:data": "?data" }
  ]
}

History Queries

History queries let you see all changes (assertions and retractions) within a time range. Specify the range using from and to keys with time-specced endpoints:

Time Range Syntax

{
  "from": "ledger:main@t:1",
  "to": "ledger:main@t:latest"
}

Binding Transaction Metadata

Use @t and @op annotations on value objects to capture metadata:

  • @t - Binds the transaction time (integer) when the fact was asserted/retracted.
  • @op - Binds the operation type as a boolean: true for assertions, false for retractions. (Mirrors Flake.op on disk; constants "assert" / "retract" are not accepted — use true / false.)

Both annotations work uniformly for literal-valued and IRI-valued objects.

Entity History:

{
  "@context": { "ex": "http://example.org/ns/" },
  "from": "ledger:main@t:1",
  "to": "ledger:main@t:latest",
  "select": ["?name", "?age", "?t", "?op"],
  "where": [
    { "@id": "ex:alice", "ex:name": { "@value": "?name", "@t": "?t", "@op": "?op" } },
    { "@id": "ex:alice", "ex:age": "?age" }
  ],
  "orderBy": "?t"
}

Property-Specific History:

{
  "@context": { "ex": "http://example.org/ns/" },
  "from": "ledger:main@t:1",
  "to": "ledger:main@t:100",
  "select": ["?age", "?t", "?op"],
  "where": [
    { "@id": "ex:alice", "ex:age": { "@value": "?age", "@t": "?t", "@op": "?op" } }
  ],
  "orderBy": "?t"
}

Time Range with ISO:

{
  "@context": { "ex": "http://example.org/ns/" },
  "from": "ledger:main@iso:2024-01-01T00:00:00Z",
  "to": "ledger:main@iso:2024-12-31T23:59:59Z",
  "select": ["?name", "?t", "?op"],
  "where": [
    { "@id": "ex:alice", "ex:name": { "@value": "?name", "@t": "?t", "@op": "?op" } }
  ]
}

Filter by Operation:

You can either use a constant @op shorthand (preferred) or filter on the bound variable:

{
  "@context": { "ex": "http://example.org/ns/" },
  "from": "ledger:main@t:1",
  "to": "ledger:main@t:latest",
  "select": ["?name", "?t"],
  "where": [
    { "@id": "ex:alice", "ex:name": { "@value": "?name", "@t": "?t", "@op": false } }
  ]
}

The shorthand "@op": false lowers to FILTER(op(?name) = false). Equivalent long form using a bound variable: "@op": "?op" plus ["filter", "(= ?op false)"].

All Properties History:

{
  "@context": { "ex": "http://example.org/ns/" },
  "from": "ledger:main@t:1",
  "to": "ledger:main@t:latest",
  "select": ["?property", "?value", "?t", "?op"],
  "where": [
    { "@id": "ex:alice", "?property": { "@value": "?value", "@t": "?t", "@op": "?op" } }
  ],
  "orderBy": "?t"
}

Graph Source Queries

Query graph sources using the same syntax:

BM25 Search:

{
  "@context": {
    "f": "https://ns.flur.ee/db#"
  },
  "from": "products:main@t:1000",
  "select": ["?product", "?score"],
  "where": [
    {
      "f:graphSource": "products-search:main",
      "f:searchText": "laptop",
      "f:searchLimit": 10,
      "f:searchResult": { "f:resultId": "?product", "f:resultScore": "?score" }
    }
  ],
  "orderBy": [["desc", "?score"]],
  "limit": 10
}

Vector Similarity:

{
  "@context": {
    "ex": "http://example.org/",
    "f": "https://ns.flur.ee/db#"
  },
  "from": "documents:main",
  "select": ["?document", "?similarity"],
  "values": [
    ["?queryVec"],
    [{"@value": [0.1, 0.2, 0.3], "@type": "https://ns.flur.ee/db#embeddingVector"}]
  ],
  "where": [
    {
      "f:graphSource": "documents-vector:main",
      "f:queryVector": "?queryVec",
      "f:searchLimit": 5,
      "f:searchResult": { "f:resultId": "?document", "f:resultScore": "?similarity" }
    }
  ],
  "orderBy": [["desc", "?similarity"]],
  "limit": 5
}

Note: f:* keys used for graph source queries should be defined in your @context for clarity.

Complete Examples

Simple Select Query

{
  "@context": {
    "ex": "http://example.org/ns/"
  },
  "select": ["?name", "?age"],
  "where": [
    {
      "@id": "?person",
      "@type": "ex:User",
      "ex:name": "?name",
      "ex:age": "?age"
    },
    ["filter", "(> ?age 18)"]
  ],
  "orderBy": ["?name"],
  "limit": 10
}

Complex Query with Joins

{
  "@context": {
    "ex": "http://example.org/ns/"
  },
  "select": ["?person", "?friend", "?friendName"],
  "where": [
    { "@id": "?person", "ex:name": "?name" },
    { "@id": "?person", "ex:friend": "?friend" },
    { "@id": "?friend", "ex:name": "?friendName" },
    ["filter", "(= ?name \"Alice\")"]
  ]
}

Aggregation Query

{
  "@context": {
    "ex": "http://example.org/ns/"
  },
  "select": [
    "?category",
    "(as (count ?product) ?count)",
    "(as (avg ?price) ?avgPrice)"
  ],
  "groupBy": ["?category"],
  "having": [["filter", "(> (count ?product) 5)"]],
  "where": [
    { "@id": "?product", "ex:category": "?category", "ex:price": "?price" }
  ],
  "orderBy": [["desc", "?count"]]
}

Parse Options

JSON-LD queries accept parse-time options under a top-level opts object. These control how the query is parsed (not what it returns).

strictCompactIri

By default, JSON-LD queries reject unresolved compact-looking IRIs (prefix:suffix where the prefix is not in @context) at parse time. To opt out:

{
  "@context": {"ex": "http://example.org/ns/"},
  "opts": {"strictCompactIri": false},
  "select": ["?id", "?name"],
  "where": {"@id": "?id", "ex:name": "?name"}
}

The default is true. Disable only when you are intentionally working with bare prefix:suffix strings as opaque identifiers. See IRIs and @context — Strict Compact-IRI Guard for the full policy.

Best Practices

  1. Always Provide @context: Makes queries readable and maintainable
  2. Use Specific Patterns: More specific patterns are more efficient
  3. Limit Result Sets: Use limit for large result sets
  4. Flexible Filter Placement: Filters can be placed anywhere in where clauses - the query engine automatically applies each filter as soon as all its required variables are bound
  5. Use Time Specifiers: Use @t: when transaction numbers are known (fastest)
  6. Graph Source Selection: Choose appropriate graph sources for query patterns