CobbleDB: בנייה מחדש של אחסון חיפוש AI להשהייה ועלות נמוכות יותר

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

מחבריםPerplexity Engineering

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

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

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

בעזרת עזרה מנחיל חדש של סוכני תכנות פנימיים, בנינו מחדש שכבה זו סביב CobbleDB, חנות חמה של מפתח-ערך, הנתמכת על ידי Pillar למצב מסמך עמיד ופרסום, ועל ידי Lorry עבור עדכונים באצוות. העיצוב מחדש מפריד בין עיבוד מסמכים לבין קליטה לחנות החמה ומאפשר לנו לכוונן את השירות לעומס העבודה שלנו. מדידות לפני ואחרי בייצור מראות השהיית קריאת אצווה נמוכה פי 5 בקירוב, ומודל העלויות הפנימי שלנו מעריך חיסכון של לפחות 20% ביחס ל-DynamoDB.

תשתית אחסון לחיפוש מבוסס-בינה מלאכותית

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

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

שני תהליכי עבודה נפרדים

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

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

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

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

היכן שמסד נתונים מנוהל נופל קצר

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

ראשית, התמחור המבוסס על שימוש גובה עבור כל בית שנקרא או נכתב. בקשת Search API אחת מכילה 100–120 מפתחות המתאימים לדפים שעלינו להביא. תהליך שליפת הנתונים מחלק בקשה לאצוות קטנות יותר של 10–20 מפתחות דפים, עם גודל פריט ממוצע של כ־50 KB. קריאה חוזרת ונשנית של רשומות אלה צרכה קיבולת משמעותית, בעוד שסריקות רציפות ושינויים בחלוקת המקטעים או במודלי ההטמעה יצרו יותר כתיבות. הכלכלה גם הייתה תלויה בגודל ובתדירות של הרשומות, ולא רק במספר קריאות ה-API.

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

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

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

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

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

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

CobbleDB: נתיב הקריאה הניתן לכוונון

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

השינוי החשוב הוא השליטה שלנו בתצורת האחסון.

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

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

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

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

עיצוב זה תומך בדפוסי הקריאה שעומס העבודה שלנו דורש. לאחר ששאילתת עוברת שליפה ודירוג כדי לקבל קבוצת מועמדים של עמודים רלוונטיים, החנות החמה נשאלת בכמות גדולה עבור התכנים של אותם עמודים. בקשות קריאה אלו מגיעות לנתב (router) ומנותבות לצומת נתונים תואם. הנתב נותן עדיפות לשליחת בקשות לצומת באותו אזור זמינות כדי למזער השהייה בין אזורים, אם אפשר. כל צומת נתונים יכול לשלוף את תוכן העמוד באמצעות MultiGet, ממשק API לקריאת אצווה של RocksDB שנועד לשלוף מספר מפתחות בו-זמנית בהשהייה נמוכה. אם צומת מגיב לאט, המערכת יכולה גם לשאול עותק משובט אחר של מחיצה בצומת אחר, ובכך לשפר את השהיית הזנב.

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

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

Pillar ו-Lorry: נתיב הכתיבה העמיד

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

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

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

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

CobbleDB מצפה שעדכונים יאוגדו באצוות בהתאם למחיצה. ייצוא Pillar מומר לפורמט זה על ידי שירות חסר מצב (stateless) שאנו קוראים לו Lorry. התפקיד היחיד שלו הוא לקרוא רשומות ייצוא מתור עמיד המותאם למחיצות, לצבור אותן לקובצי אצווה לפי מחיצה, ולמסור אותן ל-CobbleDB.

בין Lorry ל-Cobble, פרוטוקול נוסף מפריד בין מישור הנתונים (S3) למישור הבקרה. Lorry כותבת את האצווה ל-S3 תחת מזהה ייחודי ורושמת את האצווה בתוך Cobble. עותקי מחיצה של Cobble שואלים באופן עצמאי את האצווה או את ה-API של מישור הבקרה עבור אצווה S3 הבאה לקליטה ומחילים באופן אסינכרוני עדכוני אצוות בסדר כרונולוגי. המשמעות היא שצומת איטי או מתאושש יכול להדביק את הפער בקצב שלו, במקום להאט את שאר המערכת.

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

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

אימות

מהיר פי 5

