Come velocizzare il motore di ricerca di WordPress utilizzando un indice JSON statico

No comments

24.07.2026|

No comments

Tiempo de lectura Lectura: 10 min, 53 s
Número de palabras Palabras: 2015
Número de visitas Visitas: 6
Icono de traducción

Non è un segreto per nessuno che il motore di ricerca di WordPress sia una schifezza. Per quanto riguarda l’aspetto, è relativamente facile da sistemare; tuttavia, il suo funzionamento è tutta un’altra storia, dato che non offre alcuna opzione di configurazione e, inoltre, le prestazioni sono pessime.

Quando un sito WordPress accumula migliaia di post, il motore di ricerca nativo inizia a presentare dei problemi. Diventa così lento da risultare praticamente inutilizzabile. Quando si effettua una ricerca, WordPress esegue una query LIKE %parola% sul database, che setaccia l’intero contenuto della tabella wp_posts. Ciò genera query SQL che possono richiedere da pochi secondi a un’eternità e causare picchi di utilizzo della memoria che finiscono per bloccare il server.

Era esattamente quello che mi stava già succedendo. Con 6.040 post pubblicati (solo in spagnolo), i tempi di risposta delle ricerche con più risultati erano un vero inferno, con tempi superiori ai 10 secondi o anche di più. Ed è arrivato il momento di risolvere la questione una volta per tutte.

Dopo aver riflettuto a lungo sulla questione, ho scoperto che c’erano almeno tre possibilità per risolvere il problema. La prima e più semplice era ricorrere a un plugin a pagamento, come Relevanssi (la cui versione gratuita è insufficiente), oppure a un servizio esterno, come Algolia, opzione che ho scartato fin da subito, poiché il mio obiettivo è velocizzare le ricerche in modo nativo.

Tra le due opzioni rimanenti, ho provato a creare un indice FULLTEXT in MySQL utilizzando le query MATCH() AGAINST(), ma ho fallito miseramente. Nonostante avessi regolato i parametri del motore InnoDB e ottimizzato gli indici, MySQL continuava a bloccarsi durante l’elaborazione di migliaia di parole chiave complesse, con tempi di risposta inaccettabili e un sovraccarico del server. È stato allora che ho scartato completamente l’idea di affidare tutto il carico di lavoro al database e ho optato per la terza alternativa: creare un indice statico in JSON.

In questo tutorial cerchiamo di spiegare come risolvere questo problema sostituendo le query MySQL più pesanti con un file JSON statico che funge da indice di ricerca ultraveloce.

Fase 1: Individuare ed eliminare le query pesanti o ridondanti

Prima di applicare l'indice JSON, è fondamentale preparare il terreno. Molti plugin (come quelli per le directory, i layout o la ricerca secondaria) si integrano con il processo di ricerca di WordPress tramite hook (pre_get_posts, posts_where, ecc.), generando query aggiuntive e non necessarie al database.

Come si possono identificare queste query con Query Monitor?

  1. Installa e attiva il plugin gratuito Query Monitor.
  2. Effettua una ricerca qualsiasi sul tuo sito.
  3. Apri il menu a tendina del Query Monitor nella barra superiore e seleziona "Query del database " (Database Queries).
  4. Filtra per “Query lente” oppure esamina l’elenco alla ricerca delle query che richiedono più tempo. Cogli l’occasione per annotare anche le query duplicate e, se possibile, risolvi anche questo problema.
  5. Osserva la colonna dei dettagli delle tabelle: lì vedrai esattamente quale plugin o file sta eseguendo quella query.

Nel mio caso, ho dovuto risolvere alcune query duplicate del plugin Name_Directory e di alcuni blocchi di GenerateBlocks nel modello di post, ma qualsiasi plugin, codice personalizzato o elemento aggiunto potrebbe generare query che rallentano la risposta.

Come disattivare questi hook tramite codice

Dopo aver individuato il nome della funzione o della classe che sta causando il rallentamento della query, è possibile disattivarla nel file functions.php oppure tramite un plugin per snippet di codice (come Code Snippets, Code di Perfmatters o simili):

