Introduction
Following from the principle that each relationship should be in only one record, there needs to be a solution that allows clients to determine that the relationship exists from the referenced record rather than the record with the relationship. For example, the object record lists who was responsible for creating it and thus the artist record does not have the list of objects created by that person. However, it is clearly important to be able to determine which objects were created by the artist. The solution to this is to have a well-defined search interaction, which could potentially be calculated in advance rather than dynamically, available to list the inverses of each relationship between records. Further, in order to not define a search query grammar, which would be difficult to implement consistently, we use the HAL link pattern to allow the client to simply retrieve the results rather than generate a query itself.
Referring Records Links
The links to the lists of referring records can be added to the _links
block, defined in the HAL specification.
The links are named in camelCase according to the current record's type, the relationship that it has to the entities in the response, and then the expected type of those entities. For example, the set of works that are about the current object would be object
plus SubjectOf
(being the inverse of about
) plus Work
. This convention helps to ensure that there will not be collisions with naming in future versions.
Retrieving the URI of the link name, generated by expanding the name with the template in the curies
section, in the browser will display documentation about that particular link. For example, see objectHasPartObject. The full set of possible links is described at the URI given in the curies
block.
If there are no other records that participate in the relationship (for example the person did not create any objects, or the object does not have any parts), then the link SHOULD NOT be present in the _links
list. This will prevent clients from making unnecessary requests to retrieve information that does not exist.
The URI given in href
has no structural requirements. It can be a search with query parameters, a direct translation of the link name and the identifier for the object, or anything else. The only requirement is that it produce the correct response format when retrieved, as defined below.
A complete HAL _links
block might thus look like:
{
"_links": {
"self": "http://example.org/linked_art/objects/1234"
"curies": [
{"name":"la","href":"https://linked.art/api/rels/1/{rel}","templated":true}
],
"la:modelVersion": {"href":"https://linked.art/model/1.0/", "name": "v1.0"},
"la:apiVersion": {"href":"https://linked.art/api/1.0/", "name": "v1.0"},
"la:localVersion": {"href":"https://example.org/extension/maps/geo-210", "name": "v2.1.0-beta-01"},
"la:objectHasPartObject": {"href": "https://example.org/api/objectHasPartObject/1234/1"},
"la:objectSubjectOfWork": {"href": "https://example.org/api/objectSubjectOfWork/1234/1"}
}
}
Search Response Format
The format described below is not new, and is used in a variety of places within the cultural heritage API ecosystem. It is based upon the Collections model from Activity Streams 2.0 and is used in specifications such as the IIIF Change Discovery API, the IIIF Content Search API, the W3C's Web Annotation Model, and beyond. It is a very straight forward paging model where the server rather than the client gets to define the page size.
This documentation will not go into great detail about the format and focus on the core functionality needed for implementation. If excruciating detail is desirable, then reading the above links will provide that experience.
The format has two main responses: the overall Collection of items or results, which consists of one or more pages that enumerate the included items. The HAL links from records will refer to the first page of the collection, rather than to the collection directly. The collection is then embedded within each page, meaning that it never has to be dereferenced separately.
Result Pages
Result pages have a very simple structure with the following properties:
@context
- This MUST be present, with the value"https://linked.art/ns/v1/search.json"
id
- This MUST be present, with the value being the URI of the pagetype
- This MUST be present, with the value being the string"OrderedCollectionPage"
partOf
- This MUST be present, with the value being a JSON object for the Collection, described in the next section.next
- This MUST be present unless it is the last page. The value is a JSON object with exactly two properties:id
being the id of the next page in the collection, andtype
with the value"OrderedCollectionPage"
.prev
- This MUST be present unless it is the first page. The value is a JSON object with exactly two properties:id
being the id of the previous page in the collection, andtype
with the value"OrderedCollectionPage"
.startIndex
- This SHOULD be present, with the value being the 0-based index of the first entry in theorderedItems
list within the overall collection.orderedItems
- This MUST be present, with the value being an array of items. Each item is a JSON object with exactly two properties:id
andtype
from the included Linked Art record.
An example page would thus be:
{
"@context": "https://linked.art/ns/v1/search.json",
"id": "https://example.org/api/objectHasPartObject/1234/2",
"type": "OrderedCollectionPage",
"partOf": { },
"next": {
"id":"https://example.org/api/objectHasPartObject/1234/3",
"type": "OrderedCollectionPage"
},
"prev": {
"id":"https://example.org/api/objectHasPartObject/1234/1",
"type": "OrderedCollectionPage"
},
"startIndex": 20,
"orderedItems": [
{
"id":"https://example.org/data/object/1234-21",
"type": "HumanMadeObject"
},
{
"id":"https://example.org/data/object/1234-22",
"type": "HumanMadeObject"
}
]
}
Collections
The Collection represents the overall search results (or other set of items). In Linked Art, it is embedded within each page, but MAY also be available separately.
Collections have the following properties:
@context
- If the collection is being requested separately then this MUST be present, with the value"https://linked.art/ns/v1/search.json"
. It MUST NOT be present when embedded.id
- This MUST be present, with the value being the URI of the collection.type
- This MUST be present, with the value being the string"OrderedCollection"
first
- This MUST be present. The value is a JSON object with exactly two properties:id
being the id of the first page of the collection, andtype
with the value"OrderedCollectionPage"
.last
- This MUST be present. The value is a JSON object with exactly two properties:id
being the id of the last page in the collection, andtype
with the value"OrderedCollectionPage"
.totalItems
- This SHOULD be present, with the value being the count of entries in the entire collection.label
- This MAY be present. If present, the value is a JSON object with the keys being the two letter code for a language, and the values being an array of strings, where each string is a name or label for the collection in that language.summary
- This MAY be present. If present, the value is a JSON object with the keys being the two letter code for a language, and the values being an array of strings, where each string is a description or summary of the collection in that language.
The collection, embedded within a page, might thus look like:
{
"@context": "https://linked.art/ns/v1/search.json",
"id": "https://example.org/api/objectHasPartObject/1234/2",
"type": "OrderedCollectionPage",
"partOf": {
"id": "https://example.org/api/objectHasPartObject/1234/",
"type": "OrderedCollection",
"first": {
"id": "https://example.org/api/objectHasPartObject/1234/1",
"type": "OrderedCollectionPage"
},
"last": {
"id": "https://example.org/api/objectHasPartObject/1234/10",
"type": "OrderedCollectionPage"
},
"totalItems": 195,
"label": {
"en": ["Parts of Manuscript 1234"]
},
"summary": {
"en": ["Manuscript 1234 has 195 pages, each described separately"]
}
}
}