Внедрение полнотекстового поиска: Manticore Search vs Elasticsearch для каталога из 2 млн товаров

Задача клиента

Компания «КаталогПро» — крупный маркетплейс электроники — обратилась к itfresh.ru с проблемой: поиск по каталогу из 2 миллионов товаров работал через LIKE '%query%' в PostgreSQL и отвечал за 3-5 секунд. Конверсия падала, пользователи уходили после первого запроса. Нужен был полноценный поисковый движок с морфологией, fuzzy-поиском и фасетной фильтрацией.

Изначально клиент склонялся к Elasticsearch — де-факто стандарту в индустрии. Мы предложили рассмотреть Manticore Search как альтернативу на C++, которая экономит RAM и проще в эксплуатации. Чтобы принять обоснованное решение, мы развернули оба движка и провели серию бенчмарков на реальных данных клиента.

Архитектура: два подхода к поиску

Elasticsearch построен на Apache Lucene (Java). Каждая нода — JVM-процесс с управляемой heap-памятью. Для кластера из трёх нод требуется минимум 3 x 8 GB RAM = 24 GB только под JVM heap, плюс память для OS page cache и off-heap буферов.

Manticore Search — форк Sphinx, полностью переписанный на C++. Работает как единый процесс без JVM, без сборщика мусора, без GC-пауз. Минимальные требования — 512 MB RAM для индекса в 2 млн документов.

Мы развернули тестовый стенд на двух идентичных серверах (8 vCPU, 32 GB RAM, NVMe SSD):

# Установка Manticore Search на Ubuntu 22.04
wget https://repo.manticoresearch.com/manticore-repo.noarch.deb
dpkg -i manticore-repo.noarch.deb
apt update && apt install -y manticore manticore-columnar-lib

systemctl enable manticore
systemctl start manticore

# Проверка статуса
mysql -h 127.0.0.1 -P 9306 -e "SHOW STATUS"
# Установка Elasticsearch 8.x
wget https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-8.12.0-amd64.deb
dpkg -i elasticsearch-8.12.0-amd64.deb

# Настройка JVM heap
cat > /etc/elasticsearch/jvm.options.d/heap.options << 'EOF'
-Xms8g
-Xmx8g
EOF

systemctl enable elasticsearch
systemctl start elasticsearch

Создание индексов и загрузка данных

Первый шаг — импорт каталога из PostgreSQL. У клиента товары хранились в таблице products с полями: name, description, category, brand, price, attributes (JSONB). Мы создали индексы в обоих движках.

Конфигурация индекса в Manticore через SQL-интерфейс:

-- Подключение через mysql-клиент
mysql -h 127.0.0.1 -P 9306

-- Создание таблицы с real-time индексом
CREATE TABLE products (
    id BIGINT,
    name TEXT,
    description TEXT,
    category STRING,
    brand STRING,
    price FLOAT,
    in_stock INTEGER,
    rating FLOAT,
    created_at TIMESTAMP
) morphology='stem_ru, stem_en, soundex'
  min_word_len='2'
  charset_table='non_cjk, cjk'
  html_strip='1';

Для Elasticsearch — маппинг через JSON API:

curl -X PUT "localhost:9200/products" -H 'Content-Type: application/json' -d '{
  "settings": {
    "number_of_shards": 3,
    "number_of_replicas": 1,
    "analysis": {
      "filter": {
        "russian_stemmer": { "type": "stemmer", "language": "russian" },
        "english_stemmer": { "type": "stemmer", "language": "english" }
      },
      "analyzer": {
        "product_analyzer": {
          "tokenizer": "standard",
          "filter": ["lowercase", "russian_stemmer", "english_stemmer"]
        }
      }
    }
  },
  "mappings": {
    "properties": {
      "name": { "type": "text", "analyzer": "product_analyzer" },
      "description": { "type": "text", "analyzer": "product_analyzer" },
      "category": { "type": "keyword" },
      "brand": { "type": "keyword" },
      "price": { "type": "float" },
      "in_stock": { "type": "integer" },
      "rating": { "type": "float" }
    }
  }
}'

Загрузку данных из PostgreSQL в Manticore мы автоматизировали через FEDERATED-таблицу MySQL, что позволило обойтись без промежуточных скриптов.

Поисковые запросы: морфология, стемминг, fuzzy

Главное требование клиента — поиск должен понимать русскую морфологию. Пользователь вводит «красные наушники», а в каталоге — «красный наушник». Оба движка справляются, но по-разному.

Поиск в Manticore через SQL:

-- Полнотекстовый поиск с ранжированием
SELECT id, name, price, WEIGHT() AS relevance
FROM products
WHERE MATCH('красные беспроводные наушники')
  AND in_stock = 1
  AND price BETWEEN 2000 AND 15000
