Skip to content

‏$substrBytes חותך בייטים על תוכן עברי ומפיל את השאילתה — שני מוקדים, ובשניהם הכשל שקט #3353

Description

@amirbiron

מה קרה

הלוגים שנוספו ב-#3349 תפסו את זה בריצה הראשונה שלהם בפרודקשן:

pymongo.errors.OperationFailure: PlanExecutor error during aggregation :: caused by ::
$substrBytes: Invalid range, ending index is in the middle of a UTF-8 character.
code: 28657, codeName: Location28657
    at webapp/app.py, line 9515, in _safe_search

השורש: שתי יחידות מידה שהתערבבו

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

מה הורץ התוצאה היחידה
$regexFind ← idx 5 תווים
$indexOfCP 5 תווים
$indexOfBytes 9 בייטים
$strLenCP 10 תווים
$strLenBytes 14 בייטים

והקוד ב-webapp/app.py:9468‑9484:

'_match_idx': {'$ifNull': ['$_m.idx', 0]},                                # ← אינדקס תווים
'_snippet_start': {'$max': [0, {'$subtract': ['$_match_idx', 50]}]},
'snippet_preview': {'$substrBytes': ['$code', '$_snippet_start', 200]},   # ← מצפה לבייטים

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

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

מה הורץ מה מונגו החזירה
$substrBytes("שלום world", 0, 3) ending index is in the middle of a UTF-8 character ← זו שבפרודקשן
$substrBytes("שלום world", 1, 5) starting index is a UTF-8 continuation byte

החיסרון בפועל — מה זה עולה

מוקד 1 · החיפוש · webapp/app.py:9484

הפייפליין המהיר מת, ו-_safe_search נופל למסלול ה-$regex על code — סריקה מלאה בלי אינדקס. אם גם הוא נופל, יורדים לפייפליין הישן, וזה מחזיר [].

שלושה מחירים, לפי סדר חומרה:

  1. חיפוש שבור נראה בדיוק כמו חיפוש שלא מצא. אם גם הפולבאק נופל, המשתמש מקבל "לא נמצאו תוצאות" על חיפוש שמעולם לא רץ. אין שום סימן שמשהו נשבר — לא בממשק, ולא בהתנהגות.
  2. עלות מיותרת. ה-$regex שרץ במקום הוא סריקה מלאה. השאילתה הזו נמדדה כאיטית ביותר שנרשמה בלוח (3,730ms), והיא רצה רק מפני שהמסלול המהיר נפל.
  3. הסטטיסטיקה מזדהמת. הפרופיילר רושם את שאילתת הפולבאק כשאילתה איטית אמיתית, ומי שמסתכל בדשבורד מנתח שאילתה שלא הייתה אמורה לרוץ מלכתחילה. (זה מה ששלח את החקירה שלושה סבבים לכיוון הלא נכון.)

מוקד 2 · שיתוף קובץ · webapp/app.py:16166 — גרוע יותר

POST /api/share/<file_id> במצב preview:

'snippet_preview': {'$substrBytes': ['$code', 0, 2000]},
...
except Exception:
    meta = {}
if not meta:
    return jsonify({'ok': False, 'error': 'קובץ לא נמצא'}), 404

אותו באג, אבל כאן ה-except בולע אותו והתשובה למשתמש היא 404 "קובץ לא נמצא" — על קובץ שקיים. הודעת שגיאה שקרית, בלי שום עקבה בלוג. זה הדפוס של except שבולע, במקום השני באותו קובץ.

פגם שלישי מאותו שורש · ההדגשה בדפדפן

'_match_len': {'$strLenBytes': {'$ifNull': ['$_m.match', '']}},   # בייטים

הערך הזה נכנס ל-highlight_ranges, שמחושב במרחב תווים. global_search.js:315 (highlightSnippet) חותך את ה-snippet לפי המספרים האלה, ולכן התאמה עברית מודגשת בערך בכפול מאורך המילה. קוסמטי, אבל נראה לעין.


התיקון: להשתמש באופרטורים התו-מבוססים

בריפו יש שלושה מסלולים שמייצרים snippet, ושניים מהם כבר עובדים בתווים:

מסלול איך הוא חותך היחידה
search_engine.py:1141 content_value[start:end] תווים (חיתוך פייתון)
webapp/app.py:9655 code_text[start:end], m.start() תווים
webapp/app.py:9484 — המסלול המהיר $substrBytes בייטים ← החריג

אותו דבר בשיתוף: webapp/app.py:16211 ו-webapp/app.py:3566 עושים code[:2000] — תווים. כלומר הכוונה בקוד היא תווים בכל מקום, ורק שני שלבי אגרגציה סוטים ממנה. המעבר ל-$substrCP לא מוסיף התנהגות חדשה, הוא מיישר את מסלול ה-DB לכוונה שכבר כתובה בשאר הקוד. $substrCP ו-$strLenCP אינם בשימוש היום באף מקום בריפו.

נמדד ש-$substrCP מטפל נקי בכל מקרי הקצה שהפילו את $substrBytes:

מה הורץ התוצאה
$substrCP("שלום world", 0, 3) "שלו"
$substrCP("שלום world", 5, 200) — מעבר לסוף "world"
$substrCP("שלום", 99, 200) — התחלה מעבר לסוף ""

בלי חריגה, בשלושתם.

מה לא משתנה: file_size נשאר $strLenBytes בכל המקומות. גודל קובץ הוא באמת בייטים, וההערה בקוד אומרת זאת מפורשות (size-bytes ולא אורך תוכן). זה השימוש הנכון היחיד ב-$strLenBytes כאן.

וגם: ה-except בשיתוף צריך logger.warning(..., exc_info=True), כדי שהכשל הבא לא יתחפש ל-404. אותו טיפול שניתן ל-_safe_search ב-#3349.


מה עדיין לא ידוע

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

מה שכן שוחזר בעקביות: משפט ארוך בתיבת החיפוש מפיל. הסבר סביר — משפט ארוך אינו מתאים לכלום ב-$regexFind, ולכן _snippet_start נשאר 0 והחיתוך נופל על גבול 200 הבייטים; אבל לא אימתתי את ההסבר הזה, ולכן הוא השערה ולא ממצא.

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


היקף מוצע

  1. webapp/app.py:9484 — $substrBytes ← $substrCP
  2. webapp/app.py:9470 — _match_len ← $strLenCP, כדי ש-highlight_ranges יהיה במרחב יחידות אחד
  3. webapp/app.py:9468 — _has_code_match ← $strLenCP (שקול לוגית, אבל אורך בבייטים ליד אורך בתווים באותן שלוש שורות הוא בדיוק המלכודת שיצרה את הבאג)
  4. webapp/app.py:16166 — $substrBytes ← $substrCP, וה-except מקבל לוג
  5. טסטים שקודם נופלים על הקוד הישן, ואימות מול הקלאסטר על מסמכים עבריים אמיתיים

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions