Explore o novo chatbot do Developer Center! O MongoDB AI chatbot pode ser acessado na parte superior da sua navegação para responder a todas as suas perguntas sobre o MongoDB .

Junte-se a nós no Amazon Web Services re:Invent 2024! Saiba como usar o MongoDB para casos de uso de AI .
Desenvolvedor do MongoDB
Central de desenvolvedor do MongoDBchevron-right
Produtoschevron-right
Atlaschevron-right

Usando Go para AI

Jorge D. Ortiz-Fuentes19 min read • Published Nov 07, 2024 • Updated Nov 07, 2024
AWSIAAtlasPesquisa vetorialGo
APLICATIVO COMPLETO
Ícone do FacebookÍcone do Twitterícone do linkedin
Avalie esse Tutorial
star-empty
star-empty
star-empty
star-empty
star-empty
AAI abriu as portas para a solução de problemas que, uma década atrás, eram quase inacessíveis aos computadores ou, no mínimo, terrivelmente complexos. Os carros autônomos, por exemplo, devem estar totalmente cientes do meio ambiente e de suas mudanças para tomar decisões sobre o que fazer a seguir. Isso significa coletar dados de todos os sensores disponíveis, que incluemcâmeras de vídeo, e "entender" as informações que eles fornecem.
As redes populacionais são uma ótima opção para lidar com esse e outros problemas que exigem “interpretação” ou “entendimento” de entradas complexas. Uma rede causal é um modelo computacional que recebe entradas e as transforma em saídas. Mas antes de podermos usar uma rede causal, temos que treiná-la. Um conjunto de entradas e suas saídas correspondentes esperadas, conhecido como conjunto de treinamento, é usado para obter os valores dos parâmetros da rede que permitem que ele responda conforme o esperado às entradas do conjunto de treinamento e também a outras entradas que não foram pertencer ao conjunto de treinamento. Isso resulta no modelo, ou seja, o layout dos nós e camadas e os parâmetros obtidos após o treinamento. Em seguida, podemos usar o modelo com novas entradas para fornecer saídas, e isso é chamado de inferência.
Neste artigo, vamos encontrar um problema interessante para resolver que requer AI, explicar como a solução funciona atrás das Cortinas e escrever um back-end do zero que implemente a solução que definimos. Portanto, nãopisque durante a leitura, se não quer perder nenhum detalhe.

Um problema do mundo real

Se você frequentemente se gaba de seus amigos por se parecer com Al Pagno, ou que é um doppelganger de michael J.fox, seria melhor para você se uma inteligência artificial confirmasse sua afirmação. Não vamos esquecer que algumas pessoas confiam em AI mais do que outras pessoas ou até mesmo especialistas. E, se não houver esse aplicativo de AI , podemos construí-lo nós mesmos e escrevê-lo em Go e nos Diversão no processo.
O aplicativo que vamos construir ao longo deste artigo vai aceitar uma imagem de uma página da web, tirada com uma web web, e encontrar as três Celebridades com as quais mais nos parecemos (entre as que selecionamos anteriormente). Este é um problema interessante de resolver e algo que seria realmente difícil de resolver sem a AI. Então, como devemos fazer isso?

Design de nível superior da solução

Devemos começar decidir que este será um aplicação da web e vamos divisão o trabalho entre um aplicativo de front-end que tirará a imagem, fará uma solicitação com ela para o back-end e exibirá os resultados fornecidos por ele . A extremidade posterior fará o trabalho pesado. Lamento não dar mais detalhes sobre o aplicativo front-end, mas estamos ocupados escrevendo um aplicativo de back-end no Go (}).
O back-end será responsável por todas as tarefas. Algumas são bastante rotineiras, como oferecer um endpoint HTTP ou analisar os argumentos recebidos por meio dele. Mas também temos que implementar a funcionalidade necessária para descobrir quais das imagens disponíveis de Celebridades se parecem mais com a fornecida pelo front end e retornar uma explicação de como elas são semelhantes a ela. Vamos usar dois modelos de AI para obter essa funcionalidade: um para encontrar as correspondências de Celebridades e o outro para explicar as similaridades.

Os modelos AI

