Warum ich ein kontrolliertes Vokabular erfunden habe
Die Lücke schließen zwischen dem, womit ein Foto getaggt ist, und dem, was jemand tatsächlich eintippt
Fotografie-KI·Experte·9 Min. Lesezeit · 10. Juli 2026
Das Foto
Ein Foto in der Galerie ist mit "bicycle" getaggt. Suche nach "bicycle", und es erscheint sofort — ein exakter lexikalischer Treffer, +5 wert, von nichts Semantischem zu schlagen. Suche stattdessen nach "cyclist", und nach den Ranking-Regeln aus dem vorigen Kapitel sollte dieses Foto immer noch gewinnen. Tat es nicht. "cyclist" ist nicht der Tag. Es fällt direkt zum semantischen Abruf durch, wo es auf einer gedeckelten 0–2-Skala gegen ein Foto konkurriert, das nur statistisch mit dem Wort korreliert.
Das Problem
Hybrid Retrieval v2 behob die Ranking-Regression, schuf aber eine Präzisionsanforderung, die es allein nicht erfüllen konnte: Lexikalischer Abruf ist nur so gut wie exakte String-Treffer gegen ein festes Vokabular. Eine Anfrage nach "biking" hat keinen lexikalischen Weg zum Tag "bike" — Kanonisierung (Groß-/Kleinschreibung, Plural) hilft nicht, weil das kein Rechtschreibfehler ist, sondern ein wirklich anderes Wort für dasselbe Konzept. Die Lücke war kein Fehler in der neuen Formel; sie war der Preis dafür, lexikalischen Abgleich streng genug zu machen, um vertrauenswürdig zu sein.
Die Untersuchung
Die Korrektur, die ich baute, war expandPhotoQuery() — kein Synonymwörterbuch, sondern ein Embedding-basierter Beförderungsschritt, der vor der Intent-Extraktion läuft. Er embedded das tatsächlich genutzte kontrollierte Vokabular (welche Tags, Stimmungen und Orte auch immer auf veröffentlichten Fotos existieren, plus die festen Farb- und Ausrichtungslisten) und befördert ein Anfrage-Token nur dann auf den nächstgelegenen Vokabularbegriff, wenn die Ähnlichkeit einen Schwellenwert überschreitet. Ich habe die erste Version mit einem Schwellenwert von 0,75 ausgeliefert, gewählt ohne Test gegen echte Daten.
Er war zu konservativ. Eine manuelle Suche nach "cyclist" lieferte immer noch nur semantische Treffer — keine Beförderung — obwohl "bicycle" ein echter Tag im Vokabular war. Ich fügte ein dauerhaftes Diagnose-Log hinzu und testete gegen echte Embeddings erneut: "cyclist" → "bicycle" (Tag) landete bei einer Ähnlichkeit von 0,604. "beautiful" → "serene and majestic" (Stimmung) landete bei 0,485. Keines überschritt 0,75. Ich kalibrierte auf 0,55 neu — den Mittelwert der beiden echten Beobachtungen, mit Spielraum in beide Richtungen — noch am selben Tag, sobald tatsächliche Zahlen zum Kalibrieren existierten statt einer Vermutung.
Dieser eine flache Schwellenwert hielt drei Tage, bis am 03.07.2026 ein zweites, subtileres Problem auftauchte: "flowers", das mit einer Ähnlichkeit von 0,5527 zum Tag "blossoms" befördert wurde, war eine gute Entscheidung — Tags sind ein offenes Vokabular, das synonymähnliche Beförderung verträgt. Aber "pictures", das mit 0,5781 — einer fast identischen Ähnlichkeit — zu "portrait" (einem Ausrichtungswert) befördert wurde, war eine schlechte. Ausrichtung ist eine geschlossene Zwei-Werte-Menge; eine falsche Beförderung dort fügt kein Rauschen hinzu, sie korrumpiert aktiv die Filterextraktion. Dieselbe Zahl war in einer Kategorie eine richtige Entscheidung und in einer anderen eine falsche, bei fast demselben Ähnlichkeitswert. Es gab keinen einzelnen Schwellenwert, der für beide richtig sein konnte.
Die Entscheidung
Ich habe den einen flachen Schwellenwert in fünf aufgeteilt, einen pro Vokabularkategorie, jeweils festgelegt danach, wie viel eine falsche Beförderung tatsächlich kostet: Tag 0,55 (offenes Vokabular, verträgt breiteren Recall), Stimmung 0,62 (subjektiver, strenger), Ort 0,65 (Falsch-Positive bei Ortsnamen vermeiden), Farbe 0,70 (wenige Farben, leicht zu verwechseln), Ausrichtung 0,75 (geschlossene Zwei-Werte-Menge, eine falsche Beförderung verzerrt die Suche stark). Diese werden weiterhin ausdrücklich als konservative Startwerte bezeichnet, kalibriert aus einer Handvoll echter beobachteter Log-Einträge, nicht als endgültige — dasselbe Diagnose-Logging, das die erste Fehlkalibrierung aufdeckte, bleibt genau deshalb bestehen, damit die nächste messbar ist, nicht erraten.
Bevor überhaupt zu Embeddings gegriffen wird, habe ich außerdem einen billigeren, vollständig deterministischen Schritt hinzugefügt: canonicalizeTerm() faltet Groß-/Kleinschreibung, Unicode-Form, Akzente und reguläre englische Plurale, sodass "mountain" und "mountains" mit null Embedding-Aufrufen zu einem Vokabulareintrag zusammenfallen. Es ist bewusst kein Stemmer — es rührt "biking" oder "cyclist" nicht an, und es ist ausschließlich auf die Kategorie tag beschränkt, weil das blinde Entfernen eines abschließenden "s" von einem Ortsnamen "Athens" zu etwas Falschem verstümmeln würde. Die Embedding-Schicht für wirklich unscharfe Fälle zu reservieren, nachdem die deterministische Schicht die einfachen bereits gelöst hat, hält den probabilistischen Schritt auf das fokussiert, was ihn tatsächlich braucht.
Die Architektur
Anfrage-Token
│
▼
Exakter Vokabultreffer? → ja → unverändert nutzen, null Embedding-Aufrufe
│ nein
▼
canonicalizeTerm()-Treffer? → ja → deterministische Beförderung, null Embedding-Aufrufe
│ nein
▼
Generisches Foto-Suche-Stoppwort? → ja → Token belassen, trotzdem an semantischen Abruf gegeben
│ nein
▼
Token embedden → KNN-1 gegen photo_vocabulary_embedding_vec
│
▼
Ähnlichkeit ≥ getVocabularyPromotionThreshold(Kategorie)?
│ ja │ nein
▼ ▼
Auf nächsten Begriff befördern Token unverändert lassen
(nachgelagert nicht von einem (fällt wie zuvor zum
exakt eingetippten Treffer semantischen Abruf durch)
zu unterscheiden)
Vokabular-Embeddings leben in eigenen Tabellen (photo_vocabulary_embedding_metadata/_vec), unabhängig von Foto-Inhalts-Embeddings — ein Tag kann vollständig aus dem Vokabular verschwinden, wenn das letzte Foto, das ihn trägt, gelöscht wird, daher löscht refreshVocabularyEmbeddings() auch veraltete Begriffe, etwas, das Foto-Inhalts-Embeddings nie brauchen, da eine photoId stabil ist.
Was ich gelernt habe
Beide Fehlkalibrierungen kamen aus demselben Fehler: einen Schwellenwert auszuliefern, der sich vernünftig anfühlte, statt einen, der gegen echte Ähnlichkeitswerte gemessen wurde. text-embedding-3-small läuft merklich "kühler" — niedrigere Kosinus-Ähnlichkeit — für verwandte, aber unterschiedliche Wörter als für nahezu identische Sätze, worauf das Modell primär abgestimmt ist. Eine Zahl, die sich für Prosa intuitiv wie "klar ähnlich" anfühlt, ist nicht dieselbe Zahl für zwei Einzelwörter, die ungefähr dasselbe bedeuten.
Die zweite Fehlkalibrierung hat mich etwas gelehrt, was die erste nicht tat: Ein korrekter Schwellenwert ist keine Eigenschaft des Embedding-Modells, sondern eine Eigenschaft dessen, was eine falsche Beförderung in einer bestimmten Kategorie kostet. Ich hatte angenommen, eine Zahl könne fünf Kategorien bedienen, weil sie alle durch dieselbe Ähnlichkeitsfunktion liefen. Sie brauchen nicht denselben Schwellenwert; sie brauchen dieselbe diagnostische Disziplin — die echte Zahl protokollieren, nicht raten, neu kalibrieren, wenn die Daten es sagen.
Der Blick nach vorn
Mit exakter Absicht, kanonischen Formen und jetzt vokabularbewussten Synonymen, die alle auf denselben lexikalischen Schnellpfad auflösten, war die Suche selbst präzise geworden. Aber präzise gerankte Ergebnisse und eine tatsächliche Antwort auf eine Frage sind zwei verschiedene Dinge — und das nächste Problem drehte sich nicht mehr um die Bewertung von Fotos. Es ging darum, ob das System in eigenen Worten erklären konnte, warum eine Menge von Fotos überhaupt zusammengehörte.