Як ми побудували продуктовий пошук у Laravel + Statamic на PostgreSQL — без Elasticsearch

💡 Усі статті, обговорення, новини про DevOps — в одному місці. Приєднуйтесь до DevOps спільноти!

Вітаю, спільното! Мене звати Юлія, я Backend Developer та Team Lead в PayAtlas. У цій статті поділюся практичним досвідом, як ми вбудували повнотекстовий пошук в існуючий Laravel + Statamic проєкт, використовуючи можливості PostgreSQL замість розгортання окремого Elasticsearch/OpenSearch-кластера. Також розкажу, як ми підійшли до мультимовності, індексації та fallback-сценарію для складних запитів.

Що за продукт і навіщо потрібен пошук

У нас є публічний контентний сайт, який працює на Laravel + Statamic. З боку користувачів взаємодія виглядає доволі класично: ввели запит, отримали результати і прочитали потрібну інформацію. Але якщо подивитись на сайт як на продукт, він ближчий до каталогу знань, де контент існує в кількох форматах і сценаріях, а також є різні типи матеріалів:

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

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

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

Навіщо це бізнесу і чому не Elasticsearch/OpenSearch

Для контентного сайту пошук це інструмент, який впливає на те, чи знайде користувач потрібне з першої спроби, чи зможе він орієнтуватися в різних типах контенту, і чи буде продукт готовий до росту — нових мов, нових колекцій, більшої кількості матеріалів.

Тому, коли ми планували пошуковий шар, дивилися одразу з кількох сторін: як він впливає на конверсію й утримання, на глобальну аудиторію через мультимовність, на сукупну вартість володіння (TCO) і на керованість для команди. Саме через цю комбінацію факторів ми не пішли в окремий Elasticsearch/OpenSearch-кластер, а реалізували пошук на PostgreSQL, який уже був у нашій інфраструктурі.

Такому рішенню сприяли чотири аргументи:

  • Конверсія й утримання. Чим швидше людина знайде потрібне, тим більше шансів, що вона прочитає не один матеріал, а кілька, повернеться знову (тому що тут зручно) і зробить наступний крок — зареєструється, підпишеться або залишить заявку.
  • Глобальна аудиторія і мультимовність. Якщо пошук погано розуміє форми слів (морфологію) і не може адекватно ранжувати результати, то навіть якісно перекладений контент втрачає користь: інформація є, але дістатися до неї складно. Для глобального охоплення це критично, тому ми закладали мультимовний пошук одразу.
  • Низький TCO. Окремий кластер означав би додаткову інфраструктуру з постійними витратами: JVM, ресурси, шардінг/реплікація, моніторинг, оновлення, інциденти. Для нашого поточного масштабу це дало б небагато користі порівняно з витратами. Тож ми пішли іншим шляхом: використали PostgreSQL, який у нас уже є як основна база, і додали до нього лише необхідні розширення та індексацію.
  • Краща керованість. Ми зробили так, щоб команди продукту та маркетингу могли змінювати, що впливає на видачу і які поля індексувати через конфіг без переписування бізнес-логіки. Технічно ми розділили, що індексуємо і що показуємо: поля title/summary/body визначають текст для повнотекстового пошуку (FTS), payload_fields задає контракт даних для UI (які поля віддаємо у видачі), а правила індексації винесли в SearchIndexFilters. Таким чином, пошук можна покращувати без залучення великих зусиль зі сторони розробки.

Які вимоги ми ставили до пошуку як до продукту

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

