HebMorph–חיפוש מורפולוגי עברי. סיקור ושימוש ב-Production

22 בספטמבר 2011

תגובה אחת

ביום שלישי האחרון השתתפתי בערב מעניין (ישר כח למארגנים) שהיה במרכז המחקר והפיתוח של Microsoft בהרצליה.
אחת ההרצאות שם הייתה על כלי מעניין בשם HebMorph, שהוא תוסף ל-Lucene/Lucene.net עבור חיפושים מורפולוגיים בעברית.
לאחר ההרצאה שוחחתי קצרות עם איתמר, המרצה והמפתח, וסיפרתי לו שהכלי שלו נותן תוצאות מצויינות אבל הביצועים שלו בעייתיים.

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

למחרת בבוקר התיישבתי במשרד והחלטתי לחקור את הנושא לעומקו.

בדיקה 1: שימוש בכלים המובנים של Lucene
השתמשתי במחשב לא מזהיר במיוחד. מכונה עם מעבד 4 ליבות, 4G זיכרון, דיסק קשיח SATA בסיסי.
המערכת מאפשרת לאנדקס בערך כ-1000 מסמכים בשניה (מאגר החדשות של ערוץ 7)

בדיקה 2: שימוש ב-HebMorph, שאר הקוד ללא שינוי.
100-150 מסמכים בשניה.

בדיקה 3: שימוש ב-SimpleAnalyzer, גם כן מהחבילה של איתמר.
950~ מסמכים בשניה

מעקב אחר מנהל המשימות מראה שהכלי הקטן שפיתחתי, הן על בסיס  Lucene והן על בסיס HebMorph/SimpleAnalyzer סובל מ-2 בעיות ביצועים מרכזיות:
– ניצול של מעבד אחד בלבד (מתוך 4 ליבות)
– אי ניצול יעיל של זיכרון

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

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

קצת חפירה ברשת וקצת השקעה בקוד, ופיתחתי בסוף את הפיתרון הבא:

Dim oMem1 As Indexer = New HebrewIndexer(100)

Dim oMem2 As Indexer = New HebrewIndexer(100)

Dim oMem3 As Indexer = New HebrewIndexer(100)

Dim count As Integer = a.Length / 3
' array indexer start, end
Dim t1 As New Tasks.Task(Sub() _IndexDocuments(a, 0, oMem1, 0, count - 1))
Dim t2 As New Tasks.Task(Sub() _IndexDocuments(a, 1, oMem2, count, count * 2 - 1))
Dim t3 As New Tasks.Task(Sub() _IndexDocuments(a, 2, oMem3, count * 2, a.Length - 1))

t1.Start() : t2.Start() : t3.Start()

Tasks.Task.WaitAll(New Tasks.Task() {t1, t2, t3})

Dim t4 As New Tasks.Task(Sub()

             oMem1.Flush() : oMem2.Flush() : oMem3.Flush()

             Indexer.AddIndex(oMem1.Directory)
             Indexer.AddIndex(oMem2.Directory)
             Indexer.AddIndex(oMem3.Directory)

             oMem1.Directory.Dispose() : oMem2.Directory.Dispose() : oMem3.Directory.Dispose()
GC.Collect() ' אני לא חד-משמעי בקשר לצורך בפונקציה הזו
         End Sub)

t4.Start()

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

הפונקציה AddIndex היא בעצם הפנייה לפונקציה AddIndexesNoOptimize' של IndexWriter

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

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

 

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

בהצלחה!

הוסף תגובה
facebook linkedin twitter email

כתיבת תגובה

האימייל לא יוצג באתר. שדות החובה מסומנים *

תגובה אחת

  1. רון קליין24 באוקטובר 2011 ב 16:27

    פוסט מעניין ומסקרן, אשמח לקרוא על התפתחויות!

    הגב