7 पॉइंट द्वारा GN⁺ 2025-03-06 | 1 टिप्पणियां | WhatsApp पर शेयर करें

टेक्स्ट embeddings को पोर्टेबल तरीके से उपयोग करने का सबसे अच्छा तरीका है Parquet और Polars

  • टेक्स्ट embeddings बड़े language model द्वारा बनाए गए vectors होते हैं, जो शब्दों, वाक्यों और दस्तावेज़ों को संख्यात्मक रूप में दर्शाते हैं
  • फ़रवरी 2025 तक, कुल 32,254 "Magic: The Gathering" कार्ड embeddings बनाए गए
  • इसके ज़रिए कार्ड के design और mechanical properties के आधार पर समानता का गणितीय विश्लेषण किया जा सकता है
  • बने हुए embeddings को 2D UMAP dimensionality reduction के ज़रिए visualize किया जा सकता है
  • इस्तेमाल किया गया embedding model gte-modernbert-base है, और विस्तृत प्रक्रिया GitHub repository में व्यवस्थित की गई है
  • यह embedding dataset Hugging Face पर उपलब्ध है

vector database की ज़रूरत पर फिर से विचार

  • आम तौर पर embeddings को store और search करने के लिए vector database (faiss, qdrant, Pinecone) का उपयोग किया जाता है
  • लेकिन vector database के लिए जटिल setup की ज़रूरत होती है, और cloud services महंगी हो सकती हैं
  • छोटे पैमाने के डेटा सेट (दसियों हज़ार स्तर) के लिए vector database के बिना भी numpy का उपयोग करके तेज similarity search किया जा सकता है
  • numpy के dot product ऑपरेशन से सरल cosine similarity calculation किया जा सकता है, और 32,254 embeddings पर औसतन 1.08ms लगता है
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  
  • vector database इस्तेमाल करने पर किसी विशेष library या service पर निर्भरता बढ़ सकती है
  • GPU server पर embeddings बनाकर लोकल में डाउनलोड करने की स्थिति में, डेटा को कुशलता से store और transfer करने का तरीका चाहिए

embeddings को store करने के सबसे खराब तरीके

  • CSV फ़ाइल
    • floating point (float32) डेटा को text के रूप में store करने पर आकार 6 गुना से अधिक बढ़ जाता है
    • OpenAI के official tutorial में भी CSV का उपयोग केवल छोटे dataset के लिए सुझाया गया है
    • numpy की .savetxt() से save करने पर फ़ाइल का आकार बढ़कर 631.5MB हो जाता है
  • pickle फ़ाइल
    • तेज़ी से save और load किया जा सकता है, लेकिन इसमें security risk होता है और version compatibility कमज़ोर होती है
    • फ़ाइल आकार 94.49MB है, जो मूल memory size के बराबर है, लेकिन portability कम है

ऐसे store करने के तरीके जो बुरे नहीं हैं, लेकिन optimal भी नहीं

  • numpy का .npy format
    • allow_pickle=False setting के ज़रिए pickle storage को रोका जा सकता है
    • फ़ाइल आकार और speed pickle तरीके जैसी ही है, लेकिन individual metadata को साथ में store करना मुश्किल है
  • metadata से अलग storage structure की समस्या
    • numpy array (.npy) में store करने पर कार्ड जानकारी (नाम, टेक्स्ट आदि) और embeddings अलग हो जाते हैं
    • डेटा में बदलाव (जोड़ना/हटाना) होने पर metadata और embeddings का मिलान करना मुश्किल हो जाता है
    • vector database में metadata और vector को साथ में store किया जाता है और filtering सुविधा भी मिलती है

embeddings के लिए सबसे अच्छा storage तरीका: Parquet + polars