Функціональні вимоги

  • Пошук по кількох типах контенту. Користувач може шукати серед профілів компаній, статей, івентів і документації. Тому пошук має працювати по різних колекціях контенту і вміти зводити результати до єдиного формату видачі.
  • Мультимовний пошук з коректною морфологією. Оскільки сайт рухається до мультимовності, пошук одразу має коректно працювати для різних мов і алфавітів, з нормальною конфігурацією словників/стемінгу.
  • Підтримка різних UX-сценарів. Нам потрібна була підтримка трьох сценаріїв: швидкі підказки в меню, повноцінна сторінка пошуку з пагінацією, пошук усередині конкретної колекції (наприклад, лише документація або лише івенти). У цих запитів відрізняються ліміти, формат результатів, інколи і фільтри, тому ми закладали це одразу як частину продуктового UX.
  • Фільтрація: типи контенту, мови, published/state. Користувачі і продукт очікують, що пошук можна звузити: наприклад, показати лише статті або лише документи, обрати конкретну мову. Плюс обов’язкова вимога з боку системи: у видачу не мають потрапляти чернетки, приховані сторінки, непубліковані стани. Тому фільтрація за published/state для нас була базовим правилом.
  • Ранжування і стабільне сортування. Результати пошуку мають бути впорядкованими за релевантністю. Але є важливий нюанс: коли в кількох результатів релевантність однакова, порядок має бути стабільним при кожному запиті. Тому ми зафіксували порядок сортування як правило: спочатку rank, потім updated_at, потім id.
  • Fallback для неточних запитів. Користувачі вводять запити з помилками, уривками, спецсимволами, змішаними мовами і повнотекстовий пошук інколи може з цим не впоратись. Тому ми заклали вимогу: якщо FTS не може виконати запит, система має автоматично перейти на простіший, але стабільний fallback-пошук, щоб користувач все-таки отримав результат.

Нефункціональні вимоги

  • Швидкодія на рівні ~500мс p95/p99. Ми орієнтувались на те, щоб пошук був швидким у більшості випадків, включно з інкрементальним введенням користувача та складними запитами, тому вимірювали латентність у ~500мс p95/p99.
  • Швидке оновлення індексу. Оскільки контент живий, для нас було критично, щоб пошук швидко підхоплював оновлення — індекс має оновлюватись майже одразу після змін, в ідеалі в межах тієї ж транзакції через upsert/trigger.
  • Керованість і тестованість. Ми хотіли, щоб пошук був керованим: що індексуємо, як фільтруємо, які поля впливають на видачу. Тому заклали вимогу виносити індексаційні правила в конфіг і окремий клас фільтрів, щоб це було прозоро і піддавалося тестуванню.
  • Мінімум операційної складності. Це рішення мало працювати на поточній інфраструктурі й бути простим в експлуатації, тобто без додаткового кластеру пошуку з окремою підтримкою.
  • Контрольована деградація. Пошук не має показувати помилку користувачу, якщо запит невалідний або якщо виникла проблема зі словником. У таких випадках система знижує рівень «розумності» і переходить на fallback-пошук, але продовжує працювати.

Ключова ідея: єдиний індекс у таблиці documents

Щоб пошук працював однаково для різних типів контенту і залишався керованим, ми пішли в практичну концепцію: завели одну таблицю documents, яка зберігає індексоване представлення будь-якої сутності сайту, у нашому випадку будь-якого Statamic Entry.

Ідея в тому, що ми не намагались навчити PostgreSQL розуміти всі типи сутностей і їхні поля. Замість цього ми привели контент до єдиного формату, зручного для пошуку: текст для індексації + метадані для UI + службові поля для FTS і fallback.

Що саме потрапляє в documents

На рівні бізнес-логіки ми визначили правило: що індексується, ми описуємо в конфігу. Тобто для кожної колекції ми задаємо, які поля входять у пошуковий текст (умовно title/summary/body), і які поля потрібно зберегти у payload, щоб фронтенд одразу міг намалювати результат (лінк, назва, прев’ю, автор, дата тощо).

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

Тригер у базі: автоматичне оновлення індексу

Далі ми зробили те, що забезпечує майже миттєве оновлення пошуку без переіндексацій вручну: частина роботи виконується прямо в PostgreSQL через тригер на insert/update у documents. Цей тригер робить дві ключові речі:

  1. Будує tsvector під правильний regconfig, який залежить від lang. Тобто документ англійською індексується англійським словником, український українським і так далі. Це фундамент для коректної морфології і нормальної релевантності в мультимовному пошуку.
  2. Нормалізує searchable_text для fallback-пошуку, який працює через pg_trgm/ILIKE. Fallback нам потрібен як страховка для некоректних запитів, щоб система видавала результат навіть в таких випадках. Щоб він працював стабільно, ми зберігаємо нормалізований текст окремо і будуємо під нього trigram-індекс.