ORDER BY relevance DESC, rating DESC
LIMIT 20
OPTION ranker=proximity_bm25;

-- Fuzzy-поиск (для опечаток)
SELECT id, name, WEIGHT() AS relevance
FROM products
WHERE MATCH('наушнеки блютуз')
OPTION fuzzy=1, max_edits=2;

-- Поиск с подстановками и soundex
CALL SUGGEST('наушнеки', 'products');

Аналогичный запрос в Elasticsearch:

curl -X POST "localhost:9200/products/_search" -H 'Content-Type: application/json' -d '{
  "query": {
    "bool": {
      "must": {
        "multi_match": {
          "query": "красные беспроводные наушники",
          "fields": ["name^3", "description"],
          "fuzziness": "AUTO"
        }
      },
      "filter": [
        { "term": { "in_stock": 1 } },
        { "range": { "price": { "gte": 2000, "lte": 15000 } } }
      ]
    }
  },
  "sort": [
    { "_score": "desc" },
    { "rating": "desc" }
  ],
  "size": 20
}'

По качеству результатов оба движка показали сопоставимую релевантность. Однако Manticore предоставляет встроенную функцию CALL SUGGEST для исправления опечаток, тогда как в Elasticsearch требуется отдельный suggester-запрос.

Real-time индексация и фасетный поиск

В каталоге «КаталогПро» товары обновляются постоянно: меняются цены, появляются новинки, заканчивается наличие. Критично, чтобы изменения появлялись в поиске мгновенно.

Elasticsearch буферизует записи и делает их доступными для поиска с задержкой ~1 секунда (параметр refresh_interval). Manticore работает в true real-time режиме — вставленный документ доступен для поиска немедленно.

Мы настроили синхронизацию через триггер в PostgreSQL и очередь:

-- Триггер на обновление товара
CREATE OR REPLACE FUNCTION sync_product_to_search()
RETURNS trigger AS $$
BEGIN
    PERFORM pg_notify('product_changed', json_build_object(
        'id', NEW.id,
        'action', TG_OP,
        'name', NEW.name,
        'price', NEW.price,
        'in_stock', NEW.in_stock
    )::text);
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER product_search_sync
    AFTER INSERT OR UPDATE ON products
    FOR EACH ROW EXECUTE FUNCTION sync_product_to_search();

Фасетный поиск — группировка результатов по категориям, брендам, ценовым диапазонам — реализован в Manticore через FACET:

SELECT id, name, price
FROM products
WHERE MATCH('наушники')
FACET category ORDER BY COUNT(*) DESC
FACET brand ORDER BY COUNT(*) DESC LIMIT 10
FACET price RANGE(0, 50000, 5000);

Один SQL-запрос возвращает и результаты, и фасеты. В Elasticsearch для этого используются aggregations — мощный, но более многословный JSON.

Бенчмарки на реальных данных

Мы загрузили полный каталог «КаталогПро» (2 014 387 товаров) в оба движка и прогнали серию тестов. Использовали 500 реальных поисковых запросов из логов клиента, воспроизводили нагрузку через wrk с 50 concurrent connections.

МетрикаManticore SearchElasticsearch
Время индексации полного каталога4 мин 12 сек28 мин 45 сек
Размер индекса на диске1.8 GB4.2 GB
Потребление RAM (рабочее)680 MB9.4 GB
Средняя латентность (простой поиск)3.2 мс18 мс
Средняя латентность (поиск + фасеты)8.7 мс42 мс
P99 латентность15 мс95 мс
Throughput (QPS)12 4003 200

Manticore показал себя в 4-5 раз быстрее на поисковых запросах и в 14 раз экономнее по памяти. Разница объясняется отсутствием JVM overhead и оптимизированными структурами данных на C++.

Для проверки мы использовали встроенный профилировщик Manticore:

SET profiling = 1;

SELECT id, name, WEIGHT() AS w
FROM products
WHERE MATCH('беспроводные наушники sony')
LIMIT 20;

SHOW PROFILE;
-- +------------+----------+
-- | Status     | Duration |
-- +------------+----------+
-- | local_df   | 0.000012 |
-- | local_search | 0.002841 |
-- | sql_parse  | 0.000038 |
-- | fullscan   | 0.000000 |
-- | finalize   | 0.000021 |
-- +------------+----------+

Интеграция с PostgreSQL и продакшен-деплой

Для продакшена мы выбрали Manticore Search. Финальная архитектура:

  • PostgreSQL 15 — основная база данных товаров (источник истины)
  • Manticore Search — поисковый индекс с real-time обновлениями
  • Python-синхронизатор — слушает pg_notify и обновляет индекс
  • Nginx — проксирование поисковых запросов на Manticore

Конфигурация Manticore для продакшена:

# /etc/manticoresearch/manticore.conf
searchd {
    listen = 127.0.0.1:9306:mysql
    listen = 127.0.0.1:9308:http
    log = /var/log/manticore/searchd.log
    query_log = /var/log/manticore/query.log
    pid_file = /run/manticore/searchd.pid
    binlog_path = /var/lib/manticore/binlog
    binlog_flush = 2
    binlog_max_log_size = 256M
    max_packet_size = 128M
    workers = thread_pool
    thread_pool_workers = 4
}

common {
    plugin_dir = /usr/local/lib/manticore
}

Скрипт синхронизации на Python (фрагмент):

import psycopg2
import psycopg2.extensions
import MySQLdb  # manticore mysql protocol

def sync_loop():
    pg_conn = psycopg2.connect(dsn=PG_DSN)
    pg_conn.set_isolation_level(
        psycopg2.extensions.ISOLATION_LEVEL_AUTOCOMMIT
    )
    cursor = pg_conn.cursor()
    cursor.execute("LISTEN product_changed;")

    manticore = MySQLdb.connect(
        host='127.0.0.1', port=9306, db=''
    )

    while True:
        if select.select([pg_conn], [], [], 5) != ([], [], []):
            pg_conn.poll()
            while pg_conn.notifies:
                notify = pg_conn.notifies.pop(0)
                payload = json.loads(notify.payload)
                update_manticore_index(manticore, payload)

После деплоя среднее время ответа поиска упало с 3.5 секунд до 8 мс. Конверсия из поиска в карточку товара выросла на 34%.

Выводы и рекомендации

Manticore Search оказался идеальным выбором для задач полнотекстового поиска в e-commerce каталогах. Основные преимущества перед Elasticsearch в нашем кейсе:

  • RAM: 680 MB vs 9.4 GB — позволяет запускать поиск на тех же серверах, где стоит PostgreSQL
  • Скорость: в 4-5 раз быстрее на поисковых запросах, в 7 раз быстрее на индексации
  • Простота: SQL-интерфейс понятен любому бэкенд-разработчику, не нужно учить Query DSL
  • Real-time: мгновенная видимость изменений без refresh_interval

Elasticsearch остаётся лучшим выбором, когда нужны: Kibana для аналитики логов, развитая экосистема (Logstash, Beats), схемаless-индексирование JSON-документов произвольной структуры. Для поиска по структурированному каталогу Manticore экономнее и быстрее.

Если ваш проект требует внедрения полнотекстового поиска — обращайтесь к нам в itfresh.ru. Мы подберём оптимальный движок под ваши данные и нагрузку.

Часто задаваемые вопросы

Да, Manticore форкнулся от Sphinx 2.3.2 и активно развивается с 2017 года. Проект полностью open source (GPLv2), получает регулярные релизы и имеет коммерческую поддержку. В отличие от Sphinx, Manticore добавил columnar storage, auto-репликацию через Galera и Kubernetes Helm charts.
Частично. Manticore хорошо справляется с полнотекстовым поиском по логам, но пока не имеет полноценного аналога Kibana. Интеграция с Logstash и Beats находится в beta-стадии. Для аналитики логов Elasticsearch/OpenSearch с Kibana/Grafana остаётся более зрелым решением.
Manticore использует Galera для синхронной репликации — ту же библиотеку, что и MariaDB Galera Cluster. Управление кластером выполняется через SQL-команды: CREATE CLUSTER, JOIN CLUSTER, ALTER CLUSTER. Шардирование пока ручное, автоматический resharding в планах разработки.
Зависит от объёма текста и атрибутов. Для каталога товаров (name + description + 10 атрибутов) — около 3-4 GB RAM. С columnar storage потребление снижается до 1.5-2 GB за счёт блочного сжатия. Для сравнения: Elasticsearch на тех же данных потребует 20-30 GB.
Да. Встроенная поддержка стемминга для русского и английского языков, soundex для фонетического поиска, CALL SUGGEST для исправления опечаток. Для продвинутой морфологии можно подключить словарь Hunspell или использовать лемматизатор через плагин.

Нужна помощь с проектом?

Специалисты АйТи Фреш помогут с архитектурой, DevOps, безопасностью и разработкой — 15+ лет опыта

📞 Связаться с нами
#manticore search#elasticsearch#полнотекстовый поиск#e-commerce#фасетный поиск#морфология#стемминг#индексация
Комментарии 0

Оставить комментарий

загрузка...