Parquet फ़ाइल format का परिचय

  • Apache Parquet एक column-based data storage format है, जिसमें हर column का data type स्पष्ट रूप से निर्दिष्ट किया जा सकता है
  • यह list form (float32 array) का डेटा store कर सकता है, इसलिए embeddings के लिए उपयुक्त है
  • यह CSV की तुलना में तेज save और load performance देता है, और केवल कुछ डेटा को चुनकर load भी किया जा सकता है
  • इसमें compression सुविधा है, लेकिन embedding डेटा में redundancy कम होने के कारण compression का प्रभाव सीमित रहता है

Python में Parquet फ़ाइल का उपयोग

  • pandas के साथ Parquet फ़ाइल save और load करना:
    df = pd.read_parquet("mtg-embeddings.parquet", columns=["name", "embedding"])  
    df  
    
    • pandas nested data (list) को कुशलता से handle नहीं कर पाता और इसे numpy object में बदल देता है
    • numpy array में बदलते समय अतिरिक्त operation (np.vstack()) की ज़रूरत होती है, जिससे performance गिर सकती है
  • polars के साथ Parquet फ़ाइल save और load करना:
    df = pl.read_parquet("mtg-embeddings.parquet", columns=["name", "embedding"])  
    df  
    
    • polars float32 array को जस का तस बनाए रखता है, और to_numpy() कॉल करने पर तुरंत 2D numpy array लौटाया जा सकता है
    • allow_copy=False setting के ज़रिए अनावश्यक data copy से बचा जा सकता है
    embeddings = df["embedding"].to_numpy(allow_copy=False)  
    
  • नए embeddings जोड़ते समय भी आसानी से column जोड़कर save किया जा सकता है
    df = df.with_columns(embedding=embeddings)  
    df.write_parquet("mtg-embeddings.parquet")  
    

Parquet + polars के साथ similarity search और filtering

  • केवल उन डेटा पर फ़िल्टर लगाकर similarity search किया जा सकता है जो किसी विशेष condition को पूरा करते हों
  • उदाहरण: किसी खास कार्ड (query_embed) से मिलते-जुलते कार्ड खोजने हैं, लेकिन केवल 'Sorcery' type और 'Black' color वाले कार्डों में
    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]  
    
  • औसत execution time 1.48ms है, जो पूरे डेटा पर search की तुलना में 37% धीमा है, लेकिन फिर भी तेज़ है

बड़े पैमाने के vector data processing के लिए विकल्प

  • Parquet और dot product तरीका लाखों से कम, सैकड़ों हज़ार embeddings तक आराम से संभाल सकता है
  • इससे बड़े dataset के लिए vector database की ज़रूरत पड़ सकती है
  • एक विकल्प के रूप में SQLite आधारित sqlite-vec का उपयोग करके अतिरिक्त vector search और filtering की जा सकती है

निष्कर्ष

  • vector database हमेशा अनिवार्य नहीं होता
  • Parquet + polars का संयोजन embeddings को कुशलता से store, search और filter करने का एक मजबूत विकल्प है
  • खासकर छोटे पैमाने के प्रोजेक्ट्स में Parquet फ़ाइल का उपयोग अधिक तेज़ और लागत-कुशल हो सकता है
  • प्रोजेक्ट के अनुसार Parquet और vector database में से उचित समाधान चुनना महत्वपूर्ण है
  • GitHub repository में code और data देखा जा सकता है

