DEV Community

Lucas Pereira de Souza
Lucas Pereira de Souza

Posted on

PostgreSQL como busca vetorial (pgvector)

logotech

## Do Relacional à Inteligência: Desbloqueando o Potencial de IA do Seu Banco de Dados

A revolução da Inteligência Artificial (IA) está remodelando indústrias e a forma como interagimos com a tecnologia. Para muitos, a IA parece um universo distante, acessível apenas a startups com orçamentos ilimitados. Mas e se eu te dissesse que a chave para desbloquear o poder da IA pode já estar em suas mãos, em um local onde você menos espera: seu banco de dados relacional?

Neste post, vamos embarcar em uma jornada técnica para transformar seu confiável banco de dados relacional em uma poderosa fonte de insights de IA. Exploraremos como alavancar seus dados existentes, utilizando práticas modernas de desenvolvimento com TypeScript e Node.js, para construir aplicações inteligentes.

O Desafio: Dados Sobrecarregados, Insights Subutilizados

Bancos de dados relacionais, como PostgreSQL, MySQL e SQL Server, são a espinha dorsal de inúmeras aplicações. Eles armazenam dados estruturados de forma organizada, permitindo consultas eficientes e integridade referencial. No entanto, tradicionalmente, eles não foram projetados para lidar com as complexidades dos dados não estruturados ou para executar inferências de machine learning diretamente.

A explosão de dados - texto, imagens, áudio e vídeo - apresenta um desafio. Como podemos extrair valor preditivo e insights acionáveis desses vastos repositórios de dados relacionais, que muitas vezes contêm informações cruciais para a tomada de decisões inteligentes?

A Solução: Vetorização e Bancos de Dados Vetoriais

A resposta reside em duas técnicas poderosas: vetorização e a ascensão dos bancos de dados vetoriais.

  1. Vetorização: É o processo de converter dados (texto, imagens, etc.) em representações numéricas de alta dimensão chamadas \"vetores\" ou \"embeddings\". Modelos de IA treinados (como os da OpenAI, Google ou modelos open-source) realizam essa conversão, capturando a semântica e o contexto dos dados. Dados semelhantes terão vetores próximos no espaço multidimensional.

  2. Bancos de Dados Vetoriais: São bancos de dados otimizados para armazenar e consultar esses vetores de alta dimensão de forma eficiente. Eles permitem encontrar vetores \"semelhantes\" a um vetor de consulta, o que é a base para muitas aplicações de IA, como busca semântica, sistemas de recomendação e detecção de anomalias.

Mas a boa notícia é que você não precisa migrar todo o seu banco de dados relacional para um banco vetorial dedicado. Podemos integrar a funcionalidade vetorial ao seu sistema existente.

Integrando Vetores ao Seu Banco de Dados Relacional

Muitos bancos de dados relacionais modernos agora suportam extensões ou tipos de dados que permitem o armazenamento e a consulta eficiente de vetores. A extensão pgvector para PostgreSQL é um exemplo proeminente. Ela adiciona um tipo de dado vector e operadores para realizar buscas de similaridade (como k-NN - k-Nearest Neighbors).

Vamos demonstrar como isso funciona usando TypeScript e Node.js com um exemplo prático: busca semântica em um catálogo de produtos.

Cenário: Temos um banco de dados relacional com informações de produtos (nome, descrição, preço). Queremos permitir que os usuários busquem produtos usando linguagem natural, encontrando correspondências baseadas no significado, e não apenas em palavras-chave exatas.

Passos:

  1. Configurar o Banco de Dados: Instale a extensão pgvector no seu PostgreSQL.
  2. Geração de Embeddings: Use uma API de modelo de linguagem (como a da OpenAI) para converter as descrições dos produtos em vetores.
  3. Armazenamento: Armazene esses vetores junto com os dados do produto no seu banco de dados relacional.
  4. Busca: Quando um usuário fizer uma consulta, converta a consulta em um vetor e use o pgvector para encontrar os produtos mais semelhantes.

