הבלוג של ינון פרק

טיפים קצרים וחדשות למתכנתים

הסוד לצאת מהמינוס

22/07/2023

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

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

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

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

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

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

ואולי ChatGPT זה כמו מחשבון?

21/07/2023

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

אולי המתכנתת שנעזרת ב Chat GPT היא כמו המתמטיקאית שנעזרת במחשבון? אולי מתכנתים צריכים לדעת רק את ה"מה" ולתת למחשב להבין לבד את ה"איך"? ואולי כך יראה המקצוע שלנו בעתיד?

אולי.

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

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

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

מה זה בכלל send של Generator בפייתון?

20/07/2023

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

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

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

from collections.abc import Generator
from itertools import islice

def fib() -> Generator[int, None, None]:
    a, b = 1, 1
    while True:
        yield a
        a, b = b, a + b

for i in islice(fib(), 10):
    print(i)

לא צריך הרבה בשביל להבין שה int הראשון הוא הטיפוס של הדבר שיוצא מ yield, אבל מאיפה הגיעו שני ה None-ים שאחריו?

אז נכון יש Generators שצריכים את send ו throw ובטח יש סיבות טכניות טובות למה לא להשתמש ב Any כברירת מחדל שם. אבל אין ספק שהיה יותר קל להסביר גנרטורים לאנשים אם הייתי יכול קודם ללמד את yield עם ה Type Hint שלו ורק אחר כך ללמד את send ו throw עם הטיפוסים שלהן.

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

עשר, מאה, אלף

19/07/2023

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

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

נניח שהחלטתם שחשוב לכם להגיע למקום גבוה ברשימה כזאת ואתם מתחילים להגיש PR למאגר ציבורי כל יום. אחרי שנה שלחתם 365 תרומות קוד והגעתם למקום 165 ברשימה. זו עליה של 685 מקומות בשנה - קצב מאוד מורגש. בשנה השניה אתם ממשיכים לתקתק עבודה אבל הפעם עליתם רק למקום ה 85, עלייה של 80 מקומות בלבד. זה הרבה אבל ממש לא ה 685 מקומות שעליתם בשנה הראשונה. ובשנה השלישית באותו קצב צפויה לכם עלייה של 30 מקומות בסך הכל, למקום ה 55.

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

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

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

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

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

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

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

הערך האמיתי של Redux Selectors

18/07/2023

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

אבל הערך האמיתי של Selectors מתגלה כשאנחנו צריכים לארגן מחדש את ה Store.

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

הסיבה היא שבקוד הקומפוננטה יש התיחסות ישירה לשם השדה ולמקום שלו בתוך אוביקט ה Store:

function TodoCount() {
  const todos = useSelector(state => state.todos)
  return <p>{todos.length} things to do</p>
}

מבנה טוב יותר ייקח את כל הקריאות האלה ל useSelector לקובץ אחד, קובץ הסלקטורים. שם יהיה לנו למשל:

export const getTodos = state => state.todos;

ובתוך הקומפוננטה נכתוב רק:

function TodoCount() {
  const todos = useSelector(getTodos)
  return <p>{todos.length} things to do</p>
}

אפילו בלי Reselect ובלי ה Memoization הרווחנו המון - אם נקיד לכתוב את כל הסלקטורים במקום אחד, שינוי במבנה הסטייט או שינויי שמות יהיו הרבה יותר קלים למימוש ולא ידרשו שינוי בקומפוננטות.

היה שלום אפולו

17/07/2023

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

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

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

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

היה שלום אפולו ותודה על כל הלינקים.

תבנית לכתיבת בדיקה לבוט ב python-telegram-bot

16/07/2023

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

from telegram import Message, MessageEntity, User, Chat, Update
import pytest
from unittest.mock import MagicMock
from datetime import datetime
import client.telegram.bot as bot

class AsyncMock(MagicMock):
    async def __call__(self, *args, **kwargs):
        return super(AsyncMock, self).__call__(*args, **kwargs)



@pytest.mark.asyncio
async def test_create_user(monkeypatch):
    with monkeypatch.context() as m:
        app = bot.create_application()
        await app.initialize()

        message = Message(
            message_id=0,
            date=datetime.utcnow(),
            chat=Chat(3, "private"),
            from_user=User(first_name='Misses Test', id=123, is_bot=False),
            text="/start",
            entities=[MessageEntity(type=MessageEntity.BOT_COMMAND,
                                    offset=0, length=len('/start'))]
        )
        message.set_bot(app.bot)

        send_message = AsyncMock()
        m.setattr(app.bot, "_send_message", send_message)

        await app.process_update(Update(update_id=0, message=message))
        assert 'Hello World' in send_message.call_args[0][1]['text']

והקוד עבור הבוט המתאים, מתוך קובץ שבדוגמה שלנו נקרא client/telegram/bot.py יהיה:

async def start_conversation(update: Update, context: ContextTypes.DEFAULT_TYPE):
    await update.message.reply_text(f"Hello World")


def create_application():
    # Create the Application and pass it your bot's token.
    token = os.environ.get("TELEGRAM_BOT_TOKEN")
    application = Application.builder().token(token).build()

    application.add_handler(CommandHandler('start', start_conversation))
    return application


def main() -> None:
    """Start the bot."""
    application = create_application()
    # Run the bot until the user presses Ctrl-C
    application.run_polling()


if __name__ == "__main__":
    print("Starting...")
    main()

