Embeddings: Teoria e Prática
Cada sistema de busca semântica, cada pipeline RAG e cada aplicação de IA que trabalha com linguagem natural tem um componente fundamental em comum: os embeddings. São a tradução do significado em números, a ponte entre o mundo do texto e o da matemática. Sem embeddings, um banco de dados não poderia distinguir “cão” de “automóvel” — com os embeddings, sabe que “cão” está mais perto de “gato” do que de “torradeira”.
No primeiro artigo desta série configuramos o pgvector e aprendemos a salvar e consultar vetores no PostgreSQL. Mas de onde vêm esses vetores? Como gerar um embedding de qualidade? E sobretudo, qual modelo escolher entre as dezenas disponíveis? Neste artigo respondemos a todas essas perguntas, da teoria matemática à prática com Python e PostgreSQL.
Visão Geral da Série
| # | Artigo | Foco |
|---|---|---|
| 1 | pgvector | Instalação, operadores, indexing |
| 2 | Você está aqui - Embeddings | Modelos, distâncias, geração |
| 3 | RAG com PostgreSQL | Pipeline RAG end-to-end |
| 4 | Similarity Search Avançada | Hybrid search, filtering |
| 5 | Indexing e Performance | HNSW, IVFFlat, tuning |
| 6 | RAG em Produção | Monitoring, scaling, CI/CD |
O Que Você Aprenderá
- O que é um embedding e por que é fundamental para a IA moderna
- A evolução histórica: de one-hot encoding a Word2Vec, GloVe, BERT e Sentence Transformers
- As propriedades matemáticas dos embeddings: analogias vetoriais e clustering semântico
- As quatro métricas de distância com fórmulas e casos de uso
- Como gerar embeddings com Python: local e via API
- Como salvar e consultar embeddings no PostgreSQL com pgvector
- Embeddings multimodais: texto, imagens, áudio e código
- Como avaliar a qualidade de um modelo de embedding (MTEB)
- Custos e estratégias de escalonamento para milhões de documentos
1. O Que São os Embeddings
Um embedding é uma representação vetorial densa de um objeto (palavra, frase, documento, imagem) em um espaço contínuo de dimensionalidade reduzida. Em termos práticos, é um array de números de ponto flutuante que captura o “significado” desse objeto.
# O embedding da frase "O gato dorme no sofá"
# gerado com text-embedding-3-small da OpenAI (1536 dimensões)
embedding = [
0.0231, -0.0456, 0.0891, -0.0123, 0.0567, -0.0234,
0.0789, -0.0345, 0.0123, -0.0678, 0.0456, -0.0891,
# ... outros 1524 valores ...
]
print(f"Tipo: {type(embedding)}") # <class 'list'>
print(f"Dimensões: {len(embedding)}") # 1536
A intuição chave é esta: em um espaço vetorial bem treinado, a distância geométrica entre dois vetores reflete a similaridade semântica entre os conceitos que representam. Frases com significado similar terão vetores próximos, frases com significados diferentes estarão distantes.
Características dos Embeddings
| Propriedade | Descrição | Exemplo |
|---|---|---|
| Densos | Cada dimensão tem um valor diferente de zero | [0.023, -0.045, 0.089, ...] |
| Contínuos | Valores reais, não discretos | Cada componente é um float32/float16 |
| De dimensionalidade fixa | O mesmo modelo produz sempre vetores do mesmo comprimento | 384, 768, 1536 ou 3072 dimensões |
| Semanticamente significativos | As distâncias entre vetores refletem relações de significado | sim(“gato”, “felino”) > sim(“gato”, “carro”) |
Se pensarmos no espaço dos embeddings como um mapa, os conceitos similares formam “bairros”: os animais em uma zona, os veículos em outra, as emoções em outra ainda. Mas a beleza está no fato de que essas relações emergem automaticamente do treinamento, não são programadas manualmente.
2. De Palavras a Vetores: Evolução Histórica
A história dos embeddings é uma progressão de ideias cada vez mais sofisticadas, cada uma resolvendo os limites da anterior. Compreender essa evolução ajuda a entender por que os modelos modernos funcionam tão bem.
2.1 One-Hot Encoding (Anos 90)
A abordagem mais simples: cada palavra é representada por um vetor com um único 1 e todos os outros 0. Se o vocabulário tem V palavras, cada vetor tem V dimensões.
# Vocabulário: ["gato", "cão", "peixe", "carro", "moto"]
# Dimensão do vetor = dimensão do vocabulário = 5
gato = [1, 0, 0, 0, 0]
cao = [0, 1, 0, 0, 0]
peixe = [0, 0, 1, 0, 0]
carro = [0, 0, 0, 1, 0]
moto = [0, 0, 0, 0, 1]
# Problema 1: a distância entre "gato" e "cão" é igual
# à distância entre "gato" e "carro"
import numpy as np
dist_gato_cao = np.linalg.norm(
np.array(gato) - np.array(cao)
) # sqrt(2) = 1.414
dist_gato_carro = np.linalg.norm(
np.array(gato) - np.array(carro)
) # sqrt(2) = 1.414 -- idêntica!
# Problema 2: com um vocabulário de 100.000 palavras,
# cada vetor tem 100.000 dimensões (esparso, ineficiente)
Limites do One-Hot Encoding
Dimensionalidade explosiva: para um vocabulário de 100K palavras, cada vetor tem 100K dimensões, quase todas a zero. Nenhuma informação semântica: todos os vetores são equidistantes entre si. “Gato” está igualmente distante de “felino” quanto de “terremoto”. Esta abordagem não captura nenhuma relação de significado entre as palavras.
2.2 TF-IDF (Term Frequency - Inverse Document Frequency)
Um passo adiante: em vez de 0/1, os componentes do vetor indicam o quão importante é uma palavra em um documento em relação ao corpus inteiro. Mas cada documento torna-se um vetor esparso na dimensionalidade do vocabulário.
from sklearn.feature_extraction.text import TfidfVectorizer
documentos = [
"o gato dorme no sofá",
"o cão brinca no jardim",
"o automóvel corre na estrada",
"o felino descansa na poltrona",
]
vectorizer = TfidfVectorizer()
tfidf_matrix = vectorizer.fit_transform(documentos)
# Resultado: matriz esparsa (4 documentos x N termos)
print(f"Shape: {tfidf_matrix.shape}") # (4, 14)
print(f"Termos: {vectorizer.get_feature_names_out()}")
# Problema: "gato dorme" e "felino descansa" estão distantes
# porque usam palavras diferentes, embora o significado seja similar
from sklearn.metrics.pairwise import cosine_similarity
sim = cosine_similarity(tfidf_matrix[0:1], tfidf_matrix[3:4])
print(f"Similaridade gato-felino: {sim[0][0]:.3f}") # ~0.07 (baixa!)
TF-IDF melhora o one-hot encoding pesando as palavras por importância, mas sofre do mesmo problema fundamental: não entende que “gato” e “felino” são sinônimos, porque raciocina apenas com correspondências lexicais exatas.
2.3 Word2Vec: A Revolução (2013)
Em 2013, Tomas Mikolov e sua equipe no Google publicaram o Word2Vec, que mudou tudo. A ideia genial: uma palavra é definida pelo contexto em que aparece. Palavras que aparecem em contextos similares terão representações similares.
Word2Vec usa redes neurais superficiais para aprender vetores densos (tipicamente 100-300 dimensões) de grandes corpora de texto. Duas arquiteturas:
Arquiteturas Word2Vec
| Arquitetura | Entrada | Saída | Descrição |
|---|---|---|---|
| CBOW | Palavras de contexto | Palavra alvo | Prediz a palavra central dado o contexto circundante |
| Skip-gram | Palavra alvo | Palavras de contexto | Prediz as palavras circundantes dada a palavra central |
from gensim.models import Word2Vec
# Corpus de exemplo (em produção: milhões de frases)
frases = [
["o", "gato", "dorme", "no", "sofá"],
["o", "cão", "brinca", "no", "jardim"],
["o", "felino", "descansa", "na", "poltrona"],
["o", "cão", "corre", "no", "parque"],
]
# Treinamento Word2Vec (Skip-gram)
model = Word2Vec(
sentences=frases,
vector_size=100, # dimensionalidade do embedding
window=5, # contexto: 5 palavras antes e depois
min_count=1, # incluir palavras com pelo menos 1 ocorrência
sg=1, # 1 = Skip-gram, 0 = CBOW
epochs=100
)
# Agora "gato" e "felino" estão próximos!
print(model.wv.most_similar("gato", topn=3))
# [('felino', 0.92), ('cão', 0.85), ('dorme', 0.71)]
# Acesso ao vetor
vetor_gato = model.wv["gato"]
print(f"Dimensões: {vetor_gato.shape}") # (100,)
print(f"Primeiros 5: {vetor_gato[:5]}")
2.4 GloVe: Global Vectors (2014)
Stanford desenvolveu o GloVe (Global Vectors for Word Representation) com uma abordagem diferente: em vez de uma rede neural, o GloVe fatoriza a matriz de co-ocorrência global do corpus. Combina as vantagens dos métodos estatísticos globais (como LSA) com os do contexto local do Word2Vec.
GloVe minimiza uma função de custo que garante que o produto escalar entre dois vetores de palavras seja proporcional ao logaritmo de sua probabilidade de co-ocorrência:
2.5 FastText: Subword Embeddings (2016)
O Facebook AI Research (FAIR) estendeu o Word2Vec com o FastText, que representa cada palavra como um conjunto de n-gramas de caracteres. Isso resolve dois problemas críticos:
- Palavras raras ou fora do vocabulário (OOV): FastText pode gerar embeddings para palavras nunca vistas, compondo os vetores dos sub-segmentos
- Morfologia: palavras morfologicamente correlacionadas (ex. “correr”, “corria”, “corredor”) compartilham n-gramas e portanto têm vetores similares
Evolução: De Representações Esparsas a Densas
| Método | Ano | Tipo | Dimensões Típicas | Semântica |
|---|---|---|---|---|
| One-hot | - | Esparso | V (vocabulário) | Nenhuma |
| TF-IDF | 1972 | Esparso | V (vocabulário) | Estatística |
| Word2Vec | 2013 | Denso | 100-300 | Contextual local |
| GloVe | 2014 | Denso | 50-300 | Global + local |
| FastText | 2016 | Denso | 100-300 | Subword + contexto |
| BERT | 2018 | Denso | 768 | Contextual dinâmica |
| Sentence Transformers | 2019 | Denso | 384-1024 | Frases inteiras |
3. Propriedades Matemáticas dos Embeddings
Uma das descobertas mais fascinantes do Word2Vec é que o espaço vetorial aprende relações algébricas entre conceitos. As operações aritméticas nos vetores produzem resultados semanticamente coerentes.
3.1 Analogias Vetoriais
A famosa analogia: king - man + woman = queen. Em termos vetoriais, a diferença entre “king” e “man” captura o conceito de “realeza”, e adicionando-o a “woman” obtém-se “queen”. Formalmente:
import gensim.downloader as api
# Carrega embeddings GloVe pré-treinados
model = api.load("glove-wiki-gigaword-100")
# king - man + woman = ?
result = model.most_similar(
positive=["king", "woman"],
negative=["man"],
topn=3
)
print(result)
# [('queen', 0.7698), ('princess', 0.6450), ('monarch', 0.6345)]
# Outras analogias que funcionam:
# Paris - França + Itália = Roma
result2 = model.most_similar(
positive=["paris", "italy"],
negative=["france"],
topn=1
)
print(result2) # [('rome', 0.8722)]
# bom - mau + triste = ?
result3 = model.most_similar(
positive=["good", "sad"],
negative=["bad"],
topn=1
)
print(result3) # [('happy', 0.6891)]
3.2 Clustering Semântico
Os embeddings formam naturalmente clusters no espaço vetorial. Se projetarmos os vetores em 2D (usando t-SNE ou UMAP), observamos que palavras da mesma categoria se agrupam: animais perto de animais, países perto de países, profissões perto de profissões.
Esta propriedade é fundamental para as aplicações práticas: a similarity search funciona precisamente porque documentos sobre temas similares têm embeddings próximos no espaço vetorial.
4. Embeddings Modernos: Contextual Embeddings
Word2Vec e GloVe geram um único vetor por palavra, independente do contexto. Mas “banco” tem significados diferentes em “banco do parque” e “banco de peixes”. Os contextual embeddings, introduzidos com BERT em 2018, resolvem este problema: a mesma palavra tem vetores diferentes de acordo com o contexto.
4.1 BERT Embeddings
BERT (Bidirectional Encoder Representations from Transformers) processa a frase inteira e produz um vetor para cada token. Para obter um embedding da frase inteira, usa-se tipicamente:
- CLS token: o primeiro token especial [CLS] contém uma representação agregada da frase
- Mean pooling: média de todos os vetores de tokens — geralmente produz melhores resultados para similarity search
BERT não é ótimo para similarity search
O BERT original não foi treinado para produzir embeddings de frase de qualidade. O CLS token é otimizado para classificação, não para similaridade semântica. Para similarity search, são necessários modelos especializados como Sentence Transformers.
4.2 Sentence Transformers (SBERT)
Em 2019, Reimers e Gurevych introduziram o Sentence-BERT, fine-tuning do BERT com uma estrutura siamesa para produzir embeddings de frase significativos. Isso revolucionou a similarity search: pela primeira vez, era possível comparar frases com uma simples distância cosseno, obtendo resultados de alta qualidade.
4.3 Modelos de Embedding: Comparação Completa
Modelos de Embedding em Comparação (2026)
| Modelo | Fornecedor | Dimensões | MTEB Score | Custo / 1M token | Notas |
|---|---|---|---|---|---|
| text-embedding-3-small | OpenAI | 1536 | 62.3 | $0.02 | Bom custo-benefício |
| text-embedding-3-large | OpenAI | 3072 | 64.6 | $0.13 | Máxima qualidade OpenAI |
| embed-v3 | Cohere | 1024 | 64.5 | $0.10 | Suporta mais de 100 idiomas |
| voyage-3 | Voyage AI | 1024 | 67.1 | $0.06 | Top para retrieval |
| all-MiniLM-L6-v2 | HuggingFace | 384 | 56.3 | Gratuito | Rápido, local, compacto |
| all-mpnet-base-v2 | HuggingFace | 768 | 57.8 | Gratuito | Melhor modelo open-source base |
| gte-large-en-v1.5 | Alibaba (HF) | 1024 | 65.4 | Gratuito | Competitivo com modelos comerciais |
| bge-large-en-v1.5 | BAAI (HF) | 1024 | 64.2 | Gratuito | Ótimo para RAG |
Como Escolher o Modelo
- Protótipo / orçamento limitado: all-MiniLM-L6-v2 (gratuito, rápido, 384 dim)
- Produção, custos controlados: text-embedding-3-small (OpenAI, $0.02/1M token)
- Máxima qualidade de retrieval: voyage-3 ou gte-large-en-v1.5
- Multilingual: Cohere embed-v3 (mais de 100 idiomas)
- Self-hosted / privacidade: bge-large-en-v1.5 ou gte-large-en-v1.5
5. Medidas de Distância entre Vetores
A escolha da métrica de distância influencia diretamente a qualidade da similarity search. Vejamos as quatro métricas principais com suas fórmulas matemáticas, seus pontos fortes e quando usá-las.
5.1 Cosine Similarity
A métrica mais usada para embeddings de texto. Mede o ângulo entre dois vetores, ignorando sua magnitude (comprimento). Dois vetores que apontam na mesma direção têm cosine similarity 1, ortogonais 0, opostos -1.
No pgvector, o operador <=> calcula a cosine distance
(= 1 - cosine similarity), onde 0 significa idênticos e 2 significa opostos.
5.2 Distância Euclidiana (L2)
A distância “em linha reta” entre dois pontos no espaço. Leva em conta tanto a direção quanto a magnitude dos vetores.
No pgvector, o operador <-> calcula a distância L2.
5.3 Dot Product (Produto Escalar)
O produto escalar mede tanto a direção quanto a magnitude. Para vetores normalizados (norma = 1), o dot product é equivalente à cosine similarity.
No pgvector, o operador <#> calcula o negative inner product (para compatibilidade
com ORDER BY ASC).
5.4 Distância de Manhattan (L1)
Soma das diferenças absolutas componente por componente. Menos sensível a outliers em relação à distância euclidiana.
import numpy as np
from scipy.spatial.distance import cosine, euclidean, cityblock
# Dois vetores de exemplo (normalizados)
a = np.array([0.5, 0.3, 0.8, 0.1, 0.6])
b = np.array([0.4, 0.35, 0.75, 0.15, 0.55])
# Normalização L2
a_norm = a / np.linalg.norm(a)
b_norm = b / np.linalg.norm(b)
# 1. Cosine Similarity (1 - cosine distance)
cos_sim = 1 - cosine(a_norm, b_norm)
print(f"Cosine similarity: {cos_sim:.6f}") # ~0.999
# 2. Distância Euclidiana (L2)
l2_dist = euclidean(a_norm, b_norm)
print(f"Distância L2: {l2_dist:.6f}") # ~0.042
# 3. Dot Product (para vetores normalizados = cosine similarity)
dot = np.dot(a_norm, b_norm)
print(f"Dot product: {dot:.6f}") # ~0.999
# 4. Distância Manhattan (L1)
l1_dist = cityblock(a_norm, b_norm)
print(f"Distância L1: {l1_dist:.6f}") # ~0.072
# Relação L2-Cosine para vetores normalizados:
# d_L2^2 = 2 * (1 - cos_sim)
print(f"\nVerificação: L2^2 = {l2_dist**2:.6f}")
print(f"2*(1-cos) = {2*(1-cos_sim):.6f}") # igual!
Quando Usar Qual Métrica
| Métrica | Operador pgvector | Usar Quando | Evitar Quando |
|---|---|---|---|
| Cosine | <=> | Embeddings de texto, quando a magnitude não importa | Dados espaciais onde a magnitude é significativa |
| L2 (Euclidiana) | <-> | Imagens, dados numéricos, quando a magnitude importa | Vetores com escalas diferentes entre componentes |
| Dot Product | <#> | Vetores já normalizados (performance ligeiramente melhor) | Vetores não normalizados (resultados distorcidos pela magnitude) |
| Manhattan (L1) | Não nativo no pgvector | Dados esparsos, robustez a outliers | Uso geral com embeddings densos |
Regra Prática
Para 95% dos casos com embeddings de texto, use cosine distance
(<=> no pgvector). Os modelos de embedding modernos produzem vetores
já normalizados, o que torna cosine e dot product praticamente equivalentes. A distância
euclidiana faz sentido para dados espaciais ou quando a magnitude do vetor transporta informação.
6. Gerar Embeddings em Python
Vejamos agora como gerar embeddings com três abordagens diferentes: modelos locais com Sentence Transformers, API da OpenAI e HuggingFace Inference API. Cada abordagem tem vantagens e compromissos específicos.
6.1 Sentence Transformers (Local)
A abordagem mais flexível e privada: o modelo roda na sua máquina, nenhum dado sai da rede, nenhum custo por chamada API.
# pip install sentence-transformers
from sentence_transformers import SentenceTransformer
import numpy as np
# Carrega o modelo (baixado automaticamente no primeiro uso)
model = SentenceTransformer("all-MiniLM-L6-v2")
# Embedding de uma única frase
frase = "PostgreSQL é um banco de dados relacional open-source"
embedding = model.encode(frase)
print(f"Tipo: {type(embedding)}") # numpy.ndarray
print(f"Dimensões: {embedding.shape}") # (384,)
# Embedding de várias frases (batch - muito mais eficiente)
frases = [
"PostgreSQL é um banco de dados relacional open-source",
"pgvector adiciona suporte a vetores ao PostgreSQL",
"O machine learning requer grandes quantidades de dados",
"A pizza margherita é um prato típico napolitano",
]
embeddings = model.encode(
frases,
batch_size=32,
show_progress_bar=True,
normalize_embeddings=True
)
print(f"Shape: {embeddings.shape}") # (4, 384)
6.2 OpenAI Embedding API
A API da OpenAI oferece modelos de alta qualidade sem gestão de infraestrutura. Ideal para produção com volumes moderados.
# pip install openai
from openai import OpenAI
import os
client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
def get_embeddings(
texts: list[str],
model: str = "text-embedding-3-small"
) -> list[list[float]]:
"""Gera embeddings para uma lista de textos."""
response = client.embeddings.create(
input=texts,
model=model,
)
return [item.embedding for item in response.data]
# Embedding individual
texto = "PostgreSQL como vector database para IA"
embedding = get_embeddings([texto])[0]
print(f"Dimensões: {len(embedding)}") # 1536
6.3 HuggingFace Inference API
Um compromisso entre modelos locais e APIs comerciais: acesso a milhares de modelos open-source via API, com um plano gratuito generoso.
# pip install huggingface_hub
from huggingface_hub import InferenceClient
import os
client = InferenceClient(
token=os.getenv("HF_TOKEN")
)
def get_hf_embeddings(
texts: list[str],
model: str = "BAAI/bge-large-en-v1.5"
) -> list[list[float]]:
"""Gera embeddings usando HuggingFace Inference API."""
result = client.feature_extraction(
text=texts,
model=model,
)
return result
# Gera embeddings
textos = [
"Vector search com PostgreSQL e pgvector",
"Como criar índices HNSW para busca rápida",
]
embeddings = get_hf_embeddings(textos)
print(f"Embeddings: {len(embeddings)}") # 2
print(f"Dimensões: {len(embeddings[0])}") # 1024 (bge-large)
6.4 Batch Processing Eficiente
Quando é preciso gerar embeddings para milhares ou milhões de documentos, a eficiência do batch processing torna-se crítica.
import time
from typing import Generator
from sentence_transformers import SentenceTransformer
import numpy as np
def chunk_list(
lst: list, chunk_size: int
) -> Generator[list, None, None]:
"""Divide uma lista em chunks de tamanho fixo."""
for i in range(0, len(lst), chunk_size):
yield lst[i:i + chunk_size]
def generate_embeddings_batch(
texts: list[str],
model_name: str = "all-MiniLM-L6-v2",
batch_size: int = 256,
device: str = "cpu" # "cuda" para GPU
) -> np.ndarray:
"""Gera embeddings em batch com acompanhamento de progresso."""
model = SentenceTransformer(model_name, device=device)
all_embeddings = []
total_batches = (len(texts) + batch_size - 1) // batch_size
start = time.time()
for i, batch in enumerate(chunk_list(texts, batch_size)):
batch_emb = model.encode(
batch,
batch_size=batch_size,
normalize_embeddings=True,
show_progress_bar=False
)
all_embeddings.append(batch_emb)
elapsed = time.time() - start
rate = (i + 1) * batch_size / elapsed
print(
f"Batch {i+1}/{total_batches} - "
f"{rate:.0f} textos/seg"
)
return np.vstack(all_embeddings)
# Uso
texts = [f"Documento número {i}" for i in range(10_000)]
embeddings = generate_embeddings_batch(
texts,
batch_size=256,
device="cuda" # usa GPU se disponível
)
print(f"Shape final: {embeddings.shape}") # (10000, 384)
7. Armazenar Embeddings no PostgreSQL
Agora que sabemos gerar embeddings, vejamos como salvá-los no PostgreSQL com pgvector e executar consultas de similarity search. Esta é a conexão prática com o artigo 1 da série.
7.1 Esquema da Tabela
-- Ativa pgvector
CREATE EXTENSION IF NOT EXISTS vector;
-- Tabela de documentos com embedding
CREATE TABLE documents (
id BIGSERIAL PRIMARY KEY,
title VARCHAR(500) NOT NULL,
content TEXT NOT NULL,
source VARCHAR(255),
category VARCHAR(100),
embedding vector(384), -- dimensão do modelo escolhido
created_at TIMESTAMPTZ DEFAULT NOW(),
metadata JSONB DEFAULT '{}'::jsonb
);
-- Índice HNSW para busca rápida (cosine distance)
CREATE INDEX idx_documents_embedding
ON documents
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 200);
-- Índice em categoria para filtros combinados
CREATE INDEX idx_documents_category
ON documents (category);
7.2 Inserção a partir de Python
import psycopg2
from psycopg2.extras import execute_values
from sentence_transformers import SentenceTransformer
import numpy as np
# Configuração
DB_CONFIG = {
"host": "localhost",
"port": 5432,
"dbname": "vectordb",
"user": "admin",
"password": "secret_password",
}
# 1. Gera embeddings
model = SentenceTransformer("all-MiniLM-L6-v2")
documentos = [
{
"title": "Introdução ao pgvector",
"content": "pgvector é uma extensão do PostgreSQL para vetores...",
"source": "blog",
"category": "database"
},
{
"title": "RAG com LangChain",
"content": "Retrieval Augmented Generation combina retrieval...",
"source": "tutorial",
"category": "ai"
},
{
"title": "Python para Data Science",
"content": "Python é a linguagem mais usada para data science...",
"source": "guide",
"category": "programming"
},
]
# Gera embeddings para os conteúdos
textos = [d["content"] for d in documentos]
embeddings = model.encode(textos, normalize_embeddings=True)
# 2. Salva no PostgreSQL
conn = psycopg2.connect(**DB_CONFIG)
cur = conn.cursor()
values = []
for doc, emb in zip(documentos, embeddings):
values.append((
doc["title"],
doc["content"],
doc["source"],
doc["category"],
emb.tolist()
))
execute_values(
cur,
"""INSERT INTO documents
(title, content, source, category, embedding)
VALUES %s""",
values,
template="(%s, %s, %s, %s, %s::vector)"
)
conn.commit()
print(f"Inseridos {len(values)} documentos com embeddings")
cur.close()
conn.close()
7.3 Similarity Search a partir de Python
def similarity_search(
query: str,
top_k: int = 5,
category: str = None,
threshold: float = 0.3
) -> list[dict]:
"""Busca documentos similares à consulta."""
query_embedding = model.encode(
query, normalize_embeddings=True
).tolist()
conn = psycopg2.connect(**DB_CONFIG)
cur = conn.cursor()
if category:
cur.execute("""
SELECT id, title, content, category,
1 - (embedding <=> %s::vector) AS similarity
FROM documents
WHERE category = %s
AND 1 - (embedding <=> %s::vector) > %s
ORDER BY embedding <=> %s::vector
LIMIT %s
""", (
query_embedding, category,
query_embedding, threshold,
query_embedding, top_k
))
else:
cur.execute("""
SELECT id, title, content, category,
1 - (embedding <=> %s::vector) AS similarity
FROM documents
WHERE 1 - (embedding <=> %s::vector) > %s
ORDER BY embedding <=> %s::vector
LIMIT %s
""", (
query_embedding,
query_embedding, threshold,
query_embedding, top_k
))
results = []
for row in cur.fetchall():
results.append({
"id": row[0],
"title": row[1],
"content": row[2][:200],
"category": row[3],
"similarity": round(row[4], 4),
})
cur.close()
conn.close()
return results
# Exemplo de uso
resultados = similarity_search(
"como usar vetores em um banco de dados",
top_k=3,
category="database"
)
for r in resultados:
print(f"[{r['similarity']}] {r['title']}")
7.4 Indexing: HNSW vs IVFFlat
Para datasets com mais de alguns milhares de documentos, um índice é essencial para performance aceitável. pgvector oferece dois tipos de índice:
HNSW vs IVFFlat
| Característica | HNSW | IVFFlat |
|---|---|---|
| Velocidade de consulta | Muito rápida | Rápida |
| Recall | 95-99% | 85-95% |
| Tempo de construção | Lento (minutos) | Rápido (segundos) |
| Memória | Alta (grafo em RAM) | Baixa (centroide) |
| Insert/Update | Bom (atualização incremental) | Requer reconstrução periódica |
| Recomendado para | Produção, alta qualidade | Prototipagem, datasets estáticos |
-- HNSW (recomendado para produção)
CREATE INDEX idx_hnsw_cosine
ON documents
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 200);
-- IVFFlat (mais rápido de construir)
CREATE INDEX idx_ivfflat_cosine
ON documents
USING ivfflat (embedding vector_cosine_ops)
WITH (lists = 100);
-- Parâmetros de consulta para controlar recall vs velocidade
SET hnsw.ef_search = 100;
SET ivfflat.probes = 10;
8. Embeddings para Diferentes Tipos de Dados
Os embeddings não se limitam ao texto. Os modelos modernos podem gerar representações vetoriais para imagens, áudio, código fonte e até dados multimodais.
Embeddings Multimodais: Modelos por Tipo de Dado
| Tipo de Dado | Modelo | Dimensões | Caso de Uso |
|---|---|---|---|
| Texto | all-MiniLM-L6-v2, text-embedding-3-small | 384-3072 | Busca semântica, RAG, classificação |
| Imagens | CLIP (OpenAI), SigLIP (Google) | 512-768 | Busca de imagens, classificação visual |
| Áudio | Whisper, CLAP | 512-1280 | Busca de áudio, classificação musical |
| Código | CodeBERT, StarCoder embeddings | 768 | Code search, duplicate detection |
| Multimodal | CLIP, ImageBind (Meta) | 512-1024 | Busca cross-modal (texto para imagem) |
# pip install transformers pillow
from transformers import CLIPProcessor, CLIPModel
from PIL import Image
import torch
import numpy as np
# Carrega CLIP
model = CLIPModel.from_pretrained("openai/clip-vit-base-patch32")
processor = CLIPProcessor.from_pretrained("openai/clip-vit-base-patch32")
# Embedding de uma imagem
image = Image.open("foto_gato.jpg")
inputs = processor(images=image, return_tensors="pt")
with torch.no_grad():
image_embedding = model.get_image_features(**inputs)
image_emb = image_embedding[0].numpy()
print(f"Image embedding: {image_emb.shape}") # (512,)
# Embedding de texto (no MESMO espaço!)
text_inputs = processor(
text=["um gato dormindo", "um cão brincando"],
return_tensors="pt",
padding=True
)
with torch.no_grad():
text_embeddings = model.get_text_features(**text_inputs)
text_embs = text_embeddings.numpy()
# Calcula similaridade cross-modal
from numpy.linalg import norm
for i, text in enumerate(["um gato dormindo", "um cão brincando"]):
sim = np.dot(image_emb, text_embs[i]) / (
norm(image_emb) * norm(text_embs[i])
)
print(f"Similaridade '{text}': {sim:.4f}")
# "um gato dormindo" terá maior similaridade com foto_gato.jpg
A potência do CLIP é que texto e imagens vivem no mesmo espaço vetorial. Você pode buscar imagens com uma consulta textual ou encontrar textos relacionados a uma imagem. Isso abre cenários como a busca multimodal no PostgreSQL: você salva embeddings CLIP na mesma tabela pgvector e busca com consultas textuais.
9. Avaliar a Qualidade dos Embeddings
Como saber se um modelo de embedding é “bom”? A resposta depende da tarefa específica, mas existem benchmarks padronizados e métricas objetivas.
9.1 MTEB: Massive Text Embedding Benchmark
MTEB é o benchmark de referência para avaliar modelos de embedding. Mede o desempenho em mais de 58 tarefas agrupadas em 8 categorias:
- Retrieval: encontrar documentos relevantes dada uma consulta
- Semantic Textual Similarity (STS): quão similares são duas frases
- Classification: classificar textos em categorias
- Clustering: agrupar textos similares
- Pair Classification: estabelecer se dois textos estão relacionados
- Reranking: reordenar resultados por relevância
- Summarization: qualidade dos resumos
- BitextMining: encontrar traduções paralelas
9.2 Avaliação Intrínseca vs Extrínseca
Duas Abordagens de Avaliação
| Tipo | O Que Mede | Exemplo | Quando Usar |
|---|---|---|---|
| Intrínseca | Propriedades dos vetores em si | Analogias, clustering, STS | Comparação rápida entre modelos |
| Extrínseca | Desempenho na tarefa final | Qualidade RAG, precisão de busca | Decisão final em produção |
Conselho Prático
Não confie apenas na pontuação MTEB. Um modelo pode ter pontuação MTEB alta mas funcionar mal no seu domínio específico. Avalie sempre no seu dataset: crie um pequeno conjunto de consultas e documentos relevantes do seu domínio, e meça nDCG e MAP. Isso dará uma estimativa muito mais confiável do desempenho real.
10. Redução da Dimensionalidade
Os vetores de alta dimensionalidade são difíceis de visualizar e podem ser custosos em termos de armazenamento e computação. As técnicas de redução de dimensionalidade ajudam tanto para a visualização quanto para a otimização.
10.1 Técnicas de Visualização
Técnicas de Redução Dimensional
| Técnica | Preserva | Velocidade | Uso Típico |
|---|---|---|---|
| PCA | Variância global | Muito rápida | Redução de dimensões para armazenamento, pré-processamento |
| t-SNE | Estrutura local | Lenta | Visualização 2D de clusters |
| UMAP | Estrutura local + global | Média | Visualização 2D, também para redução pré-indexing |
10.2 Matryoshka Embeddings
Uma técnica recente e inovadora: os embeddings Matryoshka Representation Learning (MRL) são treinados de modo que os primeiros N componentes do vetor já são um embedding válido. Você pode truncar o vetor de 1536 para 512 ou 256 dimensões mantendo boa qualidade.
11. Custos e Estratégias de Escalonamento
Quando se passa do protótipo para a produção, os custos de geração e armazenamento dos embeddings tornam-se um fator crítico. Vejamos uma análise detalhada.
Estimativa de Custos: 1M Documentos (média 500 token/doc)
| Modelo | Custo de Geração | Dim. Vetor | Armazenamento (float32) | Total Inicial |
|---|---|---|---|---|
| all-MiniLM-L6-v2 | $0 (local) | 384 | ~1,4 GB | Apenas tempo GPU/CPU |
| text-embedding-3-small | ~$10 | 1536 | ~5,7 GB | ~$10 + armazenamento |
| text-embedding-3-large | ~$65 | 3072 | ~11,4 GB | ~$65 + armazenamento |
| voyage-3 | ~$30 | 1024 | ~3,8 GB | ~$30 + armazenamento |
Fórmula de armazenamento: N documentos * dimensões * 4 bytes (float32). Exemplo: 1M * 1536 * 4 = 5,7 GB apenas para os vetores.
11.2 Self-Hosted vs API: Trade-off
Comparação Self-Hosted vs API
| Aspecto | Self-Hosted | API (OpenAI, Cohere) |
|---|---|---|
| Custo inicial | Alto (GPU ~$1-3/hora) | Baixo (pagamento por uso) |
| Custo em volume | Mais econômico a partir de ~10M doc | Linear, escala com o volume |
| Latência | Baixa (sem rede) | 50-200ms por chamada |
| Privacidade | Dados permanecem on-premise | Dados enviados a terceiros |
| Manutenção | Gestão de GPU, atualizações, monitoramento | Zero |
| Qualidade | Depende do modelo escolhido | Geralmente alta e consistente |
11.3 Estratégias de Caching
O caching de embeddings é fundamental para reduzir custos e latência. Se o mesmo texto é solicitado várias vezes, não faz sentido regenerar o embedding.
12. Conexão com a Série PostgreSQL AI
Recapitulemos como os embeddings se integram no ecossistema que estamos construindo nesta série:
O Fluxo Completo: Do Artigo 1 ao Artigo 3
| Passo | Artigo | Ação |
|---|---|---|
| 1 | pgvector (Art. 1) | Configura PostgreSQL com pgvector, cria tabelas com colunas vector |
| 2 | Embeddings (Art. 2 - este) | Escolhe o modelo, gera embeddings, salva no pgvector |
| 3 | RAG com PostgreSQL (Art. 3) | Combina retrieval via pgvector com LLM para responder perguntas |
No artigo 1 preparamos a infraestrutura: PostgreSQL com pgvector instalado, tabelas com colunas vetoriais e índices HNSW configurados. Neste artigo preenchemos a lacuna fundamental: de onde vêm esses vetores, como escolher o modelo adequado e como gerá-los eficientemente. No próximo artigo construiremos uma pipeline RAG completa que usa todo esse stack: documentos indexados no pgvector, embeddings gerados on-the-fly para as consultas, e um LLM que gera respostas baseadas no contexto recuperado.
13. Conclusões e Checklist
Os embeddings são o componente fundamental que conecta a linguagem natural à matemática dos bancos de dados vetoriais. Escolher o modelo adequado, a métrica de distância apropriada e uma estratégia de escalonamento eficiente são decisões que impactam diretamente a qualidade e os custos do seu sistema de IA.
Checklist: Escolher o Modelo de Embedding Adequado
- Defina a tarefa: retrieval, classificação, clustering, STS?
- Identifique o idioma: apenas inglês, multilingual, ou domínio específico?
- Avalie as restrições: orçamento, privacidade, latência, infraestrutura disponível
- Escolha 2-3 candidatos da tabela de modelos (seção 4.3)
- Crie um conjunto de avaliação do seu domínio (50-100 consultas com documentos relevantes)
- Meça nDCG e MAP no seu dataset para cada candidato
- Calcule os custos em regime para o volume previsto (seção 11)
- Teste dimensões reduzidas: 512 dim frequentemente bastam para muitos casos de uso
- Implemente caching para reduzir custos de regeneração
- Monitore a qualidade ao longo do tempo com seu conjunto de avaliação
Próximo Artigo: RAG com PostgreSQL
No próximo artigo da série construiremos uma pipeline RAG completa (Retrieval Augmented Generation) usando PostgreSQL + pgvector como base de conhecimento. Veremos como combinar similarity search com chunking inteligente, como integrar LLMs como GPT-4 e Claude, e como medir a qualidade das respostas geradas.







