Skip to main content

Curated collections

Introduction​

A curated collection is a grouping of entities. It is the main way for a presentation layer to browse and filter the entities of an integration layer. An integration layer can include any entity of any type in a curated collection, and can create any number of curated collections, nested in any way, depending on the requirements of presentation layers. An entity can be a part of any number of collections, and of no collection at all. A collection is a selection, not a type.

For example: an integration layer may have a collection for all entities of type 'Heritage object'. The integration layer may also have a 'Masterpieces' collection with its finest art-related entities, and a 'Paintings from the 17th century' collection; 'The Night Watch' can be a part of both. The integration layer may also have a 'Great for kids' collection with entities that are interesting for children. The integration layer decides how the entities are selected and put into a collection: entities may be hand-picked, derived by a query, or assembled by aggregating other collections.

An integration layer may add extra functionality to a curated collection. For example: users of a presentation layer may want to find entities in the 'Masterpieces' collection using faceted search or users may want to get suggestions to find entities in the 'Great for kids' collection using autocompletion. This specification defines two optional functionalities: facets and suggestions.

Data model​

NameDescription
CollectionAn ordered list of resources. See Resources.
Curated CollectionAn ordered list of entities or further curated collections. It may consist of pages, containing sublists of the items in the collection.
PageAn ordered sublist of the items in a curated collection.
EntityAn identifiable 'thing' relevant to heritage. See Entities.

The following class diagram visualises the data model:

Endpoint: Retrieve the root collection​

The endpoint retrieves the root collection: the curated collection that is not a part of another collection. Its items are curated collections, entities, or a mixture of both. The integration layer MUST implement this endpoint, even if the root collection is the only collection it offers: an integration layer that does not nest its collections offers its entities as the items of the root collection.

The root collection is the entry point into the API: it allows a presentation layer to identify the collections and entities the integration layer offers, and their endpoint URIs. An integration layer exposes its root at URI /{version}/collections, so that a presentation layer can assume where the tree begins. It's up to the integration layer to define what the tree holds and how the collections are nested.

HTTP request​

GET /{version}/collections

Path parameters​

NameData typeCardinalityDescription
versionstring1The version of the API. Example: v1.

Query parameters​

None.

Request body​

None.

Response body​

The response body MUST contain at least the following properties:

NameData typeCardinalityDescription
@contextstring1The URI of the JSON-LD context that maps the terms in the response to URIs.
idstring1The identifier of the collection.
typestring1The type of the collection. It MUST be CuratedCollection or a specialisation.
namestring1The name of the collection.
totalItemsnumber0 or 1The exact total number of items in the collection, being its further collections, its entities, or both. Not set if the exact total is too costly to calculate. Mutually exclusive with estimatedTotalItems.
estimatedTotalItemsnumber0 or 1An estimate of the total number of items in the collection. It can be higher or lower than the real total. Not set if there is no estimate. Mutually exclusive with totalItems.
itemsarray0 or 1A list of the items in the collection. Not set if the items are parts of pages.
items[*]CuratedCollection, Entity1A CuratedCollection or a specialisation, or a specialisation of Entity. Not set if the items are parts of pages.
firstPage0 or 1The first page in the collection. Not set if the collection is empty or if it is not divided into pages.
first.idstring1The identifier of the first page in the collection.
first.typestring1The type of the first page in the collection. It MUST be Page or a specialisation.
lastPage0 or 1The last page in the collection. Not set if the collection is empty, if the collection is not divided into pages, or if the last page is unknown (e.g. in case of cursor navigation).
last.idstring1The identifier of the last page in the collection.
last.typestring1The type of the last page in the collection. It MUST be Page or a specialisation.
capabilitiesarray0 or 1The URIs of the capabilities the integration layer implements for this collection. MUST be omitted if the collection supports no capabilities. See Capability discovery.
facetsCollection0 or 1The list of this collection's facet collections. MUST be omitted if the collection has no facet collections. See Facets.
facets.idstring1The identifier of the list.
facets.typestring1The type of the list. It MUST be Collection or a specialisation.
facets.namestring1The name of the list.
suggestionsCollection0 or 1The list of this collection's suggestion collections. MUST be omitted if the collection has no suggestion collections. See Suggestions.
suggestions.idstring1The identifier of the list.
suggestions.typestring1The type of the list. It MUST be Collection or a specialisation.
suggestions.namestring1The name of the list.

