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-apidirectly (embedded),@contextis not injected automatically. Queries must supply their own context or use full IRIs. Usedb_with_default_context()orGraphDb::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:
| Attempt | Result |
|---|---|
(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
| SPARQL | JSON-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:
fromNamedis 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:
| Operator | String syntax | Array syntax | Description |
|---|---|---|---|
| One or more | ex:p+ | ["+", "ex:p"] | Transitive closure (1+ hops) |
| Zero or more | ex:p* | ["*", "ex:p"] | Reflexive transitive closure (0+ hops) |
| Inverse | ^ex:p | ["^", "ex:p"] | Traverse predicate in reverse direction |
| Alternative | ex:a|ex:b | [“|”, “ex:a”, “ex:b”] | Match any of several predicates |
| Sequence | ex: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):
| Operator | String syntax | Array syntax |
|---|---|---|
| Zero or one | ex: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 toex: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:
@pathand@reverseare mutually exclusive on the same term definition (produces an error).@pathand@idmay coexist on the same term definition; when the alias key appears in a WHERE node-map, the@pathdefinition 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):
=yieldsfalse— values of different types are not equal!=yieldstrue— 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))afteroptionalis 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:
truefor assertions,falsefor retractions. (MirrorsFlake.opon disk; constants"assert"/"retract"are not accepted — usetrue/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
- Always Provide @context: Makes queries readable and maintainable
- Use Specific Patterns: More specific patterns are more efficient
- Limit Result Sets: Use
limitfor large result sets - Flexible Filter Placement: Filters can be placed anywhere in
whereclauses - the query engine automatically applies each filter as soon as all its required variables are bound - Use Time Specifiers: Use
@t:when transaction numbers are known (fastest) - Graph Source Selection: Choose appropriate graph sources for query patterns
Related Documentation
- SPARQL: SPARQL query language
- Time Travel: Historical queries
- Graph Sources: Graph source queries
- Output Formats: Query result formats
- IRIs and @context: IRI resolution and the strict compact-IRI guard