Multi-Modal Embeddings (Text + Image)

Embeddings & Vector Search advanced Building with AI ~22 min read

Overview

Multi-modal embeddings map inputs from different modalities—text, images, audio, video—into a shared vector space where semantic similarity can be measured across modality boundaries. A text query like “sunset over the ocean” should produce a vector close to an image of an ocean sunset, enabling cross-modal retrieval: searching images with text, finding text descriptions that match a photo, or clustering mixed-media content by meaning rather than format.

The search platform in a production AI platform leverages Google’s multimodalembedding model via Vertex AI to generate unified vector representations from both text and image inputs. This allows the system to index a heterogeneous corpus—documentation pages, screenshots, diagrams, photos—into a single vector index and retrieve relevant results regardless of the query or document modality.

Building a production multi-modal embedding pipeline introduces challenges beyond text-only systems: images require preprocessing (resizing, format conversion, byte encoding), the embedding space geometry differs from text-only models (cosine similarity distributions shift), and latency profiles change dramatically when processing image payloads. Understanding these differences is critical for building reliable cross-modal search.

Core Concepts

Shared Embedding Space

Multi-modal models are trained with contrastive objectives (like CLIP’s InfoNCE loss) that pull matching text-image pairs together and push non-matching pairs apart in vector space. The result is a single geometric space where distances are meaningful across modalities. A text vector and an image vector can be compared directly using cosine similarity, without any modality-specific transformation.

Contrastive Learning

The training process for multi-modal embeddings uses contrastive learning: given a batch of (text, image) pairs, the model learns to maximize similarity between matching pairs while minimizing similarity between all non-matching combinations. This creates a rich, discriminative embedding space. Google’s multimodalembedding model extends this to handle text, image, and video inputs in a single model.

Modality-Specific Encoders

Multi-modal models use separate encoder architectures for each modality—a Vision Transformer (ViT) for images and a text transformer for language—then project both into a shared embedding space through learned projection layers. The encoders are trained jointly so their output spaces align. This architecture means you can embed text-only, image-only, or text+image inputs, all producing vectors in the same space.

Cross-Modal Retrieval

The primary application of multi-modal embeddings is cross-modal retrieval: using a query in one modality to find results in another. Text-to-image search (finding photos matching a description), image-to-text search (finding captions or documents describing an image), and mixed queries (text + reference image) are all supported by computing nearest neighbors in the shared embedding space.

Vertex AI Multimodal Embedding API

Google’s Vertex AI provides the multimodalembedding@001 model endpoint that accepts text, image (as base64-encoded bytes or GCS URI), and video inputs. It returns 1408-dimensional vectors by default, with options for 128, 256, or 512 dimensions. The API handles preprocessing internally—images are resized and normalized by the service—but input size limits (max 20MB per image) and rate limits must be managed client-side.

How It Works

The search platform processes multi-modal content through a pipeline that normalizes inputs, calls the Vertex AI embedding endpoint, and stores the resulting vectors alongside metadata in the vector index. Text and image inputs follow different preprocessing paths but converge at the embedding API call.

Embedding Text and Images via Vertex AI

package multimodal

import (
    "context"
    "encoding/base64"
    "fmt"
    "os"

    aiplatform "cloud.google.com/go/aiplatform/apiv1"
    "cloud.google.com/go/aiplatform/apiv1/aiplatformpb"
    "google.golang.org/protobuf/types/known/structpb"
)

// MultiModalEmbedder generates embeddings for text and images using Vertex AI.
type MultiModalEmbedder struct {
    client    *aiplatform.PredictionClient
    endpoint  string // projects/{proj}/locations/{loc}/publishers/google/models/multimodalembedding@001
    dimension int
}

// EmbedText generates an embedding vector for a text input.
func (m *MultiModalEmbedder) EmbedText(ctx context.Context, text string) ([]float32, error) {
    instance, _ := structpb.NewValue(map[string]any{
        "text": text,
    })

    params, _ := structpb.NewValue(map[string]any{
        "dimension": m.dimension,
    })

    resp, err := m.client.Predict(ctx, &aiplatformpb.PredictRequest{
        Endpoint:   m.endpoint,
        Instances:  []*structpb.Value{instance},
        Parameters: params,
    })
    if err != nil {
        return nil, fmt.Errorf("vertex predict (text): %w", err)
    }

    return extractEmbedding(resp.Predictions[0], "textEmbedding")
}

