วิธีที่ดีที่สุดในการใช้ text embeddings แบบพกพาได้คือ Parquet และ Polars
- text embeddings คือเวกเตอร์ที่สร้างจากโมเดลภาษาขนาดใหญ่ ซึ่งใช้แทนคำ ประโยค และเอกสารในรูปแบบตัวเลข
- ณ เดือนกุมภาพันธ์ 2025 ได้สร้าง embeddings ของการ์ด "Magic: The Gathering" ทั้งหมด 32,254 ใบ
- ทำให้สามารถวิเคราะห์ความคล้ายคลึงกันทางคณิตศาสตร์ได้จากการออกแบบและคุณสมบัติเชิงกลไกของการ์ด
- embeddings ที่สร้างขึ้นสามารถนำไปแสดงผลด้วยการลดมิติแบบ 2D UMAP
- โมเดล embedding ที่ใช้คือ gte-modernbert-base และกระบวนการโดยละเอียดถูกรวบรวมไว้ใน GitHub repository
- ชุดข้อมูล embedding นี้เผยแพร่บน Hugging Face
ทบทวนความจำเป็นของ vector database
- โดยทั่วไปมักใช้ vector database (faiss, qdrant, Pinecone) เพื่อจัดเก็บและค้นหา embeddings
- แต่ vector database ต้องการการตั้งค่าที่ซับซ้อน และบริการคลาวด์อาจมีค่าใช้จ่ายสูง
- หากเป็นข้อมูลขนาดเล็กในระดับหลายหมื่นรายการ ก็สามารถใช้ numpy เพื่อค้นหาความคล้ายคลึงได้อย่างรวดเร็วโดยไม่ต้องใช้ vector database
- หากใช้การคำนวณ
dot product ของ numpy ก็สามารถคำนวณ cosine similarity แบบง่าย ๆ ได้ โดยใช้เวลาเฉลี่ย 1.08ms สำหรับ embeddings 32,254 รายการ
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 มักเพิ่มความเสี่ยงที่จะผูกติดกับไลบรารีหรือบริการเฉพาะ
- หากสร้าง embeddings บนเซิร์ฟเวอร์ GPU แล้วดาวน์โหลดกลับมาไว้ในเครื่อง ก็จำเป็นต้องมีวิธีจัดเก็บและส่งผ่านข้อมูลที่มีประสิทธิภาพ
วิธีจัดเก็บ embeddings ที่แย่ที่สุด
- ไฟล์ CSV
- หากเก็บข้อมูลทศนิยมแบบลอยตัว (
float32) เป็นข้อความ ขนาดจะเพิ่มขึ้นมากกว่า 6 เท่า
- แม้แต่ บทช่วยสอนทางการ ของ OpenAI ก็แนะนำให้ใช้ CSV เฉพาะกับชุดข้อมูลขนาดเล็ก
- หากบันทึกด้วย
.savetxt() ของ numpy ขนาดไฟล์จะเพิ่มเป็น 631.5MB
- ไฟล์ pickle
- บันทึกและโหลดได้รวดเร็ว แต่มีความเสี่ยงด้านความปลอดภัยและความเข้ากันได้ระหว่างเวอร์ชันต่ำ
- ขนาดไฟล์คือ 94.49MB เท่ากับขนาดหน่วยความจำต้นฉบับ แต่มีความสามารถในการพกพาต่ำ
วิธีจัดเก็บที่ไม่แย่ แต่ยังไม่เหมาะที่สุด
- รูปแบบ
.npy ของ numpy
- สามารถป้องกันการจัดเก็บแบบ pickle ได้ด้วยการตั้งค่า
allow_pickle=False
- ขนาดไฟล์และความเร็วเท่ากับวิธี pickle แต่ยากต่อการจัดเก็บ metadata รายรายการร่วมกัน
- ปัญหาของโครงสร้างการจัดเก็บที่แยก metadata ออกจากกัน
- หากเก็บเป็นอาร์เรย์ numpy (
.npy) ข้อมูลการ์ด เช่น ชื่อ ข้อความ ฯลฯ จะถูกแยกออกจาก embeddings
- เมื่อข้อมูลมีการเปลี่ยนแปลง (เพิ่ม/ลบ) จะจับคู่ metadata กับ embeddings ได้ยาก
- ใน vector database นั้น metadata และเวกเตอร์จะถูกเก็บร่วมกันและมีความสามารถด้านการกรอง
วิธีจัดเก็บ embeddings ที่เหมาะที่สุด: Parquet + polars
แนะนำรูปแบบไฟล์ Parquet
- Apache Parquet คือรูปแบบจัดเก็บข้อมูลแบบคอลัมน์ ซึ่งสามารถกำหนดชนิดข้อมูลของแต่ละคอลัมน์ได้อย่างชัดเจน
- สามารถเก็บข้อมูลแบบลิสต์ (อาร์เรย์
float32) ได้ จึงเหมาะกับการเก็บ embeddings
- ให้ประสิทธิภาพในการบันทึกและโหลดที่ดีกว่า CSV และสามารถเลือกโหลดเฉพาะบางส่วนของข้อมูลได้
- แม้จะมีความสามารถในการบีบอัด แต่ข้อมูล embedding มีความซ้ำต่ำจึงได้ประโยชน์จากการบีบอัดไม่มาก
การใช้ไฟล์ Parquet ใน Python
- การบันทึกและโหลดไฟล์ Parquet ด้วย pandas:
df = pd.read_parquet("mtg-embeddings.parquet", columns=["name", "embedding"])
df
- pandas จัดการข้อมูลแบบ nested (ลิสต์) ได้ไม่ดีนัก และจะถูกแปลงเป็น numpy
object
- เมื่อต้องแปลงเป็นอาร์เรย์ numpy จะต้องมีการประมวลผลเพิ่ม (
np.vstack()) ซึ่งอาจทำให้ประสิทธิภาพลดลง
- การบันทึกและโหลดไฟล์ Parquet ด้วย polars:
df = pl.read_parquet("mtg-embeddings.parquet", columns=["name", "embedding"])
df
- polars จะคงอาร์เรย์
float32 ไว้ตามเดิม และเมื่อเรียก to_numpy() ก็สามารถคืนค่าเป็นอาร์เรย์ numpy แบบ 2 มิติได้ทันที
- สามารถป้องกันการคัดลอกข้อมูลที่ไม่จำเป็นได้ด้วยการตั้งค่า
allow_copy=False
embeddings = df["embedding"].to_numpy(allow_copy=False)
- เมื่อต้องการเพิ่ม embeddings ใหม่ ก็สามารถเพิ่มคอลัมน์แล้วบันทึกได้อย่างง่ายดาย
df = df.with_columns(embedding=embeddings)
df.write_parquet("mtg-embeddings.parquet")
การค้นหาความคล้ายคลึงและการกรองด้วย Parquet + polars
- สามารถกรองเฉพาะข้อมูลที่ตรงตามเงื่อนไขก่อน แล้วค่อยทำ similarity search ได้
- ตัวอย่าง: ค้นหาการ์ดที่คล้ายกับการ์ดหนึ่ง (
query_embed) แต่จำกัดเฉพาะการ์ดประเภท 'Sorcery' และมีสี 'Black'
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]
- เวลาเฉลี่ยในการทำงานคือ 1.48ms ช้ากว่าการค้นหาทั้งชุดข้อมูล 37% แต่ก็ยังเร็วอยู่มาก
ทางเลือกสำหรับการจัดการข้อมูลเวกเตอร์ขนาดใหญ่
- วิธีแบบ Parquet และ dot product สามารถรองรับได้สบายจนถึงระดับ embeddings หลายแสนรายการ
- หากต้องจัดการกับชุดข้อมูลที่ใหญ่กว่านั้น อาจจำเป็นต้องใช้ vector database
- อีกทางเลือกหนึ่งคือใช้ sqlite-vec ที่ทำงานบน SQLite เพื่อเพิ่มความสามารถด้านการค้นหาและกรองเวกเตอร์
บทสรุป
- vector database ไม่ได้เป็นสิ่งจำเป็นเสมอไป
- การใช้ Parquet + polars เป็นทางเลือกที่ทรงพลังสำหรับการจัดเก็บ ค้นหา และกรอง embeddings อย่างมีประสิทธิภาพ
- โดยเฉพาะในโปรเจ็กต์ขนาดเล็ก การใช้ไฟล์ Parquet อาจเร็วกว่าและคุ้มค่ากว่า
- สิ่งสำคัญคือการเลือกโซลูชันที่เหมาะสมระหว่าง Parquet และ vector database ตามลักษณะของโปรเจ็กต์
- สามารถดูโค้ดและข้อมูลได้ที่ GitHub repository
1 ความคิดเห็น
ความคิดเห็นจาก Hacker News
ปัญหาของ Parquet คือมันเป็นแบบสถิต จึงไม่เหมาะหากต้องการการเขียนและอัปเดตอย่างต่อเนื่อง แต่เมื่อใช้ไฟล์ Parquet ของ DuckDB กับ object storage ก็ได้ผลลัพธ์ที่ดี เวลาโหลดเร็ว
numpy float32เป็นไบต์ แล้วถอดรหัสกลับเป็นอาร์เรย์ numpy ได้บทความนี้ยอดเยี่ยมมาก ฉันติดตามและชื่นชอบงานของคุณมานาน สำหรับคนที่กำลังจะลองทำบน SQLite อยากเสริมว่า DuckDB เริ่มมีฟีเจอร์ด้าน vector similarity บางอย่างที่อ่าน Parquet ได้ และรองรับกรณีการใช้งานนี้ได้อย่างพอดี
ฉันยังไม่ชอบ data frame อยู่ดี แต่ Polars ดีกว่า pandas มาก
ลองดู usearch ของ Unum มันชนะเกือบทุกอย่างและใช้งานง่ายมาก ทำสิ่งที่ต้องการได้ตรงเป๊ะ
ถ้าอยากลอง คุณสามารถโหลดแบบ lazy จาก HF และใช้ตัวกรองได้
POLARS_MAX_THREADSกับ Ray Actor เพื่อปรับตามระดับการใช้ทรัพยากรของโหนดเดียวได้มีข้อค้นพบดี ๆ มากมาย
resume.jsonเป็นข้อความเต็มก่อน แล้วค่อยสร้าง embedding ดูเหมือนว่าผลลัพธ์จะดีกว่า แต่ยังไม่ค่อยเห็นความเห็นที่ชัดเจนเกี่ยวกับเรื่องนี้ในเอกสารของ Vespa มีทริกที่เรียบร้อยดี คือแปลงเวกเตอร์เป็นเลขฐานสองแล้วใช้รูปแบบแทนค่าแบบฐานสิบหก
Polars + Parquet ยอดเยี่ยมมากทั้งในด้านความสามารถในการพกพาและประสิทธิภาพ โพสต์นี้เน้นเรื่องความสามารถในการพกพาของ Python แต่ Polars ก็มี Rust API ที่ใช้งานง่ายสำหรับฝังเอนจินไว้ในหลาย ๆ ที่ได้ด้วย
ฉันเป็นแฟนตัวยงของ Polars แต่ไม่เคยคิดจะใช้มันเก็บ embedding มาก่อน (ก่อนหน้านี้กำลังทดลองกับ sqlite-vec อยู่) ฟังดูเป็นไอเดียที่น่าสนใจมาก
ขอแนะนำ lancedb เป็นอีกไลบรารีหนึ่งที่มีประสิทธิภาพและฟีเจอร์ยอดเยี่ยม เช่น full-text indexing และการทำเวอร์ชันของการเปลี่ยนแปลง