Metin embedding’lerini taşınabilir biçimde kullanmanın en iyi yolu: Parquet ve Polars
- Metin embedding’leri, büyük dil modelleri tarafından üretilen vektörlerdir; kelimeleri, cümleleri ve belgeleri sayısal olarak temsil etmenin bir yoludur
- 2025 Şubat itibarıyla toplam 32.254 adet "Magic: The Gathering" kart embedding’i üretildi
- Bu sayede kartların tasarım ve mekanik özelliklerine dayalı benzerlikler matematiksel olarak analiz edilebiliyor
- Üretilen embedding’ler, 2D UMAP boyut indirgeme ile görselleştirilebiliyor
- Kullanılan embedding modeli gte-modernbert-base ve ayrıntılı süreç GitHub deposunda yer alıyor
- İlgili embedding veri kümesi Hugging Face üzerinden sunuluyor
Vektör veritabanı gerekliliğini yeniden düşünmek
- Genelde embedding’leri depolamak ve aramak için vektör veritabanları (faiss, qdrant, Pinecone) kullanılıyor
- Ancak vektör veritabanları karmaşık kurulum gerektirebilir ve bulut hizmetleri pahalı olabilir
- Küçük ölçekli verilerde, yani on binler düzeyinde, vektör veritabanı olmadan da numpy ile hızlı benzerlik araması yapılabilir
- numpy’nin
dot product işlemiyle basit kosinüs benzerliği hesaplanabilir; 32.254 embedding için ortalama süre 1,08 ms’dir
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
- Vektör veritabanı kullanmak, belirli kütüphanelere ve hizmetlere bağımlılığı artırabilir
- Embedding’ler GPU sunucusunda üretildikten sonra yerel ortama indiriliyorsa, veriyi depolamak ve aktarmak için verimli bir yöntem gerekir
Embedding saklamanın en kötü yolları
- CSV dosyaları
- Kayan noktalı (
float32) veriyi metin olarak saklamak, boyutu 6 kattan fazla artırır
- OpenAI’nin resmî eğitiminde de CSV yalnızca küçük veri kümeleri için öneriliyor
- numpy’nin
.savetxt() fonksiyonuyla kaydedildiğinde dosya boyutu 631.5MB’a çıkıyor
- pickle dosyaları
- Hızlı kaydetme ve yükleme sağlar, ancak güvenlik riski taşır ve sürüm uyumluluğu zayıftır
- Dosya boyutu 94.49MB ile bellekteki özgün boyutla aynıdır, fakat taşınabilirliği düşüktür
Fena olmayan ama ideal de olmayan saklama yöntemleri
- numpy’nin
.npy biçimi
allow_pickle=False ayarıyla pickle kullanımını engellemek mümkündür
- Dosya boyutu ve hız açısından pickle ile aynıdır, ancak ayrı metadata saklamak zordur
- Metadata’dan ayrı saklama yapısının sorunları
- numpy dizisi (
.npy) olarak saklandığında kart bilgileri (isim, metin vb.) ile embedding’ler birbirinden ayrılır
- Veri değiştiğinde, yani ekleme veya silme olduğunda, metadata ile embedding eşleştirmesi zorlaşır
- Vektör veritabanları metadata ve vektörleri birlikte saklar, ayrıca filtreleme de sunar
En iyi embedding saklama yöntemi: Parquet + polars
Parquet dosya biçimine giriş
- Apache Parquet, sütun tabanlı bir veri saklama biçimidir ve her sütunun veri tipini açıkça tanımlamayı sağlar
- Liste biçimindeki (
float32 dizi) verileri saklayabildiği için embedding depolamak için uygundur
- CSV’ye kıyasla daha hızlı kaydetme ve yükleme sunar, ayrıca yalnızca gerekli verileri seçerek yüklemeye izin verir
- Sıkıştırma desteği vardır, ancak embedding verilerinde tekrar oranı düşük olduğundan sıkıştırma etkisi sınırlıdır
Python’da Parquet kullanımı
- pandas ile Parquet dosyası kaydetme ve yükleme:
df = pd.read_parquet("mtg-embeddings.parquet", columns=["name", "embedding"])
df
- pandas, iç içe veri yapılarıyla (liste gibi) verimli çalışamaz ve bunları numpy
object tipine dönüştürür
- numpy dizisine çevrilirken ek işlem (
np.vstack()) gerektiği için performans kaybı yaşanabilir
- polars ile Parquet dosyası kaydetme ve yükleme:
df = pl.read_parquet("mtg-embeddings.parquet", columns=["name", "embedding"])
df
- polars,
float32 dizilerini olduğu gibi korur ve to_numpy() çağrıldığında doğrudan 2D numpy dizisi döndürebilir
allow_copy=False ayarıyla gereksiz veri kopyalamaları önlenebilir
embeddings = df["embedding"].to_numpy(allow_copy=False)
- Yeni embedding’ler eklenirken de sadece yeni bir sütun ekleyerek kolayca saklama yapılabilir
df = df.with_columns(embedding=embeddings)
df.write_parquet("mtg-embeddings.parquet")
Parquet + polars ile benzerlik arama ve filtreleme
- Belirli koşulları sağlayan veriler önce filtrelenip sonra benzerlik araması yapılabilir
- Örnek: Belirli bir kartla (
query_embed) benzer kartları bulurken yalnızca 'Sorcery' türündeki ve 'Black' rengine sahip kartları aramak
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]
- Ortalama çalışma süresi 1.48ms; bu, tüm veri üzerinde aramadan %37 daha yavaş olsa da hâlâ çok hızlıdır
Büyük ölçekli vektör verisi işleme için alternatifler
- Parquet ve dot product yaklaşımı, yüz binlerce embedding düzeyine kadar yeterli olabilir
- Daha büyük veri kümelerinde vektör veritabanı kullanmak gerekebilir
- Alternatif olarak, SQLite tabanlı sqlite-vec ile ek vektör arama ve filtreleme yapılabilir
Sonuç
- Vektör veritabanı her zaman zorunlu değildir
- Parquet + polars ikilisi, embedding’leri verimli biçimde depolamak, aramak ve filtrelemek için güçlü bir alternatiftir
- Özellikle küçük ölçekli projelerde Parquet dosyası kullanmak daha hızlı ve maliyet açısından daha verimlidir
- Projenin ihtiyaçlarına göre Parquet ile vektör veritabanı arasında doğru çözümü seçmek önemlidir
- Kod ve veriler GitHub deposunda incelenebilir
1 yorum
Hacker News görüşleri
Parquet'in sorunu statik olması. Sürekli yazma ve güncelleme gereken durumlar için uygun değil. Ancak DuckDB ve nesne depolamadaki Parquet dosyalarını kullandığımda iyi sonuçlar aldım. Yükleme süreleri hızlı
Gerçekten harika bir yazı. Uzun zamandır çalışmalarınızı beğenerek takip ediyorum. SQLite uygulamalarına dalanlar için şunu da ekleyebilirim: DuckDB, Parquet okuyabiliyor ve bu kullanım senaryosunu gayet iyi ele alan bazı vektör benzerliği özellikleri sunmaya başladı
DataFrame'leri hâlâ sevmiyorum ama Polars, pandas'tan çok daha iyi
Unum'un usearch'üne bakın. Her şeyi geçiyor ve kullanımı çok kolay. İhtiyacınız olan şeyi tam olarak yapıyor
Denemek isterseniz, HF'den lazy load yapıp filtre uygulayabilirsiniz
POLARS_MAX_THREADSuygulayarak tek düğüm doygunluğuna göre ayarlayabilirsinizBirçok harika bulgu var
Vespa belgelerinde, vektörü ikiliye çevirip ardından onaltılık gösterim kullanmaya dair hoş bir hile var
Polars + Parquet, taşınabilirlik ve performans açısından harika. Bu gönderi Python taşınabilirliğine odaklanmıştı ama Polars'ın, motoru birçok yere embed etmeyi sağlayan kullanımı kolay bir Rust API'si de var
Polars'ın büyük bir hayranıyım ama bunu embedding depolamak için kullanmayı düşünmemiştim (
sqlite-vecile denemeler yapıyordum). Gerçekten ilginç bir fikir gibi görünüyorTam metin indeksleme ve değişiklik sürümleme gibi güçlü performans ve özelliklere sahip başka bir kütüphane olarak lancedb'yi öneririm