חיפוש גלובלי — סוגי החיפוש
איפה זה בממשק
תיבת החיפוש הגלובלי יושבת בראש עמוד הקבצים (/files). ליד התיבה יש רשימה נפתחת (#searchType ב-webapp/templates/files.html) שבה בוחרים את סוג החיפוש, וברירת המחדל היא תוכן.
הדפדפן (performGlobalSearch ב-webapp/static/js/global_search.js) שולח את החיפוש ל-POST /api/search/global (api_search_global). שני סוגים נשלחים ל-endpoint נפרד: חיפוש חכם (חיפוש חכם) ותיעוד (תיעוד).
סוגי החיפוש
תווית בממשק |
ערך |
על מה מחפש |
איך מתאים |
|---|---|---|---|
תוכן |
|
תוכן הקוד בלבד |
מחרוזת רצופה, כמו שהוקלדה |
חיפוש חכם |
|
קטעי קוד לפי משמעות |
חיפוש היברידי: וקטורים + טקסט |
טקסט |
|
מילים מתוך תוכן הקוד |
כל מילה בנפרד, גם כחלק ממילה ארוכה יותר |
Regex |
|
תוכן הקוד בלבד |
ביטוי רגולרי |
מטושטש |
|
שם הקובץ, תוכן הקוד והתגיות |
דמיון משוער, גם עם שגיאות הקלדה |
פונקציות |
|
שמות פונקציות שחולצו מהקוד |
השאילתה מופיעה בתוך שם הפונקציה |
תיעוד |
|
סעיפים באתר התיעוד (לאדמין בלבד) |
לפי משמעות: חיפוש וקטורי באינדקס התיעוד |
”תוכן“, ”טקסט“, ”Regex“, ”מטושטש“ ו“פונקציות“ לא מבחינים בין אותיות גדולות לקטנות.
חשוב
”תוכן“, ”טקסט“, ”Regex“ ו“פונקציות“ לא מחפשים בשם הקובץ — הם מסתכלים על הקוד בלבד. שם הקובץ נכנס רק ב“מטושטש“, ובחיפוש החכם (כחלק מהטקסט שממנו נבנה הווקטור של כל קטע). מי שמחפש קובץ לפי השם שלו צריך לבחור ”מטושטש“, או להשתמש בחיפוש לפי שם שבאותו עמוד (לא לבלבל עם). יש חריג אחד: כשהסוג שבחרתם לא מוצא כלום, החיפוש עובר לגיבוי שכן מסתכל גם על שמות — ראו כשהסוג שבחרתם לא מוצא כלום.
תוכן
המימוש הוא _content_search ב-search_engine.py. הוא עובר על כל הקבצים של המשתמש במנות (SEARCH_PAGE_SIZE), ובכל קובץ סופר כמה פעמים השאילתה מופיעה כמחרוזת רצופה בתוך הקוד. אין פירוק למילים: get user נמצא רק איפה שכתוב בדיוק get user, עם הרווח.
הדירוג לפי צפיפות: כמה הופעות יש ביחס לאורך הקובץ. קובץ קצר עם שתי הופעות יעלה מעל קובץ ארוך עם אותן שתי הופעות.
הסוג הזה לא משתמש באינדקס בזיכרון ולא באינדקס הטקסט של מונגו — הוא קורא את הקוד עצמו.
טקסט
המימוש הוא _text_search. השאילתה מתפרקת למילים, ומילות עצירה (stop_words) ומילים של תו אחד נזרקות. כל מילה שנשארת מחפשת באינדקס המילים שבזיכרון (SearchIndex.word_index), שנבנה מתוך תוכן הקוד בלבד.
כל מילה מתאימה בשתי דרכים:
התאמה מדויקת — המילה קיימת בקוד כמו שהיא. מקבלת ניקוד גבוה יותר.
התאמה חלקית — המילה היא תחילית של מילה בקוד, או מופיעה בתוכה. למשל
userמתאים גם ל-usernameוגם ל-get_user_id.
הניקוד של קובץ הוא סכום הניקוד של כל המילים שמצאו אותו. סדר המילים בשאילתה לא משנה, ואין דרישה שכל המילים יופיעו: קובץ שמתאים למילה אחת בלבד עדיין נכנס לתוצאות.
Regex
המימוש הוא _regex_search. התחביר הוא של מודול re בפייתון, בלי הבחנה בין אותיות גדולות לקטנות, ו-^ ו-$ מתייחסים לתחילת שורה ולסוף שורה ולא לקובץ כולו. הדירוג לפי מספר ההתאמות בקובץ.
לפני שהחיפוש רץ, _is_regex_safe (בתוך api_search_global) דוחה תבניות שעלולות לתקוע את השרת: תבנית ארוכה מ-200 תווים, .* שמופיע פעמיים, כמתים מקוננים כמו (a+)+, כמתים מספריים גדולים, ותבנית שלא מתקמפלת. תבנית שנדחתה מחזירה שגיאה ולא רצה.
מטושטש
המימוש הוא _fuzzy_search, עם fuzz.partial_ratio מ-rapidfuzz. לכל קובץ מחושב ציון דמיון בנפרד לשם הקובץ, לתוכן הקוד ולתגיות (מחוברות ברווח), והקובץ מקבל את הציון הגבוה מבין השלושה.
קובץ נכנס לתוצאות רק אם הציון שלו הוא לפחות 60 מתוך 100. הסף הזה כתוב ישירות בתוך _fuzzy_search.
partial_ratio מחפש את הקטע הכי דומה בתוך הטקסט ולא משווה את כל הטקסט. לכן שאילתה קצרה מוצאת קובץ ארוך שמכיל משהו דומה לה.
פונקציות
המימוש הוא _function_search. הוא מחפש באינדקס הפונקציות שבזיכרון (SearchIndex.function_index), שנבנה משמות הפונקציות ש-code_processor.extract_functions מחלץ מכל קובץ. פונקציה מתאימה אם השאילתה מופיעה בתוך השם שלה. כל פונקציה מתאימה מוסיפה לניקוד של הקובץ שלה, לפי כמה השם שלה קרוב לשאילתה כולה: בשאילתה save, פונקציה בשם save מוסיפה יותר מ-save_all_files_to_disk. לכן קובץ עם כמה פונקציות מתאימות יכול לעלות מעל קובץ עם התאמה מדויקת אחת.
מה שהחילוץ לא מזהה כפונקציה לא יימצא כאן. במקרה כזה אפשר לחפש את השם עם ”תוכן“.
חיפוש חכם
”חיפוש חכם“ לא עובר דרך /api/search/global. הדפדפן שולח אותו ל-POST /api/search/semantic (api_semantic_search), שמריץ את הפונקציה semantic_search ב-search_engine.py. זה חיפוש היברידי: חיפוש וקטורי על קטעי קוד (snippet_chunks) יחד עם חיפוש טקסט של Atlas, והתוצאות של שניהם מאוחדות. הפרטים הפנימיים, כולל שני רפי הציון, נמצאים ב-זרימת חיפוש (Search Flow).
מסנן השפה: בחיפוש חכם נשלחת רק השפה הראשונה מבין השפות שסימנתם.
יש לו גיבוי גם בשרת וגם בדפדפן, וחשוב להכיר אותם, כי בשניהם תקבלו תוצאות אבל לא של חיפוש לפי משמעות:
בשרת — כש-
SEMANTIC_SEARCH_ENABLEDכבוי, כששירות ה-embeddings לא זמין, כשיצירת ה-embedding לשאילתה נכשלה, או כשהאוסףsnippet_chunksלא זמין,semantic_searchמחזיר במקום זה תוצאות של מטושטש (_fallback_text_search).בדפדפן — כשהבקשה ל-
/api/search/semanticנכשלת,performGlobalSearchשולח את אותה שאילתה ל-/api/search/globalעםsemanticכסוג.api_search_globalלא מכיר את הערך הזה וממפה אותו ל-תוכן.
האפשרות ”חיפוש חכם“ מופיעה ברשימה תמיד, גם כשהחיפוש הסמנטי כבוי.
תיעוד
האפשרות ”תיעוד“ מופיעה ברשימה רק לאדמין, וגם לו לא במצב צפייה כמשתמש (user_is_admin בתבנית). היא מחפשת בסעיפים של אתר התיעוד, ולא בקבצים. הדפדפן שולח אותה ל-POST /api/search/docs (api_search_docs), ואין מעבר לחיפוש אחר כשמשהו נכשל: /api/search/global היה ממפה סוג שהוא לא מכיר ל“תוכן“, ותוצאות של קבצים היו נראות כמו תשובה מהתיעוד. במקום זה מוצגת הודעה עם הסיבה.
החיפוש עצמו הוא search_docs ב-services/docs_search_service.py:
השאלה מוטמעת במודל שהאינדקס נבנה בו, ולא במודל שבהגדרות כרגע. אחרי החלפת מודל, ועד שמעבר של האינדקס מסיים להטמיע מחדש, השאלה והנתחים עדיין באותו מרחב וקטורי.
האינדקס הווקטורי נבדק לפני ההטמעה. כשהוא חסר, במצב שגיאה, עוד לא מוכן לחיפוש או במימד אחר, התשובה היא
index_unavailableעם הסיבה (reason), ולא ”לא נמצאו תוצאות“.$vectorSearchמסנן כבר בתוך האינדקס לנתחים של אותו מודל. נתחים של אותו סעיף מתאחדים לציון הגבוה שבהם, והמארקדאון נשלף רק לסעיפים שמוחזרים.סעיף ארוך לא דוחק החוצה את השאר. הסבב הראשון מבקש
CHUNKS_PER_RESULTנתחים לכל סעיף שמוחזר. כשהנתחים נופלים בפחות סעיפים ממה שהתבקש והחיפוש מילא את כל המכסה — כי סעיף ארוך תפס את רובה — המכסה מוכפלת: עד שיש מספיק סעיפים, עד שהחיפוש מחזיר פחות נתחים ממה שביקשנו, או עד התקרה של Atlas (MAX_NUM_CANDIDATES). כשהסבב הראשון מוצא מספיק סעיפים, אין סבב נוסף.לכל שלב יש תקרה משלו:
QUERY_EMBED_DEADLINE_SECONDSלהטמעת השאלה, ו-SEARCH_DB_TIMEOUT_SECONDSלכל אחד משני החלקים מול המסד. ההמתנה של הדפדפן (DOCS_SEARCH_DEADLINE_MSב-global_search.js) ארוכה מסכומן, ו-tests/test_docs_search_files_page.pyמשווה ביניהן.שאלה ארוכה מ-
QUERY_MAX_BYTESבתים של UTF-8, אותו תקציב כמו נתח של התיעוד, נדחית ב-query_too_longולא נחתכת.אין לה מגבלת קצב משלה: אדמינים פטורים מהמגבלות של Flask-Limiter (
_is_rate_limit_exempt_userב-webapp/app.py). הקצב מול Gemini נשמר בשער של התהליך, כמו במעבר של האינדקס.
כל סעיף מוצג בכרטיס, מימין לשמאל, כמו בתצוגת ה-Markdown של דפדפן הקוד: אותו רינדור (MarkdownLiveRenderer), אותו עיצוב (markdown-preview.css) ואותו משטח (תצוגה מרונדרת (Markdown ועמודי תיעוד)). מעל הסעיף מופיעים קובץ המקור שלו בריפו (source_path בתשובה, למשל docs/webapp/global-search.rst), שביל הכותרות והציון, ומתחתיו ”פתח באתר התיעוד“ (בלשונית חדשה, עם rel="noopener") ו“העתק כמארקדאון“. קובץ המקור מוצג כטקסט, ולא כקישור. ברשומה של עדכון ביומן עדכונים (אינדקס התיעוד) הכותרת היא הפתיח של העדכון, ו“פתח באתר התיעוד“ קופץ אליו ולא לראש היום: אחרי העוגן של היום, הכתובת נושאת את הפתיח (השדה link_text של הרשומה, דרך text_fragment ב-services/docs_search_contract.py). את הקפיצה עושה הדפדפן, לפי התקן של URL Fragment Text Directives (https://wicg.github.io/scroll-to-text-fragment/), והוא מחפש את הפתיח בלי הבדל בין אותיות גדולות לקטנות ובלי ניקוד. ב-Chromium 141, על האתר עצמו (11.10.2026), הקפיצה מביאה את העדכון למסך. כשהפתיח כבר אינו בעמוד, למשל כי העדכון נערך אחרי שהאינדקס נבנה, הדפדפן מגיע לראש היום, כמו בקישור בלי קפיצה. דפדפן שאינו תומך בתחביר הזה לא קופץ לעדכון, ולאן הוא מגיע לא נמדד. סעיף ארוך מוצג בהתחלה רק בחלקו העליון, דוהה בתחתית, עם ”הצג את כל הסעיף“. כשהסעיף פתוח הכפתור מציע לכווץ אותו: הטקסט, האייקון ו-aria-expanded נגזרים יחד מהמצב של הסעיף (syncDocsExpandButton ב-global_search.js). הכפתור יושב בתחתית הסעיף הפתוח, ולכן מגיעים אליו בגלילה. אחרי הכיווץ, כשראש הכרטיס מעל המסך, המסך חוזר לראש הכרטיס, גם כשהכרטיס המכווץ גבוה מהמסך. כרטיס שראשו כבר על המסך לא זז. מזהים בתוך כרטיס מקבלים קידומת של הכרטיס, כדי ששני סעיפים עם אותה כותרת לא ייתנו אותו id בעמוד.
שורת המצב מעל הכרטיסים אומרת לאיזה קומיט של האתר האינדקס נכון, ומקשרת לעמוד האדמין של האינדקס (אינדקס התיעוד). כשהאינדקס לא מעודכן לפריסה האחרונה, או שנבנה בכללים קודמים (index_complete שקר, ראו ”שלם או חלקי“ ב-אינדקס התיעוד), השורה אומרת שייתכן שחסרים בו סעיפים. מספר התוצאות הוא ברירת המחדל של ה-API (DEFAULT_LIMIT). מיון, מספר תוצאות בעמוד וסינון לפי שפה לא חלים כאן, ולכן הפקדים שלהם מושבתים כשבוחרים ”תיעוד“.
חיפוש שהתחיל אחרון הוא שמוצג. תשובה של חיפוש קודם, שמגיעה אחרי שחיפוש חדש כבר התחיל, לא מוצגת. הכלל הזה חל על כל סוגי החיפוש בתיבה, כי Enter, הצעה ומעבר עמוד מתחילים חיפוש חדש גם כשהכפתור מושבת.
כשהסוג שבחרתם לא מוצא כלום
api_search_global לא קורא למנוע ישירות, אלא דרך _safe_search ב-webapp/app.py. אם המנוע מחזיר רשימה ריקה, או זורק חריגה, _safe_search מריץ חיפוש גיבוי ישירות במונגו:
ב-Regex —
$regexעל תוכן הקוד בלבד.בכל שאר הסוגים —
$textעל אינדקס הטקסטsearch_text_idx. האינדקס הזה (מוגדר ב-database/manager.py) כולל את שם הקובץ, התיאור, התגיות והקוד. כלומר בגיבוי חיפוש ”תוכן“ או ”טקסט“ כן יכול למצוא קובץ לפי השם שלו.
הקוד לא מעביר ל-$text שפה או רגישות לאותיות, ולכן חלות ברירות המחדל של מונגו. לפי התיעוד של MongoDB על $text:
הוא מתאים מילה שלמה (אחרי stemming) ולא חלק ממילה:
blueלא מוצאblueberry.הוא לא מבחין בין אותיות גדולות לקטנות.
כמה מילים בשאילתה מחוברות ב-או:
get userמחזיר כל קובץ שיש בוgetאוuser.
מה שונה בתוצאות של הגיבוי:
לכל התוצאות אותו ציון, כך שהמיון לפי רלוונטיות לא אומר כלום.
הסדר הוא תמיד מהקובץ שעודכן לאחרונה, בלי קשר למיון שבחרתם.
רק מסנני השפה והתגיות מוחלים.
מגבלות
מגבלה |
ערך |
איפה נאכפת |
|---|---|---|
אורך שאילתה |
500 תווים |
|
תבנית Regex |
ראו Regex |
|
קצב בקשות |
הכלל |
|
תוצאות בעמוד |
בין 1 ל-100 (בממשק: 5, 10, 20 או 50) |
|
האינדקס בזיכרון
”טקסט“ ו“פונקציות“ קוראים מאינדקס שנבנה בזיכרון של התהליך (SearchIndex). הבנייה היא סריקה של כל קבצי המשתמש, כולל הקוד. היא קורית בחיפוש הראשון מהסוגים האלה, וחוזרת כשהאינדקס מתיישן (SearchIndex.should_rebuild). ”תוכן“, ”Regex“ ו“מטושטש“ לא קוראים מהאינדקס, ולכן גם לא בונים אותו — אלא אם SEARCH_MEMORY_INDEX_EAGER_BUILD דלוק.
משתני הסביבה ששולטים בזה (את ברירות המחדל שלהם מתעד משתני סביבה - רפרנס):
SEARCH_MEMORY_INDEX_ENABLED— כשהוא כבוי, האינדקס נשאר ריק. ”טקסט“ ו“פונקציות“ לא ימצאו כלום במנוע, ולכן תמיד יעברו לגיבוי (כשהסוג שבחרתם לא מוצא כלום). בגיבוי ההתאמה החלקית של ”טקסט“ נעלמת, ו“פונקציות“ כבר לא מחפש בשמות פונקציות.SEARCH_MEMORY_INDEX_EAGER_BUILD— כשהוא דלוק, האינדקס נבנה גם בחיפושי ”תוכן“, ”Regex“ ו“מטושטש“. הסיבה היחידה להדליק אותו היא ההשלמה האוטומטית (השלמה אוטומטית), שקוראת מהאינדקס רק אם הוא כבר קיים.SEARCH_PAGE_SIZE— כמה קבצים נקראים בכל מנה, בסוגים שסורקים את הקבצים עצמם.
קאש
api_search_global שומר את התוצאות בקאש דרך cache.set_dynamic עם סוג התוכן search_results. מפתח הקאש נבנה מהמשתמש, מהשאילתה, מהסוג, מהמסננים, מהמיון, מהעמוד ומכמות התוצאות בעמוד. כלומר אותו חיפוש שחוזר על עצמו מגיע מהקאש, וכל שינוי באחד מהם הוא חיפוש חדש.
זמן התפוגה נקבע ב-DynamicTTL.calculate_ttl לפי סוג התוכן ולפי ההקשר. ההסבר על המנגנון נמצא ב-מערכת Caching מתקדמת עם TTL דינמי.
השלמה אוטומטית
כשמקלידים בתיבה, הדפדפן מבקש הצעות מ-GET /api/search/suggestions (api_search_suggestions), שקורא ל-suggest_completions. ההצעות מגיעות מהמקורות האלה:
שמות קבצים, תגיות (בצורת
#tag) ושפות, מ-autocomplete_manager.חיפוש
$textבמונגו, רק כשהשאילתה כבר מכילה מילה שלמה.מילים ושמות פונקציות מהאינדקס שבזיכרון, רק אם הוא כבר נבנה. ההשלמה לא בונה אותו בעצמה.
לחיצה על הצעה מכניסה אותה לתיבה ומריצה חיפוש מהסוג שבחור ברשימה.
לא לבלבל עם
החיפוש לפי שם בעמוד הקבצים. זו תיבה אחרת באותו עמוד (שדה
q, והטקסט ”חיפוש לפי שם, תיאור או תגית…“). היא שולחת את הדף מחדש עם?q=, ומחפשת בשם, בתיאור ובתגיות של הגרסה האחרונה של כל קובץ — לא בקוד: כל מילה בשאילתה צריכה להופיע באחד מהשלושה, גם כחלק ממילה, ובלי הבדל בין אותיות גדולות לקטנות. הכלל ב-_files_search_file_matchב-webapp/app.py, והוא חל בכל התצוגות של העמוד. אין לה סוגי חיפוש, והיא לא משתמשת ב-search_text_idx. עד אוקטובר 2026 היא הריצה$textעל האינדקס הזה, ולכן, בניגוד לטקסט שבה, מצאה גם קבצים שהמילה רק בקוד שלהם.``/search`` בבוט. הבוט לא משתמש בסוגי החיפוש שבעמוד הזה. יש שני מימושים של
search_command, ואף אחד מהם לא קורא למנוע: זה שב-main.pyמזהה תגית (#tag) או שם של שפה, ובכל מקרה אחר קורא ל-db.search_code. זה שב-bot_handlers.pyרק מפרק את השאילתה ומחזיר תצוגת הדגמה.
אינדקס התיעוד
החיפוש בתיעוד (הסוג ”תיעוד“ בתיבה, תיעוד) נשען על אינדקס של הסעיפים באתר התיעוד. האינדקס נבנה בוובאפ מתוך קובץ הייצוא של האתר (_export/sections.json, ראו Doc Authoring Guide (Sphinx/RTD)), ולא ממקור ה-rst, ומתעדכן אחרי כל פריסה של האתר. המימוש הוא services/docs_index_service.py. השמות שהכותב והחיפוש חייבים להסכים עליהם — האוספים, השדות, האינדקס הווקטורי וכתובת האתר — יושבים ב-services/docs_search_contract.py.
מה נכנס לאינדקס
סעיף (האוסף
docs_sections) — מה שכרטיס בתוצאות יציג: העמוד, העוגן, שביל הכותרות והמארקדאון של הסעיף.נתח (האוסף
docs_chunks) — הטקסט שנשלח להטמעה והווקטור שלו: שביל הכותרות, שורה ריקה, והגוף. סעיף ארוך מ-CHUNK_MAX_BYTES(services/chunking_service.py) מתחלק ב-split_code_to_chunks, ושביל הכותרות נספר בתוך התקציב.עמוד (האוסף
docs_pages) — עמוד rst שלם, למסך התצוגה המרונדרת בדפדפן הקוד (עמודי התיעוד (rst)): כל הסעיפים של העמוד לפי הסדר בקובץ, כל אחד עם הכותרת שלו ברמה שלו (page_markdownו-build_pages). בעמוד נכנסים כל הסעיפים, גם אלה שהחיפוש מדלג עליהם, ויומן עדכונים נשמר בו כימים ולא כעדכונים. עם העמוד נשמרים הקבצים האחרים שהוא נבנה מהם (dependenciesבקובץ הייצוא), שדפדפן הקוד משווה כדי לדעת אם העמוד ישן מהקוד שהוא מציג. המפתח הוא קובץ המקור. עמוד לא מוטמע, ולכן הוא אינו נספר בעלות ובאישור.סעיף בלי טקסט, או שכולו קישורים (”ראו גם“), לא נכנס כסעיף ולא כנתח (
is_links_only). בעמוד הוא נשאר.יומן עדכונים (
UPDATE_LOG_SOURCES, היום What’s New) — שם רשומה היא עדכון אחד ולא יום שלם: כל פריט ברשימה של יום נכנס כרשומה משלו. הכותרת שלה היא הפתיח המודגש של העדכון, והשביל הוא שביל היום ועוד הפתיח, כך שגם כל נתח של עדכון ארוך פותח בו. הגוף הוא שאר העדכון, כמו שסעיף נשמר בלי הכותרת שלו. עדכון בלי פתיח מודגש נכנס כרשומה בשם של היום, עם העדכון כולו. כך גם עדכון שהפתיח שלו מופיע בעמוד יותר מפעם אחת, בהשוואה של הדפדפן — בלי הבדל בין אותיות גדולות לקטנות, ובלי ניקוד ואקסנטים: אז הוא תווית, כמו ”מגבלה ידועה:“, ולא שם של עדכון. וכך גם עדכון שהפתיח שלו ארוך מהתקרה של כותרת בקובץ הייצוא (MAX_TITLE_BYTES), או שהשביל איתו ארוך מהתקרה של שביל (MAX_BREADCRUMB_BYTES): הפתיח הארוך לא נחתך, ורק עליו נרשמת אזהרה בלוג. מה שביום מחוץ לרשימה, כמו פסקה אחרי הבולטים, נכנס כרשומה אחת בשם של היום. איך כותבים עדכון שמתפרק כך: רשומה ב-What’s New.המפתח של סעיף הוא קובץ המקור, שביל הכותרות ומספר סידורי לשביל שחוזר באותו עמוד — בלי העוגן, כי העוגן של כותרת בלי תווית זז. עוגן שזז מעדכן את הסעיף בלי הטמעה מחדש. נתח מוטמע מחדש רק כשה-
sha256של הטקסט שלו השתנה, או כשהמודל בהגדרות השתנה. ברשומה של עדכון השביל כולל את הפתיח, ולכן עדכון שנוסף ליומן לא משנה את המפתח של עדכון אחר עם פתיח, כל עוד הפתיח שלו נשאר ייחודי בעמוד.
המעבר
מעבר (run_pass) משווה את מה שבקובץ למה שבמסד, ומיישר: מטמיע רק נתחים חדשים או שהשתנו, ומוחק מה שהקובץ כבר לא מכיר. מעבר אחד רץ בכל פעם, ב-thread ברקע, תחת lease במסמך המצב (האוסף docs_index_state). בקשה שמגיעה בזמן שמעבר רץ ממתינה ורצה אחריו; כמה בקשות שממתינות מתאחדות לאחרונה שבהן, כי כל מעבר קורא ממילא את הקובץ העדכני.
מה מפעיל מעבר:
פריסה של האתר — ה-webhook של GitHub, באירוע
deployment_status, כשפריסה שלgithub-pagesבריפו המקור הסתיימה בהצלחה (handle_deployment_status_eventב-webapp/routes/webhooks.py). המעבר קורא את העותק של הקובץ לפי הקומיט של הפריסה, כי הקובץ הראשי עלול להגיע ממטמון האתר.רשת ביטחון — ה-webhook של
pushלענף הראשי של ריפו המקור (handle_push_eventב-webapp/routes/webhooks.py), לצד הסנכרון של המראה ובלי תלות בו: היא קוראת את הקובץ הראשי של האתר עםIf-None-Match, ובדרך כלל חוזרת ב-304 בלי לעשות כלום. היא תופסת פריסה שה-webhook שלה לא הגיע — אבל רק ב-push שאחריה, כי בזמן ה-push הבנייה שלו עוד לא נפרסה.”בדוק עכשיו“ בעמוד האדמין — כמו רשת הביטחון.
השומרים:
עלות. מעבר אוטומטי שיש לו יותר מ-
APPROVAL_THRESHOLD_CHUNKSנתחים להטמעה — וכל מילוי ראשון או החלפת מודל — לא שולח כלום ועוצר ב“מחכה לאישור“, עם התוכנית וטביעת אצבע שלה. האישור בעמוד האדמין מריץ אותה רק אם התוכנית שהמעבר מחשב מחדש זהה לזו שאושרה; אם היא השתנתה לתוכנית שאינה דורשת אישור, היא רצה כמו מעבר אוטומטי. כשהאינדקס נכנס להמתנה נשלחת התראה לבוט, עם מספר הנתחים, התקרה והקומיטים, ושורת Quick Fix לעמוד האדמין (ALERT_APPROVAL_NEEDED, ראו אינדקס התיעוד). מעבר שעוצר שוב בזמן שהאינדקס כבר מחכה — למשל מיזוג שמחשב מחדש את אותו פער מצטבר — אינו שולח עוד אחת. ההמתנה נגמרת כשמעבר מתחיל לכתוב, ולכן אחריו — בין שהצליח ובין שנעצר באמצע, למשל ב-paused_quota— הכניסה הבאה להמתנה שולחת שוב: זו כבר בקשה חדשה לאישור.מחיקה. מוחקים רק כשהקובץ מכיר לפחות סעיף, נתח או עמוד אחד מאלה שבמסד. קובץ שלא מכיר אף אחד מהם לא מוחק כלום, והמעבר רושם כמה מחיקות נחסכו.
לא אחורה. קובץ שנבנה לפני הקובץ שכבר הוחל לא מוחל (הקוד
older_export): עותק ישן ממטמון האתר, או webhook של פריסה קודמת שהגיע באיחור, לא מחזירים את האינדקס אחורה.שלם או חלקי. הקומיט שהאינדקס נכון אליו (
indexed_source_commit) נמחק לפני הכתיבה הראשונה של מעבר, ונכתב רק אחרי קריאה חוזרת שמוצאת כל סעיף, נתח ועמוד רצויים, עם ה-sha (ובנתחים גם המודל) הנכונים. בין שני הרגעים העמוד מציג את האינדקס כ“חלקי“. לצד הקומיט נשמרת גרסת הכללים שבנו את הרשומות (RECORDS_VERSION). קוד שהופך את אותו קובץ לרשומות אחרות מעלה אותה, ועד שמעבר בונה את האינדקס בכללים החדשיםis_index_completeמחזיר שקר: רשת הביטחון ו“בדוק עכשיו“ לא נעצרים על 304, אלא קוראים את הקובץ בליIf-None-Matchובונים את התוכנית מחדש — גם כשקובץ הייצוא לא השתנה — ושורת המצב בחיפוש אומרת שייתכן שחסרים סעיפים. עמוד האדמין קובע ”שלם“ לפי הקומיט בלבד (admin_docs_index.html), ולכן אינו מסמן את המצב הזה.עצירה ודדליין. לפני כל אצווה שכותבת — הטמעה, כתיבת סעיפים או מחיקה — נבדקות בקשת עצירה ו-
PASS_DEADLINE_SECONDS, וה-lease מתחדש. עצירה היא גם בלם: מעבר שמוחק או כותב בלי הטמעות נעצר באותה נקודה, והספירה של מה שכבר נמחק נשמרת ב-run.deleted. מכסה של Gemini (429) עוצרת את המעבר במצבpaused_quota; המעבר הבא ממשיך מאיפה שהקודם עצר, ושולח רק את מה שחסר.
עמוד האדמין
/admin/docs-index, וכרטיס שמוביל אליו בהגדרות, בקבוצה ”כלי אדמין“. בעמוד: המעבר האחרון ומצבו, הקומיט שהאינדקס נכון אליו מול הקובץ שנקרא לאחרונה ומול ה-HEAD של המראה (כדי שפיגור ייראה), תוכנית ”יבשה“ מהקובץ הראשי, אישור של תוכנית, ”בדוק עכשיו“ ועצירה. ה-API של העמוד (/api/docs-index והנתיבים שתחתיו) פתוח לאדמינים בלבד, וחסום גם במצב צפייה כמשתמש (admin_api_required). פעולה שמשנה מצב היא POST עם גוף JSON.
הגדרה חד-פעמית
ה-webhook. ב-webhook של הריפו ב-GitHub — אותו webhook שמפעיל את הסנכרון (דפדפן קוד (Code Browser)) — בוחרים Let me select individual events ומסמנים, לצד
push, גם אתdeployment_status. בלי זה רק רשת הביטחון והכפתור מפעילים מעבר. ה-webhook מוחרג מהמגבלה הגורפת של קצב הבקשות בוובאפ, כי GitHub שולח מכמה כתובות קבועות, ולכל פריסה יש כמה סטטוסים.האינדקס הווקטורי. ב-Atlas יוצרים אינדקס מסוג Vector Search בשם
docs_vector_indexעל האוסףdocs_chunks, עם ההגדרה שעמוד האדמין מציג (vector_index_definition):cosineעלchunkEmbeddingבמימד של ההגדרות הפעילות, ופילטר עלembeddingModelKey. אחרי החלפת מודל במימד אחר צריך ליצור אותו מחדש, והעמוד אומר אם המימד תואם. בשרת שאינו Atlas העמוד מציג את מצב האינדקס כ“לא ידוע“.מילוי ראשון. ”בדוק עכשיו“ — המעבר עוצר לאישור ומציג כמה נתחים יישלחו — ואז ”אשר והתחל“.
האירועים של המעבר ושל העמוד מתועדים ב-אינדקס התיעוד.
ראו גם
זרימת חיפוש (Search Flow) — הפרטים הפנימיים של מנוע החיפוש:
SearchIndex, מסננים, מיון, והחיפוש הסמנטי.תחזוקת קטלוג הפקודות (commands.json) — כרטיסי ”קיצורי הדרך“ שמופיעים באותה תיבת חיפוש.
משתני סביבה - רפרנס — המשתנים
SEARCH_*ו-SEMANTIC_*.אינדקס התיעוד — האירועים של אינדקס התיעוד.