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);
}
}
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);
});
// ...
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)