MariaDB проти Elasticsearch для пошуку на сайті: коли пошуковий кластер обов’язковий

Було корисно?

Ваш пошук на сайті «працює» доти, доки не перестає: запуск продукту, вірусне посилання або один зацікавлений клієнт, який вводить
«wireless noise cancelling…», і CPU бази даних піднімається до максимуму. Далі — сторінки, що таймаутяться, релевантність
перетворюється на випадковість, і хтось пропонує кешувати все «поки не виправимо». Так інциденти отримують сувеніри.

Це практична межа між «MariaDB достатньо» і «вам потрібен пошуковий кластер». Не з ідеології, а з операційної точки зору:
хвостова латентність, перевіндексація, контроль ранжування, домени відмов і як діагностувати те, що справді повільно.

Справжнє питання: що ви оптимізуєте?

«MariaDB проти Elasticsearch» — оманлива постановка. У продакшені ви не просто обираєте технологію; ви обираєте, за які
режими відмов готові відповідати.

MariaDB чудова, коли пошук — це проблема відфільтрованого пошуку. Elasticsearch — коли пошук — це проблема мови та релевантності.
Коли вам одночасно потрібні швидкі фільтри, нечіткість, синоніми, автодоповнення, «мабуть ви мали на увазі», тонке налаштування ранжування,
мультипольове зважування — ви перейшли від «запитів до записів» до «системи витягування». Такі системи хочуть власної інфраструктури,
бо робота в них інша.

Ось правило для продакшну: використовуйте MariaDB для пошуку, коли запити передбачувані, обмежені й індексуються з високою селективністю.
Використовуйте Elasticsearch, коли треба обслуговувати намір, а не точні співпадіння, і коли потрібно контролювати релевантність без щотижневих переписувань SQL.

Пастка в тому, щоб думати, що Elasticsearch — «для масштабу», а MariaDB — «для малого». Це навпаки. Обидві системи масштабуються.
Elasticsearch потрібен для поведінок пошуку, які реляційні рушії не призначені виконувати дешево чи стабільно.

Цікаві факти й історичний контекст (коротко, корисно)

  • Lucene з’явився раніше за Elasticsearch. Основна бібліотека індексації/пошуку (Lucene) почалася в 1999 році; Elasticsearch (побудований на Lucene) з’явився близько 2010 року і упакував його для розподіленого використання.
  • BM25 став базовою моделлю релевантності. Lucene перейшов від класичного TF/IDF до BM25 як більш сучасної й налаштовуваної моделі оцінки. Це важливо, бо «релевантність» — це математика, не враження.
  • MySQL/MariaDB FULLTEXT створювався для документів, але з обмеженнями. Він корисний, але аналіз мови, контролі оцінки та композиції запитів обмежені в порівнянні з рушіями на базі Lucene.
  • InnoDB FULLTEXT з’явився пізніше, ніж багато хто пам’ятає. Історично FULLTEXT асоціювався з MyISAM; підтримка InnoDB з’явилася пізніше, і операційні очікування відставали від реальності.
  • Elasticsearch зробив популярним «майже в реальному часі» індексування. Концепція refresh interval (сегменти стають доступними після оновлення) — це свідомий компроміс між пропускною здатністю й актуальністю.
  • Розподілений пошук — це проблема координації. Складність не в індексації; складність у шардінгу, репліках, маршрутизації, злиттях сегментів і дуже людському бажанні змінювати mappings після релізу.
  • SQL LIKE став первородним гріхом пошуку на сайті. Люди досі відправляють його в продакшн, бо він працює в перший день. На тридцятий день приходить рахунок за CPU.
  • Пошукові апарати існували до хмарних «керованих пошуків». Підприємства використовували закриті пошукові коробки задовго до сучасних хостованих опцій; операційний біль не новий, просто перепакований.

Що може MariaDB для пошуку на сайті (і де вона тріщить)

MariaDB чудова, коли це справді пошук по індексу

MariaDB сяє, коли ваш «пошук» переважно:

  • Точні співпадіння (SKU, ID, електронна пошта, ім’я користувача).
  • Префіксні співпадіння, які можуть використовувати індекс (наприклад, нормалізована таблиця ключових слів).
  • Фільтрація/сортування по структурованих стовпцях (категорія, ціна, статус, дозволи).
  • Невеликі набори даних, де повні сканування все ще дешеві й передбачувані.

База дає сильну узгодженість, легкі JOIN-и, транзакції та єдину операційну поверхню.
Якщо ваш продукт потребує «знайти товари з color=blue і price < 50 і в наявності», MariaDB — дорослий у кімнаті.

FULLTEXT: корисний, але не прикидаймо його пошуковим рушієм

MariaDB FULLTEXT цілком може підтримати базовий пошук на сайті. Зазвичай це правильний вибір, коли:

  • У вас одна мова, обмежені потреби морфології і переважно буквальні співпадіння.
  • Ви готові до грубої релевантності і не плануєте щотижневе тонке налаштування ранжування.
  • Корпус не величезний, а конкурентність запитів помірна.
  • Вам не потрібні витончені аналізатори (синоніми, правила префіксів для полів, n-грами для автодоповнення).

Але FULLTEXT — це місце, де оптимізм йде на погодження бюджету, а потім вмирає в продакшні.
Тріщини з’являються в чотирьох місцях:

  1. Контроль ранжування: у вас менше важелів, і ці важелі менш композиційні. Якщо потрібно «збіги в заголовку переважають, якщо нещодавно було сильне співпадіння в тілі», доведеться будувати користувацьку логіку оцінки поза SQL.
  2. Аналіз мови: токенізація, стоп-слова, стемінг, синоніми — це не фокус реляційних рушіїв. Ви можете латати, але латати доведеться вічно.
  3. Операційна ізоляція: важке навантаження пошуку конкурує з OLTP-записами. Коли пошук спекає, ваш трафік оформлення замовлень не повинен цього зазнавати.
  4. Гнучкість запитів: нечіткість, толерантність до помилок, фразові запити, близькість і мультипольове бустингування можливі в деякій формі, але еволюцію їх болісно проводити без переписування.

Великий «смердючий» сигнал продакшну: ваша БД стає пошуковою коробкою

Якщо ви запускаєте пошук всередині MariaDB, ви прив’язуєте поведінку введення користувача до первинного сховища. Це означає:

  • Кожен натиск клавіші на ендпоїнті автодоповнення може стати DB-запитом.
  • Пошук може викликати повнотекстові запити, які погано кешуються.
  • Повільні запити крадуть буферний пул і CPU у транзакцій.

Тут команди кінчають «вирішувати» пошук, викидаючи читацькі репліки на нього. Це працює, поки не перестає, і переносить
відмови у відставання реплікації та непослідовні результати. Це не стратегія; це затягування.

Жарт №1: Якщо ви використовуєте LIKE '%term%' в продакшні, база даних рано чи пізно призведе до дзвінка з інцидентом.

Що насправді дає Elasticsearch

Це спеціалізований рушій витягування з певними компромісами

Elasticsearch — не «заміна базі даних». Це система індексації й витягування, оптимізована для:

  • Швидких повнотекстових запитів по великих корпусах.
  • Гнучкого DSL запитів: булева логіка, фрази/близькість, нечіткість, бустинг, ваги полів.
  • Пайплайнів аналізу тексту: токенізатори, фільтри, синоніми, стемінг, n-грами, аналізатори по полю.
  • Горизонтального масштабування з шардом і репліками.
  • Майже реального часу оновлень (refresh cycles), а не суворої транзакційної узгодженості.

Операційно ви купуєте ізоляцію й інші ручки

Причина запускати Elasticsearch у продакшені — ізоляція: навантаження пошуку має жити в окремому кластері з власним масштабуванням,
власним переліком властивостей продуктивності та власним радіусом ураження при відмові.

Elasticsearch дає вам ручки, яких немає в MariaDB:

  • Паралелізм на рівні шардів: роздача запитів по шард-ам і злиття результатів. Ви платите за координацію; виграєте пропускну здатність.
  • Управління життєвим циклом індексу: rollover, retention, злиття сегментів і tiering можна планувати, а не гадати.
  • Перевіндексація як перша операція: зміни схеми болючі, але екосистема очікує перевіндексацію і дає робочі процеси.
  • Швидкість ітерацій релевантності: змінювати аналізатори, бустинг, синоніми і логіку запитів можна без редизайну реляційної схеми.

Що ви також купуєте: нові режими відмов

Elasticsearch охоче дозволить створити кластер, який виглядає здоровим, але працює жахливо. Типові проблеми:

  • Вибух шардів: надто багато маленьких шардів, надмірний оверхед.
  • Тиск злиттів сегментів: пропускна здатність індексування падає через злиття.
  • Тиск на heap і паузи GC: хвостова латентність перетворюється на пилкоподібний графік.
  • Помилки mapping: поля проіндексовані неправильно, що призводить до дорогих запитів або зламаної релевантності.
  • Надто низький refresh_interval: «чому індексація така повільна?» — бо ви перетворили кожну секунду на коміт-паті.

І так, це ще один кластер. Ще набір знімків/резервних копій, оновлень і сюрпризів на кол-он-колл. Але якщо пошук — функція продукту, а не галочка,
кластер не опціональний. Це ціна ведення бізнесу.

Коли пошуковий кластер обов’язковий (тригери рішення)

«Обов’язковий» — сильне слово. Ось сильні, операційні тригери. Якщо у вас два або більше — припиніть дебати і починайте планувати кластер пошуку.

1) Автодоповнення та typeahead при реальному трафіку

Автодоповнення змінює профіль запитів: замість одного запиту за сесію ви отримуєте кілька за взаємодію користувача.
Запити короткі, з малою селективністю і важко кешуються. MariaDB може це робити, але робитиме гучно.

2) Релевантність — це KPI продукту, а не бажана опція

Якщо зацікавлені говорять про «якість результатів», «мерчендайзинг», «підсилити цю категорію», «понизити відсутні на складі»,
«синоніми» або «толерантність до помилок», ви в території пошукових рушіїв. Контроль релевантності FULLTEXT не місце, щоб витрачати кар’єру.

3) Багатомовність, морфологія або спеціалізована термінологія

Як тільки потрібні аналізатори на мову, кастомна токенізація (артикул, хімічні назви, правові посилання) або синоніми, що змінюються без деплоїв,
Elasticsearch стає практичним вибором.

4) Потрібні гібридні сигнали ранжування