לאחר העברת החנות החמה (hot store), חציוון (median) השהיית קריאת האצווה (batch-read latency) ירד מ-31.4 מילישניות ל-5.60 מילישניות. ה-p90 ירד מ-56.7 מילישניות ל-9.77 מילישניות, וה-p99 ירד מ-123 מילישניות ל-24.2 מילישניות. היחסים נעים בין 5.08× ל-5.80×, עם שיפורים הן בבקשות הטיפוסיות והן בקצוות (tail).

חביון קריאת אצווה בייצור לפני ואחרי המיגרציה: P50 ירד מ-31.4 ל-5.60 ms,‏ P90 מ-56.7 ל-9.77 ms, ו-P99 מ-123 ל-24.2 ms. אלה הם חביוני קריאה מהחנות החמה מתעבורה חיה בזמנים שונים, לא בקשות תואמות ולא חביון תשובה מקצה לקצה.

מספרים אלו נגזרים מקריאות באצוות, שבהן כל שאילתת מבקשת בערך 10-15 מפתחות. גודל פריט המטען הממוצע הוא 50KB. שתי המערכות נשאלו בכ-200 אלף בקשות בשנייה (rps), אם כי הרצנו גם בדיקות עומס עד 500 אלף rps ללא פגיעה בביצועים.

כדי לבודד את תרומתה של CobbleDB, כל שאר ההיבטים של ההגדרה נשמרו קבועים. עם זאת, זו נותרת השוואת תצפית לפני ואחרי; המערכות הגישו תעבורת ייצור חיה בזמנים שונים ולא את אותן בקשות תחת תנאים מבוקרים. לכן מדדנו גם את הביצועים של שתי המערכות בבנצ'מארק סינתטי. כדי לקרב את הבקשות הנראות בייצור, תשלנו אל שתיהן אצוות של 10-15 מפתחות וערכים הנעים בין 100B ל-100 KiB.

חביון קריאת אצווה סינתטי עבור אצוות של 10 ו-15 מפתחות על פני גדלי ערכים מ-100 B עד 100 KiB. כל נקודה היא הממוצע של חביון P50,‏ P90 או P99 ברמת הרצה משתי הרצות לכל backend ולכל תצורה.

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

זול ב-20%

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

חישבנו את החיסכון בעלויות על סמך הערכות פנימיות של גודל האחסון, יחידות קיבולת כתיבה ויחידות קיבולת קריאה. בכל רמת התחייבות, CobbleDB זולה לפחות ב-20% מ-DynamoDB. החיסכון האמיתי כנראה אפילו גבוה יותר, מכיוון שתוצאות אלו אינן לוקחות בחשבון את חיסכון הגיבוי הסביר כתוצאה מכיווץ.

כיצד נחיל של סוכני בינה מלאכותית עוזר לנו לבנות זאת

הבנייה של CobbleDB והתשתית שמסביב רחוקה מלהיות טריוויאלית. מסד הנתונים הבסיסי של CobbleDB לבדו כולל כ-40,000 שורות קוד ב-Rust, וההגירה דרשה גם עבודה נרחבת כדי לאמת את הנכונות והתנהגות הכשל בקנה מידה של ייצור, לשלב את CobbleDB עם נתיבי העיבוד והשירות כאחד, ולהעביר תעבורה חיה בבטחה.

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

הקצנו קבוצה של סוכנים פרו-אקטיביים ופעילים תמיד לפרויקט CobbleDB. המשימה הראשונה שלהם הייתה לבנות ממודל מדויק של העבודה שכבר הייתה בעיצומה. הם ביצעו ביקורת על ערוץ הפרויקט ועל השרשורים הפעילים, חיברו פריטים יוצאי דופן לבקשות המשיכה (pull requests) שלהם ולבעליהם, והפיקו תמונת מצב עדכנית של בריאות המערכת ושל הפעולות הבאות. משם, הם פעלו בכמה חלקים של לולאת ההנדסה:

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

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

סיכום

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

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

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

ציטוט

BibTeX
@online{perplexity2026Cobbledb,
  author = {{Perplexity} and Alexandr Yarats and Marco Montagna and Aleksei Bykov and Alexey Gusev and Ali Abdullaev and Amir Mokhammed-Ali and Andrey Zosimov and Denis Kremko and Gleb Vishnevsky and Micah Zirn and Phi Hoang and Claire Beamer and Jeremy Yang and Jerry Ma and Denis Yarats},
  title = {{CobbleDB: Rebuilding AI search storage for lower latency and cost}},
  date = {2026-09-14},
  url = {https://www.perplexity.ai/hub/blog/cobbledb}
}