# Vector Search + RAG — EF Core 10 + pgvector (.NET 10)

> **Scope:** blazor-azure
> **Layer:** 2 (on keyword)
> **Keywords:** postgres, pgvector, vector, embedding, search, rag, ai
> **Load When:** vector search or rag keywords detected

EF Core 10 (.NET 10) com PostgreSQL + extensão `pgvector` para vector search em workloads de AI. Para a modelagem da coluna `vector`, índices `ivfflat`/`hnsw` e os operadores de distância do `pgvector`, ver `infrastructure/neon/neon-pgvector.md` — este standard cobre o pipeline RAG end-to-end (embeddings, search, geração) sobre essa base.

---

## 🎯 O Que É Vector Search?

**Vector search** permite buscar dados por **similaridade semântica** ao invés de correspondência exata de texto.

### Conceito

```
Texto → Embedding (vetor de números) → Busca por similaridade
```

**Exemplo:**
- Query: "Como resetar senha?"
- Documento 1: "Tutorial de recuperação de senha" ← **Match semântico!**
- Documento 2: "Alterar credenciais de acesso" ← **Match semântico!**
- Documento 3: "Configurar email" ← Não match

### Casos de Uso

| Caso de Uso | Descrição |
|-------------|-----------|
| **RAG (Retrieval-Augmented Generation)** | Buscar documentos relevantes para enviar ao LLM |
| **Busca semântica** | Encontrar conteúdo similar sem keywords exatas |
| **Recomendações** | Sugerir produtos/artigos similares |
| **Deduplicação** | Identificar conteúdo duplicado |

---

## 📦 Setup

### 1. Packages Necessários

```xml
<!-- .csproj -->
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.0" />
<PackageReference Include="Pgvector.EntityFrameworkCore" Version="0.2.2" />
<PackageReference Include="Microsoft.Extensions.AI" Version="9.9.1" />
```

### 2. PostgreSQL + pgvector Requerido

**Requisito:** PostgreSQL com a extensão `pgvector` instalada (`CREATE EXTENSION vector;`).
Neon, Supabase e Azure Database for PostgreSQL já trazem `pgvector` disponível.

**Nota:** habilite a extensão via migration EF Core — `migrationBuilder.Sql("CREATE EXTENSION IF NOT EXISTS vector;")` — antes de criar as colunas de vetor.

---

## 🗄️ Modelo de Dados com Vectors

### Entidade com Embedding

```csharp
using Microsoft.EntityFrameworkCore;
using Pgvector;

public class Document
{
    public int Id { get; set; }
    public string Title { get; set; } = null!;
    public string Content { get; set; } = null!;
    public DateTime CreatedAt { get; set; } = DateTime.UtcNow;

    // Vector embedding (1536 dimensões para text-embedding-3-small).
    // O tipo `Vector` vem do pacote Pgvector — EF Core não mapeia `float[]`.
    public Vector Embedding { get; set; } = null!;
}
```

### DbContext Configuração

```csharp
public class AppDbContext : DbContext
{
    public DbSet<Document> Documents { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        // Registra a extensão pgvector — gera `CREATE EXTENSION vector` na migration
        modelBuilder.HasPostgresExtension("vector");

        modelBuilder.Entity<Document>(entity =>
        {
            entity.HasKey(d => d.Id);

            // Configurar coluna de vector
            entity.Property(d => d.Embedding)
                .HasColumnType("vector(1536)") // 1536 = dimensões do embedding
                .IsRequired();

            // Índice de vector para performance — operador de distância cosseno
            entity.HasIndex(d => d.Embedding)
                .HasMethod("hnsw") // ou "ivfflat"
                .HasOperators("vector_cosine_ops")
                .HasStorageParameter("m", 16)
                .HasStorageParameter("ef_construction", 64);
        });
    }
}
```

> O provider Npgsql precisa de `UseVector()` no `NpgsqlDataSourceBuilder` (ou
> no `UseNpgsql(...)`) para mapear o tipo `vector` — ver a configuração do
> `Program.cs` abaixo e `infrastructure/neon/neon-pgvector.md`.

### Migration

```bash
dotnet ef migrations add AddVectorSearch
dotnet ef database update
```

**SQL Gerado:**
```sql
CREATE EXTENSION IF NOT EXISTS vector;
ALTER TABLE "Documents" ADD "Embedding" vector(1536) NOT NULL;
CREATE INDEX "IX_Documents_Embedding" ON "Documents"
  USING hnsw ("Embedding" vector_cosine_ops) WITH (m = 16, ef_construction = 64);
```

---

## 🔢 Gerando Embeddings

### Serviço de Embedding

