Покупатель вводит «кроссовки для бега». Он не просит найти все карточки, где встречается слово «для»: таких в магазине окажутся сотни, от чехлов для телефонов до лотков для кошек. Ему нужны беговые кроссовки, желательно в его размере и в наличии. Поисковая система не знает этого сама. Она знает только то, что ей положили в индекс и как просили этот индекс читать. Поэтому построить поиск по каталогу значит не поставить OpenSearch, а договориться с ним о том, что в вашем магазине считается хорошим ответом.
Установку мы здесь не разбираем, для неё есть отдельный материал: Как установить и настроить OpenSearch на VPS. Здесь разговор про то, что происходит после: как описать товары, как обращаться с синонимами, кого пустить к кластеру и сколько памяти он съест на ваших данных, а не на чужих.
Сначала запросы и правильные ответы
Начните не с маппинга, а с таблицы. Возьмите двадцать-тридцать реальных запросов покупателей: из поисковой строки на сайте, из обращений в поддержку, из рекламных кампаний. Для каждого запишите, какие товары должны оказаться наверху и какие точно не должны. Это ваш эталон, и все дальнейшие решения проверяются о него.
Вот как это может выглядеть. Пример условный, подставьте свой каталог.
| Запрос | Должно быть в первых результатах | Не должно быть |
|---|---|---|
| кроссовки для бега | беговые модели, мужские и женские | лифтовые кеды, носки, обувные щётки |
| кросовки найк | кроссовки бренда Nike (запрос с опечаткой) | другие бренды только из-за слова «кроссовки» |
| телевизор 55 | телевизоры с диагональю 55 дюймов | кронштейны и пульты «для телевизора 55» |
| худи чёрное | чёрные худи любых размеров в наличии | серые худи с чёрным принтом |
Уже на этом этапе видно, что задача состоит из разных частей. «Для бега» — это смысл, его нужно искать в названии и описании. «55» — числовой атрибут, и лучше превратить его в фильтр, а не надеяться на текстовое совпадение. «Найк» вместо Nike — вопрос транслитерации и синонимов. «Чёрное» — значение цвета, которое хорошо живёт в отдельном поле.
От запросов к полям
Ошибка новичка — загрузить товар в индекс одним большим документом и положить всё в текстовые поля. Тогда цвет, бренд и артикул будут анализироваться одинаково и мешаться друг с другом. Полезнее разделить поля по роли в поиске.
- Название и короткое описание. Основной текст для поиска. Тут нужен анализ с учётом русской морфологии, чтобы «кроссовки» и «кроссовок» считались одним словом. Название обычно весит больше описания.
- Бренд, цвет, категория, пол, материал. Значения из справочника. Их удобно хранить как ключевые слова без анализа: по ним фильтруют и строят фасеты, а не ищут по смыслу.
- Цена, размер, диагональ, остаток. Числа и диапазоны. По ним нельзя промахнуться на морфологии, зато легко ошибиться, если они лежат строкой.
- Артикул. Отдельное поле, где слово не режется на части и не приводится к основе. Покупатель, который вводит артикул, ждёт ровно один товар.
- Признак наличия и дата обновления. Нужны, чтобы убирать из выдачи то, что нельзя купить, и чтобы понимать, когда карточка последний раз менялась.
Теперь вернёмся к «кроссовкам для бега». Слово «для» в русском языке — стоп-слово: оно почти ничего не несёт, и анализатор может его отбросить. Тогда запрос превращается в «кроссовки бег», и этого достаточно, чтобы поднять карточки, где названы оба слова. Но смысл «для бега» часто записан в каталоге иначе: в характеристике «назначение: running» или в категории «Беговая обувь». Значит, поиск должен смотреть в несколько полей, а категория или назначение должны попадать в индекс как отдельные признаки. Только после этого запрос про бег будет находить беговую модель, даже если в её названии слова «бег» нет.
Как проверить анализ, не строя весь индекс
Прежде чем загружать каталог, посмотрите, как анализатор режет ваши реальные строки. В OpenSearch для этого есть API анализа текста: вы передаёте строку и название анализатора, а в ответ получаете токены. Прогоните через него десять названий товаров и все запросы из эталона. Если «Nike Air Zoom Pegasus 40» превратился в набор токенов, из которого выпал номер модели, это видно сразу, а не через неделю по жалобам. Такая проверка дешёвая, и она отвечает на вопрос, который иначе решается догадками.
Синонимы: два острых края
Синонимы кажутся простым решением: «кроссовки», «кеды», «sneakers» — одно и то же, объединим. Но каждое такое объединение расширяет выдачу, и от него бывает не только польза.
Полезные случаи обычно такие:
- транслитерация брендов: «найк» и Nike, «самсунг» и Samsung;
- разговорные и официальные названия одного предмета: «худи» и «толстовка с капюшоном»;
- единицы и написания: «55 дюймов», «55"», «55 дюйм»;
- сокращения, которые в каталоге записаны одним способом, а покупатели набирают другим.
Опасные случаи тоже узнаваемы. Если вы объявите «кеды» и «кроссовки» равнозначными, покупатель беговой обуви получит парусиновые кеды, потому что для поиска они стали одним и тем же словом. Если объединить «чехол» и «телефон», запрос «телефон» начнёт возвращать чехлы, стёкла и зарядки, и настоящие телефоны утонут среди аксессуаров. Синоним должен означать действительно взаимозаменяемое слово в контексте вашего каталога, а не просто тематически близкое.
Работает простое правило: различайте равенство и подсказку. Равенство — «найк» и Nike, оно безопасно. Подсказка — «кеды» рядом с «кроссовками»: так связанное слово можно принять во внимание, но с меньшим весом, или показать как уточнение категории, а не подмешивать в основной результат. Даже без тонкой настройки вы можете держать список синонимов маленьким и выверенным, а каждый новый добавлять только после проверки на эталоне: поднялось ли то, что нужно, и не пропало ли то, что уже стояло правильно.
Практический способ — вести список синонимов как отдельный файл под версионным контролем и прогонять эталонные запросы при каждом изменении. Тогда спор «а давайте добавим ещё слово» превращается в измеримый вопрос: стало лучше или хуже на ваших двадцати запросах.
Кто и с какими правами ходит в кластер
Теперь о том, что в статьях про поиск пропускают, пока не становится поздно. Кластер OpenSearch сам по себе — это HTTP-интерфейс, который умеет читать, писать и удалять индексы. Если сайт ходит в него напрямую из браузера покупателя, а наружу торчит порт с административными правами, то любой, кто найдёт адрес, получает возможность не только искать, но и стирать каталог.
Подсистема безопасности OpenSearch устроена так, что права назначаются ролям, а роли связываются с пользователями; доступ можно ограничивать на уровне кластера, индекса, документа и поля, а для REST-обмена и общения между узлами поддерживается TLS (OpenSearch: Security). Из этого следует рабочая схема, в которой у каждого участника своя учётная запись и своя роль.
| Участник | Что ему нужно | Что ему не нужно |
|---|---|---|
| Приложение, обслуживающее поиск | чтение индекса каталога | запись, удаление индексов, управление кластером |
| Индексатор (синхронизация каталога) | запись документов в индекс каталога | чтение других индексов, управление пользователями |
| Администратор | управление кластером и ролями | использование этой учётной записи в приложении и скриптах |
Идея в том, что компрометация одного ключа должна приносить ограниченный ущерб. Если приложение, принимающее запросы покупателей, умеет только читать конкретный индекс, то даже украденные из его конфигурации данные не позволят удалить каталог. Если индексатор пишет, но не читает всё подряд, он не станет источником утечки чужих индексов.
Есть и полевая детализация. Если в документе товара лежат внутренние поля вроде закупочной цены или поставщика, их можно скрыть для роли приложения средствами ограничения доступа на уровне полей, о которых говорит документация по безопасности. Но проще и надёжнее вообще не класть эти поля в поисковый индекс: поиску они не нужны. Чем меньше лишнего в индексе, тем меньше его придётся защищать, а заодно и меньше памяти он займёт.
Приложение между покупателем и кластером
Браузер покупателя не должен общаться с OpenSearch напрямую. Между ними стоит ваше приложение с узким API: оно принимает строку запроса, фильтры и номер страницы, само формирует запрос к поисковому кластеру и возвращает только нужные поля. Это даёт сразу несколько вещей. Вы контролируете, какие поля и сколько результатов можно запросить, поэтому нельзя выкачать весь индекс одним обращением. Вы можете задать верхнюю границу глубины страниц. Вы ограничиваете виды запросов: покупатель не может прислать произвольный запрос и нагрузить кластер тяжёлой агрегацией.
Порт кластера при этом закрывается от внешней сети: к нему должны подключаться только приложение и индексатор, а доступ администратора идёт по закрытому каналу. Подключение к REST стоит делать по TLS, как и обмен между узлами, если кластер состоит из нескольких машин. Так работа с поиском остаётся внутренним делом вашей инфраструктуры, а наружу смотрит только то, что вы сами решили показать.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации Россия, США, Франция и Великобритания. Оплата картой РФ и по СБП.
Сколько памяти нужно индексу: меряем, а не угадываем
Самый частый вопрос звучит так: сколько оперативной памяти нужно для миллиона товаров. Честный ответ — это зависит, и единой нормы нет. Два каталога по сто тысяч позиций могут отличаться по требованиям в разы. Один хранит короткое название и десять атрибутов, другой — длинные описания, много полей и вложенные структуры вариантов. Один индексирует всё в текстовом виде, другой разумно разделяет текст и ключевые слова. Значение имеют число полей, длина текстов, способ анализа, количество реплик и то, какие запросы вы гоняете: сортировки и агрегации по полям тянут за собой собственные структуры данных.
Поэтому правильный способ оценки — сделать срез и измерить. Алгоритм простой.
- Соберите репрезентативный срез. Не первые десять тысяч товаров по идентификатору, а выборку, в которой есть все категории, самые длинные описания и самые бедные карточки, товары с большим числом вариантов и без вариантов. Если выбрать только простые позиции, оценка окажется заниженной.
- Загрузите срез в индекс с теми же настройками, что планируете в работе. Тот же анализатор, те же поля, то же число реплик. Измерять индекс с другим маппингом бессмысленно.
- Посмотрите размер индекса на диске. В OpenSearch есть статистика по индексам и cat-интерфейсы, которые показывают объём хранимых данных и число документов. Разделите размер на число документов и получите средний размер документа в индексе.
- Посмотрите, что происходит в памяти под нагрузкой. Прогоните эталонные запросы и реалистичный поток, включая фильтры, сортировки и фасеты, и следите за использованием кучи JVM и страничного кеша операционной системы. Если на сортировке куча заметно растёт, это сигнал пересмотреть, по каким полям вы сортируете.
- Экстраполируйте осторожно. Умножайте не слепо, а с запасом и проверкой: загрузите срез в десять раз больше и посмотрите, линейно ли растут показатели.
Для иллюстрации возьмём условное число. Допустим, на срезе в двадцать тысяч документов индекс занял определённый объём на диске. Разделив его на двадцать тысяч, вы получите средний размер документа, и он станет вашей собственной единицей измерения. Если полный каталог в двадцать раз больше, это ещё не значит, что нужна в двадцать раз большая машина: часть структур растёт не линейно, а часть, наоборот, делится между документами. Поэтому второй замер на срезе крупнее нужен как проверка, а не формальность.
Заметьте и обратное: запас по оперативной памяти нужен не только индексу. Поисковому процессу нужна куча JVM, а операционной системе — свободная память под кеш файлов, на котором во многом держится скорость чтения. Если отдать всю память процессу, кеш файлов съёжится, и поиск станет медленнее. Баланс подбирается замером на вашем срезе.
Как это соотносится с тарифами VPS
На VPS ресурсы заданы конфигурацией: у MAATRIX это ступени от Ba с 1 vCPU, 2 ГБ RAM и 40 ГБ NVMe до Djed с 16 vCPU, 32 ГБ и 320 ГБ; промежуточные — Uab (2 vCPU, 4 ГБ, 80 ГБ), Heka (4 vCPU, 8 ГБ, 160 ГБ) и Maat (8 vCPU, 16 ГБ, 240 ГБ). Эти цифры не обещание ёмкости и не минимум для OpenSearch: они лишь описывают, что у вас будет в распоряжении. Практический порядок такой: сначала замер на срезе, потом подбор ступени с запасом. Небольшой каталог для проверки гипотез разумно прогнать на младшей конфигурации и посмотреть, где упирается, а для рабочего поиска опираться на результаты собственного замера. Диск на NVMe важен для индекса, который постоянно читается и перестраивается, но подтверждённых цифр скорости за него здесь называть нельзя, их тоже даёт только ваш замер. На VPS есть root-доступ, поэтому настройки системы под поиск вы можете выставить сами; описание предложения доступно на странице аренды VPS MAATRIX.
Если вы собираетесь держать отдельно приложение, базу каталога и поисковый кластер, помните, что память делится между ними. Оценка индекса на срезе даёт цифру для одного компонента, а подбирать ступень нужно под сумму.
Карточка изменилась или исчезла: как поиск узнаёт об этом
Хороший индекс на день запуска ничего не гарантирует. Через неделю цена изменилась, товар закончился, карточку сняли с продажи, а в поиске он всё ещё стоит первым и ведёт покупателя на страницу с ошибкой. Это самая неприятная форма поисковой ошибки: система уверенно отвечает устаревшим. Источником правды остаётся база каталога, а индекс — её производная копия, и вся задача в том, чтобы копия не жила собственной жизнью.
Есть несколько способов поддерживать соответствие, и выбор зависит от того, насколько быстро изменения должны попадать в поиск.
- Событийное обновление. При изменении или удалении товара приложение пишет событие в очередь, а индексатор забирает его и обновляет документ. Изменение появляется в поиске быстро, но нужно следить, чтобы событие не потерялось.
- Периодическая синхронизация. Индексатор раз в определённое время выбирает из базы товары, изменённые после последней отметки, и обновляет их. Проще, но между изменением и поиском есть задержка.
- Полная переиндексация в новый индекс. Периодически каталог строится заново в новом индексе, затем приложение переключается на него. Так исчезают накопившиеся расхождения, в том числе удалённые товары, о которых индексатор мог не узнать.
Удаление заслуживает отдельного внимания. Добавление и обновление естественно выражаются в данных, а удаление — это отсутствие записи, и по периодической выборке изменённых строк о нём не узнать. Поэтому удалять нужно явно: либо отправлять событие об удалении, либо вместо физического стирания в базе ставить признак «снят с продажи» и обрабатывать его как обычное изменение. Тогда индексатор видит запись, понимает, что её надо убрать из поиска, и убирает.
Отдельный предохранитель — фильтр по признаку наличия и актуальности в самом запросе приложения. Даже если индексатор задержался, приложение не покажет товар, который база уже считает недоступным, при условии что признак доходит до индекса достаточно быстро. Но на этот фильтр нельзя полагаться как на единственную защиту: он прячет симптом, а расхождение остаётся. Поэтому полезна периодическая сверка: сравнить число документов в индексе с числом активных товаров в базе и выборочно проверить, нет ли в поиске идентификаторов, которых уже нет в каталоге. Расхождение — повод искать, где индексатор теряет события.
Помните и про порядок обновлений. Если в очередь пришли два изменения одной карточки, а индексатор обработал их в обратном порядке, в индексе окажется старое состояние. Простая защита — записывать в документ время или версию изменения из базы и не применять обновление старше уже записанного.
Возвращаемся к покупателю
Теперь можно снова открыть таблицу с запросами из начала. Для каждой строки выполните реальный запрос через узкий API приложения, а не через административную консоль, и посмотрите на первые результаты. Нашлись ли беговые кроссовки по запросу «кроссовки для бега»? Понял ли поиск «найк» и опечатку «кросовки»? Не вытеснили ли кронштейны телевизоры по запросу «телевизор 55»? Не показался ли в выдаче товар, которого уже нет?
Делать это стоит не на тестовых позициях, а на товарах, которые реально продаются: на хитах, на длинном хвосте, на карточках с плохими описаниями, на новинках, добавленных вчера. Именно там поиск или оправдывает своё существование, или нет. Скорость ответа тут вторична: быстрый поиск, который возвращает не то, не помогает продавать, а аккуратно собранный эталон показывает это раньше любой метрики. Поменяли синонимы, поле или вес — прогоните эталон снова. Если тот же список работает лучше, чем вчера, значит, вы настраиваете именно то, что нужно покупателю.