O primeiro modelo que utilizaremos aqui receberá a imagem como entrada e fornecerá um vetor de características. Os AI-tros () chamam esse vetor de incorporação e ele codifica características de uma forma que não é diretamente legível pelos humanos. A incorporação da imagem é um vetor de vários números de ponto flutuante no intervalo (-1.0, 1.0). Usaremos 1024 como o tamanho desse vetor - isso é suficiente para codificar muitas características da imagem. Uma caracterís os elementos descrevem. Em vez disso, o que podemos fazer com os vetores é encontrar os mais próximos para encontrar as imagens que são mais próximas. Entrei em detalhes de como isso é feito abaixo.
Além de obter as melhores correspondências de estrelas, gostariamos de entender o que as torna assim. A aparência é uma questão muito subjetiva e alguma explicação ajudaria os usuários a entender por que eles receberam essas correspondências em vez de outras que, aos seus olhos, são quase gêmeas. Assim, o segundo modelo age de uma forma muito diferente do primeiro. Forneceremos a imagem do usuário e dos três selecionados e solicitaremos uma explicação de como eles são semelhantes. Fui um pouco impreciso quando contei sobre as entradas. A história completa é que forneceremos ao modelo as imagens e um pedaço de texto descrevendo o que queremos. Esse texto em que contamos ao modelo o que queremos é conhecido como prompt e, como não estamos limitando nossa conversa apenas ao texto, mas usamos comunicação escrita e imagens, isso é chamado de prompt multimodal.
Usamos um modelo de AI para extrair as características da imagem, porque tentar estabelecer similaridades entre duas imagens comparando cada um de seus pixels obviamente não é a abordagem correta. Supondo que comecemos com duas fotos da mesma pessoa, um background diferente, uma luz diferente ou até mesmo uma posição ligeiramente diferente pode causar uma diferença maior do que a de duas fotos de pessoas diferentes com exatamente o mesmo background, luz e posição.
Mas quando usamos a imagem do usuário como entrada para o primeiro modelo, a resposta é apenas a incorporação dessa imagem. Não nos diz nada sobre quais outras imagens são semelhantes. Então, como usamos esses dados para encontrar as melhores correspondências para a imagem fornecida?
Para começar, reunimos algumas fotos de renome que são as que usaremos para comparar com a imagem do usuário. Executamos cada um deles no modelo com antecedência para obter a incorporação correspondente. Nós os armazenamos em um cluster MongoDB Atlas . Usamos uma única coleção em que cada um dos documentos contém uma imagem com sua incorporação correspondente e quaisquer outros dados relevantes. Graças à forma como o MongoDB organiza os dados em torno de um document model, podemos armazenar a incorporação como uma array adequada em um único atributo. E podemos usar a pesquisa vetorial para encontrar as imagens mais próximas de uma determinada imagem usando a pesquisa vetorial. Isso nos permitirá encontrar os vizinhos mais próximos na n-dimensional usada para a incorporação.
Se o espaço n-dimensional soa em sua cabe As pesquisas mais comuns em um banco de dados de dados são as que comparam o valor de um dos atributos (um campo de um documento ou uma coluna de um registro em uma tabela) com o valor desejado. No entanto, não é isso que acontece quando você procura "restaurantes próximos" ou o "banco caixa eletrônico mais próximo". Quando você faz queries geoespaciais, você está tentando encontrar os documentos cujas coordenadas estão mais próximas do seu destino, ou seja, você está procurando os vizinhos mais próximos em um espaço bidimensional (aquele em conformidade com a latitude e a longitude). E o modo como isso funciona não é comparando primeiro uma das coordenadas e depois a outra. Em vez disso, ela usa geometria e maneiras de cortar o espaço para obter os candidatos mais próximos de forma eficiente. A busca em um espaço n-dimensional é muito semelhante, mas os pontos de dados têm n componentes em vez de apenas dois. Em nosso caso, cada ponto de dados tem 1024 coordenadas, então não tente imaginar o 1,024 espaço dimensional em sua cabeça ou prepare-se para uma dores de cabeça muito fortes.

Juntando tudo

Após descrever todas as partes que constituem a solução, vejamos como elas trabalham juntas para produzir os resultados desejados.
  1. Primeiro, o usuário usa o navegador da Web para navegar até o URL da página inicial.
  2. O frontend, supondo que as permissões corretas sejam concedidas, captura a imagem do usuário e a envia para o endpoint HTTP do backend em uma solicitação JSON que contém a imagem no formato codificado de base64 , precedido por alguns metadados.
  3. O backend recebe a solicitação, desserializa e padroniza. Precisamos usar imagens com uma resolução de 800x600, no formato IPv com um bom fator de qualidade e base64 codificada.
  4. Em seguida, ele envia a imagem para Amazon Web Services Cama do Rock usando o modeloamazon.titan-embed-image-v1para obter a incorporação.
  5. O vetor retornado por Cama do Rock é o que o backend usa em uma query para o MongoDB Atlas. Usando uma pesquisa vetorial, o MongoDB encontra as correspondências mais próximas entre as Celebridades disponíveis em uma coleção pré-preenchida e as retorna.
  6. A última tarefa que o backend precisa resolver é explicar por que as imagens são semelhantes. Por isso, colocamos as imagens do usuário e as correspondências mais próximas em uma estrutura de dados, junto com a descrição textual do que queremos, e as passamos para Amazon Web Services CloudRock novamente. No entanto, desta vez usaremos um modelo diferente, ou seja,anthropic.claude-3-sonnet-20240229-v1:0, que fornece uma API de mensagens. Este modelo responderá com a explicação textual.
  7. Finalmente, reunimos os dados novamente na estrutura que é usada para a resposta ao front-end.