```csharp
using Microsoft.Extensions.AI;
using Pgvector;

public interface IEmbeddingService
{
    Task<Vector> GenerateEmbeddingAsync(string text, CancellationToken ct = default);
}

public class EmbeddingService : IEmbeddingService
{
    private readonly IEmbeddingGenerator<string, Embedding<float>> _embeddingGenerator;

    public EmbeddingService(IEmbeddingGenerator<string, Embedding<float>> embeddingGenerator)
    {
        _embeddingGenerator = embeddingGenerator;
    }

    public async Task<Vector> GenerateEmbeddingAsync(
        string text,
        CancellationToken ct = default)
    {
        // GenerateVectorAsync devolve o ReadOnlyMemory<float> diretamente
        var vector = await _embeddingGenerator.GenerateVectorAsync(text, cancellationToken: ct);
        return new Vector(vector);
    }
}
```

### Configuração no Program.cs

```csharp
using Azure.AI.OpenAI;
using Microsoft.Extensions.AI;
using System.ClientModel;

// DbContext com o type handler do pgvector registrado
builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseNpgsql(
        builder.Configuration.GetConnectionString("Default"),
        npgsql => npgsql.UseVector()));

// Embedding generator do Azure OpenAI exposto como IEmbeddingGenerator
builder.Services.AddSingleton<IEmbeddingGenerator<string, Embedding<float>>>(sp =>
{
    var config = sp.GetRequiredService<IConfiguration>();

    var azureClient = new AzureOpenAIClient(
        new Uri(config["AzureOpenAI:Endpoint"]!),
        new ApiKeyCredential(config["AzureOpenAI:ApiKey"]!));

    return azureClient
        .GetEmbeddingClient("text-embedding-3-small") // 1536 dimensões
        .AsIEmbeddingGenerator();
});

builder.Services.AddScoped<IEmbeddingService, EmbeddingService>();
```

---

## 🔍 Vector Search com EF Core 10

### Query de Similaridade

```csharp
using Microsoft.EntityFrameworkCore;

public class DocumentSearchService
{
    private readonly AppDbContext _context;
    private readonly IEmbeddingService _embeddingService;

    public DocumentSearchService(
        AppDbContext context,
        IEmbeddingService embeddingService)
    {
        _context = context;
        _embeddingService = embeddingService;
    }

    public async Task<List<DocumentSearchResult>> SearchAsync(
        string query,
        int limit = 5,
        CancellationToken ct = default)
    {
        // 1. Gerar embedding da query
        var queryEmbedding = await _embeddingService.GenerateEmbeddingAsync(query, ct);

        // 2. Buscar documentos similares — EF.Functions.CosineDistance traduz para
        //    o operador `<=>` do pgvector e usa o índice HNSW vector_cosine_ops
        var results = await _context.Documents
            .Select(d => new DocumentSearchResult
            {
                Document = d,
                // Distância cosseno (menor = mais similar)
                Distance = EF.Functions.CosineDistance(d.Embedding, queryEmbedding)
            })
            .OrderBy(r => r.Distance)
            .Take(limit)
            .ToListAsync(ct);

        return results;
    }
}

public class DocumentSearchResult
{
    public Document Document { get; set; } = null!;
    public double Distance { get; set; }
    public double Similarity => 1 - Distance; // Converter distância em similaridade
}
```

### Funções de Distância

O `Pgvector.EntityFrameworkCore` expõe as funções de distância como métodos
`EF.Functions.*`, cada uma traduzida para o operador correspondente do `pgvector`:

| Função (`EF.Functions.*`) | Operador pgvector | Quando Usar |
|---------------------------|-------------------|-------------|
| `CosineDistance` | `<=>` | **Recomendado** para texto / embeddings normalizados |
| `L2Distance` | `<->` | Distância euclidiana — dados numéricos |
| `MaxInnerProduct` | `<#>` | Produto interno negativo — vetores normalizados |

```csharp
EF.Functions.CosineDistance(d.Embedding, queryEmbedding)
```

> O `HasOperators` do índice precisa casar com a função usada na query —
> `vector_cosine_ops` para `CosineDistance`, `vector_l2_ops` para `L2Distance`,
> `vector_ip_ops` para `MaxInnerProduct`. Operador e índice divergentes fazem o
> Postgres ignorar o índice e cair em scan sequencial.

---

## 🤖 RAG Pattern Completo

### Implementação RAG com Agent Framework

