יצירה וניהול של אינדקסים של וקטורים

בדף הזה נסביר איך ליצור ולנהל אינדקסים של וקטורים ב-Spanner, שמשתמשים בחיפוש של השכן הקרוב המשוער (ANN) ובמבנים מבוססי-עץ כדי להאיץ את החיפושים של דמיון וקטורים בנתונים שלכם.

‫Spanner מאיץ חיפושים של וקטורים של שכן קרוב משוער (ANN) באמצעות אינדקס וקטורים ייעודי. האינדקס הזה מבוסס על Scalable Nearest Neighbor (ScaNN) של Google Research, אלגוריתם יעיל במיוחד לחיפוש השכן הקרוב ביותר.

אינדקס הווקטורים משתמש במבנה מבוסס-עץ כדי לחלק את הנתונים ולזרז את החיפושים. ‫Spanner מציע הגדרות של עץ עם שתי רמות ועם שלוש רמות:

  • הגדרת עץ דו-רמתי: צמתי עלים (num_leaves) מכילים קבוצות של וקטורים קרובים יחד עם המרכז המקביל שלהם. רמת הבסיס מורכבת מהצנטרואידים מכל צמתי העלה.
  • הגדרה של עץ עם שלוש רמות: דומה לקונספט של עץ עם שתי רמות, אבל עם שכבת ענפים נוספת (num_branches), שממנה מרכזי הצמתים של העלים מחולקים עוד יותר כדי ליצור את רמת השורש (num_leaves).

‫Spanner בוחר בשבילכם אינדקס. עם זאת, אם אתם יודעים שאינדקס ספציפי הוא המתאים ביותר, אתם יכולים להשתמש ברמז FORCE_INDEX כדי לבחור את אינדקס הווקטור המתאים ביותר לתרחיש לדוגמה שלכם.

למידע נוסף, אפשר לעיין במאמרים בנושא הצהרות VECTOR INDEX ב-GoogleSQL והצהרות INDEX ב-PostgreSQL.

מגבלות

יצירת אינדקס וקטורי

כדי לשפר את ההחזרה והביצועים של אינדקס וקטורי, מומלץ:

  • יוצרים את אינדקס הווקטורים אחרי שרוב השורות עם ההטמעות נכתבות למסד הנתונים. יכול להיות שתצטרכו גם לבנות מחדש את אינדקס הווקטורים באופן תקופתי אחרי שמוסיפים נתונים חדשים. מידע נוסף זמין במאמר בנושא בנייה מחדש של אינדקס הווקטורים.

  • ב-GoogleSQL, משתמשים בסעיף STORING, וב-PostgreSQL, משתמשים בסעיף INCLUDE כדי לאחסן עותק של עמודה באינדקס הווקטורי. אם ערך של עמודה מאוחסן באינדקס vector, ‏ Spanner מבצע סינון ברמת העלה של האינדקס כדי לשפר את ביצועי השאילתה. מומלץ לאחסן עמודה אם היא משמשת בתנאי סינון.

  • שימוש בעמודות מפתח שאינן מוטמעות באינדקס הווקטורי. העמודות המרכזיות דומות לעמודות STORING או INCLUDE, אבל הן מאפשרות למנוע השאילתות לבצע סינון בצורה יעילה יותר במהלך חיפוש וקטורי. מידע נוסף זמין במאמרים בנושא יצירת אינדקס וקטורי (GoogleSQL) או הצהרות אינדקס (PostgreSQL).

כשיוצרים את הטבלה, עמודת ההטמעה צריכה להיות מערך של סוג הנתונים FLOAT32 (GoogleSQL) או float4[] (PostgreSQL) (מומלץ), וצריכה להיות לה הערה של אורך הווקטור (vector_length=>N ל-GoogleSQL או VECTOR LENGTH N ל-PostgreSQL), שמציינת את המימד של הווקטורים.

האורך האופטימלי של הווקטור תלוי בעומס העבודה, בגודל מערך הנתונים ובמשאבי המחשוב הזמינים. כדאי להתנסות עם מימדים שונים כדי למצוא את הגודל הכי קטן ששומר על הדיוק והביצועים של האפליקציה.

