> ## Content Index
> Fetch the complete content index at: https://www.azurebrasil.cloud/llms.txt
> Use this file to discover other available public pages before exploring further.

# Busca semântica com pgvector, EF Core e Azure OpenAI
- URL: https://www.azurebrasil.cloud/blog/busca-semantica-com-pgvector-ef-core-e-azure-openai/
- Published: 2026-09-04T12:25:41.000Z
- Updated: 2026-09-04T12:25:41.000Z
- Author: Guilherme Govaski

Busca por palavra-chave funciona bem quando o usuário digita exatamente o que está no catálogo. O problema começa quando ele pesquisa *"algo pra correr na chuva"* e o produto no banco se chama *"Tênis de corrida impermeável"*. Nenhuma `LIKE` ou full-text resolve isso com elegância. A saída é transformar texto em vetores (embeddings) e comparar **significado**, não strings.

Neste artigo vamos montar uma Minimal API em [.NET](https://dotnet.microsoft.com/?ref=azurebrasil.cloud) que cadastra produtos, gera embeddings com [Azure OpenAI](https://learn.microsoft.com/azure/ai-foundry/openai/?ref=azurebrasil.cloud) e faz vector search no [PostgreSQL](https://www.postgresql.org/?ref=azurebrasil.cloud) usando a extensão [pgvector](https://github.com/pgvector/pgvector?ref=azurebrasil.cloud) via [Entity Framework Core](https://learn.microsoft.com/ef/core/?ref=azurebrasil.cloud). O código é o mesmo fluxo do projeto demo `SemanticSearchApi`.

Link do GitHub: [semantic-search-pgvector-demo](https://github.com/guigovaski/semantic-search-pgvector-demo?ref=azurebrasil.cloud)

## O que você vai ver

1. Criar o resource e o deployment de embedding no Azure
2. Preparar o PostgreSQL com pgvector via Docker Compose
3. Mapear a coluna `vector(1536)` no EF Core e o índice HNSW
4. Gerar embedding no cadastro do produto
5. Buscar por similaridade com `CosineDistance`

**Pré-requisitos:**

- [.NET 10 SDK](https://dotnet.microsoft.com/download?ref=azurebrasil.cloud)
- [Docker](https://www.docker.com/?ref=azurebrasil.cloud) instalado e rodando
- Assinatura [Azure](https://azure.microsoft.com/?ref=azurebrasil.cloud) com permissão para criar resource de Azure OpenAI
- Noções de EF Core e Minimal APIs

**Stack do exemplo:**

| Peça                                                                                                                                | Versão / valor                          |
| ----------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------- |
| Target framework                                                                                                                    | net10.0                                 |
| [Azure.AI.OpenAI](https://www.nuget.org/packages/Azure.AI.OpenAI?ref=azurebrasil.cloud)                                             | 2.1.0                                   |
| [Npgsql.EntityFrameworkCore.PostgreSQL](https://www.nuget.org/packages/Npgsql.EntityFrameworkCore.PostgreSQL?ref=azurebrasil.cloud) | 10.0.3                                  |
| [Pgvector.EntityFrameworkCore](https://www.nuget.org/packages/Pgvector.EntityFrameworkCore?ref=azurebrasil.cloud)                   | 0.3.0                                   |
| Modelo de embedding                                                                                                                 | text-embedding-3-small (1536 dimensões) |

## Por que vector search

Um **embedding** é um array de números de ponto flutuante que representa o significado de um texto em um espaço de alta dimensão. Textos parecidos ficam “perto” uns dos outros nesse espaço.

Fluxo da aplicação:

1. No **cadastro**, concatena nome + descrição do produto, manda pro modelo de embedding e grava o vetor na coluna `Embedding`
2. Na **busca**, gera o embedding da query do usuário e pede ao banco os produtos com menor distância cosseno em relação a esse vetor

Quem faz a matemática de similaridade é o **pgvector** no PostgreSQL. O EF Core só traduz `CosineDistance` para o operador `<=>` do pgvector.

## 1\. Setup do Azure OpenAI

Precisamos de um resource Azure OpenAI e de um **deployment** do modelo de embedding.

### Criar o resource (portal)

1. Acesse o [Azure portal](https://portal.azure.com/?ref=azurebrasil.cloud)
2. **Create a resource** → busque **Azure OpenAI** → **Create**  
![az-semantic-search-1.png](https://storage.ghost.io/c/00/52/0052dced-0017-4d07-b190-1f5c48e0ab59/content/images/2026/09/az-semantic-search-1.png)
3. Preencha subscription, resource group, região, nome e pricing tier
4. Prossiga com a configuração padrão dos outros passos clicando sempre em **Next** até chegar em **Review + create**
5. Revise e crie o resource  
![az-semantic-search-2.png](https://storage.ghost.io/c/00/52/0052dced-0017-4d07-b190-1f5c48e0ab59/content/images/2026/09/az-semantic-search-2.png)

### Deploy do modelo de embedding

1. Abra o resource no [Microsoft Foundry](https://ai.azure.com/?ref=azurebrasil.cloud) (portal classic, se for o fluxo que você estiver usando)
2. Vá em **Deployments** → **Deploy model** → **Deploy base model**  
![az-semantic-search-3.png](https://storage.ghost.io/c/00/52/0052dced-0017-4d07-b190-1f5c48e0ab59/content/images/2026/09/az-semantic-search-3.png)
3. Escolha **`text-embedding-3-small`**
4. Defina o **deployment name** — no exemplo usamos o mesmo nome do modelo: `text-embedding-3-small`  
![az-semantic-search-4.png](https://storage.ghost.io/c/00/52/0052dced-0017-4d07-b190-1f5c48e0ab59/content/images/2026/09/az-semantic-search-4.png)
5. Confirme o deploy e aguarde o status de sucesso.

Anote:

- **Endpoint base** no formato `https://SEU-RECURSO.openai.azure.com/`
- **API key** do resource
- **Nome do deployment** exatamente como criou

## 2\. Criar e executar o container do PostgreSQL com a extensão pgvector via Docker Compose

Crie o arquivo `docker-compose.yml` na raiz do projeto com o conteúdo abaixo (senha e dbname podem ser alterados, desde que batam com a connection string):

```yaml
version: '3.8'

services:
  db:
    image: pgvector/pgvector:pg16
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: Devenv12345!
      POSTGRES_DB: ecommercedb
    ports:
      - "5432:5432"

```

Execute `docker compose up -d` para subir o container.

## 3\. Projeto e pacotes

```bash
dotnet new web -n SemanticSearchApi
cd SemanticSearchApi

dotnet add package Azure.AI.OpenAI --version 2.1.0
dotnet add package Npgsql.EntityFrameworkCore.PostgreSQL --version 10.0.3
dotnet add package Pgvector.EntityFrameworkCore --version 0.3.0
dotnet add package Microsoft.EntityFrameworkCore.Design --version 10.0.10

```

O pacote `Pgvector.EntityFrameworkCore` puxa o tipo `Vector` e os métodos de distância traduzíveis para SQL (`CosineDistance`, `L2Distance`, etc.).

## 4\. Entidade e DbContext

Crie `AppDbContext.cs`. O ponto crítico: a dimensão da coluna **precisa bater** com a dimensão do modelo. O `text-embedding-3-small` gera vetores com **1536** dimensões por padrão.

No mesmo `OnModelCreating`, habilitamos a extensão `vector` e o índice **HNSW** com `vector_cosine_ops` — o operator class certo para `CosineDistance`. Sem isso, o demo ordena por distância em full scan; com o índice, o pgvector usa nearest neighbor aproximado.

```csharp
using System.ComponentModel.DataAnnotations.Schema;
using Pgvector;
using Microsoft.EntityFrameworkCore;

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; } = string.Empty;
    public string Description { get; set; } = string.Empty;

    // Define a propriedade Embedding como um vetor de 1536 dimensões, que é o tamanho do embedding gerado pelo modelo "text-embedding-3-small" da OpenAI.
    [Column(TypeName = "vector(1536)")]
    public Vector? Embedding { get; set; }
}

public class AppDbContext : DbContext
{
    public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }

    public DbSet<Product> Products { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        // Habilita a extensão do pgvector no banco de dados PostgreSQL
        modelBuilder.HasPostgresExtension("vector");

        modelBuilder.Entity<Product>()
            .HasIndex(p => p.Embedding)
            .HasMethod("hnsw")
            .HasOperators("vector_cosine_ops");
    }
}

```

Sem o `TypeName = "vector(1536)"`, o Npgsql não sabe o tipo exato da coluna. Se você trocar de modelo ou de dimensões, altere o atributo **e** a coluna no banco.

## 5\. Configurar EF Core e Azure OpenAI no Program.cs

Dois detalhes que não podem faltar:

1. `UseVector()` no `UseNpgsql` — registra mapeamento do tipo e os translators de distância
2. Endpoint **base** \+ nome do **deployment** no `GetEmbeddingClient`

Connection string, endpoint, key e nome do deployment vêm do `appsettings.json` (não hardcode no `Program.cs`).

`appsettings.json`:

```json
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "Default": "Host=localhost;Database=ecommercedb;Username=postgres;Password=Devenv12345!"
  },
  "AzureOpenAI": {
    "Endpoint": "https://SEU-RECURSO.openai.azure.com/",
    "Key": "SUA_API_KEY",
    "EmbeddingDeployment": "text-embedding-3-small"
  }
}

```

`Program.cs`:

```csharp
using Azure.AI.OpenAI;
using Microsoft.EntityFrameworkCore;
using Pgvector.EntityFrameworkCore;
using Pgvector;
using System.ClientModel;

var builder = WebApplication.CreateBuilder(args);

var connectionString = builder.Configuration.GetConnectionString("Default")
    ?? throw new InvalidOperationException("Connection string 'Default' não configurada.");

builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseNpgsql(connectionString, o => o.UseVector()));

var openAiEndpoint = builder.Configuration["AzureOpenAI:Endpoint"]
    ?? throw new InvalidOperationException("AzureOpenAI:Endpoint não configurado.");
var openAiKey = builder.Configuration["AzureOpenAI:Key"]
    ?? throw new InvalidOperationException("AzureOpenAI:Key não configurado.");
var embeddingDeploymentName = builder.Configuration["AzureOpenAI:EmbeddingDeployment"]
    ?? throw new InvalidOperationException("AzureOpenAI:EmbeddingDeployment não configurado.");

var aiClient = new AzureOpenAIClient(
    new Uri(openAiEndpoint),
    new ApiKeyCredential(openAiKey));

builder.Services.AddSingleton(aiClient);

var app = builder.Build();

using (var scope = app.Services.CreateScope())
{
    var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
    await db.Database.MigrateAsync();
}

// endpoints abaixo...

app.Run();

```

Crie a migration inicial:

```bash
dotnet ef migrations add Initial
dotnet run

```

A migration gera a extensão `vector` no database, a coluna `Embedding` como `vector(1536)` e o índice HNSW `IX_Products_Embedding` com `vector_cosine_ops`.

## 6\. Cadastro: gerar e persistir o embedding

No POST, montamos um texto estável a partir do produto, pedimos o embedding ao Azure e salvamos o `Vector` junto com nome e descrição.

```csharp
app.MapPost("/api/products", async (CreateProductRequest request, AppDbContext db, AzureOpenAIClient aiClient) =>
{
    if (string.IsNullOrWhiteSpace(request.Name))
        return Results.BadRequest("O nome do produto não pode estar vazio.");

    if (string.IsNullOrWhiteSpace(request.Description))
        return Results.BadRequest("A descrição do produto não pode estar vazia.");

    var embeddingClient = aiClient.GetEmbeddingClient(embeddingDeploymentName);
    var textToEmbed = $"{request.Name}. {request.Description}";
    var embedResponse = await embeddingClient.GenerateEmbeddingAsync(textToEmbed);
    var embedding = new Vector(embedResponse.Value.ToFloats().ToArray());

    var product = new Product
    {
        Name = request.Name.Trim(),
        Description = request.Description.Trim(),
        Embedding = embedding
    };

    db.Products.Add(product);
    await db.SaveChangesAsync();

    return Results.Created($"/api/products/{product.Id}", new
    {
        product.Id,
        product.Name,
        product.Description
    });
});

record CreateProductRequest(string Name, string Description);

```

Por que `Nome. Descrição`? Porque o embedding “enxerga” o texto que você manda. Se na busca o usuário descreve uso ou benefício, vale a pena o cadastro carregar esse contexto na descrição — não só o nome comercial seco.

O SDK devolve `ReadOnlyMemory<float>`; o construtor de `Pgvector.Vector` aceita o `float[]` correspondente.

## 7\. Busca: CosineDistance no LINQ

Aqui está o coração da busca semântica:

```csharp
app.MapGet("/api/products/search", async (string query, AppDbContext db, AzureOpenAIClient aiClient) =>
{
    if (string.IsNullOrWhiteSpace(query))
        return Results.BadRequest("A consulta de pesquisa não pode estar vazia.");

    var embeddingClient = aiClient.GetEmbeddingClient(embeddingDeploymentName);

    var embedResponse = await embeddingClient.GenerateEmbeddingAsync(query);

    var queryVector = new Vector(embedResponse.Value.ToFloats().ToArray());

    const double maxDistance = 0.6;

    var products = await db.Products
        .Where(p => p.Embedding!.CosineDistance(queryVector) < maxDistance)
        .OrderBy(p => p.Embedding!.CosineDistance(queryVector))
        .Select(p => new { p.Name, p.Description })
        .AsNoTracking()
        .ToListAsync();

    return Results.Ok(products);
});

```

O que o EF/pgvector faz:

| C#                                | SQL (pgvector)                             |
| --------------------------------- | ------------------------------------------ |
| CosineDistance(queryVector)       | operador <=>                               |
| OrderBy na distância              | ORDER BY embedding <=> @query              |
| filtro < maxDistance              | descarta resultados semanticamente fracos  |
| índice HNSW + vector\_cosine\_ops | acelera o nearest neighbor no operador <=> |

No pgvector, a **cosine distance** vai de **0** (mesma direção) a **2** (direções opostas). Quanto **menor**, mais parecido. O limiar `0.6` é uma escolha de produto: mais baixo = resultados mais restritos; mais alto = mais recall e mais ruído. Vale calibrar com dados reais do catálogo.

## 8\. Testando o fluxo

Suba a API:

```bash
dotnet run

```

Cadastre alguns produtos:

```bash
curl -X POST http://localhost:5015/api/products -H "Content-Type: application/json" -d '{"name":"Notebook Dell XPS 15","description":"Notebook premium com processador Intel Core i7, 32GB de RAM, SSD 1TB e tela OLED 15.6 polegadas."}'

curl -X POST http://localhost:5015/api/products -H "Content-Type: application/json" -d '{"name":"Fone de Ouvido Sony WH-1000XM5","description":"Headset com cancelamento de ruído ativo, Bluetooth 5.2 e até 30 horas de bateria."}'

curl -X POST http://localhost:5015/api/products -H "Content-Type: application/json" -d '{"name":"Headset HyperX Cloud II","description":"Headset gamer com som surround 7.1, microfone removível e conforto para longas sessões de jogo."}'

curl -X POST http://localhost:5015/api/products -H "Content-Type: application/json" -d '{"name":"Cama Ortopédica para Cachorro","description":"Cama macia e lavável para cães de médio e grande porte, com espuma de memória e capa impermeável."}'

```

Busque por intenção, não por keyword exata:

```bash
curl -G --data-urlencode "query=computador portátil" http://localhost:5015/api/products/search

curl -G --data-urlencode "query=fone com cancelamento de ruído" http://localhost:5015/api/products/search

curl -G --data-urlencode "query=headset para jogos" http://localhost:5015/api/products/search

curl -G --data-urlencode "query=cama para cachorro" http://localhost:5015/api/products/search

```

A expectativa:

- *computador portátil para trabalho* → Notebook Dell XPS 15
- *fone com cancelamento de ruído* → Sony WH-1000XM5
- *headset para jogos* → HyperX Cloud II
- *cama para cachorro* → Cama Ortopédica para Cachorro

Mesmo sem a palavra “notebook” na primeira query, o embedding deve puxar o XPS — e a cama de cachorro não deveria aparecer no meio dos fones.

## Conclusão

Vector search deixa de ser “só pra time de ML” quando você encaixa três peças que já são familiares no dia a dia .NET:

1. **Azure OpenAI** gera o embedding
2. **PostgreSQL + pgvector** armazena e compara vetores (com HNSW para não varrer a tabela inteira)
3. **EF Core + Pgvector.EntityFrameworkCore** expõe isso em LINQ com `CosineDistance`

Com uma Minimal API, um `DbContext` e dois endpoints, você já tem um catálogo que entende *"computador portátil para trabalho"*. O próximo passo natural é calibrar o limiar de distância e enriquecer o texto embutido no cadastro.

## Dicionário

- **Embedding** — representação numérica (vetor) do significado de um texto; textos parecidos produzem vetores próximos.
- **Vector search (busca vetorial)** — recuperar itens pela proximidade dos embeddings, em vez de igualdade de palavras.
- **Cosine distance** — medida de distância baseada no ângulo entre dois vetores; no pgvector, 0 é o mais similar e 2 o mais oposto.
- **pgvector** — extensão do PostgreSQL que adiciona o tipo `vector` e operadores/índices de similaridade.
- **Deployment (Azure OpenAI)** — instância publicada de um modelo no seu resource; o nome do deployment é o identificador usado nas chamadas da API.
- **HNSW** — algoritmo de índice aproximado (grafo) usado pelo pgvector para acelerar nearest neighbor search em escala.
- **Minimal API** — estilo de API no ASP.NET Core em que rotas e handlers são mapeados de forma enxuta, sem controllers obrigatórios.
- **AsNoTracking** — modo do EF Core em que as entidades lidas não entram no change tracker, reduzindo custo em consultas somente leitura.

## Referências

- [pgvector](https://github.com/pgvector/pgvector?ref=azurebrasil.cloud) — extensão PostgreSQL
- [pgvector-dotnet / EF Core](https://github.com/pgvector/pgvector-dotnet?ref=azurebrasil.cloud#entity-framework-core) — pacote .NET e exemplos de distância
- [Azure OpenAI – gerar embeddings](https://learn.microsoft.com/azure/ai-foundry/openai/how-to/embeddings?ref=azurebrasil.cloud)
- [Criar resource e deploy Azure OpenAI](https://learn.microsoft.com/azure/ai-foundry/openai/how-to/create-resource?ref=azurebrasil.cloud)
- [Entity Framework Core](https://learn.microsoft.com/ef/core/?ref=azurebrasil.cloud)
- [Npgsql EF Core provider](https://www.npgsql.org/efcore/?ref=azurebrasil.cloud)
- [ASP.NET Core Minimal APIs](https://learn.microsoft.com/aspnet/core/fundamentals/minimal-apis?ref=azurebrasil.cloud)
- [PostgreSQL](https://www.postgresql.org/?ref=azurebrasil.cloud)