Результати пошуку дедалі більше залежать від сигналів, як популярність, свіжість, клікабельність, персоналізація, наявність і бізнес-правила.
Elasticsearch може змішувати їх через function_score і структуровані фільтри без перетворення SQL у перформанс-арт.

5) Ваша OLTP-база — сакральний ресурс

Якщо MariaDB обробляє гроші, замовлення, аутентифікацію, інвентар або щось, що будить керівництво, ви хочете пошук поза нею.
Піки пошуку не повинні позбавляти транзакцій пропускної здатності.

6) Потрібна «достатня» актуальність, а не сувора узгодженість

Elasticsearch за дизайном — майже в реальному часі. Якщо «елементи стають доступними приблизно за ~1–30 секунд» — прийнятно, ви можете
від’єднати індексацію від записів. Якщо вам справді потрібна read-after-write узгодженість у пошуку, або дизайніть це явно
(гібридний шлях читання, підказки UI або синхронне індексування), або залишайтеся в базі і приймайте обмеження.

7) Потрібна безвідмовна еволюція схеми пошуку

Ви будете змінювати mappings/аналізатори. Якщо ваша організація не терпить простою пошуку під час міграції індексів,
ви хочете рушій з вбудованими workflow-ами перевіндексації та alias cutover.

Жарт №2: Пошуковий кластер — як друга кавова машина: ніхто не хоче її обслуговувати, але кожен помічає, коли вона зникла.

Розумний за замовчуванням: гібридна архітектура

Патерн: MariaDB як джерело правди, Elasticsearch як індекс, оптимізований для читання

У більшості реальних компаній найкраща відповідь — обидва:

  • MariaDB залишається джерелом правди для сутностей, дозволів, транзакцій і цілісності посилань.
  • Elasticsearch зберігає денормалізований документ на сутність, оптимізований для запитів (товар, стаття, тікет, оголошення).
  • Запити пошуку спрямовуються в Elasticsearch; результати повертають ID; додаток за потреби гідратує деталі з MariaDB.

Ця архітектура має практичну назву: приймати eventual consistency, але проектувати розрив.
Ви будуєте пайплайн індексації, моніторите відставання і вирішуєте, як UI поводиться, коли індекс відстає.

Пайплайн індексації: ваша надійність тут, а не в запиті

Пайплайн може бути:

  • CDC (change data capture) з binlog-ів MariaDB → стрім → індексер → Elasticsearch.
  • Патерн outbox: додаток записує сутність + подію в outbox в одній DB-транзакції; воркер споживає і індексує.
  • Періодичне пакетне перевіндексація для простіших систем з інкрементальними оновленнями, де можливо.

Патерн outbox нудний. Саме тому він працює. Він перетворює «чи ми індексували це?» в повторювану задачу з надійним журналом.

Ключові конструктивні рішення, що запобігають майбутньому болю

  • Незмінні версії індексів: створюйте products_v42, потім переключайте alias products_current. Ніколи не «редагуйте на місці», коли змінюються аналізатори.
  • Зберігайте поля джерела, потрібні для рендерингу: уникайте «штормів гідратації», коли кожен результат пошуку стає N запитів до бази.
  • Розділяйте типи запитів: індекс автодоповнення vs повний пошук vs індекс для перегляду категорій. Різні аналізатори, різні стратегії шардів.
  • Явний SLO актуальності: «99% оновлень доступні в пошуку протягом 30 с». Потім вимірюйте це.

Одна цитата, бо операції — це філософія з pager duty

«Надія — не стратегія.» — ген. Гордон Р. Салліван

Операційні завдання: команди, виводи, рішення (12+)

Це тип перевірок, які ви виконуєте, коли хтось каже «пошук повільний», і всі дивляться на базу. Кожне завдання включає раннобіжну команду, реалістичний фрагмент виводу, що це означає, і рішення.

Завдання 1: Визначити найважчі запити MariaDB за часом (Performance Schema)

cr0x@server:~$ mysql -e "SELECT DIGEST_TEXT, COUNT_STAR, ROUND(SUM_TIMER_WAIT/1e12,2) AS total_s, ROUND(AVG_TIMER_WAIT/1e12,4) AS avg_s FROM performance_schema.events_statements_summary_by_digest ORDER BY SUM_TIMER_WAIT DESC LIMIT 3\G"
*************************** 1. row ***************************
DIGEST_TEXT: SELECT * FROM products WHERE name LIKE ? ORDER BY updated_at DESC LIMIT ?
 COUNT_STAR: 248901
   total_s: 9123.55
     avg_s: 0.0367
*************************** 2. row ***************************
DIGEST_TEXT: SELECT id FROM products WHERE MATCH(title,body) AGAINST (? IN BOOLEAN MODE) LIMIT ?
 COUNT_STAR: 55432
   total_s: 2101.22
     avg_s: 0.0379

Що це означає: Ви витрачаєте години CPU на LIKE і FULLTEXT запити. Середня латентність виглядає прийнятною, але обсяг робить їх домінантними.

Рішення: Якщо це зовнішні кінцеві точки пошуку, ізолюйте їх. Як мінімум: припиніть використовувати LIKE із підстановками; розгляньте Elasticsearch, якщо потрібні релевантність/автодоповнення.

Завдання 2: Підтвердити, чи LIKE використовує індекс (EXPLAIN)

cr0x@server:~$ mysql -e "EXPLAIN SELECT * FROM products WHERE name LIKE '%headphones%' LIMIT 20\G"
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: products
         type: ALL
