למי זה לא קרה? נכנסת ל-KSP, הקלדת שם של מוצר וקיבלת תוצאות גרועות, לא רלוונטיות או חלקיות. ואז, דילגת לגוגל, הקלדת את אותה שאילתה בדיוק, וגוגל החזירה לך את דף המוצר המדויק לאותו אתר בדיוק. המוצר קיים, KSP מכירה אותו, אבל מנוע החיפוש שלה פשוט לא עובד. טוב לא חוכמה, גוגל השקיעה בחיפוש יותר כסף, דאטה וכוח מחשוב מכל חברה מסחרית בעולם, אבל איך אפשר לבנות חיפוש טוב לאתר בעצמנו? רוב האתרים מתייחסים לחיפוש כפעולה על טקסט: המשתמש כתב מילים? בוא נחפש את המילים בתוך רשומות. גוגל מתייחסת לחיפוש כניסיון לצמצם אי-ודאות: אדם מסוים, בהקשר מסוים, רוצה משהו, אבל כל מה שיש לנו ממנו הוא כמה מחרוזות קצרות, לעיתים לא מדויקות. מנוע החיפוש צריך לקחת את הרמזים האלה, להבין למה המשתמש התכוון, למצוא קבוצה רחבה מספיק של אפשרויות, לצמצם אותה, לסדר אותה וללמוד מהתגובה. וזה בדיוק מה שנלמד בפרק הזה: חיפוש טוב הוא לא רק אלגוריתם שמוצא תוצאה. הוא שרשרת של מערכות שכל אחת מהן מקלפת סוג אחר של אי-ודאות. יש במערכת הזו AI, כמובן, אבל אי אפשר להחליף את כולה ב-AI ולסגור עניין. בכל שלב בשרשרת משהו אובד (בא׳), משהו משתפר ונוצרת בעיה חדשה ששלב נוסף צריך לפתור. אם נבין את השרשרת הזאת, נבין למה מנוע חיפוש באתרים כל כך גרוע, ואיך אפשר לדמות אותו לגוגל, בלי התקציב של גוגל. אי-הוודאות הראשונה היא השפה. משתמשים לרוב לא דוברים את שפת הקטלוג. הקטלוג מכיל את השורה "קרם לחות היפואלרגני ללא בישום", והמשתמש, מחפש "קרם פנים שלא יעשה לי אדום". הקטלוג מכיל את השורה "תיק נסיעות 40 ליטר", והמשתמש, מחפש "טרולי שאפשר להעלות לאל על". בהרבה מקרים, קטלוג מתאר תכונות, בעוד משתמש מתאר שימוש, כאב, מגבלה או תוצאה רצויה. אם כל המשתמשים היו יודעים להקליד "Nike Air Force לבנות מידה 39", כנראה שלא היינו צריכים את הפרק הזה. זו שאילתה קלה שיש בה מותג, דגם צבע ומידה. "נעליים נוחות לטיול בקיץ" היא שאילתה הרבה יותר קשה, ולא מכילה אפילו מילה אחת משם המוצר. מנוע החיפוש לא יכול רק לאתר טקסט. הוא צריך לתרגם בין שני תיאורים שונים של המציאות. התרגום הזה מתחיל עוד לפני שהגיעה השאילתה הראשונה. אם אתן ליעל מחר משימה לבנות חיפוש עבור מיליון מוצרים, הצעד הראשון שלה לא יהיה אלגוריתם או מודל ספציפי, הוא יהיה schema - איך ליצור מבנה טבלאי ששומר על הקטלוג המקורי, אבל מוסיף לצידו כותרת נקיה, תיאור נקי, מאפיינים מסודרים. מותג, צבע, חומר, נפח וקטגוריה לא יכולים להמשיך להתחבא בתוך פסקת תיאור מפי היבואן, צריך לחלץ אותם מהטקסט, לייבא אותם, להעשיר אותם. זה החלק שבו מודל שפה גדול שימושי מאוד, דווקא כי הוא לא פוגש את המשתמש בזמן אמת. פשוט מריצים העשרה (enrichment) אופליין, בבאצ׳ים זולים, ומבקשים מהמודל לנרמל כותרת, להשלים תיאור חסר ולחלץ attributes לתוך סכמת JSON קשיחה. זה יאפשר לו להבין ש-100 מיליליטר ו-0.1 ליטר מייצגים אותו נפח, ושקרם, שנהב ו-off-white קרובים מספיק כדי להיכנס לאותה פלטת צבעים. LLM יכול גם להמציא, אז ממש לא כדאי לקבל כל תוצאה שלו כתורה מסיני. נפח חייב להיות ביחידות הנכונות, מותג חייב להגיע מרשימה סגורה ופלט שאינו עומד ב-schema נדחה. לפני שתריצו מיליון מוצרים, בואו תריצו אלף ותבדקו אותם אקראית, אחר כך תוסיפו דגל שעוקב מתי מוצר עבר enrichment. זה לא סתם בירוקרטיה, זו הדרך שלכם למנוע ממודל שפה להזות, ולהעשיר את הקטלוג במידע שלא היה בו מעולם. לאחר שלב העשרה, מתבצע שלב הטבעה או embedding. כל המידע עובר במודל וקטורי שממקם את המוצר ב-500, אלף או 1500 נקודות במרחב. התוצאה של המודל הזה, והיא בעיקר מספרים לא מובנים, צריכה להישמר בדאטהבייס לצד המוצר יחד עם גירסת המודל שהטביעה אותו. אם בחרתם מודל מקומי של 512 ממדים כדי לחסוך זיכרון, ומישהו עם משקפיים הציע לעבור ל-1,536 של OpenAI, זה אומר להטביע מחדש כל מוצר. אגב, עבור קטלוג של מיליון מוצרים, ה-Postgres שלכם מספיק בהחלט. לא צריך Elastic Search או Vector Database. לא צריך Kafka, צריך רק להחליט מאיפה לצרוך את מודל ה-embedding, וממי. בשלב ראשון אפשר לקנות אותם כ-API. סיימנו את שלב ההכנות הבסיסי, יש לנו דאטהבייס של מיליון מוצרים, עם שדות חפישים ווקטורים לצידם. גזרנו את הסרט והשאילתה הראשונה מגיעה אלינו דרך האתר. לפני שהיא תגיע לאינדקסים היא זקוקה לניקוי קצר: lowercase, הסרת רווחים כפולים, סימני פיסוק ותיקון שגיאות. שגיאת כתיב יכולה לקלקל את כל השרשרת. זה נכון שמשתמש שכתב אדידדס עם 3 דלת יקבל משהו, אבל אם אפשר לנקות ולתקן את השאילתה פעם אחת, היא תישלח נכון לכל המנגנונים, מבלי שיצטרכו להמציא פרשנות נפרדת. שימו לב, אנחנו לא מחליפים למשתמש את השאילתה המקורית, זה יהיה מוזר. אנחנו משתמשים במתוקנת, אבל שומרים את שתיהן כדי לדעת בהמשך אילו תיקונים עזרו ומה הפך שאילתה טובה למשהו אחר. אבל איך מתקנים שאילתה? הפתרון הנאיבי הוא לעבור על כל המילים במילון ולחשב את המרחק עריכה בינן לבין המילה שהמשתמש כתב. מרחק עריכה, בדרך כלל Levenshtein, סופר כמה הכנסות, מחיקות, החלפת אותיות או שיכול אותיות נדרשו כדי להפוך מילה אחת לאחרת. במילון קטן זה עובד, במנוע חיפוש שמקבל כמה מאות שאילתות לשניה, זה מתחיל להיות יקר. הפתרון המפורסם של Peter Norvig מייצר בזמן השאילתה את כל המילים האפשרויות במרחק עריכה נתון, ובודק אילו מהן קיימות במילון. יעיל, אבל למילה של 9 אותיות ומרחק 2, אפשר להגיע ל-114 אלף מועמדים. אלגוריתמים יעילים יותר מייצרים מחיקות גם מהמילה השגויה, ומצמצמים, את אותן 9 אותיות, ל-36 מועמדים בלבד. הם גם עושים את זה טרום שאילתה ובעיבוד מוקדם - בדיוק המקום שאנחנו מעדיפים לשלם בו. אבל איך יוצרים מילון? לוקחים אחד קיים ומעבים אותנו בעצמנו, מתוך כל המופעים של כל המילים מכל המוצרים, מסודרים לפי שכיחות. אנחנו סוף סוף מבצעים חיפוש לשוני ראשון, פעם TF-IDF, היום BM25 - אלגוריתמים שמעניקים חשיבות למילים לפי התדירות שלהן במסמך, אבל גם בכלל האוסף. כרית פלוצים שווה יותר מכרית שינה, והתאמה בכותרת יכולה לקבל משקל גבוה יותר מהתאמה בתיאור. BM25 לא מבין כוונה, אבל הוא מצטיין בדברים שחיפוש סמנטי נוטה לטשטש: שם מותג, דגם, מק"ט ומינוחים מדויקים. אם המשתמש כתב iPhone 17 Pro, הוא כנראה רוצה אפל מאוד ספציפי, ולא "טלפון למקצוענים". חיפוש שני יהיה סמנטי. השאילתה הנקיה עוברת וקטוריזציה בעצמה, עם אותו מודל וקטורי שהפעלנו על המוצרים. התוצאה היא שוב 500, אלף או 1500 ממדים שאפשר למצוא להם שכנים קרובים. אם חיפשתי "נעליים לעמוד איתן כל היום", אז מוצרים שיש בהם תיאור של "תמיכה", "ריפוד", "נעלי עבודה" יחזרו קודם. חיפוש סמנטי לא זקוק לחפיפה מילולית מושלמת, כי הוא משווה ייצוגים של משמעויות או קירובים של מונחים במרחב, אבל זו גם החולשה שלו. קרבה סמנטית בין כבל, מטען, מפצל ומתאם אינה אומרת שהמוצרים תחליפיים. חיפוש סמנטי יודע להחליק הבדלים, אבל ההחלקה הזו לפעמים מגליצ׳ה משתמשים אל מחוץ לאתר. חיפוש שלישי הוא בתוך ביקורות. תיאור המוצר נכתב כדי למכור. ביקורת נכתבת אחרי שימוש, ולכן מכילה מידע מסוג אחר. היצרן יכתוב "סוליה ארגונומית", המשתמש יוסיף "חרשתי איתה את ליסבון בכיף במשך שלושה ימים". היצרן יכתוב "ניחוח עדין", המשתמש יוסיף "אין סיכוי לשבת עם הבושם הזה בקרון סגור". בשאילתות שעוסקות בחוויה ולא במפרט, ביקורות הן מקור מעולה. לא מצרפים את כל הביקורות לטקסט אחד ענק ועושים embedding, גם כי אי אפשר במגבלות האורך, וגם כי אנחנו רוצים לשמור לכל ביקורת שני משקלים - ה-sentiment (כמה היא חיובית או שלילית) וה-source (האם היא מגולש אנונימי, רוכש מאומת או אתר שלישי) ולנקד אותם בהתאם. שלושת החיפושים האלה (לשוני, סמנטי וביקורות) יכולים לרוץ במקביל ולהחזיר, למשל, חמישים תוצאות כל אחד. בשלב הזה יש ערבוב של מוצרים וביקורות, לעיתים של אותו מוצר. מכל מוצר אנחנו רוצים לשמור את ההתאמה הטובה ביותר, וציון משוקלל של הביקורות שלו. אבל עוד משהו מעורבב, כל חיפוש חזר עם סולם ציונים אחר. ציון BM25 אינו בר השוואה ל-cosine similarity, ואם ביקורות השתמשו במודל embedding אחר, הם יחזרו בהתפלגות שונה. אפשר להחליט על משקל ידני, למשל 40 אחוז ללשוני, 50 אחוז וקטור ו-10 אחוז ביקורות, אבל זה מצריך כיול מחדש בכל פעם. Reciprocal Rank Fusion, או RRF, נמנע מההשוואה הזאת. הוא מתעלם מהציון הגולמי ומסתכל על המיקום היחסי של המוצר בכל אחת מהרשימות. מוצר שמדורג שני בשתי רשימות יכול לעקוף מוצר שמדורג ראשון ברשימה אחת בלבד. הסכמה בין מנגנונים שונים נחשבת חזקה יותר מביטחון קיצוני של מנגנון בודד. התוצאה של RRF עדיין אינה דף תוצאות. היא דף מועמדים, נניח מאה מוצרים, והיא הבחנה חשובה בין retrieval (שליפה) ל-ranking (מיון). בשליפה אנחנו רוצים recall גבוה - לוודא שהמוצרים הנכונים נמצאים איתנו בחדר. במיון אנחנו רוצים precision גבוה - להחליט מי יישב בשורה הראשונה. אבל אם המוצר הנכון לא נכנס ל-100 המועמדים, שום מיון לא יעזור. אנחנו בשלושת רבעי הדרך. המשתמש שאל משהו לפני פחות משניה, יש לנו את השאילתה שלו ומאה מועמדים פוטנציאליים. זה הזמן להפעיל מודל reranker יקר ומדויק יותר. בניגוד לחיפוש וקטורי, שבו השאילתה והמוצר עברו הטבעה בנפרד, ה-cross-encoder קורא אותם יחד. הוא ממקם את השאילתה לצד 100 תקצירי מוצר ומחזיר ציון התאמה נוסף. ה-reranker, לכל ה-100 מופעל בקריאת API אחת. קובעים גם timeout, נניח 800 מילישניות. אם ה-API לא ענה, לא מחזירים שגיאה, חוזרים אחורה ל-RRF. אם ה-embedding נפל, חוזרים אחורה ל-BM25. מערכת חיפוש טובה לא צריכה לעבוד בבריאות מלאה, היא צריכה להישאר שימושית גם שחלקים ממנה חולים. הרלוונטיות שה-reranker מחזיר אינה כליל השלמות. מוצר יכול להתאים כמו כפפה לשאילתה אבל המשתמש כבר הזמין אותו. הוא יכול להיראות נפלא בטקסט, אבל לסבול מהחזרות ע"י רבע מהלקוחות. הוא יכול להיראות קצת פחות נפלא, אבל עם דירוג ואחוז המרה מעולה. זה בדיוק השלב שבו עוברים מ-relevance חזרה ל-ranking, אבל הפעם - מיון שלומד ומשתפר בעצמו. בגרסה הראשונה אין עדיין דאטה שמצדיק מודל למידה. אפשר לייצר איזה blend ידני שלוקח דירוג ממוצע, מכפיל אותו ב-sentiment, מעלה בריבוע פופולריות ומחלק בשורש החזרות, אבל זה יהיה blend שמנחש. למידה מתחילה רק כאשר המערכת שומרת את התוצאות ומנטרת אותן. בשביל זה צריך להצמיד לכל חיפוש מזהה ייחודי, search ID ולחלחל אותו לכל פעולה - click, add to cart, checkout, thumbs up, thumbs down, החזרה כולם, ברמת המוצר הבודד, ישמרו יחד עם ה-search ID. טכני ומייגע, אני יודע, אבל זה מה שיקבע אם יש לכם סתם analytics או training data. אם מוצר נמכר, אתם יודעים משהו על המוצר. אם מוצר שהוצג במקום השישי עבור "קרם פנים שלא שורף", נלחץ ונקנה בתוך חצי שעה, אתם יודעים משהו על החלטת החיפוש. לאחר כמה שבועות של לוגים, אפשר להתחיל לאמן מודל XGBoost. כל אירוע מקבל משקל (thumbs down למשל הוא שלילי חזק, גם החזרה, click הוא חיובי, ו-add to cart או purchase חיוביים חזק). כשהמודל מוכן, וזה משהו שאפשר לאמן כל לילה תוך דקות, אפשר להריץ עליו כל מועמד ולקבל ציון שלומד ומתעדכן עם כל מידע חדש. המנוע מוכן, אפשר לשים אותו מאחורי feature flag ולהציג אותו לעובדים, שיבדקו side-by-side מול המנוע הישן. זה שם אתכם כנראה במאיון של מנועי החיפוש לאתרים בעולם. אם תרצו להגיע לאלפיון, תצטרכו להוסיף עוד כמה דברים: קאשינג - לשמור את התוצאה במטמון, למשל ב-Redis. זה חוסך retrieval, embedding ו-reranking אבל לא פוטר אתכם מאיסוף הדאטה. ניטור שיהוי - עם יעד ל-95% מהשאילתות בפחות מ-500 מילישניות מהמטמון, או 1500 מילישניות לחיפוש קר. שימו לב ליצור היסטוגרמה נפרדת לכל שלב בתהליך. ניצול מול גילוי או מיצוי מול חיפוש - אם נציג תמיד את אותן 20 תוצאות, מוצרים בזנב הארוך או שמעולם לא דורגו גבוה, ימשיכו לנבול, וייראו חלשים גם באימון הבא. exploration פותר את בעיית הקיפאון. אפשר ב-5% מהמקרים להקפיץ תוצאות מהמקומות הנמוכים כדי לתת הזדמנות לאחרים. Fine-tuning ל-embedding model - וזה רק אם המדידה מראה כשל ב-retrieval. אפשר לבדוק למשל, אם עגלת קניות שהושלמה כללה מוצר שלא היה ב-100 המועמדים. אם המוצר נעדר שוב ושוב, הייצוג הסמנטי לא מספיק לדומיין שלכם ואפשר לשקול מודל הטבעה מותאם. זה שינוי יקר יותר מאימון XGBoost כי הוא מחייב להפיק מחדש את כל הוקטורים בקטלוג. נסכם. הוספנו רכיבים כי זה שקדם להם לא מסוגל לפתור סוג מסוים של אי-ודאות. enrichment מסדר את המציאות לפני שמחפשים בה. תיקון שגיאות מסדר את כוונת המשתמש. חיפוש לשוני שומר על פרטים מילוליים מדויקים. חיפוש סמנטי מחבר בין ניסוחים שונים של אותו צורך. ביקורות מוסיפות ניסיון אנושי שאין בקטלוג. RRF מאחד ציונים בלתי ניתנים להשוואה. Retrieval רחב מגן על ה-Reranker. מודל XGBoost מחבר רלוונטיות למציאות עסקית. Logging הופך דאטה לבית אימון ו-Exploration מונע ממך לקבע הטיות. עכשיו זה גם ברור למה גוגל טובה כל כך. לא בגלל שיש לה אלגוריתם סודי אחד, אלא כי היא בנתה במשך עשורים שרשרת שמצמצמת אי-ודאות, מודדת את עצמה ולומדת מהאופן שבו אנשים מגיבים. רוב האתרים פשוט שמו שורת חיפוש כמו בגוגל, דחסו אותה לאינדקס אחד, ואז הופתעו שהחיפוש יצא גרוע. אתר עם מיליון מוצרים לא צריך להישען על חיפוש בגוגל. הוא צריך להתייחס לחיפוש כאל מוצר. עם סכמה ופייפליין, גרסאות, שיהוי, fallback, ניסויים ומדדי איכות. במשך שנים חיפוש היה דרך שבה המשתמש למד את המבנה שהאתר כפה עליו. הוא היה צריך להכיר את שם הקטגוריה, את המונח המקצועי, את הפילטר הנכון ואת האיות שבו בחר היבואן. במדויק. מערכת חיפוש חדשה הופכת את היחסים האלה. המשתמש לא צריך ללמוד את השפה של הקטלוג. הקטלוג צריך ללמוד איך המשתמש שלו חושב, ולהסתגל אליו, אל הרצונות שלו, מדי יום. עד הפעם הבאה, תהיו טובים, ותמשיכו להיות סקרנים. יאללה ביי.