टेक्स्ट 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 टिप्पणियां
Hacker News राय
Parquet की समस्या यह है कि यह static है। अगर लगातार write और update की ज़रूरत हो, तो यह उपयुक्त नहीं है। हालांकि DuckDB और object storage में Parquet files का उपयोग करने पर अच्छे नतीजे मिले। load time तेज़ है
यह वाकई शानदार लेख है। मैं लंबे समय से आपका काम पसंद करता रहा हूँ। जो लोग SQLite implementation में उतर रहे हैं, उनके लिए मैं यह जोड़ सकता हूँ कि DuckDB ने Parquet पढ़ने और इस use case को पूरी तरह संभालने के लिए कुछ vector similarity features शुरू किए हैं
मुझे अब भी dataframes खास पसंद नहीं हैं, लेकिन Polars, pandas से कहीं बेहतर है
Unum का usearch देखिए। यह हर चीज़ को मात देता है और इस्तेमाल करने में बहुत आसान है। जो चाहिए, वही ठीक-ठीक करता है
अगर आप इसे आज़माना चाहते हैं, तो HF से lazily load करके filtering apply कर सकते हैं
यहाँ बहुत सी शानदार बातें मिलीं
Vespa docs में vectors को binary में बदलने के बाद hexadecimal representation इस्तेमाल करने की एक साफ-सुथरी trick है
Polars + Parquet portability और performance के लिए शानदार है। यह पोस्ट Python portability पर केंद्रित थी, लेकिन Polars के पास इस्तेमाल में आसान Rust API है, जिससे engine को कई जगह embed किया जा सकता है
मैं Polars का बड़ा प्रशंसक हूँ, लेकिन इसे embeddings store करने के लिए इस्तेमाल करने के बारे में नहीं सोचा था (मैं sqlite-vec के साथ प्रयोग कर रहा था)। यह सच में एक दिलचस्प विचार लगता है
पूरे text indexing और change versioning जैसी बेहतरीन performance और features वाली एक और library के रूप में मैं lancedb की सिफारिश करता हूँ