Similar
Info
Returns the objects whose photographs sit nearest to a given object's in embedding space. Nothing here was catalogued: the grouping falls out of the images themselves.
endpoint:
GET https://data.designmuseumgent.be/v2/id/object/{PID}/similar
GET https://data.designmuseumgent.be/v2/id/object/1975-0061/similar
GET https://data.designmuseumgent.be/v2/id/object/1975-0061/similar?limit=24
GET https://data.designmuseumgent.be/v2/id/object/1975-0061/similar?minSimilarity=0.85
Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
limit | integer | 12 | Number of objects to return. Clamped to 1–50 |
minSimilarity | float | 0 | Only return objects at or above this similarity. 0 returns everything ranked |
What this measures
Each photograph is encoded as a 768-dimension vector by CLIP (ViT-L-14-openai), a model trained on images paired with the text found near them online. Images described in similar ways end up near each other.
The model has never seen this collection, was never told what a decorative tile is, and has no access to any catalogue record. It compares pictures.
That it works is easiest to see by example. The twelve nearest objects to 1975-0061, a tile painted with a sea creature, are all decorative tiles and almost all depict sea creatures — across different designs, workshops and centuries, at similarities of 0.92 to 0.95:
| Object | Title | Similarity |
|---|---|---|
1975-0069 | Decortegel met hippocampus | 0.9475 |
1740 | Decortegel met zeewezen | 0.9428 |
1693 | Decortegel met zeewezen | 0.9404 |
1751 | Decortegel met gevleugelde zeemeermin | 0.9344 |
1980-0085 | Decortegel met vis | 0.9342 |
No cataloguer ever recorded "sea creature" as a category.
What it cannot know
CLIP carries the associations of the corpus it was trained on, including its blind spots and its clichés. It will group objects for reasons that have nothing to do with design history — a shared backdrop, a similar crop, a photographic convention. Treat similarity as a measurement of two photographs, not as a judgement about two objects.
Coverage
7,307 of the 10,238 published objects carry an image embedding. Roughly three in ten have none, and for those this endpoint returns an empty collection.
An empty result is not an error
hydra:member: [] means this object has no embedding — not that it has no similar objects. The response is 200, because the object exists and the question is simply unanswerable for it. Clients should handle the empty case rather than assuming every object has neighbours.
Multiple views per object
The embedding table holds 12,325 vectors for 7,307 objects: an object photographed from six angles has six vectors.
Ranking naively would let one object's six views fill the whole result, so each candidate object is reduced to its single closest view before ranking. Each object therefore appears at most once, and its similarity is that of its best-matching photograph.
The query object is anchored on its first image, deterministically, so repeated requests return the same neighbours.
Response fields
| Field | Description |
|---|---|
hydra:totalItems | Number of objects returned — not a total available count |
hydra:member | Objects, nearest first |
Each member:
| Field | Description |
|---|---|
@id | URI of the object — resolvable to the full record |
@type | Always crm:E22_Human-Made_Object |
rdfs:label | Dutch title |
similarity | Cosine similarity of the two closest photographs, 0–1 |
crm:P138i_has_representation | Validated IIIF images, as elsewhere in the API |
image | Convenience field repeating the first image |
crm:P129i_is_subject_of | IIIF manifest, when available |
Reading similarity
Cosine similarity between unit vectors. Useful for ranking and for judging how tight a set is — not a probability and not a percentage of anything. In practice close matches sit around 0.92 and unrelated objects around 0.60, but those figures are characteristic of this collection rather than absolute.
Example response
{
"@context": {
"crm": "http://www.cidoc-crm.org/cidoc-crm/",
"rdfs": "http://www.w3.org/2000/01/rdf-schema#",
"hydra": "http://www.w3.org/ns/hydra/core#"
},
"@id": "https://data.designmuseumgent.be/v2/id/object/1975-0061/similar",
"@type": "hydra:Collection",
"rdfs:label": "Objects visually similar to 1975-0061",
"hydra:totalItems": 12,
"hydra:member": [
{
"@id": "https://data.designmuseumgent.be/v2/id/object/1975-0069",
"@type": "crm:E22_Human-Made_Object",
"rdfs:label": "Decortegel met hippocampus",
"similarity": 0.9475,
"image": {
"@id": "https://beeldbank-temp.stad.gent/iiif/image/8025/full/max/0/default.jpg",
"@type": "crm:E38_Image",
"thumbnail": "https://beeldbank-temp.stad.gent/iiif/image/8025/full/thm/0/default.jpg"
}
}
]
}
Building a thread
Because each neighbour is itself an object, the endpoint composes: take any result and ask for its neighbours.
// Walk outward from a starting object, following the closest match each time.
async function thread(pid, steps = 5) {
const path = [pid]
for (let i = 0; i < steps; i++) {
const res = await fetch(
`https://data.designmuseumgent.be/v2/id/object/${pid}/similar?limit=5`,
{ headers: { Accept: 'application/ld+json' } }
)
const data = await res.json()
const members = data['hydra:member']
// No embedding, or nowhere left to go.
if (!members.length) break
// Skip anything already visited, or the walk oscillates between a
// mutually-nearest pair forever.
const next = members.find(m => !path.includes(m['@id'].split('/').pop()))
if (!next) break
pid = next['@id'].split('/').pop()
path.push(pid)
}
return path
}
Mutual nearest neighbours
Two objects are very often each other's closest match, so a naive walk gets stuck bouncing between them immediately. Track what you have visited, as above.