Сервісний шар: Postgres шукає, застосунок формує продуктову видачу

Ми чітко тримали межу відповідальності: PostgreSQL у нас відповідає за індексацію і ефективне виконання запитів, а вся продуктова логіка живе в Laravel.

Тобто сервісний шар виконує сценарії (меню, сторінка пошуку, сторінка колекції), додає фільтри (типи, мови, published/state), забезпечує стабільне сортування і повертає на фронт готові JSON-структури під меню, картки результатів і пагінацію.

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

Технологічний стек: що в нас вже було і що ми додали

Ми не будували окремий сервіс. Рішення інтегрувалось у вже існуючий стек:

  • Laravel: HTTP-шар, DI, сервісний рівень, сценарії пошуку.
  • Statamic: Entries як джерело контенту.
  • PostgreSQL: основна база, плюс окремий connection/schema для пошукового шару, щоб відділити пошукові сутності й міграції від основних.
  • Черги (для фонових задач, де це доречно) + Sentry/NewRelic для моніторингу.

Що ми додали в PostgreSQL для пошуку

Щоб реалізувати повнотекстовий пошук і fallback, ми додали:

  • розширення unaccent для нормалізації та роботи з діакритикою;
  • розширення pg_trgm для trigram-пошуку і швидкого fallback;
  • таблицю documents;
  • функцію lang_regconfig(lang) (маппінг мови на regconfig);
  • тригери оновлення індексу;
  • індекси GIN/trgm/btree під FTS, fallback і фільтрацію/сортування.

Архітектура рішення: два потоки і чіткі межі відповідальності

У цьому рішенні я розвела два незалежні потоки:

  • Write path: як контент потрапляє в індекс.
  • Read path: як пошук обробляє запит користувача і формує видачу.

Окремо нагадаю, що ми тримали важливу рамку: доменна логіка пошуку живе в Laravel, а PostgreSQL відповідає лише за індексацію та ефективне виконання запитів. Це дозволяє змінювати сценарії, формат видачі, фільтри й правила індексації, не перетворюючи базу на бізнес-логіку.

Потік даних: індексація контенту

Індексація починається з контенту в Statamic. Коли Entry створюється або оновлюється, ми пропускаємо його через індексаційний пайплайн і робимо UPSERT у таблицю documents. Далі база сама доводить запис до стану готовності для пошуку через тригер.

Схематично це виглядає так:

Що тут важливо:

  • Indexer відповідає за процес: отримати контент і запустити індексацію для потрібних сутностей.
  • DocumentFactory/PayloadBuilder приводять різний контент до єдиного формату. Тобто тут ми вирішуємо, що саме піде в title/summary/body (текст для індексації) і які дані потрібно покласти в payload, щоб фронт міг одразу показати картку результату.
  • DocumentsGateway робить UPSERT за унікальністю (type, external_id). Це гарантує, що при повторній індексації ми оновлюємо існуючий документ, а не плодимо дублікати.
  • Тригер documents_fts_update в Postgres доводить дані до пошукового вигляду: будує tsvector під правильний regconfig через lang_regconfig(lang), а також нормалізує searchable_text для fallback-пошуку.

У підсумку застосунок записує сирі дані, а база відповідає за те, щоб ці дані стали швидко доступними для пошуку через індекси.

Потік даних: пошуковий запит

Read path починається з HTTP-запиту. Далі ми проганяємо його через сервісний шар, який вибирає сценарій (меню/сторінка/колекція), збирає параметри і звертається до репозиторію, який виконує сам пошук в documents.

Схематично:

За ролями це працює таким чином:

  • SearchService це точка входу: він розуміє, яку відповідь очікує UI (menu/page/collection), і повертає готовий DTO.
  • SearchFacade тримає сценарії пошуку: ліміти, бакети, фільтри, пагінацію. Тут ми керуємо продуктово, як саме виглядає видача і які набори даних потрібні для кожного екрану.
  • DocumentsRepository це чиста робота з БД: будує FTS-запит (ранжування, фільтри), а якщо FTS не спрацював коректно, переходить на fallback.
  • MapperRegistry + маппери колекцій доводять результат до фінального формату: поля, підписи, специфічні для типу контенту деталі, лічильники/метрики, де потрібно.

