Как сделать полнотекстовый поиск на сайте с Elasticsearch
Практика4 минРедакция Innovialabs
Коротко
Создайте индекс с отдельными полями под каждый язык и правильными анализаторами, синхронизируйте его с основной базой через события или периодическую переиндексацию, а для опечаток и подсказок используйте fuzziness и search_as_you_type.
Как это устроено коротко
Elasticsearch — это отдельный поисковый индекс рядом с основной базой. Источником истины остаётся база, а Elasticsearch получает копию полей для поиска, обработанную анализаторами: они разбивают текст на токены, приводят к нижнему регистру и к базовой форме слова. Качество поиска определяют три вещи: маппинг, синхронизация и запрос.
Шаг 1. Индекс и анализаторы
Храните каждый язык в своём поле — так у каждого будет правильный анализатор.
- Русский: встроенные стоп-слова и стеммер для русского отбрасывают окончания («телефоны» находит «телефон»). Буквы ё и е полезно приравнять.
- Узбекский: встроенного анализатора нет. Практичная основа — свой анализатор, который приводит текст к нижнему регистру и удаляет разные апострофы (‘ ’ ʻ ʼ ' и обратный апостроф), потому что пользователи пишут o‘zbek, o'zbek и ozbek вперемешку.
PUT /products
{
"settings": {
"analysis": {
"char_filter": {
"uz_apostrophes": {
"type": "pattern_replace",
"pattern": "[‘’ʻʼ'`]",
"replacement": ""
},
"ru_yo": { "type": "mapping", "mappings": ["ё => е", "Ё => Е"] }
},
"analyzer": {
"uz_text": {
"type": "custom",
"char_filter": ["uz_apostrophes"],
"tokenizer": "standard",
"filter": ["lowercase"]
},
"ru_text": {
"type": "custom",
"char_filter": ["ru_yo"],
"tokenizer": "standard",
"filter": ["lowercase", "russian_stop", "russian_stemmer"]
}
},
"filter": {
"russian_stop": { "type": "stop", "stopwords": "_russian_" },
"russian_stemmer": { "type": "stemmer", "language": "russian" }
}
}
},
"mappings": {
"properties": {
"title_ru": {
"type": "text", "analyzer": "ru_text",
"fields": { "suggest": { "type": "search_as_you_type", "analyzer": "ru_text" } }
},
"title_uz": {
"type": "text", "analyzer": "uz_text",
"fields": { "suggest": { "type": "search_as_you_type", "analyzer": "uz_text" } }
},
"description_ru": { "type": "text", "analyzer": "ru_text" },
"description_uz": { "type": "text", "analyzer": "uz_text" },
"category": { "type": "keyword" },
"price": { "type": "integer" },
"updated_at": { "type": "date" }
}
}
}
Узбекский — агглютинативный язык: без стеммера «kitoblar» и «kitob» — разные токены. Частично это закрывают исправление опечаток и поиск по префиксу (ниже). Проверяйте на реальных запросах пользователей, а разбор на токены смотрите через API _analyze.
Тип keyword — для полей, по которым фильтруете и считаете агрегаты (категория, бренд, статус), text — для полей, где ищете.
Шаг 2. Синхронизация с основной базой
| Подход | Как работает | Плюсы и минусы |
|---|---|---|
| Периодическая переиндексация | Задача выбирает строки, у которых изменился updated_at, и отправляет их через Bulk API | Просто; поиск отстаёт на интервал задачи |
| События из приложения | После сохранения приложение кладёт событие в очередь, воркер обновляет индекс | Почти в реальном времени; нужны очередь и повторы |
| Change data capture | Инструмент читает журнал изменений базы и передаёт изменения | Ловит все изменения; больше инфраструктуры |
Практические правила:
- Отправляйте данные пачками через Bulk API, а не по одному документу.
- Явно обрабатывайте удаления, например через колонку
deleted_at. - Читайте и пишите через алиас индекса. Для полной пересборки создайте новый индекс, заполните его и переключите алиас без простоя.
Шаг 3. Устойчивость к опечаткам
fuzziness: "AUTO" допускает небольшое число исправлений символов в зависимости от длины слова.
GET /products/_search
{
"query": {
"multi_match": {
"query": "телфон samsung",
"fields": ["title_ru^3", "title_uz^3", "description_ru", "description_uz"],
"fuzziness": "AUTO",
"prefix_length": 1
}
}
}
prefix_length: 1 требует совпадения первой буквы — меньше шума и быстрее запрос. Буст ^3 поднимает совпадения в названии выше совпадений в описании.
Шаг 4. Автодополнение
Поле search_as_you_type создаёт служебные подполя для поиска по префиксу. Запрашивайте их через bool_prefix:
GET /products/_search
{
"size": 5,
"_source": ["title_ru", "title_uz"],
"query": {
"multi_match": {
"query": "смартф",
"type": "bool_prefix",
"fields": [
"title_ru.suggest", "title_ru.suggest._2gram", "title_ru.suggest._3gram",
"title_uz.suggest", "title_uz.suggest._2gram", "title_uz.suggest._3gram"
]
}
}
}
На фронтенде отправляйте запрос с небольшим debounce и отменяйте устаревшие запросы.
Частые ошибки
- Одно поле
textсо стандартным анализатором для всех языков. - Доступ к Elasticsearch напрямую из браузера. Держите его за бэкендом и никогда не открывайте в интернет без аутентификации.
- Попытка поменять маппинг на месте. Большинство изменений маппинга требует переиндексации в новый индекс.