Busca Semântica com Embeddings em Next.js
O problema que busca textual não resolve
Seu usuário digita "como lidar com erro no deploy" e a busca por texto retorna zero resultados porque o conteúdo usa "falha durante publicação". Busca textual compara strings. Busca semântica compara significado. A diferença prática: um LIKE '%deploy%' nunca encontra um artigo que fala sobre "publicação em produção", mas um vetor de embedding sim, porque ambos ocupam regiões próximas no espaço vetorial.
Embeddings são representações numéricas de texto em espaços de alta dimensionalidade (1536 dimensões no caso do text-embedding-3-small da OpenAI). Textos com significado parecido ficam próximos nesse espaço. A operação de busca se resume a: transformar a query do usuário em vetor, calcular distância contra vetores armazenados, retornar os mais próximos.
Este post implementa esse fluxo completo em Next.js com App Router, PostgreSQL + pgvector e a API de embeddings da OpenAI. Ao final, você terá uma busca semântica funcional em Route Handlers, com indexação de conteúdo e consulta por similaridade.
Escolhendo onde armazenar vetores
Antes de escrever código, a decisão de storage define custo, latência e complexidade operacional.
| Critério | pgvector (PostgreSQL) | Pinecone | Qdrant (self-hosted) |
|---|---|---|---|
| Custo até 100k vetores | Zero extra (usa seu PG existente) | Plano gratuito limitado, pago acima | Custo de infra própria |
| Latência p95 (10k vetores) | 5-15ms com índice IVFFlat | 10-30ms (rede inclusa) | 3-10ms local |
| Operacional | Uma extensão, sem serviço novo | SaaS gerenciado | Container para manter |
| Filtros híbridos (metadata + vetor) | SQL nativo, joins normais | Filtros limitados por namespace | Filtros por payload |
| Escala acima de 1M vetores | Precisa tuning de índice HNSW | Escala automática | Sharding manual |
Se você já usa PostgreSQL (e se usa Supabase como backend para Next.js, pgvector já vem habilitado), não há razão para adicionar um serviço externo com menos de 500k vetores. Acima de 1M vetores com queries abaixo de 10ms, considere Qdrant ou Pinecone.
Este post usa pgvector porque a maioria das aplicações Next.js já tem PostgreSQL.
Configurando pgvector no PostgreSQL
Habilite a extensão e crie a tabela de documentos com coluna vetorial:
-- Ativa pgvector. No Supabase, já vem disponível.
-- Em PostgreSQL local, instale: apt install postgresql-16-pgvector
CREATE EXTENSION IF NOT EXISTS vector;
CREATE TABLE documents (
id SERIAL PRIMARY KEY,
title TEXT NOT NULL,
content TEXT NOT NULL,
-- 1536 dimensões: compatível com text-embedding-3-small da OpenAI
embedding vector(1536),
metadata JSONB DEFAULT '{}',
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- IVFFlat é mais rápido para criar que HNSW, suficiente até ~100k vetores.
-- lists = sqrt(número de linhas). Para 10k docs, ~100 lists.
CREATE INDEX ON documents
USING ivfflat (embedding vector_cosine_ops)
WITH (lists = 100);
Se a base crescer acima de 100k documentos, troque IVFFlat por HNSW: melhor recall sem necessidade de reindexar periodicamente, ao custo de mais memória durante a construção do índice.
Gerando embeddings com a API da OpenAI
Instale as dependências:
npm install openai pg pgvector
Crie um módulo dedicado para gerar embeddings. Separar essa responsabilidade facilita trocar de provider depois (Cohere, Voyage AI, modelo local):
// src/lib/embeddings.ts
import OpenAI from "openai";
const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
});
// text-embedding-3-small custa $0.02/1M tokens, 5x mais barato que ada-002
// com qualidade comparável para busca semântica em português
const EMBEDDING_MODEL = "text-embedding-3-small";
export async function generateEmbedding(text: string): Promise<number[]> {
// Limpa whitespace excessivo para não desperdiçar tokens
const sanitized = text.replace(/\s+/g, " ").trim();
const response = awai
---
Leia o artigo completo em [https://www.vivodecodigo.com.br/nextjs/busca-semantica-embeddings-nextjs-pgvector-openai](https://www.vivodecodigo.com.br/nextjs/busca-semantica-embeddings-nextjs-pgvector-openai)