פורסם בתאריך ב-טכנולוגיה

מודל הנתונים שעומד מאחורי הגמישות של Notion

על ידי Jake Teton-Landis

הנדסה

12 דק' קריאה

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

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

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

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

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

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

היסודות של בלוקים

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

כל בלוק כולל את המאפיינים הבאים:

  • מזהה – כל בלוק ניתן לזיהוי ייחודי על ידי המזהה שלו. אפשר לראות את המזהה של בלוקי דפים בסוף כתובת ה-URL בדפדפן. המזהים ב-Notion הם UUIDs שנוצרים באקראי (UUID v4).

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

  • סוג – לכל בלוק יש סוג, שמגדיר כיצד בלוק מוצג ואיך לפרש את המאפיינים של הבלוק. Notion תומכת בהרבה סוגי בלוקים, שאת רובם אפשר לראות בתפריט 'בלוק חדש' שמופיע כשלוחצים על הלחצן + או בתפריט /:

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

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

  • הורה – מזהה הבלוק של ההורה של הבלוק. הבלוק ההורה משמש רק להרשאות.

איך בלוקים משתלבים יחד

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

סוג ומאפיינים

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

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

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

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

תוכן ועץ ההצגה

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

בדוגמה של רשימת המשימות לביצוע, יש לנו בלוק רשימת משימות לביצוע ("Write a blog post about blocks") עם שלושה מזהי בלוקים במערך התוכן. אנחנו חושבים על המזהים האלה כעל 'מצביעים כלפי מטה', וקוראים לבלוקים שהם מפנים אליהם 'תוכן' או 'צאצאים להצגה'.

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

הנה כמה דוגמאות לאופן שבו מאפיין התוכן מוצג על ידי סוגי בלוקים שונים:

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

  • רשימות נפתחות – בלוקי רשימה נפתחת מציגים תוכן רק כאשר הם מורחבים. אחרת, הם מציגים רק את מאפיין הכותרת.

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

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

עריכת עץ ההצגה

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

למשל, בלחיצה על הזחה בבלוק תוכן, המערכת תנסה להוסיף את הבלוק לתוכן של בלוק האח הקרוב ביותר בעץ התוכן.

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

הרשאות

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

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

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

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

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

חיי בלוק

חיי בלוק מתחילים בצד הלקוח.

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

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

יצירה ועדכון של בלוקים

אתם לוחצים על מקש Enter – זה יוצר בלוק חדש של משימות לביצוע.

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

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

לאחר מכן, הלקוח מחיל את הפעולות בטרנזקציה על המצב המקומי שלו. אובייקטים חדשים של בלוקים נוצרים בתוך הזיכרון, ובלוקים קיימים משתנים. באפליקציות המקוריות שלנו, אנו שומרים את כל הרשומות שאתם ניגשים אליהן מקומית במטמון LRU (שהכי פחות נפוץ בשימוש לאחרונה) על גבי SQLite או IndexedDB שנקרא RecordCache. כאשר אתם משנים רשומות באפליקציה מקורית, אנו מעדכנים גם את העותקים המקומיים ב-RecordCache. העורך מבצע עיבוד והצגה מחדש כדי לצייר על המסך שלכם את הבלוק החדש שנוצר. כל זה קורה בתוך כמה אלפיות שנייה מהלחיצה שלכם.

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

שמירת שינויים בשרת

ככה הבלוק שלכם נשמר בבטחה בשרת, כך שהחבר שלכם יכול לראות אותו.

בדרך כלל, TransactionQueue עומד ריק, ולכן הטרנזקציה של יצירת הבלוק נשלחת מיד לשרת של Notion בבקשת API. נתוני הטרנזקציה עוברים סריאליזציה ל-JSON ונרשמים בנקודת הקצה /saveTransactions של ה-API.

התפקיד העיקרי של SaveTransaction הוא להעביר את הנתונים שלכם למסדי הנתונים המהימנים שלנו, שמאחסנים את כל נתוני הבלוקים, כמו גם את כל סוגי הרשומות האחרים השמורים ב-Notion.

ברגע שהבקשה מגיעה לשרת ה-API של Notion:

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

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

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

  4. בשלב זה, יש תגובת HTTP של 'הצלחה' לבקשת ה-API המקורית שנשלחה על ידי הלקוח. זה מאשר שהלקוח יודע שהטרנזקציה נשמרה בהצלחה ושהוא יכול להמשיך לשמירת הטרנזקציה הבאה ב-TransactionQueue.

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

בחלק הבא נסביר איך הנתונים מגיעים למסך של החבר שלכם.

עדכונים בזמן אמת

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

לכל לקוח יש חיבור WebSocket ארוך טווח ל-MessageStore, שירות העדכונים בזמן אמת של Notion. כאשר הלקוח של Notion מציג בלוק (או דף, או כל סוג אחר של רשומה), הלקוח נרשם (subscribes) לשינויים של אותה רשומה מ-MessageStore באמצעות חיבור ה-WebSocket הזה. כאשר החבר שלכם פותח את אותו דף שאתם פותחים, הוא נרשם (subscribes) לשינויים של כל אותם בלוקים.

לאחר שהשינויים שלכם עברו את תהליך ה-saveTransactions, ה-API הודיע ל-MessageStore על גרסאות חדשות שתועדו. ה-MessageStore מוצא חיבורי לקוח שרשומים (subscribed) לרשומות המשתנות הללו, ומעביר להם את הגרסה החדשה דרך חיבור ה-WebSocket שלהם.

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

קריאת בלוקים

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

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

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

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

בניית בלוקים להמשך

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

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

אנחנו מחפשים אנשים שיעזרו לנו לבנות את העתיד של Notion. אולי אתם האנשים האלה?

שיתוף פוסט זה


כדאי לנסות

לצאת לדרך בדפדפן או בגרסה למחשב

יש לנו גם אפליקציות ל-Mac ו-Windows.

יש לנו גם אפליקציות ל-iOS ול-Android.

אפליקציית אינטרנט

אפליקציה למחשב

משתמשים ב-Notion בעבודה? לבקשת הדגמה