A melhor forma de usar embeddings de texto de maneira portátil é com Parquet e Polars
- Embeddings de texto são vetores gerados por grandes modelos de linguagem, uma forma de representar numericamente palavras, frases e documentos
- Em fevereiro de 2025, foram gerados embeddings para um total de 32.254 cartas de "Magic: The Gathering"
- Isso permite analisar matematicamente a similaridade com base no design e nas propriedades mecânicas das cartas
- Também é possível visualizar os embeddings gerados em 2D por meio de redução de dimensionalidade com UMAP
- O modelo de embedding usado foi gte-modernbert-base, e o processo detalhado está organizado no repositório no GitHub
- Esse conjunto de dados de embeddings está disponível no Hugging Face
Repensando a necessidade de bancos de dados vetoriais
- Em geral, bancos de dados vetoriais (faiss, qdrant, Pinecone) são usados para armazenar e buscar embeddings
- Porém, bancos de dados vetoriais exigem configuração complexa, e serviços em nuvem podem ter custo elevado
- Para dados em pequena escala (na casa das dezenas de milhares), é possível fazer buscas rápidas de similaridade com numpy mesmo sem um banco de dados vetorial
- Usando a operação de
dot product do numpy, é possível calcular similaridade cosseno de forma simples, com média de 1,08 ms para 32.254 embeddings
def fast_dot_product(query, matrix, k=3):
dot_products = query @ matrix.T
idx = np.argpartition(dot_products, -k)[-k:]
idx = idx[np.argsort(dot_products[idx])[::-1]]
score = dot_products[idx]
return idx, score
- Ao usar um banco de dados vetorial, há grande chance de ficar preso a bibliotecas e serviços específicos
- Se os embeddings forem gerados em um servidor com GPU e depois baixados localmente, é preciso um método eficiente de armazenamento e transferência de dados
As piores formas de armazenar embeddings
- Arquivo CSV
- Ao armazenar dados de ponto flutuante (
float32) como texto, o tamanho aumenta mais de 6 vezes
- Até no tutorial oficial da OpenAI, o uso de CSV é recomendado apenas para conjuntos de dados pequenos
- Ao salvar com
.savetxt() do numpy, o tamanho do arquivo sobe para 631,5 MB
- Arquivo pickle
- Pode ser salvo e carregado rapidamente, mas traz riscos de segurança e baixa compatibilidade entre versões
- O tamanho do arquivo é 94,49 MB, igual ao tamanho original em memória, mas com baixa portabilidade
Formas aceitáveis, mas não ideais, de armazenamento
- Formato
.npy do numpy
- Com a configuração
allow_pickle=False, é possível impedir o uso de pickle no armazenamento
- O tamanho do arquivo e a velocidade são os mesmos do método com pickle, mas é difícil armazenar metadados individuais junto
- Problemas de uma estrutura de armazenamento separada dos metadados
- Ao salvar como array numpy (
.npy), as informações das cartas (nome, texto etc.) ficam separadas dos embeddings
- Quando os dados mudam (adição/remoção), fica difícil manter a correspondência entre metadados e embeddings
- Em bancos de dados vetoriais, metadados e vetores são armazenados juntos e há recursos de filtragem
A melhor forma de armazenar embeddings: Parquet + polars
Introdução ao formato de arquivo Parquet
- Apache Parquet é um formato de armazenamento de dados em colunas, no qual é possível definir claramente o tipo de cada coluna
- Como ele pode armazenar dados em formato de lista (arrays
float32), é adequado para guardar embeddings
- Oferece desempenho de leitura e gravação superior ao CSV, e permite carregar seletivamente apenas parte dos dados
- Também oferece compressão, embora embeddings tenham baixa redundância e, portanto, pouco ganho de compressão
Uso de arquivos Parquet em Python
- Salvando e carregando arquivos Parquet com pandas:
df = pd.read_parquet("mtg-embeddings.parquet", columns=["name", "embedding"])
df
- O pandas não lida de forma eficiente com dados aninhados (listas), convertendo-os em objetos numpy
object
- Ao converter para array numpy, é necessária uma operação adicional (
np.vstack()), o que pode degradar o desempenho
- Salvando e carregando arquivos Parquet com polars:
df = pl.read_parquet("mtg-embeddings.parquet", columns=["name", "embedding"])
df
- O polars preserva os arrays
float32 como estão, e ao chamar to_numpy() já é possível obter imediatamente um array numpy 2D
- Com a configuração
allow_copy=False, é possível evitar cópias desnecessárias de dados
embeddings = df["embedding"].to_numpy(allow_copy=False)
- Ao adicionar novos embeddings, também é fácil salvar tudo apenas incluindo uma nova coluna
df = df.with_columns(embedding=embeddings)
df.write_parquet("mtg-embeddings.parquet")
Busca por similaridade e filtragem com Parquet + polars
- É possível primeiro filtrar apenas os dados que atendem a certas condições e depois executar a busca por similaridade
- Exemplo: encontrar cartas semelhantes a uma carta específica (
query_embed), mas buscando apenas cartas do tipo 'Sorcery' e com a cor 'Black' incluída
df_filter = df.filter(
pl.col("type").str.contains("Sorcery"),
pl.col("manaCost").str.contains("B"),
)
embeddings_filter = df_filter["embedding"].to_numpy(allow_copy=False)
idx, _ = fast_dot_product(query_embed, embeddings_filter, k=4)
related_cards = df_filter[idx]
- O tempo médio de execução é de 1,48 ms, 37% mais lento que a busca no conjunto completo, mas ainda assim rápido
Alternativas para processar grandes volumes de dados vetoriais
- O método com Parquet e dot product consegue lidar tranquilamente com centenas de milhares de embeddings
- Para conjuntos de dados ainda maiores, pode ser necessário usar um banco de dados vetorial
- Como alternativa, é possível usar o sqlite-vec, baseado em SQLite, para realizar buscas e filtragens vetoriais adicionais
Conclusão
- Bancos de dados vetoriais não são obrigatórios
- A combinação Parquet + polars é uma alternativa poderosa para armazenar, buscar e filtrar embeddings com eficiência
- Especialmente em projetos menores, usar arquivos Parquet pode ser mais rápido e mais econômico
- Dependendo do projeto, é importante escolher a solução adequada entre Parquet e um banco de dados vetorial
- O código e os dados podem ser consultados no repositório no GitHub
1 comentários
Comentários do Hacker News
O problema do Parquet é que ele é estático. Não é adequado quando há necessidade de gravações e atualizações contínuas. Ainda assim, tive bons resultados usando arquivos Parquet com DuckDB e armazenamento de objetos. O tempo de carregamento é rápido
numpy float32como bytes e depois decodificá-los novamente em arrays donumpyusearch. Uso vetores binários e depois reordeno os 100 melhores emfloat32. Isso leva cerca de 2 ms para aproximadamente 20.000 itens, o que é mais rápido que o LanceDB. Em coleções maiores, o Lance pode vencer. Mas, no meu caso de uso, funciona bem porque cada usuário tem um arquivo SQLite dedicadoArtigo realmente excelente. Gosto do seu trabalho há muito tempo. Para quem está se aprofundando em implementações com SQLite, vale acrescentar que o DuckDB começou a incluir alguns recursos de similaridade vetorial para leitura de Parquet e lida perfeitamente com esse caso de uso
Ainda não gosto de dataframes, mas o Polars é muito melhor que o pandas
Dê uma olhada no
usearchda Unum. Ele ganha de qualquer coisa e é muito fácil de usar. Faz exatamente o que você precisaSe quiser experimentar, dá para carregar de forma preguiçosa a partir do HF e aplicar filtros
POLARS_MAX_THREADSao Ray Actor e ajustá-lo de acordo com o nível de saturação de um único nóHá muitas descobertas excelentes
jsonresume. Estou enviando a versão JSON completa como string para gerar embeddings, mas também estou experimentando usar um modelo que primeiro traduz oresume.jsonpara uma versão de texto completo antes de gerar os embeddings. Os resultados parecem melhores, mas não vi opiniões concretas sobre issoHá um truque elegante na documentação do Vespa que converte vetores em binário e depois usa uma representação hexadecimal
Polars + Parquet é excelente em portabilidade e desempenho. Este post focou na portabilidade em Python, mas o Polars tem uma API Rust fácil de usar que permite embutir o motor em vários lugares
Sou um grande fã do Polars, mas não tinha pensado em usá-lo para armazenar embeddings (eu estava experimentando com
sqlite-vec). Parece uma ideia realmente interessanteOutra biblioteca que eu recomendaria é o lancedb, por seu ótimo desempenho e recursos como indexação de texto completo e versionamento de alterações