1 टिप्पणियां

 
GN⁺ 2025-03-06
Hacker News राय
  • Parquet की समस्या यह है कि यह static है। अगर लगातार write और update की ज़रूरत हो, तो यह उपयुक्त नहीं है। हालांकि DuckDB और object storage में Parquet files का उपयोग करने पर अच्छे नतीजे मिले। load time तेज़ है

    • अगर आप अपना embedding model host करते हैं, तो numpy float32 compressed array को bytes के रूप में भेजकर फिर उसे दोबारा numpy array में decode कर सकते हैं
    • व्यक्तिगत रूप से मैं SQLite और usearch extension का उपयोग करना पसंद करता हूँ। binary vectors का उपयोग करने के बाद top 100 को float32 से rerank करता हूँ। लगभग 20,000 items पर इसमें करीब 2ms लगते हैं, जो LanceDB से तेज़ है। बड़े collections में Lance जीत सकता है। लेकिन मेरे use case में हर user के पास dedicated SQLite file होती है, इसलिए यह अच्छी तरह काम करता है
    • portability के लिए Litestream है
  • यह वाकई शानदार लेख है। मैं लंबे समय से आपका काम पसंद करता रहा हूँ। जो लोग SQLite implementation में उतर रहे हैं, उनके लिए मैं यह जोड़ सकता हूँ कि DuckDB ने Parquet पढ़ने और इस use case को पूरी तरह संभालने के लिए कुछ vector similarity features शुरू किए हैं

  • मुझे अब भी dataframes खास पसंद नहीं हैं, लेकिन Polars, pandas से कहीं बेहतर है

    • मैं time series calculation कर रहा था, मूल रूप से simple stock price adjustment किया था
    • यह देखकर हैरानी हुई कि code को वास्तव में पढ़ना और test करना संभव था
    • execution speed इतनी तेज़ थी कि लगा जैसे कुछ खराब है
  • Unum का usearch देखिए। यह हर चीज़ को मात देता है और इस्तेमाल करने में बहुत आसान है। जो चाहिए, वही ठीक-ठीक करता है

  • अगर आप इसे आज़माना चाहते हैं, तो HF से lazily load करके filtering apply कर सकते हैं

    • Polars इस्तेमाल करने में शानदार है और मैं इसे ज़ोरदार सिफारिश करता हूँ। यह single node पर CPU को saturate करने में बेहतरीन है, और अगर आपको काम distribute करना हो, तो Ray Actor पर POLARS_MAX_THREADS लागू करके single node saturation के हिसाब से इसे tune कर सकते हैं
  • यहाँ बहुत सी शानदार बातें मिलीं

    • मैं सोच रहा हूँ कि structured data को embedding API में भेजना बेहतर है या unstructured data को। ChatGPT से पूछने पर वह कहता है कि unstructured data भेजना बेहतर है
    • मेरा use case jsonresume के लिए है। मैं अभी पूरा json version string के रूप में भेजकर embedding बना रहा हूँ, लेकिन एक ऐसे model के साथ प्रयोग कर रहा हूँ जो पहले resume.json को full text version में बदलता है और फिर embedding बनाता है। नतीजे बेहतर लगते हैं, लेकिन इस पर कोई ठोस राय नहीं देखी
    • unstructured data बेहतर होने का कारण यह है कि natural language की वजह से उसमें textual/semantic meaning शामिल होता है
  • Vespa docs में vectors को binary में बदलने के बाद hexadecimal representation इस्तेमाल करने की एक साफ-सुथरी trick है

    • इस trick का उपयोग payload size कम करने के लिए किया जा सकता है। Vespa इस format को support करता है, और जब एक ही vector को document में कई बार refer किया जाता है तो यह खास तौर पर उपयोगी है। ColBERT या ColPaLi जैसे मामलों में (जहाँ कई embedding vectors होते हैं) disk पर stored vectors का size काफी घटाया जा सकता है
  • Polars + Parquet portability और performance के लिए शानदार है। यह पोस्ट Python portability पर केंद्रित थी, लेकिन Polars के पास इस्तेमाल में आसान Rust API है, जिससे engine को कई जगह embed किया जा सकता है

  • मैं Polars का बड़ा प्रशंसक हूँ, लेकिन इसे embeddings store करने के लिए इस्तेमाल करने के बारे में नहीं सोचा था (मैं sqlite-vec के साथ प्रयोग कर रहा था)। यह सच में एक दिलचस्प विचार लगता है

  • पूरे text indexing और change versioning जैसी बेहतरीन performance और features वाली एक और library के रूप में मैं lancedb की सिफारिश करता हूँ

    • यह एक vector database है और अधिक जटिल है, लेकिन इसे index बनाए बिना भी इस्तेमाल किया जा सकता है, और इसमें शानदार polars और pandas zero-copy Arrow support भी है