7 puan yazan GN⁺ 2025-03-06 | 1 yorum | WhatsApp'ta paylaş

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

 
GN⁺ 2025-03-06
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ı

    • Kendi embedding modelinizi barındırıyorsanız, numpy float32 sıkıştırılmış dizilerini byte olarak gönderip sonra tekrar numpy dizilerine decode edebilirsiniz
    • Ben şahsen SQLite ve usearch eklentisini kullanmayı tercih ediyorum. İkili vektörler kullanıp ardından en iyi 100 sonucu float32 ile yeniden sıralıyorum. Yaklaşık 20.000 öğe için yaklaşık 2 ms sürüyor; bu da LanceDB'den daha hızlı. Daha büyük koleksiyonlarda Lance öne geçebilir. Ama benim kullanım durumumda her kullanıcının kendine ait bir SQLite dosyası var, bu yüzden iyi çalışıyor
    • Taşınabilirlik için Litestream var
  • 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

    • Zaman serisi hesaplamaları yapıyordum; temel olarak basit hisse fiyatı düzeltmeleri yaptım
    • Kodun okunmasının ve test edilmesinin gerçekten mümkün olması beni şaşırttı
    • O kadar hızlı çalışıyordu ki bozuk gibi görünüyordu
  • 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 kullanımı harika ve şiddetle tavsiye ederim. Tek bir düğümde CPU'yu doyurmada mükemmel; işi dağıtmanız gerekirse Ray Actor üzerinde POLARS_MAX_THREADS uygulayarak tek düğüm doygunluğuna göre ayarlayabilirsiniz
  • Birçok harika bulgu var

    • Yapılandırılmış veriyi embedding API'sine vermenin mi yoksa yapılandırılmamış veriyi vermenin mi daha iyi olduğunu merak ediyorum. ChatGPT'ye sorarsanız, yapılandırılmamış veriyi göndermenin daha iyi olduğunu söylüyor
    • Benim kullanım senaryom jsonresume için. Şu anda embedding üretmek için tüm json sürümünü string olarak gönderiyorum, ancak önce resume.json'u tam metin sürümüne çevirip sonra embedding üreten bir modelle denemeler yapıyorum. Sonuçlar daha iyi gibi görünüyor ama bu konuda somut görüşlere pek rastlamadım
    • Yapılandırılmamış verinin daha iyi olmasının nedeni, doğal dil sayesinde metinsel/anlamsal anlam içermesi
  • Vespa belgelerinde, vektörü ikiliye çevirip ardından onaltılık gösterim kullanmaya dair hoş bir hile var

    • Bu hile payload boyutunu azaltmak için kullanılabilir. Vespa bu biçimi destekliyor ve özellikle aynı vektöre belgede birden çok kez referans verildiğinde faydalı oluyor. ColBERT veya ColPaLi gibi durumlarda (birden fazla embedding vektörü olduğunda) diskte depolanan vektörlerin boyutunu ciddi ölçüde azaltabilir
  • 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-vec ile denemeler yapıyordum). Gerçekten ilginç bir fikir gibi görünüyor

  • Tam 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

    • Bu bir vektör veritabanı ve daha karmaşık, ancak indeks oluşturmadan da kullanılabiliyor; ayrıca mükemmel polars ve pandas zero-copy arrow desteği de var