בואו נראה מה היה לנו:

  1. קוד הבדיקה הוא פונקציה אסינכרונית. בשביל שזה יעבוד יש להתקין את הפלאגין pytest-asyncio.

  2. הבדיקה צריכה לזייף הודעת /start ואז צריכה לוודא שהבוט ענה בהודעת Hello World. בשביל זה אנחנו ממש יוצרים את הבוט (תחילת קוד הבדיקה) ומפעילים את app.initialize שזו פונקציה של python-telegram-bot שדואגת לכל מיני איתחולים. בחיבור רגיל לטלגרם קריאה ל run_polling מפעילה את ה initialize אוטומטית.

  3. הפונקציה process_update היא נקודת הכניסה לבוט והיא מקבלת אוביקט הודעה ומנתבת אותו לפונקציית הטיפול המתאימה לפי הכללים שהגדיר הבוט (אלה פקודות ה add_handler שרואים בקוד הבוט). בקוד הבדיקה משתמשים בה כדי לשלוח את הודעת ה /start.

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

  5. הפקודה set_bot היא שמאפשרת בתוך קוד הבוט לגשת ל update.message.reply_text, ולכל שאר הפונקציות של הבוט דרך אוביקט ההודעה.

  6. בשביל לקבל בחזרה לבדיקה את הפרמטרים שנשלחו לפונקציות שליחת ההודעה (אלה שהיו אמורים להישלח לטלגרם בהרצה אמיתית של הבוט), אני יוצר MagicMock ושומר אותו בשדה _send_message של אוביקט הבוט. הפונקציה _send_message נועדה לשימוש פנימי ולכן אפשר לדרוס אותה. ניסיון לדרוס פונקציות ציבוריות של הבוט ייכשל בגלל הקוד הבא בתוך ספריית python-telegram-bot באוביקט הבוט:

def __setattr__(self, key: str, value: object) -> None:
    """Overrides :meth:`object.__setattr__` to prevent the overriding of attributes.

    Raises:
        :exc:`AttributeError`
    """
    # protected attributes can always be set for convenient internal use
    if key[0] == "_" or not getattr(self, "_frozen", True):
        super().__setattr__(key, value)
        return

    raise AttributeError(
        f"Attribute `{key}` of class `{self.__class__.__name__}` can't be set!"
    )
  1. אחרי דריסת _send_message נשאר רק לשלוח את ההודעה ל process_update ולוודא את ההודעה שהבוט שולח דרך בדיקה של הקריאות שבוצעו ל Mock בשורה האחרונה של הבדיקה.

ללא מגע יד

15/07/2023

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

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

  1. מערכת שרצה על שרת שלי עדיפה על מערכת בענן.

  2. מערכת שרצה בענן עדיפה על מערכת שתרוץ בדפדפן אצל משתמשים.

  3. קוד שאני כתבתי עדיף על שימוש בספריה קיימת.

  4. ספריה קיימת עדיפה על פנייה ל API ציבורי צד-שלישי.

  5. מיזלוג (fork) ספריה קיימת עדיף על כתיבת קוד שמשתמש במנגנונים לא מתועדים שלה.

  6. ממשק מבוסס טקסט עדיף על ממשק גרפי.

  7. כלי קוד פתוח עדיפים על כלים קנייניים.

  8. שמירת מידע בקבצי טקסט עדיפה על שמירתו בבסיס נתונים.

  9. שימוש במידע מקומי עדיף על שימוש במידע מהרשת.

גיוון הוא כבר לא יעד

14/07/2023

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

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

  1. התחרות קשה במיוחד עבור ג'וניורים או עבור עובדים מרוחקים.

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

  3. היו פתוחים להזדמנויות גם בחו"ל. לפעמים יותר קל לשפר את האנגלית מאשר לעבור מגייסים שפוחדים לטעות.

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

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

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

טיפ neo4j: איחוד צמתים

13/07/2023

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

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

במצב כזה אפשר להריץ את השאילתה הבאה כדי לאחד צמתים שונים המכילים טקסט של אותה מילה:

MATCH (w:Word)-[:IN_LANGUAGE]-(l:Language)

CALL {
    WITH w
    MATCH (m:Word)-[:IN_LANGUAGE]-(l)
    WHERE m <> w AND m.text = w.text
    RETURN w AS innerWord
}

WITH innerWord.text as text, collect(innerWord) AS nodes
CALL apoc.refactor.mergeNodes(nodes) YIELD node
RETURN node;

בואו נקרא ונתרגם:

  1. שורה ראשונה מחפשת מילים. כל פעם שהיא מוצאת מילה בבסיס הנתונים היא שומרת את הצומת במשתנה בשם w, ואת השפה שבה המילה כתובה במשתנה בשם l.

  2. בתוך בלוק ה CALL מופעלת תת-שאילתה. תת השאילתה לוקחת את w מבחוץ ומחפשת צמתים אחרים של מילים שאינם w אבל הן באותה שפה ויש להן את אותו טקסט.

  3. מחוץ לבלוק ה CALL הפקודה WITH מייצרת אגרגציה ומסדרת את השורות שחוזרות מהשאילתה - בכל שורה יהיה הטקסט של המילה ורשימת כל צמתי המילים שיש להם את הטקסט הזה.

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

סך הכל Cypher (שזו שפת השאילתות של neo4j) יותר פשוט לכתיבה וקריאה מ SQL, וכולל המון פונקציות מובנות כמו collect ו mergeNodes שעוזרות בכתיבת שאילתות.

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