possible_keys: idx_products_name
          key: NULL
      key_len: NULL
          rows: 1843921
         Extra: Using where

Що це означає: Повне сканування таблиці. Ваш індекс на name марний при ведучій підстановці.

Рішення: Замініть на FULLTEXT, де це прийнятно, або перенесіть до Elasticsearch для підрядкового/толерантного до помилок пошуку.

Завдання 3: Перевірити buffer pool MariaDB та тиск читань

cr0x@server:~$ mysql -e "SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_reads'; SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_read_requests';"
+-------------------------+----------+
| Variable_name           | Value    |
+-------------------------+----------+
| Innodb_buffer_pool_reads| 18499231 |
+-------------------------+----------+
+----------------------------------+-------------+
| Variable_name                    | Value       |
+----------------------------------+-------------+
| Innodb_buffer_pool_read_requests | 83199231121 |
+----------------------------------+-------------+

Що це означає: Співвідношення не надто погане, але абсолютні читання з диска великі. Запити пошуку можуть руйнувати локальність кешу.

Рішення: Якщо OLTP страждає, ізолюйте пошук. Збільшення buffer pool допомагає, поки не перестане; краще вивести IO-патерни пошуку з основної БД.

Завдання 4: Виявити відставання репліки, якщо ви віддали пошук реплікам

cr0x@server:~$ mysql -e "SHOW SLAVE STATUS\G" | egrep "Seconds_Behind_Master|Slave_IO_Running|Slave_SQL_Running"
Slave_IO_Running: Yes
Slave_SQL_Running: Yes
Seconds_Behind_Master: 47

Що це означає: Репліка відстає приблизно на 47 секунд. Результати пошуку можуть бути застарілими, і читання можуть накопичуватися.

Рішення: Або явно прийміть застарілість (підказка в UX, модель eventual), або перестаньте використовувати репліки як crutch для пошуку. Це сильний сигнал для індексу пошуку.

Завдання 5: Виміряти поведінку MariaDB FULLTEXT (і чи селективний він)

cr0x@server:~$ mysql -e "EXPLAIN SELECT id FROM products WHERE MATCH(title,body) AGAINST ('wireless headphones' IN NATURAL LANGUAGE MODE) LIMIT 20\G"
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: products
         type: fulltext
possible_keys: ft_title_body
          key: ft_title_body
      key_len: 0
          rows: 120000
         Extra: Using where

Що це означає: FULLTEXT використовується, але очікує оглянути близько 120k рядків для цього запиту. Не страшно раз; страшно при масштабі.

Рішення: Якщо потрібен високий QPS, автодоповнення або множественні поля з бустингом — переходьте на Elasticsearch і проектуйте розмір шардів відповідно.

Завдання 6: Швидко перевірити стан кластера Elasticsearch

cr0x@server:~$ curl -s http://localhost:9200/_cluster/health?pretty
{
  "cluster_name" : "search-prod",
  "status" : "yellow",
  "timed_out" : false,
  "number_of_nodes" : 6,
  "number_of_data_nodes" : 4,
  "active_primary_shards" : 48,
  "active_shards" : 48,
  "unassigned_shards" : 48
}

Що це означає: Yellow зі незадіяними репліками. Ви можете обслуговувати читання, але у вас немає надмірності для багатьох шардів.

Рішення: Розглядайте як інцидент на надійність. Виправте розподіл (disk watermarks, втрату вузлів, кількість реплік) перед тонким налаштуванням продуктивності; інакше ви будете налаштовувати кульгавий кластер.

Завдання 7: Виявити вибух шардів (кількість і розміри)

cr0x@server:~$ curl -s http://localhost:9200/_cat/shards?v | head
index           shard prirep state   docs   store ip         node
products_v42    0     p      STARTED 812341 3.2gb 10.0.1.21  es-data-1
products_v42    0     r      UNASSIGNED
products_v42    1     p      STARTED 799002 3.1gb 10.0.1.22  es-data-2
products_v42    1     r      UNASSIGNED

Що це означає: У вас незадіяні репліки, а розмір шардів невеликий. Якщо сотні/тисячі таких шардів, оверхед переважатиме.

Рішення: Консолідуйте шарди (менше primary), виправте розподіл реплік і припиніть створювати один індекс на клієнта, якщо вам не подобається біль.

Завдання 8: Перевірити розподіл латентностей пошуку (не середнє)

cr0x@server:~$ curl -s http://localhost:9200/_nodes/stats/indices/search?pretty | egrep -A3 "query_total|query_time_in_millis"
"query_total" : 9012331,
"query_time_in_millis" : 81233122,
"query_current" : 37

Що це означає: Агреговані статі показують навантаження, а не хвостову латентність. Але високий query_current під навантаженням натякає на насичення.

Рішення: Корелюйте з p95/p99 застосунку і метриками відмов threadpool. Не оголошуйте перемогу по середньому.

Завдання 9: Перевірити відмови threadpool (класичний симптом «повільно»)

cr0x@server:~$ curl -s http://localhost:9200/_nodes/stats/thread_pool/search?pretty | egrep -A6 "\"search\"|rejected"
"search" : {
  "threads" : 13,
  "queue" : 1000,
  "active" : 13,
  "rejected" : 21844,
  "largest" : 13,
  "completed" : 99123312
}