У підсумку documents виступає єдиним джерелом для пошуку, а маппери гарантують, що UI отримає очікувані картки, незалежно від типу контенту.

Компоненти рішення: хто за що відповідає

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

Коротко по ключових компонентах:

  • config/search_index.php описує, які поля формують.
  • title/summary/body, які дані входять у payload_fields, і яка мова за замовчуванням. Також тут підключаються правила публікації через SearchIndexFilters.
  • SearchIndexFilters містить правила, що індексувати (наприклад, залежно від published/state).
  • DocumentFactory/PayloadBuilder збирають дані з Entry, додають маршрути, медіа, авторів, дати та інші поля, потрібні для UI.
  • DocumentsGateway робить UPSERT у documents за унікальністю (type, external_id).
  • documents зберігає текст, мову, payload (jsonb), tsvector і searchable_text.
  • TRIGGER documents_fts_update нормалізує текст і будує tsvector відповідно до lang_regconfig(lang).
  • DocumentsRepository виконує FTS-запити з ранжуванням (наприклад, ts_rank_cd) і фільтрами по типах/мовах.
  • SearchFacade/SearchService керують сценаріями: меню, загальна сторінка пошуку, сторінка колекції.
  • Маппери (Companies/Articles/Docs/Events) фіналізують JSON-видачу: поля, структуру, додаткові лічильники/метрики.

Діаграма компонентів

Нижче наводжу високорівневу схему архітектури пошуку. Я спеціально розділила компоненти на дві зони: застосунок (Laravel + Statamic) і база (PostgreSQL).

У коді діаграми зв’язки розбиті на дві частини:

  • блок Write path (indexing) описує індексацію: від Statamic Entry до запису в documents, після чого тригер у Postgres будує tsvector і нормалізований текст для fallback.
  • блок Read path (search) описує як запит читає з documents і формує відповідь для UI, тобто як виконується пошук: від SearchService до DocumentsRepository.
flowchart LR
  subgraph App[Laravel + Statamic]
    Entry[Statamic Entry]
    Indexer[Indexer]
    DocFactory[DocumentFactory]
    Payload[PayloadBuilder]
    Gateway[DocumentsGateway (UPSERT)]
    SearchSvc[SearchService]
    Facade[SearchFacade]
    Repo[DocumentsRepository]
    Reg[MapperRegistry]
    Mapper[Collection Mappers]
  end

  subgraph DB[PostgreSQL]
    Docs[(documents)]
    Trg[TRIGGER documents_fts_update]
    Fts[(fts tsvector)]
    Txt[(searchable_text)]
    Idx[GIN/Trgm/Btree indexes]
    LangMap[(search_lang_map)]
    Func[lang_regconfig(lang)]
  end

%% Write path (indexing)
  Entry --> Indexer --> DocFactory --> Gateway --> Docs
  DocFactory --> Payload
  Docs --> Trg --> Fts
  Docs --> Trg --> Txt
  Trg --> Func --> LangMap
  Fts --> Idx
  Txt --> Idx

%% Read path (search)
  SearchSvc --> Facade --> Repo --> Docs
  Facade --> Reg --> Mapper

Головна ідея в тому, що documents це єдиний індекс, тригер робить дані придатними до повнотекстового пошуку, а маппери доводять результат до контракту UI.

Послідовність пошукового запиту і fallback

Ця діаграма показує події від моменту GET /search?q=... до JSON-відповіді.

Тут важливі два моменти:

  • Де формується продуктова видача. В SearchFacade і мапперах ми збираємо саме той формат, який потрібен UI (групування, ліміти, підрахунки, структура карток). PostgreSQL на цьому етапі лише віддає відповідні записи з documents.
  • Де спрацьовує контрольована деградація. Основний шлях завжди FTS (через websearch_to_tsquery + ранжування). Але якщо FTS-запит невалідний або не може бути стабільно виконаний (наприклад, через синтаксис введення або проблеми зі словником), репозиторій автоматично перемикається на fallback-пошук.
