Destino Dev

Blog

Busca semântica no Next.js com embeddings OpenAI e Redis Vector — passo a passo

Por John Pires · 19 de março de 2026

Por que aprender isso em 2026

Contexto técnico e tendência

Busca semântica deixou de ser experimento e virou requisito em produtos que precisam recuperar informação relevante independentemente da escolha exata de palavras do usuário. Em 2026, modelos de linguagem e embeddings são padrão na arquitetura de pesquisa de documentos, chatops e assistentes de código. Aprender a integrar embeddings com ferramentas como Redis Vector e frameworks como Next.js coloca você em posição de entregar recursos de alto valor sem depender de serviços caros para cada consulta.

Analogia simples para entender o fluxo

Imagine uma biblioteca onde cada livro tem um resumo numérico que captura o sentido do texto, em vez de um índice alfabético. Quando o leitor faz uma pergunta, você traduz a pergunta para esse mesmo formato numérico e procura pelos resumos mais próximos. Embeddings fazem o resumo numérico e Redis Vector é o armário com capacidade de procurar por proximidade de resumo de forma muito rápida.

Resumo técnico: você gera embeddings com OpenAI, armazena vetores no Redis usando o módulo de vetores (Redisearch), e consulta com KNN para obter resultados semânticos. Vamos direto ao prático.

Configuracao do ambiente

Requisitos mínimos e versões

Use Node 18LTS ou superior. Next.js 14+ recomendado, mas exemplos funcionam com 13.x em app routes ou pages routes com pequenas adaptações. Redis precisa ser 7.0+ com Redisearch e suporte a vetores, ou use Redis Enterprise ou um serviço gerenciado que expose vetor search.

Instalação de dependencias

Instale dependencias essenciais: next, react, openai ou fetch, redis ou ioredis. Exemplo de instalação com npm:

npm init -y
npm install next react react-dom openai redis

O que acontece: isso prepara o projeto e traz a biblioteca oficial openai e um cliente redis de baixo nivel. Alternativas: redis-om para abstracoes, mas no tutorial vamos mostrar comando Redisearch direto para ficar explícito.

Variaveis de ambiente

Crie um arquivo .env.local na raiz do projeto com chaves seguras. Exemplo de variaveis:

OPENAI_API_KEY='coloque_sua_chave_aqui'
REDIS_URL='redis://:senha@localhost:6379'

Contexto: mantenha a chave do OpenAI no servidor. Nunca faça chamadas para gerar embeddings diretamente do cliente com a chave embutida.

Hello World progressivo

Primeiro erro comum: embedding no cliente

Erros que vejo no code review: colocar a chave do OpenAI no frontend para chamar embeddings, ou gerar embeddings no frontend e enviar vetores brutos ao servidor sem validação. Isso expõe a chave e aumenta custo e latencia desnecessariamente.

Dica: chaves de IA e criação de embeddings são operações sensiveis. Rode no servidor e aplique rate limit e caching.

Exemplo errado: frontend gerando embedding e enviando para Redis diretamente. Isso expõe a chave e transfere work desnecessario ao cliente.

// MAL - codigo do lado do cliente que NAO deve existir
async function embedAndStore(text) {
  // chave embutida no bundle do cliente - inseguro
  const r = await fetch('https://api.openai.com/v1/embeddings', {
    method: 'POST',
    headers: { 'Authorization': 'Bearer ' + process.env.OPENAI_API_KEY },
    body: JSON.stringify({ model: 'text-embedding-3-small', input: text })
  })
  const json = await r.json()
  const vector = json.data[0].embedding
  await fetch('/api/store-vector', { method: 'POST', body: JSON.stringify({ text, vector }) })
}

O que acontece: chave exposta, usuário pode abusar da API, custo cresce e você perde controle dos embeddings criados.

Abordagem correta: gerar embeddings no servidor

Vamos criar uma rota Next.js que gera embedding com OpenAI e retorna um id. Esse endpoint roda no servidor e usa a chave do OpenAI guardada em variaveis de ambiente.