// EmbedImage generates an embedding vector for an image file.
func (m *MultiModalEmbedder) EmbedImage(ctx context.Context, imagePath string) ([]float32, error) {
    imageBytes, err := os.ReadFile(imagePath)
    if err != nil {
        return nil, fmt.Errorf("reading image: %w", err)
    }

    encoded := base64.StdEncoding.EncodeToString(imageBytes)

    instance, _ := structpb.NewValue(map[string]any{
        "image": map[string]any{
            "bytesBase64Encoded": encoded,
        },
    })

    params, _ := structpb.NewValue(map[string]any{
        "dimension": m.dimension,
    })

    resp, err := m.client.Predict(ctx, &aiplatformpb.PredictRequest{
        Endpoint:   m.endpoint,
        Instances:  []*structpb.Value{instance},
        Parameters: params,
    })
    if err != nil {
        return nil, fmt.Errorf("vertex predict (image): %w", err)
    }

    return extractEmbedding(resp.Predictions[0], "imageEmbedding")
}

// extractEmbedding pulls the float32 vector from the prediction response.
func extractEmbedding(prediction *structpb.Value, key string) ([]float32, error) {
    fields := prediction.GetStructValue().GetFields()
    embeddingVal, ok := fields[key]
    if !ok {
        return nil, fmt.Errorf("missing %s in response", key)
    }
    values := embeddingVal.GetListValue().GetValues()
    vec := make([]float32, len(values))
    for i, v := range values {
        vec[i] = float32(v.GetNumberValue())
    }
    return vec, nil
}

Cross-Modal Search Pipeline

package multimodal

import (
    "context"
    "fmt"
    "sort"
)

// SearchResult holds a matched document with its similarity score.
type SearchResult struct {
    ID       string
    Score    float32
    Modality string // "text" or "image"
    Content  string // text content or image path
}

// CrossModalSearch queries a vector index using either text or image input.
func CrossModalSearch(ctx context.Context, embedder *MultiModalEmbedder, index VectorIndex, query string, topK int) ([]SearchResult, error) {
    // Embed the text query
    queryVec, err := embedder.EmbedText(ctx, query)
    if err != nil {
        return nil, fmt.Errorf("embedding query: %w", err)
    }

    // Search the unified index (contains both text and image embeddings)
    matches, err := index.NearestNeighbors(ctx, queryVec, topK)
    if err != nil {
        return nil, fmt.Errorf("nearest neighbor search: %w", err)
    }

    results := make([]SearchResult, len(matches))
    for i, match := range matches {
        results[i] = SearchResult{
            ID:       match.ID,
            Score:    match.Score,
            Modality: match.Metadata["modality"].(string),
            Content:  match.Metadata["content"].(string),
        }
    }

    sort.Slice(results, func(i, j int) bool {
        return results[i].Score > results[j].Score
    })

    return results, nil
}

Practical Applications

The search platform uses multi-modal embeddings to power visual search across a documentation and asset library. Users can type a query like “architecture diagram showing the ingestion pipeline” and retrieve both text descriptions of the pipeline and actual architecture diagrams, ranked by semantic relevance in a single result list. This eliminates the need for separate text search and image search interfaces.

The system also supports reverse image search: uploading a screenshot or diagram and finding related documentation. When a developer encounters an unfamiliar system diagram, they can embed the image and find text documents that describe the depicted architecture. This cross-modal capability is particularly valuable for onboarding, where new team members encounter visual artifacts without textual context.

Multi-modal embeddings also enable automated content organization. By embedding all assets (documentation pages, images, diagrams, code snippets) into a shared space, the system can automatically cluster related content, detect near-duplicate images, and suggest missing documentation for visual assets that lack text descriptions.

Common Pitfalls

Further Reading