```csharp
using Microsoft.Extensions.AI;

public interface IDocumentAssistantAgent
{
    Task<string> AskQuestionAsync(string question, CancellationToken ct = default);
}

public class DocumentAssistantAgent : IDocumentAssistantAgent
{
    private readonly IChatClient _chatClient;
    private readonly DocumentSearchService _searchService;
    private readonly ILogger<DocumentAssistantAgent> _logger;

    public DocumentAssistantAgent(
        IChatClient chatClient,
        DocumentSearchService searchService,
        ILogger<DocumentAssistantAgent> logger)
    {
        _chatClient = chatClient;
        _searchService = searchService;
        _logger = logger;
    }

    public async Task<string> AskQuestionAsync(string question, CancellationToken ct = default)
    {
        // 1. Retrieval: Buscar documentos relevantes
        var relevantDocs = await _searchService.SearchAsync(question, limit: 3, ct);

        _logger.LogInformation(
            "Encontrados {Count} documentos relevantes para: {Question}",
            relevantDocs.Count,
            question
        );

        // 2. Construir contexto
        var context = string.Join("\n\n", relevantDocs.Select(r =>
            $"[Documento {r.Document.Id} - Similaridade: {r.Similarity:P0}]\n" +
            $"Título: {r.Document.Title}\n" +
            $"Conteúdo: {r.Document.Content}"
        ));

        // 3. Augmentation: Criar agente com contexto
        // VERIFY: _chatClient.CreateAgent / agent.RunAsync — superfície MAF; alinhar com o MAF Framework Revamp em andamento
        var agent = _chatClient.CreateAgent(
            instructions: """
                Você é um assistente que responde perguntas baseado em documentos fornecidos.

                Regras:
                1. Use APENAS informações dos documentos fornecidos
                2. Se a informação não estiver nos documentos, diga "Não encontrei informação sobre isso"
                3. Cite o número do documento ao responder
                4. Seja conciso e objetivo

                Documentos disponíveis:
                """ + context,
            name: "DocumentAssistant"
        );

        // 4. Generation: Gerar resposta
        var response = await agent.RunAsync(question, cancellationToken: ct);

        return response.Content;
    }
}
```

### Uso no Blazor

```razor
@page "/ask"
@inject IDocumentAssistantAgent Assistant

<h3>Assistente de Documentos</h3>

<EditForm Model="_input" OnValidSubmit="AskQuestion">
    <InputText @bind-Value="_input.Question" placeholder="Faça uma pergunta..." />
    <button type="submit" disabled="@_isLoading">Perguntar</button>
</EditForm>

@if (_isLoading)
{
    <p>Buscando resposta...</p>
}
else if (!string.IsNullOrEmpty(_answer))
{
    <div class="answer">
        <strong>Resposta:</strong>
        <p>@_answer</p>
    </div>
}

@code {
    private QuestionInput _input = new();
    private string _answer = "";
    private bool _isLoading;

    private async Task AskQuestion()
    {
        _isLoading = true;
        _answer = "";

        try
        {
            _answer = await Assistant.AskQuestionAsync(_input.Question);
        }
        finally
        {
            _isLoading = false;
        }
    }

    public class QuestionInput
    {
        public string Question { get; set; } = "";
    }
}
```

---

## 📥 Indexação de Documentos

### Serviço de Indexação

```csharp
public class DocumentIndexingService
{
    private readonly AppDbContext _context;
    private readonly IEmbeddingService _embeddingService;

    public async Task IndexDocumentAsync(
        string title,
        string content,
        CancellationToken ct = default)
    {
        // 1. Gerar embedding do conteúdo
        var embedding = await _embeddingService.GenerateEmbeddingAsync(
            $"{title}\n{content}", // Combinar título e conteúdo
            ct
        );

        // 2. Criar documento
        var document = new Document
        {
            Title = title,
            Content = content,
            Embedding = embedding,
            CreatedAt = DateTime.UtcNow
        };

        // 3. Salvar no banco
        _context.Documents.Add(document);
        await _context.SaveChangesAsync(ct);
    }

    public async Task BulkIndexAsync(
        List<(string Title, string Content)> documents,
        CancellationToken ct = default)
    {
        foreach (var (title, content) in documents)
        {
            await IndexDocumentAsync(title, content, ct);
        }
    }
}
```

### Job de Indexação com Hangfire

```csharp
public class DocumentIndexingJob
{
    private readonly DocumentIndexingService _indexingService;
    private readonly IDocumentProvider _documentProvider;

    public async Task IndexAllDocumentsAsync()
    {
        // Buscar documentos de fonte externa (API, arquivos, etc.)
        var documents = await _documentProvider.GetAllDocumentsAsync();

        await _indexingService.BulkIndexAsync(documents);
    }
}

// Program.cs - Agendar job diário
RecurringJob.AddOrUpdate<DocumentIndexingJob>(
    "index-documents",
    job => job.IndexAllDocumentsAsync(),
    Cron.Daily
);
```

---

## 📊 Índices de Performance

### Tipos de Índices

| Tipo | Descrição | Performance | Precisão |
|------|-----------|-------------|----------|
| **IVFFlat** | Inverted File + Flat compression | Boa | Alta |
| **HNSW** | Hierarchical Navigable Small World | Excelente | Alta |

### Configuração IVFFlat

