Procure por “filhote” em um sistema que só compara palavras-chave e um documento sobre adestramento de cães não vai aparecer, mesmo que qualquer pessoa que lesse os dois achasse que eles têm tudo a ver. A busca por palavras-chave compara strings de texto. Ela não sabe que “filhote” e “cachorro” significam quase a mesma coisa. Os embeddings resolvem exatamente isso. Eles transformam um texto em uma lista de números, um vetor, posicionado de forma que textos com significado parecido fiquem próximos entre si, independentemente das palavras usadas.
Como um modelo de embeddings transforma texto em números
Um modelo de embeddings é uma rede neural treinada para mapear texto em um ponto de um espaço de alta dimensionalidade: 384, 768 ou 1536 dimensões são tamanhos comuns, dependendo do modelo. Ele não procura palavras em um dicionário. A partir de enormes quantidades de texto, aprendeu quais palavras e frases aparecem em contextos parecidos, e codifica esse padrão na posição final do vetor.
A distância nesse espaço acompanha a similaridade de significado. “Rei” e “rainha” ficam próximos. “Rei” e “golden retriever” ficam distantes. Ninguém disse ao modelo para comparar essas palavras: a geometria é resultado do treinamento.
Você pode ver o mesmo padrão com números escolhidos à mão, antes de qualquer modelo real entrar em cena. Dê a cada palavra um vetor de 3 números (um modelo real usa centenas):
| |
king e queen têm valores parecidos nos dois primeiros números. dog não. Para transformar essa semelhança em uma única nota, você precisa de uma métrica de similaridade.
Cosine similarity, dot product ou Euclidean distance: qual métrica usar?
Três métricas aparecem o tempo todo no trabalho com embeddings, e cada uma responde a uma pergunta um pouco diferente:
| Métrica | O que mede | Quando usar |
|---|---|---|
| Cosine similarity | O ângulo entre dois vetores, ignorando o comprimento deles | Padrão para embeddings de texto, já que a maioria dos modelos é treinada para isso |
| Dot product | Ângulo e comprimento juntos | O modelo foi treinado explicitamente para ranking por dot product (alguns modelos de retrieval são) |
| Euclidean distance | Distância em linha reta entre os dois pontos | Dados espaciais ou baseados em contagem; menos comum para texto |
Cosine similarity é a que você vai usar quase sempre. Muitos modelos de text embedding normalizam a saída para comprimento 1, então a direção é a única coisa que varia, e em vetores normalizados cosine e dot product dão o mesmo ranking. A nota vai de -1 (oposto) a 1 (idêntico), com 0 significando sem relação. Veja como fica nos vetores escolhidos à mão:
| |
0,801 contra 0,375. king fica muito mais perto de queen do que de dog. Um modelo real produz o mesmo tipo de diferença, espalhada por centenas de dimensões que ele aprendeu a partir de dados em vez de três escolhidas à mão.
Como gerar embeddings em Python
sentence-transformers é a biblioteca open-source padrão para isso. Ela roda localmente, sem precisar de API key:
| |
| |
all-MiniLM-L6-v2 transforma cada frase em um vetor de 384 números, e model.similarity calcula a cosine similarity para cada par. As duas frases sobre tempo ensolarado quase não compartilham palavras, mas ainda assim pontuam alto entre si e baixo em relação à do estádio. Mesmo padrão do exemplo de brinquedo, só que agora os números vêm de um modelo que aprendeu a linguagem.
As APIs hospedadas retornam a mesma coisa. Tanto o endpoint text-embedding-3-small da OpenAI quanto o embed da Cohere entregam uma lista de números decimais por texto. Se você já roda modelos localmente, o Ollama também serve embeddings: ollama pull nomic-embed-text, depois chame o endpoint /api/embed dele (a configuração é tratada em Como Executar um LLM Local com o Ollama). A biblioteca muda. O vetor, e o que você faz com ele, não.
Como a busca por similaridade vetorial encontra a correspondência mais próxima
Calcular o embedding de um único texto é a parte fácil. O trabalho real quase sempre é: dado um vetor de consulta, encontrar as correspondências mais próximas entre milhares ou milhões de vetores armazenados. Essa é a busca por similaridade vetorial, e a versão direta é força bruta: pontuar a consulta contra cada vetor armazenado e manter os melhores resultados.
| |
Em uma máquina comum, isso imprime algo próximo de:
| |
O tempo cresce linearmente com o número de vetores, O(n) nos termos da notação Big O, mas o NumPy torna cada passo barato o suficiente para que um milhão de vetores leve bem menos de 100 ms. Isso deixa de ser suficiente com tráfego concorrente pesado, dezenas de milhões de vetores, ou ambos. Nesse ponto você precisa de um índice approximate nearest neighbor (ANN), que faz pelos vetores o que uma B-tree faz por uma cláusula WHERE em um banco de dados (veja Como Funcionam os Índices de Banco de Dados). Dois tipos aparecem em todo lugar:
- HNSW (Hierarchical Navigable Small World) constrói um grafo em várias camadas ligando cada vetor aos seus vizinhos, de forma que uma busca percorre o grafo em direção à resposta em vez de tocar em cada vetor. Oferece o melhor recall por milissegundo entre as opções comuns, e paga isso com mais memória e construções de índice mais lentas.
- IVFFlat agrupa os vetores em clusters em torno de centroides, e então busca apenas nos clusters mais próximos da consulta. Constrói mais rápido e usa menos memória que o HNSW, mas precisa de dados representativos já carregados antes de ser criado. Construído sobre uma tabela vazia, gera clusters ruins e recall ruim.
No Postgres com a extensão pgvector, cada um é uma única instrução:
| |
Os dois trocam exatidão por velocidade. Podem não encontrar o vetor realmente mais próximo — aceitável para busca e recomendações, um problema para qualquer caso que precise sempre da resposta exata.
Quais erros comuns aparecem na busca com embeddings
- Consultar com um modelo e armazenar com outro. Vetores de dois modelos diferentes vivem em espaços incompatíveis. Se você trocar de modelo, recalcule o embedding de cada documento armazenado com o novo. Uma mistura de vetores antigos e novos retorna correspondências inúteis sem gerar nenhum erro.
- Ranking por dot product em vetores nunca normalizados. O dot product inclui o comprimento, então um vetor longo pode superar no ranking um mais curto e mais relevante. Normalize na hora de gravar, ou use o operador cosine, que ignora o comprimento.
- Confundir proximidade com estar certo. Uma nota cosine alta significa “aparece em contextos parecidos”, não “é a resposta certa”. Uma consulta sobre os efeitos colaterais de um medicamento pode trazer de volta um trecho sobre a dosagem desse mesmo medicamento: relacionado, e errado. Esse é o principal modo de falha em pipelines de RAG. A similaridade reduz os candidatos. Ela não os verifica.
- Adicionar um índice ANN antes da força bruta ficar realmente lenta demais. O benchmark acima processa um milhão de vetores em menos de 100 ms. HNSW ou IVFFlat trazem tempo de construção, parâmetros para ajustar e resultados aproximados. Vale a pena além de dezenas de milhões de vetores ou sob carga concorrente pesada, não por padrão.
- Esquecer de recalcular o embedding quando a fonte muda. Um embedding é uma foto do texto no momento em que você o calculou. Edite o documento sem recalcular e a busca vai continuar retornando a versão antiga com total confiança.
Você precisa de um banco de dados vetorial?
Para começar, não. Calcule o embedding de algumas centenas dos seus documentos reais com sentence-transformers, guarde os vetores em um array NumPy e busque neles com o loop de força bruta acima. Sem banco de dados, sem índice. Migre para pgvector ou um banco de dados vetorial gerenciado quando a força bruta ficar mensuravelmente lenta demais para o seu volume de dados e consultas. Adicione um índice ANN só quando a varredura da tabela for o gargalo que você realmente mediu.