// pages/api/embed.js
import OpenAI from 'openai'
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY })

export default async function handler(req, res) {
  const { text } = req.body
  if (!text) return res.status(400).json({ error: 'text required' })

  const resp = await openai.embeddings.create({ model: 'text-embedding-3-small', input: text })
  const embedding = resp.data[0].embedding
  return res.status(200).json({ embedding })
}

O que acontece: servidor chama OpenAI, chave fica segura. Agora você pode optar por armazenar o vetor no Redis aqui mesmo ou retornar para servidor que fará o armazenamento.

Mini-projeto pratico

Descrição do mini-projeto

Construiremos um index simples de notas. Fluxo: o servidor gera embedding ao salvar nota e persiste no Redis. Para busca, o servidor recebe a query, gera embedding e executa uma consulta KNN no Redis, retornando textos mais proximos. Tudo feito com Next.js API routes.

Criando o index vector no Redis

Contexto: antes de inserir vetores, crie um índice Redisearch configurado para vetores. Esse comando pode ser executado via redis-cli ou por codigo. Dimensão deve bater com o modelo de embedding.

// script para criar indice via node usando cliente redis
import { createClient } from 'redis'

async function createIndex() {
  const client = createClient({ url: process.env.REDIS_URL })
  await client.connect()
  // assume model retorna 1536 dimensoes
  try {
    await client.sendCommand(['FT.CREATE', 'idx:notes', 'ON', 'HASH', 'PREFIX', '1', 'note:', 'SCHEMA', 'content', 'TEXT', 'vector', 'VECTOR', 'FLAT', '6', 'TYPE', 'FLOAT32', 'DIM', '1536', 'DISTANCE_METRIC', 'COSINE'])
    console.log('index criado')
  } catch (err) {
    console.log('provavelmente indice ja existe', err.message)
  }
  await client.quit()
}

createIndex()

O que acontece: o indice idx:notes é criado com um campo vector para busca KNN. Em producao, escolha HNSW para maior escala e ajuste parametros conforme memoria. Erro comum: dimension diferente do vetor real causa falha na insercao.

Inserindo documentos com embeddings

Agora um endpoint que gera embedding e salva no Redis convertendo o vetor para formato binario exigido pelo Redis.

// pages/api/save-note.js
import OpenAI from 'openai'
import { createClient } from 'redis'
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY })

export default async function handler(req, res) {
  const { id, content } = req.body
  if (!id || !content) return res.status(400).json({ error: 'id and content required' })

  const embResp = await openai.embeddings.create({ model: 'text-embedding-3-small', input: content })
  const vector = embResp.data[0].embedding

  const client = createClient({ url: process.env.REDIS_URL })
  await client.connect()

  // converter array de floats para Buffer little endian float32
  const float32 = new Float32Array(vector)
  const buffer = Buffer.from(float32.buffer)

  await client.hSet('note:' + id, { content, vector: buffer })
  await client.quit()

  return res.status(200).json({ ok: true })
}

O que acontece: hSet armazena o texto e o vetor binario. Em consultas KNN, o Redis vai ler esse campo vector para calcular proximidade.

Consulta semantica com KNN

Agora o endpoint de busca: recebe query, gera embedding, e faz FT.SEARCH com KNN. O parse retorna os ids e scores.

// pages/api/search.js
import OpenAI from 'openai'
import { createClient } from 'redis'
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY })

export default async function handler(req, res) {
  const { q, k = 5 } = req.body
  if (!q) return res.status(400).json({ error: 'q required' })

  const emb = await openai.embeddings.create({ model: 'text-embedding-3-small', input: q })
  const vector = emb.data[0].embedding
  const float32 = new Float32Array(vector)
  const queryVector = Buffer.from(float32.buffer).toString('base64')

  const client = createClient({ url: process.env.REDIS_URL })
  await client.connect()

  // FT.SEARCH idx:notes @vector:[KNN k $vec AS score] RETURN 2 content score PARAMS 2 vec <blob>
  const query = ['FT.SEARCH', 'idx:notes', 'KNN', String(k), 'vector', 'BLOB', queryVector, 'RETURN', '2', 'content', 'vector']
  // many redis clients nao suportam BLOB direto; aqui mostramos o que precisa acontecer em baixo nivel
  const resp = await client.sendCommand(query)

  // parse simplificado: resp vem alternando id, fields
  const results = []
  for (let i = 0; i < resp.length; i += 2) {
    const id = resp[i]
    const fields = resp[i + 1]
    // fields e array alternando chave e valor
    const obj = {}
    for (let j = 0; j < fields.length; j += 2) obj[fields[j]] = fields[j + 1]
    results.push({ id, content: obj.content })
  }

  await client.quit()
  return res.status(200).json({ results })
}

O que acontece: o servidor retorna documentos semânticos similares. Ajuste retorno e score para atender sua interface.

Erros comuns e armadilhas

Lista de erros que surgem frequentemente em code review quando se trabalha com embeddings e Redis:

  • Dimensao do vetor errada ao criar indice
  • Armazenar vetores como strings JSON em vez de binario, causando busca KNN ineficiente ou falhas
  • Expor a chave do OpenAI no cliente
  • Ignorar normalizacao e metodos de distancia adequados ao criar o indice
  • Consultar Redis sem limites ou sem paging, gerando uso excessivo de CPU

Exemplo errado de armazenamento: salvar vetor como JSON string e tentar usar FT.SEARCH no campo faz com que Redis nao reconheca como vetor.

// MAL - nao fazer isso
await client.hSet('note:' + id, { content, vector: JSON.stringify(vector) })

Corrija convertendo para Float32Array e Buffer como mostrado acima. Isso garante compatibilidade com o formato VECTOR do Redis.

Recursos para continuar estudando

Documentacao oficial e tracking de novos modelos

Leia a documentacao do OpenAI sobre embeddings e combinacoes de modelos, e a documentacao do Redis sobre RediSearch e vectordb. Esses recursos definem parametros de dimensao, metricas e exemplos de producao.

Como evoluir o mini-projeto

Depois de ter o pipeline basico, evolua para: versionamento de embeddings por modelo, usar HNSW para escalabilidade, armazenar metadados para filtros faceted, e criar pipelines de atualização incremental de embeddings com filas (ex: BullMQ).

Outra evolucao é integrar um reranker do tipo cross-encoder para refinar ordem dos candidatos retornados pelo vetor, reduzindo falsos positivos.

Conclusao pratica

Checklist pratico apos seguir o tutorial

Ao terminar: voce tera um endpoint que salva notas gerando embeddings no servidor, um indice Redis compatível com vetores, e um endpoint de busca que retorna resultados semanticos. Verifique se: variaveis de ambiente estao configuradas, indices tem dimensao correta, e chaves nunca saem do servidor.

Sugestoes de proximo passo

Implemente autentificacao e controle de custos, adicione testes de integracao que validem formato do vetor e sucesso de FT.CREATE/FT.SEARCH, e monitore latencia e uso de memoria do Redis em producao. Pequenas otimizações no formato de armazenamento e tuning do indice fazem grande diferença em escala.

Resumo final: evite a exposicao de chave, armazene vetores em formato binario float32, crie indices adequados e trate consultas KNN no servidor. Esses passos cobrem as armadilhas mais comuns e te deixam pronto para aplicar busca semantica em apps reais.

Checklist tecnico rapido

  • Gerar embeddings no servidor
  • Converter array de floats para Float32Array e Buffer
  • Usar FT.CREATE com DIM correto e TYPE FLOAT32
  • Consultar com KNN e parametrizar k
  • Adicionar limitacao e caching

Fim. Use estes exemplos como base, ajustando nome do modelo, dimensao e parametros Redis conforme atualizacoes do provedor de embeddings e do Redis. Boa pratica em 2026: tratar embeddings como first class citizens na arquitetura de busca.

Assuntos: Next.js, IA, NoSQL, embeddings, semantic-search

Leia também