Ви випустили пошук. Користувачам він сподобався тиждень. Потім почали приходити тікети «пошук повільний», далі — «результати пошуку некоректні», і нарешті класика від керівництва: «Чи можемо ми просто зробити його схожим на Google?»
Саме тоді фінанси задають найнебезпечніше для операцій запитання: «Чи дешевше Elasticsearch, ніж робити це в Postgres?»
Чесна відповідь: «дешевше» залежить менше від прайсу і більше від того, за що ви підписуєтеся експлуатувати наступні три роки: рух даних, гігієна індексів, домени відмов і людські витрати на підтримку ще однієї розподіленої системи.
Рамка рішення: що ви насправді купуєте
PostgreSQL повнотекстовий пошук (FTS) і Elasticsearch не є суперниками в сенсі «всі функції в чекбоксі». Вони суперничають у сенсі «скільки сторінок о 3:00 ранку ви готові отримувати на місяць».
Обидва можуть відповісти «знайти документи, що відповідають цим термінам, з прийнятним ранжуванням». Витрати розходяться з часом, бо моделі експлуатації різні.
Що залишається дешевим у Postgres
- Менше рухомих частин. Та сама система бекапів, та сама стратегія HA, той самий стек спостережуваності, той самий календар оновлень.
- Транзакційна узгодженість за замовчуванням. Результати пошуку відображають зафіксовані записи без CDC-пайплайна або подвійного запису.
- Малі та середні корпуси з передбачуваними запитами. Каталоги продуктів, тікети, нотатки, записи CRM, внутрішні документи.
- Команди, що вже вміють працювати з Postgres. Використання навичок не є абстрактною перевагою; це рядок у бюджеті.
Що залишається дешевим в Elasticsearch
- Складна релевантність і аналітичні запити. Фасети, агрегації, фуззінг, синоніми, підсилення по полю, «чого ви мали на увазі» та масштабні експерименти з ранжуванням.
- Високий фановий розподіл запитів з вигодою кешування. Якщо ваш трафік читання значно перевищує записи, ES може бути ефективним запитним конячком — за умови налаштування.
- Великі текстові корпуси та мульти-орендна пошукова служба коли ви прийняли операційні витрати і правильно підібрали розмір шард.
- Організації, що вже добре експлуатують Elastic. Якщо у вас є платформи команда з перевіреними плейбуками, гранична вартість падає.
Питання «що дешевше» в довгостроковій перспективі насправді три питання
- Ви хочете одну систему запису чи дві? Пошукові кластери рідко є системами запису, отже потрібні пайплайни інгесту, бекфіли та звірки.
- Чи можете ви терпіти евентуальну узгодженість у результатах пошуку? Якщо ні, ви заплатите десь — зазвичай у складності.
- Хто відповідатиме за якість релевантності? Якщо «ніхто», Postgres FTS часто перемагає завдяки тому, що «достатньо добре» і стабільно.
Парафраз ідеї Вернера Вогельса (CTO Amazon): Все ламається постійно; проєктуйте системи, ураховуючи відмови.
Це не поезія. Це прогноз бюджету.
Цікавинки та коротка історія (бо це не з’явилося вчора)
- PostgreSQL FTS не новий. Основний повнотекстовий пошук з’явився в PostgreSQL 8.3 (2008), спираючись на ранні модулі tsearch.
- GIN-індекси змінили гру. Підтримка Generalized Inverted Index (GIN) зробила пошук токен→рядок практичним у масштабі для tsvector-даних.
- Elasticsearch пішов на хвилі Lucene. Lucene старший за Elasticsearch на десятиліття; ES упакував Lucene в розподілену службу з REST API та кластерними функціями.
- «Майже в реальному часі» — це буквально. Системи на Lucene оновлюють сегменти періодично; нові документи стають доступними після refresh, а не миттєво при commit.
- Ранжування в Postgres корениться в IR. ts_rank/ts_rank_cd реалізують класичні ідеї інформаційного пошуку; це не магія, але й не наївний пошук підрядка.
- За замовчуванням кількість шард в ES підставляла багато команд. Шарди не безкоштовні; надто багато шард збільшує накладні витрати та час відновлення.
- Vacuum — це важіль витрат. Блоут таблиць і індексів у Postgres може перетворити «дешевий пошук» на «чому наш диск зайнятий на 90%».
- Зміни мапінгу можуть бути дорогими в операціях. В ES багато змін мапінгу вимагають повного реіндексування; ви платите в I/O і часі.
- Синоніми — це організаційна проблема. У Postgres або ES списки синонімів стають продуктовою політикою — хтось має вести суперечки й ухвалювати рішення.
Жарт №1: Релевантність пошуку — як офісна кава: у всіх є думки, і ніхто не хоче обслуговувати апарат.
Довгострокова модель витрат: обчислення, зберігання, люди та ризик
Витратна категорія 1: обчислення та пам’ять
Postgres FTS зазвичай спалює CPU під час запитів (ранжування, фільтрація) і під час записів (підтримка GIN-індексів, тригери, згенеровані колонки). Він також дуже виграє від кешу: «гарячі» сторінки індексу в пам’яті мають значення.
Якщо ви запускаєте Postgres з «мінімумом RAM», то повнотекстові запити швидко перетворять випадкові I/O у спосіб життя.
Elasticsearch витрачає пам’ять на heap (метадані кластера, читачі сегментів, кеші) і широко використовує OS page cache для сегментів Lucene. Якщо недопропишете heap — отримаєте драму GC; якщо перебільшите heap — позбавите page cache. У будь-якому випадку у вас буде електронна таблиця.
Витратна категорія 2: ампліфікація зберігання
Саме на зберіганні довгострокова математика часто змінює відповідь. Пошукові індекси не компактні. Це навмисна надмірність, щоб зробити запити швидкими.
- Postgres: одна копія даних (плюс WAL) та один або кілька індексів. FTS додає зберігання tsvector і GIN (або GiST) індекс. Блоут — це тихий множник.
- Elasticsearch: одна копія source (якщо не вимкнути), структури інвертованого індексу, doc values для агрегацій і зазвичай щонайменше одна репліка. Це вже 2× до першого кліку. Снепшоти додають ще шар.
Якщо ви порівнюєте один екземпляр Postgres з кластером ES з репліками, ви порівнюєте не програмне забезпечення. Ви порівнюєте толерантність до ризику.
Витратна категорія 3: рух даних і бекфіли
Postgres FTS: дані вже тут. Ви додаєте колонку, будуєте індекс — і готово, поки не зміните конфіг токенізації або не додасте нове поле і не потрібно буде перерахувати вектори.
Elasticsearch: ви повинні інгестити. Це означає CDC (logical decoding), патерн outbox, стрімінговий пайплайн або подвійний запис. Це також означає бекфіли. Бекфіли трапляються в найгірший час: коли ви вже залежите від пошуку для доходу.
Витратна категорія 4: люди та процес
Elasticsearch у продакшні — це не «встановив і шукай». Це політики життєвого циклу індексів, розмір шард, refresh інтервали, mappings, analyzers та оновлення кластера, які не можна розглядати як просту бібліотечну залежність.
Postgres FTS теж не безкоштовний, але ви платите менший «податковий збір за нову систему».
Найдешевша система — та, яку ваш on-call може пояснити під тиском. Якщо ваша команда ніколи не робила rolling restart ES-кластера під час релокації шард і пікового трафіку, ви ще не заклали бюджет.
Витратна категорія 5: ризик і радіус ураження
Elasticsearch ізолює навантаження пошуку від вашої первинної БД. Це може знизити ризик, якщо запити пошуку важкі та непередбачувані.
Postgres тримає все разом: менше систем, але вища ймовірність, що поганий пошуковий запит перетвориться на інцидент бази даних.
Довгостроково дешево — це не «найнижчий щомісячний рахунок». Довгостроково дешево — це «найменша кількість міжкомандних інцидентів і вихідних з реіндексом у вихідні».
PostgreSQL повнотекстовий пошук: що він робить добре, а за що карає
Переваги (і чому вони залишаються перевагами)
Postgres FTS блискуче працює, коли пошук — це атрибут транзакційних даних, а не окремий продукт.
Ви можете зберегти модель даних простою: таблиця, колонка tsvector, GIN-індекс і запити з to_tsquery або plainto_tsquery.
- Узгодженість: В межах однієї транзакції ви можете атомарно оновити контент і вектор пошуку (згенеровані колонки або тригери).
- Операційна простота: Один бекап, одне відновлення. Одне місце для політик безпеки. Один набір контролю доступу.
- Чудово для «пошуку в додатку»: де UX — переважно «введи слова, отримай записи», з легкими потребами ранжування.
Штрафи (де ростуть витрати)
Postgres змусить вас заплатити за три гріхи: занадто великі рядки, високий обсяг записів і необмежені шаблони запитів.
- Ампліфікація записів: Оновлення tsvector та GIN-індексу може бути дорогим при великому обсязі записів.
- Блоут: Часті оновлення/видалення індексованих колонок можуть надути таблиці й GIN-індекси, збільшуючи I/O і сповільнюючи VACUUM.
- Потолок релевантності: Ви можете робити ваги, словники і налаштування, але ви не отримаєте інструментів ES для синонімів, per-field analyzers на великому масштабі та експериментів з релевантністю без власної розробки.
- Підводні камені мульти-орендності: Якщо ви робите «tenant_id AND tsquery» для тисяч орендарів, можливо, потрібні часткові індекси, партиціювання або і те, і інше.
Коли Postgres — дешевший довгостроковий вибір
- Ваш корпус пошуку менше десятків мільйонів рядків і документи можна тримати компактними.
- Ви можете обмежити запити (без leading wildcard, без «OR на все» запитів, що вибухають).
- Вам потрібна сильна узгодженість і прості операції більше, ніж найсучасніша релевантність.
- Ваша команда вже укомплектована для Postgres, а не для розподіленого пошуку.
Коли Postgres стає дорогим вибором
- Трафік пошуку настільки великий, що конкурує з OLTP-запитами, і ви не можете ізолювати його репліками.
- Вам потрібні тяжкі фасети/агрегації по багатьох полях на низькій затримці.
- Налаштування релевантності стає продуктовою відмінністю і потрібні швидкі ітерації, які SQL плюс власний код не забезпечують.
Elasticsearch: що він робить добре, а за що карає
Переваги
Elasticsearch побудований для пошуку. Він не соромиться попередньо обчислювати структури індексу, щоб зберігати низьку латентність запитів.
Також він спроєктований для горизонтального масштабування: додавайте ноди, ребалансуйте шард і продовжуйте роботу. На практиці «продовжувати роботу» — це те, за що ваші runbook-и отримують зарплату.
- Функції релевантності та UX: analyzers, token filters, синоніми, fuzziness, підсвічування, підсилення по полю, «more like this».
- Агрегації: фасети, гістограми, оцінки кардинальності і аналітичні запити, які Postgres може робити, але часто з іншим профілем витрат.
- Ізоляція: Ви можете тримати навантаження пошуку окремо від транзакційної бази даних.
- Історія масштабування: За правильного розміру шард ES може масштабувати читання та зберігання по нодах чисто.
Штрафи
Elasticsearch карає команди, які ставляться до нього як до чорної скриньки й дивуються, коли він поводиться як розподілена система.
Шарди — ось де ховаються проблеми.
- Накладні витрати шард: Надто багато шард витрачають heap, збільшують дескриптори файлів, уповільнюють оновлення стану кластера і подовжують відновлення.
- Податок реіндексування: Помилки мапінгу або зміни аналайзера часто вимагають повного реіндексування. Це час, I/O і операційний ризик.
- Евентуальна узгодженість: Потрібно керувати лагом інгесту, інтервалами refresh і тикетами «чому мій запис ще не пошуковий».
- Хореографія оновлень: Rolling upgrades можливі, але версії, плагіни і breaking changes вимагають дисципліни.
- Прихована зв’язність: Ваш застосунок, пайплайн інгесту, шаблони індексу, ILM і налаштування кластера стають одним великим організмом.
Жарт №2: Elasticsearch легкий, поки вам не треба, щоб він був надійним — тоді це курс з розподілених систем, на який ви не записувалися.
Коли Elasticsearch — дешевший довгостроковий вибір
- Пошук — ключова продуктова функція і вам потрібно швидко ітерувати релевантність.
- Ваші патерни запитів включають агрегації/фасети по багатьох полях з вимогами низької латентності.
- Ваш датасет настільки великий, що виділений пошуковий шар рятує OLTP-базу від перевантаження.
- У вас є (або ви профінансуєте) операційну зрілість: sizing, моніторинг, ILM, снапшоти та протестоване відновлення.
Коли Elasticsearch стає дорогим вибором
- У вас немає чистого сценарію інгесту і ви робите подвійний запис з неконсистентною поведінкою.
- Ви запускаєте надто багато шард «на всякий випадок» і платите за heap та CPU вічно.
- Ви ставитеся до реіндексування як до рідкісної події і робите його в піковий сезон.
Архітектурні патерни, що рятують від проблем
Патерн A: «Тільки Postgres» з розумними обмеженнями
Робіть це, якщо пошук вторинний. Використовуйте згенеровану колонку tsvector, GIN-індекс і прийміть, що ви будуєте «гарний внутрішній пошук», а не пошукову компанію.
Помістіть обмежувачі в API, щоб користувачі не могли генерувати патологічні запити.
- Використовуйте
websearch_to_tsqueryдля вводу від користувачів (кращий UX, менше сюрпризів). - Використовуйте ваги і невелику кількість полів; не засовуйте весь JSON-blob в один вектор, якщо ви цього не маєте на увазі.
- Розгляньте репліки для ізоляції трафіку пошуку.
Патерн B: Postgres як джерело істини + Elasticsearch як проекція
Це звичний «дорослий» патерн: OLTP в Postgres, пошук в ES. Вартість — у пайплайні.
Робіть це лише коли ви впевнено відповісте на «як ми відновимо ES з Postgres».
- Використовуйте outbox-таблицю і consumer для індексації змін.
- Проєктуйте операції індексації ідемпотентними.
- Плануйте бекфіли та еволюцію схеми (версійовані документи або аліаси індексів).
Патерн C: Двошарове рішення — дешевий дефолт, дорогі розширення
Тримайте більшість пошуку в Postgres. Маршрутизуйте лише розширені запити (фасети, фузі-матчинг, тяжке ранжування) в Elasticsearch.
Це зменшує навантаження на ES і тримає пайплайн компактнішим. Також створює «дві джерела правди для поведінки пошуку», тому дійте обдумано.
Жорстке правило: не робіть пошук шляхом запису
Якщо записи користувачів залежать від здоров’я ES, ви перетворили пошуковий кластер на критичну транзакційну залежність. Так інцидент пошуку стає інцидентом з доходами.
Тримайте шлях запису в Postgres; дозвольте ES відставати, а не блокувати.
Практичні завдання (команди), що означає вивід і яке рішення прийняти
Це ті перевірки, які перетворюють «думаю, що повільно» у «повільно через X, і ми можемо виправити це Y».
Команди — приклади, що можна запустити. Замініть імена БД і шляхи під своє середовище.
Завдання 1: Перевірити розміри індексів Postgres (чи їсть FTS ваш диск?)
cr0x@server:~$ psql -d appdb -c "\di+ public.*"
List of relations
Schema | Name | Type | Owner | Table | Persistence | Access method | Size | Description
--------+--------------------+-------+----------+-------------+-------------+---------------+--------+-------------
public | documents_fts_gin | index | app | documents | permanent | gin | 12 GB |
public | documents_pkey | index | app | documents | permanent | btree | 2 GB |
(2 rows)
Що це означає: Ваш GIN-індекс — найбільший. Це нормально — поки не перестає бути нормою.
Рішення: Якщо FTS-індекс домінує на диску, проаналізуйте, чи індексуєте занадто багато полів, занадто великий текст або страждаєте від блоуту через чур часті зміни. Якщо churn високий, сплануйте стратегію VACUUM/REINDEX.
Завдання 2: Переглянути індикатори блоуту таблиць і індексів (швидке наближення)
cr0x@server:~$ psql -d appdb -c "SELECT relname, n_live_tup, n_dead_tup, last_vacuum, last_autovacuum FROM pg_stat_user_tables ORDER BY n_dead_tup DESC LIMIT 5;"
relname | n_live_tup | n_dead_tup | last_vacuum | last_autovacuum
------------+------------+------------+--------------------+-------------------------
documents | 42000000 | 9800000 | | 2025-12-29 03:12:01+00
events | 180000000 | 1200000 | 2025-12-28 01:02:11| 2025-12-29 02:44:09+00
(2 rows)
Що це означає: Мертві кортежі високі. Autovacuum працює, але може не встигати.
Рішення: Налаштуйте autovacuum для гарячих таблиць або зменшіть churn оновлень індексованих текстових колонок. Якщо мертві кортежі ростуть, очікуйте різкі падіння продуктивності й неприємні сюрпризи з диском.
Завдання 3: Перевірити, чи ваш FTS-запит використовує GIN-індекс (EXPLAIN ANALYZE)
cr0x@server:~$ psql -d appdb -c "EXPLAIN (ANALYZE, BUFFERS) SELECT id FROM documents WHERE fts @@ websearch_to_tsquery('english','backup policy');"
QUERY PLAN
---------------------------------------------------------------------------------------------------------------
Bitmap Heap Scan on documents (cost=1234.00..56789.00 rows=1200 width=8) (actual time=45.210..62.118 rows=980 loops=1)
Recheck Cond: (fts @@ websearch_to_tsquery('english'::regconfig, 'backup policy'::text))
Heap Blocks: exact=8412
Buffers: shared hit=120 read=8410
-> Bitmap Index Scan on documents_fts_gin (cost=0.00..1233.70 rows=1200 width=0) (actual time=40.901..40.902 rows=980 loops=1)
Index Cond: (fts @@ websearch_to_tsquery('english'::regconfig, 'backup policy'::text))
Buffers: shared hit=10 read=2100
Planning Time: 0.322 ms
Execution Time: 62.543 ms
(10 rows)
Що це означає: Використовує GIN-індекс, але читає багато heap-блоків з диска.
Рішення: Якщо читання домінують, додайте RAM (кеш), зменшіть розмір результату фільтраціями або розгляньте стратегію покриття (зберігати менше великих колонок, мудро використовувати TOAST, розглянути денормалізацію лише для шляху пошуку).
Завдання 4: Перевірити ефективність кеша Postgres (чи ви I/O-забиндені?)
cr0x@server:~$ psql -d appdb -c "SELECT datname, blks_hit, blks_read, round(100.0*blks_hit/nullif(blks_hit+blks_read,0),2) AS hit_pct FROM pg_stat_database WHERE datname='appdb';"
datname | blks_hit | blks_read | hit_pct
--------+-----------+-----------+---------
appdb | 982341210 | 92341234 | 91.41
(1 row)
Що це означає: 91% хіт-рейтинг — це так себе, не відмінно для БД, що прагне низької латентності. Для FTS-вагових навантажень зазвичай хочеться ще більше.
Рішення: Якщо хіт-рейтинг падає під час піку пошуку, ви платите за cloud I/O у вигляді латентності. Розгляньте більше RAM, кращу індексацію або винос навантаження пошуку з primary (репліка або ES).
Завдання 5: Знайти найповільніші FTS-запити (pg_stat_statements)
cr0x@server:~$ psql -d appdb -c "SELECT mean_exec_time, calls, rows, query FROM pg_stat_statements WHERE query ILIKE '%tsquery%' ORDER BY mean_exec_time DESC LIMIT 3;"
mean_exec_time | calls | rows | query
----------------+-------+------+-----------------------------------------------------------
812.44 | 1200 | 100 | SELECT ... WHERE fts @@ websearch_to_tsquery($1,$2) ORDER BY ...
244.10 | 8400 | 20 | SELECT ... WHERE fts @@ plainto_tsquery($1,$2) AND tenant_id=$3
(2 rows)
Що це означає: Ваші найповільніші пошуки тепер очевидні, а не міфічні.
Рішення: Додавайте LIMIT, звужуйте фільтри, коригуйте стратегію ранжування/сортування, або попередньо обчислюйте поля ранжування. Якщо повільні запити — «глобальний пошук по всьому», розгляньте ES.
Завдання 6: Перевірити налаштування autovacuum для гарячої таблиці (чи ви вакууми late?)
cr0x@server:~$ psql -d appdb -c "SELECT relname, reloptions FROM pg_class JOIN pg_namespace n ON n.oid=relnamespace WHERE n.nspname='public' AND relname='documents';"
relname | reloptions
-----------+---------------------------------------------
documents | {autovacuum_vacuum_scale_factor=0.02}
(1 row)
Що це означає: Хтось вже знизив vacuum scale factor (добре).
Рішення: Якщо блоут усе ще високий, збільште кількість autovacuum workers, відрегулюйте cost limits або призначте періодичний REINDEX/pg_repack (з контролем змін).
Завдання 7: Виміряти обсяг WAL (чи індексування пошуку інфлюує вартість записів?)
cr0x@server:~$ psql -d appdb -c "SELECT pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(),'0/0')) AS wal_since_boot;"
wal_since_boot
----------------
684 GB
(1 row)
Що це означає: Обсяг WAL великий; може бути нормою для зайнятої системи, або ознакою того, що індексований текст сильно churn-иться.
Рішення: Якщо ріст WAL корелює з оновленнями тексту, зменшіть частоту оновлень, уникайте перезапису цілих документів або винесіть проекцію пошуку в ES.
Завдання 8: Перевірити здоров’я кластера 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" : 128,
"active_shards" : 256,
"unassigned_shards" : 12
}
Що це означає: Yellow означає, що primary-ші алоковані, але репліки — ні. Це знижує надмірність і може стати проблемою продуктивності під час відновлення.
Рішення: Якщо невіддані репліки зберігаються, виправте проблеми алокації перед масштабуванням трафіку. Не приймайте yellow як «нормально», якщо у вас немає явного дозволу на ризик.
Завдання 9: Порахуйте шарди на вузол (чи ви платите за шардовий податок?)
cr0x@server:~$ curl -s http://localhost:9200/_cat/shards?v
index shard prirep state docs store ip node
docs-v7 0 p STARTED 912341 18gb 10.0.0.21 data-1
docs-v7 0 r STARTED 912341 18gb 10.0.0.22 data-2
docs-v7 1 p STARTED 901122 17gb 10.0.0.23 data-3
docs-v7 1 r STARTED 901122 17gb 10.0.0.24 data-4
...output truncated...
Що це означає: Ви бачите розміри сховища по шард і розміщення.
Рішення: Якщо бачите сотні чи тисячі маленьких шард (менше 1 ГБ), консолідуйте (менше primary шард на індекс, політика rollover, реіндекс). Шарди — фіксовані накладні витрати; платіть за це один раз, а не вічно.
Завдання 10: Перевірити тиск JVM heap (чи GC — ваша прихована латентність?)
cr0x@server:~$ curl -s http://localhost:9200/_nodes/stats/jvm?pretty | head -n 25
{
"cluster_name" : "search-prod",
"nodes" : {
"q1w2e3" : {
"name" : "data-1",
"jvm" : {
"mem" : {
"heap_used_in_bytes" : 21474836480,
"heap_max_in_bytes" : 25769803776
},
"gc" : {
"collectors" : {
"young" : { "collection_count" : 124234, "collection_time_in_millis" : 982341 },
"old" : { "collection_count" : 231, "collection_time_in_millis" : 412341 }
}
}
Що це означає: Використання heap приблизно 83% від максимуму, і кількість old GC не тривіальна.
Рішення: Якщо heap сидить високо з частими old GC, зменшіть кількість шард, відрегулюйте кеші або змініть розмір heap (обережно). «Додати heap» не завжди вирішує проблему; це може зменшити OS cache і нашкодити.
Завдання 11: Перевірити тиск індексації через refresh і merge активність (чи ви I/O-забиндені через записи?)
cr0x@server:~$ curl -s http://localhost:9200/_nodes/stats/indices/refresh,merges?pretty | head -n 40
{
"nodes" : {
"q1w2e3" : {
"indices" : {
"refresh" : {
"total" : 882341,
"total_time_in_millis" : 9123412
},
"merges" : {
"current" : 12,
"current_docs" : 402341,
"total_time_in_millis" : 22123412
}
Що це означає: Великий час merge і поточні merges вказують на інтенсивну активність записів/сегментів, часто I/O-забиндену.
Рішення: Якщо індексація конкурує з латентністю пошуку, налаштуйте refresh інтервали, обмежте швидкість інгесту або ізолюйте hot ingest індекси. Не «оптимізуйте» шляхом бездумного вимкнення refresh; ви просто перемістите проблему.
Завдання 12: Перевірити позицію реплік і снапшотів (яка ваша історія відновлення?)
cr0x@server:~$ curl -s http://localhost:9200/_cat/indices?v
health status index uuid pri rep docs.count store.size
yellow open docs-v7 xYz 16 1 18000000 320gb
Що це означає: Yellow плюс rep=1 свідчить, що репліки існують, але не повністю алоковані (або вам бракує нод).
Рішення: Якщо ви покладаєтеся на репліки для HA, зробіть стан green або зменшіть кількість реплік з явним обміном. Також переконайтеся, що у вас є снапшоти і ви тестували відновлення; репліки — не заміна бекапу.
Завдання 13: Виміряти відставання інгесту (ES «відстає» від Postgres?)
cr0x@server:~$ psql -d appdb -c "SELECT now() - max(updated_at) AS db_freshness FROM documents;"
db_freshness
--------------
00:00:03.421
(1 row)
cr0x@server:~$ curl -s http://localhost:9200/docs-v7/_search -H 'Content-Type: application/json' -d '{"size":1,"sort":[{"updated_at":"desc"}],"_source":["updated_at"]}' | jq -r '.hits.hits[0]._source.updated_at'
2025-12-30T10:41:12Z
Що це означає: Якщо max(updated_at) у Postgres новіше, ніж найновіший timestamp в ES, ваш пайплайн відстає.
Рішення: Вирішіть, чи прийнятна евентуальна узгодженість. Якщо ні, виправте throughput інгесту, додайте backpressure або маршрутуйте «критичні за свіжістю» запити в Postgres.
Завдання 14: Вибірка латентності Postgres vs ES (припиніть вгадувати)
cr0x@server:~$ time psql -d appdb -c "SELECT count(*) FROM documents WHERE fts @@ websearch_to_tsquery('english','error budget');"
count
-------
1242
(1 row)
real 0m0.219s
user 0m0.010s
sys 0m0.005s
cr0x@server:~$ time curl -s http://localhost:9200/docs-v7/_search -H 'Content-Type: application/json' -d '{"query":{"match":{"body":"error budget"}},"size":0}' | jq '.took'
37
real 0m0.061s
user 0m0.012s
sys 0m0.004s
Що це означає: Поле «took» в ES 37 мс і end-to-end приблизно 61 мс; Postgres тут ≈219 мс.
Рішення: Якщо ES стабільно швидший і пайплайн здоровий, ES може бути дешевшим з точки зору UX — але дорожчим в операціях. Якщо Postgres «достатньо швидкий», не купуйте додатковий кластер, щоб зекономити 150 мс.
Завдання 15: Перевірити IO-навантаження Linux (універсальний ворог)
cr0x@server:~$ iostat -xz 1 3
Linux 6.5.0 (db-1) 12/30/2025 _x86_64_ (16 CPU)
avg-cpu: %user %nice %system %iowait %steal %idle
12.1 0.0 4.3 18.7 0.0 64.9
Device r/s w/s rkB/s wkB/s await svctm %util
nvme0n1 820.0 410.0 81200 32100 14.2 0.9 98.7
Що це означає: %util ≈99% і await ≈14 мс: сховище насичене.
Рішення: Перед переробкою запитів вирішіть проблему I/O: швидші диски, більше пам’яті, менше блоуту, кращий кеш або винесіть навантаження пошуку. Насичені диски перетворюють «нормальні» системи на фабрики пейджів.
Швидкий плейбук діагностики: знайти вузьке місце за хвилини
По-перше: вирішіть, чи це обчислення, I/O або координація
- Перевірте end-to-end латентність. Метрики застосунку: p50/p95/p99 для endpoints пошуку. Якщо p99 поганий — фокусуйтеся на ньому.
- Перевірте насичення I/O системи.
iostat -xzна DB/пошукових нодах. Високий await і %util — ваш сигнал «зупиніть усе». - Перевірте CPU і пам’ять. Якщо CPU навантажений, а I/O в порядку — ви compute-bound (ранжування, merges, GC або забагато парсингу JSON).
По-друге: ізолюйте, де витрачається час
- Postgres: використовуйте
EXPLAIN (ANALYZE, BUFFERS). Якщо buffers показують багато читань — це I/O/кеш. Якщо це CPU — ви побачите високий час з переважними hits. - Elasticsearch: порівняйте
tookі клієнтський час. Якщо «took» низький, але клієнтський час високий — у вас мережа, load balancer, TLS або черги threadpool. - Пайплайн: перевірте відставання інгесту. «Пошук неправильний» часто означає «ES відстає».
По-третє: оберіть найменший безпечний важіль
- Обмежуйте запити. Додавайте LIMIT, фільтри, прибирайте патологічні опції запитів.
- Виправляйте блоут/шарди. Vacuum/reindex (Postgres) або консолідуйте шард/налаштуйте ILM (ES).
- Додавайте апаратні ресурси тільки після цього. RAM допомагає обом системам, але це найдорожчий пластир, коли дизайн ламається.
Три корпоративні міні-історії з поля
Інцидент через хибні припущення: «Пошук евентуально узгоджений, але користувачі не помітять»
Середня B2B SaaS-компанія додала Elasticsearch, щоб покращити пошук по тікетах клієнтів. Вони підключили CDC-стрім з Postgres в сервіс індексації і запустили.
На початку метрики виглядали добре: нижчий p95 латентності, щасливіша підтримка, менше спайків БД.
Потім настала аудиторська тиждень. Працівники підтримки шукали щойно створені тікети і не бачили їх. Вони створювали тікети повторно, дублювали вкладення і ескалували в інженерів.
Інженери вважали, що «refresh interval» — дрібний налаштувальний параметр. Раніше вони збільшили його, щоб знизити накладні витрати індексації під час навантажувального тесту.
Під час аудиту обсяг тікетів зріс. Ingest lag збільшився, індексер відстав, а refresh приховував нові документи довше.
Режим відмови був не «Elasticsearch лежить». Було гірше: система працювала і повертала правдоподібно-але-неправильні результати.
Виправлення було операційним, а не філософським: ужорсточити SLO щодо свіжості, явно вимірювати lag і маршрутувати «щойно створені» перегляди в Postgres на короткий період.
Вони також додали банер «пошук може відобразити зміни з затримкою N секунд» для внутрішніх робочих процесів — нудний, чесний UX, що запобігав дублюванню роботи.
Оптимізація, що відбилася боком: «Давайте зменшимо навантаження Postgres, індексуючи все раз»
Інша компанія поклалася на Postgres FTS для бази знань. Спочатку все працювало, аж поки продуктова команда не попросила багатше ранжування.
Інженер вирішив попередньо обчислити монстроподібний tsvector, що включав title, body, теги, коментарі та витягнений текст PDF — усе.
Латентність запитів спочатку покращилась, бо менше join-ів і менше обчислень потрібно на запит. Команда відсвяткувала і перейшла далі.
Через два місяці база даних почала рости швидше, ніж очікувалося. Autovacuum відстав. I/O виріс. Репліки почали відставати.
«Оптимізація» збільшила ампліфікацію записів. Будь-яке редагування будь-якої частини документа переписувало більший рядок і оновлювало більший набір GIN-індексів.
Система не впала гучно. Вона падала повільно: зростання p95, випадкові timeouts, а потім вихідні з екстреним налаштуванням vacuum і розширенням диска.
Вони відновилися, зменшивши те, що індексували; розділили рідко змінюваний витягнений текст у окрему таблицю і оновлювали її вектор лише коли цей текст змінювався.
Урок: у Postgres можна купити швидкість запиту за рахунок вартості записів. Якщо ви не вимірюєте вартість записів, вона виставить вам рахунок пізніше.
Нудна, але правильна практика, що врятувала день: «Перебудовуваний пошук і протестоване відновлення»
Одна фінтех-компанія експлуатувала і Postgres, і Elasticsearch. Пошук не був системою запису, але клієнтським і впливав на дохід.
Їхня платформи команда наполягала на щоквартальній «rebuild» game day: drop-нути індекс, перебудувати з джерела, виміряти час і валідувати лічильники та вибіркові результати.
Нікому це не подобалося. Це не було гламурно і ніколи не потрапляло в слайд дорожньої карти.
Але це давало впевненість: вони знали, скільки часу займає реіндекс, скільки коштує та де живуть вузькі місця.
Одного дня помилка мапінгу прослизнула в продакшн і певні запити почали поводитися дивно. Команда не панікувала.
Вони розгорнули нову версію індексу за аліасом, реіндексували у фоновому режимі і переключили трафік, коли валідація пройшла.
Клієнти побачили невелику коливання релевантності на короткий період, а не повний outage. Компанія не потребувала war room з дванадцятьма людьми і одним знесиленим інцидентним командиром.
Нудна практика врятувала день — це найвища похвала в операціях.
Поширені помилки: симптом → корінь → виправлення
1) «Пошук Postgres став повільним з часом»
Симптом: p95 латентність зростає місяць за місяцем; використання диска зростає; autovacuum працює постійно.
Корінь: Блоут таблиці і GIN-індексу через високий churn оновлень/видалень індексованих текстових полів; autovacuum не налаштований для великих таблиць.
Виправлення: Знизьте autovacuum scale factors для гарячих таблиць, збільште ресурси vacuum, зменшіть churn оновлень і розгляньте періодичний REINDEX/pg_repack у вікнах техпідтримки.
2) «Elasticsearch швидкий в тестах, але повільний у продакшні»
Симптом: Лабораторні бенчмарки відмінні; в продакшні з’являються хвости латентності і таймаути.
Корінь: Забагато шард, тиск на heap і GC, або merges конкурують із запитами при реальному інгесті.
Виправлення: Зменшіть кількість шард, правильно підібрати розмір шард, налаштуйте refresh інтервал і швидкість інгесту, перевірте баланс heap/page cache і моніторте backlog merges.
3) «Результати пошуку не містять свіжих даних»
Симптом: Користувачі не знаходять новостворені/оновлені записи, але запис існує в Postgres.
Корінь: Лаг пайплайну інгесту, занадто довгий refresh interval або провалені події індексації без алертів.
Виправлення: Інструментуйте lag, ставте алерти на backlog, додавайте ідемпотентні ретраї і забезпечте стратегію свіжості (read-your-writes через Postgres або sticky sessions).
4) «Пошук спричинив інцидент бази даних»
Симптом: CPU і I/O злетіли на primary; пов’яли непов’язані транзакційні endpoint-и.
Корінь: Ендпоінт пошуку виконує дорогі запити на primary без LIMIT-ів; відсутня ізоляція читань; слабкі обмеження запитів.
Виправлення: Маршрутуйте пошук на репліки, впровадьте бюджети запитів, додавайте LIMIT, вимагайте фільтри і кешуйте часті запити.
5) «Ми не можемо змінити analyzer/mapping без даунтайму»
Симптом: Будь-яка зміна релевантності перетворюється на страшний проект реіндексування.
Корінь: Немає версіювання індексів/аліасів; немає репетицій реіндекс-процедур.
Виправлення: Прийміть практику версіонування індексів з аліасами, автоматизуйте пайплайни реіндексування і тренуйтеся регулярно.
6) «Elasticsearch green, але запити все одно таймаутяться»
Симптом: Здоров’я кластера green; користувачі бачать періодичні таймаути.
Корінь: Насичення threadpool, повільні запити або координаційні витрати (наприклад, важкі агрегації) незважаючи на здоровий розподіл шард.
Виправлення: Ідентифікуйте повільні запити, додайте timeouts/circuit breakers на рівні застосунку, зменште кардинальність агрегацій і попередньо обчислюйте поля.
7) «Ранжування Postgres FTS здається «не таким»»
Симптом: Результати містять збіги, але порядок здається людям неправильним.
Корінь: Відсутні ваги, неправильна конфіг/мовний регконфіг, або змішування полів без структури.
Виправлення: Використовуйте зважені вектори по полях, оберіть правильний regconfig, розгляньте phrase-запити і валідуйте на кураторованому тестовому наборі.
Чеклісти / покроковий план
Покроково: обираємо Postgres FTS (і тримаємо його дешевим)
- Визначте обмеження запитів. Вирішіть, що користувачі можуть шукати: поля, оператори, макс. довжина, макс. токенів.
- Спроектуйте вектор. Тримайте його свідомим: title + body + кілька metadata-полів, зважені.
- Індекс з GIN. Побудуйте GIN-індекс і перевірте через EXPLAIN, що він використовується.
- Бюджет на churn. Якщо ви часто оновлюєте документи, налаштуйте autovacuum зарані, не після появи блоуту.
- Захистіть primary. Помістіть пошук на репліки, коли трафік зростає. Найдешевший інцидент — той, якого немає.
- Вимірюйте. Слідкуйте за латентністю запитів, buffer reads, мертвими кортежами і ростом диска щомісяця.
Покроково: обираємо Elasticsearch (і уникаємо дорогих пасток)
- Спроєктуйте пайплайн інгесту спочатку. Outbox/CDC, ретраї, ідемпотентність, бекфіл.
- Визначте версіонування індексів. Використовуйте аліаси, щоб реіндексувати без даунтайму.
- Свідомо розмірюйте шард. Обирайте розміри шард, що зберігають відновлення реалістичним і накладні витрати низькими; уникайте крихітних шард.
- Плануйте ILM/ретеншн. Hot/warm/cold якщо потрібно; принаймні політики rollover і delete.
- Встановіть SLO по свіжості. Вимірюйте lag, а не настрій.
- Тестуйте відновлення. Snapshot/restore drill-и не опційні, якщо пошук має значення.
- Пишіть runbook-и. Відмова нод, кластер yellow/red, реіндекс, зміна мапінгу, реакція на тиск heap.
Покроково: гібридний підхід (більшість команд має починати звідси)
- Почніть з Postgres FTS для основних потоків і вивчіть реальні патерни запитів.
- Інструментуйте поведінку пошуку. Логуйте запити (безпечно), вимірюйте латентність, фіксуйте випадки «немає результатів».
- Переходьте до Elasticsearch лише для можливостей, які справді цього вимагають (фасети, фузі, інтенсивна ітерація релевантності).
- Тримайте «read-your-writes» у Postgres для елементів UI, критичних за свіжістю.
- Робіть ES перебудовуваним. Якщо ви не можете його перебудувати — він вами володіє; не ви ним.
FAQ
1) Чи «достатньо» Postgres повнотекстового пошуку для користувацького пошуку?
Часто так — особливо для B2B-додатків, де користувачі знають, що шукають. Якщо вам потрібні тяжкі фасети, фузі, синоніми в масштабі та швидка ітерація релевантності, ES зазвичай краще підходить.
2) Яка найбільша прихована довгострокова вартість Elasticsearch?
Пайплайн і операційна дисципліна: версіонування індексів, розмір шард, реіндекс-процедури та моніторинг свіжості. Кластер — це легка частина; підтримувати його правильним — ось витрата.
3) Яка найбільша прихована довгострокова вартість Postgres FTS?
Ампліфікація записів і блоут. Якщо ви індексуєте великі mutable текстові поля і часто їх оновлюєте, ваш GIN-індекс і таблиця можуть рости і сповільнюватися в спосіб, що виглядає як «таємне погіршення».
4) Чи роблять репліки Postgres пошук «не гіршим» за пошуковий кластер?
Репліки допомагають ізолювати навантаження, але вони не перетворюють Postgres на движок релевантності. Вони дозволяють «дешево і стабільно» масштабуватися далі. Вони не надають ES-подібних аналайзерів і ергономіки агрегацій.
5) Чи безпечно робити dual-write в Postgres і Elasticsearch?
Може бути, але рідко це найпростіший безпечний варіант. Dual-write вводить проблеми консистентності при часткових відмовах. Віддавайте перевагу outbox/CDC, щоб Postgres залишався авторитетним джерелом запису, а ES — проекцією.
6) Як вирішити на основі лише розміру даних?
Розмір даних — слабкий предиктор без розуміння патернів запитів і швидкості оновлень. Десятки мільйонів рядків можуть бути нормою для Postgres FTS при хороших обмеженнях і харді. Кілька мільйонів документів можуть бути болісними в ES, якщо ви створюєте тисячі крихітних шард.
7) Чи є репліки Elasticsearch бекапом?
Ні. Репліки захищають від втрати ноди, але не від помилок оператора, поганих мапінгів, випадкових видалень чи корупції, що розповсюджується по копіях. Вам все одно потрібні снапшоти і протестовані відновлення.
8) А якщо використовувати trigram пошук у Postgres замість повнотекстового?
Триграми відмінні для підрядкового і частково фузі пошуку на коротших полях (імена, ідентифікатори) і можуть доповнювати FTS. Вони також можуть бути дорогими при неправильному використанні на великих текстових блобах. Використовуйте правильний індекс для питання.
9) А якщо нам потрібні і пошук, і аналітика?
Якщо ви тягнете тяжкі агрегації і дашборди в ES, ви по суті запускаєте аналітичне навантаження на пошуковому кластері. Це може працювати, але збільшує конкуренцію ресурсів. Розділяйте обов’язки, якщо починає виникати конфлікт.
10) Як утримувати довгострокові витрати передбачуваними?
Вимірюйте драйвери: блоут/WAL-обсяг у Postgres і кількість шард/тиск heap/ingest lag в ES. Витрати стають передбачуваними, коли темпи зростання видимі та прив’язані до планів ємності, а не до несподіваних інцидентів.
Практичні наступні кроки
Якщо вам потрібна найдешевша довгострокова відповідь для більшості продуктових команд: починайте з Postgres FTS, дисципліновано його використовуйте, і додавайте Elasticsearch лише коли зможете сформулювати брак можливостей в одному реченні.
«Тому що всі використовують Elasticsearch» — це не аргумент; це крок, що обмежує кар’єру з щомісячним рахунком.
- Перелічіть свої реальні вимоги. Фасети? Фузі? Синоніми? Багатомовність? Обмеження по свіжості?
- Запустіть завдання вище на вашій системі. Отримайте розміри індексів, сигнали блоуту, кількість шард, тиск heap і відставання індексації.
- Вирішіть, яку вартість ви платите: contention бази даних (Postgres) або пайплайн + операції кластера (ES).
- Якщо обираєте Postgres: впровадьте бюджети запитів, ізолюйте навантаження репліками, налаштуйте autovacuum і тримайте вектори маленькими і осмисленими.
- Якщо обираєте Elasticsearch: побудуйте перебудовувану проекцію з аліасами, адекватним розміром шард, SLO по свіжості і відпрацьованими процедурами відновлення/реіндексування.
Дешевий в довгостроковій перспективі пошук — не бренд. Це набір звичок: обмежуйте запити, вимірюйте правильні лічильники і ставте обслуговування індексів у проміжок продакшн-навантаження.