Що це означає: Запити пошуку відкидаються. Користувачі бачать таймаути та переривчасті помилки.

Рішення: Масштабуйте кластер, зменшіть вартість запитів (фільтри, fielddata, глибока пагінація), додайте кешування або впровадьте обмеження. Rejected означає, що ви зараз за межами ємності.

Завдання 10: Виявити повільні запити Elasticsearch через slowlog

cr0x@server:~$ sudo tail -n 5 /var/log/elasticsearch/search-prod_index_search_slowlog.log
[2025-12-30T10:21:41,902][WARN ][index.search.slowlog.query] [es-data-2] [products_v42][1] took[2.8s], took_millis[2803], total_hits[200000], search_type[QUERY_THEN_FETCH], source[{"query":{"match":{"body":{"query":"wireless headphones","operator":"and"}}},"sort":["_score",{"updated_at":"desc"}],"from":0,"size":10000}]

Що це означає: Глибока пагінація (size:10000) плюс сортування — дорогі й часто зайві.

Рішення: Впровадьте search_after або обмежте size, повертайте менше результатів або використовуйте окремий endpoint для перегляду з попередньо обчисленими сортуваннями.

Завдання 11: Перевірити кількість сегментів і тиск злиттів

cr0x@server:~$ curl -s http://localhost:9200/_cat/segments/products_v42?v | head
index        shard prirep segment generation docs.count size  committed searchable version compound
products_v42 0     p      _0      0          5123      9.8mb true      true       9.11.1  true
products_v42 0     p      _1      1          4981      9.5mb true      true       9.11.1  true

Що це означає: Багато маленьких сегментів можуть вказувати на часті refresh-и та важкі злиття пізніше.

Рішення: Для важкого індексування збільшіть refresh_interval, групуйте оновлення або використовуйте bulk indexing. Дайте злиттям можливість «дихати».

Завдання 12: Підтвердити refresh_interval індексу (актуальність vs пропускна здатність)

cr0x@server:~$ curl -s http://localhost:9200/products_v42/_settings?pretty | egrep -A3 "refresh_interval"
        "refresh_interval" : "1s",
        "number_of_shards" : "12",
        "number_of_replicas" : "1"

Що це означає: 1s refresh агресивний. Чудово для актуальності, погано для пропускної здатності індексації та зносу сегментів.

Рішення: Якщо вам не потрібна секундна актуальність, перейдіть на 5–30 с і виміряйте покращення інжесту й запитів.

Завдання 13: Підтвердити помилки mapping, що викликають дорогі запити

cr0x@server:~$ curl -s http://localhost:9200/products_v42/_mapping?pretty | egrep -n "\"name\"|\"type\"|\"keyword\"" | head
34:        "name" : {
35:          "type" : "text"
36:        },
78:        "category" : {
79:          "type" : "text"
80:        }

Що це означає: category має тип text лише; фільтрація/агрегації по ньому будуть повільними або вимагатимуть fielddata (погані новини).

Рішення: Додайте підполя keyword для точних фільтрів/агрегацій. Заплануйте перевіндексацію з alias cutover.

Завдання 14: Виміряти відставання індексації (здоров’я пайплайну)

cr0x@server:~$ mysql -e "SELECT MAX(updated_at) AS db_latest, (SELECT MAX(indexed_at) FROM search_index_audit) AS indexed_latest\G"
*************************** 1. row ***************************
     db_latest: 2025-12-30 10:27:12
indexed_latest: 2025-12-30 10:26:02

Що це означає: Індекс відстає приблизно на 70 секунд від записів БД (за умови, що indexed_at відстежує успішні індекс-оновлення).

Рішення: Якщо ваш SLO актуальності — 30 с, це інцидент. Розслідуйте чергу індексера, помилки bulk або обмеження інжесту ES.

Завдання 15: Перевірити файлову систему та затримку диска на нодах даних

cr0x@server:~$ iostat -x 1 3
Device            r/s     w/s   rkB/s   wkB/s  await  svctm  %util
nvme0n1         210.3   98.2  8123.4  4312.8  18.42   0.62  97.8

Що це означає: Диск майже насичений з підвищеним await. Пошук і злиття — IO-ненажерливі.

Рішення: Масштабуйте пропускну здатність сховища (більше вузлів, швидші диски), зменшіть кількість шардів і уникайте важкого індексування під час пікових вікон запитів.

Швидкий план діагностики

Коли «пошук повільний», вам потрібна триетапна сортування: симптоми користувача, насичення бекенда, потім дизайн запиту/індексу.
Не починайте з гадань. Почніть зі звуження області.

Перший етап: підтвердіть, де витрачається час

  1. Метрики застосунку: p50/p95/p99 латентність для ендпоїнта пошуку та рівні помилок/таймаутів.
  2. Розбивка залежностей: час БД vs час ES vs час рендерингу застосунку. Якщо у вас немає трасування — додайте його. Ваші припущення коштують дорого.
  3. Конкуренція: скільки одночасних запитів накопичуються? Черги — тихий вбивця.

Якщо p50 нормальний, а p99 жахливий — у вас насичення, паузи GC, блокування або невеликий відсоток патологічних запитів.
Якщо все однаково повільно — у вас системні обмеження: диск, CPU або мережа.

