Implementierungsbriefing: Go-Client für immich-machine-learning zur CLIP-basierten Similar-Image-Suche
Erstelle in Go einen wiederverwendbaren Client für den Docker-Container ghcr.io/immich-app/immich-machine-learning, um dessen CLIP-Funktionalität außerhalb von Immich zu nutzen.
Der Fokus liegt auf:
- Bild -> Embedding
- Text -> Embedding
- Bild-zu-Bild Similarity Search
- Text-zu-Bild Search
Die Implementierung soll nicht die komplette Immich-Serverlogik nachbauen, sondern den ML-Container als internen Embedding-Service verwenden.
- Der Container ist in Immich als interner Dienst gedacht.
- Standard-Port ist 3003
- Standard-Healthcheck-Endpunkt ist
GET /ping - Inferenz-Endpunkt ist
POST /predict - Requests werden als
multipart/form-datagesendet. - Die Modell-Requests werden im Feld
entriesals JSON serialisiert übertragen. - Je nach Request wird zusätzlich entweder:
- ein Bild im Feld
image - oder Text im Feld
textgesendet.
- ein Bild im Feld
- Der Dienst liefert CLIP-Embeddings als serialisierte Zeichenkette zurück, nicht garantiert als natives JSON-Float-Array.
- Der Dienst besitzt keine eingebaute Authentifizierung und sollte nur intern, hinter Proxy/VPN/API-Gateway oder in privaten Netzwerken genutzt werden.
Immich verwendet standardmäßig:
http://immich-machine-learning:3003
Immich prüft den Dienst über:
GET /ping
Immich ruft Inferenz über:
POST /predict
auf.
Immich modelliert CLIP-Anfragen so:
- Task:
"clip" - Typ
"visual"für Bild-Embedding - Typ
"textual"für Text-Embedding
Struktur von entries:
{
"clip": {
"visual": {
"modelName": "ViT-B-32__openai"
}
}
}Struktur von entries:
{
"clip": {
"textual": {
"modelName": "ViT-B-32__openai",
"options": {
"language": "de"
}
}
}
}Für Bild-Requests:
- Feld
entries: JSON-String - Feld
image: Binärdatei
Für Text-Requests:
- Feld
entries: JSON-String - Feld
text: Query-String
Immich erzeugt Text-Embeddings per CLIP und sucht damit gegen gespeicherte Bild-Embeddings. Zusätzlich unterstützt Immich auch eine Suche über eine vorhandene Asset-Embedding-Referenz, was konzeptionell eine Bild-zu-Bild-Suche ist.
Immich speichert Bild-Embeddings in einer Vektor-Spalte in Postgres und nutzt Cosine Similarity mit HNSW Index. Für die Go-Implementierung soll zunächst nur der Client erstellt werden. Die Vektordatenbank-Anbindung kann optional abstrahiert werden.
Erstelle ein Go-Paket, zum Beispiel:
- Modulname:
immichml - Package:
immichml
Das Paket soll einen robusten HTTP-Client kapseln.
Es soll ungefähr folgende API geben:
type Client struct {
BaseURL string
HTTPClient *http.Client
ModelName string
}
func NewClient(baseURL string, opts ...Option) *Client
func (c *Client) Ping(ctx context.Context) error
func (c *Client) EncodeImageFile(ctx context.Context, path string) ([]float32, error)
func (c *Client) EncodeImageBytes(ctx context.Context, filename string, data []byte) ([]float32, error)
func (c *Client) EncodeText(ctx context.Context, text string, language *string) ([]float32, error)Optional zusätzlich:
type SearchResult struct {
ID string
Score float32
Metadata map[string]any
}
type VectorStore interface {
Upsert(ctx context.Context, id string, embedding []float32, metadata map[string]any) error
QuerySimilar(ctx context.Context, embedding []float32, limit int) ([]SearchResult, error)
}
func FindSimilarFromImage(ctx context.Context, client *Client, store VectorStore, image []byte, limit int) ([]SearchResult, error)
func FindSimilarFromText(ctx context.Context, client *Client, store VectorStore, query string, language *string, limit int) ([]SearchResult, error)Folgende Optionen sollen unterstützt werden:
- Base URL, z. B.
http://localhost:3003 - Modellname, Default:
ViT-B-32__openai
- HTTP Timeout
- Optional Custom Headers
- Optional Logger
- Optional Retry-Strategie
Implementiere eine interne Funktion zum Erstellen von multipart/form-data Requests.
Für Bild-Requests:
entriesals JSON-Feldimageals Dateiinhalt
Für Text-Requests:
entriesals JSON-Feldtextals Textfeld
Da das Embedding laut Immich-Code als serialisierte Zeichenkette zurückkommt, muss der Parser robust sein.
Er soll mindestens folgende Fälle verarbeiten können:
Response enthält ein Feld:
{
"clip": "[0.12, -0.44, 0.98]"
}Dann muss das Feld clip als String gelesen und in []float32 geparsed werden.
Response enthält direkt:
{
"clip": [0.12, -0.44, 0.98]
}Dann ebenfalls in []float32 umwandeln.
Response enthält zusätzliche Metadaten wie:
{
"clip": "[...]",
"imageHeight": 1080,
"imageWidth": 1920
}Diese Zusatzfelder sollen toleriert werden.
Der Client soll:
- HTTP Statuscodes ungleich 2xx als Fehler behandeln
- Response-Body im Fehler möglichst mitschneiden
- Timeouts sauber propagieren
- JSON-/Embedding-Parsingfehler klar benennen
- leere Embeddings als Fehler behandeln
Nicht als Public-Internet-Client designen. Im Prompt klar festhalten:
- kein OAuth
- keine öffentliche Freigabe
- nur interne Services
- optional API-Gateway davor
Bitte die Implementierung so strukturieren, dass sie gut testbar ist:
httptest.Serverfür Unit-Tests- Testfälle für
Ping - Testfälle für Bild-Embedding-Request
- Testfälle für Text-Embedding-Request
- Testfälle für Parser mit String-Embedding
- Testfälle für Parser mit Array-Embedding
- Testfälle für Fehlerfälle
type clipVisualEntries struct {
Clip map[string]clipVisualConfig `json:"clip"`
}
type clipVisualConfig struct {
Visual clipModelConfig `json:"visual"`
}
type clipTextualEntries struct {
Clip map[string]clipTextualConfig `json:"clip"`
}
type clipTextualConfig struct {
Textual clipTextualModelConfig `json:"textual"`
}
type clipModelConfig struct {
ModelName string `json:"modelName"`
}
type clipTextualModelConfig struct {
ModelName string `json:"modelName"`
Options map[string]interface{} `json:"options,omitempty"`
}Besser: nicht exakt obige Typen übernehmen, sondern semantisch korrekt und einfach modellieren. Wichtig ist nur, dass das resultierende JSON exakt zur erwarteten Struktur passt.
Empfohlene finale JSON-Strukturen:
- Bild:
{
"clip": {
"visual": {
"modelName": "ViT-B-32__openai"
}
}
}- Text:
{
"clip": {
"textual": {
"modelName": "ViT-B-32__openai",
"options": {
"language": "de"
}
}
}
}Da die API variabel sein kann, nicht zu starr modellieren.
Beispiel:
type predictResponse struct {
Clip any `json:"clip"`
ImageHeight int `json:"imageHeight,omitempty"`
ImageWidth int `json:"imageWidth,omitempty"`
}Dann Clip dynamisch behandeln:
string[]any[]float64
Implementiere eine Funktion wie:
func parseEmbedding(v any) ([]float32, error)Verhalten:
- Wenn
veinstringist:- String als JSON-Array interpretieren
- z. B.
"[1.0, 2.0, 3.0]"
- Wenn
vbereits ein Array ist:- Elemente nach
float32konvertieren
- Elemente nach
- Sonst Fehler
Zusätzliche Robustheit:
- führende/trailing spaces ignorieren
- bei leerem Array kein Fehler, aber optional validieren
- bei ungültigen Elementen klarer Fehler mit Index
- Datei lesen
entriesJSON erzeugen:{ "clip": { "visual": { "modelName": "ViT-B-32__openai" } } }- Multipart Request bauen
POST {baseURL}/predict- JSON Response lesen
clipFeld extrahieren- in
[]float32umwandeln - zurückgeben
entriesJSON erzeugen:{ "clip": { "textual": { "modelName": "ViT-B-32__openai", "options": { "language": "de" } } } }- Multipart Request mit
textFeld bauen POST {baseURL}/predict- Response parsen
- Embedding zurückgeben
ctx := context.Background()
client := immichml.NewClient(
"http://localhost:3003",
immichml.WithModel("ViT-B-32__openai"),
immichml.WithTimeout(30*time.Second),
)
if err := client.Ping(ctx); err != nil {
log.Fatalf("ml service unavailable: %v", err)
}
imgVec, err := client.EncodeImageFile(ctx, "./example.jpg")
if err != nil {
log.Fatalf("encode image failed: %v", err)
}
lang := "de"
txtVec, err := client.EncodeText(ctx, "rotes Auto bei Sonnenuntergang", &lang)
if err != nil {
log.Fatalf("encode text failed: %v", err)
}
fmt.Println(len(imgVec), len(txtVec))Zusätzlich kann ein separates Package erstellt werden, z. B. similarity, das den ML-Client mit einem Vektorstore kombiniert.
- Bild embeddieren
- Embedding im Store ablegen
- mit Cosine Similarity abfragen
type VectorStore interface {
Upsert(ctx context.Context, id string, embedding []float32, metadata map[string]any) error
QuerySimilar(ctx context.Context, embedding []float32, limit int) ([]SearchResult, error)
}- pgvector
- Qdrant
- Weaviate
- Elasticsearch/OpenSearch dense_vector
- einfacher In-Memory-Teststore
Nur Embeddings desselben Modells vergleichen. Falls Modell gewechselt wird, müssen Bestandsdaten re-embedded werden.
Beispiel für den eigenständigen Betrieb des Containers:
name: reusable_ml
services:
immich-machine-learning:
image: ghcr.io/immich-app/immich-machine-learning:release
container_name: immich_machine_learning
volumes:
- model-cache:/cache
restart: always
ports:
- "3003:3003"
volumes:
model-cache:Hinweise:
- Modelle werden im Cache-Volume gespeichert
- Start kann beim ersten Abruf länger dauern, da Modelle geladen/heruntergeladen werden
- Healthcheck erst nach Containerstart prüfen
- Service nicht ungeschützt öffentlich exponieren
Die Implementierung soll nicht:
- Immich vollständig nachbauen
- Facial Recognition implementieren
- OCR implementieren
- Immich DB-Schema replizieren
- Job Queues oder Worker nachbauen
- öffentliche Auth/API-Schichten bauen
Der Scope ist rein:
- Go-Client für CLIP im
immich-machine-learningContainer - optional Similarity-Layer über abstrahierten Vektorstore
- Verwende idiomatisches Go
- Unterstütze
context.Contextin allen Netzwerkmethoden - Nutze
mime/multipartfür FormData - Nutze
encoding/jsonfür Request- und Response-Verarbeitung - Begrenze Memory-Kopien soweit sinnvoll
- Definiere saubere, exportierte Fehler oder Fehlerwraps
- Schreibe vollständige Unit-Tests
- Dokumentiere die Public API mit GoDoc
- Liefere ein kleines
example_test.goodercmd/demo/main.go - Trenne klar zwischen:
- HTTP-Transport
- Request-Encoding
- Response-Parsing
- Similarity/VectorStore-Abstraktion
Die KI soll idealerweise liefern:
- Vollständiges Go-Package
immichml - Optional Package
similarity - Unit-Tests
- kleines Nutzungsbeispiel
- README mit:
- Start des Docker-Containers
- Beispielnutzung
- bekannte Einschränkungen
Die Implementierung soll defensiv sein, weil immich-machine-learning primär ein interner Immich-Dienst ist.
Daher:
- Response locker parsen
- Zusatzfelder tolerieren
- Embedding sowohl als String als auch als Array akzeptieren
- keine harte Annahme über vollständig stabile Public API machen
Bitte implementiere in Go einen Client für immich-machine-learning, der:
GET /pingunterstütztPOST /predictmit Multipart senden kann- CLIP Bild-Embeddings erzeugt
- CLIP Text-Embeddings erzeugt
- Embeddings robust in
[]float32parst - für spätere Similarity Search erweiterbar ist
Fokus:
- robust
- testbar
- idiomatisch
- klar gekapselt
- produktionsnah für interne Services