DEV Community

Edson Miziara
Edson Miziara

Posted on

Minha Jornada para Fazer a Busca Vetorial Funcionar em C# com Pgvector e Alibaba-NLP

Olá, comunidade Dev.to!

Recentemente, em um projeto pessoal, mergulhei no mundo da busca semântica e da vetorização de dados em C#. Meu objetivo era claro: usar o PostgreSQL com a extensão Pgvector, combinado com o poderoso modelo de embedding Alibaba-NLP/gte-base-en-v1.5 da Hugging Face.

Parecia simples, mas a jornada revelou alguns desafios interessantes que gostaria de compartilhar. Este post documenta os problemas que enfrentei e, mais importante, como os resolvi.

Parte 1: A Primeira Tentativa e a Biblioteca "Fantasma"
A recomendação mais comum na internet para usar modelos como o gte-base em .NET era a biblioteca SentenceTransformers. A documentação e os exemplos (principalmente em Python) mostravam um uso simples e direto, geralmente instanciando uma classe chamada SentenceTransformer.

Entretanto, após instalar o pacote NuGet e tentar diversas versões, deparei-me com um problema frustrante: a classe SentenceTransformer simplesmente não estava acessível publicamente na versão da biblioteca compatível com meu projeto. A API para C# era fundamentalmente diferente da sua contraparte em Python, e a documentação não deixava isso claro. Cheguei a um beco sem saída.

Parte 2: A Nova Abordagem - Mergulhando no ONNX
Decidi abandonar o SentenceTransformers e partir para uma abordagem mais direta e robusta: usar uma biblioteca que interagisse diretamente com o formato ONNX (Open Neural Network Exchange), que é como o modelo gte-base é disponibilizado.

A escolha foi o ecossistema do OnnxStack, um wrapper poderoso para o Microsoft.ML.OnnxRuntime. A documentação inicial e alguns exemplos que encontrei online também mostravam uma certa forma de usar a biblioteca, mas, novamente, a realidade se mostrou diferente. Os nomes das classes, interfaces e a forma de instanciar os objetos na versão que instalei não batiam com os exemplos.

Foi aí que aprendi a lição mais valiosa: quando a documentação falha, investigue a DLL diretamente.

Parte 3: A Solução - O Código que Realmente Funciona
Usando o "Object Browser" do Visual Studio, analisei as classes e interfaces que a biblioteca OnnxStack.FeatureExtractor realmente me oferecia. Em vez de ficar frustrado, decidi entender as peças que eu tinha em mãos e montar o quebra-cabeça.

O resultado foi um serviço de embedding que funciona perfeitamente.

O Serviço de Vetorização (GteEmbeddingService.cs)
Após a análise, descobri que a melhor forma de instanciar o modelo era através da classe FeatureExtractorModel e que a interface IFeatureExtractor exigia float como TResult para me devolver um IEnumerable no método Transform.

Este é o serviço final que criei para gerar os vetores:

using BioGalactic.API.Services; // Namespace da minha interface IEmbeddingService
using OnnxStack.FeatureExtractor.Common;
using OnnxStack.FeatureExtractor.Pipelines;
using Pgvector;
using System;
using System.IO;
using System.Linq; // Necessário para o método .ToArray()
using System.Threading.Tasks;

public class GteEmbeddingService : IEmbeddingService
{
// A interface, como descoberto na análise, espera 'float' para retornar um IEnumerable
private readonly IFeatureExtractor _featureExtractor;

public GteEmbeddingService()
{
    // O caminho para os arquivos do modelo ONNX que baixei manualmente
    var modelPath = Path.Combine(AppContext.BaseDirectory, "models", "gte-base");

    // A forma correta de criar a instância do modelo nesta versão da lib
    _featureExtractor = (IFeDatureExtractor<float, string>)FeatureExtractorModel.Create(modelPath);
}

public async Task<Vector> GenerateEmbeddingAsync(string text)
{
    // O método 'Transform' é síncrono, então o rodo em uma Task para não bloquear
    var embeddingArray = await Task.Run(() => _featureExtractor.Transform(text).ToArray());

    // Crio o tipo Vector do Pgvector a partir do array de floats
    return new Vector(embeddingArray);
}
Enter fullscreen mode Exit fullscreen mode

}

O Endpoint de Busca na API
Com o serviço de vetorização pronto e funcionando, o passo final foi criar um endpoint na minha API que o utilizasse para buscar os dados já vetorizados no banco de dados.

Usando o Entity Framework Core e a biblioteca Pgvector.EntityFrameworkCore, o endpoint ficou limpo e eficiente:

// No arquivo PassageExtensions (usando Minimal APIs)

//...

group.MapGet("/api/search", async (
[FromQuery] string query,
BiogalacticContext dbContext,
IEmbeddingService embeddingService) =>
{
if (string.IsNullOrWhiteSpace(query))
{
return Results.BadRequest("A query não pode ser vazia.");
}

// 1. Vetoriza o texto da busca em tempo real
var queryVector = await embeddingService.GenerateEmbeddingAsync(query);

// 2. Usa a função L2Distance do Pgvector para encontrar os vizinhos mais próximos
var results = await dbContext.Passages
    .OrderBy(p => p.Embedding.L2Distance(queryVector))
    .Take(10)
    .ToListAsync();

return Results.Ok(results);
Enter fullscreen mode Exit fullscreen mode

});

// ...

Dependências Finais
Para que tudo isso funcione, este é o conjunto final de pacotes NuGet no meu projeto:

Microsoft.ML.OnnxRuntime

Microsoft.ML.OnnxRuntime.Managed

OnnxStack.Core

OnnxStack.FeatureExtractor

Npgsql.EntityFrameworkCore.PostgreSQL

Pgvector.EntityFrameworkCore

Microsoft.EntityFrameworkCore

Microsoft.EntityFrameworkCore.Tools

Microsoft.EntityFrameworkCore.Design

Conclusão
Trabalhar com o ecossistema de IA em C# pode ter seus desafios, especialmente quando se trata de documentação versus a realidade das versões dos pacotes. A maior lição que aprendi foi: não tenha medo de "abrir o capô" e investigar as bibliotecas por conta própria. A solução é muitas vezes mais simples do que parece e está esperando para ser descoberta.

Espero que minha jornada ajude outros desenvolvedores a economizar tempo e a implementar busca semântica em seus projetos .NET!

Top comments (0)