הצהרת ה-DDL הבאה יוצרת טבלת Documents עם עמודת הטמעה DocEmbedding עם אורך וקטור:

GoogleSQL

CREATE TABLE Documents (
  UserId INT64 NOT NULL,
  DocId INT64 NOT NULL,
  Author STRING (1024),
  DocContents Bytes(MAX),
  DocEmbedding ARRAY<FLOAT32>(vector_length=>128) NOT NULL,
  NullableDocEmbedding ARRAY<FLOAT32>(vector_length=>128),
  WordCount INT64
) PRIMARY KEY (DocId);

PostgreSQL

CREATE TABLE documents (
  user_id bigint not null,
  doc_id bigint not null,
  author varchar(1024),
  doc_contents bytea,
  doc_embedding float4[] VECTOR LENGTH 128 not null,
  nullable_doc_embedding float4[] VECTOR LENGTH 128,
  word_count bigint,
  PRIMARY KEY (doc_id)
);

אחרי שממלאים את הטבלה Documents, אפשר ליצור אינדקס וקטורי עם עץ דו-רמתי ו-1,000 צמתי עלים בטבלה Documents עם עמודת הטמעה DocEmbedding באמצעות מרחק קוסינוס:

GoogleSQL

CREATE VECTOR INDEX DocEmbeddingIndex
  ON Documents(DocEmbedding)
  STORING (WordCount)
  OPTIONS (distance_type = 'COSINE', tree_depth = 2, num_leaves = 1000);

PostgreSQL

CREATE INDEX doc_embedding_index
  ON documents
  USING scann(doc_embedding)
  INCLUDE (word_count)
  WITH (distance_type = 'COSINE', num_leaves = 1000)
  WHERE doc_embedding IS NOT NULL;

אם עמודת ההטמעה לא מסומנת כ-NOT NULL בהגדרת הטבלה, צריך להצהיר עליה באמצעות פסקה WHERE COLUMN_NAME IS NOT NULL בהגדרת אינדקס הווקטור, כאשר COLUMN_NAME הוא השם של עמודת ההטמעה. כדי ליצור אינדקס וקטורי עם עץ בן שלוש רמות ו-1,000,000 צמתי עלים בעמודת ההטמעה NullableDocEmbedding שאפשר להזין בה ערך null, באמצעות מרחק קוסינוס:

GoogleSQL

CREATE VECTOR INDEX DocEmbeddingThreeLevelIndex
  ON Documents(NullableDocEmbedding)
  STORING (WordCount)
  WHERE NullableDocEmbedding IS NOT NULL
  OPTIONS (distance_type = 'COSINE', tree_depth = 3, num_branches=1000, num_leaves = 1000000);

PostgreSQL

CREATE INDEX doc_embedding_index
  ON documents
  USING scann(nullable_doc_embedding)
  INCLUDE (word_count)
  WITH (distance_type = 'COSINE', tree_depth = 3, num_branches = 1000, num_leaves = 1000000)
  WHERE nullable_doc_embedding IS NOT NULL;

סינון אינדקס וקטורי

אפשר גם ליצור אינדקס וקטורי מסונן כדי למצוא את הפריטים הכי דומים במסד הנתונים שתואמים לתנאי הסינון. אינדקס וקטורי מסונן מבצע אינדוקס באופן סלקטיבי של שורות שעומדות בתנאי הסינון שצוינו, וכך משפר את ביצועי החיפוש.

בדוגמה הבאה, לטבלה Documents2 יש עמודה בשם Category. בחיפוש הווקטורי שלנו, אנחנו רוצים להוסיף לאינדקס את הקטגוריה 'טכנולוגיה', ולכן אנחנו יוצרים עמודה שנוצרת על ידי המערכת ומחזירה את הערך NULL אם תנאי הקטגוריה לא מתקיים.

GoogleSQL

