רוב תיבות החיפוש קיימות כדי לענות על שאלה אחת: מה תואם למילים שהקלדתי? זו השאלה הנכונה עבור קטלוג ספרייה. זו השאלה הלא נכונה עבור אפליקציית תזונה.
כשמשתמש פותח את עמוד המתכונים ב-Appetec, המילים שהוא מקליד הן החלק הפחות מעניין בבקשה. מה שבאמת חשוב הוא בלתי נראה: קלוריות שנותרו להיום, מה שהם בישלו שבוע אחרי שבוע מבלי לחשוב על כך, האם הם vegan, והאם הם כבר חרגו מהתקציב שלהם עד ארוחת הערב. מנוע חיפוש שמתעלם מכל זה יחזיר בשמחה מתכונים תואמים — ובאותה מידה ישבש בשמחה את הסיבה שבגללה המשתמש פתח את האפליקציה. זו הארכיטקטורה שמאחורי דפוס שנבנה כדי לסגור את הפער הזה: רלוונטיות שמתאימה לאדם הזה, כרגע.
מדוע התאמת טקסט בלבד אינה מספיקה
הדפוס חל בכל פעם שאותה שאילתה צריכה להחזיר תוצאות שונות עבור משתמשים שונים — כאשר "הטוב ביותר" תלוי בהקשר שהמשתמש מעולם לא הקליד, וכאשר הקטלוג שבבעלותכם קטן יותר מהקטלוג שהמשתמשים שלכם מצפים לו. חיפוש מתכונים מותאם אישית מתייחס לרלוונטיות כתערובת של שלושה סיגנלים במקום אחד:
| אות | מה הוא לוכד |
|---|---|
| טקסט | מה שהמשתמש הקליד — או, במצב סריקת ארוחה, מה זוהה על הצלחת שלו |
| התאמה למטרה | עד כמה מתכון מתאים לקלוריות שנותרו למשתמש עבור ארוחה זו, היום |
| הרגל | באיזו תדירות משתמש ספציפי זה באמת אוכל את המתכון הזה |
מנוע תמים רואה רק את השורה הראשונה בטבלה זו. דפוס זה מאחד את שלושתם לרשימה מדורגת אחת, ואז ממלא תוכן ממקור חיצוני בכל פעם שהקטלוג המקומי הופך לדל — ומרחיב בשקט את הקטלוג בכל פעם שהוא עושה זאת. התוצאה מרגישה פחות כמו שאילתה למסד נתונים ויותר כמו חבר שכבר מכיר את המטבח והתזונה שלך.
כיצד מורכב ה-Pipeline
המערכת פועלת כ-retrieval pipeline עם שכבת התאמה אישית מקדימה וקטלוג מתרפא עצמו מאחוריה.
שני מנועי חיפוש, חוזה אחד. Elasticsearch הוא העיקרי: סובלנות שגיאות הקלדה פאזית, גזירת מילים באנגלית ("grill" מוצא "grilled"), ניקוד רלוונטיות משוקלל. אם הוא אי פעם לא זמין, אותה שאילתה עוברת לחיפוש MongoDB המכבד את אותם מסננים וכללי התאמה אישית. החיפוש נפגע תחת תקלה; הוא לעולם אינו נעלם.
השכבה שמשתמשים מרגישים אך לעולם אינם רואים. לפני שהתוצאות מוחזרות, שלושה דברים מעצבים מחדש את הרשימה. תקציב הקלוריות מחושב מהיעד היומי בניכוי מה שתועד, ומעלה מתכונים שמתאימים לכל חלון שנותר — ואם המשתמש עבר את היעד שלו, הרשימה מסודרת מחדש בשקט מהקלוריות הנמוכות ביותר קודם, ודוחפת להתאוששות במקום להתמכרות. היסטוריית אכילה מזקקת את 60 הימים האחרונים של יומני ארוחות למפת "מה שאתה באמת אוכל", ושומרת מועדפים במרחק נגיעה אחת בעמוד הראשון. העדפות תזונה — veg, vegan, non-veg, בתוספת שני תריסרים של תגים עדינים יותר כמו keto, gluten-free, ו-high-protein — מסננים ומדרגים מחדש את כל מה שמתחת.
קטלוג שגדל מעצמו. כאשר התוצאות המקומיות הופכות לדלות, ה-pipeline דולה מידע ממקור חיצוני, FatSecret, ותופר את התוצאות הללו לאותו פיד גלילה אינסופי ללא תפרים גלויים. לאחר מכן הוא כותב את המתכונים החיצוניים הללו חזרה לקטלוג המקומי, תוך שימוש ב-LLM כדי לסווג כל אחד כ-veg, non-veg, או vegan, כך שיהיה ניתן להתאים אותו אישית כהלכה בפעם הבאה — אותו אינסטינקט מאחורי ארכיטקטורת אחזור היברידית שיישמנו במקומות אחרים, כאשר תוצאות מקומיות וחיצוניות צריכות להרגיש כמערכת אחת.
שלושה מטמונים, שלוש משימות. ערכות תוצאות משולבות, מפת הרגלי המשתמש, ותגובות API חיצוניות נושאות כל אחת אורך חיים עצמאי משלהן, כך ששאילתה מותאמת אישית מרובת מקורות עדיין מוחזרת בערך במהירות של חיפוש פשוט יחיד.
הוויתורים שראוי לציין
שום דבר מזה לא הגיע בחינם, ולהיות כנה לגבי העלות הוא חלק ממה שהופך את הדפוס לאמין ולא רק חכם.
הרלוונטיות הפכה במכוון ללא ניטרלית. דירוג רלוונטיות טקסטואלי טהור הוא "הוגן" באופן שאינו מועיל כאן. התוצאות מוטות בכוונה להתאמה למטרה ולהרגל, מכיוון שמתכון פחות מושלם מבחינה טקסטואלית שמתאים לתקציב ולטעם של מישהו הוא התשובה הטובה יותר. "מדוע זה דורג ראשון?" הופכת להחלטת מוצר, לא לברירת מחדל של מנוע חיפוש — ולכן משקלים אלו חיים בקוד היישום, ולא בקובץ תצורה שאינו בבעלות.
הבעלות על הקטלוג כולו לא הייתה המטרה — הבעלות על החלק הנכון ממנו הייתה. הקיצוניות שנשקלו היו קטלוג בניהול מלא (איכות גבוהה, רוחב צר) או פרוקסי מלא ל-API חיצוני (רוחב בלתי מוגבל, אפס שליטה, אפס התאמה אישית). המערכת מפצלת את ההבדל: מובילה עם מתכונים בבעלות ובכתיבת משתמשים, ממלאת תוכן חיצוני כשצריך, וסופגת את מה שממולא — ובכך נוטה, לאורך זמן, לבעלות על בדיוק מה שמשתמשים באמת מחפשים.
עמידות נבחרה על פני יעילות שיא. שני מנועי חיפוש מלאים עולים יותר לתפעול מאחד — אותה חשיבה של עמידות קודם כל מאחורי רוב החלטות ה-backend שלנו. עלות זו התקבלה במכוון: חיפוש מתכונים הוא פונקציונליות ליבה בשימוש יומיומי, שבה חיפוש פגום הוא מטרד קל אך חיפוש חסר הוא אפליקציה שבורה.
התאמה אישית עולה בחיזוי. התוצאות שונות באמת לפי שעת היום, קלוריות שנצרכו, והיסטוריה — אפילו עבור אותו משתמש בארוחת בוקר לעומת ארוחת ערב. זה מכוון, אבל זה מעלה את הרף לבדיקות ותמיכה: "זה עובד בטלפון שלי" מפסיק להיות משמעותי כאשר נכונות מוגדרת לכל משתמש, לכל רגע.
מתי דפוס זה מתאים — ומתי לא
חשוב להיות ברורים לגבי מתי אין להשתמש בדפוס זה. הוא מצדיק את מורכבותו כאשר התוצאות צריכות באמת להיות שונות לכל משתמש בהתבסס על הקשר שהם מעולם לא הקלידו, כאשר הקטלוג שבבעלות קטן ממה שמשתמשים מצפים לו אך ניתן להעשיר אותו חיצונית, כאשר התכונה תדירה מספיק כך ש-graceful degradation חשוב יותר מתשתית מינימלית, וכאשר "רלוונטיות" כוללת באופן לגיטימי סיגנלים מעבר לטקסט.
זהו הכלי הלא נכון כאשר התוצאות זהות עבור כל משתמש — חיפוש docs ציבורי, חיפוש SKU — היכן שהתאמה אישית מוסיפה עלות ללא תמורה. זה מיותר כאשר הקטלוג קטן ויציב מספיק שמנוע אחד עם סינון פשוט מספיק, והוא פועל נגדך בכל מקום שבו דירוג קפדני, זהה וניתן להסבר מלא הוא דרישה חובה.
המשימה האמיתית
איכות האחזור כאן מטופלת כבעיית התאמה אישית, לא רק כבעיית התאמה. חיפוש מתכונים שמושלם מבחינה טקסטואלית תוך התעלמות מכך שהמשתמש טבעוני, כבר חרג מהתקציב, ובישל את אותן חמש ארוחות ערב כל החודש, הצליח טכנית אך נכשל למעשה. המשימה מעולם לא הייתה למצוא מתכונים התואמים לשאילתה — היא להעלות את המתכון שאדם זה צריך לבשל הבא. כל שכבה בארכיטקטורה זו, ממנועי החיפוש הכפולים ועד למיון מחדש השקט כאשר מישהו חורג מהתקציב, קיימת כדי לסגור את הפער הזה.
אם אתם שוקלים החלטת "בנה מול שלב" דומה עבור חיפוש או אחזור במוצר שלכם, נשמח לדון בכך.
בלוגים נוספים
1. הרחבת פלטפורמת בריאות דיגיטלית עם Microservices

