Working with Context
This guide explains how to leverage @context
to simplify your queries and
transactions and to make your data more readable and operational.
Overview
Hi there! 👋 As you read through this guide about the value of using @context
, you'll get the most out of things by trying the various queries and transactions against a live Fluree db. We've made that really easy for you!
Either at the bottom-right of your screen, or inside of each code snippet when you hover your cursor over them, you'll see a little yeti icon--that's our friend, Freddy the Yeti!
Clicking on him, will open a live sandbox IDE that allows you to test various transactions and queries directly against an in-memory database! Finding him in individual code snippets will even directly load the code snippet into the sandbox for you!
If you're not already familiar with the JSON-LD @context
keyword, it effectively allows you to define a set of terms that can be used in place of full IRIs. For example, instead of using http://www.w3.org/1999/02/22-rdf-syntax-ns#Property
you can use rdf:Property
(where rdf
is defined in your @context
).
In the world of linked or semantic data, we want to unlock data's potential by making it meaningful not simply to the closed-ecosystem of our database or a single application, but to an open-ecosystem of applications and databases -- a web of data. Using fully-qualified IRIs to identify both vocabulary terms and entity IDs empowers our data to share common meaning even as they travel across the web and are used across all sorts of different systems.
However, this can make our data difficult to read and write. For example, the following query is valid, but it's not very readable:
{ "select": { "?s": ["*"] }, "where": { "@id": "?s", "@type": "http://schema.org/Person", "http://example.org/favNums": 7 }}
Using @context
, we can define a set of terms that can be used in place of the full IRIs. For example, we could define a @context
that sets prefixes for schema
and ex
, and then our query might look like this:
{ "@context": { "schema": "http://schema.org/", "ex": "http://example.org/" }, "select": { "?s": ["*"] }, "where": { "@id": "?s", "@type": "schema:Person", "ex:favNums": 7 }}
In the following guide, we will explore how to use @context
to simplify your queries and transactions and to make your data more readable and operational.
Defining a Context
Overview
When thinking of JSON-LD data generally (which is to say, outside of the realm of databases, queries, and transactions) @context
provides a shorthand for merely simplifying the way that data appears in a very particular JSON-LD document.
For example, if we think of the following JSON-LD data--not as a query or as a transaction--but simply as a set of facts about an entity, the terms defined in @context
simply make the data in this document more readable, while also allowing any consumer (be they machine or human) to understand how the data could expand to fully-qualified IRIs for all vocabulary terms or entity identifiers:
{ "@context": { "schema": "http://schema.org/", "ex": "http://example.org/" }, "@id": "ex:Person/1", "@type": "schema:Person", "schema:name": "Jane Doe", "schema:email": "jdoe@gmail.com"}
In this example, schema:name
is more cleanly readable as a property, name
, of the schema
vocabulary. But thanks to our term definitions in @context
, a machine trying to interpret this data could still expand schema:name
to http://schema.org/name
, allowing any consumer to globally disambiguate the meaning of name
no matter where this data is created, used, or stored.
Defining a Context in Fluree
When considering the utility of @context
in Fluree specifically, the shorthand terms that we define not only simplify the way that data appears in a particular JSON-LD document, but they also simplify how we write queries & transactions, and even simplify and help shape how our query results are returned to us.
For example, if we issue a query with an @context
that defines prefixes for "http://example.org/"
and http://schema.org/
, then we not only simplify the structure of our query, but our query results will also compact any fully-qualified IRIs that we prefixed in our @context
map:
{ "@context": { "ex": "http://example.org/", "schema": "http://schema.org/" }, "select": { "?s": ["*"] }, "where": { "@id": "?s", "schema:name": "Jane Doe" }}
We are even able to provide entirely arbitrary aliases for terms. For example, in the following query, we define schema
as a prefix for http://schema.org/
, but we also alias schema:name
as, simply, name
:
{ "@context": { "ex": "http://example.org/", "schema": "http://schema.org/", "name": "schema:name" }, "select": { "?s": ["*"] }, "where": { "@id": "?s", "name": "Jane Doe" }}
If you're using the Fluree Sandbox (by clicking the Freddy the Yeti icon in the code snippets above), you may have noticed that part of our query results included an expanded IRI for the property http://xmlns.com/foaf/0.1/homepage
. This is because we did not define a prefix for http://xmlns.com/foaf/0.1/
in our @context
map.
If we had defined a prefix such as foaf
for http://xmlns.com/foaf/0.1/
in our @context
map, then our query results would have been compacted to use that prefix instead of the fully-qualified IRI (e.g. foaf:homepage
).
When using @context
in queries and transactions, the terms that we define are specific to the individual instance of that single query or transaction. In other words, the terms that we define in @context
are not persisted in the database, and they are not available to other queries or transactions by default.
Using Context in Transactions
Leveraging either @context
maps in transactions is much the same as it is for queries, except that our transactions do not return JSON-LD data results. The value, then, is predominantly for allowing us to use shorthand terms in our transactions themselves.
Take, for example, the preference for allowing application code to more easily compose JSON-shaped transactions without having to fully qualify IRIs for each property, entity, etc.
Here is a transaction that has been simplified by using @context
to shorthand our longer IRIs:
{ "@context": { "schema": "http://schema.org/", "ex": "http://example.org/" }, "ledger": "working-with-context-dataset", "insert": [ { "@id": "ex:Person/1", "@type": "schema:Person", "schema:name": "Jane Doe" } ]}
It is very important to remember that one of the goals of semantic / linked data and JSON-LD in particular is to describe data in a way that it is commonly meaningful across systems of data. The use of globally unique IRIs for properties, classes, and entities makes this possible.
With that in mind, it is important that any machine (and Fluree in particular) would be able to expand any shorthand terms used in a transaction to their fully-qualified IRIs. For example, if we were to issue the transaction above, Fluree must expand schema:name
to http://schema.org/name
and ex:Person/1
to http://example.org/Person/1
before validating and committing that transaction. That is exactly what @context
allows us to do.
As a result, while @context
is a useful shorthand to simplify transactions themselves, all data will (and should be) stored according to their expanded, fully-qualified IRIs.
What does this mean in terms of actual use? In the example above, my transaction would have been expanded and committed like so:
{ "@id": "http://example.org/Person/1", "@type": "http://schema.org/Person", "http://schema.org/name": "Jane Doe"}
I could retrieve them as query results in the exact shape I transacted them, simply by using the same @context
. That is, if I queried for that data with schema
and ex
defined for http://schema.org/
and http://example.org/
, respectively, then I would see the following results:
{ "@id": "ex:Person/1", "@type": "schema:Person", "schema:name": "Jane Doe"}
However I could also retrieve that data without defining any @context
at all, and I would see the following results:
{ "@id": "http://example.org/Person/1", "@type": "http://schema.org/Person", "http://schema.org/name": "Jane Doe"}
And similarly, if I was retrieving this data for use in an application where foobar:name
was equivalent to schema:name
and where the application only understood foobar:name
, I could use @context
to return this data in a shape different from either my original transaction or the fully expanded version that I persisted:
{ "@context": { "ex": "http://example.org/", "foobar": "http://schema.org/" }, "select": { "?s": ["*"] }, "where": { "@id": "?s", "foobar:name": "Jane Doe" }}
Context-Specific JSON-LD Keywords
In the sections above, we have discussed how to use @context
to define prefix terms for fully-qualified IRIs. However, the use of @context
allows us to qualify more than simply prefixes for IRIs. We can also use several JSON-LD keywords that are specific to @context
to further shape the way that our data is interpreted and returned.
The following table gives a brief overview of those keywords and how they provide meaningful value to our data:
Keyword | Example | Description |
---|---|---|
@base | "@base": "http://example.org/" | Defines a base IRI that is used to resolve relative IRIs. |
@vocab | "@vocab": "http://schema.org/" | Defines a base IRI that is used to resolve relative IRIs, and also defines a default prefix for all properties and classes. |
@type | "ex:friend": { "@type": "@id" } | Qualifies that a particular property in the query/transaction takes a particular datatype or references a particular node by an IRI |
@container | "ex:recipeSteps": { "@container": "@list" } | Qualifies whether multi-cardinality properties should treat their values (e.g. as an ordered list of possibly-duplicate values or as an unordered set of unique values) |
@reverse | "ex:employees": { "@reverse": "ex:employedBy" } | Qualifies that a particular property in the query/transaction is a reverse property, meaning that the subject and object of the property are reversed. |
@base
In the examples above, we have seen how we can use @context
to define prefix terms for IRIs. For example, ex
for http://example.org/
allows us to use ex:Person/1
instead of http://example.org/Person/1
.
But we may want to define vocabulary terms and entity IRI identifiers without any prefix term at all. Leveraging @base
and @vocab allows us to do this.
In particular, @base
allows us to define a base IRI that is used to resolve all entity IRI identifiers. For example, if we define @base
as http://example.org/
, then we can use Person/1
instead of either http://example.org/Person/1
or ex:Person/1
.
// Query{ "@context": { "@base": "http://example.org/" }, "select": { "Person/1": ["*"] // note that this has been compacted from http://example.org/Person/1 }}// Result[ { "@id": "Person/1", // note that this has been compacted from http://example.org/Person/1 "@type": "http://schema.org/Person", "http://schema.org/name": "Jane Doe", ... }]
This is true not only for subject identifiers (e.g. @id
on a particular entity) but for object values that refer to another entity as well.
Imagine that we have a property, http://example.org/friend
, that refers to another entity. If we define @base
as http://example.org/
, then our query and our query results can simplify the IRI of that entity as well:
// Query{ "@context": { "@base": "http://example.org/" }, "select": { "?s": ["*"] }, "where": { "@id": "?s", "http://example.org/friend": { "@id": "Person/2" } }, "values": ["?s", ["Person/1"]]}// Results[ { "@id": "Person/1", "@type": "http://schema.org/Person", "http://example.org/friend": { "@id": "Person/2" }, ... }]
Note that in the example above, we have simplified the IRIs for all data entities entirely, to the point that we do not even need a prefix such as ex:Person/1
. However, although the vocabulary terms (i.e. http://example.org/friend
) still share the same root IRI that is defined for @base
, they are not compacted whatsoever.
This is because @base
is only used to resolve entity IRI identifiers, and not to compact vocabulary terms. For that, we would need to use @vocab
.
@base
and @vocab
can be used together, but they are not interchangeable. Using both is very common and provides the simplest path towards working with vanilla JSON data but enriching it with semantic meaning.
@vocab
In the examples above, we have seen how we can use @context
to define prefix terms for IRIs. For example, schema
for http://schema.org/
allows us to use schema:name
instead of http://schema.org/name
.
But we may want to define vocabulary terms and entity IRI identifiers without any prefix term at all. Leveraging @vocab
and @base allows us to do this.
In particular, @vocab
allows us to define a root IRI that is used to expand all vocabulary terms (e.g. properties and classes). For example, if we define @vocab
as http://schema.org/
, then we can use name
instead of either http://schema.org/name
or schema:name
.
// Query{ "@context": { "@vocab": "http://example.org/" }, "select": { "?s": ["*"] }, "where": { "@id": "?s", "name": "Jane Doe" }}// Result[ { "@id": "http://example.org/Person/1", "@type": "Person", // note that this has been compacted from http://schema.org/Person "name": "Jane Doe", // note that this has been compacted from http://schema.org/name, "email": "jdoe@gmail.com" // note that this has been compacted from http://schema.org/email ... }]
Note that in the example above, we have simplified the IRIs for all vocabulary terms entirely, to the point that we do not even need a prefix such as schema:name
. However, entity IRI identifiers (such as http://example.org/Person/1
) are not compacted whatsoever.
This is because @vocab
is only used to compact vocabulary terms, and not to compact entity IRI identifiers. For that, we would need to use @base
.
@vocab
and @base
can be used together, but they are not interchangeable. Using both is very common and provides the simplest path towards working with vanilla JSON data but enriching it with semantic meaning.
@type
We will often see @type
used to set the type of a node (e.g. "@type": "schema:Person"
). However, @type
can also be used to set the type of a property or to set the data type of a value for a particular property. To learn more about the data types supported by Fluree, check out our reference documentation on the subject here
We will explore this particular usage of @type
in the section below.
When we use @type
in our @context
maps, it is to express one of a two possible data scenarios:
- The value of a particular property may be a string literal, but it describes a reference to another entity IRI
{ "@context": { "ex:friend": { "@type": "@id" }, ... }, "f:insert": [ { "@id": "ex:Person/1", "ex:friend": "ex:Person/2" }, { "@id": "ex:Person/2", "ex:friend": "ex:Person/1" } ]}
- The value of a particular property may be one data type in the JSON, but it represents--and should be coerced to--a different data type (e.g. an ISO string that represents a datetime value)
{ "@context": { "ex:birthDate": { "@type": "xsd:dateTime" }, ... }, "f:insert": [ { "@id": "ex:Person/1", "ex:birthDate": "2021-10-18T14:52:17.653Z" }, { "@id": "ex:Person/2", "ex:birthDate": "2020-10-18T14:52:17.653Z" } ]}
It is important to understand that in these scenarios, @type
provides a shorthand convenience that allows our actual document JSON to avoid having to express these data types in a more verbose way. We can, however, express both of these scenarios without @context
and @type
. For example, the following is equivalent to the two scenarios above:
{ "f:insert": [ { "@id": "ex:Person/1", "ex:friend": { "@id": "ex:Person/2" }, "ex:birthDate": { "@type": "xsd:dateTime", "@value": "2021-10-18T14:52:17.653Z" } }, { "@id": "ex:Person/2", "ex:friend": { "@id": "ex:Person/1" }, "ex:birthDate": { "@type": "xsd:dateTime", "@value": "2020-10-18T14:52:17.653Z" } } ]}
We can easily imagine scenarios, however, where we would be reusing properties like ex:friend
or ex:birthDate
dozens or hundreds of times within a single transaction. Expressing the data types for those properties in a @context
map allows us to forego having to express those data types for each individual instance where those properties are used in our data payload.
For a full list of the data types that can be used with @type
and that are
supported by Fluree, see Data Types in Fluree.
@container
Unless otherwise specified, when we provide an "array" of values for a property, we are instructing Fluree to treat the multi-cardinality nature of this "array" as an unordered set (i.e. where order is not preserved and where duplicate values are dropped).
Although this is the default behavior, if we were to be explicit about this (e.g. on a property such as ex:favoriteNumbers
), we would use @container
to specify that the property should be treated as a @set
:
{ "@context": { "ex:favoriteNumbers": { "@container": "@set" }, ... }, "f:insert": [ { "@id": "ex:Person/1", "ex:favoriteNumbers": [13, 7, 99] } ]}
If it is important that a multi-cardinality property preserve order in its values and allow for duplicate values at different index positions within the described list, we can use @container: @list
to qualify that.
{ "@context": { "ex:recipeSteps": { "@container": "@list" }, ... }, "f:insert": [ { "@id": "ex:Recipe/1", "ex:recipeSteps": ["Wash Hands", "Mix Ingredients", "Wash Hands", "Cook"] } ]}
We used scare quotes around "array", because even though JSON-LD uses JSON arrays to describe values on multi-cardinality properties, this is just a JSON serialization of a set of RDF facts.
For more information on how JSON-LD represents RDF facts, see Understanding RDF in our docs, or Value Ordering in the W3C JSON-LD specification.
@reverse
In the Defining a Context in Fluree section above, we discussed how we can use @context
to introduce arbitrary property names as aliases for other properties.
This is similar to the use case for @reverse
, except in this scenario, we want to introduce an arbitrary term as a reverse property of a property that already exists. Let's explain a bit what we mean by that.
In JSON-LD / RDF, a relationship between two entities is a directed edge, which means that if ex:Person/1
relates to ex:Person/2
via a property, ex:friend
, then ex:Person/1
is the subject of that relationship and ex:Person/2
is the object of that relationship.
It is easy to query for ex:Person/1
as an entity that is friends with ex:Person/2
. That query would look something like this:
{ "select": { "?s": ["*"] }, "where": { "@id": "?s", "ex:friend": { "@id": "ex:Person/2" } }}
But because this friend relationship is a directed edge via the property ex:friend
and because that property-object relationship exists specifically on the entity, ex:Person/1
, it is not possible to query for the same relationship from ex:Person/2
without expressing a reverse property.
A reverse property, then, is simply the same relationship, but expressed in the opposite direction. In the following example, we are inventing an arbitrary property friendOf
, and declaring it in the @context
as the reverse property of ex:friend
.
We use @reverse
to accomplish this:
{ "@context": { "friendOf": { "@reverse": "ex:friend" }, ... }, "select": { "?s": ["*"] }, "where": { "@id": "?s", "friendOf": { "@id": "ex:Person/1" } }}
If the explanation above did not already make this clear, there is not
actually any property on ex:Person/2
that directly points at ex:Person/1
.
Without a reverse property definition in @context
, we would not be able to
find ex:Person/2
by querying for an ex:friend
value of ex:Person/1
because it is, in fact, ex:Person/1
who has a property of ex:friend
that
points at ex:Person/2
.