"Quero jogar com isso"

Sabemos que sim. Você tem várias maneiras de fazer isso. Temos a front-end e a back-end em execução na nuvem. Abra seu navegador e descobrir quem são seus doppelgangers.

Mostre-me o código

Estou ouvindo você dizer: "Código ou não aconteceu", então vamos começar a digitar. Começarei cuidando da infraestrutura do backend.
Por uma questão de simplicidade, vamos colocar todo o código em um único arquivo. Logicamente, você poderia melhorar a capacidade de manutenção e reutilização do código usando mais arquivos e até mesmo pacotes para organizar o código.

O back-end de HTTP

Nesta seção, criaremos um servidor HTTP com um único manipulador HTTP. A partir deste manipulador, enviaremos as solicitações para Amazon Web Services Cama de Rock e MongoDB Atlas.
  1. As primeiras coisas primeiro. Vamos inicializar o módulo que vamos usar:
    1go mod init github.com/jdortiz/goai
  2. E no mesmo diretório criamos um arquivo chamado server.go. Este arquivo pertencerá ao pacote principal .
    1package main
    2
    3func main() {
    4}
  3. Vamos definir uma estrutura para manter as dependências do nosso manipulador HTTP:
    1type App struct {
    2}
  4. Esse tipo também terá os métodos para controlar o aplicação. Vamos começar criando um para iniciar o servidor HTTP e importar os dois pacotes necessários.
    1func (app App) Start() error {
    2 const serverAddr string = "0.0.0.0:3001"
    3 log.Printf("Starting HTTP server: %s\n", serverAddr)
    4
    5 return http.ListenAndServe(serverAddr, nil)
    6}
  5. Esse método agora pode ser usado na função principal, criando primeiro uma instância do tipo.
    1app := App{}
    2log.Fatal(app.Start())
  6. Execute-o para verificar se funciona até o momento.

Adicionar o endpoint

Agora precisamos ser capazes de obter a solicitação no endpoint esperado que é onde toda a milagrosidade ocorrerá.
  1. Vamos implementar este manipulador como um método do tipoApp. Como só precisamos acessar esse método a partir do próprio tipo, não o exportamos (o nome começa com uma letra minúscula).
    1func (app App) imageSearch(w http.ResponseWriter, r *http.Request) {
    2 log.Println("Image search invoked")
    3}
  2. Usamos esse manipulador no roteador e o associamos ao verbo HTTP POST.
    1http.HandleFunc("POST /api/search", app.imageSearch)
  3. Nós o executamos novamente e o testamos na linha de comando.
    1curl -IX POST localhost:3001/api/search
  4. Este é apenas o início de um bonito programa. Como você curte até agora?

