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

Поиск по каталогу на 100 000 товаров может начать тормозить уже после добавления второго фильтра, хотя простой запрос по названию работал быстро. Причина обычно не в одном медленном SQL-запросе, а в том, что фильтры, сортировка, подсчёт результатов и адрес страницы спроектированы независимо. Ниже — практическая схема для веб-приложения: от контракта запроса до выбора между PostgreSQL 16 и отдельным поисковым движком.
Сначала зафиксируйте контракт поиска
До выбора индекса или движка опишите, что именно означает каждый параметр. Строка «телефон» может искать точное значение в поле модели, совпадение по словам, похожее написание или только товары, доступные на складе. Если это не зафиксировать, интерфейс будет обещать одно, а база данных — возвращать другое.
Разделите параметры по смыслу
У запроса должны быть отдельные группы условий:
- текстовый запрос — что пользователь вводит в строку поиска;
- точные фильтры — категория, бренд, цвет, статус, наличие;
- диапазоны — цена, рейтинг, дата, вес;
- сортировка — по релевантности, цене, дате или популярности;
- пагинация — способ перехода к следующей порции результатов.
Не смешивайте эти понятия. Сортировка по цене не должна менять смысл текстового поиска, а фильтр «в наличии» не должен быть спрятан в логику отображения.
Правильный поиск — это не строка, которая что-то находит. Это предсказуемый контракт: одинаковый запрос даёт одинаковые правила отбора, сортировки и подсчёта.
Сразу определите поведение пустого запроса. В каталоге он может возвращать товары с фильтрами, а на отдельной странице поиска — ничего не возвращать. Это разные сценарии, и их нельзя оставлять на усмотрение обработчика по умолчанию.
Выберите место выполнения поиска
Есть три рабочих подхода. Выбор зависит не от моды, а от языка запросов, объёма данных и цены задержки.
| Подход | Когда подходит | Что вы получаете | Цена решения |
|---|---|---|---|
| Фильтрация в PostgreSQL 16 | Каталог, CRM, внутренний кабинет, умеренное число текстовых полей | Единая транзакционная база и простая согласованность данных | Сложные подсказки, опечатки и гибкое ранжирование придётся проектировать отдельно |
| Полнотекстовый поиск PostgreSQL | Поиск по описанию, заголовку и нескольким полям без сложной морфологии | Индексы и поиск рядом с исходными данными | Меньше возможностей для сложных синонимов и поисковой аналитики |
| Отдельный движок, например Elasticsearch | Много текстовых полей, подсказки, опечатки, фасеты и независимое ранжирование | Специализированные поисковые возможности и масштабирование чтения | Синхронизация, отдельная инфраструктура и возможная задержка обновления индекса |
Для нового каталога обычно разумно начать с PostgreSQL, если у вас уже есть эта база, а запросы сводятся к точным фильтрам, диапазонам и простому полнотекстовому поиску. Отдельный движок оправдан, когда требования к опечаткам, синонимам, подсказкам и фасетам становятся самостоятельной частью продукта. Переносить поиск туда заранее — значит платить за синхронизацию до появления доказанной проблемы.
Сноска. Фасет — фильтр с группировкой и количеством доступных результатов, например «Бренд: A — 132 товара, B — 87 товаров». Пользователь ожидает, что эти числа пересчитываются с учётом уже выбранных условий.
Спроектируйте фильтры вокруг индексов
Индекс не ускоряет запрос автоматически. Он помогает только тогда, когда его структура совпадает с реальным условием, сортировкой и распределением данных.
Точные значения и диапазоны
Для полей вроде статуса, категории или идентификатора сначала проверьте распределение значений. Индекс на колонке с двумя значениями, где 99% строк имеют один статус, может дать небольшой выигрыш: планировщик базы иногда быстрее прочитает таблицу целиком.
Диапазонные фильтры по цене и дате часто используются вместе с сортировкой. Замеряйте именно такие сочетания, а не отдельный запрос по каждой колонке. Важны порядок условий, число возвращаемых строк и доля данных, которую приходится просматривать.
Текстовый поиск
Поиск по шаблону с символом в начале строки плохо сочетается с обычным индексом: базе приходится проверять большое число значений. Для PostgreSQL 16 используйте встроенный полнотекстовый поиск и подходящий индекс, если задача — искать слова в подготовленном тексте. Для поиска по части слова, опечаткам и синонимам заранее определите правила нормализации и проверьте, нужен ли отдельный движок.
Не объединяйте в один текстовый индекс всё подряд. Название товара, артикул и описание имеют разную ценность. Артикул обычно должен находиться по точному совпадению выше, чем случайное слово из длинного описания.
Подсчёт результатов и фасеты
Запрос списка и запрос полного количества строк — не всегда одна и та же операция. На больших выборках точный подсчёт может стоить дороже, чем вывод первых 20 товаров. Решите, нужен ли пользователю точный итог, приблизительное значение или только признак наличия следующей страницы.
Для фасетов задайте явную семантику. Если выбран бренд A, должен ли счётчик бренда A показывать все товары A или только товары, совместимые с другими фильтрами? Оба варианта встречаются в интерфейсах, но каждый требует другой логики запроса.
Сделайте URL частью контракта
Фильтры, которыми можно поделиться, должны отражаться в адресе страницы. Это позволяет открыть результат в новой вкладке, сохранить его в закладки и воспроизвести проблему по ссылке.
- используйте стабильные имена параметров;
- не храните состояние фильтров только в памяти браузера;
- нормализуйте порядок значений, чтобы одинаковые запросы не создавали разные URL;
- не помещайте в адрес секреты, внутренние идентификаторы и служебные параметры;
- ограничьте допустимые поля сортировки белым списком, а не передавайте имя колонки напрямую в базу.
Для первой страницы обычно достаточно нумерованной пагинации. Но при изменяющемся каталоге переход на страницу 20 может показать дубликаты или пропуски: новые строки сдвинули границу выборки. Для лент и больших наборов данных чаще подходит cursor- или keyset-пагинация — переход по последнему значению сортировки и уникальному идентификатору.
Сначала измерьте запрос, потом оптимизируйте
Минимальный набор измерений должен включать p50, p95 и p99 времени ответа, число результатов, долю пустых запросов, частоту каждого фильтра и размер ответа. Одного среднего времени недостаточно: редкий запрос с дорогим подсчётом может портить пользовательский опыт сильнее, чем сотни быстрых.
Проверяйте сценарии на данных, похожих на боевые. Десять тысяч равномерно распределённых строк не показывают поведение каталога, где один бренд занимает половину записей, а 90% товаров имеют одинаковый статус. Для PostgreSQL смотрите план выполнения, фактическое число обработанных строк и объём чтения, а не только наличие индекса.
Что обычно ломается
- Фильтр работает в интерфейсе, но не влияет на запрос. Часто фронтенд меняет состояние компонента, а параметр не попадает в HTTP-запрос. Проверяйте итоговый URL или тело запроса в браузере.
- Результаты меняются при обновлении страницы. Причина может быть в сортировке без уникального второго ключа. Добавьте стабильное разрешение одинаковых значений, иначе строки с одинаковой ценой могут менять порядок.
- Поиск находит артикул хуже описания. Для разных полей заданы одинаковые веса. Разделите точное совпадение, начало строки и полнотекстовое совпадение.
- Страница с фильтрами стала медленной после запуска. Обычно нагрузку создают фасеты и точный COUNT, а не сам вывод первой порции. Измеряйте их отдельно.
- Новый товар не находится сразу. При отдельном поисковом индексе это может быть задержка синхронизации. Пользователю нужно показать согласованное правило: данные появляются мгновенно или с допустимой задержкой.
Когда какой подход выбирать
Оставляйте поиск в PostgreSQL, если основная ценность — согласованность с заказами, остатками и правами доступа, а фильтры хорошо описываются условиями по колонкам. Это снижает число компонентов и упрощает отладку.
Добавляйте полнотекстовый слой в PostgreSQL, когда нужно искать по нескольким текстовым полям, но нет требований к сложным подсказкам и большому числу языковых правил.
Выносите поиск в Elasticsearch или другой специализированный движок, если продукту нужны опечатки, синонимы, сложное ранжирование, быстрые фасеты и отдельное масштабирование чтения. До этого решения посчитайте цену задержки индекса, повторной индексации и восстановления после рассинхронизации.
Что сделать дальше
Возьмите десять реальных поисковых запросов из логов или интервью и оформите для каждого ожидаемые результаты, сортировку, фильтры и допустимое время ответа. Затем соберите небольшой набор боевых данных и сравните три сценария: точный фильтр, текстовый поиск и фильтр с фасетами. После этого зафиксируйте URL-контракт, добавьте метрики p95 и отдельно измеряйте подсчёт результатов. Инвалидация кеша и обучение ранжирования — отдельные темы; сначала добейтесь предсказуемого поведения без них.
Поиск источников: Реальные статьи и книги со ссылками, библиография по ГОСТ
Спросить за 15 ₽Источники: PostgreSQL 16 Documentation: Full Text Search, Elasticsearch Reference: Boolean query
Частые вопросы
Нужно ли сразу ставить Elasticsearch для каталога?
Почему индекс не ускорил фильтр?
Что выбрать: страницы или cursor-пагинацию?
Что делать, если у меня другая версия PostgreSQL?
Почему новый товар не появляется в поиске сразу?
Можно ли передавать имя сортировки напрямую из URL?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.