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

> **Novidade .NET 10:** EF Core 10 suporta vector search nativo no Azure SQL e SQL Server para workloads de AI.

---

## 🎯 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="Microsoft.EntityFrameworkCore.SqlServer" Version="10.0.0" />
<PackageReference Include="Microsoft.Extensions.AI.Embeddings" Version="1.0.0" />
```

### 2. Azure SQL / SQL Server Requerido

**Requisito:** Azure SQL Database ou SQL Server 2022+

**Nota:** Vector search não funciona em LocalDB.

---

## 🗄️ Modelo de Dados com Vectors

### Entidade com Embedding

```csharp
using Microsoft.EntityFrameworkCore;
using System.Numerics.Tensors;

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)
    public ReadOnlyMemory<float> Embedding { get; set; }
}
```

### DbContext Configuração

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

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        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
            entity.HasIndex(d => d.Embedding)
                .HasMethod("ivfflat") // ou "hnsw"
                .HasOptions("lists = 100");
        });
    }
}
```

### Migration

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

**SQL Gerado:**
```sql
ALTER TABLE Documents ADD Embedding vector(1536) NOT NULL;
CREATE INDEX IX_Documents_Embedding ON Documents USING ivfflat (Embedding) WITH (lists = 100);
```

---

## 🔢 Gerando Embeddings

### Serviço de Embedding

```csharp
using Microsoft.Extensions.AI;

public interface IEmbeddingService
{
    Task<ReadOnlyMemory<float>> 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<ReadOnlyMemory<float>> GenerateEmbeddingAsync(
        string text,
        CancellationToken ct = default)
    {
        var embeddings = await _embeddingGenerator.GenerateAsync([text], cancellationToken: ct);
        return embeddings[0].Vector;
    }
}
```

### Configuração no Program.cs

```csharp
using Microsoft.Extensions.AI;

builder.Services.AddSingleton<IEmbeddingGenerator<string, Embedding<float>>>(sp =>
{
    var config = sp.GetRequiredService<IConfiguration>();

    return new EmbeddingGenerator(
        model: "text-embedding-3-small", // 1536 dimensões
        credential: new ApiKeyCredential(config["AzureOpenAI:ApiKey"]!),
        endpoint: new Uri(config["AzureOpenAI:Endpoint"]!)
    );
});

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
        var results = await _context.Documents
            .Select(d => new DocumentSearchResult
            {
                Document = d,
                // Calcular distância (menor = mais similar)
                Distance = EF.Functions.VectorDistance(d.Embedding, queryEmbedding, DistanceFunction.Cosine)
            })
            .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

| Função | Descrição | Quando Usar |
|--------|-----------|-------------|
| `Cosine` | Distância cosseno (0-2) | **Recomendado** para texto |
| `Euclidean` | Distância euclidiana | Dados numéricos |
| `DotProduct` | Produto escalar | Vetores normalizados |

```csharp
EF.Functions.VectorDistance(vector1, vector2, DistanceFunction.Cosine)
```

---

## 🤖 RAG Pattern Completo

### Implementação RAG com Agent Framework

```csharp
using Microsoft.Agents.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
        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")
    .HasOptions("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+`

### Configuração HNSW

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

**Parâmetros:**
- `m`: Número de conexões (padrão: 16)
- `ef_construction`: Qualidade do índice (padrão: 64)

---

## 💰 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 instalado
- [ ] Azure SQL ou SQL Server 2022+
- [ ] Entidade com propriedade `ReadOnlyMemory<float>`
- [ ] Índice de vector criado (`ivfflat` ou `hnsw`)
- [ ] `IEmbeddingService` configurado
- [ ] Serviço de search implementado
- [ ] RAG pattern com Agent Framework
- [ ] Job de indexação configurado
- [ ] Testes de similaridade funcionando

---

## 🐛 Troubleshooting

### Erro: "vector type not supported"

**Causa:** SQL Server não suporta vectors ou versão antiga.

**Solução:** Use Azure SQL Database ou SQL Server 2022+.

### Performance lenta em queries

**Causa:** Índice não criado ou mal configurado.

**Solução:**
- Verifique se índice foi criado: `SELECT * FROM sys.indexes WHERE name LIKE '%Embedding%'`
- Ajuste parâmetros `lists` (IVFFlat) ou `m` (HNSW)

### 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

- [EF Core 10 - Vector Search](https://learn.microsoft.com/ef/core/what-is-new/ef-core-10.0/whatsnew)
- [Azure SQL Vector Search](https://learn.microsoft.com/azure/azure-sql/database/ai-artificial-intelligence-vector-search)
- [OpenAI Embeddings](https://platform.openai.com/docs/guides/embeddings)
- [RAG Architecture](https://learn.microsoft.com/azure/architecture/ai-ml/guide/rag/rag-solution-design-and-evaluation-guide)

---

*MORPH-SPEC by Polymorphism Tech*