Note: the response body carries no partOf property — the root collection is not a part of another collection.

Example​

An example of the response body of the integration layer:

{
"@context": "https://example.org/v1/context.json",
"id": "https://example.org/v1/collections",
"type": "CuratedCollection",
"name": "Collections",
"totalItems": 3,
"items": [
{
"id": "https://example.org/v1/collections/objects",
"type": "CuratedCollection",
"name": "Heritage objects"
},
{
"id": "https://example.org/v1/collections/masterpieces",
"type": "CuratedCollection",
"name": "Masterpieces"
},
{
"id": "https://example.org/v1/collections/persons",
"type": "CuratedCollection",
"name": "Persons"
}
]
}

The response indicates that the integration layer has three collections that are a part of the root collection. The root collection groups collections, not entities — it supports no capabilities and omits the capabilities property.

A collection can hold further collections. Example response for the 'Persons' collection:

{
"@context": "https://example.org/v1/context.json",
"id": "https://example.org/v1/collections/persons",
"type": "CuratedCollection",
"name": "Persons",
"totalItems": 2,
"items": [
{
"id": "https://example.org/v1/collections/persons/painters",
"type": "CuratedCollection",
"name": "Painters"
},
{
"id": "https://example.org/v1/collections/persons/writers",
"type": "CuratedCollection",
"name": "Writers"
}
],
"partOf": {
"id": "https://example.org/v1/collections",
"type": "CuratedCollection",
"name": "Collections"
}
}

The response indicates that the 'Persons' collection groups two collections: one for 'Painters' and one for 'Writers'. The collection itself is a part of a parent collection, 'Collections'. The collection groups collections rather than entities — it supports no capabilities and omits the capabilities property.

Endpoint: Retrieve a collection​

The endpoint retrieves a curated collection. The integration layer MAY implement this endpoint, for the collections it chooses to expose.

HTTP request​

GET /{version}/collections(/{...collections})/{collection}

Path parameters​

NameData typeCardinalityDescription
versionstring1The version of the API. Example: v1.
...collectionsstring0 or moreThe path identifier(s) of the collection(s) the collection is part of. Example: persons.
collectionstring1The path identifier of the collection. Example: masterpieces, objects.

Query parameters​

NameData typeCardinalityDescription
qstring0 or 1A keyword query for filtering the items. Minimum length: defined by the integration layer (e.g. 1 character). Maximum length: defined by the integration layer (e.g. 100 characters).
sizenumber0 or 1The maximum number of items to retrieve. Minimum: 1. Default: 10. Maximum: defined by the integration layer (e.g. 100).
orderBystring0 or 1The sorting order of the items. One of relevance, value. Default: relevance:desc (most relevant entity first) if q is set; otherwise the order defined by the integration layer. The integration layer defines which property it sorts by value on (e.g. the name of the item).
filterstring0 or moreThe rules for filtering the items. See Filters.

Request body​

None.

Response body​

The response body MUST contain at least the following properties:

NameData typeCardinalityDescription
@contextstring1The URI of the JSON-LD context that maps the terms in the response to URIs.
idstring1The identifier of the collection.
typestring1The type of the collection. It MUST be CuratedCollection or a specialisation.
namestring1The name of the collection.
totalItemsnumber0 or 1The exact total number of items in the collection, being its further collections, its entities, or both. Not set if the exact total is too costly to calculate. Mutually exclusive with estimatedTotalItems.
estimatedTotalItemsnumber0 or 1An estimate of the total number of items in the collection. It can be higher or lower than the real total. Not set if there is no estimate. Mutually exclusive with totalItems.
itemsarray0 or 1A list of the items in the collection. Not set if the items are parts of pages.
items[*]CuratedCollection, Entity1A CuratedCollection or a specialisation, or a specialisation of Entity. Not set if the items are parts of pages.
firstPage0 or 1The first page in the collection. Not set if the collection is empty or if it is not divided into pages.
first.idstring1The identifier of the first page in the collection.
first.typestring1The type of the first page in the collection. It MUST be Page or a specialisation.
lastPage0 or 1The last page in the collection. Not set if the collection is empty, if the collection is not divided into pages, or if the last page is unknown (e.g. in case of cursor navigation).
last.idstring1The identifier of the last page in the collection.
last.typestring1The type of the last page in the collection. It MUST be Page or a specialisation.
partOfCuratedCollection0 or 1The collection of which this collection is a part. Not set if this collection is the root collection.
partOf.idstring1The identifier of the collection.
partOf.typestring1The type of the collection. It MUST be CuratedCollection or a specialisation.
partOf.namestring1The name of the collection.
capabilitiesarray0 or 1The URIs of the capabilities the integration layer implements for this collection. MUST be omitted if the collection supports no capabilities. See Capability discovery.
facetsCollection0 or 1The list of this collection's facet collections. MUST be omitted if the collection has no facet collections. See Facets.
facets.idstring1The identifier of the list.
facets.typestring1The type of the list. It MUST be Collection or a specialisation.
facets.namestring1The name of the list.
suggestionsCollection0 or 1The list of this collection's suggestion collections. MUST be omitted if the collection has no suggestion collections. See Suggestions.
suggestions.idstring1The identifier of the list.
suggestions.typestring1The type of the list. It MUST be Collection or a specialisation.
suggestions.namestring1The name of the list.

Example​

An example of the response body of a collection:

{
"@context": "https://example.org/v1/context.json",
"id": "https://example.org/v1/collections/objects",
"type": "CuratedCollection",
"name": "Heritage objects",
"totalItems": 195,
"first": {
"id": "https://example.org/v1/collections/objects?page=1",
"type": "Page"
},
"last": {
"id": "https://example.org/v1/collections/objects?page=20",
"type": "Page"
},
"partOf": {
"id": "https://example.org/v1/collections",
"type": "CuratedCollection",
"name": "Collections"
},
"capabilities": [
"https://specs.nde.nl/rest/v1/page-pagination",
"https://specs.nde.nl/rest/v1/keyword-search"
]
}

The response indicates that this collection contains 195 entities, spread over 20 pages, and that it supports keyword search for it.

Another example of the response body of a collection, with extended capabilities:

{
"@context": "https://example.org/v1/context.json",
"id": "https://example.org/v1/collections/masterpieces",
"type": "CuratedCollection",
"name": "Masterpieces",
"totalItems": 195,
"first": {
"id": "https://example.org/v1/collections/masterpieces?page=1",
"type": "Page"
},
"last": {
"id": "https://example.org/v1/collections/masterpieces?page=20",
"type": "Page"
},
"partOf": {
"id": "https://example.org/v1/collections",
"type": "CuratedCollection",
"name": "Collections"
},
"capabilities": [
"https://specs.nde.nl/rest/v1/page-pagination",
"https://specs.nde.nl/rest/v1/keyword-search",
"https://specs.nde.nl/rest/v1/facets",
"https://specs.nde.nl/rest/v1/suggestions"
],
"facets": {
"id": "https://example.org/v1/collections/masterpieces/facets",
"type": "Collection",
"name": "Facets"
},
"suggestions": {
"id": "https://example.org/v1/collections/masterpieces/suggestions",
"type": "Collection",
"name": "Suggestions"
}
}

The response indicates that this collection supports keyword search, facets and suggestions.

Endpoint: Retrieve a page in a collection​

The endpoint retrieves a page in a curated collection. The integration layer MUST implement this endpoint for every collection it divides into pages.

HTTP request​

GET /{version}/collections(/{...collections})/{collection}?page={page}

Path parameters​

NameData typeCardinalityDescription
versionstring1The version of the API. Example: v1.
...collectionsstring0 or moreThe path identifier(s) of the collection(s) the collection is part of. Example: persons.
collectionstring1The path identifier of the collection. Example: masterpieces, objects.

Query parameters​

