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

No comments

24.07.2026|

No comments

Tiempo de lectura Lectura: 5 min, 16 s
Número de palabras Palabras: 975
Número de visitas Visitas: 6
Icono de traducción

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

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

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

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

מבין שתי האפשרויות שנותרו, ניסיתי ליצור אינדקס FULLTEXT ב-MySQL באמצעות שאילתות MATCH() AGAINST() ונכשלתי כישלון חרוץ. למרות שהתאמתי את הפרמטרים של מנוע ה-InnoDB ומיטבתי את האינדקסים, MySQL המשיך להיתקע בעת עיבוד אלפי מילות מפתח מורכבות, והציג זמני תגובה בלתי מקובלים והעמיס על השרת. בשלב זה ויתרתי לחלוטין על הרעיון להטיל את העומס הכבד על מסד הנתונים ובחרתי בדרך השלישית: בניית אינדקס סטטי ב-JSON.

במדריך זה ננסה להסביר כיצד לפתור את הבעיה הזו על ידי החלפת שאילתות MySQL כבדות בקובץ JSON סטטי המשמש כאינדקס חיפוש מהיר במיוחד.

שלב 1: איתור וחיסול שאילתות כבדות או מיותרות

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

כיצד ניתן לאתר שאילתות אלה באמצעות Query Monitor?

  1. התקן והפעל את התוסף החינמי Query Monitor.
  2. בצע כל חיפוש באתר שלך.
  3. פתח את התפריט הנפתח של Query Monitor בסרגל העליון ובחר באפשרות " שאילתות מסד נתונים " (Database Queries).
  4. סנן לפי שאילתות איטיות או עבר על הרשימה כדי לאתר את השאילתות שלוקחות יותר זמן. תוך כדי כך, רשום גם את השאילתות הכפולות, ואם תוכל, טפל גם בהן.
  5. שים לב לעמודת הפירוט בטבלאות: שם תוכל לראות בדיוק איזה תוסף או קובץ מבצע את השאילתה הזו.

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

כיצד להשבית את ה-hooks האלה באמצעות קוד

לאחר שאיתרת את שם הפונקציה או המחלקה שגורמת לשאילתה האיטית, תוכל להשבית אותה בקובץ functions.php שלך או באמצעות תוסף לקטעי קוד (כגון Code Snippets, Code של Perfmatters או תוסף דומה):

* הערה: בבלוק 1 עליך להוסיף את ה-hooks שגורמים לשאילתות איטיות. אם זה לא המצב אצלך, תוכל למחוק חלק זה ולהוסיף רק את בלוק 2 לקטע הקוד.

/**
 * 1. Desactivar funciones invasivas o redundantes en las búsquedas globales de WP
 */
add_action('init', function() {
    // Ejemplo: Si Query Monitor muestra que un plugin de directorio se ejecuta en pre_get_posts:
    // remove_action('hook_de_wordpress', 'nombre_de_la_funcion_del_plugin', prioridad);
    remove_action('pre_get_posts', 'name_directory_search', 10);
    remove_filter('posts_where', 'name_directory_insert_sitewide_search_results', 10);
}, 20);

/**
 * 2. Bloquear la búsqueda SQL nativa tipo LIKE %palabra% en MySQL
 */
add_filter('posts_search', function($search, $wp_query) {
    if (!is_admin() && is_search()) {
        return ''; // Neutraliza el LIKE %palabra% en MySQL globalmente en páginas de búsqueda
    }
    return $search;
}, 10, 2);

הערה: כדי לבטל הוק באמצעות remove_action או remove_filter, עליך להשתמש בדיוק באותו שם אירוע (pre_get_posts), באותו שם הפונקציה שזיהית ב-Query Monitor ובאותה עדיפות שבה הוא נוספה (ברירת המחדל היא 10).

שלב 2: יצירת קובץ ה-JSON לחיפוש ושמירה על עדכניותו

כדי להחליף את MySQL, עלינו ליצור קובץ סטטי בפורמט JSON שיכיל את נתוני המפתח של הפוסטים שלנו (מזהה, כותרת וטקסט רגיל). קובץ זה יישמר בתיקייה wp-content/uploads/ ויעודכן באופן אוטומטי בכל פעם שתיצור, תערוך או תמחק פוסט.

PHP

/**
 * Actualizar el archivo search-es.json automáticamente al guardar o borrar un post
 */
add_action('save_post', 'mi_sitio_actualizar_indice_busqueda_json', 10, 3);
add_action('deleted_post', 'mi_sitio_actualizar_indice_busqueda_json', 10, 1);

function mi_sitio_actualizar_indice_busqueda_json($post_id) {
    if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) return;
    if (wp_is_post_revision($post_id)) return;
    
    $post = get_post($post_id);
    if (!$post || $post->post_type !== 'post') return;

    // Opcional: Filtrar por idioma si usas un plugin multilingüe como Polylang
    if (function_exists('pll_get_post_language') && pll_get_post_language($post_id) !== 'es') {
        return;
    }

    mi_sitio_reconstruir_indice_json_completo();
}

/**
 * Función que recorre todas las entradas publicadas y crea el JSON
 */
function mi_sitio_reconstruir_indice_json_completo() {
    $args = array(
        'post_type'      => 'post',
        'post_status'    => 'publish',
        'posts_per_page' => -1,
        'orderby'        => 'date',
        'order'          => 'ASC',
        'fields'         => 'ids', // Solo IDs para optimizar memoria
    );

    if (function_exists('pll_current_language')) {
        $args['lang'] = 'es';
    }

    $all_post_ids = get_posts($args);
    $indice = array();

    foreach ($all_post_ids as $id) {
        $titulo = get_the_title($id);
        $contenido = get_post_field('post_content', $id);

        $indice[] = array(
            'i' => (int)$id,
            't' => (string)$titulo,
            'k' => (string)wp_strip_all_tags($contenido),
        );
    }

    $upload_dir = wp_upload_dir();
    $json_path  = $upload_dir['basedir'] . '/search-es.json';

    file_put_contents($json_path, json_encode($indice, JSON_UNESCAPED_UNICODE));
}

הגדרה ראשונית: בפעם הראשונה שתתקין את הקטע הקוד הזה, תוכל להריץ באופן זמני את הפקודה my_site_rebuild_full_json_index(); או פשוט לשמור/לעדכן כל רשומה קיימת כדי שהקובץ search-es.json ייווצר לראשונה. לאחר שתעשה זאת, בקר בנתיב wp-content/uploads/ כדי לוודא שהקובץ נוצר ולבדוק את גודלו.

כדי שתוכל לקבל מושג על הגודל המשוער של קובץ ה-search-es.json הזה: עבור 6,040 הערכים שלי נוצר קובץ בגודל 3.41 מגה-בייט – גודל סביר למדי, שכן כל עוד הוא לא עולה על 15 או 20 מגה-בייט (השווה לכ-30,000 או 40,000 רשומות ארוכות) אין סיבה לדאגה, מכיוון שקבצי JSON סטטיים נטענים בבת אחת ישירות למטמון של השרת או של PHP (file_get_contents), וגודל של כמה מגה-בייט נקרא בפחות מאלפית שנייה; עם זאת, אם הקובץ גדל באופן חריג, בכל פעם שתבוצע חיפוש, PHP יצרוך שיא נוסף ומיותר של זיכרון RAM כדי לטעון קובץ JSON ענק, ובמקרה כזה כדאי לשקול לאנדקס רק את הכותרות והתקצירים במקום את כל התוכן המלא, או לעבור לפתרון חיפוש המבוסס על אינדקס במסד נתונים ייעודי.

שלב 3: פונקציה עזר לנורמליזציה של חיפושים (ללא סימני ניקוד)

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

PHP

/**
 * Función auxiliar para eliminar acentos y pasar a minúsculas
 */
if (!function_exists('mi_sitio_limpiar_acentos')) {
    function mi_sitio_limpiar_acentos($cadena) {
        $cadena = mb_strtolower($cadena, 'UTF-8');
        $string = array(
            'á'=>'a', 'à'=>'a', 'ä'=>'a', 'â'=>'a', 'ª'=>'a', 'Á'=>'a', 'À'=>'a', 'Ä'=>'a', 'Â'=>'a',
            'é'=>'e', 'è'=>'e', 'ë'=>'e', 'ê'=>'e', 'É'=>'e', 'È'=>'e', 'Ë'=>'e', 'Ê'=>'e',
            'í'=>'i', 'ì'=>'i', 'ï'=>'i', 'î'=>'i', 'Í'=>'i', 'Ì'=>'i', 'Ï'=>'i', 'Î'=>'i',
            'ó'=>'o', 'ò'=>'o', 'ö'=>'o', 'ô'=>'o', 'Ó'=>'o', 'Ò'=>'o', 'Ö'=>'o', 'Ô'=>'o',
            'ú'=>'u', 'ù'=>'u', 'ü'=>'u', 'û'=>'u', 'Ú'=>'u', 'Ù'=>'u', 'Ü'=>'u', 'Û'=>'u',
            'ñ'=>'n', 'Ñ'=>'n'
        );
        return strtr($cadena, $string);
    }
}