Obter a imagem e padronizar

  1. A solicitação é enviada no formato JSON e vamos usar o decodificador JSON do Go para analisá-la nessa estrutura.
    1type CelebMatchRequest struct {
    2 Image64 string `json:"img"`
    3}
  2. Então, nós decodificamos.
    1// Deserialize request
    2var imgReq CelebMatchRequest
    3err := json.NewDecoder(r.Body).Decode(&imgReq)
    4if err != nil {
    5 log.Println("ERR: parsing json data", err)
    6 http.Error(w, err.Error(), http.StatusBadRequest)
    7 return
    8}
  3. E separe os metadados:
    1// Split image into metadata and data
    2imgParts := strings.Split(imgReq.Image64, ",")
    3parts := len(imgParts)
    4if parts != 2 {
    5 log.Printf("ERR: expecting metadata and data. Got %d parts\n", parts)
    6 http.Error(w, fmt.Sprintf("expecting metadata and data. Got %d parts", parts), http.StatusBadRequest)
    7 return
    8}
  4. O próximo passo é padronizar a imagem. Poderemos adicionar esta tarefa diretamente no manipulador, mas para manter a legibilidade, vamos criar uma função privada.
    1// Receives a base64 encoded image
    2func standardizeImage(imageB64 string) (*string, error) {
    3}
  5. A imagem pode então ser decodificada da base64 e, por sua vez, como Jpeg.
    1// Get the base64 decoder as an io.Reader and use it to decode the image from the data
    2b64Decoder := base64.NewDecoder(base64.StdEncoding, strings.NewReader(imageB64))
    3origImg, _, err := image.Decode(b64Decoder)
    4if err != nil {
    5 return nil, fmt.Errorf("standardizing image failed: %w", err)
    6}
  6. O Go oferece algumas funcionalidades básicas de edição de imagens que vamos usar para redimensionar a imagem para 800x600.
    1// Resize to 800x600
    2resizedImg := image.NewRGBA(image.Rect(0, 0, 800, 600))
    3draw.NearestNeighbor.Scale(resizedImg, resizedImg.Rect, origImg, origImg.Bounds(), draw.Over, nil)
  7. E codifique-o de volta como Jppe, com um fator de boa qualidade, e para basear64 novamente.
    1// Reencode the image to JPEG format with Q=85
    2var jpegToSend bytes.Buffer
    3// Encode the image into the buffer
    4if err = jpeg.Encode(&jpegToSend, resizedImg, &jpeg.Options{Quality: 85}); err != nil {
    5 return nil, fmt.Errorf("standardizing image failed: %w", err)
    6}
    7// Re-encode to base64
    8stdImgB64 := base64.StdEncoding.EncodeToString(jpegToSend.Bytes())
    9return &stdImgB64, nil
  8. Essa função toma conta de todas as etapas necessárias, então vamos usá-la a partir do manipulador.
    1// Decode image from base 64, resize image to 800x600 with Q=85, and re-encode to base64
    2stdImage, err := standardizeImage(imgParts[1])
    3if err != nil {
    4 log.Println("ERR:", err)
    5 http.Error(w, "Error standardizing image", http.StatusInternalServerError)
    6 return
    7}
  9. Antes de podermos compilar, temos que adicionar o módulo para edição de imagem.
    1go get golang.org/x/image/draw
  10. Isso foi um pouco de discussão de dados, mas não foi tão complicado. Prepare-se para a carnuda.

Obter a incorporação