* Nota. Nel blocco 1 devi aggiungere i tuoi hook che generano query lente. Se non è il tuo caso, puoi omettere questa parte e aggiungere allo snippet solo il blocco 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);

Nota: per disattivare un hook con `remove_action` o `remove_filter`, è necessario utilizzare esattamente lo stesso nome dell'evento (`pre_get_posts`), lo stesso nome della funzione individuata in Query Monitor e la stessa priorità con cui è stato aggiunto (per impostazione predefinita, 10).

Fase 2: Generare e mantenere aggiornato il file JSON di ricerca

Per sostituire MySQL, dobbiamo creare un file statico in formato JSON che contenga i dati principali dei nostri post (ID, titolo e testo semplice). Questo file verrà salvato nella cartella wp-content/uploads/ e verrà aggiornato automaticamente ogni volta che creerai, modificherai o eliminerai un post.

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));
}

Generazione iniziale: la prima volta che installi questo snippet, puoi eseguire temporaneamente my_site_rebuild_full_json_index(); oppure semplicemente salvare/aggiornare un qualsiasi post esistente affinché il file search-es.json venga creato per la prima volta. Successivamente, accedi alla cartella wp-content/uploads/ per verificare che il file sia stato creato e per controllarne le dimensioni.

Per darti un'idea delle dimensioni approssimative di questo file search-es.json: per le mie 6. 040 voci, è stato generato un file di 3,41 Mb, una dimensione abbastanza ragionevole, poiché, a meno che non superi i 15 o 20 MB (l'equivalente di circa 30.000 o 40.000 voci estese), non c’è motivo di preoccuparsi, poiché i file JSON statici vengono caricati direttamente nella cache del server o di PHP (file_get_contents) in un’unica operazione, e una dimensione di pochi megabyte viene letta in meno di un millesimo di secondo; tuttavia, se il file dovesse crescere in modo sproporzionato, ogni volta che viene eseguita una ricerca, PHP consumerà un picco aggiuntivo e superfluo di memoria RAM per caricare un JSON gigantesco; a quel punto, sarebbe consigliabile valutare di indicizzare solo i titoli e gli estratti, anziché l’intero contenuto, oppure passare a una soluzione di ricerca indicizzata in un database dedicato.

Fase 3: Funzione ausiliaria per normalizzare le ricerche (senza accenti)

Per garantire che le ricerche restituiscano risultati indipendentemente dal modo in cui l'utente digita (con o senza accenti, in maiuscolo o minuscolo), si definisce una funzione di pulizia:

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);
    }
}

Passaggio 4: Intercettare la query principale con posts_pre_query

Tramite il filtro `posts_pre_query`, la query di ricerca di WordPress viene intercettata prima di raggiungere il database. Il JSON viene letto, vengono identificati gli ID corrispondenti ai termini cercati e solo questi risultati vengono restituiti a WordPress, specificando la paginazione corrispondente.

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);

Fase 5: Verifica dei risultati

Dopo aver completato i passaggi precedenti, esegui un test di ricerca e apri il Query Monitor:

  1. Tempo totale del BD: noterai che ora passa da diversi secondi a pochi millisecondi (di solito tra 0,03 s e 0,2 s).
  2. Query SQL: la clausola LIKE %termine% scomparirà completamente.
  3. Memoria RAM: il picco di utilizzo della memoria rimarrà eccezionalmente basso (circa 30 MB).
  4. Ordinamento: i post verranno visualizzati in ordine cronologico rigorosamente decrescente (dal più recente al più vecchio), rispettando l'impostazione predefinita di WordPress.

Sono più che soddisfatto del risultato. Non solo ho risolto un problema che mi affliggeva da anni, ma ora il risultato è semplicemente spettacolare. Anche in una delle ricerche più complesse, come questa con 4. 887 risultati per“viñeta”, il motore di ricerca risponde molto rapidamente. Per le ricerche con meno risultati, si può dire che questi vengano visualizzati all’istante. Potete verificarlo voi stessi cercando qualsiasi altra cosa.

Come velocizzare il motore di ricerca di WordPress utilizzando un indice JSON statico 1

Lascia un commento

Qualcosa da dire?

Este blog se aloja en LucusHost

LucusHost, el mejor hosting