CREATE TABLE Documents2 (
  UserId INT64 NOT NULL,
  DocId INT64 NOT NULL,
  DocName STRING (1024),
  Author STRING (1024),
  DocContents Bytes(MAX),
  Category STRING(MAX),
  NullIfFiltered BOOL AS (IF(Category = 'Tech', TRUE, NULL)) HIDDEN,
  DocEmbedding ARRAY<FLOAT32>(vector_length=>128)
) PRIMARY KEY (DocId);

PostgreSQL

CREATE TABLE documents2 (
  user_id bigint not null,
  doc_id bigint not null,
  doc_name varchar(1024),
  author varchar(1024),
  doc_contents bytea,
  category varchar,
  null_if_filtered boolean GENERATED ALWAYS AS (CASE WHEN category = 'Tech' THEN true END) VIRTUAL HIDDEN,
  doc_embedding float4[] VECTOR LENGTH 128,
  PRIMARY KEY (doc_id)
);

לאחר מכן, יוצרים אינדקס וקטורי עם מסנן. האינדקס הווקטורי TechDocEmbeddingIndex מוסיף לאינדקס רק מסמכים בקטגוריה 'טכנולוגיה'.

GoogleSQL

CREATE VECTOR INDEX TechDocEmbeddingIndex
  ON Documents2(DocEmbedding)
  STORING(NullIfFiltered)
  WHERE DocEmbedding IS NOT NULL AND NullIfFiltered IS NOT NULL
  OPTIONS (...);

PostgreSQL

CREATE INDEX tech_doc_embedding_index
  ON documents2
  USING scann(doc_embedding)
  INCLUDE (null_if_filtered)
  WITH (distance_type = 'COSINE', num_leaves = 1000)
  WHERE doc_embedding IS NOT NULL AND null_if_filtered IS NOT NULL;

כש-Spanner מריץ את השאילתה הבאה, שיש לה מסננים שתואמים ל-TechDocEmbeddingIndex, הוא בוחר באופן אוטומטי את TechDocEmbeddingIndex ומאיץ את הפעולה באמצעות TechDocEmbeddingIndex. השאילתה מחפשת רק מסמכים בקטגוריה 'טכנולוגיה'. אפשר גם להשתמש ברמז FORCE_INDEX (@{FORCE_INDEX=TechDocEmbeddingIndex} ל-GoogleSQL או /*@ FORCE_INDEX = tech_doc_embedding_index */ ל-PostgreSQL) כדי לחייב את Spanner להשתמש באינדקס באופן מפורש.

GoogleSQL

SELECT *
FROM Documents2
WHERE DocEmbedding IS NOT NULL AND NullIfFiltered IS NOT NULL
ORDER BY APPROX_(....)
LIMIT 10;

PostgreSQL

SELECT *
FROM documents2
WHERE doc_embedding IS NOT NULL AND null_if_filtered IS NOT NULL
ORDER BY spanner.approx_cosine_distance(doc_embedding, ARRAY[1.0::float4, 2.0::float4, 3.0::float4])
LIMIT 10;

כדי לשפר את הביצועים של השאילתות, אפשר לכלול עמודות מפתח שאינן הטמעה באינדקס הווקטורי. כך מנוע השאילתות יכול לבצע סינון בצורה יעילה יותר במהלך חיפוש וקטורי.

בהצהרה על יצירת האינדקס, צריך לפרט את עמודות המפתח הנוספות האלה אחרי עמודת ההטמעה. לדוגמה, ההצהרה הבאה יוצרת אינדקס וקטורי שכולל את עמודות המפתח DocName ו-Author לסינון יעיל יותר:

GoogleSQL

CREATE VECTOR INDEX DocEmbeddingIndexWithKeys
  ON Documents2(DocEmbedding, DocName, Author)
  STORING(NullIfFiltered)
  WHERE DocEmbedding IS NOT NULL AND NullIfFiltered IS NOT NULL
  OPTIONS (...);

PostgreSQL

CREATE INDEX doc_embedding_index_with_keys
  ON documents2
  USING scann(doc_embedding, doc_name, author)
  INCLUDE (null_if_filtered)
  WITH (distance_type = 'COSINE', num_leaves = 1000)
  WHERE doc_embedding IS NOT NULL AND null_if_filtered IS NOT NULL;

המאמרים הבאים