sequenceDiagram
  participant UI as Frontend
  participant API as /search endpoint
  participant SS as SearchService
  participant SF as SearchFacade
  participant R as DocumentsRepository
  participant DB as PostgreSQL

  UI->>API: GET /search?q=term
  API->>SS: getDataForMenu/page/collection
  SS->>SF: scenario call
  SF->>R: items(term, types, select, limit, offset, langs)
  R->>DB: FTS query (websearch_to_tsquery + rank)
  alt FTS ok
    DB-->>R: rows ordered by rank
  else FTS failed or invalid
    R->>DB: fallback (searchable_text ILIKE / trgm)
    DB-->>R: rows
  end
  SF->>SF: count() per bucket
  SF->>Mapper: mapMenu/mapPage
  Mapper-->>SF: final JSON
  SF-->>SS: response DTO
  SS-->>API: JSON response
  API-->>UI: JSON for UI

Fallback виконує роль страховки і використовується лише тоді, коли FTS-запит виявився некоректним або не може бути виконаний стабільно. Основним завжди лишається FTS.

Мультимовність: як ми підв’язали словники і не ускладнили все

Ми хотіли, щоб додавання нової мови було лише питанням конфігурації, тому винесли відповідність «мова → словник» у базу даних. Для цього створили таблицю search_lang_map(code, regcfg), яка зберігає, який regconfig використовувати для кожної мови. Наприклад: en → english, uk → ukrainian, de → german.

Далі додали функцію lang_regconfig(lang), яка повертає правильну конфігурацію для FTS. Якщо мова невідома або ще не налаштована, функція повертає english за замовчуванням. Завдяки цьому система не падає на незнайомій мові і завжди має зрозумілий fallback-варіант.

Найприємніше те, що для нової мови достатньо лише додати один рядок у search_lang_map. Після цього переіндексація відбувається автоматично, тому що tsvector перебудовується тригером при INSERT/UPDATE у documents. І важливий момент для підтримки: бізнес-логіка та HTTP-шар не змінюються взагалі, змінюється лише налаштування словника.

Конфігурованість: як ми керуємо індексацією під різні колекції

У нас є кілька колекцій контенту, і у кожної своя природа даних і очікування від пошуку. У конфігу ми описуємо, що саме індексувати для кожної колекції. Для статей, наприклад, виявилось замало індексувати лише title і content, тож ми додали до індексації продуктові осі навігації: topic, type, journey, layer, tags. Це підсилює релевантність, бо користувачі часто шукають тему або контекст, який редакція вже знає і розмічає.

Пошук по кількох колекціях: ліворуч перелік типів контенту для навігації

Окремо ми тримаємо payload_fields як контракт для UI. Туди потрапляють речі типу роутів, зображень, авторів, дат, time-to-read, географії, статусів тощо. Завдяки цьому UI отримує готову відповідь і не мусить робити додаткові запити, щоб доповнити елементи картки результату.

А правила щодо того, що взагалі потрапляє в індекс, ми винесли з конфігу в окремий клас SearchIndexFilters. Це дозволяє мати прозоре джерело правди для published/state, перевикористовувати правила і нормально тестувати їх, а не ловити сюрпризи в проді. В результаті пошук можна еволюційно підкручувати і розширювати колекції без переписування ядра.

Продуктивність та експлуатація: скільки це важить і як поводиться під навантаженням

Ми подивились на рішення, як на інфраструктурний компонент — який розмір індексу, які індекси найбільші, як виглядають плани запитів і яка реальна латентність у типових сценаріях?

На момент написання статті пошуковий індекс містить близько 3800 документів. За розмірами у нас вийшло так:

  • дані таблиці documents: ~3.2 MB
  • індекси: ~19 MB
  • загальний розмір (разом із TOAST): ~57 MB

