Statik bir JSON dizini kullanarak WordPress arama motorunu nasıl hızlandırabilirsiniz?

No comments

24.07.2026|

No comments

Tiempo de lectura Lectura: 11 min, 29 s
Número de palabras Palabras: 2127
Número de visitas Visitas: 6
Icono de traducción

WordPress’in arama motorunun berbat olduğu artık herkesin malumu. Görsel açıdan bu sorunu çözmek nispeten kolay olsa da, işlevselliği bambaşka bir konu; zira herhangi bir yapılandırma seçeneği sunmuyor ve üstüne üstlük performansı da içler acısı.

WordPress’teki bir sitede binlerce yazı biriktiğinde, yerleşik arama motoru hatalar vermeye başlar. Yavaşlar ve neredeyse kullanılamaz hale gelir. Bir arama yapıldığında, WordPress veritabanında wp_posts tablosunun tüm içeriğini tarayan bir LIKE %kelime% sorgusu çalıştırır. Bu, birkaç saniyeden sonsuza kadar sürebilen SQL sorguları oluşturur ve sonunda sunucuyu kilitleyen bellek yükü artışlarına neden olur.

Bu tam da başıma gelen şeydi. Yayınlanmış 6040 gönderi (sadece İspanyolca) varken, en çok sonuç veren aramaların yanıt süreleri 10 saniyeyi aşan, tam bir cehennem gibiydi. Ve sonunda bu sorunu bir kez ve sonsuza kadar çözmenin zamanı geldi.

Uzun uzun düşündükten sonra, bunu çözmek için en az üç olasılık olduğunu fark ettim. İlk ve en basit olanı, Relevanssi gibi ücretli bir eklentiyi (ücretsiz sürümü yetersiz) veya Algolia gibi harici bir hizmeti kullanmaktı; ancak aradığım şey aramaları yerel olarak hızlandırmak olduğu için bu seçeneği baştan eledim.

Kalan iki seçenekten, MATCH() AGAINST() sorguları kullanarak MySQL’de bir FULLTEXT dizini oluşturmayı denedim ve feci şekilde başarısız oldum. InnoDB motorunun parametrelerini ayarlayıp dizinleri optimize etmeme rağmen, MySQL binlerce karmaşık anahtar kelimeyi işlerken hâlâ takılıp kalıyor, kabul edilemez yanıt süreleri veriyor ve sunucuyu aşırı yüklemeye devam ediyordu. İşte o zaman, bu ağır yükü veritabanına bırakma fikrini tamamen bir kenara bıraktım ve üçüncü yolu, yani JSON'da statik bir dizin oluşturmayı tercih ettim.

Bu eğitimde, MySQL’deki ağır sorguları, ultra hızlı bir arama dizini görevi gören statik bir JSON dosyasıyla değiştirerek bu sorunun nasıl çözüleceği anlatılmaya çalışılmaktadır.

1. Adım: Ağır veya gereksiz sorguları tespit etmek ve ortadan kaldırmak

JSON dizini uygulamadan önce, ortamı temizlemek çok önemlidir. Birçok eklenti (dizinler, sayfa düzenleyiciler veya ikincil arama eklentileri gibi), WordPress’in arama sürecine kancalar (pre_get_posts, posts_where vb.) aracılığıyla müdahale ederek, veritabanına ek ve gereksiz sorgular gönderir.

Query Monitor ile bu sorguları nasıl tespit edebiliriz?

  1. Ücretsiz Query Monitor eklentisini yükleyin ve etkinleştirin.
  2. Web sitenizde istediğiniz herhangi bir aramayı yapın.
  3. Üst çubuktaki Query Monitor açılır menüsünü açın ve “Veritabanı Sorguları” (Database Queries) seçeneğini seçin.
  4. "Yavaş sorgular "a göre filtreleyin veya listeyi inceleyerek en uzun süren sorguları bulun. Bu arada, yinelenen sorguları da not alın ve mümkünse bunları da düzeltin.
  5. Tablolardaki ayrıntı sütununa bakın: orada, o sorguyu hangi eklentinin veya dosyanın çalıştırdığını tam olarak görebilirsiniz.

Benim durumumda, Name_Directory eklentisinden gelen birkaç yinelenen sorguyu ve yazı şablonundaki bazı GenerateBlocks bloklarını düzeltmem gerekti; ancak herhangi bir eklenti, özel kod veya eklenen öğe, yanıt süresini yavaşlatan sorgular oluşturuyor olabilir.

Bu hook'ları kod yoluyla nasıl devre dışı bırakılır?

Yavaş sorguyu tetikleyen işlevin veya sınıfın adını belirledikten sonra, bunu functions.php dosyanızda veya bir kod parçacığı eklentisi ( Code Snippets, PerfmattersCode veya benzeri) aracılığıyla devre dışı bırakabilirsiniz:

* Not. 1. bloğa, yavaş sorgular oluşturan hook'larınızı eklemelisiniz. Sizin durumunuzda böyle değilse, bu kısmı kaldırıp snippet'e sadece 2. bloğu ekleyebilirsiniz.

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

Not: Bir hook’u remove_action veya remove_filter ile kaldırmak için, tam olarak aynı olay adını (pre_get_posts), Query Monitor’da belirlediğiniz işlev adını ve eklenirken kullanılan önceliği (varsayılan olarak 10) kullanmanız gerekir.

2. Adım: Arama JSON dosyasını oluşturmak ve güncel tutmak

MySQL'in yerine geçmesi için, yazılarımızın temel verilerini (ID, başlık ve düz metin) içeren JSON formatında statik bir dosya oluşturmamız gerekiyor. Bu dosya wp-content/uploads/ klasörüne kaydedilecek ve bir yazı oluşturduğunuzda, düzenlediğinizde veya sildiğinizde otomatik olarak güncellenecektir.

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

İlk kurulum: Bu kod parçasını ilk kez kurduğunuzda, geçici olarak my_site_rebuild_full_json_index(); işlevini çalıştırabilir veya mevcut herhangi bir girişi kaydedip güncelleyerek search-es.json dosyasının ilk kez oluşturulmasını sağlayabilirsiniz. Bu işlemi tamamladıktan sonra wp-content/uploads/ yolunu ziyaret ederek dosyanın oluşturulduğundan emin olun ve dosya boyutunu kontrol edin.

Bu search-es.json dosyasının yaklaşık boyutunu anlaman için bir örnek vereyim: Benim 6040 girdim için 3,41 Mb’lik bir dosya oluşturuldu; bu, 15 veya 20 MB’yi (yaklaşık 30.000 veya 40.000 uzun girişe eşdeğer) aşmadıkça endişelenmenize gerek yok; çünkü statik JSON dosyaları sunucunun önbelleğine veya PHP’ye (file_get_contents) tek seferde yüklenir ve birkaç megabaytlık bir dosya milisaniyenin binde birinden daha kısa sürede okunur; ancak dosya aşırı derecede büyürse, her arama yapıldığında PHP, devasa bir JSON dosyasını yüklemek için gereksiz bir RAM pik tüketimi yaşayacaktır; bu durumda, tüm içeriğin yerine sadece başlıkları ve özetleri indekslemeyi düşünmek veya özel bir veritabanı tabanlı indeksli arama çözümüne geçmek uygun olacaktır.

3. Adım: Aramaları normalleştirmek için yardımcı işlev (aksansız)

Kullanıcının nasıl yazdığına bakılmaksızın (aksanlı ya da aksansız, büyük ya da küçük harflerle) aramaların sonuç vermesi için bir temizleme işlevi tanımlanır:

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. Adım: posts_pre_query ile ana sorguyu durdurma

posts_pre_query filtresi aracılığıyla, WordPress’in arama sorgusu veritabanına ulaşmadan önce yakalanır. JSON dosyası okunur, aranan terimlerle eşleşen ID’ler bulunur ve yalnızca bu girişler, ilgili sayfalandırma bilgisi ile birlikte WordPress’e geri gönderilir.

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. Adım: Sonuçların doğrulanması

Yukarıdaki adımları tamamladıktan sonra, bir arama testi yapın ve Query Monitor'ü açın:

  1. Toplam veritabanı süresi: Gördüğünüz gibi, bu süre artık birkaç saniyeden sadece birkaç milisaniyeye (genellikle 0,03 saniye ile 0,2 saniye arasında) düşmüştür.
  2. SQL Sorguları: LIKE %terim% cümlesi tamamen ortadan kalkacaktır.
  3. RAM: Bellek kullanımının en yüksek seviyesi son derece düşük kalacaktır (yaklaşık 30 MB).
  4. Sıralama: Yazılar, WordPress’in kendi sayfa numaralandırma sistemine uygun olarak, tam olarak kronolojik azalan sırayla (en yenisinden en eskisine doğru) gösterilecektir.

Sonuçtan son derece memnunum. Yıllardır süren bir sorunu ortadan kaldırmakla kalmadım, şimdi de sonuç adeta muhteşem. "viñeta" kelimesi için 4887 sonuç gibi en zorlu aramalardan birinde bile arama motoru çok hızlı yanıt veriyor. Daha az sonuç içeren aramalarda ise sonuçların anında görüntülendiği söylenebilir. Başka herhangi bir şey aratarak bunu kendiniz de kontrol edebilirsiniz.

Statik bir JSON dizini kullanarak WordPress arama motorunu nasıl hızlandırabilirsiniz? 1

Yorum yapın

Söyleyeceğin bir şey var mı?

Este blog se aloja en LucusHost

LucusHost, el mejor hosting