SELECT * WHERE name LIKE '%запрос%' — это не поиск, это медленное и неудобное «нашли что-то с подстрокой». Настоящий поиск понимает опечатки, синонимы, морфологию русского, релевантность, фасеты. Для этого нужен отдельный движок. Разбираем, что выбрать в 2026.
Когда нужен поисковый движок
| Размер каталога / контента | Решение |
|---|---|
| 100-1000 записей, простые запросы | Postgres FTS (tsvector) |
| 1k-100k, нужны опечатки и фасеты | Meilisearch / Typesense |
| 100k-10M, сложная релевантность | OpenSearch / Elasticsearch |
| > 10M, аналитика и логи | Elasticsearch / OpenSearch |
| Семантический поиск (по смыслу) | pgvector + LLM-embeddings |
Часто на сайте достаточно Postgres FTS — не лезьте в Elasticsearch на 5000 товарах.
Postgres FTS — самое недооценённое
Встроенный полнотекст в Postgres — отлично работает для простых сайтов. Поддерживает русскую морфологию через pg_russian или pg_morph.
ALTER TABLE products ADD COLUMN search tsvector
GENERATED ALWAYS AS (
setweight(to_tsvector('russian', coalesce(name, '')), 'A') ||
setweight(to_tsvector('russian', coalesce(description, '')), 'B')
) STORED;
CREATE INDEX idx_products_search ON products USING GIN(search);
SELECT id, name, ts_rank(search, q) AS rank
FROM products, plainto_tsquery('russian', 'красное платье') q
WHERE search @@ q
ORDER BY rank DESC
LIMIT 20;
Скорость — миллисекунды на сотнях тысяч записей. Не умеет: опечатки, синонимы, гибкие фасеты. Для них — отдельный движок.
Meilisearch — простой и быстрый
Российские разработчики любят за «поставил и работает». Написан на Rust, ставится одним бинарником.
docker run -d -p 7700:7700 -v meili_data:/meili_data \
-e MEILI_MASTER_KEY=secret getmeili/meilisearch:v1.10
Индексирование:
import { MeiliSearch } from "meilisearch";
const client = new MeiliSearch({ host: "http://localhost:7700", apiKey: "secret" });
const index = client.index("products");
await index.addDocuments([
{ id: 1, name: "Платье красное", price: 4990, category: "Одежда" },
{ id: 2, name: "Брюки чёрные", price: 3490, category: "Одежда" },
]);
Поиск:
const results = await index.search("плате крас", {
filter: ["category = 'Одежда'", "price < 6000"],
facets: ["category", "brand"],
limit: 20,
});
Опечатки — из коробки («плате» найдёт «платье»). Синонимы — настраиваются:
await index.updateSettings({
synonyms: { "куртка": ["парка", "анорак"] },
stopWords: ["и", "в", "на"],
});
Минусы: не подходит для миллионов документов с тяжёлой аналитикой. Не настолько гибок, как Elastic.
Typesense — альтернатива Meilisearch
Очень похож, тоже C++. Чуть быстрее на больших объёмах, чуть менее удобный API. В РФ менее популярен.
docker run -p 8108:8108 -v ts_data:/data \
typesense/typesense --api-key=xyz --data-dir=/data
Хорош там, где много фасетов и пользователь крутит фильтры в реальном времени.
Elasticsearch / OpenSearch
Сильнее всех, сложнее всех. Elasticsearch с 2021 закрыл лицензию (SSPL), OpenSearch — форк от AWS на Apache 2.0. Технически почти идентичны.
docker run -d -p 9200:9200 -e "discovery.type=single-node" \
-e "OPENSEARCH_INITIAL_ADMIN_PASSWORD=Strong123!" \
opensearchproject/opensearch:2.15.0
import { Client } from "@opensearch-project/opensearch";
const client = new Client({ node: "https://localhost:9200" });
await client.indices.create({
index: "products",
body: {
mappings: {
properties: {
name: { type: "text", analyzer: "russian" },
price: { type: "integer" },
category: { type: "keyword" },
},
},
},
});
Поиск с релевантностью, фасетами, suggesters, geo-фильтрами, агрегациями. Можно строить аналитику логов, ELK-стек, дашборды.
Но: жрёт RAM (минимум 4 ГБ под небольшой кластер), сложно администрировать, требует понимания шардирования.
Сравнение
| Критерий | Postgres FTS | Meilisearch | Typesense | OpenSearch |
|---|---|---|---|---|
| Установка | уже есть | 1 бинарник | 1 бинарник | сложная, JVM |
| RAM минимум | 256 МБ | 512 МБ | 512 МБ | 4 ГБ |
| Опечатки | нет | да | да | да |
| Морфология русского | да (с расширением) | хорошо | средне | да (analyzer russian) |
| Фасеты | руками | да | да | да |
| Геопоиск | PostGIS | базовый | да | топ |
| Сложные запросы | средне | средне | средне | топ |
| Объём данных | до 10М | до 1-5М | до 5-10М | от 10М до миллиардов |
| Кривая обучения | пологая | пологая | пологая | крутая |
| Лицензия | open source | MIT | Apache 2.0 | Apache 2.0 |
Семантический поиск
Когда пользователь ищет «дешёвая куртка для зимы», обычный полнотекст ищет точные слова. Семантический — понимает смысл.
Решение — embeddings. Преобразуете каждый товар в вектор (через OpenAI Embeddings, российские аналоги вроде ai.gigachat или sbert-models локально), храните в pgvector.
CREATE EXTENSION vector;
ALTER TABLE products ADD COLUMN embedding vector(1536);
CREATE INDEX ON products USING hnsw (embedding vector_cosine_ops);
SELECT id, name FROM products
ORDER BY embedding <=> '[0.1, 0.2, ...]'::vector
LIMIT 20;
В реальности комбинируют: Meilisearch для лексического поиска + pgvector для семантического + переранжирование результатов. Это hybrid search.
Индексация
Главная боль — синхронизация между основной БД и поисковым движком. Варианты:
- При записи: после
INSERT/UPDATEсразу шлёте в индекс. Просто, но добавляет latency и может сломать запись, если индекс лежит. - Очередь: пишете задачу в Redis/RabbitMQ, воркер индексирует асинхронно. Надёжнее, но сложнее.
- CDC (change data capture): Debezium слушает WAL Postgres и пишет в Meili/OS. Идеально для больших систем, сложно настроить.
- Полная переиндексация по cron: раз в час дамп БД → индекс. Дёшево и сердито для небольших каталогов.
Для большинства сайтов — вариант 2 (очередь) с fallback на вариант 4 (раз в сутки полная сверка для надёжности).
Faceted search
Фильтры с подсчётом «сколько товаров в каждой категории» — обязательны для e-commerce.
const results = await index.search("платье", {
facets: ["brand", "color", "size"],
filter: ["price 1000 TO 5000"],
});
// results.facetDistribution = { brand: { Mango: 12, Zara: 8 }, color: { ... } }
В Meilisearch и Typesense — из коробки. В OpenSearch — через aggregations. В Postgres — самописно через GROUP BY.
Подсветка совпадений
Все три движка умеют highlight:
await index.search("платье красное", {
attributesToHighlight: ["name", "description"],
highlightPreTag: "<mark>",
highlightPostTag: "</mark>",
});
В UI выводите подсвеченный текст — пользователь видит, что совпало с его запросом.
Стоимость
| Решение | Хостинг | Месяц |
|---|---|---|
| Postgres FTS | в основной БД | 0 |
| Meilisearch self-hosted | VPS 2 ГБ | 500-1000 ₽ |
| Meilisearch Cloud | managed | $30-100 |
| Typesense Cloud | managed | $30-300 |
| Elastic Cloud | managed | $95-500 |
| OpenSearch self-hosted | VPS 4-8 ГБ | 2-5 тыс. ₽ |
Для российских проектов чаще self-hosted — оплата зарубежных managed сложна.
Итого
Для большинства сайтов с каталогом 1-100 тыс. записей — Meilisearch. Дешёво, быстро, простой API, хорошая поддержка русского. Postgres FTS — если данных мало и не нужны опечатки. OpenSearch — когда нужна аналитика, миллионы документов, сложные запросы, и есть человек, который умеет его готовить.
Частые вопросы
Можно ли использовать Postgres FTS вместо отдельного движка?
Да, для каталогов до десятков тысяч записей и простых запросов. Постгрес умеет морфологию русского (через pg_russian), фасеты можно собирать GROUP BY, ранжирование ts_rank. Минусы: нет нативных опечаток, ограниченная гибкость, на больших объёмах GIN-индекс становится тяжёлым. Если каталог растёт — выносите в Meilisearch.
Meilisearch или Typesense?
Очень похожи. Meilisearch — чуть проще API, лучше документация на русском, российские разработчики чаще его выбирают. Typesense — чуть быстрее на 5+ млн документов, лучше геопоиск. Если сомневаетесь — берите Meilisearch, на типичном e-commerce разницы не заметите.
Elastic или OpenSearch?
OpenSearch — open source форк, без ограничений лицензии, развивается AWS и сообществом. Elastic с 2021 — SSPL, не Apache. Для коммерческого использования и self-hosted предпочтителен OpenSearch. Совместимость кода ~95%, миграция в обе стороны несложная.
Как обновлять индекс при изменении товара?
Лучшая практика — асинхронно через очередь. После UPDATE products пишете задачу в BullMQ/RabbitMQ, воркер берёт и обновляет в Meili/OS. Время от изменения до видимости в поиске — секунды, при сбое индексатора основная БД не страдает. Плюс раз в сутки cron делает полную сверку, чтобы поймать пропущенные.
Сколько RAM нужно для Meilisearch на 100 тыс. товаров?
Около 1-2 ГБ оперативки и 1-2 ГБ диска. Это VPS за 500-1000 ₽/мес. Запросы в десятки миллисекунд. Для 1 млн товаров — 4-8 ГБ RAM. Для миллионов — задумайтесь о Typesense с шардированием или OpenSearch.
Можно ли искать по PDF и Word документам?
Сначала извлечь текст: pdf-parse, mammoth для DOCX, tesseract для сканов. Потом обычное индексирование текста. OpenSearch имеет ingest-pipeline с attachment processor, который сам это умеет. В Meilisearch — на стороне приложения.
Стоит ли делать семантический поиск через embeddings?
Если у вас контент-сайт (статьи, документация, FAQ) — да, hybrid search заметно улучшает релевантность. Для e-commerce — обычно избыточно, пользователи ищут конкретные слова и фильтруют. Embeddings дают +10-20% точности и стоят нагрузки на GPU/API. Сначала попробуйте обычный полнотекст, если запросы реально страдают на смысловом уровне — добавляйте семантику.