Skip to main content

Advanced Query Features

Advanced query patterns for complex data retrieval scenarios.

Property Paths (Transitive Queries)

Property paths allow you to traverse relationships across multiple hops in a single query. This is useful for querying hierarchical data like organizational structures, social networks, or category trees.

Syntax

PatternDescriptionExample
<prop+>One or more hops (transitive closure)"<ex:knows+>"
<prop*>Zero or more hops (includes self)"<ex:knows*>"

The property is wrapped in angle brackets with + or * suffix.

One or More Hops (+)

Find everyone Alice knows directly or indirectly:


{
"@context": {"ex": "http://example.org/"},
"select": "?who",
"where": {"@id": "ex:alice", "<ex:knows+>": "?who"}
}

If Alice knows Bob, and Bob knows Carol, this returns both Bob and Carol.

Zero or More Hops (*)

The * operator includes the starting node itself (reflexive transitive closure):


{
"@context": {"ex": "http://example.org/"},
"select": "?who",
"where": {"@id": "ex:alice", "<ex:knows*>": "?who"}
}

This returns Alice, Bob, and Carol (Alice is included because of zero hops).

Finding Ancestors

Property paths work in both directions. Find all ancestors of a person:


{
"@context": {"ex": "http://example.org/"},
"select": "?ancestor",
"where": {"@id": "?ancestor", "<ex:parent+>": {"@id": "ex:alice"}}
}

All Pairs

You can use variables on both sides to find all connected pairs:


{
"@context": {"ex": "http://example.org/"},
"select": ["?person", "?connection"],
"where": {"@id": "?person", "<ex:knows+>": "?connection"}
}

Cycle Handling

Property paths handle cycles automatically. If Alice knows Bob and Bob knows Alice, the query will still terminate and return the correct results without infinite loops.

Subqueries

Subqueries allow you to nest one query inside another. They are useful for:

  • Computing aggregates and using them in the parent query
  • Limiting results before joining with other data
  • Combining results from multiple independent queries

Syntax


["query", { ...query object... }]

Subqueries are placed in the where clause as an array with "query" as the first element.

Basic Subquery

Join data from a subquery with the parent query:


{
"@context": {"schema": "http://schema.org/"},
"select": ["?name", "?age"],
"where": [
{"@id": "?s", "schema:name": "?name"},
["query", {
"select": ["?s", "?age"],
"where": {"@id": "?s", "schema:age": "?age"}
}]
],
"orderBy": "?name"
}

Variables bound in the subquery (?s, ?age) are available in the parent query.

Subquery with Aggregate

Calculate an aggregate in a subquery and use it in the parent:


{
"@context": {"ex": "http://example.org/"},
"select": ["?person", "?favNums"],
"where": [
{"@id": "?person", "ex:favNums": "?favNums"},
["filter", "(> ?favNums ?avgFavNum)"],
["query", {
"where": {"ex:favNums": "?favN"},
"select": ["(as (avg ?favN) ?avgFavNum)"]
}]
]
}

This finds people whose favorite numbers are above the overall average.

Subquery with Limit

Limit results in a subquery before joining:


{
"@context": {"schema": "http://schema.org/", "ex": "http://example.org/"},
"select": ["?age", "?favNums"],
"where": [
{"schema:age": "?age"},
["query", {
"select": ["?favNums"],
"where": {"ex:favNums": "?favNums"},
"limit": 2
}]
]
}

selectDistinct in Subqueries

Use selectDistinct to get unique values from a subquery:


{
"select": ["?name", "?email"],
"where": [
{"schema:name": "?name"},
["query", {
"selectDistinct": ["?email"],
"where": {"schema:email": "?email"}
}]
]
}

Multiple Subqueries

You can have multiple subqueries in the same where clause:


{
"select": ["?age", "?favNums"],
"where": [
["query", {"select": ["?age"], "where": {"schema:age": "?age"}}],
["query", {"select": ["?favNums"], "where": {"ex:favNums": "?favNums"}}]
]
}

Nested Subqueries

Subqueries can be nested within other subqueries:


{
"select": ["?name", "?email", "?age"],
"where": [
{"schema:name": "?name"},
["query", {
"selectDistinct": ["?age", "?email"],
"where": [
{"schema:age": "?age"},
["query", {"select": ["?email"], "where": {"schema:email": "?email"}}]
]
}]
]
}

Subqueries in Union

Subqueries can be used inside union operations:


{
"select": ["?person", "?avgFavNum"],
"where": [
["union",
["query", {
"where": {"@id": "ex:alice", "ex:favNums": "?favN"},
"select": ["(as (str \"Alice\") ?person)", "(as (avg ?favN) ?avgFavNum)"]
}],
["query", {
"where": {"@id": "ex:bob", "ex:favNums": "?favN"},
"select": ["(as (str \"Bob\") ?person)", "(as (avg ?favN) ?avgFavNum)"]
}]
]
]
}

Time Travel in Queries

Fluree supports querying historical data using time travel syntax in the from clause. This allows you to query the database as it existed at a specific point in time.

Syntax

Append a time specifier to the ledger name using @:

SyntaxDescriptionExample
ledger@t:NQuery at transaction number N"myLedger@t:5"
ledger@iso:TIMESTAMPQuery at ISO-8601 timestamp"myLedger@iso:2024-01-15T10:30:00Z"
ledger@sha:HASHQuery at commit SHA"myLedger@sha:bafyrei..."

Query at Transaction Number

Query the state of the ledger at a specific transaction:


{
"from": "myLedger@t:5",
"select": ["?s", "?name"],
"where": {"@id": "?s", "schema:name": "?name"}
}

This returns data as it existed after transaction 5.

Query at ISO-8601 Timestamp

Query the state of the ledger at a specific point in time:


{
"from": "myLedger@iso:2024-06-15T14:30:00Z",
"select": ["?s", "?name"],
"where": {"@id": "?s", "schema:name": "?name"}
}

If the timestamp falls between transactions, Fluree returns the state as of the most recent transaction before that time.

Query at Commit SHA

Query the state at a specific commit using its SHA hash:


{
"from": "myLedger@sha:bafyreib",
"select": ["?s", "?name"],
"where": {"@id": "?s", "schema:name": "?name"}
}

SHA prefixes must be at least 6 characters. You can use the full SHA or a unique prefix.

With Branch Specification

Combine branch and time travel:


{
"from": "myLedger:main@t:10",
"select": ["?s", "?name"],
"where": {"@id": "?s", "schema:name": "?name"}
}

Value Map Filters

In addition to using filter as a where operation, you can apply filters directly inline within where conditions using value maps. This provides a more compact syntax for simple filtering.

Syntax


{
"propertyName": {"@value": "?variable", "@filter": "expression"}
}

Or using context aliases:


{
"@context": {"value": "@value", "filter": "@filter"},
"where": {
"schema:age": {"value": "?age", "filter": "(> ?age 45)"}
}
}

Example

Filter inline while binding variables:


{
"@context": {
"schema": "http://schema.org/",
"ex": "http://example.org/",
"value": "@value",
"filter": "@filter"
},
"select": ["?name", "?last"],
"where": {
"@type": "ex:User",
"schema:age": {"value": "?age", "filter": "(> ?age 45)"},
"schema:name": "?name",
"ex:last": {"value": "?last", "filter": "(strEnds ?last \"ith\")"}
}
}

This is equivalent to:


{
"select": ["?name", "?last"],
"where": [
{"@type": "ex:User", "schema:age": "?age", "schema:name": "?name", "ex:last": "?last"},
["filter", "(> ?age 45)", "(strEnds ?last \"ith\")"]
]
}

Federated Queries (Multi-Ledger)

Fluree supports querying across multiple ledgers in a single query. This is useful for:

  • Distributed data across microservices
  • Separating concerns (users, products, orders in different ledgers)
  • Combining data from multiple sources

Combined Datasets (from with Array)

When you provide an array of ledger names to from, the ledgers are merged into a single unified graph:


{
"@context": "https://schema.org",
"from": ["test/authors", "test/books", "test/movies"],
"select": ["?movieName", "?authorName"],
"where": {
"@type": "Movie",
"name": "?movieName",
"isBasedOn": {
"author": {"name": "?authorName"}
}
}
}

