Ähnlichkeitssuche
Der Index trägt zu jedem Digitalisat zwei Vektoren: einen über die beschreibenden Metadaten und einen über das Bild. /v2/similar findet damit Objekte, die einem anderen Objekt ähneln — /v2/similar/image solche, die einem mitgeschickten Bild ähneln. Der Vektor des Bildes kommt mit der Antwort zurück, damit sich die Suche ohne erneuten Upload wiederholen lässt — auch aus einem Link heraus.
Die zwei Vektoren
on | Was verglichen wird | Modell |
|---|---|---|
metadata (Standard) | Worum es bei einem Datensatz geht — Titel, Beschreibung, Urheber, Schlagwörter, Material, Ort, Zeit | multilingual-e5 |
image | Was das Bild zeigt | CLIP |
Die beiden lassen sich nicht mischen: sie haben unterschiedliche Dimensionen und leben in verschiedenen Räumen. Eine Anfrage nennt genau einen.
Ähnlich zu einem Objekt
GET https://api.kulturpool.at/v2/similar
| Parameter | Typ | Standard | Beschreibung |
|---|---|---|---|
id | string | – | Pflicht. Kulturpool-ID des Ausgangsobjekts |
on | string | metadata | metadata oder image |
per_page | integer | 20 | Treffer (1–250) |
filter_by | string | – | Filterausdruck wie bei der Suche |
threshold | float | – | Maximale Distanz |
curl "https://api.kulturpool.at/v2/similar?id=5a86fa63-198a-4468-b6e5-6f7d71d30c2a&on=image&per_page=12"
Das Ausgangsobjekt selbst ist im Ergebnis nicht enthalten.
Ähnlich zu einem Bild
POST https://api.kulturpool.at/v2/similar/image
Ein Upload, eine Antwort. Das Bild wird serverseitig durch CLIP geschickt und der Vektor direkt gegen den Index gesucht. Die Antwort trägt neben den Treffern das Feld vector — den Vektor des Bildes, mit dem sich die Suche wiederholen lässt.
| Feld | Typ | Standard | Beschreibung |
|---|---|---|---|
image | Datei | – | Pflicht. Die Bilddatei (multipart/form-data) |
per_page | integer | 20 | Treffer (1–250) |
filter_by | string | – | Filterausdruck |
threshold | float | – | Maximale Distanz |
curl https://api.kulturpool.at/v2/similar/image \
-F image=@kirche.jpg \
-F per_page=24
const form = new FormData();
form.append('image', file);
form.append('per_page', '24');
const response = await fetch(
'https://api.kulturpool.at/v2/similar/image',
{method: 'POST', body: form},
);
const {hits, vector} = await response.json();
import requests
with open("kirche.jpg", "rb") as fh:
response = requests.post(
"https://api.kulturpool.at/v2/similar/image",
files={"image": fh},
data={"per_page": 24},
)
payload = response.json()
for hit in payload["hits"]:
print(round(hit["vector_distance"], 3), hit["document"]["title"])
vector = payload["vector"] # für die nächste Anfrage, siehe unten
Grenzen des Uploads
| Formate | JPEG, PNG, WebP, GIF, BMP, TIFF, AVIF |
| Größe | höchstens 8 MB |
| Speicherung | keine — das Bild wird gelesen, eingebettet und verworfen; zurück kommt nur sein Vektor |
Große Bilder werden serverseitig schrittweise verkleinert. Sie können trotzdem vor dem Hochladen verkleinern: das spart Übertragungszeit und ändert am Ergebnis nichts Wesentliches.
Mit dem Vektor weitersuchen
GET https://api.kulturpool.at/v2/similar/image
Jede Antwort auf einen Upload enthält im Feld vector den Vektor des Bildes. Damit lässt sich dieselbe Suche beliebig oft wiederholen — mit anderem Filter, anderer Seitengröße, oder aus einem Link heraus — ohne das Bild noch einmal zu schicken.
| Parameter | Typ | Standard | Beschreibung |
|---|---|---|---|
vector | string | – | Pflicht. Das Feld vector aus einer Antwort (1366 Zeichen) |
per_page | integer | 20 | Treffer (1–250) |
filter_by | string | – | Filterausdruck |
threshold | float | – | Maximale Distanz |
VECTOR=$(curl -s https://api.kulturpool.at/v2/similar/image -F image=@kirche.jpg | jq -r .vector)
curl "https://api.kulturpool.at/v2/similar/image?vector=$VECTOR&per_page=24&filter_by=dataProvider:=Albertina"
// Einmal hochladen …
const upload = await fetch('https://api.kulturpool.at/v2/similar/image', {
method: 'POST',
body: form,
});
const {vector} = await upload.json();
// … dann so oft wie nötig mit dem Vektor weitersuchen
const params = new URLSearchParams({
vector,
per_page: '24',
filter_by: 'dataProvider:=Albertina',
});
const repeat = await fetch(`https://api.kulturpool.at/v2/similar/image?${params}`);
const {hits} = await repeat.json();
repeat = requests.get(
"https://api.kulturpool.at/v2/similar/image",
params={"vector": vector, "per_page": 24, "filter_by": "dataProvider:=Albertina"},
)
Was dabei gilt:
- Das Ergebnis ist identisch mit dem des Uploads: schon die Suche beim Upload verwendet genau den Vektor, den
vectorträgt. - Kein Upload, keine Einbettung, keine Anfragegrenze. Die Wiederholung kostet nicht mehr als
/v2/similar. - Der Vektor ist alles, was den Server verlässt — kein Bild, kein Vorschaubild, keine Kennung. Nichts wird gespeichert; der Zustand liegt allein beim Aufrufer, und ein Link kann ihn tragen.
- Der Wert ist als undurchsichtig zu behandeln. Er kodiert die 512 Werte des CLIP-Vektors als float16 (little-endian, base64url ohne Padding, 1366 Zeichen) und gilt nur für das Modell dieser API. Ein anderer String führt zu
422.
Wer den Vektor selbst weiterverwenden will — etwa in einer hybriden Suche über vector_query auf /v2/search — dekodiert ihn entsprechend.
Die Antwort lesen
Das Format entspricht dem der Suche. Zusätzlich trägt jeder Treffer ein Feld vector_distance — je kleiner, desto ähnlicher.
{
"found": 250,
"hits": [
{
"document": {
"uuid": "…",
"title": "Skizze",
"dataProvider": "Albertina",
"previewImage": "https://media.kulturpool.at/images/…_small.webp"
},
"vector_distance": 0.2264
}
],
"vector": "…"
}
vector trägt nur /v2/similar/image — nach einem Upload wie nach einer Wiederholung per GET.
Sie sortiert die gesamte Sammlung nach Ähnlichkeit. Es gibt immer einen nächsten Nachbarn, auch wenn er nichts mit der Anfrage zu tun hat — die letzten Treffer einer Seite sind die am wenigsten schlechten, nicht die guten.
Nur threshold macht „keine gute Übereinstimmung“ zu einer möglichen Antwort.
Größenordnungen
Die beiden Felder liegen auf sehr verschiedenen Skalen. Ein Wert, der beim einen streng ist, verwirft beim anderen alles:
| Anfrage | Feld | Distanz zum besten Treffer |
|---|---|---|
| „Zeichnung einer Kirche“ | metadata | ≈ 0,12 |
| „a drawing of a church“ | image | ≈ 0,65 |
| Ein hochgeladenes Bild | image | ≈ 0,65 bei guter Übereinstimmung |
| Dasselbe Bild, das im Index liegt | image | ≈ 0,000 |
Lesen Sie erst die Distanzen einer Antwort, dann setzen Sie threshold. Als Ausgangspunkt: 0,15 für metadata, 0,75 für image.
CLIP legt Text und Bilder in denselben Raum, aber ein Foto einer Kirche und eine Zeichnung einer Kirche liegen darin weiter auseinander als zwei Zeichnungen.
Textsuche gegen die Vektoren
Für eine rein textliche semantische Suche braucht es keinen eigenen Endpunkt: query_by auf der Suche akzeptiert die Vektorfelder direkt. Die Suchmaschine bettet den Suchbegriff dann mit dem Modell des jeweiligen Feldes ein.
# Semantisch statt wörtlich
curl "https://api.kulturpool.at/v2/search?q=alpine+H%C3%BCtte&query_by=metadataEmbedding"
# Bilder, die zu einer Beschreibung passen
curl "https://api.kulturpool.at/v2/search?q=a+drawing+of+a+church&query_by=imageEmbedding"
Anfragegrenzen
POST /v2/similar/image ist der einzige Endpunkt dieser API, der nennenswert Rechenzeit kostet, und der einzige mit einer Anfragegrenze: 10 Anfragen pro Minute je Client-Adresse, kurzfristig bis zu 4 am Stück. Die Wiederholung per GET mit vector rechnet nichts und ist nicht begrenzt.
Über der Grenze antwortet die API mit 429 und einem Retry-After-Header. Sind gerade zu viele Einbettungen gleichzeitig unterwegs, kommt 503 mit Retry-After — die Anfrage wird nicht unbegrenzt in eine Warteschlange gestellt.
Eine abgelehnte Anfrage verbraucht kein Kontingent: ein Wiederholungsversuch schiebt den nächsten Erfolg nicht weiter hinaus.
Fehlerbehandlung
| Code | Bedeutung |
|---|---|
400 | Der Datensatz hat keinen Vektor in diesem Feld, oder die Datei ist kein lesbares Bild |
413 | Die Datei überschreitet 8 MB |
415 | Der Dateityp ist kein unterstütztes Bildformat |
422 | id fehlt oder ist ungültig, on ist unbekannt, vector ist kein gültiger Token |
429 | Anfragegrenze erreicht — siehe Retry-After |
503 | Der Dienst ist ausgelastet oder die Bildsuche steht nicht zur Verfügung |
Ein Datensatz ohne brauchbares Bild hat kein imageEmbedding. on=image führt dann zu einem 400 mit der Meldung der Suchmaschine — nutzen Sie in dem Fall on=metadata.