Exemplo de Código (TypeScript/Node.js)

Primeiro, vamos configurar nosso ambiente. Assumimos que você já tem um projeto Node.js configurado com TypeScript e um cliente PostgreSQL (como pg ou typeorm).

1. Definição do Tipo do Produto com Vetor:

// src/types/Product.ts

/**
 * Representa um produto em nosso catálogo, incluindo sua representação vetorial.
 */
export interface Product {
  id: number;
  name: string;
  description: string;
  price: number;
  /**
   * Representação vetorial da descrição do produto.
   * Armazenado como um array de números. A dimensão deve corresponder
   * à saída do modelo de embedding utilizado.
   */
  embedding: number[];
}
Enter fullscreen mode Exit fullscreen mode

2. Função para Gerar Embeddings (Simulado):

Em um cenário real, você chamaria uma API externa aqui. Para fins de demonstração, vamos simular a geração de embeddings.

// src/services/embeddingService.ts
import { Product } from '../types/Product';

// Simula a chamada a um serviço de embeddings (ex: OpenAI API)
// Em produção, use bibliotecas como 'openai' ou similares.
async function generateEmbedding(text: string): Promise<number[]> {
  // Simulação: Retorna um vetor de exemplo com base no comprimento do texto
  const baseVector = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8]; // Exemplo de vetor de 8 dimensões
  const textLength = text.length;
  // Cria um vetor mais dinâmico baseado no texto
  const dynamicVector = Array.from({ length: 8 }, (_, i) =>
    parseFloat((Math.sin(i * textLength / 100 + Date.now() / 1e9) * 0.5 + 0.5).toFixed(4))
  );
  console.log(`Generated embedding for text: \"${text.substring(0, 30)}...\"`);
  return dynamicVector;
}

/**
 * Gera embeddings para as descrições dos produtos e os anexa aos objetos Product.
 * @param products Array de produtos para os quais gerar embeddings.
 * @returns Array de produtos com a propriedade 'embedding' preenchida.
 */
export async function enrichProductsWithEmbeddings(products: Omit<Product, 'embedding'>[]): Promise<Product[]> {
  const enrichedProducts: Product[] = [];
  for (const product of products) {
    const embedding = await generateEmbedding(product.description);
    enrichedProducts.push({ ...product, embedding });
  }
  return enrichedProducts;
}

/**
 * Gera um embedding para uma consulta de texto.
 * @param query A consulta de texto do usuário.
 * @returns O vetor de embedding para a consulta.
 */
export async function generateQueryEmbedding(query: string): Promise<number[]> {
    return generateEmbedding(query);
}
Enter fullscreen mode Exit fullscreen mode

3. Funções de Interação com o Banco de Dados (usando pg):

Precisamos de uma tabela no PostgreSQL com uma coluna do tipo vector.

-- SQL para criar a tabela (execute via psql ou seu cliente de DB)
CREATE TABLE products (
    id SERIAL PRIMARY KEY,
    name VARCHAR(255) NOT NULL,
    description TEXT NOT NULL,
    price NUMERIC(10, 2) NOT NULL,
    embedding vector(8) -- Assumindo vetores de 8 dimensões
);

-- Crie um índice para buscas eficientes (ex: IVFFlat)
CREATE INDEX ON products USING ivfflat (embedding vector_l2_ops) WITH (lists = 100);
-- Ou para similaridade de cosseno:
-- CREATE INDEX ON products USING ivfflat (embedding vector_cosine_ops) WITH (lists = 100);
Enter fullscreen mode Exit fullscreen mode

Agora, as funções TypeScript:

// src/db/productRepository.ts
import { Pool, QueryResult } from 'pg';
import { Product } from '../types/Product';

