Представьте: ваш интернет-магазин вырос до 500 000 товаров, и поиск по названию через ILIKE % в PostgreSQL тормозит на каждой букве. Клиенты уходят, конверсия падает — каждая секунда задержки снижает её на 7%. Мы с такой ситуацией сталкивались не раз. В одном проекте поиск по 1 млн товаров занимал 8 секунд, после миграции на Elasticsearch — 80 мс. Конверсия выросла на 15%. За время работы мы настроили Elasticsearch для 30+ проектов — от каталогов до маркетплейсов. Наш опыт показывает: правильная настройка индексов и анализаторов сокращает время поиска в 10 раз.
Почему Elasticsearch, а не полнотекстовый поиск в PostgreSQL?
PostgreSQL умеет полнотекстовый поиск через tsvector, но он не справляется с высокими нагрузками, сложной морфологией и фасетами. Elasticsearch превосходит PostgreSQL в 10-20 раз по скорости на реальных нагрузках. Сравним:
| Критерий | PostgreSQL (ILIKE/tsvector) | Elasticsearch |
|---|---|---|
| Скорость поиска по 1 млн записей | 200-500 мс | 10-50 мс |
| Морфология русского языка | Базовая через словари | Стемминг, синонимы, кастомные анализаторы |
| Фасетная фильтрация | Ограничена | Мощные агрегации |
| Автодополнение | Костыли с триграммами | Edge n-gram, Suggester |
| Геопоиск | Через PostGIS, медленно | Нативно, быстро |
Кроме того, распределенный поиск Elasticsearch позволяет обрабатывать миллиарды документов.
Как мы настраиваем Elasticsearch под вашу задачу
Мы не ставим Elasticsearch «из коробки». Всегда анализируем структуру данных и типичные запросы. Вот реальный пример: для каталога электроники с 200 000 товаров мы создали индекс с двумя анализаторами: русским (стемминг + стоп-слова) и автодополнением (edge n-gram). Настройка заняла 4 дня. В другом проекте потребовался кастомный char_filter для очистки названий от спецсимволов.
Установка Elasticsearch 8.x
# Добавить репозиторий wget -qO - https://artifacts.elastic.co/GPG-KEY-elasticsearch | gpg --dearmor -o /usr/share/keyrings/elasticsearch-keyring.gpg echo "deb [signed-by=/usr/share/keyrings/elasticsearch-keyring.gpg] https://artifacts.elastic.co/packages/8.x/apt stable main" > /etc/apt/sources.list.d/elastic-8.x.list apt update && apt install -y elasticsearch # Сохранить пароль superuser из вывода установки systemctl enable elasticsearch && systemctl start elasticsearch Минимальный конфиг для single-node dev:
# /etc/elasticsearch/elasticsearch.yml cluster.name: myapp-search node.name: node-1 path.data: /var/lib/elasticsearch path.logs: /var/log/elasticsearch network.host: 127.0.0.1 discovery.type: single-node xpack.security.enabled: true xpack.security.http.ssl.enabled: false # для dev; в prod — включить Heap выделяем так: не более 50% RAM, не более 32 ГБ (из-за compressed OOP). Для старта 4 ГБ достаточно.
Маппинг индекса
Маппинг — схема индекса. Неверный маппинг не исправить без переиндексации. Подробнее читайте в маппинг Elasticsearch:
PUT /products { "settings": { "number_of_shards": 2, "number_of_replicas": 1, "analysis": { "analyzer": { "russian_analyzer": { "type": "custom", "tokenizer": "standard", "filter": ["lowercase", "russian_stop", "russian_stemmer"] }, "autocomplete_analyzer": { "type": "custom", "tokenizer": "standard", "filter": ["lowercase", "edge_ngram_filter"] }, "autocomplete_search": { "type": "custom", "tokenizer": "standard", "filter": ["lowercase"] } }, "filter": { "russian_stop": { "type": "stop", "stopwords": "_russian_" }, "russian_stemmer": { "type": "stemmer", "language": "russian" }, "edge_ngram_filter": { "type": "edge_ngram", "min_gram": 2, "max_gram": 20 } } } }, "mappings": { "properties": { "id": { "type": "keyword" }, "name": { "type": "text", "analyzer": "russian_analyzer", "fields": { "autocomplete": { "type": "text", "analyzer": "autocomplete_analyzer", "search_analyzer": "autocomplete_search" }, "keyword": { "type": "keyword" } } }, "description": { "type": "text", "analyzer": "russian_analyzer" }, "category": { "type": "keyword" }, "brand": { "type": "keyword" }, "price": { "type": "scaled_float", "scaling_factor": 100 }, "in_stock": { "type": "boolean" }, "attributes": { "type": "object", "dynamic": true }, "location": { "type": "geo_point" }, "created_at": { "type": "date" } } } } Поисковый запрос с фасетами
POST /products/_search { "query": { "bool": { "must": [ { "multi_match": { "query": "беспроводные наушники", "fields": ["name^3", "description", "name.autocomplete^2"], "type": "best_fields", "fuzziness": "AUTO" } } ], "filter": [ { "term": { "in_stock": true } }, { "range": { "price": { "gte": 1000, "lte": 10000 } } }, { "terms": { "category": ["audio", "headphones"] } } ] } }, "aggs": { "categories": { "terms": { "field": "category", "size": 20 } }, "brands": { "terms": { "field": "brand", "size": 30 } }, "price_ranges": { "range": { "field": "price", "ranges": [ { "to": 1000 }, { "from": 1000, "to": 5000 }, { "from": 5000, "to": 15000 }, { "from": 15000 } ] } } }, "highlight": { "fields": { "name": {}, "description": { "fragment_size": 150 } } }, "from": 0, "size": 24, "sort": [{ "_score": "desc" }, { "created_at": "desc" }] } Как настроить автодополнение?
Автодополнение реализуется через edge n-gram анализатор, как показано в маппинге. Edge n-gram создаёт токены от 2 до 20 символов. Пользователи получают подсказки уже после ввода 2-3 символов — это повышает UX. Поле name.autocomplete индексирует начало каждого слова. В поисковом запросе используем match_phrase_prefix или multi_match по этому полю.
Ориентировочные сроки
Сроки зависят от сложности. Базовая настройка с одним индексом и интеграцией — 3-5 дней. Если нужны автодополнение, фасеты и синхронизация из PostgreSQL — ещё 3-5 дней. Кластер из 3 нод с мониторингом — 1-2 недели. Стоимость рассчитывается индивидуально. Ниже таблица ориентировочных сроков по этапам:
| Этап | Длительность |
|---|---|
| Аналитика и проектирование | 1-2 дня |
| Установка и настройка индекса | 1-2 дня |
| Интеграция с приложением | 2-3 дня |
| Тестирование и оптимизация | 1-2 дня |
| Деплой и мониторинг | 1 день |
Синхронизация данных из PostgreSQL
Для синхронизации используем логическую репликацию через Debezium + Kafka в промышленных сценариях. Для старта подходит периодический реиндекс через cron. Ниже пример на TypeScript:
// sync/product-indexer.ts import { Client } from '@elastic/elasticsearch' import { Pool } from 'pg' const es = new Client({ node: 'http://localhost:9200', auth: { username: 'elastic', password: process.env.ES_PASSWORD! } }) const pg = new Pool({ connectionString: process.env.DATABASE_URL }) export async function indexProduct(id: string) { const { rows } = await pg.query(` SELECT p.*, c.name AS category_name, json_agg(json_build_object('key', a.key, 'value', a.value)) AS attributes FROM products p LEFT JOIN categories c ON c.id = p.category_id LEFT JOIN product_attributes a ON a.product_id = p.id WHERE p.id = $1 GROUP BY p.id, c.name `, [id]) if (!rows.length) { await es.delete({ index: 'products', id }) return } const p = rows[0] await es.index({ index: 'products', id: p.id, document: { id: p.id, name: p.name, description: p.description, category: p.category_name, price: p.price, in_stock: p.stock_quantity > 0, attributes: Object.fromEntries(p.attributes?.map((a: any) => [a.key, a.value]) ?? []), created_at: p.created_at } }) } export async function reindexAll() { const { rows } = await pg.query('SELECT id FROM products WHERE deleted_at IS NULL') const chunks = chunk(rows.map(r => r.id), 100) for (const ids of chunks) { await Promise.all(ids.map(indexProduct)) console.log(`Indexed ${ids.length} products`) } } Пошаговый план настройки
- Аналитика — изучаем структуру данных, типичные запросы, требования к скорости (1-2 дня).
- Проектирование — разрабатываем маппинг, анализаторы, схему синхронизации (1-2 дня).
- Реализация — устанавливаем Elasticsearch, настраиваем индекс, пишем интеграцию (3-5 дней).
- Тестирование — проверяем релевантность поиска, фасеты, скорость (1-2 дня).
- Деплой — разворачиваем на production, настраиваем мониторинг (1 день).
Что входит в работу
После завершения работ вы получаете:
- Настроенный кластер Elasticsearch (одна нода или кластер) с доступом
- Индексы с кастомными анализаторами под русский язык
- Интеграцию с вашим приложением через REST API
- Скрипты синхронизации данных (например, из PostgreSQL)
- Документацию по эксплуатации
- Обучение вашей команды
Мы даём гарантию на 3 месяца: если поиск работает не так, как ожидалось, исправляем бесплатно.
Мониторинг кластера
Рекомендуем отслеживать здоровье кластера через _cluster/health и включать slowlog для поисковых запросов. Для сбора метрик используйте Elastic Metricbeat — это позволяет вовремя заметить деградацию.
Типичные ошибки при настройке
- Неправильный маппинг — dynamic mapping приводит к неожиданным типам полей, что ломает фасеты. Всегда задавайте схему явно.
- Слишком маленький heap — нехватка памяти вызывает частые GC паузы и падение производительности. Выделяйте не менее 50% RAM, но не более 32 ГБ.
- Отсутствие slowlog — без него не увидеть медленные запросы. Включите slowlog в конфиге:
index.search.slowlog.threshold.query.warn: 2s. - Игнорирование реплик — для отказоустойчивости нужно хотя бы 1 реплика. Настройте
number_of_replicas: 1.
Начать работу
Если ваш поиск тормозит или не справляется с нагрузкой — свяжитесь с нами. Мы оценим вашу систему и предложим решение. Закажите настройку Elasticsearch под ключ — получите консультацию бесплатно. Не откладывайте: каждая секунда задержки стоит вам клиентов. Закажите аудит сегодня.







