Embeddings na prática: de texto para vetor para busca semântica
1. O que são embeddings e por que eles importam
Embeddings são representações densas de significado em vetores numéricos. Diferentemente das abordagens tradicionais como bag-of-words, que criam vetores esparsos onde cada posição representa uma palavra específica, os embeddings capturam relações semânticas em espaços contínuos de baixa dimensionalidade.
Um vetor bag-of-words para um documento com 10 mil palavras únicas teria 10 mil dimensões, majoritariamente preenchidas com zeros. Um embedding de qualidade, por outro lado, pode representar o mesmo documento em 384 ou 768 dimensões, com valores densos e contínuos.
As propriedades-chave dos embeddings incluem:
- Similaridade semântica: textos com significados próximos geram vetores próximos no espaço
- Operações algébricas: relações como "rei - homem + mulher ≈ rainha" funcionam no espaço vetorial
- Redução de dimensionalidade: compressão eficiente da informação semântica
# Exemplo conceitual de similaridade entre embeddings
"gato" → [0.23, 0.87, -0.12, 0.45, ...]
"felino" → [0.21, 0.85, -0.10, 0.48, ...] # similaridade: 0.98
"carro" → [0.67, -0.34, 0.89, -0.12, ...] # similaridade: 0.12
2. Como gerar embeddings a partir de texto
Modelos populares para geração de embeddings incluem Sentence-BERT (all-MiniLM-L6-v2), OpenAI Ada-002, e alternativas open-source como BGE e instructor-xl. O pipeline prático segue três etapas:
- Tokenização: converter texto em tokens numéricos
- Codificação: passar tokens pelo modelo transformer
- Normalização: ajustar o vetor resultante para comprimento unitário
# Pipeline de geração de embedding com Sentence-BERT
from sentence_transformers import SentenceTransformer
modelo = SentenceTransformer('all-MiniLM-L6-v2')
texto = "Embeddings transformam significado em vetores"
# Tokenização e codificação automáticas
vetor = modelo.encode(texto, normalize_embeddings=True)
print(f"Dimensões: {len(vetor)}") # Saída: 384
print(f"Primeiros valores: {vetor[:5]}")
# Saída: [0.023, -0.045, 0.089, 0.012, -0.067]
Cuidados importantes: truncamento automático para 256 ou 512 tokens, padding em lote para processamento eficiente, e normalização para garantir comparabilidade.
3. Armazenando e indexando vetores em escala
Para milhões de documentos, o armazenamento plano de vetores é inviável. Vector databases como Pinecone, Weaviate e Qdrant oferecem índices aproximados para busca eficiente. Para soluções locais, FAISS e Annoy são as bibliotecas mais utilizadas.
Os principais índices de aproximação incluem:
- HNSW (Hierarchical Navigable Small World): excelente equilíbrio entre precisão e velocidade
- IVF (Inverted File Index): mais rápido para conjuntos muito grandes, com leve perda de precisão
- Produto Escalar vs. Cosseno: escolha baseada na normalização dos vetores
# Indexação com FAISS para busca aproximada
import faiss
import numpy as np
# Simulando 1000 embeddings de 384 dimensões
embeddings = np.random.random((1000, 384)).astype('float32')
# Construção do índice HNSW
dim = 384
indice = faiss.IndexHNSWFlat(dim, 32) # 32 conexões por nó
indice.add(embeddings)
# Salvando o índice para uso futuro
faiss.write_index(indice, "indice_embeddings.faiss")
Estratégias de particionamento: dividir por domínio (ex: documentos financeiros vs. técnicos) ou usar sharding por hash do conteúdo para distribuição horizontal.
4. Busca semântica: do embedding à recuperação
A busca semântica opera em três etapas: converter a query em embedding, calcular similaridade com todos os documentos indexados, e retornar os top-k vizinhos mais próximos.
As métricas de similaridade mais comuns são:
- Cosseno: mede o ângulo entre vetores (padrão para embeddings normalizados)
- Distância Euclidiana: distância geométrica direta
- Produto Escalar: equivalente ao cosseno quando vetores são normalizados
# Busca semântica completa
from sentence_transformers import SentenceTransformer
import numpy as np
modelo = SentenceTransformer('all-MiniLM-L6-v2')
# Documentos indexados
documentos = [
"Python é uma linguagem de programação versátil",
"JavaScript é usado principalmente para web",
"Embeddings representam significado em vetores"
]
embeddings_docs = modelo.encode(documentos, normalize_embeddings=True)
# Query do usuário
query = "linguagens de programação populares"
embedding_query = modelo.encode(query, normalize_embeddings=True)
# Cálculo de similaridade por cosseno
similaridades = np.dot(embeddings_docs, embedding_query)
indices_top3 = np.argsort(similaridades)[::-1][:3]
for i, idx in enumerate(indices_top3):
print(f"{i+1}. {documentos[idx]} (score: {similaridades[idx]:.4f})")
# Saída:
# 1. Python é uma linguagem de programação versátil (score: 0.7823)
# 2. JavaScript é usado principalmente para web (score: 0.6541)
# 3. Embeddings representam significado em vetores (score: 0.1234)
Filtros híbridos combinam busca semântica com metadados: por exemplo, filtrar por data antes de calcular similaridade, ou usar filtros booleanos para restringir o escopo da busca.
5. Métricas e avaliação de qualidade
Avaliar a qualidade da busca semântica requer métricas específicas:
- Recall@k: proporção de documentos relevantes recuperados entre os top-k
- Precision@k: proporção de documentos relevantes entre os top-k retornados
- Mean Reciprocal Rank (MRR): posição do primeiro resultado relevante
# Cálculo de métricas de avaliação
def recall_at_k(relevantes_recuperados, total_relevantes, k):
return len(relevantes_recuperados[:k]) / min(total_relevantes, k)
def precision_at_k(relevantes_recuperados, k):
return len(relevantes_recuperados[:k]) / k
# Exemplo: 3 documentos relevantes no total
relevantes = [0, 1, 2] # índices dos documentos relevantes
recuperados = [0, 3, 1, 4, 2] # ordem dos resultados
print(f"Recall@3: {recall_at_k(recuperados, len(relevantes), 3):.2f}") # 0.67
print(f"Precision@3: {precision_at_k(recuperados, 3):.2f}") # 0.67
Testes com queries de validação e ground truth manual são essenciais para identificar o modelo de embedding ideal para cada domínio.
6. Casos de uso práticos e armadilhas comuns
Casos reais:
- Busca em documentação técnica: embeddings permitem encontrar "Como instalar pacotes" mesmo quando o termo exato não aparece
- FAQ inteligente: respostas semanticamente similares a perguntas frequentes
- Recomendação de conteúdo: artigos relacionados por significado, não apenas por tags
Armadilhas frequentes:
- Viés do modelo: embeddings treinados em dados gerais podem falhar em domínios com jargão técnico
- Domínios específicos: termos como "kernel" (Linux vs. semente) exigem fine-tuning
- Idiomas: modelos multilíngues podem perder precisão em idiomas menos representados
# Fine-tuning simples para domínio específico
from sentence_transformers import SentenceTransformer, InputExample, losses
from torch.utils.data import DataLoader
modelo = SentenceTransformer('all-MiniLM-L6-v2')
# Pares de treinamento: (texto1, texto2, similaridade)
exemplos_treino = [
InputExample(texts=["kernel Linux", "núcleo do sistema"], label=0.9),
InputExample(texts=["kernel Linux", "semente de milho"], label=0.1),
]
dataloader = DataLoader(exemplos_treino, shuffle=True, batch_size=16)
perda = losses.CosineSimilarityLoss(modelo)
modelo.fit(train_objectives=[(dataloader, perda)], epochs=3)
7. Integração com LLMs: RAG e além
Embeddings são o backbone do Retrieval-Augmented Generation (RAG). O pipeline completo envolve:
- Indexação: converter documentos em embeddings e armazenar
- Busca: converter query em embedding e recuperar contexto relevante
- Prompt aumentado: combinar query original com documentos recuperados
- Geração: LLM responde com base no contexto enriquecido
# Pipeline RAG simplificado
from sentence_transformers import SentenceTransformer
import openai
modelo_emb = SentenceTransformer('all-MiniLM-L6-v2')
documentos = ["Embeddings capturam semântica", "RAG combina busca com geração"]
# Indexação
embeddings_docs = modelo_emb.encode(documentos)
# Busca
query = "Como funciona busca semântica?"
emb_query = modelo_emb.encode(query)
idx_relevante = np.argmax(np.dot(embeddings_docs, emb_query))
# Prompt aumentado
contexto = documentos[idx_relevante]
prompt = f"Contexto: {contexto}\nPergunta: {query}\nResposta:"
# Geração (simulada)
resposta = "A busca semântica usa embeddings para encontrar significado..."
print(resposta)
Monitoramento contínuo do índice é crucial: atualizações periódicas, re-embedding de documentos modificados, e validação de drift semântico garantem que o sistema mantenha qualidade ao longo do tempo.
Referências
- Sentence-Transformers Documentation — Documentação oficial da biblioteca Sentence-BERT com tutoriais práticos de geração de embeddings
- FAISS: A Library for Efficient Similarity Search — Repositório oficial do Facebook Research com implementações de índices de busca vetorial
- OpenAI Embeddings Guide — Guia oficial da OpenAI sobre geração e uso de embeddings com a API Ada
- Pinecone Vector Database Documentation — Documentação completa sobre armazenamento e busca de embeddings em escala
- Retrieval-Augmented Generation (RAG) Paper — Artigo seminal de Lewis et al. sobre integração de embeddings com LLMs para geração aumentada
- Weaviate Vector Search Engine — Documentação do banco de dados vetorial open-source com exemplos de busca híbrida
- HuggingFace Sentence Embeddings Leaderboard — Ranking atualizado de modelos de embedding com métricas de desempenho