Как устроен современный поиск по каталогу интернет-магазина
Поиск — вторая точка входа в магазин после главной страницы, и каждый неудачный запрос конвертируется в уход к конкуренту. Хорошая новость: для каталога на сотни тысяч товаров современный поиск собирается из PostgreSQL и трёх инженерных решений, без покупки отдельного движка.
Покупатель, который не нашёл товар за десять секунд, не пишет в поддержку — он уходит. Поиск по каталогу часто воспринимают как «коробочную» функцию, но под капотом это система с четырьмя слоями, у каждого свои требования и свои метрики. Разберём, как такой поиск строится на связке PostgreSQL и здравого смысла: от индексации каталога до формул ранжирования и показателей, по которым судят о качестве.
Индексация: половина успеха делается до первого запроса #
Основа — полнотекстовый поиск PostgreSQL. Каждому товару при сохранении строится вектор лексем: заголовок получает максимальный вес, бренд — средний, атрибуты вроде цвета, материала и размера — минимальный. Веса превращают запрос «nike кроссовки белые» в осмысленный рейтинг, а не в мешок совпадений, где карточка носков ранжируется наравне с искомой парой.
Почему не Elasticsearch, справедливо спросят владельцы больших каталогов. Он отличный инструмент, но добавляет в план второй stateful-сервис со своей синхронизацией, мониторингом и версиями. Для каталога до миллиона позиций связка «одна база, один индекс» проще в эксплуатации и быстрее, чем мост между двумя хранилищами, который неизбежно отстаёт на ночные обновления. Точка перехода к отдельному движку наступает не по количеству товаров, а по сложности запросов — и она наступает реже, чем принято думать.
product_search.sqlSQL
-- Индексируемое представление каталога: вектор собирается на стороне базыCREATETABLEproduct_search ( product_id integerPRIMARY KEYREFERENCES products (id), title textNOT NULL, brand text, attributes jsonb NOT NULLDEFAULT'{}', search_vector tsvector GENERATEDALWAYSAS ( setweight(to_tsvector('russian', coalesce(title, '')), 'A')|| setweight(to_tsvector('russian', coalesce(brand, '')), 'B')|| setweight(to_tsvector('russian', coalesce(attributes::text, '')), 'C') ) STORED);CREATEINDEXproduct_search_vector_idxON product_searchUSING gin (search_vector);
Сгенерированная колонка обновляется автоматически при записи товара, а GIN-индекс делает поиск по вектору быстрым даже на сотнях тысяч строк. Русский словарь приведёт «кроссовки» и «кроссовок» к одной лексеме — морфология работает из коробки, без отдельного сервиса лемматизации. Единственное, за чём стоит следить, — объём: GIN-индекс по векторам весит заметно больше самой таблицы, и на массовых перезаливках каталога его перестроение занимает минуты.
Запрос покупателя: грязный, короткий, с опечатками #
Реальный запрос выглядит не как «белые кроссовки Nike Air», а как «нук башмак 42 бел». Между сырым вводом и полнотекстовым запросом стоит конвейер нормализации, и каждый его шаг закрывает класс ошибок.
— Нормализация: нижний регистр, «ё» к «е», вычищение пунктуации и дублирующихся пробелов;
— Разбор русским словарём — формы одного слова перестают быть разными словами;
— Триграммный запасной путь: pg_trgm ловит опечатки там, где словарь бессилен, и находит «нукбокс» рядом с «nubuck»;
— Подстановка синонимов магазина: «мокасины» и «туфли», сленг покупателей и внутренние артикулы;
— Сборка через websearch_to_tsquery : покупатель может писать в гугловском стиле, кавычки и минусы не сломают запрос.
На примере: «нук башмак 42 бел» после нормализации превращается в лексемы {нук, башмак, 42, бел}, триграммы подсказывают, что «нук» — это скорее всего «nubuck», синонимы добавляют {ботинок, замша}, и в индекс уходит уже осмысленный запрос с весами. Всё это — несколько миллисекунд и сто строк кода, без единого обращения к внешнему сервису.
Ранжирование: из десяти тысяч совпадений — двадцать карточек #
Под «белые кроссовки» может подойти десяток тысяч позиций, а пользователь увидит двадцать. Текстовая релевантность ts_rank — лишь первый фильтр. Дальше подключаются бизнес-сигналы: наличие на складе, продажи за последние тридцать дней, маржинальность категории, свежесть карточки. Итоговая формула у нас — взвешенная сумма текстового и бизнес-рейтинга, и подбор весов это отдельная работа с A/B-тестами, а не настройка на глаз.
Есть и ловушка: бизнес-сигналы легко перекосить. Если маржинальность перевесит, наверху окажутся неликвидные позиции, которые магазину выгодно продать, — и конверсия поиска упадёт в течение недели. Наше правило: релевантность держит не меньше двух третей итогового веса, а любые изменения формулы выкатываются на процент трафика и живут там не меньше месяца.
Следующий уровень — гибридный поиск: к лексическому добавляются векторные эмбеддинги, ловящие семантику, а кандидаты от двух систем переупорядочивает реранкер. Для каталога среднего размера это опциональная роскошь: лексический поиск с хорошими синонимами закрывает большинство запросов. Семантика по-настоящему нужна там, где покупатель описывает потребность, а не товар — «тёплая куртка в минус тридцать» должна находить пуховики, даже если слово «пуховик» в карточке не встречается.
Красивый прототип не значит ничего без метрик. Мы следим за четырьмя показателями и пересматриваем их каждый раз, когда меняется формула ранжирования. Первый — доля запросов без результатов: здоровый каталог укладывается в три процента, и каждый такой запрос — это либо пробел в синонимах, либо товар, который стоит добавить в ассортимент. Второй — доля кликов по первому результату: если покупатель открывает вторую-третью позицию, ранжирование ошибается в сторону порядка, а не состава.
Третий показатель — конверсия сессий с поиском против сессий без него: этот разрыв и есть деньги, которые приносит поиск, и именно его показывают руководству, когда нужно выделить ресурс на развитие. Четвёртый — время от ввода запроса до клика: метрика объединяет и качество ранжирования, и скорость ответа, потому что медленная выдача с отличной релевантностью всё равно проигрывает быстрой.
Отдельный контур — производительность. Поиск обязан отвечать за десятки миллисекунд, иначе польза от идеального ранжирования исчезает: покупатель уйдёт раньше, чем увидит результаты. Когда каталог растёт и поиск начинает тормозить, не гадайте — смотрите план выполнения. Как это делать системно, мы описали в разборе медленных запросов в PostgreSQL : от симптома в мониторинге до виноватого оператора плана и точечного индекса.
Итого архитектура без экзотики: tsvector с весами, GIN-индекс, триграммы, словарь синонимов, ранжирование с бизнес-сигналами и четыре метрики на дашборде. Большинству магазинов этого хватает надолго, а момент перехода к гибридному поиску станет очевидным из метрик, а не из желания применить новый инструмент.