Найбільші індекси:

  • GIN(trgm) по searchable_text: ~9.3 MB (fallback-пошук, префікси/часткові збіги)
  • GIN по tsvector: ~5.7 MB (основний FTS)
  • GIN по payload: ~2.1 MB

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

Що показали EXPLAIN (ANALYZE, BUFFERS) на типових сценаріях

Для перевірки ми прогнали EXPLAIN (ANALYZE, BUFFERS) для кількох характерних запитів.

  1. Основний сценарій (FTS + fallback + фільтрація по типу). Планер використовував Bitmap Index Scan по type, далі Bitmap Heap Scan по documents. Час виконання близько ~47 ms, без sequential scan. Це якраз те, що ми хотіли бачити для типового сценарію: індекси працюють, вибірка не пробігає всю таблицю.
  2. Чистий FTS-запит без додаткових фільтрів. Через малий розмір таблиці (~3800 рядків) планер обрав Seq Scan. Час виконання близько ~38 ms, і запит повністю відпрацював з кешу (shared hit). Це нормальна поведінка, на малих таблицях Seq Scan може бути кращим рішенням, ніж робота з індексом.
  3. Fallback-пошук (ILIKE + trigram) для широких запитів. Планер використовував Bitmap Index Scan по GIN(trgm). Час виконання був близько ~113 ms. Це повільніше, ніж основний FTS, але важливо, що поведінка залишалась стабільною навіть для неточних або часткових запитів.

Тож ми отримали передбачувану латентність у всіх сценаріях, без потреби в окремому кластері.

Дорожня карта впровадження

  1. Застосування міграції: documents, функції, тригери, індекси. Підключити unaccent і pg_trgm.
  2. Налаштування config/search_index.php під колекції та потрібні поля (окремо уважно до articles).
  3. Підключення SearchIndexFilters і зареєструвати маппери в DI.
  4. Первинна індексація батчем через Indexer.
  5. Перевірка EXPLAIN, селективності індексів і латентності на типових запитах.

За бажанням можна також додати підсвітку через ts_headline і ваги в тригері для кращого ранжування.

Користь для SEO і контент-вітрин

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

По-перше, у статтях ми завели поля на кшталт synonyms_use/synonyms_avoid, щоб редакція могла узгоджувати термінологію. Це зменшує хаос у формулюваннях і прямо впливає на релевантність видачі.

По-друге, для івентів нормалізовані поля location і period роблять фільтри та вітрини подій зрозумілими і для людей, тому що легко відфільтрувати, і для роботів — структуровані дані, менше текстового шуму.

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

Коли PostgreSQL FTS достатньо замість Elasticsearch/OpenSearch

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

Цим підходом ми уникаємо окремого JVM-кластера, складнішої підтримки, eventual consistency, переіндексацій через помилки в мапінгу, подвоєння сховища та загалом вищого TCO.

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

Висновок

У цьому проєкті ми шукали баланс між якістю пошуку й реальними витратами на впровадження та підтримку. PostgreSQL дав нам усе базове, що потрібно продукту: повнотекстовий пошук із нормальним ранжуванням, мультимовність через словники, стабільну видачу, швидке оновлення індексу після змін контенту та fallback для невалідних запитів. І все це без окремого Elasticsearch/OpenSearch-кластера з його інфраструктурою, операційними ризиками та витратами.

З точки зору впровадження рішення теж вийшло доволі легким: ми додали одну індексну таблицю documents, кілька функцій/тригерів та індексів у Postgres, описали індексацію в конфігу і обмежилися існуючим стеком Laravel + Statamic. Для мене, як для тімліда, було важливо запустити пошук і зробити його керованим для команди та продукту. Це дає нам змогу його поступово розвивати і змінювати формат видачі без масштабних релізів.

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

Сподобалась стаття? Підписуйтесь на автора, щоб отримувати сповіщення про нові публікації на пошту.

👍ПодобаєтьсяСподобалось20
До обраногоВ обраному14
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Цікаво побачити розвиток підходів, коли буде не 3800, а 380000 документів...

Дякую, зберіг на випадок схожої задачі.

Цікава стаття

Було цікаво. Дякую за статтю 👍

Підписатись на коментарі