In this mode:

  • All data is merged as if it were in one ledger
  • Entities with the same @id across ledgers are unified
  • You can traverse relationships that span ledgers

Named Graphs (from-named with graph)

For more control, use from-named with explicit graph clauses to query ledgers separately:


{
"@context": "https://schema.org",
"from-named": ["test/authors", "test/books", "test/movies"],
"select": ["?movieName", "?bookIsbn", "?authorName"],
"where": [
["graph", "test/movies", {
"@id": "?movie",
"@type": "Movie",
"name": "?movieName",
"isBasedOn": "?book"
}],
["graph", "test/books", {
"@id": "?book",
"isbn": "?bookIsbn",
"author": "?author"
}],
["graph", "test/authors", {
"@id": "?author",
"name": "?authorName"
}]
]
}

In this mode:

  • Each ledger is kept separate
  • You explicitly specify which ledger to query with ["graph", "ledger-name", {...}]
  • Variables bind across graph clauses to join data

Graph Clause Syntax


["graph", "ledger-name", { ...where pattern... }]

The graph clause takes:

  1. The string "graph"
  2. The ledger name
  3. A where pattern to match in that ledger

Time Travel with Federated Queries

You can combine time travel with federated queries:


{
"from-named": ["ledger1@t:5", "ledger2@iso:2024-01-01T00:00:00Z"],
"select": ["?s", "?value"],
"where": [
["graph", "ledger1@t:5", {"@id": "?s", "value": "?value"}]
]
}

Fluree supports vector similarity search for AI/ML applications like semantic search, recommendations, and embeddings.

Vector Datatype

Store vectors using the f:Vector datatype:


{
"@context": {
"ex": "http://example.org/",
"f": "https://ns.flur.ee/ledger#"
},
"insert": {
"@id": "ex:document1",
"ex:embedding": {
"@value": [0.6, 0.5, 0.3, 0.8],
"@type": "f:Vector"
}
}
}

Similarity Functions

Use these functions in bind expressions to compute similarity scores:

FunctionDescriptionRange
dotProductDot product of two vectorsUnbounded
cosineSimilarityCosine similarity (angle-based)0 to 1
euclidianDistanceEuclidean distance0 to infinity (lower = more similar)

Find similar vectors using values to inject a target vector:


{
"@context": {"ex": "http://example.org/", "f": "https://ns.flur.ee/ledger#"},
"select": ["?doc", "?score"],
"values": ["?targetVec", [{"@value": [0.7, 0.6, 0.4, 0.9], "@type": "f:Vector"}]],
"where": [
{"@id": "?doc", "ex:embedding": "?vec"},
["bind", "?score", "(cosineSimilarity ?vec ?targetVec)"]
],
"orderBy": "(desc ?score)"
}

Filtering by Score

Add a filter to only return results above a similarity threshold:


{
"@context": {"ex": "http://example.org/", "f": "https://ns.flur.ee/ledger#"},
"select": ["?doc", "?score"],
"values": ["?targetVec", [{"@value": [0.7, 0.6], "@type": "f:Vector"}]],
"where": [
{"@id": "?doc", "ex:embedding": "?vec"},
["bind", "?score", "(cosineSimilarity ?vec ?targetVec)"],
["filter", "(> ?score 0.8)"]
]
}

Combining with Other Filters

Filter by metadata before computing similarity:


{
"@context": {"ex": "http://example.org/", "f": "https://ns.flur.ee/ledger#"},
"select": ["?doc", "?score", "?title"],
"values": ["?targetVec", [{"@value": [0.7, 0.6], "@type": "f:Vector"}]],
"where": [
{"@id": "?doc", "ex:embedding": "?vec", "ex:category": "technology", "ex:title": "?title"},
["bind", "?score", "(cosineSimilarity ?vec ?targetVec)"]
],
"orderBy": "(desc ?score)",
"limit": 10
}

Best Practices

  1. Normalize vectors — Especially for cosine similarity, normalized vectors give more consistent results
  2. Use appropriate dimensions — Match your embedding model's output dimension
  3. Filter first — Apply metadata filters before vector operations to reduce computation
  4. Limit results — Vector search over large datasets should use limit for performance

Next Steps