// Configure sua pool de conexão com o banco de dados
const pool = new Pool({
  user: 'your_db_user',
  host: 'localhost',
  database: 'your_db_name',
  password: 'your_db_password',
  port: 5432,
});

/**
 * Insere um novo produto no banco de dados, incluindo seu embedding.
 * @param product O produto a ser inserido.
 * @returns O produto inserido com seu ID do banco de dados.
 */
export async function insertProduct(product: Product): Promise<Product> {
  const queryText = `
    INSERT INTO products(name, description, price, embedding)
    VALUES($1, $2, $3, $4) RETURNING id
  `;
  const values = [product.name, product.description, product.price, product.embedding];

  try {
    const res: QueryResult = await pool.query(queryText, values);
    console.log(`Product inserted with ID: ${res.rows[0].id}`);
    return { ...product, id: res.rows[0].id };
  } catch (error) {
    console.error('Error inserting product:', error);
    throw error;
  }
}

/**
 * Busca produtos por similaridade vetorial.
 * @param queryEmbedding O vetor de embedding da consulta.
 * @param k O número de resultados mais similares a serem retornados.
 * @returns Uma lista de produtos mais similares à consulta.
 */
export async function searchSimilarProducts(queryEmbedding: number[], k: number = 5): Promise<Product[]> {
  // A função 'vector_dims' obtém a dimensão do vetor armazenado na tabela.
  // 'vector_l2_ops' especifica a operação de distância (Euclideana). Use 'vector_cosine_ops' para similaridade de cosseno.
  const queryText = `
    SELECT id, name, description, price, embedding
    FROM products
    ORDER BY embedding <=> $1
    LIMIT $2
  `;
  const values = [queryEmbedding, k];

  try {
    const res: QueryResult = await pool.query(queryText, values);
    // O tipo retornado pelo pg é 'any[]', então precisamos fazer um cast ou mapeamento seguro.
    const products: Product[] = res.rows.map((row: any) => ({
      id: row.id,
      name: row.name,
      description: row.description,
      price: parseFloat(row.price), // NUMERIC é retornado como string
      embedding: row.embedding,
    }));
    return products;
  } catch (error) {
    console.error('Error searching similar products:', error);
    throw error;
  }
}

