← Blog

Embeddings explicados: a base da busca semântica

2025-04-05EmbeddingsPythonLLM

O que são embeddings e por que importam

Embeddings são representações numéricas de texto — listas de números (vetores) que capturam significado semântico. Textos com sentido parecido ficam próximos no espaço vetorial; textos diferentes ficam distantes.

Essa ideia é a base de quase tudo em IA generativa aplicada: busca semântica, RAG, clustering de documentos, detecção de duplicatas, recomendação. Sem embeddings, você depende de busca por palavra-chave — que falha quando o usuário pergunta "como compartilhar dados financeiros" e o documento diz "Open Finance com consentimento".

A pergunta prática não é "o que é um vetor", mas como escolher o modelo, como medir similaridade e como indexar milhões de documentos sem consultar cada um linearmente. Este post cobre esses três pontos.

Como funciona na prática

O fluxo básico tem três passos:

  1. Encoder — modelo de embedding converte texto em vetor (tipicamente 384 a 3072 dimensões)
  2. Indexação — vetores são armazenados em um vector store (Chroma, Pinecone, FAISS)
  3. Consulta — a pergunta também vira vetor; o sistema retorna os documentos mais próximos
Busca por similaridade

Digite uma consulta e clique em Buscar para ver a similaridade semântica

A demo acima simula o que acontece na consulta: cada documento recebe um score de similaridade em relação à pergunta. Documentos sobre Open Finance sobem no ranking quando a consulta é sobre compartilhamento de dados financeiros — mesmo sem palavras idênticas.

Medindo similaridade: cosine similarity

A métrica mais usada entre embeddings é a similaridade de cosseno — mede o ângulo entre dois vetores, ignorando magnitude:

import numpy as np

def cosine_similarity(a: np.ndarray, b: np.ndarray) -> float:
    return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))

# Vetores normalizados: valores entre -1 e 1 (tipicamente 0 a 1 para embeddings de texto)
query = np.array([0.12, 0.45, 0.88, 0.31])
doc_a = np.array([0.15, 0.42, 0.85, 0.29])  # similar
doc_b = np.array([0.91, 0.05, 0.12, 0.77])  # diferente

print(cosine_similarity(query, doc_a))  # ~0.99
print(cosine_similarity(query, doc_b))  # ~0.35

Na prática, você não calcula isso manualmente — bibliotecas e vector stores fazem busca aproximada de vizinho mais próximo (ANN) em escala.

Gerando embeddings em Python

Com OpenAI:

from openai import OpenAI

client = OpenAI()

def embed_texts(texts: list[str], model: str = "text-embedding-3-small") -> list[list[float]]:
    response = client.embeddings.create(input=texts, model=model)
    return [item.embedding for item in response.data]

documents = [
    "Open Finance permite compartilhar dados financeiros com consentimento.",
    "Fine-tuning ajusta pesos do modelo com dados rotulados.",
]

vectors = embed_texts(documents)
query_vector = embed_texts(["como funciona open finance?"])[0]

Com sentence-transformers (local, sem API):

from sentence_transformers import SentenceTransformer

model = SentenceTransformer("all-MiniLM-L6-v2")

documents = [
    "Open Finance permite compartilhar dados financeiros com consentimento.",
    "Fine-tuning ajusta pesos do modelo com dados rotulados.",
]

doc_vectors = model.encode(documents)
query_vector = model.encode("como funciona open finance?")

# Similaridade
from sentence_transformers.util import cos_sim
scores = cos_sim(query_vector, doc_vectors)
print(scores)  # tensor([[0.72, 0.18]])

Três decisões ao escolher modelo:

  • Dimensão — vetores maiores capturam mais nuance, mas custam mais storage e latência
  • Domínio — modelos generalistas funcionam bem; domínios muito específicos (jurídico, médico) podem pedir fine-tune do encoder
  • Consistência — use o mesmo modelo na indexação e na consulta; trocar exige reindexar tudo

Indexação e busca em escala

Para poucos documentos, similaridade linear basta. Em produção, use ANN (Approximate Nearest Neighbors):

import chromadb

client = chromadb.PersistentClient(path="./chroma_db")
collection = client.get_or_create_collection("docs")

collection.add(
    documents=documents,
    ids=["doc1", "doc2"],
    embeddings=vectors,  # ou deixe o Chroma gerar com embedding function
)

results = collection.query(
    query_embeddings=[query_vector],
    n_results=3,
)
print(results["documents"])

FAISS (Facebook AI Similarity Search) é alternativa para volumes massivos em memória ou disco. Pinecone e pgvector servem quando você quer vector store gerenciado ou SQL integrado.

Além do básico: pontos de atenção

Limitações e boas práticas:

  • Busca semântica não é mágica — perguntas vagas retornam resultados vagos; chunking e metadados ajudam
  • Idioma — modelos multilíngues (ex.: multilingual-e5) para conteúdo em PT e EN
  • Normalização — muitos modelos já retornam vetores normalizados; verifique na documentação
  • Custo — embedding de milhões de chunks via API tem custo; cache e batch reduzem
  • Avaliação — meça precision@k com perguntas reais antes de confiar no retrieval

Armadilhas comuns:

  • Reindexar com modelo diferente sem avisar — scores ficam incomparáveis
  • Embedar documentos inteiros sem chunking — vetor diluído, busca imprecisa
  • Ignorar metadados — filtrar por data, tipo ou tenant antes da busca vetorial melhora muito

Conclusão

Embeddings transformam texto em busca por significado. O fluxo é: encoder gera vetores, vector store indexa, consulta retorna os mais similares via cosine similarity ou ANN.

Domine isso antes de montar um RAG — a qualidade do retrieval depende quase inteiramente da qualidade dos embeddings e do chunking. O próximo passo natural é avaliar se o sistema RAG completo está pronto para produção.

Referencias

  1. OpenAI — Guia de Embeddings
  2. Sentence Transformers — Documentação
  3. Chroma — Documentação
  4. FAISS — Wiki
  5. MTEB Leaderboard — Embedding models
  6. Pinecone — What are embeddings?