Neste momento, preparamos a imagem para calcular a incorporação. Vamos fazer isso agora usando Amazon Web Services Cama do Rock. Você precisará de uma conta ativa Amazon Web Services e os modelos que planejamos usar no serviço Cama do Rock estejam habilitados.
  1. Antes de calcular a incorporação, gostaria de ter a configuração do Amazon Web Services disponível e vamos usar Amazon Web Services SDK para isso.
    1go get github.com/aws/aws-sdk-go-v2/config
    2go get github.com/aws/aws-sdk-go-v2/service/bedrockruntime
  2. Adicionamos a configuração como um campo de nossa estrutura de aplicação .
    1configur *aws.Config
  3. Inicializamos esta configuração em uma função privada. Isso pressupõe que suas credenciais estejam armazenadas nos arquivos canônicos.
    1func connectToAWS(ctx context.Context) (*aws.Config, error) {
    2 const dfltRegion string = "us-east-1"
    3 const credAccount string = "your-account-name"
    4 // Load the Shared AWS Configuration (~/.aws/config)
    5 cfg, err := config.LoadDefaultConfig(ctx,
    6 config.WithSharedConfigProfile(credAccount), // this must be the name of the profile in ~/.aws/config and ~/.aws/credentials
    7 config.WithRegion(dfltRegion),
    8 )
    9 return &cfg, err
    10}
  4. E vamos inicializá-lo em um novo construtor para o nosso tipoApp.
    1func NewApp(ctx context.Context) (*App, error) {
    2 cfg, err := connectToAWS(ctx)
    3 if err != nil {
    4 log.Println("ERR: Couldn't load default configuration. Have you set up your AWS account?", err)
    5 return nil, err
    6 }
    7
    8 return &App{
    9 config: cfg,
    10 }, nil
    11}
  5. Também queremos ter a conexão com a Cama do Rock disponível no(s) manipulador(es).
    1bedrock *bedrockruntime.Client
  6. E inicialize no construtor também.
    1// Initialize bedrock client
    2bedrockClient := bedrockruntime.NewFromConfig(*cfg)
    3
    4 return &App{
    5 config: cfg,
    6 bedrock: bedrockClient,
    7 }, nil
  7. Substituimos a inicialização do aplicativo para usar o construtor.
    1ctx := context.Background()
    2app, err := NewApp(ctx)
    3if err != nil {
    4 panic(err)
    5}
  8. Criamos um novo método privado que lidará com a comunicação com Amazon Web Services BI para calcular a incorporação. Ele retornará o vetor ou um erro se algo der errado.
    1// Prepare request to titan-embed-img-v1
    2func (app App) computeImageEmbedding(ctx context.Context, image string) ([]float64, error) {
    3}
  9. Como mencionamos antes, o modelo que queremos usar para obter a incorporação é itan e definimos uma constante com o nome completo.
    1const titanEmbedImgV1ModelId string = "amazon.titan-embed-image-v1"
  10. A solicitação para esse modelo tem uma estrutura predefinida que devemos usar, por isso definimos estruturas para acomodar os dados.
    1type EmbeddingConfig struct {
    2 OutputEmbeddingLength int `json:"outputEmbeddingLength"`
    3}
    4
    5type BedrockRequest struct {
    6 InputImage string `json:"inputImage"`
    7 EmbeddingConfig EmbeddingConfig `json:"embeddingConfig"`
    8 InputText *string `json:"inputText,omitempty"`
    9}
  11. E temos que inserir os dados para criar a solicitação, definindo o tamanho do vetor para 1024.
    1// Prepare the request to bedrock
    2payload := BedrockRequest{
    3 InputImage: image,
    4 EmbeddingConfig: EmbeddingConfig{
    5 OutputEmbeddingLength: 1024,
    6 },
    7 InputText: nil,
    8}
  12. OBedRock usa uma única interface para interagir com diferentes modelos que usam diferentes parâmetros e retornar objetos com campos diferentes. Assim, ele usa um único atributo de suas solicitações (Body) que contém a respectiva solicitação para o modelo serializado como um fluxo de bits. Vamos fazer a serialização em uma fatia de bytes.
    1bedrockBody, err := json.Marshal(payload)
    2if err != nil {
    3 return nil, fmt.Errorf("failed to get embedding from bedrock: %w", err)
    4}
  13. A fatia de bytes entra na solicitação real que temos que preparar para a Cama do Rock.
    1bedrockReq := bedrockruntime.InvokeModelInput{
    2 ModelId: aws.String(titanEmbedImgV1ModelId),
    3 Body: bedrockBody,
    4 ContentType: aws.String(contentTypeJson),
    5}
  14. A constante do tipo de conteúdo que acabamos de usar será útil para algumas solicitações futuras, por isso a declaramos como uma constante global.
    1const contentTypeJson = "application/json"
  15. Agora que concluímos a solicitação, podemos invocar o modelo com ele para fazer com que ele infira a incorporação.
    1// Invoke model to obtain embedding for the image
    2embeddingResp, err := app.bedrock.InvokeModel(ctx, &bedrockReq)
    3if err != nil {
    4 return nil, fmt.Errorf("failed to get embedding from bedrock: %w", err)
    5}
  16. A resposta do modelo também vem no campo Body como uma fatia de bytes. Poderemos desserializar a resposta com uma nova estrutura e, em seguida, desserializar o campo novamente. Mas, em vez disso, vamos usar um módulo que simplifica a tarefa. É chamado GJSON.
    1go get github.com/tidwall/gjson
  17. O GJSON pode ser utilizado para extrair os dados de qualquer parte de um documento JSON. Estamos interessados na fatia de bytes retornados no campo Bodyda respostabedrockruntime.InvokeModelOutput . Também temos que converter a fatia de bytes que contém a representação de string da incorporação em um vetor real de flutuantes e retornar o vetor resultante.
    1result := gjson.GetBytes(embeddingResp.Body, "embedding")
    2var embedding []float64
    3result.ForEach(func(key, value gjson.Result) bool {
    4 embedding = append(embedding, value.Float())
    5 return true
    6})
    7
    8return embedding, nil
  18. Com este método pronto, basta invocá-lo a partir do manipulador.
    1// Compute the embedding using titan-embed-image-v1
    2embedding, err := app.computeImageEmbedding(r.Context(), *stdImage)
    3if err != nil {
    4 log.Println("ERR:", err)
    5 http.Error(w, "Error computing embedding", http.StatusInternalServerError)
    6 return
    7}
  19. Agora você pode contar a seus amigos que tem usado AI em um de seus programas. Parabéns!

Encontre as melhores correspondências

