Zum Hauptinhalt springen

Ähnlichkeitssuche

Überblick

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​

onWas verglichen wirdModell
metadata (Standard)Worum es bei einem Datensatz geht — Titel, Beschreibung, Urheber, Schlagwörter, Material, Ort, Zeitmultilingual-e5
imageWas das Bild zeigtCLIP

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
ParameterTypStandardBeschreibung
idstring–Pflicht. Kulturpool-ID des Ausgangsobjekts
onstringmetadatametadata oder image
per_pageinteger20Treffer (1–250)
filter_bystring–Filterausdruck wie bei der Suche
thresholdfloat–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.

FeldTypStandardBeschreibung
imageDatei–Pflicht. Die Bilddatei (multipart/form-data)
per_pageinteger20Treffer (1–250)
filter_bystring–Filterausdruck
thresholdfloat–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​

FormateJPEG, PNG, WebP, GIF, BMP, TIFF, AVIF
Größehöchstens 8 MB
Speicherungkeine — 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.

ParameterTypStandardBeschreibung
vectorstring–Pflicht. Das Feld vector aus einer Antwort (1366 Zeichen)
per_pageinteger20Treffer (1–250)
filter_bystring–Filterausdruck
thresholdfloat–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 vector trä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.

Eine Vektorsuche liefert nie ein leeres Ergebnis

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:

AnfrageFeldDistanz zum besten Treffer
„Zeichnung einer Kirche“metadata≈ 0,12
„a drawing of a church“image≈ 0,65
Ein hochgeladenes Bildimage≈ 0,65 bei guter Übereinstimmung
Dasselbe Bild, das im Index liegtimage≈ 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​

CodeBedeutung
400Der Datensatz hat keinen Vektor in diesem Feld, oder die Datei ist kein lesbares Bild
413Die Datei überschreitet 8 MB
415Der Dateityp ist kein unterstütztes Bildformat
422id fehlt oder ist ungültig, on ist unbekannt, vector ist kein gültiger Token
429Anfragegrenze erreicht — siehe Retry-After
503Der 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.