אינדקסים מבוססי גרפים — HNSW

מהו HNSW?

HNSW — ראשי תיבות של Hierarchical Navigable Small World — הוא אחד מאלגוריתמי ה-ANN הנפוצים והמוצלחים ביותר בפרקטיקה, והאינדקס המובנה כברירת מחדל ברוב מסדי הנתונים הווקטוריים המובילים. הרעיון המרכזי: לארגן את כל הוקטורים כגרף רב-שכבתי (multi-layer graph), שבו כל וקטור הוא צומת (node), וצמתים קרובים זה לזה במרחב מחוברים בקשתות (edges).

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

תרשים המציג שלוש שכבות גרף מוערמות זו מעל זו: השכבה העליונה דלילה עם 3 צמתים בלבד וקשתות ארוכות, השכבה האמצעית צפופה יותר, והשכבה התחתונה הצפופה ביותר מכילה את כל הצמתים. קווים מקווקווים אנכיים מקשרים בין אותה נקודה פיזית בשכבות שונות. מסלול כתום מקווקו עם חצים מדגים חיפוש חמדני שמתחיל בנקודת כניסה בשכבה העליונה ויורד שכבה אחר שכבה עד לצומת היעד בשכבה התחתונה.
מבנה רב-שכבתי של HNSW: שכבה עליונה דלילה למעבר מהיר, ירידה הדרגתית לשכבות צפופות יותר עד לצומת היעד

איך מתבצע חיפוש בגרף

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

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

ההשוואה המדויקת ביותר היא לרשימת דילוג (skip list): קודם קופצים קפיצות גדולות שמצמצמות מהר את המרחב הרלוונטי לחיפוש, ורק לקראת הסוף עוברים לחיפוש עדין ומדויק באזור הקטן שכבר אותר.

למה זה מהיר — אינטואיציה לסיבוכיות

בניגוד לסריקת brute-force שבודקת כל וקטור בבסיס הנתונים (סיבוכיות ליניארית, O(n)), החיפוש בגרף HNSW "מדלג" על רוב הצמתים לגמרי — כל שכבה מצמצמת את מרחב החיפוש הרלוונטי משמעותית לפני שממשיכים לשכבה הבאה, מה שנותן לחיפוש סיבוכיות שקרובה ללוגריתמית ביחס לגודל בסיס הנתונים (בערך O(log n)) במקום ליניארית.

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

גרף המציג שני עקומים על אותם צירים: קו ישר כתום העולה בתלילות משמאל לימין (סיבוכיות O(n) של brute-force, גדל ליניארית), ועקומה כחולה שעולה בחדות בהתחלה ואז משתטחת ומתקרבת כמעט לאופקית (סיבוכיות O(log n) של HNSW) — שני הקווים מתחילים יחד בראשית ונפרדים ככל שגודל בסיס הנתונים (ציר X) גדל.
זמן חיפוש כפונקציה של גודל בסיס הנתונים: O(n) ב-brute-force מול O(log n) קרוב ב-HNSW

המחיר: זיכרון וזמן בנייה

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

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

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