אופטימיזציה של פריסת הנתונים
החלטות במידול נתונים משפיעות באופן משמעותי על ביצועי חיפוש וקטורי. איך אתה מבנה טבלאות, בוחר סוגי נתונים למטא-דאטה, ויוצר אינדקסים תומכים קובע אם השאילתות מתבצעות ביעילות ככל שמערך הנתונים שלך גדל.
הערה
דוגמאות קוד ביחידה זו מדגימות דפוסי עיצוב סכמה לנתוני וקטור עם מטא-דאטה. התאימו את הדפוסים הללו למודל הנתונים הספציפי שלכם ולדרישות השאילתה.
שיקולי אחסון וקטורי
עמודות וקטור צורכות משאבי אחסון ועיבוד משמעותיים. הבנת מאפייני האחסון עוזרת לך לקבל החלטות מושכלות לגבי עיצוב הסכימה.
כל ממד וקטורי מוסיף 4 בתים של אחסון (לצוף מדויק יחיד) בנוסף לעומס קבוע. הקשר בין הממדים לאחסון הוא ליניארי:
| מידות | בתים לכל וקטור | מיליון וקטורים |
|---|---|---|
| 384 | ~1.5 KB | ~1.5 GB |
| 768 | ~3 KB | ~3 GB |
| 1536 | ~6 KB | ~6 GB |
| 3072 | ~12 KB | ~12 GB |
עבור קטלוג מוצרים עם שני מיליון פריטים המשתמשים בהטמעות 1536 ממדים, עמודת הווקטור לבדה דורשת כ-12GB של אחסון. הוספת מדדי HNSW מגדילה זאת בכ-50%.
רבים ממודלי ההטמעה מציעים אפשרויות רב-ממדיות. ממדים נמוכים יותר מפחיתים עלויות אחסון וחישוב תוך שמירה על איכות סבירה במקרים רבים של שימושים. הגדרת ממדים בהגדרת העמודה מספקת אימות. ניסיונות להכניס וקטורים בממדים שונים נכשלים עם שגיאה, מה שמונע מבאגים עדינים למודלים לא תואמים בהטמעה. הגדר את הטבלה שלך עם מגבלת מימד מפורשת באמצעות embedding vector(768) הגדרת העמודה.
חלק מהיישומים דורשים וקטורים ממודלים שונים. לדוגמה, אפשר לאחסן הטמעות כותרות מוצר, הטמעות, והטמעות-התנהגות משתמש בנפרד. כל עמודה וקטורית צריכה אינדקס משלה כי אי אפשר ליצור אינדקס אחד שמכסה מספר עמודות וקטוריות.
CREATE TABLE products (
id BIGSERIAL PRIMARY KEY,
name TEXT NOT NULL,
title_embedding vector(768), -- Text embedding model
image_embedding vector(512), -- Image embedding model
category_id INTEGER,
price NUMERIC(10,2)
);
-- Create separate indexes for each embedding type
CREATE INDEX ON products USING hnsw (title_embedding vector_cosine_ops);
CREATE INDEX ON products USING hnsw (image_embedding vector_cosine_ops);
סוגי נתוני מטא-דאטה: עמודות מובנות לעומת JSONB
המלצות מוצרים כמעט ולא משתמשות רק בדמיון וקטורי. שאילתות בדרך כלל מסננות לפי קטגוריה, טווח מחירים, זמינות או תכונות אחרות לפני או לצד חיפוש וקטורי. איך אתה מאחסן את המטא-דאטה הזה משפיע על ביצועי השאילתות.
עמודות מובנות משתמשות בסוגי הנתונים המקוריים של PostgreSQL (מספר שלם, חותמת זמן, מספרית, טקסט) עם סכימה מפורשת. עמודות אלו מציעות יתרונות בביצועי שאילתות מכיוון שסוגים טבעיים מאפשרים אינדקסים יעילים של עץ B לשאילתות שוויון וטווח, יעילות אחסון באמצעות פורמטים אופטימליים של אחסון, בטיחות סוגי סוגים באמצעות אימות בזמן הכנסה, ואופטימיזציה של שאילתות באמצעות סטטיסטיקות מדויקות של מתכננים. השתמש בעמודות מובנות כאשר המאפיינים ידועים בזמן העיצוב, אתה מסנן או ממיין לעיתים קרובות לפי תכונות ספציפיות, או כאשר ביצועי השאילתה קריטיים.
CREATE TABLE products (
id BIGSERIAL PRIMARY KEY,
name TEXT NOT NULL,
embedding vector(1536),
category_id INTEGER NOT NULL,
price NUMERIC(10,2) NOT NULL,
in_stock BOOLEAN DEFAULT true,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
brand TEXT,
rating NUMERIC(2,1)
);
JSONB מאחסן נתונים חצי-מובנים כ-JSON בינארי, ומספק גמישות למאפיינים דינמיים. JSONB מספק גמישות בסכמה (מוצרים שונים יכולים להיות בעלי תכונות שונות), אבולוציה קלה (הוספת תכונות חדשות ללא הגירות סכימה), ומבנים מקוננים (אחסון נתונים היררכיים מורכבים). עם זאת, ל-JSONB יש עומס תקורה לשאילתות (חילוץ ערכים דורש ניתוח), מגבלות אינדקס (אינדקסים GIN עובדים לשאילתות הכלה אך לא לשאילתות טווח), ואי-ודאות מתכנן (סטטיסטיקות פחות מדויקות עבור תוכן JSONB).
בחיפושי וקטור מסוננים, ביצועי מסנן המטא-דאטה משפיעים ישירות על סך זמן השאילתה. עמודות מובנות עם אינדקסים של B-tree מאפשרות ל-PostgreSQL לצמצם במהירות מועמדים לפני חישובי מרחק וקטורי, בעוד ש-JSONB דורש דפוסי שאילתות וסוגי אינדקסים שונים. פילטר עמודה מובנה יכול WHERE category_id = 5 AND price BETWEEN 100 AND 500 להשתמש באינדקס עץ B כדי (category_id, price) לזהות במהירות שורות מועמדים. מסנן JSONB דומה WHERE attributes @> '{"category": "electronics"}' AND (attributes->>'price')::numeric BETWEEN 100 AND 500 דורש או אינדקס GIN (שלא עוזר בשאילתות טווח על המחיר) או סריקה סדרתית של עמודת JSONB.
יישומים רבים נהנים משילוב של עמודות מובנות ו-JSONB: משתמשים בעמודות מובנות עבור תכונות מסוננות לעיתים קרובות שבהן ביצועי השאילתה חשובים, וב-JSONB עבור תכונות דינמיות או מסוננות לעיתים נדירות שבהן גמישות הסכמה חשובה יותר. התבנית הזו מאפשרת לך למקסם את המקרה הנפוץ מבלי לוותר על גמישות במקרים קיציוניים.
CREATE TABLE products (
id BIGSERIAL PRIMARY KEY,
name TEXT NOT NULL,
embedding vector(1536),
-- Structured columns for common filters
category_id INTEGER NOT NULL,
price NUMERIC(10,2) NOT NULL,
in_stock BOOLEAN DEFAULT true,
-- JSONB for dynamic attributes
attributes JSONB DEFAULT '{}'
);
אינדקסי מטא-דאטה לחיפושים מסוננים
אינדקסי מטא-דאטה משלימים אינדקסים וקטוריים על ידי האצת שלב הסינון של השאילתות. בלי אינדקסים נכונים של מטא-דאטה, PostgreSQL עשוי להזדקק לסרוק את כל השורות כדי להחיל מסננים לפני חיפוש וקטורי.
צור אינדקסים של עץ B על עמודות המשמשות במשפטי WHERE. אינדקסים חד-עמודיים מטפלים בהתאמות מדויקות, בעוד שאינדקסים מורכבים מטפלים בשילובי מסננים. אינדקסים מורכבים יעילים במיוחד כאשר השאילתות מסננות בעמודות השמאליות ביותר. אינדקס על (category_id, price) ביעילות מטפל WHERE category_id = 5 ב-, WHERE category_id = 5 AND price < 100אבל זה לא עוזר לבד WHERE price < 100 כי המחיר אינו העמודה השמאלית ביותר.
-- Single-column index for exact matches
CREATE INDEX idx_products_category ON products (category_id);
-- Composite index for common filter combinations
CREATE INDEX idx_products_category_price ON products (category_id, price);
אם רוב השאילתות מסננות באותו תנאי (כמו מוצרים במלאי), אינדקס חלקי מקטין את גודל האינדקס ומשפר את הביצועים. אינדקס זה קטן יותר מאינדקס מלא ומשמש רק לשאילתות הכוללות WHERE in_stock = trueאת . עבור מנוע המלצות למסחר אלקטרוני שבו כמעט כל השאילתות מכוונות למוצרים זמינים, זה יכול להפחית משמעותית את העומס של תחזוקת המדד. צור אינדקס חלקי עם CREATE INDEX idx_products_instock_category ON products (category_id) WHERE in_stock = true;.
אם אתה משתמש ב-JSONB לתכונות, אינדקסים של GIN תומכים בשאילתות הכלה באמצעות @> אופרטורים (contains), <@ (contained by), ? (key exists) ו ?|/?& -(all/all keys exist). הם לא מאיצים שאילתות טווח או ביטויי מסלולי JSON שרירותיים. צור אינדקס GIN עם CREATE INDEX idx_products_attributes ON products USING gin (attributes);. בשדות JSONB שמקבלים שאילתות תכופות ודורשים שאילתות טווח, יש לשקול אינדקסים של ביטוי. צור אינדקס ביטוי על שדה JSONB שמופק כמספרי עם CREATE INDEX idx_products_json_price ON products (((attributes->>'price')::numeric)); כדי לאפשר שאילתות טווח בשדה זה.
שילוב חיפוש וקטורי עם מסנני מטא-דאטה
PostgreSQL מבצע שאילתות על ידי שילוב סריקות אינדקס עם סינון. הבנת דפוסי הביצוע עוזרת לך לכתוב שאילתות יעילות.
הדפוס היעיל ביותר מפעיל תחילה מסנני מטא-דאטה, ומפחית את קבוצת הווקטורים שדורשים חישובי דמיון. PostgreSQL משתמש באינדקסים של מטא-דאטה כדי לזהות מוצרים התואמים את המסננים, ואז מיישם דמיון וקטורי רק לאותם מועמדים. אם 5% של מוצרים תואמים את המסננים, אתה מחפש 100,000 וקטורים במקום 2 מיליון.
-- Efficient: filter narrows candidates before vector search
SELECT id, name, embedding <=> $1 AS distance
FROM products
WHERE category_id = 5
AND in_stock = true
AND price BETWEEN 100 AND 500
ORDER BY embedding <=> $1
LIMIT 10;
השתמש EXPLAIN ANALYZE כדי לוודא ששאילתות משתמשות באינדקסים צפויים ולזהות צווארי בקבוק בביצועים. תוכנית השאילתות מראה האם PostgreSQL משתמשת באינדקסי המטא-דאטה שלך כדי לסנן מועמדים לפני חיפוש וקטורי, או אם היא פונה לסריקות עוקבות שבודקות כל שורה. חפש סריקת אינדקס או סריקת אינדקס ביטמאפ על עמודות מטא-דאטה (יעילה), סריקת אינדקס באמצעות אינדקס וקטורי (יעיל), וסריקת Seq בטבלאות גדולות (פוטנציאלית לא יעילה). אם אתה רואה סריקות רציפות בלתי צפויות, בדוק שקיימים אינדקסים מתאימים ושהסטטיסטיקות עדכניות באמצעות ANALYZE products;.
חלק מהשאילתות לא מתאימות לסינון יעיל. כאשר המסננים לא מבטלים הרבה שורות (כמו WHERE price > 0 שכמעט כל המוצרים תואמים), PostgreSQL עשוי לדלג על אינדקסי מטא-דאטה לחלוטין ולהסתמך רק על אינדקס הווקטור. זו התנהגות צפויה כי האופטימייזר מקבל החלטות מבוססות עלות.
לפעמים צריך תוצאות מאינדקס הווקטורים שגם הן עומדות במגבלות שלא ניתן לסנן אותן ביעילות מראש. תבנית הפוסט-סינון מביאה יותר מועמדים דומים לווקטורים מהנדרש, ואז מיישמת מסננים. כוון את ה-LIMIT הפנימי בהתאם לסלקטיביות המסנן הצפויה.
-- Get more candidates than needed, then filter
WITH candidates AS (
SELECT id, name, price, in_stock, embedding <=> $1 AS distance
FROM products
ORDER BY embedding <=> $1
LIMIT 100
)
SELECT id, name, distance
FROM candidates
WHERE in_stock = true AND price BETWEEN 100 AND 500
ORDER BY distance
LIMIT 10;
חלוקת טבלאות למערכי נתונים גדולים
חלוקה מחלקת שולחן גדול לחלקים קטנים יותר ונוחים יותר. בעומסי עבודה וקטוריים, חלוקה יכולה לשפר את ביצועי השאילתות ולפשט את התחזוקה.
שקול חלוקה כאשר הטבלאות עולות על עשרות מיליוני שורות, השאילתות מסננות באופן טבעי לפי מפתח מחיצה (תאריך, קטגוריה, דייר), צריך להוריד נתונים ישנים ביעילות (חיתוך מחיצות), או שזמני בניית האינדקס הופכים לבלתי אפשריים בטבלה המלאה.
ביישומים שמעבדים הטמעות בסדרות זמן (כגון וקטורי פעילות משתמש או תוכן שפורסם לאורך זמן), חלוקת טווח לפי תאריך היא יעילה. שאילתות שמסננות לפי תאריך סורקות רק מחיצות רלוונטיות. לכל מחיצה יש אינדקסים משלה, מה שהופך את התחזוקה לנוחה יותר.
-- Create partitioned table
CREATE TABLE user_interactions (
id BIGSERIAL,
user_id BIGINT NOT NULL,
embedding vector(768),
created_at TIMESTAMP NOT NULL,
interaction_type TEXT
) PARTITION BY RANGE (created_at);
-- Create partitions for each month
CREATE TABLE user_interactions_2025_01
PARTITION OF user_interactions
FOR VALUES FROM ('2025-01-01') TO ('2025-02-01');
ליישומים מרובי שוכרים או קטלוגי מוצרים עם קטגוריות טבעיות, חלוקת רשימות או גיבוב יכולה לעזור. שאילתות המסוננות לפי קטגוריה סורקות רק את המחיצה הרלוונטית, ומקטינות הן את גודל הנתונים שנסרקו והן את גודל האינדקס.
צור אינדקסים בטבלת האב כדי ליצור אוטומטית אינדקסים תואמים בכל המחיצות באמצעות CREATE INDEX ON products USING hnsw (embedding vector_cosine_ops);. לכל מחיצה יש אינדקס משלה, שניתן לבנות או לבנות מחדש באופן עצמאי. זה בעל ערך עבור מערכי נתונים גדולים שבהם בניית מדד גלובלי אחד תיקח שעות.
חלוקה מוסיפה מורכבות. שאילתות שמכסות מספר מחיצות עשויות להיות איטיות יותר מאשר בטבלה אחת. אילוצים ייחודיים חוצי-חלוקות דורשים את מפתח המחיצה בתוך האילוצ. לוגיקת היישום עשויה לדרוש מודעות לגבולות החלוקה. בדוק האם דפוסי השאילתות שלך תואמים למפתחות מחיצה פוטנציאליים לפני יישום חלוקת המחיצות.