Другий етап: перевірте очевидні сигнали насичення

  1. MariaDB: slow query log, CPU, пропуски InnoDB buffer pool, lock waits, відставання реплікації (якщо використовуєте репліки для пошуку).
  2. Elasticsearch: threadpool rejections, GC, використання heap, дискозавантаження/латентність, незадіяні шарди, гарячі вузли.
  3. Інфраструктура: IO вузлів (iostat), CPU steal (віртуалізація), мережеві падіння/затримки між аплікацією і нодами пошуку.

Третій етап: ізолюйте дорогі шаблони запитів

  1. Глибока пагінація (from/size) і сортування по неаналізованих полях.
  2. Запити з підстановками і ведучі підстановки в SQL.
  3. Агрегації з високою кардинальністю (ES) або неселективні предикати (SQL).
  4. Погані mapping-и, що змушують використовувати fielddata або scripting.
  5. Занадто агресивна актуальність (дуже низький refresh_interval) під час інтенсивного індексування.

Четвертий етап: вирішіть, це проблема ємності чи дизайну

Якщо є rejections threadpool, насичення диска або відставання реплік — у вас зараз проблема ємності.
Якщо запити фундаментально дорогі (підстановки, глибока пагінація, низька селективність) — це проблема дизайну, яку ємність лише приховає.

Три корпоративні міні-історії (реалістичні, анонімізовані)

Міні-історія 1: інцидент через хибне припущення

Середній маркетплейс запустив «підказки пошуку», бо продукт хотів, щоб UI здавався швидким. Інженери вибрали очевидний короткий шлях: запит MariaDB до таблиці products,
WHERE name LIKE 'term%', з індексом на name. Він працював у стенді. Працював і в проді — доти, доки був ранковий трафік.

Хибне припущення було в тому, що «префіксний пошук використовує індекс, отже він дешевий». Це лише напівправда. У проді користувачі вводили короткі префікси: «a», «b», «s».
Ці префікси відповідали великим діапазонам. Запит використовував індекс, так, але все одно обходив величезні його частини, робив сортування і повертав топ-N. Латентність росла. CPU зростав. Підключення накопичувалися.
Оформлення замовлень почало таймаутитися, бо воно ділило ту саму базу.

Інцидент-реакція була класичною: вертикальне масштабування БД, додавання репліки, кешування. Кеш допоміг для найпопулярніших префіксів, поки промоакції не змінили поведінку користувачів і кеш пропускав.
Репліка відставала, підказки стали непослідовними, і служба підтримки отримувала запити про «відсутні товари».

Виправлення не було героїчним. Вони винесли підказки в Elasticsearch з n-gram аналізатором. Додали rate-limit для підказок, дебаунс на фронтенді і жорсткий ліміт на кількість результатів.
MariaDB повернулася до своєї ролі транзакційного сховища. Урок був не в тому, що «Elasticsearch швидший». Урок був у тому, що форма введення користувача важить більше за визначення індексу.

Міні-історія 2: оптимізація, що відбилася бумерангом

Внутрішній портал документації використовував Elasticsearch і мав прийнятну латентність — поки інженер не «оптимізував релевантність». План:
збільшити поля в запиті (title, headings, body, tags), додати нечіткість, синоніми і підсилити свіжі документи.
Мета хороша. DSL запиту розрісся до маленької новели.

Вони вивели це перед масовим онбордінгом. Трафік пошуку зріс. Раптом p99 латентності став вимірюватися секундами, а не мілісекундами. Вузли були «здорові», але rejections в threadpool зросли.
Почали з’являтися паузи GC. Кластер не впав; він став ввічливо марний.

Відбійна реакція прийшла від кількох складених витрат: нечіткість по кількох полях, низький minimum_should_match і великий size, бо UI хотів показати «багато результатів» без пагінації.
Кожен запит розширився на багато терм-комбінацій і тягнув великі вікна результатів. CPU і heap зросли. Кеші запитів неефективні, бо кожен запит унікальний.

Відновлення полягало в тому, щоб трактувати фічі релевантності як бюджет. Вони обмежили нечіткість до коротких полів, посилили minimum_should_match, зменшили size,
прибрали дорогі сорти і ввели двофазний підхід: спочатку дешеве отримання кандидатів, потім опціональний Rerank для топ-N. Релевантність покращилася і кластер вижив.

Мораль: «покращення релевантності» може стати самостійною атакою відмови службою, яку ви написали. Робіть її вимірюваною й обмеженою.

Міні-історія 3: нудна, але правильна практика, що врятувала день

Рітейл-сайт працював у гібридному режимі: MariaDB — істина, Elasticsearch — для пошуку. Нічого складного. Але вони мали outbox-таблицю в MariaDB, яку обробляв невеликий фліт воркерів.
Кожне оновлення індексу було задачею. Кожна задача мала повтори. Кожна помилка потрапляла до dead-letter з достатнім контекстом для повторного відтворення.

Одного дня деплой вніс зміну mapping-а, що відкидав документи для підмножини продуктів — лише для тих з дивним атрибутом. Воркер почав повторювано падати на цих задачах.
Без захисту це перетворилося б на необмежений бекалог, потім інцидент «пошук не показує товари», потім довгу ніч.

Замість цього спрацювали нудні контролі. У воркера був circuit breaker: після N невдач завдання поміщалося в карантин.
Попередження про відставання індексації спрацювали, бо SLO було порушено. On-call миттєво бачив, які документи не пройшли і чому, бо outbox зберігав ID сутності та тип операції.