NameData typeCardinalityDescription
pagestring1The identifier of the page: a page number or cursor, depending on the pagination strategy of the integration layer.
qstring0 or 1A keyword query for filtering the items. Minimum length: defined by the integration layer (e.g. 1 character). Maximum length: defined by the integration layer (e.g. 100 characters).
sizenumber0 or 1The maximum number of items to retrieve. Minimum: 1. Default: 10. Maximum: defined by the integration layer (e.g. 100).
orderBystring0 or 1The sorting order of the items. One of relevance, value. Default: relevance:desc (most relevant entity first) if q is set; otherwise the order defined by the integration layer. The integration layer defines which property it sorts by value on (e.g. the name of the item).
filterstring0 or moreThe rules for filtering the items. See Filters.
facetstring0 or moreThe facets that must be retrieved. MUST be ignored by the integration layer if it does not support facets. See facets.

Request body​

None.

Response body​

The response body MUST contain at least the following properties:

NameData typeCardinalityDescription
@contextstring1The URI of the JSON-LD context that maps the terms in the response to URIs.
idstring1The identifier of the current page.
typestring1The type of the page. It MUST be Page or a specialisation.
namestring1The name of the page.
itemsarray1A list of the items in the collection that fall on this page.
items[*]CuratedCollection, Entity1A CuratedCollection or a specialisation, or a specialisation of Entity. If an Entity: all properties MUST be embedded — see the response body of endpoint Retrieve an entity.
facetsarray0 or 1The facet results requested via the facet query parameters. MUST be omitted if no facets were requested. See Facets on a page.
facets[*]FacetCollection, FacetPage1The result of one requested facet. It is a FacetPage if the facet collection is divided into pages, and a FacetCollection if it is not. The result is the response body of endpoint Retrieve a facet collection or Retrieve a page in a facet collection.
prevPage0 or 1The previous page in the collection. Not set if there is no previous page.
prev.idstring1The identifier of the previous page in the collection.
prev.typestring1The type of the previous page in the collection. It MUST be Page or a specialisation.
nextPage0 or 1The next page in the collection. Not set if there is no next page.
next.idstring1The identifier of the next page in the collection.
next.typestring1The type of the next page in the collection. It MUST be Page or a specialisation.
partOfCuratedCollection1The collection to which the items contained by the page belong.
partOf.idstring1The identifier of the collection.
partOf.typestring1The type of the collection. It MUST be CuratedCollection or a specialisation.
partOf.namestring1The name of the collection.
partOf.totalItemsnumber0 or 1The exact total number of items in the collection, being its further collections, its resources, or both. Not set if the exact total is too costly to calculate. Mutually exclusive with partOf.estimatedTotalItems.
partOf.estimatedTotalItemsnumber0 or 1An estimate of the total number of items in the collection. It can be higher or lower than the real total. Not set if there is no estimate. Mutually exclusive with partOf.totalItems.
partOf.firstPage1The first page in the collection.
partOf.first.idstring1The identifier of the first page in the collection.
partOf.first.typestring1The type of the first page in the collection. It MUST be Page or a specialisation.
partOf.lastPage0 or 1The last page in the collection. Not set if the last page is unknown (e.g. in case of cursor navigation).
partOf.last.idstring1The identifier of the last page in the collection.
partOf.last.typestring1The type of the last page in the collection. It MUST be Page or a specialisation.

Example​

An example of the response body:

{
"@context": "https://example.org/v1/context.json",
"id": "https://example.org/v1/collections/masterpieces?page=3",
"type": "Page",
"name": "Masterpieces: page 3",
"items": [
{
"id": "https://example.org/v1/entities/1234",
"type": "HeritageObject",
"name": "The Night Watch"
// Other properties...
},
{
"id": "https://example.org/v1/entities/5678",
"type": "HeritageObject",
"name": "Ford V8 Cabriolet"
// Other properties...
}
// Other items...
],
"prev": {
"id": "https://example.org/v1/collections/masterpieces?page=2",
"type": "Page"
},
"next": {
"id": "https://example.org/v1/collections/masterpieces?page=4",
"type": "Page"
},
"partOf": {
"id": "https://example.org/v1/collections/masterpieces",
"type": "CuratedCollection",
"name": "Masterpieces",
"totalItems": 195,
"first": {
"id": "https://example.org/v1/collections/masterpieces?page=1",
"type": "Page"
},
"last": {
"id": "https://example.org/v1/collections/masterpieces?page=20",
"type": "Page"
}
}
}

