Elasticsearch relevance: 7 шагов, которые мы повторяем в каждом e-com
Автор: WebGoodPeople
Если у вас 1000+ SKU и встроенный поиск Битрикс/WordPress/Shopify — ваш поиск, с вероятностью 80%, работает так: пользователь вводит запрос, получает либо «ничего не найдено», либо выдачу, в которой первый товар — не то, что он искал. Он уходит. Конверсия через поиск — 15–25% от всех продаж, и она у вас утекает.
Эта статья — playbook из 7 шагов, которые мы применяем в каждом проекте с Elasticsearch. Порядок неслучайный: каждый шаг требует данных от предыдущего.
Вводные: почему именно Elasticsearch
Альтернативы:
- Встроенный поиск Битрикс — работает на MySQL FULLTEXT, без типосовместимости, без синонимов, релевантность «как повезёт».
- OpenSearch — форк ES 7.x от Amazon. На 95% совместим с нашими шагами.
- Meilisearch, Typesense — хороши для небольших каталогов (<50k), но ограничены в кастомном скоринге.
- Algolia — отличный продукт, но $500+/мес для среднего e-com.
Для каталогов 50k+ SKU с серьёзными требованиями к управляемости — ES или OpenSearch.
Шаг 1. Анализ текущих запросов и zero-result rate
Что делаем: собираем все поисковые запросы за последние 30 дней. Считаем долю запросов с нулевым результатом (zero_result_rate).
-- В Loki или ClickHouse SELECT query, count(*) as cnt, countIf(hits = 0) as zero_results, countIf(hits = 0) / count(*) as zero_rate FROM search_logs WHERE ts >= now() - interval 30 day GROUP BY query ORDER BY cnt DESC LIMIT 200;
Что ищем: запросы из топ-200 по частоте с zero_rate > 5%. Это прямая потеря: пользователь искал, ничего не нашёл, ушёл.
Типичные находки: опечатки (плате вместо платье), синонимы (кроссы вместо кроссовки), иностранные варианты (sneakers на русскоязычном сайте).
Ожидаемый baseline: хорошо настроенный e-com — zero_rate < 3%. Типичный не настроенный — 15–25%.
Шаг 2. Log shipping поисковых запросов в отдельный индекс
Что делаем: каждый поисковый запрос логируется с метаданными:
{
"ts": "2026-05-19T10:00:00Z",
"user_id": "u42",
"query": "красное платье",
"hits": 24,
"first_click_position": 3,
"converted": false
}
Индекс называем search_queries. Retention — 90 дней (для аналитики достаточно).
Зачем: все последующие шаги без этого индекса — стрельба по фанерным мишеням.
Шаг 3. Синонимы (манаульный + ML-derived словарь)
Что делаем: создаём словарь синонимов. Два уровня:
Ручной (30–50 пар, быстро):
кроссовки, кроссы, sneakers платье, плате, dress свитер, джемпер
ML-derived (из логов): используем Word2Vec или fasttext на логе запросов, находим пары слов, которые часто встречаются в семантически близких контекстах.
Elasticsearch принимает это как:
{
"settings": {
"analysis": {
"filter": {
"ru_synonyms": {
"type": "synonym",
"synonyms_path": "analysis/synonyms-ru.txt"
}
},
"analyzer": {
"ru_analyzer": {
"tokenizer": "standard",
"filter": ["lowercase", "ru_synonyms", "russian_stemmer"]
}
}
}
}
}
Ожидаемый эффект: zero_rate падает на 30–50% на первой итерации.
Шаг 4. Field boosting (что важнее — название или описание)
Что делаем: при multi_match указываем веса полей:
{
"query": {
"multi_match": {
"query": "красное платье",
"fields": [
"title^5",
"brand^3",
"categories^2",
"description^1",
"attributes.*^0.5"
],
"type": "best_fields",
"fuzziness": "AUTO"
}
}
}Почему такие веса: в e-com пользователи ищут по названию товара в 70% случаев. Brand — сильный сигнал (если есть). Описание — слабее (переполнено ключевыми словами от SEO-команды).
Важно: эти веса — стартовые. Финальные — после шага 7 (A/B-тестирование).
Шаг 5. Fuzziness (типо-толерантность)
Что делаем: включаем fuzzy matching, но осторожно. "fuzziness": "AUTO" даёт:
- 0 опечаток для слов до 3 символов
- 1 опечатка для слов 3–5 символов
- 2 опечатки для слов 6+ символов
Для русского языка — этого обычно достаточно. Для брендов и кодов SKU fuzziness лучше отключить (чтобы iPhone 15 не матчился с iPhone 16).
"query": {
"bool": {
"should": [
{
"multi_match": {
"query": "{{query}}",
"fields": ["title^5", "description^1"],
"fuzziness": "AUTO"
}
},
{
"term": { "sku": "{{query}}" } // exact match for SKU
}
]
}
}Шаг 6. Function scoring — popularity, stock, recency
Что делаем: ранжируем не только по текстовой релевантности, но и по бизнес-сигналам.
{
"query": {
"function_score": {
"query": { /* multi_match из шага 4 */ },
"functions": [
{
"field_value_factor": {
"field": "popularity_score",
"modifier": "log1p",
"missing": 1
}
},
{
"filter": { "term": { "in_stock": true }},
"weight": 2
},
{
"exp": {
"created_at": {
"origin": "now",
"scale": "90d",
"decay": 0.5
}
}
}
],
"score_mode": "multiply",
"boost_mode": "multiply"
}
}
}Что даёт каждая функция:
field_value_factorпоpopularity_score— популярные товары выше.popularity_scoreсобирается отдельно (например, просмотры за 30 дней).filterпоin_stock— товары в наличии получают boost × 2. Отсутствующие — проваливаются.expdecay поcreated_at— новинки получают бонус, затухающий за 90 дней.
Ожидаемый эффект: позиция first-click растёт с 4–5 до 1–2.
Шаг 7. A/B-тестирование релевантности через interleaving
Что делаем: сравниваем две версии скоринга через team-draft interleaving. Пользователю показывается смешанная выдача (половина из A, половина из B), мы считаем, с какой стороны больше кликов.
def interleave(results_a, results_b, n=20):
out = []
turn = random.choice(['A', 'B'])
i_a, i_b = 0, 0
while len(out) < n and (i_a < len(results_a) or i_b < len(results_b)):
if turn == 'A' and i_a < len(results_a):
out.append((results_a[i_a], 'A'))
i_a += 1
turn = 'B'
elif i_b < len(results_b):
out.append((results_b[i_b], 'B'))
i_b += 1
turn = 'A'
return outПочему именно interleaving: классический A/B требует больших выборок (4–6 недель на статзначимость). Interleaving — в 10 раз быстрее (2–3 дня).
Ожидаемый outcome после 7 шагов
На каталоге 50k+ SKU с нулевой настройкой релевантности:
zero_result_rate: с 20% до 2%- Средняя позиция first-click: с 5 до 1.5
- Конверсия через поиск: +30–60%
- Время поиска (p95): 20–60 мс на запрос (правильная настройка индексов ES даёт это даже на 500k SKU)
Что НЕ делаем
- Не добавляем ML-based ranking на первой итерации. Переход на learning-to-rank имеет смысл после того, как базовая настройка работает.
- Не оптимизируем 100+ полей одновременно. 5–7 ключевых полей × правильные веса — 80% выигрыша.
- Не делаем "персонализацию" без данных. Персонализация работает с 100+ сессий на пользователя. У большинства e-com такой глубины нет.
Если хочется быстрее
48-часовой аудит поиска — мы смотрим ваш ES-индекс, даём конкретный query-шаблон и словарь синонимов под ваш каталог. Free, без обязательств.