A incorporação que passamos a receber da Camarock pode ser usada para encontrar as melhores correspondências entre as Celebridades em nosso banco de dados de dados. Se você quiser testar o código com seu próprio banco de dados de dados e demonstrar à sua família que seus filhos são mais parecidos com o seu lado da família, Go até sua biblioteca de fotos e escolha algumas fotos de todos os membros. Quanto mais, melhor. Em seguida, você pode executá-los por meio do código anterior para obter as incorporações correspondentes e armazená-los juntos no MongoDB Atlas – o cluster gratuito será suficiente – cada um em um documento diferente.
  1. Como vamos consultar o banco de dados de dados que contém as imagens de estrelas (ou da sua família) e suas incorporações, devemos armazenar o URI no Atlas cluster incluindo as credenciais necessárias. Vamos ter um arquivo.env com os dados do seu cluster (não copie/use este).
  2. Há um módulo que pode nos ajudar a obter essa configuração do arquivo ou do ambiente.
    1go get github.com/joho/godotenv
  3. Carregamos o módulo no início da função main.
    1var uri string
    2err := godotenv.Load()
    3if err != nil {
    4 log.Fatal("Unable to load .env file")
    5}
    6if uri = os.Getenv("MONGODB_URI"); uri == "" {
    7 log.Fatal("You must set your 'MONGODB_URI' environment variable. See\n\t https://docs.mongodb.com/drivers/go/current/usage-examples/")
    8}
  4. Agora, vamos adicionar o driver MongoDB e usaremos a versão 2.0.
    1go get go.mongodb.org/mongo-driver/v2
  5. Estabeleceremos uma conexão com o cluster Atlas e a disponibilizaremos para o manipulador HTTP. Começamos adicionando outro campo à estruturaApp.
    1client *mongo.Client
  6. E inicializamos em uma função privada.
    1func newDBClient(uri string) (*mongo.Client, error) {
    2 // Use the SetServerAPIOptions() method to set the Stable API version to 1
    3 serverAPI := options.ServerAPI(options.ServerAPIVersion1)
    4 opts := options.Client().ApplyURI(uri).SetServerAPIOptions(serverAPI)
    5 // Create a new client and connect to the server
    6 client, err := mongo.Connect(opts)
    7 if err != nil {
    8 return nil, err
    9 }
    10
    11 return client, nil
    12}
  7. Adicionamos um parâmetro URI ao construtor.
    1func NewApp(ctx context.Context, uri string) (*App, error) {
  8. E passamos o URI que obtivemos do arquivo.env ou do ambiente para o construtor.
    1app, err := NewApp(ctx, uri)
  9. Nós o usamos no construtor.
    1client, err := newDBClient(uri)
    2if err != nil {
    3 log.Println("ERR: connecting to MongoDB cluster:", err)
    4 return nil, err
    5}
    6
    7return &App{
    8 client: client,
    9 config: cfg,
    10 bedrock: bedrockClient,
    11}, nil
  10. Queremos ter certeza de que esse cliente está fechado corretamente quando terminarmos, por isso vamos criar outro método que lidará com isso.
    1func (app *App) Close() {
    2 if err := app.client.Disconnect(context.Background()); err != nil {
    3 panic(err)
    4 }
    5}
  11. O uso defer garantirá que ele seja executado antes de fechar o aplicação. Então, fazemo-lo logo após inicializar o aplicativo.
    1defer func() {
    2 app.Close()
    3}()
  12. Como fizemos nas etapas anteriores, definimos um novo método privado para encontrar as imagens no banco de banco de dados.
    1func (app App) findSimilarImages(ctx context.Context, embedding []float64) ([]string, error) {
    2}
  13. Neste método, obtemos uma referência à coleção que contém os documentos com imagens e incorporações.
    1// Get celeb image collection
    2imgCollection := app.client.Database("celebrity_matcher").Collection("celeb_images")
  14. Uma das funcionalidades mais legais do MongoDB é a capacidade de realizar e refinar pesquisas por meio de diferentes etapas: o pipeline de agregação . O resultado de uma etapa é a entrada para a próxima, e uma das possíveis etapas é usar a pesquisa vetorial.
    1// Aggregation pipeline to get the 3 closest images to the given embedding.
    2vectorSchStage := bson.D{{"$vectorSearch", bson.D{{"index", "vector_index"},
    3 {"path", "embeddings"},
    4 {"queryVector", embedding},
    5 {"numCandidates", 15},
    6 {"limit", 3}}}}
  15. O segundo estágio se encarregará de escolher apenas os campos relevantes a partir dos resultados. Para isso, usamos projeções.
    1projectStage := bson.D{{"$project", bson.D{{"image", 1}}}}
  16. O pipeline de agregação é o resultado de colocar esses estágios em uma lista ordenada.
    1pipeline := mongo.Pipeline{vectorSchStage, projectStage}
  17. E o usamos para fazer a query que retorna um cursor.
    1// Make query
    2imgCursor, err := imgCollection.Aggregate(ctx, pipeline)
    3if err != nil {
    4 return nil, fmt.Errorf("failed to get similar images from the database: %w", err)
    5}
  18. O cursor pode ser usado para obter as imagens reais.
    1// Get all the result using the cursor
    2similarImgs := []struct {
    3 Id bson.ObjectID `bson:"_id,omitempty"`
    4 Image string `bson:"image"`
    5}{}
    6if err = imgCursor.All(ctx, &similarImgs); err != nil {
    7 return nil, fmt.Errorf("failed to get similar images from the database: %w", err)
    8}
  19. E as imagens são processadas para também estar no formato necessário com a função que criamos antes e retornamos.
    1// Return just the standardized images in an array
    2var images []string
    3var stdImage *string
    4for _, item := range similarImgs {
    5 stdImage, err = standardizeImage(item.Image)
    6 if err != nil {
    7 return nil, fmt.Errorf("failed to standardize similar images: %w", err)
    8 }
    9 images = append(images, *stdImage)
    10}
    11return images, nil
  20. Com a nossa função para obter as melhores correspondências do banco de banco de dados , podemos usá-la a partir do nosso manipulador.
    1// Find similar images using vector search in MongoDB
    2images, err := app.findSimilarImages(r.Context(), embedding)
    3if err != nil {
    4 log.Println("ERR:", err)
    5 http.Error(w, "Error getting similar images", http.StatusInternalServerError)
    6 return
    7}
  21. Outra etapa resolvida. Viva!

Explique-se

Obter imagens semelhantes é bastante interessante, mas fazer com que o sistema explique o motivo é ainda mais tentador. Vamos usar outro modelo, Classe, e aproveitar a natureza conversacional e multimodal para obter essa explicação.
  1. Não precisamos de mais dependências em nosso aplicativo, pois já temos um cliente Cama do Rock. Em seguida, como nas etapas anteriores, vamos adicionar um novo método privado que lidará com essa funcionalidade.
    1// https://docs.aws.amazon.com/bedrock/latest/userguide/model-parameters-anthropic-claude-messages.html
    2func (app App) getImageSimilaritiesDescription(ctx context.Context, imgB64 string, similarImgB64 []string) (*string, error) {
    3}
  2. Dentro dele, declaramos uma constante com o nome do modelo que utilizaremos.
    1const claude3SonnetV1ModelId string = "anthropic.claude-3-sonnet-20240229-v1:0"
  3. E as estruturas que usaremos para interagir com a API de mensagens.
    1type ClaudeBodyMsgSource struct {
    2 Type string `json:"type"`
    3 MediaType *string `json:"media_type,omitempty"`
    4 Data *string `json:"data,omitempty"`
    5}
    6type ClaudeBodyMsgContent struct {
    7 Type string `json:"type"`
    8 Source *ClaudeBodyMsgSource `json:"source,omitempty"`
    9 Text *string `json:"text,omitempty"`
    10}
    11type ClaudeBodyMsg struct {
    12 Role string `json:"role"`
    13 Content []ClaudeBodyMsgContent `json:"content"`
    14}
    15type ClaudeBody struct {
    16 AnthropicVersion string `json:"anthropic_version"`
    17 MaxTokens int `json:"max_tokens"`
    18 System string `json:"system"`
    19 Messages []ClaudeBodyMsg `json:"messages"`
    20}
  4. Em seguida, criamos uma instância com os dados desejados.
    1// Prepare the request to bedrock
    2const mediaTypeImage = "image/jpeg"
    3prompt := "Please let the user know how their first image is similar to the other 3 and which one is the most similar?"
    4payload := ClaudeBody{
    5 AnthropicVersion: "bedrock-2023-05-31",
    6 MaxTokens: 1000,
    7 System: "Please act as face comparison analyzer.",
    8 Messages: []ClaudeBodyMsg{
    9 {
    10 Role: "user",
    11 Content: []ClaudeBodyMsgContent{
    12 {
    13 Type: "image",
    14 Source: &ClaudeBodyMsgSource{
    15 Type: "base64",
    16 MediaType: aws.String(mediaTypeImage),
    17 Data: &imgB64,
    18 },
    19 },
    20 {
    21 Type: "image",
    22 Source: &ClaudeBodyMsgSource{
    23 Type: "base64",
    24 MediaType: aws.String(mediaTypeImage),
    25 Data: &similarImgB64[0],
    26 },
    27 },
    28 {
    29 Type: "image",
    30 Source: &ClaudeBodyMsgSource{
    31 Type: "base64",
    32 MediaType: aws.String(mediaTypeImage),
    33 Data: &similarImgB64[1],
    34 },
    35 },
    36 {
    37 Type: "image",
    38 Source: &ClaudeBodyMsgSource{
    39 Type: "base64",
    40 MediaType: aws.String(mediaTypeImage),
    41 Data: &similarImgB64[2],
    42 },
    43 },
    44 {
    45 Type: "text",
    46 Text: &prompt,
    47 },
    48 },
    49 },
    50 },
    51}
  5. Como na etapa anterior em que trabalhei com a Cama de Rock, vamos colocar todos esses dados serializados no campo Body da solicitação.
    1bedrockBody, err := json.Marshal(payload)
    2if err != nil {
    3 return nil, fmt.Errorf("failed to get embedding from bedrock: %w", err)
    4}
    5bedrockReq := bedrockruntime.InvokeModelInput{
    6 ModelId: aws.String(claude3SonnetV1ModelId),
    7 Body: bedrockBody,
    8 ContentType: aws.String(contentTypeJson),
    9 Accept: aws.String(contentTypeJson),
    10}
  6. Podemos usar a varinha Mágica e invocar o modelo.
    1// Invoke the model with the request
    2bedrockResp, err := app.bedrock.InvokeModel(ctx, &bedrockReq)
    3if err != nil {
    4 return nil, fmt.Errorf("failed to get embedding from bedrock: %w", err)
    5}
  7. E extraímos a explicação usando GJSON como fizemos antes e a devolvemos.
    1description := gjson.GetBytes(bedrockResp.Body, "content.0.text").String()
    2
    3return &description, nil
  8. Agora, os principais podem usar esse método e obter a descrição explicando como eles se parecem.
    1description, err := app.getImageSimilaritiesDescription(r.Context(), *stdImage, images)
    2if err != nil {
    3 log.Println("ERR: failed to describe similarities with images", err)
    4 http.Error(w, "Error describing similarities with images", http.StatusInternalServerError)
    5 return
    6}
  9. E é assim que você faz Mágica. Bem, na verdade, é assim que você usa a inferência de um modelo de AI .

Retorne os resultados para o frontend

Não resta muito. Temos que envolver o presente e dá-lo ao usuário. Vamos terminar triunfantemente.
  1. Dentro do manipulador, definimos a estrutura que será usada para serializar a resposta como JSON.
    1type CelebMatchResponse struct {
    2 Description string `json:"description"`
    3 Images []string `json:"images"`
    4}
  2. Usamos essa estrutura para criar uma instância que contém a descrição e as imagens.
    1response := CelebMatchResponse{
    2 Description: *description,
    3 Images: images,
    4}
  3. E escreva a resposta do manipulador HTTP.
    1jData, err := json.Marshal(response)
    2if err != nil {
    3 log.Fatalln("error serializing json", err)
    4}
    5// Set response headers and return JSON
    6w.Header().Set("Content-Type", contentTypeJson)
    7w.Header().Set("Content-Length", strconv.Itoa(len(jData)))
    8w.WriteHeader(http.StatusOK)
    9w.Write(jData)
  4. Dê um presente a si mesmo e comemore alto. Você está oficialmente usando a AI e, em vez de algumas das disparates que às vezes você vê, criamos algo que é útil. Sem mais discussões com a sua família-a-lei. . . .

Conclusão

Desenvolvemos um servidor backend totalmente operacional que usa AI e pesquisa vetorial no MongoDB. Usamos o Go e escrevemos o código inteiro do zero. Esperam que este exemplo registre alguns dos casos de uso de AI e como ela pode ser usada em um aplicação real. E, finalmente, você pode confirmar que você e aquele ator/atriz/atriz são gêmeas quase idênticas. É um dia.
Se você decidir tentar isso, não deixe de nos contar como foi, na MongoDB Developer Community.
Mantenha-se atento. Hackeie seu código. Até a próxima!
Principais comentários nos fóruns
Ainda não há comentários sobre este artigo.
Iniciar a conversa

Ícone do FacebookÍcone do Twitterícone do linkedin
Avalie esse Tutorial
star-empty
star-empty
star-empty
star-empty
star-empty
Relacionado
Exemplo de código

myLeG


Jul 07, 2022 | 1 min read
Tutorial

Utilizar Globbing e Proveniência de Coleção no Data Federation


Jun 28, 2023 | 5 min read
Artigo

Cluster global multinuvem do Atlas: sempre disponível, até se o mundo acabar


Sep 23, 2022 | 4 min read
Tutorial

Como colocar dados do MongoDB em Parquet em 10 segundos ou menos


Jun 28, 2023 | 5 min read
Sumário
  • Um problema do mundo real