Facets on a page​

A page can carry the results of the facets that the presentation layer requested in a dedicated facets property.

An entry in the facets property mirrors the facet collection it belongs to. If that facet collection is divided into pages, the entry is a FacetPage. If it is not, the entry is the FacetCollection itself, which lists its facet items inline. A presentation layer therefore has to accept both shapes. It distinguishes them by the type of the entry.

Both shapes are allowed in one facets property. The API of an integration layer is easier to use, however, when all of its facet collections have the same shape. An integration layer SHOULD therefore either divide all of its facet collections into pages, or divide none of them. Then every entry in facets has the same type, and a presentation layer can read all of them in one way.

The results in facets belong to the whole collection, not to one page. They depend on the query in the request, such as q and filter, and not on page. Every page of the same collection for the same query therefore carries the same facet results; only the items change when a presentation layer moves to the next page. A presentation layer can request the facets on the first page and reuse the results on the following pages.

A page response has two kinds of next and prev links. The links of the page itself move within the collection. The links inside an entry of facets move within the results of that facet. They are not the same, and a presentation layer must not confuse them.

The next two examples show the two shapes separately. Both are allowed, and an integration layer MAY return either one.

Facets that are not divided into pages​

An example of a response body in which the presentation layer requested two facet collections that are not divided into pages. Both entries in facets are a FacetCollection, which lists its items inline. A presentation layer reads every entry in the same way. An entry is the complete response body that the endpoint Retrieve a facet collection returns. It has no first and no last, because a collection that is not divided into pages has no pages.

{
"@context": "https://example.org/v1/context.json",
"id": "https://example.org/v1/collections/masterpieces?page=3",
"type": "Page",
"name": "Masterpieces: page 3",
"items": [
// Omitted for brevity; see the example response above
],
"facets": [
{
"id": "https://example.org/v1/collections/masterpieces/facets/centuries?orderBy=count:desc&context=...",
"type": "FacetCollection",
"name": "Made in century",
"totalItems": 4,
"items": [
{
"type": "FacetItem",
"count": 8,
"value": {
"id": "https://example.org/v1/entities/3456",
"type": "Concept",
"name": "18th century"
}
},
{
"type": "FacetItem",
"count": 5,
"value": {
"id": "https://example.org/v1/entities/3457",
"type": "Concept",
"name": "17th century"
}
},
{
"type": "FacetItem",
"count": 3,
"value": {
"id": "https://example.org/v1/entities/3458",
"type": "Concept",
"name": "19th century"
}
},
{
"type": "FacetItem",
"count": 1,
"value": {
"id": "https://example.org/v1/entities/3459",
"type": "Concept",
"name": "16th century"
}
}
],
"partOf": {
"id": "https://example.org/v1/collections/masterpieces/facets",
"type": "Collection",
"name": "Facets"
}
},
{
"id": "https://example.org/v1/collections/masterpieces/facets/subjects?orderBy=count:desc&context=...",
"type": "FacetCollection",
"name": "Subject",
"totalItems": 3,
"items": [
{
"type": "FacetItem",
"count": 8,
"value": {
"id": "https://example.org/v1/entities/4567",
"type": "Concept",
"name": "Religious art"
}
},
{
"type": "FacetItem",
"count": 3,
"value": {
"id": "https://example.org/v1/entities/4568",
"type": "Concept",
"name": "Portraits"
}
},
{
"type": "FacetItem",
"count": 1,
"value": {
"id": "https://example.org/v1/entities/4569",
"type": "Concept",
"name": "Prints"
}
}
],
"partOf": {
"id": "https://example.org/v1/collections/masterpieces/facets",
"type": "Collection",
"name": "Facets"
}
}
],
"prev": {
"id": "https://example.org/v1/collections/masterpieces?page=2",
"type": "Page"
},
"next": {
"id": "https://example.org/v1/collections/masterpieces?page=4",
"type": "Page"
},
"partOf": {
"id": "https://example.org/v1/collections/masterpieces",
"type": "CuratedCollection",
"name": "Masterpieces",
"totalItems": 195,
"first": {
"id": "https://example.org/v1/collections/masterpieces?page=1",
"type": "Page"
},
"last": {
"id": "https://example.org/v1/collections/masterpieces?page=20",
"type": "Page"
}
}
}

