Skip to content

Instantly share code, notes, and snippets.

@dhcgn
Created May 12, 2026 08:54
Show Gist options
  • Select an option

  • Save dhcgn/e666d57ca06612aaf19412ff7bb3d419 to your computer and use it in GitHub Desktop.

Select an option

Save dhcgn/e666d57ca06612aaf19412ff7bb3d419 to your computer and use it in GitHub Desktop.

Implementierungsbriefing: Go-Client für immich-machine-learning zur CLIP-basierten Similar-Image-Suche

Ziel

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:

  1. Bild -> Embedding
  2. Text -> Embedding
  3. Bild-zu-Bild Similarity Search
  4. Text-zu-Bild Search

Die Implementierung soll nicht die komplette Immich-Serverlogik nachbauen, sondern den ML-Container als internen Embedding-Service verwenden.


Wichtige Rahmenbedingungen

  • 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-data gesendet.
  • Die Modell-Requests werden im Feld entries als JSON serialisiert übertragen.
  • Je nach Request wird zusätzlich entweder:
    • ein Bild im Feld image
    • oder Text im Feld text gesendet.
  • 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.

Relevante Erkenntnisse aus Immich

Standard-URL in Immich

Immich verwendet standardmäßig:

http://immich-machine-learning:3003

Healthcheck

Immich prüft den Dienst über:

  • GET /ping

Prediction

Immich ruft Inferenz über:

  • POST /predict

auf.

Request-Struktur für CLIP

Immich modelliert CLIP-Anfragen so:

  • Task: "clip"
  • Typ "visual" für Bild-Embedding
  • Typ "textual" für Text-Embedding

Bild-Embedding Request

Struktur von entries:

{
  "clip": {
    "visual": {
      "modelName": "ViT-B-32__openai"
    }
  }
}

Text-Embedding Request

Struktur von entries:

{
  "clip": {
    "textual": {
      "modelName": "ViT-B-32__openai",
      "options": {
        "language": "de"
      }
    }
  }
}

Multipart-Nutzlast

Für Bild-Requests:

  • Feld entries: JSON-String
  • Feld image: Binärdatei

Für Text-Requests:

  • Feld entries: JSON-String
  • Feld text: Query-String

Smart Search in Immich

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.

Embedding Storage in Immich

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.


Implementierungsziel in Go

Erstelle ein Go-Paket, zum Beispiel:

  • Modulname: immichml
  • Package: immichml

Das Paket soll einen robusten HTTP-Client kapseln.


Öffentliche API des Go-Pakets

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)

Technische Anforderungen

1. Konfigurierbarkeit

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

2. Multipart Request Builder

Implementiere eine interne Funktion zum Erstellen von multipart/form-data Requests.

Für Bild-Requests:

  • entries als JSON-Feld
  • image als Dateiinhalt

Für Text-Requests:

  • entries als JSON-Feld
  • text als Textfeld

3. Response Parsing

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:

Fall A

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.

Fall B

Response enthält direkt:

{
  "clip": [0.12, -0.44, 0.98]
}

Dann ebenfalls in []float32 umwandeln.

Fall C

Response enthält zusätzliche Metadaten wie:

{
  "clip": "[...]",
  "imageHeight": 1080,
  "imageWidth": 1920
}

Diese Zusatzfelder sollen toleriert werden.

4. Fehlerbehandlung

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

5. Sicherheit

Nicht als Public-Internet-Client designen. Im Prompt klar festhalten:

  • kein OAuth
  • keine öffentliche Freigabe
  • nur interne Services
  • optional API-Gateway davor

6. Testbarkeit

Bitte die Implementierung so strukturieren, dass sie gut testbar ist:

  • httptest.Server fü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

Erwartete interne Datenstrukturen

Go-Structs für Requests

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"
      }
    }
  }
}

Go-Struct für Response

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

Parser-Anforderung: Embedding String -> []float32

Implementiere eine Funktion wie:

func parseEmbedding(v any) ([]float32, error)

Verhalten:

  • Wenn v ein string ist:
    • String als JSON-Array interpretieren
    • z. B. "[1.0, 2.0, 3.0]"
  • Wenn v bereits ein Array ist:
    • Elemente nach float32 konvertieren
  • 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

Beispielablauf Bild -> Embedding

  1. Datei lesen
  2. entries JSON erzeugen:
    {
      "clip": {
        "visual": {
          "modelName": "ViT-B-32__openai"
        }
      }
    }
  3. Multipart Request bauen
  4. POST {baseURL}/predict
  5. JSON Response lesen
  6. clip Feld extrahieren
  7. in []float32 umwandeln
  8. zurückgeben

Beispielablauf Text -> Embedding

  1. entries JSON erzeugen:
    {
      "clip": {
        "textual": {
          "modelName": "ViT-B-32__openai",
          "options": {
            "language": "de"
          }
        }
      }
    }
  2. Multipart Request mit text Feld bauen
  3. POST {baseURL}/predict
  4. Response parsen
  5. Embedding zurückgeben

Beispiel für gewünschte Go-Nutzung

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))

Optionaler zweiter Teil: Similarity Layer

Zusätzlich kann ein separates Package erstellt werden, z. B. similarity, das den ML-Client mit einem Vektorstore kombiniert.

Ziel

  • Bild embeddieren
  • Embedding im Store ablegen
  • mit Cosine Similarity abfragen

Interface

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)
}

Mögliche Backends

  • pgvector
  • Qdrant
  • Weaviate
  • Elasticsearch/OpenSearch dense_vector
  • einfacher In-Memory-Teststore

Wichtig

Nur Embeddings desselben Modells vergleichen. Falls Modell gewechselt wird, müssen Bestandsdaten re-embedded werden.


Docker-Hinweise

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

Nicht-Ziele

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-learning Container
  • optional Similarity-Layer über abstrahierten Vektorstore

Implementierungsdetails, die die KI berücksichtigen soll

  1. Verwende idiomatisches Go
  2. Unterstütze context.Context in allen Netzwerkmethoden
  3. Nutze mime/multipart für FormData
  4. Nutze encoding/json für Request- und Response-Verarbeitung
  5. Begrenze Memory-Kopien soweit sinnvoll
  6. Definiere saubere, exportierte Fehler oder Fehlerwraps
  7. Schreibe vollständige Unit-Tests
  8. Dokumentiere die Public API mit GoDoc
  9. Liefere ein kleines example_test.go oder cmd/demo/main.go
  10. Trenne klar zwischen:
  • HTTP-Transport
  • Request-Encoding
  • Response-Parsing
  • Similarity/VectorStore-Abstraktion

Erwartete Ausgabe der KI

Die KI soll idealerweise liefern:

  1. Vollständiges Go-Package immichml
  2. Optional Package similarity
  3. Unit-Tests
  4. kleines Nutzungsbeispiel
  5. README mit:
    • Start des Docker-Containers
    • Beispielnutzung
    • bekannte Einschränkungen

Zusätzliche Anmerkung zur API-Stabilität

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

Zusammenfassung für die Umsetzung

Bitte implementiere in Go einen Client für immich-machine-learning, der:

  • GET /ping unterstützt
  • POST /predict mit Multipart senden kann
  • CLIP Bild-Embeddings erzeugt
  • CLIP Text-Embeddings erzeugt
  • Embeddings robust in []float32 parst
  • für spätere Similarity Search erweiterbar ist

Fokus:

  • robust
  • testbar
  • idiomatisch
  • klar gekapselt
  • produktionsnah für interne Services
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment