Webdev
Backend

Мгновенный поиск по 50 000+ товарам: устройство OpenSearch

Почему PostgreSQL ILIKE не справляется с большим каталогом, как устроен OpenSearch и сколько стоит держать его в продакшене.

23 апреля 2026 г.4 мин чтенияBoris · e-webdev
Мгновенный поиск по 50 000+ товарам: устройство OpenSearch

Когда у магазина 200 товаров, поиск по WHERE name ILIKE '%query%' работает мгновенно. Когда товаров 50 000 — каждый запрос выполняется секунду. Когда 500 000 — поиск превращается в проблему. В этой статье разберу, как мы решаем поиск в больших каталогах с помощью OpenSearch.

Почему PostgreSQL не справляется с поиском

PostgreSQL — отличная реляционная БД. Но полнотекстовый поиск по большим объёмам не его задача:

  • ILIKE без индекса: full table scan, O(n) на каждый запрос
  • tsvector (встроенный FTS): лучше, но без морфологии и нечёткого поиска
  • Нет фасетов из коробки: «найти все ноутбуки в категории X с RAM > 16 ГБ от бренда Lenovo» — это сложный SQL с GROUP BY
  • Релевантность примитивная: только TF-IDF, без BM25 или семантики

При 50k+ записей и сложных фильтрах PostgreSQL выдаёт 800-1500 мс на запрос. Для UX это смерть — пользователь уходит.

Что такое OpenSearch

OpenSearch — это форк Elasticsearch, который Amazon выделил после изменения лицензии в 2021 году. Под капотом — Apache Lucene, индексирующая библиотека из Java-мира с 25-летней историей.

Принцип работы:

  1. Каждый документ (товар, объявление, статья) индексируется как набор полей
  2. Каждое поле проходит анализаторы: токенизация, нормализация регистра, морфология (для русского — стемминг и лемматизация)
  3. Индекс хранится в специальной структуре — inverted index — где ключ это токен, значение это список документов с этим токеном
  4. Поиск находит документы по токенам и ранжирует по релевантности (BM25 по умолчанию)

Результат: поиск по 50k+ записям с фильтрами — 10-50 мс. По 1М+ записей — 100-200 мс.

Реальный пример: Diamond Pharm

В моём проекте Diamond Pharm 1700+ товаров профессиональной косметики. На PostgreSQL поиск работал, но фильтры тормозили: «биоревитализация по гиалуроновой кислоте от 22мг/мл объёмом 2мл от бренда Aquashine» — это 8 фильтров, JOIN с таблицей атрибутов, скан большой выборки. 800-1200 мс.

Перенёс товары в OpenSearch. Тот же запрос — 25 мс. Без оптимизаций. Из коробки.

python
# Запрос к OpenSearch на Python
from opensearchpy import OpenSearch

client = OpenSearch(...)

result = client.search(
    index="products",
    body={
        "query": {
            "bool": {
                "must": [{"match": {"name": "биоревитализация"}}],
                "filter": [
                    {"term": {"category.slug": "biorevitalization"}},
                    {"term": {"brand": "Aquashine"}},
                    {"range": {"hyaluronic_concentration": {"gte": 22}}},
                    {"term": {"volume_ml": 2}},
                ]
            }
        },
        "aggs": {
            "by_brand": {"terms": {"field": "brand"}},
            "price_range": {"stats": {"field": "price"}}
        }
    }
)

Один запрос возвращает и подходящие товары, и фасеты для боковой фильтрации.

Архитектура индексации

Главный вопрос — как держать OpenSearch синхронизированным с PostgreSQL. Три варианта:

1. Синхронная индексация при сохранении

python
# Django signal
@receiver(post_save, sender=Product)
def index_product(sender, instance, **kwargs):
    es_client.index(index="products", id=instance.id, body=instance.to_es())

Плюс: всегда актуально. Минус: каждое сохранение — лишний сетевой запрос. При большом импорте (10k товаров за раз) — медленно.

2. Асинхронная очередь через Celery

python
@receiver(post_save, sender=Product)
def queue_indexing(sender, instance, **kwargs):
    index_product_task.delay(instance.id)

@app.task
def index_product_task(product_id):
    product = Product.objects.get(id=product_id)
    es_client.index(...)

Плюс: не блокирует основной поток. Минус: задержка 1-5 секунд между сохранением и видимостью в поиске.

3. CDC через Debezium

Для серьёзного продакшена с большим объёмом изменений: Debezium слушает WAL PostgreSQL и автоматически индексирует все изменения в OpenSearch. Самый сложный, но и самый надёжный вариант.

В Diamond Pharm я остановился на варианте 2 (Celery) — задержка приемлемая, инфраструктура простая.

Морфология русского языка

Главная сложность поиска по-русски — морфология. «Купить» и «покупаю» — это разные строки, но один смысл. PostgreSQL FTS этого не понимает. OpenSearch с правильным анализатором — понимает.

json
{
  "settings": {
    "analysis": {
      "analyzer": {
        "ru_analyzer": {
          "tokenizer": "standard",
          "filter": ["lowercase", "russian_morphology", "english_morphology"]
        }
      }
    }
  }
}

Russian morphology plugin делает стемминг (купить → купи) и лемматизацию (купил → купить). После этого «купить ботокс» находит и «куплю ботокс», и «купите ботокс», и «купи ботокс».

Сколько это стоит

Реальные расходы для проекта 1700-50k товаров:

Вариант 1: AWS OpenSearch Service (managed)

  • t3.small.search × 1 (single-AZ, для production-ready минимум 2)
  • $35-45 в месяц

Вариант 2: Self-hosted на VPS

  • 2 vCPU + 4 GB RAM VPS = $15-20/мес
  • Time on maintenance: ~2 часа в месяц

Для каталога до 100k товаров я обычно беру self-hosted — экономия в 2-3 раза, минимальные требования к поддержке.

Стоит ли подключать OpenSearch с самого начала

Нет, если:

  • Каталог < 5000 товаров
  • Простой поиск по name + 1-2 фильтра
  • Нет планов на скорое масштабирование

Да, если:

  • 10k+ товаров
  • Сложные фильтры с фасетами
  • Морфология русского критична
  • Аналитика по запросам нужна (что искали, что нашли)

Альтернативы OpenSearch

Не всем нужен Elasticsearch-class инструмент:

  • Meilisearch — лёгкая альтернатива, проще в установке, отличный для каталогов до 10М документов
  • Typesense — open-source, единый бинарник, очень быстрый
  • Algolia — managed-сервис, отличный UX, дорогой при росте
  • PostgreSQL pg_trgm + tsvector — для каталогов до 50k и простого поиска бывает достаточно

В Diamond Pharm я выбрал OpenSearch потому что планировался рост до 50k+, нужны были сложные фасеты и хотелось контроля над инфраструктурой. Для меньших каталогов Meilisearch часто более рациональный выбор.

Итог

OpenSearch — мощный инструмент для каталогов от 10k записей. Решает проблему медленного поиска и сложных фильтров за вечер интеграции. Стоит $20-50/мес инфры. Для серьёзного e-commerce, B2B-маркетплейса или контент-портала — почти всегда правильный выбор.

Не подключайте на ранних этапах: сначала проверьте, что pg_trgm + tsvector справляются. Когда упрётесь — переходите без сожалений.

Теги#opensearch#search#elasticsearch#performance#backend
ПоделитьсяTelegramWhatsApp

Хотите такое же?

Соберу стек под ваш проект