Facets that are divided into pages​

An example of a response body in which the presentation layer requested two facets collections that are divided into pages. Both entries in facets are a FacetPage. A presentation layer reads every entry in the same way, and follows the next property of an entry to see more values of that facet.

An entry is the complete response body of a FacetPage, the same body that the endpoint Retrieve a page in a facet collection returns, including prev and next. The entries below have no prev, because they are the first page of their facet collection. Their partOf carries totalItems, first and last, so a presentation layer that wants a different page of that facet follows one of those.

{
"@context": "https://example.org/v1/context.json",
"id": "https://example.org/v1/collections/objects?page=2",
"type": "Page",
"name": "Objects: page 2",
"items": [
// Omitted for brevity; see the example response above
],
"facets": [
{
"id": "https://example.org/v1/collections/objects/facets/creators?page=1&size=3&orderBy=count:desc&context=...",
"type": "FacetPage",
"name": "Creator: page 1",
"items": [
{
"type": "FacetItem",
"count": 12,
"value": {
"id": "https://example.org/v1/entities/7890",
"type": "Person",
"name": "Arno Haag"
}
},
{
"type": "FacetItem",
"count": 8,
"value": {
"id": "https://example.org/v1/entities/7891",
"type": "Person",
"name": "Hans de Haan"
}
},
{
"type": "FacetItem",
"count": 2,
"value": {
"id": "https://example.org/v1/entities/7892",
"type": "Person",
"name": "John Jansen"
}
}
],
"next": {
"id": "https://example.org/v1/collections/objects/facets/creators?page=2&size=3&orderBy=count:desc&context=...",
"type": "FacetPage"
},
"partOf": {
"id": "https://example.org/v1/collections/objects/facets/creators",
"type": "FacetCollection",
"name": "Creator",
"totalItems": 195,
"first": {
"id": "https://example.org/v1/collections/objects/facets/creators?page=1&size=3&orderBy=count:desc&context=...",
"type": "FacetPage"
},
"last": {
"id": "https://example.org/v1/collections/objects/facets/creators?page=65&size=3&orderBy=count:desc&context=...",
"type": "FacetPage"
}
}
},
{
"id": "https://example.org/v1/collections/objects/facets/places?page=1&size=3&orderBy=count:desc&context=...",
"type": "FacetPage",
"name": "Made in place: page 1",
"items": [
{
"type": "FacetItem",
"count": 9,
"value": {
"id": "https://example.org/v1/entities/3450",
"type": "Place",
"name": "Amsterdam"
}
},
{
"type": "FacetItem",
"count": 6,
"value": {
"id": "https://example.org/v1/entities/3451",
"type": "Place",
"name": "The Hague"
}
},
{
"type": "FacetItem",
"count": 1,
"value": {
"id": "https://example.org/v1/entities/3452",
"type": "Place",
"name": "Delft"
}
}
],
"next": {
"id": "https://example.org/v1/collections/objects/facets/places?page=2&size=3&orderBy=count:desc&context=...",
"type": "FacetPage"
},
"partOf": {
"id": "https://example.org/v1/collections/objects/facets/places",
"type": "FacetCollection",
"name": "Made in place",
"totalItems": 42,
"first": {
"id": "https://example.org/v1/collections/objects/facets/places?page=1&size=3&orderBy=count:desc&context=...",
"type": "FacetPage"
},
"last": {
"id": "https://example.org/v1/collections/objects/facets/places?page=14&size=3&orderBy=count:desc&context=...",
"type": "FacetPage"
}
}
}
],
"prev": {
"id": "https://example.org/v1/collections/objects?page=1",
"type": "Page"
},
"next": {
"id": "https://example.org/v1/collections/objects?page=3",
"type": "Page"
},
"partOf": {
"id": "https://example.org/v1/collections/objects",
"type": "CuratedCollection",
"name": "Objects",
"totalItems": 120,
"first": {
"id": "https://example.org/v1/collections/objects?page=1",
"type": "Page"
},
"last": {
"id": "https://example.org/v1/collections/objects?page=12",
"type": "Page"
}
}
}