// Exemplo de função para popular o banco de dados (uso único)
export async function populateDatabase() {
    const sampleProductsData = [
        { name: \"Laptop Gamer X\", description: \"Potente laptop para jogos com RTX 4090 e 32GB RAM.\", price: 2500.00 },
        { name: \"Smartphone Pro Max\", description: \"O mais recente smartphone com câmera avançada e tela OLED.\", price: 1200.00 },
        { name: \"Teclado Mecânico RGB\", description: \"Teclado de alta performance com switches blue e iluminação personalizável.\", price: 150.00 },
        { name: \"Monitor Ultrawide 4K\", description: \"Monitor curvo de 34 polegadas com resolução 4K para imersão total.\", price: 800.00 },
        { name: \"Webcam Full HD\", description: \"Câmera web 1080p com microfone embutido, ideal para streaming.\", price: 70.00 },
    ];

    console.log(\"Enriquecendo produtos com embeddings...\");
    const productsWithEmbeddings = await enrichProductsWithEmbeddings(sampleProductsData);

    console.log(\"Inserindo produtos no banco de dados...\");
    for (const product of productsWithEmbeddings) {
        await insertProduct(product);
    }
    console.log(\"Banco de dados populado com sucesso!\");
}

// Para fechar a pool quando a aplicação terminar
export async function closeDbConnection() {
    await pool.end();
}

Enter fullscreen mode Exit fullscreen mode

4. Exemplo de Uso:

// src/index.ts
import { populateDatabase, searchSimilarProducts, closeDbConnection } from './db/productRepository';
import { generateQueryEmbedding } from './services/embeddingService';

async function main() {
  try {
    // 1. Popule o banco de dados (execute apenas uma vez ou se precisar resetar)
    // await populateDatabase();

    // 2. Defina uma consulta de usuário
    const userQuery = \"Estou procurando um computador rápido para jogar.\";
    console.log(`\nUser query: \"${userQuery}\"`);

    // 3. Gere o embedding para a consulta
    const queryEmbedding = await generateQueryEmbedding(userQuery);

    // 4. Busque produtos similares no banco de dados
    const similarProducts = await searchSimilarProducts(queryEmbedding, 3);

    console.log(\"\n--- Produtos Similares Encontrados ---\");
    if (similarProducts.length > 0) {
      similarProducts.forEach(product => {
        console.log(`ID: ${product.id}, Nome: ${product.name}, Descrição: ${product.description}, Preço: $${product.price}`);
      });
    } else {
      console.log(\"Nenhum produto similar encontrado.\");
    }

  } catch (error) {
    console.error(\"An error occurred:\", error);
  } finally {
    await closeDbConnection();
    console.log(\"\nDatabase connection closed.");
  }
}

main();
Enter fullscreen mode Exit fullscreen mode

Execução:

  1. Certifique-se de ter o PostgreSQL rodando e a extensão pgvector instalada.
  2. Ajuste as credenciais do banco de dados no productRepository.ts.
  3. Execute npm install pg @types/pg para instalar as dependências.
  4. Descomente await populateDatabase(); na função main para popular o banco de dados com os produtos de exemplo. Execute o script (ts-node src/index.ts).
  5. Execute novamente, mas agora com a linha populateDatabase comentada, para realizar a busca.

Este exemplo demonstra a busca semântica. Imagine as possibilidades: sistemas de recomendação personalizados, detecção de fraudes analisando padrões em transações, sumarização de feedback de clientes, etc.

Boas Práticas e Considerações

  • Escolha do Modelo de Embedding: A qualidade dos seus embeddings é crucial. Experimente diferentes modelos (OpenAI, Sentence-BERT, etc.) para encontrar o que melhor se adapta aos seus dados e caso de uso.
  • Dimensionalidade do Vetor: Modelos diferentes produzem vetores de diferentes dimensionalidades (ex: 768, 1536). Certifique-se de que sua coluna vector no banco de dados corresponda a essa dimensão.
  • Índices Vetoriais: Para bancos de dados com milhões de registros, a criação de índices vetoriais apropriados (como IVFFlat, HNSW) é essencial para garantir performance de busca aceitável. A escolha do índice e seus parâmetros (como lists para IVFFlat) afeta a precisão e a velocidade.
  • Operador de Distância: Escolha o operador de distância correto (vector_l2_ops para Euclidiana, vector_cosine_ops para similaridade de cosseno) que corresponda à forma como seu modelo de embedding foi treinado ou como você deseja medir a similaridade.
  • Gerenciamento de Embeddings: Mantenha seus embeddings atualizados. Se os dados no seu banco de dados mudarem, os embeddings correspondentes também podem precisar ser recalculados e atualizados.
  • Segurança: Ao usar APIs de terceiros para gerar embeddings, esteja ciente das políticas de privacidade e segurança dos dados enviados.

Conclusão

Seu banco de dados relacional não é apenas um repositório de dados históricos; é uma mina de ouro esperando para ser explorada com o poder da IA. Ao integrar técnicas de vetorização e alavancar as capacidades crescentes dos bancos de dados relacionais modernos (ou extensões como pgvector), você pode transformar seus dados estruturados em uma fonte de inteligência preditiva e insights acionáveis.

A jornada de dados relacionais para IA não exige uma reescrita completa da sua infraestrutura. Com as ferramentas e abordagens certas, você pode começar a construir aplicações mais inteligentes hoje mesmo, desbloqueando um novo nível de valor a partir dos seus ativos de dados mais valiosos. A revolução da IA está ao seu alcance, e ela pode começar com uma simples consulta SQL aprimorada.

Top comments (0)