```csharp
entity.HasIndex(d => d.Embedding)
    .HasMethod("ivfflat")
    .HasOperators("vector_cosine_ops")
    .HasStorageParameter("lists", 100); // Ajustar conforme dataset
```

**Recomendação de `lists`:**
- Pequeno dataset (<10k docs): `lists = 50`
- Médio dataset (10k-100k): `lists = 100`
- Grande dataset (>100k): `lists = 500+`

> IVFFlat só fica eficiente depois de o índice ser populado — crie-o **após**
> carregar os dados, ou reindexe (`REINDEX`) quando o dataset crescer.

### Configuração HNSW

```csharp
entity.HasIndex(d => d.Embedding)
    .HasMethod("hnsw")
    .HasOperators("vector_cosine_ops")
    .HasStorageParameter("m", 16)
    .HasStorageParameter("ef_construction", 64);
```

**Parâmetros:**
- `m`: número de conexões por nó (padrão: 16)
- `ef_construction`: qualidade do índice na construção (padrão: 64)

HNSW é o padrão recomendado: melhor recall e não exige dados pré-carregados.

---

## 💰 Custos

### Embedding Generation

| Modelo | Dimensões | Custo |
|--------|-----------|-------|
| text-embedding-3-small | 1536 | $0.02 / 1M tokens |
| text-embedding-3-large | 3072 | $0.13 / 1M tokens |

**Recomendação:** Use `text-embedding-3-small` (melhor custo-benefício).

### Storage

| Dimensões | Tamanho por Documento | 10k Docs | 100k Docs |
|-----------|-----------------------|----------|-----------|
| 1536 | ~6 KB | ~60 MB | ~600 MB |
| 3072 | ~12 KB | ~120 MB | ~1.2 GB |

---

## ✅ Checklist de Implementação

- [ ] EF Core 10 + `Npgsql.EntityFrameworkCore.PostgreSQL` + `Pgvector.EntityFrameworkCore` instalados
- [ ] PostgreSQL com extensão `pgvector` (`HasPostgresExtension("vector")`)
- [ ] `UseVector()` chamado no `UseNpgsql(...)`
- [ ] Entidade com propriedade `Pgvector.Vector`
- [ ] Índice de vector criado (`hnsw` ou `ivfflat`) com `HasOperators("vector_cosine_ops")`
- [ ] `IEmbeddingService` configurado
- [ ] Serviço de search implementado com `EF.Functions.CosineDistance`
- [ ] RAG pattern com Agent Framework
- [ ] Job de indexação configurado
- [ ] Testes de similaridade funcionando

---

## 🐛 Troubleshooting

### Erro: "type \"vector\" does not exist"

**Causa:** extensão `pgvector` não instalada no banco.

**Solução:** rode `CREATE EXTENSION vector;` — ou registre `HasPostgresExtension("vector")`
no `OnModelCreating` para que a migration EF Core a crie automaticamente.

### Erro: "column ... is of type vector but expression is of type ..."

**Causa:** `UseVector()` não foi chamado — o Npgsql não mapeia o tipo `vector`.

**Solução:** chame `npgsql.UseVector()` no `UseNpgsql(...)` (ou `UseVector()` no
`NpgsqlDataSourceBuilder`).

### Performance lenta em queries

**Causa:** índice não criado, ou operador da query difere do `HasOperators` do índice.

**Solução:**
- Verifique se o índice existe: `SELECT * FROM pg_indexes WHERE indexname LIKE '%Embedding%'`
- Confirme com `EXPLAIN ANALYZE` que a query usa o índice (e não `Seq Scan`)
- Garanta que o operador casa: `CosineDistance` ↔ `vector_cosine_ops`
- Ajuste parâmetros `m`/`ef_construction` (HNSW) ou `lists` (IVFFlat)

### Embeddings com dimensões erradas

**Causa:** Modelo de embedding diferente do esperado.

**Solução:**
- `text-embedding-3-small` → 1536 dimensões
- `text-embedding-3-large` → 3072 dimensões
- Atualize `vector(N)` na migration

---

## 📚 Referências

- [pgvector](https://github.com/pgvector/pgvector) — extensão PostgreSQL para vector search
- [Pgvector.EntityFrameworkCore](https://github.com/pgvector/pgvector-dotnet) — type handler + `EF.Functions` de distância
- [Npgsql EF Core Provider](https://www.npgsql.org/efcore/) — `UseVector()`, `HasPostgresExtension`
- [Microsoft.Extensions.AI - Embeddings](https://learn.microsoft.com/dotnet/ai/microsoft-extensions-ai)
- [OpenAI Embeddings](https://platform.openai.com/docs/guides/embeddings)
- `infrastructure/neon/neon-pgvector.md` — modelagem da coluna, índices e operadores pgvector

---

*MORPH-SPEC by Polymorphism Tech*