Вони відкотили зміну mapping-а, повторили карантинні задачі і повернулися до стійкого стану без розкопок у SQL.
Без геройств, без ручної археології. Просто журнал і цикл повторів.

Урок: ваш пайплайн індексації — розподілена система. Ставтеся до нього відповідно, навіть якщо це «лише пошук».

Типові помилки: симптом → корінь → виправлення

1) Симптом: пошук періодично таймаутиться під час спайків трафіку

Корінь: Пошук ділимося ресурсами MariaDB з OLTP; пул підключень насичується; повільні пошуки ставляться в чергу за записами.

Виправлення: Ізолюйте навантаження пошуку (Elasticsearch або окремий кластер БД), встановіть таймаути запитів і впровадьте rate limits для автодоповнення.

2) Симптом: «ми додали репліку і стало гірше»

Корінь: Відставання репліки викликає непослідовні результати; ретраї аплікації посилюють навантаження; очікування read-after-write ламається.

Виправлення: Припиніть використовувати репліки як двигун пошуку. Якщо мусите — явно прийміть застарілість у UX; інакше будуйте індекс.

3) Симптом: Elasticsearch «здоровий», але запити повільні

Корінь: Насичення threadpool, гарячі шарди або паузи GC; стан кластера не відображає продуктивність.

Виправлення: Перевірте відмови thread_pool, тиск heap, IO диска та розподіл шардів; масштаб або зменшуйте вартість запитів.

4) Симптом: фільтри/агрегації в Elasticsearch жахливо повільні

Корінь: Фільтрація по text-полях, відсутній keyword mapping; увімкнений fielddata, що експлодуює heap.

Виправлення: Додайте підполя keyword, перевіндексуйте і приберіть хитрощі з fielddata.

5) Симптом: індексація гальмує саме під час піків трафіку

Корінь: Дуже низький refresh_interval, важкі злиття та одночасне навантаження запитів; IO конкурує.

Виправлення: Збільшіть refresh_interval, пакетне bulk-індексування, плануйте важке перевіндексування в непіковий час і забезпечте headroom по IO для датанодів.

6) Симптом: «пошук не показує нові елементи»

Корінь: Відставання пайплайну індексації, збої задач або мовчазні відхилення документів через mapping.

Виправлення: Відстежуйте відставання явно, впровадьте dead-letter/quarantine, алертуйте про відхилені документи і надайте інструменти для повторного запуску.

7) Симптом: релевантність змінюється несподівано після деплою

Корінь: Зміни аналізаторів/mapping потребують перевіндексації; часткові зміни створюють змішану поведінку між сегментами і полями.

Виправлення: Використовуйте версійні індекси + alias cutover. Ніколи не «ремонтуйте шини, коли їдете», змінюючи аналізатори на місці.

8) Симптом: CPU MariaDB стрибає від пошуку, навіть з FULLTEXT

Корінь: Низька селективність запитів, висока конкурентність або змішане навантаження; FULLTEXT усе ще коштує CPU і пам’яті.

Виправлення: Виносьте пошук, або обмежуйте його (мінімальна довжина запиту, фільтри), додавайте кеш для частих запитів і обмежуйте конкурентність.

Контрольні списки / покроковий план

Покроково: вирішіть, чи Elasticsearch обов’язковий

  1. Перелічіть потрібні поведінки пошуку: автодоповнення, толерантність до помилок, синоніми, багатомовність, фразові запити, бустинг, кастомні сигнали ранжування.
  2. Виміряйте профіль запитів: QPS, p95/p99, конкуренція і чи генерують користувачі кілька запитів за взаємодію.
  3. Перевірте вплив на БД: чи виявляються запити пошуку серед найбільших споживачів часу? Чи страждають OLTP-ендпоїнти під час піків пошуку?
  4. Визначте вимоги до актуальності: чи прийнятні 1–30 секунд? Якщо так — роз’єднуйте індексацію. Якщо ні — плануйте гібридний шлях читання або перегляньте функціонал.
  5. Прийміть рішення про ізоляцію: якщо OLTP критичний для бізнесу, за замовчуванням ізолюйте пошук.
  6. Оцініть операційний бюджет: навички on-call, графік оновлень, стратегія знімків, планування ємності для шардів/heap/диска.

Покроково: реалізувати гібридну архітектуру пошуку без крихкості

  1. Визначте модель документів: денормалізуйте поля, потрібні для пошуку і для рендерингу сторінки результатів.
  2. Виберіть механізм індексації: outbox або CDC. Віддавайте перевагу outbox, якщо потрібна контрольована обробка помилок.
  3. Побудуйте ідемпотентність: індексування однієї й тієї ж сутності двічі має бути безпечним.
  4. Додайте таблиці аудиту/метрики: відстежуйте останній проіндексований timestamp і кількість помилок.
  5. Використовуйте версійні індекси: items_v1, items_v2 з alias items_current.
  6. План перевіндексації: фонова збірка, двійний запис (опціонально), потім alias cutover і виведення старого індексу.
  7. Захисні механізми: обмежуйте розміри результатів, тротлінгуйте автодоповнення, забороняйте глибоку пагінацію і встановлюйте таймаути запитів.
  8. UX при збої: якщо ES впав, вирішіть, показувати запасний варіант (обмежений DB-пошук) або дружнє повідомлення. Виберіть один і протестуйте його.

