Overview
Este tutorial te muestra cómo compilar un backend potenciado por IA que empareja rostros utilizando Go, MongoDB Vector Search y los modelos de AWS Bedrock AI. La aplicación procesa una imagen subida, genera una incrustación vectorial, busca caras similares en MongoDB Atlas y devuelve las tres coincidencias más cercanas con explicaciones.
El backend realiza las siguientes operaciones:
Estandariza imágenes en formato JPEG de 800x600
Genera incrustaciones 1024-dimensionales usando AWS Bedrock
Consulta MongoDB Atlas mediante MongoDB Vector Search
Genera explicaciones de similitud usando Claude
Cómo funcionan los embeddings de vectores
Un embedding es un vector de números en punto flotante que codifica las características de la imagen. Las imágenes similares generan vectores similares, lo que te permite encontrar coincidencias comparando la proximidad de los vectores en un espacio n-dimensional. Las incrustaciones 1024-dimensionales contienen valores en el rango (-1.0, 1.0).
MongoDB Atlas almacena representaciones previamente calculadas para imágenes de referencia. Cuando un usuario sube una imagen, el backend genera su incrustación y utiliza MongoDB Vector Search para encontrar las coincidencias más cercanas.
Flujo de la aplicación
La aplicación realiza los siguientes pasos para procesar las solicitudes:
El frontend captura una imagen y la envía al backend como una solicitud JSON codificada en base64.
El backend estandariza la imagen al formato JPEG 800x600.
AWS Bedrock's
amazon.titan-embed-image-v1El modelo genera una incrustación a partir de la imagen.MongoDB Vector Search encuentra las tres incrustaciones más coincidentes.
El modelo
anthropic.claude-3-sonnet-20240229-v1:0de AWS Bedrock genera una explicación en lenguaje natural de las similitudes.El backend devuelve las imágenes coincidentes y la explicación al frontend.
Prueba la aplicación
Puedes probar la aplicación en mongodb-celeb-search.com.
Tutorial
Este tutorial te muestra cómo realizar las siguientes acciones:
Crear un servidor HTTP para gestionar solicitudes de procesamiento de imágenes
Estandarizar las imágenes subidas a un formato coherente
Generar incrustaciones vectoriales mediante AWS Bedrock
Realizar una consulta en MongoDB Atlas para encontrar imágenes similares
Genera explicaciones en lenguaje natural sobre similitudes de imágenes
Verificar los prerrequisitos
Antes de comenzar con este tutorial, asegúrate de tener los siguientes componentes:
Una cuenta de MongoDB Atlas con un clúster configurado. Para aprender a configurar un clúster de Atlas, consulte la guía de introducción a MongoDB.
Una cuenta de AWS activa con acceso a Bedrock.
Credenciales de AWS configuradas en
~/.aws/configy~/.aws/credentials. Para obtener más información sobre cómo configurar las credenciales de AWS, consulta la documentación del archivo de credenciales de AWS.Go instalado.
Inicializa el módulo y crea el archivo principal
Crea un nuevo directorio para tu proyecto y navega hacia él.
Para inicializar el módulo, ejecute el siguiente comando desde el directorio de su proyecto:
go mod init github.com/jdortiz/goai
Luego, crea un archivo server.go en el mismo directorio con la estructura básica de la aplicación:
package main import ( "context" "log" "net/http" "os" "github.com/joho/godotenv" ) type App struct { } func (app App) Start() error { const serverAddr string = "0.0.0.0:3001" log.Printf("Starting HTTP server: %s\n", serverAddr) return http.ListenAndServe(serverAddr, nil) } func main() { app := App{} log.Println(app.Start()) }
Agregar el endpoint de búsqueda de imágenes
Para agregar un método handler al tipo App y registrarlo con el router, agregue un método imageSearch() a su archivo server.go y regístrelo en el método Start():
func (app App) imageSearch(w http.ResponseWriter, r *http.Request) { log.Println("Image search invoked") } func (app App) Start() error { const serverAddr string = "0.0.0.0:3001" http.HandleFunc("POST /api/search", app.imageSearch) log.Printf("Starting HTTP server: %s\n", serverAddr) return http.ListenAndServe(serverAddr, nil) }
Inicia la aplicación ejecutando el siguiente comando desde tu directorio de proyecto:
go mod tidy go run server.go
Para probar el endpoint, ejecuta el siguiente comando desde la línea de comandos:
curl -IX POST localhost:3001/api/search
Configurar el procesamiento de imágenes
Para definir la estructura de la solicitud y agregar la lógica de estandarización de imágenes, agrega el siguiente código a tu archivo server.go:
type CelebMatchRequest struct { Image64 string `json:"img"` } // Receives a base64 encoded image func standardizeImage(imageB64 string) (*string, error) { // Get the base64 decoder as an io.Reader and use it to decode the image from the data b64Decoder := base64.NewDecoder(base64.StdEncoding, strings.NewReader(imageB64)) origImg, _, err := image.Decode(b64Decoder) if err != nil { return nil, fmt.Errorf("standardizing image failed: %w", err) } // Resize to 800x600 resizedImg := image.NewRGBA(image.Rect(0, 0, 800, 600)) draw.NearestNeighbor.Scale(resizedImg, resizedImg.Rect, origImg, origImg.Bounds(), draw.Over, nil) // Reencode the image to JPEG format with Q=85 var jpegToSend bytes.Buffer if err = jpeg.Encode(&jpegToSend, resizedImg, &jpeg.Options{Quality: 85}); err != nil { return nil, fmt.Errorf("standardizing image failed: %w", err) } // Re-encode to base64 stdImgB64 := base64.StdEncoding.EncodeToString(jpegToSend.Bytes()) return &stdImgB64, nil }
Para actualizar el controlador para decodificar y estandarizar la imagen, modifica el método imageSearch() como se muestra en el siguiente código:
func (app App) imageSearch(w http.ResponseWriter, r *http.Request) { // Deserialize request var imgReq CelebMatchRequest err := json.NewDecoder(r.Body).Decode(&imgReq) if err != nil { log.Println("ERR: parsing json data", err) http.Error(w, err.Error(), http.StatusBadRequest) return } // Split image into metadata and data imgParts := strings.Split(imgReq.Image64, ",") parts := len(imgParts) if parts != 2 { log.Printf("ERR: expecting metadata and data. Got %d parts\n", parts) http.Error(w, fmt.Sprintf("expecting metadata and data. Got %d parts", parts), http.StatusBadRequest) return } // Decode image from base 64, resize image to 800x600 with Q=85, and re-encode to base64 stdImage, err := standardizeImage(imgParts[1]) if err != nil { log.Println("ERR:", err) http.Error(w, "Error standardizing image", http.StatusInternalServerError) return } }
Instale el módulo de edición de imágenes ejecutando el siguiente comando desde su directorio de proyecto:
go get golang.org/x/image/draw
Configura AWS Bedrock
Para instalar el AWS SDK y añadir la información de configuración al struct App, ejecute los siguientes comandos desde el directorio de su proyecto:
go get github.com/aws/aws-sdk-go-v2/config go get github.com/aws/aws-sdk-go-v2/service/bedrockruntime
Luego, agrega los siguientes campos de configuración de AWS a la estructura App en tu archivo server.go:
type App struct { config *aws.Config bedrock *bedrockruntime.Client }
Para inicializar la configuración de AWS, añade los siguientes métodos a tu archivo server.go:
func connectToAWS(ctx context.Context) (*aws.Config, error) { const dfltRegion string = "us-east-1" const credAccount string = "your-account-name" cfg, err := config.LoadDefaultConfig(ctx, config.WithSharedConfigProfile(credAccount), config.WithRegion(dfltRegion), ) return &cfg, err } func NewApp(ctx context.Context) (*App, error) { cfg, err := connectToAWS(ctx) if err != nil { log.Println("ERR: Couldn't load default configuration. Have you set up your AWS account?", err) return nil, err } bedrockClient := bedrockruntime.NewFromConfig(*cfg) return &App{ config: cfg, bedrock: bedrockClient, }, nil }
Por último, actualice el método main() para usar el constructor agregando el siguiente código:
func main() { ctx := context.Background() app, err := NewApp(ctx) if err != nil { panic(err) } log.Println(app.Start()) }
Generar incrustaciones con AWS Bedrock
Para definir las estructuras de solicitud y crear un método para calcular incrustaciones, añade el siguiente código a tu archivo server.go:
const titanEmbedImgV1ModelId string = "amazon.titan-embed-image-v1" const contentTypeJson = "application/json" type EmbeddingConfig struct { OutputEmbeddingLength int `json:"outputEmbeddingLength"` } type BedrockRequest struct { InputImage string `json:"inputImage"` EmbeddingConfig EmbeddingConfig `json:"embeddingConfig"` InputText *string `json:"inputText,omitempty"` } func (app App) computeImageEmbedding(ctx context.Context, image string) ([]float64, error) { payload := BedrockRequest{ InputImage: image, EmbeddingConfig: EmbeddingConfig{ OutputEmbeddingLength: 1024, }, InputText: nil, } bedrockBody, err := json.Marshal(payload) if err != nil { return nil, fmt.Errorf("failed to get embedding from bedrock: %w", err) } bedrockReq := bedrockruntime.InvokeModelInput{ ModelId: aws.String(titanEmbedImgV1ModelId), Body: bedrockBody, ContentType: aws.String(contentTypeJson), } embeddingResp, err := app.bedrock.InvokeModel(ctx, &bedrockReq) if err != nil { return nil, fmt.Errorf("failed to get embedding from bedrock: %w", err) } result := gjson.GetBytes(embeddingResp.Body, "embedding") var embedding []float64 result.ForEach(func(key, value gjson.Result) bool { embedding = append(embedding, value.Float()) return true }) return embedding, nil }
A continuación, instala GJSON ejecutando el siguiente comando desde el directorio de tu proyecto:
go get github.com/tidwall/gjson
Para actualizar el manejador para calcular la incrustación, modifique el método imageSearch como se muestra en el siguiente código:
func (app App) imageSearch(w http.ResponseWriter, r *http.Request) { // ...existing code... // Compute the embedding using titan-embed-image-v1 embedding, err := app.computeImageEmbedding(r.Context(), *stdImage) if err != nil { log.Println("ERR:", err) http.Error(w, "Error computing embedding", http.StatusInternalServerError) return } }
Configurar MongoDB Atlas
En tu directorio de proyecto, crea un archivo .env. Agrega el siguiente código a este archivo para almacenar el URI de conexión de MongoDB Atlas. Reemplaza los marcadores de posición con tu información real de clúster y credenciales. Para aprender a recuperar tu URI de conexión, consulta la guía de MongoDB Comienza aquí.
MONGODB_URI=mongodb+srv://<username>:<password>@<cluster>.mongodb.net/
A continuación, instala los módulos requeridos ejecutando los siguientes comandos desde el directorio de tu proyecto:
go get github.com/joho/godotenv go get go.mongodb.org/mongo-driver/v2
Agregue un campo de cliente MongoDB a la estructura App:
type App struct { client *mongo.Client config *aws.Config bedrock *bedrockruntime.Client }
Para crear métodos para inicializar el cliente de MongoDB, añade el siguiente código a tu archivo server.go:
func newDBClient(uri string) (*mongo.Client, error) { serverAPI := options.ServerAPI(options.ServerAPIVersion1) opts := options.Client().ApplyURI(uri).SetServerAPIOptions(serverAPI) client, err := mongo.Connect(opts) if err != nil { return nil, err } return client, nil } func (app *App) Close() { if err := app.client.Disconnect(context.Background()); err != nil { panic(err) } }
Para actualizar la firma y la implementación del constructor, modifica el método NewApp() como se muestra en el siguiente código:
func NewApp(ctx context.Context, uri string) (*App, error) { cfg, err := connectToAWS(ctx) if err != nil { log.Println("ERR: Couldn't load default configuration. Have you set up your AWS account?", err) return nil, err } bedrockClient := bedrockruntime.NewFromConfig(*cfg) client, err := newDBClient(uri) if err != nil { log.Println("ERR: connecting to MongoDB cluster:", err) return nil, err } return &App{ client: client, config: cfg, bedrock: bedrockClient, }, nil }
Por último, para cargar variables de entorno, actualice el método main() como se muestra a continuación:
func main() { var uri string err := godotenv.Load() if err != nil { log.Fatal("Unable to load .env file") } if uri = os.Getenv("MONGODB_URI"); uri == "" { log.Fatal("You must set your 'MONGODB_URI' environment variable. See\n\t https://mongodb.com/es/docs/drivers/go/current/usage-examples/") } ctx := context.Background() app, err := NewApp(ctx, uri) if err != nil { panic(err) } defer func() { app.Close() }() log.Println(app.Start()) }
Consulta imágenes similares utilizando MongoDB Vector Search
Para crear un método para encontrar imágenes similares en MongoDB, añade el siguiente código a tu archivo server.go:
func (app App) findSimilarImages(ctx context.Context, embedding []float64) ([]string, error) { imgCollection := app.client.Database("celebrity_matcher").Collection("celeb_images") vectorSchStage := bson.D{{"$vectorSearch", bson.D{{"index", "vector_index"}, {"path", "embeddings"}, {"queryVector", embedding}, {"numCandidates", 15}, {"limit", 3}}}} projectStage := bson.D{{"$project", bson.D{{"image", 1}}}} pipeline := mongo.Pipeline{vectorSchStage, projectStage} imgCursor, err := imgCollection.Aggregate(ctx, pipeline) if err != nil { return nil, fmt.Errorf("failed to get similar images from the database: %w", err) } similarImgs := []struct { Id bson.ObjectID `bson:"_id,omitempty"` Image string `bson:"image"` }{} if err = imgCursor.All(ctx, &similarImgs); err != nil { return nil, fmt.Errorf("failed to get similar images from the database: %w", err) } var images []string var stdImage *string for _, item := range similarImgs { stdImage, err = standardizeImage(item.Image) if err != nil { return nil, fmt.Errorf("failed to standardize similar images: %w", err) } images = append(images, *stdImage) } return images, nil }
Para actualizar el controlador para encontrar imágenes similares, modifique el método imageSearch de la siguiente manera:
func (app App) imageSearch(w http.ResponseWriter, r *http.Request) { // ...existing code... // Find similar images using vector search in MongoDB images, err := app.findSimilarImages(r.Context(), embedding) if err != nil { log.Println("ERR:", err) http.Error(w, "Error getting similar images", http.StatusInternalServerError) return } }
Generar explicaciones de similitud con Claude
Para definir las estructuras de solicitud de Claude y crear un método para generar explicaciones, añada el siguiente código a su archivo server.go:
const claude3SonnetV1ModelId string = "anthropic.claude-3-sonnet-20240229-v1:0" type ClaudeBodyMsgSource struct { Type string `json:"type"` MediaType *string `json:"media_type,omitempty"` Data *string `json:"data,omitempty"` } type ClaudeBodyMsgContent struct { Type string `json:"type"` Source *ClaudeBodyMsgSource `json:"source,omitempty"` Text *string `json:"text,omitempty"` } type ClaudeBodyMsg struct { Role string `json:"role"` Content []ClaudeBodyMsgContent `json:"content"` } type ClaudeBody struct { AnthropicVersion string `json:"anthropic_version"` MaxTokens int `json:"max_tokens"` System string `json:"system"` Messages []ClaudeBodyMsg `json:"messages"` } func (app App) getImageSimilaritiesDescription(ctx context.Context, imgB64 string, similarImgB64 []string) (*string, error) { const mediaTypeImage = "image/jpeg" prompt := "Please let the user know how their first image is similar to the other 3 and which one is the most similar?" payload := ClaudeBody{ AnthropicVersion: "bedrock-2023-05-31", MaxTokens: 1000, System: "Please act as face comparison analyzer.", Messages: []ClaudeBodyMsg{ { Role: "user", Content: []ClaudeBodyMsgContent{ { Type: "image", Source: &ClaudeBodyMsgSource{ Type: "base64", MediaType: aws.String(mediaTypeImage), Data: &imgB64, }, }, { Type: "image", Source: &ClaudeBodyMsgSource{ Type: "base64", MediaType: aws.String(mediaTypeImage), Data: &similarImgB64[0], }, }, { Type: "image", Source: &ClaudeBodyMsgSource{ Type: "base64", MediaType: aws.String(mediaTypeImage), Data: &similarImgB64[1], }, }, { Type: "image", Source: &ClaudeBodyMsgSource{ Type: "base64", MediaType: aws.String(mediaTypeImage), Data: &similarImgB64[2], }, }, { Type: "text", Text: &prompt, }, }, }, }, } bedrockBody, err := json.Marshal(payload) if err != nil { return nil, fmt.Errorf("failed to get embedding from bedrock: %w", err) } bedrockReq := bedrockruntime.InvokeModelInput{ ModelId: aws.String(claude3SonnetV1ModelId), Body: bedrockBody, ContentType: aws.String(contentTypeJson), Accept: aws.String(contentTypeJson), } bedrockResp, err := app.bedrock.InvokeModel(ctx, &bedrockReq) if err != nil { return nil, fmt.Errorf("failed to get embedding from bedrock: %w", err) } description := gjson.GetBytes(bedrockResp.Body, "content.0.text").String() return &description, nil }
Para actualizar el gestor para que genere la descripción, modifica el método imageSearch como se muestra en el siguiente código:
func (app App) imageSearch(w http.ResponseWriter, r *http.Request) { // ...existing code... description, err := app.getImageSimilaritiesDescription(r.Context(), *stdImage, images) if err != nil { log.Println("ERR: failed to describe similarities with images", err) http.Error(w, "Error describing similarities with images", http.StatusInternalServerError) return } }
Devolver los resultados
Para definir la estructura de la respuesta y completar el handler, agrega el siguiente código a tu archivo server.go:
type CelebMatchResponse struct { Description string `json:"description"` Images []string `json:"images"` } func (app App) imageSearch(w http.ResponseWriter, r *http.Request) { // ...existing code... response := CelebMatchResponse{ Description: *description, Images: images, } jData, err := json.Marshal(response) if err != nil { log.Println("error serializing json", err) http.Error(w, "error serializing json", http.StatusInternalServerError) return } w.Header().Set("Content-Type", contentTypeJson) w.Header().Set("Content-Length", strconv.Itoa(len(jData))) w.WriteHeader(http.StatusOK) w.Write(jData) }
Vuelve a iniciar la aplicación ejecutando el siguiente comando desde el directorio de tu proyecto:
go mod tidy go run server.go
Navegue a http://localhost:3001 en su navegador web y pruebe la API enviando una solicitud POST a /api/search con una imagen codificada base64.
Recursos adicionales
Para obtener más información sobre MongoDB Vector Search, consulte la documentación de MongoDB Vector Search en el manual de MongoDB Server.
Para obtener más información sobre AWS Bedrock, consulte la documentación de AWS Bedrock.