Мало що краще показує «ми цінуємо ваш час», ніж поле пошуку, яке повертає результати після того, як ваша кава охолоне. На завантаженому сайті WordPress стандартний пошук може буквально тягнути базу даних крізь уламки: підстановки, гігантські таблиці й плагіни, що поводяться з wp_postmeta як з необмеженою кладовкою сміття.
Немає потреби в дорогому хостингу-пошуковому сервісі, щоб це виправити. Потрібно спочатку виміряти, а потім застосувати невелику низку нудних, але високоефективних змін: кращі запити, правильні індекси, розумне кешування й прибрати випадкову складність, яка накопичилася в період «отримай і відправ».
План швидкої діагностики
Якщо ви нічого більше не зробите — зробіть це по порядку. Мета — знайти вузьке місце за хвилини, а не витрачати день на «оптимізацію» не того шару.
1) Підтвердіть, що це дійсно пошук (а не PHP, мережа або плагін, що робить ще десять речей)
- Перевірте час запиту на краю (інструменти розробника в браузері або логи CDN).
- Перевірте журнал доступу сервера на наявність запитів з
?s=і їх час відповіді. - Якщо сторінка повільна, але час БД в нормі — проблема в рендерингу PHP, хуках плагінів або віддалених викликах.
2) Визначте шаблон запиту й чи використовує він індекси
- Увімкніть лог повільних запитів на короткий час (або використайте Performance Schema, якщо він вже є).
- Зловіть один репрезентативний повільний пошуковий запит.
- Запустіть
EXPLAINі подивіться на повні сканування таблиць, величезні оцінки «rows» і на «Using temporary; Using filesort».
3) Вирішіть: спочатку виправляйте запит/індекс, потім кеш, потім інфраструктуру
- Якщо
EXPLAINпоказує сканування мільйонів рядків: спочатку індексація і формування запиту. - Якщо запит БД швидкий, але сторінка повільна: об’єктне кешування, профайлер PHP, аудит плагінів.
- Якщо БД насичена (великий IO wait, пропуски буферного пулу): налаштуйте InnoDB + зменшіть «бруд»; лише потім думайте про залізо.
Перефразована ідея (приписують): Вернер Фогельс довго наполягав, що треба «виміряти, а потім оптимізувати», бо гадання — як оптимізувати не те.
Чому стандартний пошук WordPress повільний (що він насправді робить)
Стандартний пошук WordPress — це не «пошук» у класичному розумінні, а SQL-пошук за шаблоном. Ядро будує запит, який шукає термін у wp_posts.post_title та wp_posts.post_content, часто використовуючи умови LIKE. LIKE '%term%' — класична пастка продуктивності: ведучий підстановочний символ унеможливлює допомогу звичайних B-tree індексів. В результаті БД часто змушена сканувати велику кількість рядків.
І це ще до того, як підключаться плагіни. Багато «покращувачів пошуку» додають JOIN до wp_postmeta, щоб шукати користувацькі поля. wp_postmeta на багатьох сайтах величезний, і він структурований для гнучкості, а не для швидкості: це key-value сховище з довгими текстовими значеннями, наповнене автозавантаженими опціями й роками зайвих даних. Якщо ваш пошук торкається meta — ви перетворили проблему «скануй пости» на «скануй пости плюс склад».
Є ще сторона виводу: сторінки результатів пошуку тригерять шаблони, пов’язані пости, рекламу, персоналізацію, аналітику та іноді віддалені API-виклики. База даних може виявитися невинною, тоді як PHP виконує танці інтерпретації.
Правило зі шкалою думки: розглядайте повільний пошук як проблему бази даних, поки не доведете зворотне. Потім розглядайте як проблему кешування, поки не доведете, що кеш не допоможе.
Жарт №1: Стандартний пошук WordPress — як grep по PDF: технічно можливо, емоційно сумнівно.
Цікаві факти & історичний контекст
- Ядро пошуку WordPress навмисно просте. Воно віддає перевагу широкій сумісності над якістю пошуку, тому використовує SQL
LIKEзамість алгоритмів ранжування. - FULLTEXT індекси в MySQL раніше не були дружні до InnoDB. Старіші версії MySQL змушували багато сайтів переходити на MyISAM для FULLTEXT; сучасні MySQL/MariaDB підтримують FULLTEXT на InnoDB, що змінило компроміси.
- Проблема з wp_postmeta погіршилася разом з ростом сторінко-білдерів. Багато білдерів зберігають макети й блоки в meta. Пошук, який робить JOIN на meta, може стати катастрофою в уповільненому режимі.
- InnoDB став сховищним двигуном за замовчуванням у MySQL 5.5. Це зробило розмір буферного пулу й IO-шаблони центральними для роботи з продуктивністю WordPress.
- WordPress рано ввів хуки для персистентного об’єктного кешу. API є; багато сайтів просто ніколи його не підключили до Redis/Memcached.
- Стоп-слова й мінімальна довжина токенів мають значення. FULLTEXT ігнорує надто поширені слова й (за замовчуванням) короткі токени, що дивує команди при міграції зі наївного LIKE-пошуку.
- Зміна наборів символів змінила гру. Перехід на
utf8mb4збільшив розміри індексів; деякі «індекси, що вміщувалися раніше», перестали вміщуватися й продуктивність змінилася. - MariaDB та MySQL розійшлися шляхами. Поведінка FULLTEXT, рішення оптимізатора й значення за замовчуванням можуть відрізнятися; ваші поради з налаштування не завжди переносимі.
- Деякі хости вимикають логи повільних запитів на шарі shared. Тому для діагностики все ще корисні інструменти захоплення запитів на рівні додатка.
Спочатку виміряйте: доведіть, куди йде час
Ви збираєтеся робити реальну роботу. Це починається з даних: час запитів, кількість перевірених рядків і чи очікує база на CPU чи диск. Нижче практичні завдання для типового Linux + Nginx/Apache + MySQL/MariaDB сервера. Кожне завдання містить те, на що дивитися, і яке рішення воно підказує.
Завдання 1: Знайдіть повільні запити пошуку у журналі доступу веб-сервера
cr0x@server:~$ sudo awk '$7 ~ /\?s=|&s=/ {print $4,$5,$7,$9,$10}' /var/log/nginx/access.log | tail -n 20
[27/Dec/2025:09:10:12 +0000] GET /?s=backup 200 84123
[27/Dec/2025:09:11:08 +0000] GET /?s=pricing 200 90211
Що означає вивід: Ви вибірково переглядаєте останні пошуки і перевіряєте, що вони існують і не перенаправляються. Останній стовпець — розмір відповіді; вам все ще потрібен час.
Рішення: Якщо запити пошуку скупчуються навколо певних термінів або кінцевих точок (кастомний URL пошуку, маршрути плагіна для мультимовності), ймовірно, проблема в конкретному шаблоні або плагіні.
Завдання 2: Додайте час запиту в логи доступу (Nginx) та знайдіть повільні пошуки
Припускаючи, що ваш формат логів Nginx включає $request_time. Якщо ні — додайте його і перезавантажте; потім:
cr0x@server:~$ sudo awk '$7 ~ /\?s=|&s=/ && $NF > 1.5 {print $4,$5,$7,"t="$NF}' /var/log/nginx/access.log | tail -n 20
[27/Dec/2025:09:14:02 +0000] GET /?s=invoice t=2.113
[27/Dec/2025:09:14:55 +0000] GET /?s=api t=1.874
Що означає вивід: Запити пошуку, що перевищують 1.5 с. Поріг обирайте самі — візьміть те, що болить.
Рішення: Якщо час запиту високий, але пізніше час БД виглядає низьким — переключайтеся на профілювання PHP/плагінів.
Завдання 3: Підтвердіть, що база даних є «гарячою точкою» (оперативна перевірка IO/CPU)
cr0x@server:~$ vmstat 1 5
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
r b swpd free buff cache si so bi bo in cs us sy id wa st
2 0 0 432112 61264 912340 0 0 60 110 420 700 12 4 79 5 0
1 1 0 401988 61272 910120 0 0 2800 3400 600 900 18 6 54 22 0
Що означає вивід: Стовпець wa — очікування вводу/виводу. Стійко високий IO wait під час пошуку вказує на роботу бази, обмежену диском.
Рішення: Високе IO wait: потрібні індекси, зменшення «блоку» таблиць і налаштування буферного пулу до того, як ви додасте більше CPU.
Завдання 4: Перевірте поточне навантаження MySQL і топ-запит
cr0x@server:~$ mysql -e "SHOW FULL PROCESSLIST\G" | sed -n '1,120p'
*************************** 1. row ***************************
Id: 31284
User: wp
Host: 127.0.0.1:51614
db: wordpress
Command: Query
Time: 3
State: Sending data
Info: SELECT SQL_CALC_FOUND_ROWS wp_posts.ID FROM wp_posts WHERE 1=1 AND ((wp_posts.post_title LIKE '%invoice%') OR (wp_posts.post_content LIKE '%invoice%')) AND wp_posts.post_type IN ('post','page') AND (wp_posts.post_status = 'publish') ORDER BY wp_posts.post_date DESC LIMIT 0, 10
Що означає вивід: Ви спіймали реальний запит і стан. «Sending data» часто означає, що виконується сканування/повернення великої кількості рядків або сортування.
Рішення: Скопіюйте запит (санітизувавши значення) і виконайте EXPLAIN. Якщо відбувається повне сканування — змінюйте підхід до пошуку або додавайте стратегію індексування.
Завдання 5: Короткочасно увімкніть лог повільних запитів (MySQL/MariaDB)
cr0x@server:~$ mysql -e "SET GLOBAL slow_query_log = 'ON'; SET GLOBAL long_query_time = 0.5; SET GLOBAL log_queries_not_using_indexes = 'ON';"
Що означає вивід: Відсутність виводу означає, що команда виконалася. Шлях до логу повільних запитів залежить від конфігурації.
Рішення: Використовуйте це під час контрольованого вікна. Якщо не можете робити це в проді, зробіть в клоні зі схожими даними.
Завдання 6: Прочитайте лог повільних запитів у пошуках шаблонів
cr0x@server:~$ sudo tail -n 50 /var/log/mysql/mysql-slow.log
# Time: 2025-12-27T09:14:02.123456Z
# User@Host: wp[wp] @ localhost []
# Query_time: 2.107 Lock_time: 0.000 Rows_sent: 10 Rows_examined: 842311
SET timestamp=1766826842;
SELECT SQL_CALC_FOUND_ROWS wp_posts.ID FROM wp_posts WHERE 1=1 AND ((wp_posts.post_title LIKE '%invoice%') OR (wp_posts.post_content LIKE '%invoice%')) AND wp_posts.post_type IN ('post','page') AND (wp_posts.post_status = 'publish') ORDER BY wp_posts.post_date DESC LIMIT 0, 10;
Що означає вивід: 10 рядків повернуто, 842k перевірено. Це підпис сканування.
Рішення: Ви не «вирешите» це масштабуванням сервера. Потрібна індексна стратегія, що можна індексувати, або радикальне звуження множини кандидатів.
Завдання 7: Виконайте EXPLAIN для запиту і інтерпретуйте шкоду
cr0x@server:~$ mysql wordpress -e "EXPLAIN SELECT wp_posts.ID FROM wp_posts WHERE ((wp_posts.post_title LIKE '%invoice%') OR (wp_posts.post_content LIKE '%invoice%')) AND wp_posts.post_type IN ('post','page') AND (wp_posts.post_status = 'publish') ORDER BY wp_posts.post_date DESC LIMIT 10\G"
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: wp_posts
partitions: NULL
type: ALL
possible_keys: type_status_date
key: NULL
key_len: NULL
ref: NULL
rows: 921344
filtered: 2.50
Extra: Using where; Using filesort
Що означає вивід: type: ALL означає повне сканування таблиці. Using filesort означає, що MySQL сортує результати замість читання їх у порядку індекса.
Рішення: Або (a) перейти до FULLTEXT, (b) агресивно звузити пошук до індексованої підмножини, або (c) створити спеціальну таблицю індексу пошуку під вашим контролем.
Завдання 8: Перевірте розміри таблиць (знайдіть справжніх злочинців)
cr0x@server:~$ mysql -e "SELECT table_name, engine, table_rows, ROUND((data_length+index_length)/1024/1024,1) AS total_mb FROM information_schema.tables WHERE table_schema='wordpress' ORDER BY (data_length+index_length) DESC LIMIT 12;"
+----------------+--------+------------+----------+
| table_name | engine | table_rows | total_mb |
+----------------+--------+------------+----------+
| wp_postmeta | InnoDB | 18322104| 2860.4 |
| wp_posts | InnoDB | 921344| 640.2 |
| wp_options | InnoDB | 81234| 94.7 |
+----------------+--------+------------+----------+
Що означає вивід: Якщо wp_postmeta переважає всі інші — будь-який пошук, що торкається meta, буде дорогим, якщо ви його не переробите.
Рішення: Якщо meta величезна: перестаньте шукати її без розбору. Дозвольте лише конкретні ключі, зменшіть сміття або створіть окрему індексну таблицю.
Завдання 9: Знайдіть роздутий обсяг autoload-опцій (тиха вага сторінки)
cr0x@server:~$ mysql wordpress -e "SELECT option_name, LENGTH(option_value) AS bytes FROM wp_options WHERE autoload='yes' ORDER BY bytes DESC LIMIT 10;"
+----------------------------+---------+
| option_name | bytes |
+----------------------------+---------+
| some_builder_global_styles | 1948120 |
| plugin_cache_blob | 822114 |
| theme_mods_mytheme | 310992 |
+----------------------------+---------+
Що означає вивід: WordPress завантажує autoload опції на кожен запит — у тому числі й під час пошуку. Якщо це мегабайти — ви платите за це всюди.
Рішення: Зменшіть розмір autoload: виправте плагіни/теми, що зберігають великі бінарні блоби в autoload, або обережно змініть окремі опції на autoload='no' (і протестуйте).
Завдання 10: Перевірте стан буферного пулу InnoDB (читаємо з RAM чи з диска?)
cr0x@server:~$ mysql -e "SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_read%';"
+---------------------------------------+------------+
| Variable_name | Value |
+---------------------------------------+------------+
| Innodb_buffer_pool_read_requests | 1928831123 |
| Innodb_buffer_pool_reads | 18299312 |
+---------------------------------------+------------+
Що означає вивід: reads — фізичні читання; read_requests — логічні. Співвідношення показує, як часто буфер пропускає.
Рішення: Якщо пропуски часті під навантаженням — збільшіть innodb_buffer_pool_size (у межах ОЗП) і зменшіть робочий набір (блок і марні індекси).
Завдання 11: Захопіть профіль одного пошукового запиту з EXPLAIN ANALYZE (MySQL 8+)
cr0x@server:~$ mysql wordpress -e "EXPLAIN ANALYZE SELECT ID FROM wp_posts WHERE post_status='publish' AND post_type IN ('post','page') AND (post_title LIKE '%invoice%' OR post_content LIKE '%invoice%') ORDER BY post_date DESC LIMIT 10;"
+----------------------------------------------------------------------------------------------------------------------------------+
| EXPLAIN |
+----------------------------------------------------------------------------------------------------------------------------------+
| -> Limit: 10 row(s) (cost=... rows=10) (actual time=0.105..2107.331 rows=10 loops=1) |
| -> Sort: wp_posts.post_date DESC, limit input to 10 row(s) per chunk (actual time=0.104..2107.329 rows=10 loops=1) |
| -> Filter: ((wp_posts.post_title like '%invoice%') or (wp_posts.post_content like '%invoice%')) (rows=...) |
| -> Table scan on wp_posts (actual time=0.050..2001.002 rows=921344 loops=1) |
+----------------------------------------------------------------------------------------------------------------------------------+
Що означає вивід: Табличне сканування домінує. Це ваш курціний доказ із відмітками часу.
Рішення: Перестаньте займатися мікро-налаштуванням. Змініть механіку пошуку або звузьте сканування.
Завдання 12: Перевірте, чи активне об’єктне кешування на боці WordPress
cr0x@server:~$ wp --path=/var/www/html cache type
Default
Що означає вивід: «Default» зазвичай означає, що персистентний об’єктний кеш не підключений.
Рішення: Встановіть/налаштуйте Redis або Memcached для об’єктного кешування, якщо у вас динамічні сторінки й повторювані запити. Це не виправить табличне сканування, але може запобігти повторним проблемам.
Завдання 13: Підтвердіть доступність Redis (якщо ви його ввімкнули)
cr0x@server:~$ redis-cli ping
PONG
Що означає вивід: Redis живий.
Рішення: Якщо не отримуєте стабільний PONG — не будуйте на ньому план кешування пошуку.
Завдання 14: Проінспектуйте найважчі meta-ключі (цілі для очищення або виключення)
cr0x@server:~$ mysql wordpress -e "SELECT meta_key, COUNT(*) AS rows, ROUND(SUM(LENGTH(meta_value))/1024/1024,1) AS value_mb FROM wp_postmeta GROUP BY meta_key ORDER BY value_mb DESC LIMIT 10;"
+-----------------------+----------+----------+
| meta_key | rows | value_mb |
+-----------------------+----------+----------+
| _builder_data | 310221 | 940.3 |
| _some_plugin_cache | 901122 | 512.7 |
| _thumbnail_id | 610010 | 12.4 |
+-----------------------+----------+----------+
Що означає вивід: Кілька ключів часто складають більшість навантаження meta. Саме вони карають JOINи і призводять до частого оновлення кешу.
Рішення: Виключіть ці ключі з пошуку; розгляньте можливість перемістити їх з meta, якщо плагін це дозволяє; очищуйте застарілі кеш-blob’и.
Завдання 15: Перевірте тривалі DDL або блокування таблиць (рідко, але неприємно)
cr0x@server:~$ mysql -e "SHOW ENGINE INNODB STATUS\G" | sed -n '1,120p'
TRANSACTIONS
------------
Trx id counter 123456789
Purge done for trx's n:o < 123456700 undo n:o < 0 state: running
History list length 4123
...
Що означає вивід: Велика довжина списку історії може вказувати на довгі транзакції, що затримують purge і роздувають undo, що шкодить продуктивності.
Рішення: Якщо бачите це під час сповільнень — знайдіть довгі транзакції (часто це бекапи, аналітика або адмін-інструменти) і виправте їх.
Виправлення бази даних, які дають результат
1) Перестаньте вдавати, що LIKE — індексований підхід до пошуку
Якщо ваш поточний запит — LIKE '%term%' по великих текстових полях, база робить грубу роботу. Ви можете кинути на це ОЗП, але це як купувати швидшу доріжку, щоб хом’як біг ще інтенсивніше.
Є три прагматичні варіанти, що не вимагають дорогого зовнішнього сервісу:
- Використайте FULLTEXT індекси на
wp_postsдля пошуку заголовків/контенту. - Підтримуйте власну індексну таблицю пошуку (денормалізований «документ пошуку» на пост) і запитуйте її.
- Агресивно звужуйте множину кандидатів (типи постів, вік вікон, фільтри таксономій, розділення за мовою/сайтом) та приймайте, що це все ще LIKE-базований підхід.
2) Додайте FULLTEXT на пости (сучасні MySQL/MariaDB), потім адаптуйте запити WordPress
FULLTEXT — це відповідь «використайте базу даних, за яку ви вже платите». Це не ідеальний пошук, але набагато кращий за сканування всіх постів за кожне натискання клавіші.
На що звертати увагу: стоп-слова, мінімальний розмір токена, відсутність стеммінга (за замовчуванням), поведінка ранжування і те, що FULLTEXT не виконує підстроковий пошук. Користувачі, що шукають «inv», не знайдуть «invoice», якщо ви не обробите префіксну поведінку (часто fallback на LIKE для дуже коротких термінів).
cr0x@server:~$ mysql wordpress -e "ALTER TABLE wp_posts ADD FULLTEXT KEY ft_title_content (post_title, post_content);"
Що означає вивід: На великих таблицях це може зайняти час і IO. Якщо ваша версія блокує надто агресивно, плануйте технічне обслуговування або використайте online DDL, де це доступно.
Рішення: Якщо хостинг не дозволяє робити це онлайн — виконати у вікно низької активності або на репліці, яку промотуєте під час технічного вікна.
Тепер важлива частина: WordPress не почне автоматично використовувати MATCH ... AGAINST. Потрібен невеликий плагін або mu-plugin, який підключається до posts_search / posts_where і переписує умову пошуку на FULLTEXT, коли довжина терміну підходяща.
3) Побудуйте спеціальну індексну таблицю пошуку (нудно, керовано, швидко)
Якщо потрібно шукати вибрані meta-поля, витяги або назви таксономій — я прихильник створення невеликої бічної таблиці:
- Один рядок на пост (або на мовну версію), що містить попередньо зібраний текстовий «документ», який ви обираєте.
- FULLTEXT індекс на цьому блоці.
- Оновлення на
save_post, пакетне відтворення та простота розуміння.
Це дозволяє уникнути багатьох JOINів у час виконання. Ви платите вартість при записі, щоб купити швидкість при читанні. Це правильний компроміс для пошуку.
Приклад схеми (концептуально; налаштуйте під себе):
cr0x@server:~$ mysql wordpress -e "CREATE TABLE wp_search_index (post_id BIGINT UNSIGNED NOT NULL PRIMARY KEY, lang VARCHAR(12) NOT NULL DEFAULT 'en', doc LONGTEXT NOT NULL, updated_at DATETIME NOT NULL, FULLTEXT KEY ft_doc (doc), KEY lang_updated (lang, updated_at)) ENGINE=InnoDB;"
Що означає вивід: Таблицю створено, готова до наповнення.
Рішення: Якщо ви не можете безпечно міняти генерацію запитів WordPress, маршрутизируйте пошук на кастомну кінцеву точку, що запитує цю таблицю напряму (все ще всередині WordPress), а потім рендерить результати.
4) Якщо мусите шукати meta — робіть whitelist ключів і індексуйте відповідно
Пошук по всьому meta — найшвидший шлях розплавити базу й при цьому отримати сміттєві результати («button_color: #ffffff» не відповідає намірам користувача). Замість цього:
- Обирайте 3–10 meta-ключів, що дійсно мають значення (SKU, код товару, ім’я автора тощо).
- Переконайтеся, що ці ключі короткі, послідовні й не зберігають великі бінарні блоби.
- Додайте складений індекс, що допомагає вашій схемі JOINів.
cr0x@server:~$ mysql wordpress -e "ALTER TABLE wp_postmeta ADD INDEX meta_key_post_id (meta_key(191), post_id);"
Що означає вивід: Це допомагає запитам, які шукають записи за конкретним meta_key і потім приєднують по post_id. Це не зробить meta_value LIKE '%term%' миттєво швидким, але зменшить обсяг meta, який ви зачіпаєте.
Рішення: Якщо плагін шукає meta_value з ведучими підстановками — вам потрібна інша стратегія (індексна таблиця або FULLTEXT на куратованому документі з meta).
5) Прибрати SQL_CALC_FOUND_ROWS і дорогі схеми пагінації
Багато WordPress-запитів використовують SQL_CALC_FOUND_ROWS для підрахунку загальної кількості результатів для пагінації. Це зручно, але часто повільно, особливо з важкими фільтрами. Ви можете часто уникнути цього:
- Показуйте «Далі» без «Сторінка 1 з 500».
- Обмежте результати («показати топ 100») для пошуку — це нормально для UX.
- Підрахунки обчислюйте асинхронно або кешуйте.
Якщо тема вимагає загальних підрахунків, розгляньте кешування кількостей за сигнатурою запиту з коротким TTL.
6) Зменшіть «бруд»: ваші таблиці не просто великі, вони захломлені
Продуктивність пошуку сильно корелює з тим, скільки сміття запит має перетинати. Три джерела «блоку» найпоширеніші:
- Ревізії постів і автозбереження.
- Сирі meta від видалених постів або покинутих плагінів.
- Транзієнти і кеш-блоб’и, що залишилися плагінами.
Практичне очищення (обережно; зробіть бекап):
cr0x@server:~$ wp --path=/var/www/html post delete $(wp --path=/var/www/html post list --post_type='revision' --format=ids --posts_per_page=2000) --force
Success: Trashed post 1201.
Success: Trashed post 1202.
Що означає вивід: Ревізії видалені. Залежно від конфігурації WP CLI, це може «перемістити в кошик», а потім примусово видалити.
Рішення: Якщо ревізії потрібні з бізнес- причин — обмежте їх у конфігурації замість постійного видалення.
Перевірка сирої meta:
cr0x@server:~$ mysql wordpress -e "SELECT COUNT(*) AS orphan_meta FROM wp_postmeta pm LEFT JOIN wp_posts p ON p.ID = pm.post_id WHERE p.ID IS NULL;"
+-------------+
| orphan_meta |
+-------------+
| 421233 |
+-------------+
Що означає вивід: Meta рядки, що посилаються на відсутні пости. Це чисте марнотратство.
Рішення: Якщо це значно — заплануйте очищення в годину з низькою активністю і потім відслідковуйте, щоб знайти плагін, що це породжує.
7) Налаштуйте InnoDB розумно (не перетворюйте БД на виставку науки)
Для WordPress зазвичай важливі банальні параметри:
innodb_buffer_pool_size: достатній, щоб вміщати «гарячі» дані й індекси.innodb_log_file_size: не крихітний; уникайте постійного тиску на чекпоінти.- Швидке сховище і стабільні fs-параметри (особливо в хмарних томах).
Але пам’ятайте: налаштування допомагають тоді, коли план запиту розумний. Воно не виправить повне сканування, викликане ведучими підстановками.
Формування запитів: зменшуйте обсяг роботи, не «налаштовуйте» біль
1) Зробіть пошук менш глобальним
Багато сайтів WordPress трактують пошук як «шукати все». Це включає сторінки, які не повинні бути видимі користувачам, старі прес-релізи і кастомні типи постів, створені лише заради плагіна. Звуження області пошуку — не шахрайство, а продуктова ясність.
- Виключіть типи постів, що не орієнтовані на користувача.
- Виключіть attachment-пости, якщо у вас немає UX для пошуку медіатеки.
- Виключіть чернетки/приватні елементи, очевидно.
- Віддавайте перевагу пошуку по заголовку + витягу; включайте контент тільки якщо це необхідно.
2) Не сортуйте за датою, якщо це не потрібно
Сортування за post_date поширене, але якщо ви застосовуєте FULLTEXT, ви зазвичай хочете ранжування за релевантністю. Сортування за датою змушує виконувати додаткову роботу й може боротися з оптимізатором.
З FULLTEXT можна сортувати за оцінкою відповідності, а дату використовувати як додатковий критерій для випадків рівної оцінки. Якщо лишаєтеся з LIKE — сортування за датою після фільтрації дорого, оскільки фільтрація вже дорога.
3) Короткі запити не повинні запускати дорогі алгоритми пошуку
Одно- і двохсимвольні запити — це шум. Також вони дорогі, коли реалізовані через LIKE.
- Вимагайте мінімальної довжини запиту (наприклад, 3 символи) перед виконанням пошуку.
- Для коротких термінів показуйте пропозиції, популярні сторінки або повідомлення «введіть більше символів».
4) Уникайте «пошуку під час набору», який вдаряє по PHP + MySQL на кожен натиск клавіші
Автозаповнення — ок. Автозаповнення, що виконує повний WP-запит на кожен натиск клавіші — це самостійний DoS, який ви написали.
- Дебаунсуйте на клієнті (300–500 мс).
- Вимагайте мінімальної довжини.
- Кешуйте результати агресивно з коротким TTL.
Жарт №2: Пошук-під-час-набору без дебаунсу — це як питати вашу базу даних «ми вже приїхали?» кожні 100 мілісекунд.
Кешування пошуку без обману користувачів
Кешувати результати пошуку не тільки дозволено — це норма. Хитрість у тому, щоб кешувати так, щоб команда контенту не була в люті, коли нові пости не з’являються годинами.
1) Кешуйте за нормалізованою сигнатурою запиту
Нормалізуйте рядок пошуку (обрізайте, в нижній регістр, стискайте пробіли) і створіть ключ кеша, що включає:
- термін пошуку
- мову/ід сайту (для multisite)
- фільтр типу поста
- роль користувача, якщо результати різняться за правами (зазвичай не повинні)
- номер сторінки (або краще: курсорна пагінація)
TTL: 30–300 секунд для активних сайтів; 5–30 хв для майже статичних. Можна також робити інвалідизацію по save_post для релевантних типів постів.
2) Кешуйте дорогий етап, а не весь HTML
Кешування повних сторінок відмінно працює для анонімного трафіку, але пошукові терміни експоненційно збільшують простір ключів кеша. Замість цього:
- Кешуйте список ID відповідних постів.
- Потім отримуйте пости за ID (дешево, індекс-дружньо), або нехай це робить WP.
Так кеші будуть малі й стабільні, і ви уникнете кешування «шуму» шаблонів.
3) Правильно використовуйте персистентний об’єктний кеш (Redis/Memcached)
Об’єктний кеш WordPress допомагає при повторних запитах і завантаженні опцій. Він не виправить патологічний запит, але зменшить супутні навантаження й запобіжить «дог-пайлінгу» бази.
Коли ви підключаєте Redis:
- Слідкуйте за використанням пам’яті Redis і політикою витіснення.
- Не ставте надто довгі TTL, щоб не повертати застарілі результати для редакторської роботи.
- Не зберігайте гігантські блоби (деякі плагіни так роблять; їх треба обмежити).
Налаштування інфраструктури (те, що люди переоцінюють)
1) Розділяйте базу і веб лише якщо знаєте, що виправляєте
Винос БД з веб-сервера може допомогти з конкуренцією за ресурси, але додає мережеву латентність і операційну складність. Якщо ваш пошук робить повні сканування, переміщення бази просто розповсюджує проблему на більшу площу.
2) Сховище має значення: латентність важливіша за пропускну здатність для OLTP
Навантаження WordPress зазвичай — багато дрібних читань/записів, а не великі послідовні передачі. Для БД важлива низька латентність. Якщо ви на мережевих томах — розберіться з класом продуктивності і моделлю «burst vs sustained».
3) Репліки допомагають для звітності, а не для зламаних запитів пошуку
Репліки корисні для зняття навантаження з репортингу і бекапів. Але пошук — це частина трафіку користувача. Можна направляти читання пошуку на репліку, якщо ваша архітектура витримує затримку реплікації і вам не потрібна сильна консистентність. Багато сайтів контенту можуть дозволити це. E-commerce зазвичай — ні.
4) PHP-FPM не лиходій, але може підсилювати біль
Якщо виклики БД блокують, процеси PHP накопичуються, утворюються черги і Nginx починає таймаути. Переконайтеся, що у вас достатньо воркерів для звичайної роботи, але не «вирішуйте» повільний пошук подвоєнням воркерів — це дозволить більшій кількості одночасних запитів тиснути на базу.
Три міні-історії з корпоративного світу
Інцидент: хибне припущення («Це ж просто контент, не може бути таким великим»)
У середній контентній компанії латентність пошуку повільно зростала протягом року. Команда вважала, що причина — зростання трафіку. Вони масштабували веб-сервери, потім знову, бо панелі показували високий CPU на PHP під час піків пошуку.
Хибне припущення було тонким: вони вірили, що важка таблиця — це пости. Насправді плагін сторінко-білдера зберігав великі серіалізовані блоби в wp_postmeta, а «покращувач пошуку» робив JOIN на meta для кожного запиту — без whitelist ключів. До того ж UI пошуку виконував AJAX-пошук-під-час-набору без дебаунсу.
Коли вони увімкнули лог повільних запитів, схема стала очевидною: запити пошуку оглядали мільйони meta-рядків. CPU PHP був високий, бо воркери PHP чекали бази, а не через дорогий рендер. Показники навантаження брехали так, як вони люблять брехати.
Виправлення було не гламурним: відключили пошук по meta за замовчуванням, дозволили тільки два ключі, що дійсно важливі, додали мінімальну довжину запиту і дебаунс на клієнті. Також створили невелику індексну таблицю для дозволених meta і відтворювали її щонічно. Пошук став швидким. Білдер залишив свої блоби. Речі заспокоїлися.
Оптимізація, що вдарила у відповідь (кеш, що став самостійним інцидентом)
Маркетинговий сайт SaaS прагнув миттєвих результатів пошуку. Хтось запустив «кешувати все» підхід: кешувати повний HTML сторінок пошуку за рядком запиту з довгим TTL. У демо це виглядало геніально.
У проді це спричинило дві проблеми. По-перше, churn кеша: довгохвості запити створювали необмежену кількість ключів, виводячи дійсно корисні кеш-об’єкти. По-друге, коректність контенту: коли юристи просили видалити документ, він продовжував з’являтися у кешованих результатах до закінчення TTL.
Реакція на інцидент була незручна, бо це не типовий даунтайм. Сайт «працював», але подавав результати, які не мали з’являтися. Це той вид надійності, що приводить до нарад з людьми в дорогому взутті.
Команда відкотила повне кешування сторінок для пошуку і натомість кешувала лише список ID постів на короткий TTL з явною інвалідизацією при зміні контенту. Кеш перестав рости без меж, і запити «це має зникнути зараз» стали вирішуваними без очищення всього кешу.
Нудна, але правильна практика, що врятувала ситуацію (репліки + логи повільних запитів + контроль змін)
Внутрішня база знань працювала на WordPress. Пошук сповільнився після оновлення плагіна, прямо перед великим внутрішнім запуском. SRE на зміні зробив найменш захоплюючу річ: вони пішли за playbook.
Вони увімкнули лог повільних запитів на 15 хв, зловили найбільш проблемні запити і порівняли їх з базою за тиждень. Дельта показала нову схему запитів з JOIN на wp_postmeta для кожного пошуку, навіть коли жодні meta-поля не були налаштовані.
Оскільки була репліка — вони протестували додавання індексу й підправку запиту там перш ніж іти в прод. Оскільки був контроль змін — спланували DDL на низьку активність і повідомили про короткий ризик. У них був план відкату, тому вони не панікували, коли перша спроба зайняла більше часу, ніж очікувалося.
Результат був нудний: переключення налаштування плагіна, додавання індексу і невеликий mu-plugin для мінімальної довжини запиту. Жодних героїчних дій. Жодного war room. Просто менше людей скаржаться «пошук зламався» щоранку.
Поширені помилки: симптом → корінна причина → виправлення
1) Симптом: пошук повільний лише для популярних термінів
Корінна причина: популярні терміни збігаються з великою кількістю постів; запит сканує й сортує велику множину кандидатів; стоп-слова FULLTEXT також можуть змінювати поведінку.
Виправлення: використайте FULLTEXT з ранжуванням за релевантністю і розгляньте додаткові фільтри (тип поста, таксономія). Кешуйте часті запити з коротким TTL.
2) Симптом: латентність пошуку зростає під час редакторської активності
Корінна причина: autoload опції або інвалідизація об’єктного кешу приводить до трясіння; часті записи витісняють кеши; деякі плагіни виконують важку роботу на save_post.
Виправлення: аудитуйте autoload опції, ввімкніть персистентний об’єктний кеш і перенесіть дорогі індексні перебудови в асинхронні джоби/батчі.
3) Симптом: CPU бази даних високий, але додавання CPU мало допомагає
Корінна причина: повні сканування + сортування залежать від пам’яті/IO; оптимізатор не може використати індекси з ведучими підстановками.
Виправлення: змініть стратегію запиту (FULLTEXT або індексна таблиця). Налаштовуйте буферний пул тільки після того, як план запиту стане розумним.
4) Симптом: пошук швидкий для адміністраторів, повільний для анонімних (або навпаки)
Корінна причина: різні плагіни/хуки виконуються залежно від ролі; або кеш працює тільки для анонімів; або є логіка персоналізації.
Виправлення: порівняйте згенерований SQL і хуки; уніфікуйте логіку пошуку між ролями; кешуйте дані (ID), а не HTML за ролями, якщо це можливо.
5) Симптом: лише сторінка 2+ результатів повільна
Корінна причина: глибока пагінація через OFFSET (LIMIT 10000,10) змушує пропускати багато рядків.
Виправлення: обмежте глибину пагінації, використайте курсорну пагінацію або кешуйте результати й пагінуйте ID у пам’яті.
6) Симптом: повільно після встановлення «покращувача пошуку»
Корінна причина: плагін додає JOIN на meta і LIKE по meta_value, або додає JOINи таксономій без індексів.
Виправлення: налаштуйте його шукати лише в заголовку/контенті; зробіть whitelist meta-ключів; або замініть його на підхід з FULLTEXT.
7) Симптом: пошук викликає очікування блокувань
Корінна причина: не типово для SELECT, але може статися при довгих транзакціях, змінах схеми або тиску тимчасових таблиць.
Виправлення: знайдіть блокувальників (processlist, InnoDB status). Плануйте DDL, виправте довгі транзакції і перевірте наявність тимчасового простору.
8) Симптом: «випадкові» повільні пошуки після міграції хоста
Корінна причина: інша версія MySQL/MariaDB, змінені значення за замовчуванням, повільніше сховище або менший буферний пул.
Виправлення: порівняйте версії і конфігурації; базуйте показник hit rate буферного пулу; перевірте латентність сховища; перевірте плани запитів заново.
Контрольні списки / покроковий план
Фаза 0: Не ламаємо продакшн (30–60 хв)
- Переконайтеся, що маєте свіжий бекап (і що його можна відновити).
- Обирайте 15-хвилинне вікно для тимчасового включення логу повільних запитів, якщо це дозволено.
- Запишіть «що добре»: ціль p95 часу відповіді пошуку, прийнятний рівень застарівання кеша.
Фаза 1: Швидкі виграші (той самий день)
- Мінімальна довжина запиту: вимагайте 3+ символи перед виконанням пошуку.
- Дебаунс автозаповнення: 300–500 мс затримки на клієнті.
- Звуження області: обмежте типи постів; розгляньте виключення контенту, якщо заголовок/витяг достатні.
- Вимкніть пошук по meta за замовчуванням: дозволяйте тільки значущі ключі.
- Зменшіть autoload-блок: знайдіть основних винуватців і виправте налаштування плагінів/тем.
Фаза 2: Структурні виправлення (1–3 дні)
- Увімкніть видимість повільних запитів: логи повільних запитів або Performance Schema.
- Додайте FULLTEXT індекс: на
wp_postsабо на спеціальну індексну таблицю. - Перепишіть запити: використайте
MATCH ... AGAINST, коли можливо; залиште fallback для коротких термінів. - Кешуйте ID результатів: короткий TTL, нормалізація ключів, явна інвалідизація при публікації/оновленні.
Фаза 3: Закріплення й обслуговування (постійно)
- Регулярне очищення блоку: ревізії, сирі meta, транзієнти — автоматизуйте.
- Слідкуйте за регресіями: тримайте щотижневу базу повільних запитів; сповіщайте про зміни форми запитів, а не лише про CPU.
- Планування потужності: забезпечте, щоб буферний пул вміщував робочий набір; перевіряйте латентність сховища під навантаженням.
- Контроль змін: індекси й зміни плагінів тестуються в стаджингу і мають план rollout/rollback.
Операційні запобіжники (що я б вимагав)
- Жоден плагін не отримує права робити JOIN на
wp_postmetaу пошуку без письмового allowlist meta-ключів. - Жодна кінцева точка пошуку без мінімальної довжини запиту і rate limiting.
- Ніякого «кешувати все» без ревізії кардинальності ключів кеша.
- Будь-який DDL на великих таблицях потребує плану щодо блокувань, часу виконання і відкату.
FAQ
1) Чи можна зробити стандартний пошук WordPress швидким без зміни SQL?
Ви можете зробити його менш поганим, звузивши область пошуку (менше типів постів, виключити контент, мінімальна довжина) і кешуючи результати. Але якщо ви зберігаєте LIKE '%term%' по великих таблицях — ви фундаментально платите за сканування.
2) Чи допоможе індекс на post_title?
Не для ведучих підстановок LIKE. Індекс допоможе для LIKE 'term%' (префіксний пошук), але не для '%term%'. Саме тому існує FULLTEXT.
3) Чи достатній FULLTEXT для реальних користувачів?
Часто так для контентних сайтів і баз знань. Ви отримуєте швидкість і базову релевантність. Ви не отримаєте стеммінгу, синонімів, толерантності до помилок або кастомного ранжування без додаткової роботи — але багатьом сайтам цього й не треба, щоб перестати «боліти».
4) А пошук по PDF, вкладеннях або кастомних полях?
Не робіть JOIN на випадкові meta під час запиту й не сподівайтеся. Побудуйте куратований індекс (таблицю), що зберігає витяги тексту або вибрані поля, і застосуйте FULLTEXT до цієї таблиці.
5) Redis автоматично прискорить пошук?
Redis прискорює повторні звернення і зменшує шум у базі. Він не робить один патологічний запит швидким. Використовуйте його, щоб зменшити побічні витрати і кешувати ID результатів пошуку.
6) Чи безпечно очищувати wp_postmeta і ревізії?
Може бути безпечно, але також може зламати функціональність, якщо видалите дані, на які спирається плагін. Робіть бекап, починайте з сирих рядків і очевидних кеш-блобів, і тестуйте поведінку плагінів у стаджингу.
7) Чому сторінка 10 результатів така повільна?
OFFSET-пагінація змушує базу знаходити і відкинути багато рядків. Розгляньте обмеження сторінок, «Load more» з курсорами або кешування списку ID і нарізання його.
8) Спочатку чи переходити на більший сервер бази даних?
Тільки після того, як ви переконалися, що план запиту розумний. Більші сервери корисні для виконання більшої кількості роботи. Вони погано ховають те, що робиться невірно.
9) Спільний хостинг: що реально можна зробити?
Ви можете не мати контролю над конфігом MySQL або логами, але все одно можна: звузьте область пошуку, вимагаючи мінімальну довжину, відключіть пошук по meta, очистіть ревізії і налаштуйте кеш на рівні додатка. Якщо FULLTEXT дозволено — це найкращий «без зовнішнього сервісу» апгрейд.
10) Як зрозуміти, що виправлення спрацювало?
Виміряйте p95 часу відповіді пошуку, кількість перевірених рядків для репрезентативних запитів і CPU/IO wait під час навантаження. Якщо кількість перевірених рядків впала на порядок — ви справді виправили проблему.
Наступні кроки (практично, а не поетично)
Зробіть ці три речі цього тижня:
- Доведіть вузьке місце за допомогою логів повільних запитів і
EXPLAIN. Якщо ви скануєте сотні тисяч рядків на пошук — припиніть торгуватися з цим. - Обирайте реальну стратегію пошуку: FULLTEXT на
wp_postsдля простих потреб або спеціальна індексна таблиця, якщо треба включити вибрані meta/таксономії. - Стабілізуйте процес за допомогою правил: мінімальна довжина запиту, дебаунс автозаповнення, обмежені типи постів і кешування ID результатів з коротким TTL та адекватною інвалідизацією.
Якщо зробити все правильно, поле пошуку перестане бути інструментом навантаження і знову стане корисною функцією. Тиша — це мета.