Операційний чекліст: щоб Elasticsearch не став будинком з привидами

  • Політика розміру шардів (уникати крихітних шардів; уникати надто великої кількості).
  • Дашборди для: відмов threadpool, використання heap, GC, затримки диска, швидкості індексування, часу refresh/merge.
  • Стратегія знімків і план відновлення з тестами.
  • Явні SLO: p95/p99 запитів і відставання індексації.
  • Runbooks для: незадіяних шардів, гарячих вузлів, відхилених документів, cutover перевіндексації.

Поширені запитання

1) Чи може MariaDB FULLTEXT замінити Elasticsearch для малого сайту?

Так, якщо «пошук» — це одномовна функція з низьким QPS, скромними очікуваннями релевантності і без автодоповнення.
Момент, коли продукт просить синоніми, нечіткість або мультипольовий бустинг — плануйте міграцію.

2) Чи Elasticsearch лише для «великих даних»?

Ні. Elasticsearch — для складності поведінки пошуку і ізоляції навантаження. Невеликі набори даних теж можуть його виправдати, якщо UX залежить від релевантності та typeahead.

3) Яка найбільша операційна різниця між пошуком в MariaDB і Elasticsearch?

Пошук в MariaDB конкурує з записами й транзакціями; пошук в Elasticsearch конкурує з індексуванням і злиттями.
В обох випадках ви керуєте контенцією — але в Elasticsearch ви можете ізолювати її від OLTP.

4) Чому б не додати більше реплік MariaDB для пошуку?

Репліки міняють CPU на відставання реплікації та операційну складність. Це підходить для масштабування читання передбачуваних запитів.
Запити пошуку часто непередбачувані і малоселективні, і репліки не вирішать обмежень релевантності.

5) Як обробляти дозволи (ACL) в Elasticsearch?

Або фільтруйте за полями дозволів під час запиту (документ містить tenant/org/visibility поля), або використовуйте індекси на рівні клієнта, якщо кількість клієнтів мала.
Уникайте індексів на рівні користувача, якщо не любите довгі вихідні дні. Для складних ACL подумайте про повернення кандидатів-IDs, а потім застосуйте дозволи на рівні застосунку.

6) Як перевіндексувати без простою?

Використовуйте версійні індекси та alias. Побудуйте items_v2, заповніть даними, потім атомарно перемістіть alias items_current з v1 на v2.
Тримайте v1 для швидкого відкату, поки впевненість не зросте.

7) Яка найпоширеніша помилка масштабування Elasticsearch?

Занадто багато шардів. Це підвищує вартість координації, накладні витрати пам’яті та час відновлення. Менше, правильно підібраних шардів краще за багато дрібних у більшості продакшн-настроювань.

8) Чи гідрувати результати пошуку з MariaDB чи повністю віддавати з Elasticsearch?

Якщо ви гідруєте кожен рядок результату з MariaDB, ризикуєте перетворити пошук у N+1 запитів під навантаженням.
Зберігайте достатньо полів в Elasticsearch для рендерингу сторінки результатів. Гідруйте тільки при кліку на деталі.

9) Чи можна використовувати Elasticsearch як систему запису?

Можна, але зазвичай не варто. Elasticsearch не оптимізований для реляційної цілісності та транзакційних робочих процесів.
Використовуйте його як похідний індекс, а правду тримайте в MariaDB або іншому транзакційному сховищі.

10) Яку актуальність реально очікувати від Elasticsearch?

Зазвичай секунди до десятків секунд, залежно від refresh_interval, навантаження індексації і дизайну пайплайну.
Якщо потрібна «миттєва доступність», ви або приймаєте вищу вартість індексації, або впроваджуєте гібридний шлях читання.

Висновок: наступні кроки, які можна виконати

Якщо ваш пошук на сайті по суті структурована фільтрація з випадковими ключовими збігами, MariaDB може справитися — особливо якщо зберігати запити селективними і уникати підстановок.
Але якщо пошук — продуктова функція з автодоповненням, толерантністю до помилок, налаштуванням релевантності і мультипольовим ранжуванням, пошуковий кластер — це не розкіш. Це правильна межа.

Практичні наступні кроки:

  1. Запустіть діагностику: визначте, чи MariaDB зараз оплачує рахунок за пошук (повільні запити, CPU, тиск на буфер).
  2. Прийміть рішення про ізоляцію: якщо OLTP і пошук ділять ресурси, відокремте їх до наступного спайку трафіку, який зробить це за вас.
  3. Спроектуйте пайплайн: виберіть outbox або CDC, визначте SLO актуальності і побудуйте повтори + карантин.
  4. Впровадьте версійні індекси: alias cutover — як уникнути драми перевіндексації і простоїв.
  5. Встановіть захисні механізми: обмежуйте вікна результатів, тротлінгуйте автодоповнення і забороняйте глибоку пагінацію в API-контрактах.

Будуйте пошук так, ніби ним буде користуватися людство в масштабі: нетерпляче, креативно і одночасно. Це не песимізм. Це операції.

← Попередня
WooCommerce: кошик зникає після перезавантаження — виправлення сесій і кешу, які працюють
Наступна →
Intelligent App Control (IAC) пояснено: охоронець запуску невідомих додатків у Windows

Залишити коментар