Als Embeddings mich belogen haben
Zwei Fehler, zwei Tage auseinander — erst Stille, dann eine falsche Antwort, die sich sicher anhörte
Fotografie-KI·Experte·9 Min. Lesezeit · 9. Juli 2026
Das Foto
Stell dir noch einmal das mit "street" getaggte Foto vor, das irgendwo anders in der Galerie neben einem unverwandten Foto liegt. Am Morgen des 28.06.2026 tippte ich eine Suchanfrage ein und erwartete, dass das Street-Foto zurückkäme. Nichts kam zurück — nicht dieses Foto, kein Foto, bei keiner Anfrage. Die Suche war genau an diesem Morgen live gegangen, und sie war, funktional, stumm.
Das Problem
Das Such-Backend (Phase 5, erste Version) war semantik-first gebaut: Jede Anfrage lief über KNN gegen Foto-Embeddings, mit kleinen additiven Boosts (+0,03 bis +0,05), falls eine Anfrage zufällig auch exakt zu einem Tag oder einer Stimmung passte. Es sah vollständig aus — Vokabular, deterministische Filterextraktion, eine Embedding-Tabelle mit vec0-Index. Was fehlte, waren tatsächliche Embeddings, gegen die man suchen konnte.
Die Untersuchung
Die Diagnose war fast peinlich einfach, sobald ich nachsah: 6 veröffentlichte Fotos, alle KI-angereichert, alle mit Bildunterschriften und Tags und einer Stimmung — und 0 Zeilen in photo_embedding_vec. buildPhotoEmbeddingText() funktionierte. upsertPhotoEmbeddings.ts funktionierte. refreshPhotoEmbeddings() funktionierte, wenn man es aufrief. Niemand tat das. enrichPhotoById() aktualisierte die KI-Metadaten eines Fotos und hörte dann auf — es rief nie upsertPhotoEmbedding() auf. Der Anreicherungsablauf, Schritt für Schritt aufgeschlüsselt, war tatsächlich:
1. Bild von Firebase herunterladen ✓
2. Claude Vision aufrufen (enrichPhoto) ✓
3. DB mit KI-Metadaten aktualisieren ✓
4. Embedding für Suche upserten ✗ ← fehlt
5. Ergebnis zurückgeben ✓
Schritt 4 existierte schlicht nicht. Jedes Foto, das ich in den vorangegangenen zwei Tagen angereichert hatte, hatte eine Bildunterschrift und Tags und überhaupt kein Embedding — und nichts am Rückgabewert des Anreicherungsablaufs oder der Admin-UI hätte mir das gesagt, außer eine direkte Abfrage gegen die Vec-Tabelle.
Ich habe es noch am selben Morgen behoben — Foto nach dem Metadaten-Update erneut abrufen, sein Embedding upserten, in try/catch einhüllen, damit ein fehlgeschlagenes Embedding niemals einen bereits erfolgreichen Metadaten-Schreibvorgang zurückrollt. Zwei Tage später, am 30.06.2026, mit jetzt tatsächlich befüllten Embeddings, tauchte ein zweiter Fehler auf: Ein mit "street" getaggtes Foto mit einer semantischen Ähnlichkeit von 0,3 verlor einen Ranking-Wettstreit gegen ein völlig unverwandtes Foto mit einer Ähnlichkeit von 0,85. Der kleine additive Boost für einen exakten Tag-Treffer (+0,03 bis +0,05) reichte bei Weitem nicht, um diese Lücke zu überwinden. Semantische Ähnlichkeit führte, im Stillen, das gesamte Ranking — und sie hatte keine Ahnung, was "street" für mich bedeutete, nur womit es statistisch korrelierte.
Die Entscheidung
Für den Fehler der fehlenden Embeddings war die Korrektur mechanisch, sobald gefunden — den fehlenden Aufruf verdrahten, ihn nicht-fatal machen, fertig. Die schwierigere Entscheidung betraf das Ranking, denn die ehrliche Korrektur war kein größerer Boost, sondern die gesamte Prämisse zu verwerfen, dass semantische Ähnlichkeit primär sein sollte. Ich erwog, stattdessen die Boost-Werte hochzudrehen — von 0,03 auf etwas Größeres — und verwarf das, aus demselben Grund, warum ein größerer Evidenz-Deckel das Halluzinationsproblem der Anschreiben nicht behoben hätte: eine größere Zahl in derselben kaputten Formel verschiebt nur, wo es bricht, sie verhindert nicht, dass es bricht.
Application Documents hatte genau diese Frage für die Anschreiben-Evidenz bereits beantwortet: Deterministischer Abruf liefert Präzision, semantischer Abruf liefert Recall, und exakte Absicht dominiert immer fuzzy Ähnlichkeit. Ich habe diese Philosophie direkt übernommen, statt eine fotografiespezifische neu herzuleiten. Die Formel wurde additiv mit einem harten Deckel: finalScore = lexicalScore + min(semanticScore × 2, 2). Ein einzelner exakter Tag-Treffer ist +5 wert. Der bestmögliche semantische Beitrag, gedeckelt, ist 2 wert. Ein Foto kann eine exakte Übereinstimmung nicht mehr überranken, nur weil es bloß ähnlich ist — das ist keine Tuning-Entscheidung, es ist eine Invariante, und das Ranking-Entscheidungsdokument markiert die Formel ausdrücklich als stable, nicht als Phase in Arbeit: Jede künftige Änderung daran sollte als Wiedereröffnung genau dieser Regression behandelt werden, nicht als Routineanpassung.
Die Architektur
Anfrage
│
▼
extractPhotoFilters() deterministisch: Tags, Stimmung, Location, Farbe, Ausrichtung
│
├─────────────┐
▼ ▼
Lexikalischer Semantischer
Abruf Abruf (KNN über photo_embedding_vec — jetzt tatsächlich befüllt)
│ │
└──────┬──────┘
▼
mergePhotoRetrieval() ID-basierte Zusammenführung, vereinigt matchedFields, nie doppelt
▼
rankPhotos() finalScore = lexicalScore + min(semanticScore × 2, 2)
▼
applyDiversity() × 2
▼
Ergebnisse
enrichPhotoById() upsertet das Embedding jetzt unmittelbar nach dem Metadaten-Schreibvorgang, im selben nicht-fatalen try/catch-Muster, das später auch für die Vokabular-Embedding-Aktualisierung verwendet wurde — ein fehlgeschlagenes Embedding wird protokolliert, nicht still verschluckt, und blockiert nie die Anreicherung, um die der Admin tatsächlich gebeten hat.
Was ich gelernt habe
Das Peinliche ist nicht, dass der Fehler existierte — Pipelines haben manchmal fehlende Schritte. Es ist, dass nichts an der eigenen Ausgabe des Systems mir gesagt hätte, dass er fehlte. Sechs angereicherte Fotos, alle in der Admin-UI korrekt angereichert aussehend, mit einem stillen, unsichtbaren Loch zwei Schritte später. Ich hatte angenommen, eine funktionierende Admin-UI bedeute eine funktionierende Pipeline; sie bedeutete, dass die Teile, die ich sehen konnte, funktionierten.
Der Ranking-Fehler hat mich etwas Konkreteres gelehrt: Ich hatte angenommen, additive Boosts seien eine sichere, konservative Art, zwei Signale zu mischen. Sind sie nicht, wenn die beiden Signale auf wild unterschiedlichen Skalen operieren — ein gedeckelter semantischer Beitrag ist kein Tuning-Parameter, er ist eine strukturelle Garantie, und der Unterschied zwischen beidem zeigt sich erst, wenn man tatsächlich eine Anfrage testet, bei der die beiden Signale sich widersprechen.
Der Blick nach vorn
Mit jetzt maßgeblichem lexikalischem Abgleich tauchte fast sofort ein neues, viel engeres Problem auf: Lexikalischer Abgleich erkennt nur exaktes Vokabular. Jemand, der "biking" sucht, bekommt nichts von einem mit "bike" getaggten Foto — nicht weil das Ranking falsch ist, sondern weil den beiden Wörtern nie gesagt wurde, dass sie dasselbe bedeuten.