שלב 4: יירוט השאילתה הראשית באמצעות posts_pre_query

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

PHP

/**
 * Interceptación de la búsqueda nativa leyendo el archivo JSON
 */
add_filter('posts_pre_query', function($posts, $wp_query) {
    if (!is_admin() && $wp_query->is_search() && $wp_query->is_main_query()) {
        
        if (function_exists('pll_current_language')) {
            if (pll_current_language() !== 'es') return $posts;
        }

        $s = $wp_query->get('s');
        if (empty($s)) return array();

        $upload_dir = wp_upload_dir();
        $json_path  = $upload_dir['basedir'] . '/search-es.json';

        if (!file_exists($json_path)) return $posts;

        $indice = json_decode(file_get_contents($json_path), true);
        if (!is_array($indice)) return $posts;

        $s_clean = mi_sitio_limpiar_acentos(wp_strip_all_tags($s));
        $palabras_buscadas = array_filter(explode(' ', $s_clean));

        if (empty($palabras_buscadas)) return array();

        $matched_ids = array();

        // Recorrer el índice acumulando coincidencias
        foreach ($indice as $item) {
            $titulo_clean = mi_sitio_limpiar_acentos($item['t']);
            $texto_clean  = mi_sitio_limpiar_acentos($item['k']);

            $coinciden = true;
            foreach ($palabras_buscadas as $palabra) {
                if (mb_strlen($palabra, 'UTF-8') < 2) continue; // Ignorar palabras de 1 letra

                if (mb_strpos($titulo_clean, $palabra) === false && mb_strpos($texto_clean, $palabra) === false) {
                    $coinciden = false;
                    break; 
                }
            }

            if ($coinciden) {
                $matched_ids[] = (int)$item['i'];
            }
        }

        // Si no hay resultados, devolver array vacío
        if (empty($matched_ids)) {
            $wp_query->found_posts = 0;
            $wp_query->max_num_pages = 0;
            return array();
        }

        // Paginación
        $posts_per_page = (int)get_option('posts_per_page', 10);
        $paged = max(1, (int)$wp_query->get('paged'));

        $total_encontrados = count($matched_ids);
        
        $wp_query->found_posts = $total_encontrados;
        $wp_query->max_num_pages = ceil($total_encontrados / $posts_per_page);

        // Recuperar solo los objetos de los posts encontrados, ordenados por fecha
        return get_posts(array(
            'post__in'               => $matched_ids,
            'orderby'                => 'date',
            'order'                  => 'DESC',
            'posts_per_page'         => $posts_per_page,
            'paged'                  => $paged,
            'post_type'              => 'post',
            'no_found_rows'          => true,
            'cache_results'          => true,
            'update_post_meta_cache' => false,
            'update_post_term_cache' => false,
            'suppress_filters'       => true,
        ));
    }

    return $posts;
}, 10, 2);

שלב 5: אימות התוצאות

לאחר ביצוע השלבים שלעיל, בצע בדיקת חיפוש ופתח את Query Monitor:

  1. זמן ה-BD הכולל: תוכל לראות שכעת הוא מתקצר מכמה שניות לכמה מילי-שניות בלבד (בדרך כלל בין 0.03 שניות ל-0.2 שניות).
  2. שאילתות SQL: הסעיף LIKE %מונח% ייעלם לחלוטין.
  3. זיכרון RAM: שיא השימוש בזיכרון יישאר נמוך במיוחד (כ-30 MB).
  4. מיון: הפוסטים יוצגו בסדר כרונולוגי יורד קפדני (מהחדש ביותר לישן ביותר), תוך שמירה על חלוקת העמודים המקורית של וורדפרס.

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

כיצד להאיץ את מנוע החיפוש של וורדפרס באמצעות אינדקס JSON סטטי 1

כתיבת תגובה

יש לך מה לומר?

Este blog se aloja en LucusHost

LucusHost, el mejor hosting