MAATRIX / Блог / Meilisearch в Docker Compose: готовый файл

Meilisearch в Docker Compose: готовый файл

MAATRIX

Каталог товаров или база статей растут, а LIKE '%запрос%' в PostgreSQL уже не тянет — не находит запрос с опечаткой, не ранжирует по релевантности, тормозит на полном сканировании таблицы. Ставить ради этого Elasticsearch с его JVM и конфигом на сотню строк — оверкилл для проекта, где поиск нужен по паре сущностей. Meilisearch поднимается одним контейнером и из коробки прощает опечатки — ниже рабочий compose-файл и что с ним делать дальше.

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →

Что такое Meilisearch и когда он лучше Elasticsearch

Meilisearch — движок полнотекстового поиска, написанный на Rust, с REST API и упором на скорость «из коробки». В отличие от Elasticsearch, где опечатко-устойчивость, релевантность и фасеты — это конфиг из fuzzy-запросов, анализаторов и маппингов, в Meilisearch это поведение по умолчанию: отправили документы, отправили запрос — получили релевантные результаты с учётом опечаток без единой настройки.

Честное сравнение по задачам:

КритерийMeilisearchElasticsearch
Порог входаодин бинарник/контейнер, минимум конфигакластер, JVM, маппинги, шарды
RAM на стартедесятки–сотни МБот 1–2 ГБ на один узел
Опечатко-устойчивостьвстроена по умолчаниюнужно настраивать fuzzy-запросы
Масштабмиллионы документов на одном узлесотни миллионов — миллиарды, кластер
Аналитика и агрегациибазовые фасетыполноценный агрегационный движок, Kibana
Экосистемамоложе, растётзрелая, огромное сообщество

Если задача — поиск по каталогу товаров, базе статей блога, CRM-клиентам или документации, Meilisearch закрывает её быстрее и дешевле по ресурсам. Если нужна аналитика логов и агрегации уровня Kibana на петабайтах — это территория Elasticsearch или его форка OpenSearch, про который у нас есть отдельная статья с установкой на VPS.

Готовый docker-compose.yml

Рабочий минимальный конфиг для одного узла:

services:
  meilisearch:
    image: getmeili/meilisearch:v1.10
    container_name: meilisearch
    restart: unless-stopped
    environment:
      MEILI_MASTER_KEY: ${MEILI_MASTER_KEY}
      MEILI_ENV: production
      MEILI_NO_ANALYTICS: "true"
    ports:
      - "7700:7700"
    volumes:
      - meili_data:/meili_data
    healthcheck:
      test: ["CMD", "wget", "--spider", "-q", "http://localhost:7700/health"]
      interval: 30s
      timeout: 5s
      retries: 3
    deploy:
      resources:
        limits:
          memory: 1g

volumes:
  meili_data:

Порт 7700 — единственный, через него идут и запросы поиска, и административные операции (создание индексов, загрузка документов, настройки). Разделения на «API» и «админку» здесь нет — всё разграничивается ключами доступа, о них в следующем разделе.

Тег v1.10 вместо latest — сознательный выбор для прода: между минорными версиями иногда меняется формат индекса на диске, и переход должен быть осознанным шагом с бэкапом перед docker compose pull. Актуальный тег смотрите в релизах на GitHub перед первым запуском.

Файл .env рядом с compose:

MEILI_MASTER_KEY=сгенерируйте-случайную-строку-от-32-символов

Сгенерировать ключ можно прямо в терминале:

openssl rand -base64 32
chmod 600 .env

Запуск и проверка:

docker compose up -d
docker compose logs -f meilisearch
curl -s http://localhost:7700/health

Ответ {"status":"available"} значит контейнер поднялся и готов принимать запросы.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Master key, ключи доступа и переменные окружения

MEILI_MASTER_KEY — корневой секрет, с ним можно управлять всем: создавать и удалять индексы, генерировать другие ключи, менять настройки. Без него в режиме production API вообще не отвечает на запросы, требующие авторизации — это защита по умолчанию, в отличие от режима разработки (MEILI_ENV: development), где часть операций доступна без ключа.

Master key используется только для административных задач и выпуска рабочих ключей — в само приложение (фронтенд, мобильное приложение) его передавать нельзя, это равносильно root-паролю от базы. Для клиентского поиска создаётся отдельный ключ с ограниченными правами через API:

curl -X POST 'http://localhost:7700/keys' \
  -H "Authorization: Bearer $MEILI_MASTER_KEY" \
  -H 'Content-Type: application/json' \
  --data '{
    "description": "Search key for frontend",
    "actions": ["search"],
    "indexes": ["products"],
    "expiresAt": null
  }'

Такой ключ умеет только искать по индексу products — его можно спокойно встраивать во фронтенд-код, он не даст изменить или удалить данные. Для операций записи (импорт документов из бэкенда) заведите отдельный ключ с правами documents.add, documents.delete — тот же принцип разделения секретов по задачам, что и в любом продакшен-стеке, подробнее в статье про управление паролями через Docker secrets.

Порт 7700 наружу пробрасывайте только если к нему обращаются внешние клиенты напрямую. Если поиск идёт исключительно из вашего бэкенда, оставьте сервис в docker-сети без публикации порта на хосте:

services:
  backend:
    environment:
      MEILI_URL: http://meilisearch:7700

Это работает, если оба сервиса в одном compose-проекте или в общей внешней сети networks:.

Тома и персистентность данных

Именованный том meili_data хранит и сами индексы, и служебные данные — снапшоты, дампы, task queue. Для прода часто удобнее bind mount на конкретный смонтированный диск:

    volumes:
      - /mnt/storage/meilisearch:/meili_data

Про типы томов и когда какой выбирать — в отдельной статье Docker volumes: типы и когда какой использовать.

Индекс на диске обычно занимает больше места, чем исходные документы в JSON — это плата за структуры данных, которые дают быстрый поиск и опечатко-устойчивость. Ориентировочно закладывайте объём диска в 2–3 раза больше объёма исходных данных, но точная цифра зависит от количества полей и длины текста — проверяйте на своих данных, не полагайтесь на общее правило.

Бэкап делается снапшотом — Meilisearch умеет создавать их сам:

    environment:
      MEILI_DUMP_DIR: /meili_data/dumps
      MEILI_SCHEDULE_SNAPSHOT: "true"
      MEILI_SNAPSHOT_INTERVAL_SEC: "86400"

Снапшоты пишутся в том же томе, поэтому их всё равно нужно копировать наружу отдельным заданием — общие практики регулярного копирования и проверки восстановления разобраны в статье про бэкап Docker volume.

HTTPS и reverse-proxy перед Meilisearch

По умолчанию контейнер отдаёт голый HTTP на 7700, TLS-терминацию выносите на reverse-proxy, как и для любого другого сервиса. Конфиг Nginx:

server {
    listen 443 ssl;
    server_name search.example.com;

    location / {
        proxy_pass http://127.0.0.1:7700;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

Специфики буферизации больших тел запроса, как у объектных хранилищ, здесь нет — документы для индексации обычно небольшие, разве что при массовой загрузке каталога стоит поднять client_max_body_size под объём партии. Порядок настройки Nginx как reverse-proxy с сертификатом с нуля — в статье Nginx как reverse-proxy: пошаговая установка.

Если стек уже за Traefik, добавьте лейблы прямо в сервис meilisearch того же compose-файла — принцип тот же, что в статье Traefik как reverse proxy для Docker:

    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.meili.rule=Host(`search.example.com`)"
      - "traefik.http.routers.meili.tls.certresolver=letsencrypt"
      - "traefik.http.services.meili.loadbalancer.server.port=7700"

Загрузка документов и первый запрос к API

Индекс в Meilisearch создаётся неявно при первой отправке документов — отдельного шага «создать таблицу» с описанием схемы не требуется, движок сам определяет типы полей по первому документу:

curl -X POST 'http://localhost:7700/indexes/products/documents' \
  -H "Authorization: Bearer $MEILI_MASTER_KEY" \
  -H 'Content-Type: application/json' \
  --data '[
    {"id": 1, "name": "Кроссовки беговые", "brand": "Nike", "price": 8990},
    {"id": 2, "name": "Кроссовки для трейла", "brand": "Salomon", "price": 12490}
  ]'

Загрузка документов — асинхронная операция, API сразу возвращает taskUid, а не результат. Статус проверяется отдельным запросом:

curl -s 'http://localhost:7700/tasks/0' -H "Authorization: Bearer $MEILI_MASTER_KEY"

Пока status не станет succeeded, документы в поиске не появятся — скорость индексации зависит от количества и типа полей, длины текста и диска под сервером, готовых цифр здесь не будет, проверяйте на своём объёме данных.

Поиск — обычный POST с текстом запроса:

curl -X POST 'http://localhost:7700/indexes/products/search' \
  -H "Authorization: Bearer $SEARCH_KEY" \
  -H 'Content-Type: application/json' \
  --data '{"q": "кросовки найк"}'

Обратите внимание на опечатки в запросе выше — «кросовки» вместо «кроссовки», «найк» вместо бренда «Nike» без учёта регистра. Оба документа из примера всё равно найдутся, это и есть базовое поведение движка без единой настройки.

Опечатко-устойчивость, синонимы и настройка релевантности

Опечатко-устойчивость (typo tolerance) в Meilisearch включена по умолчанию и учитывает длину слова: для коротких слов (1–4 символа) опечатки не прощаются вообще — иначе поиск начнёт путать разные короткие слова между собой, для слов от 5 до 8 символов допускается одна опечатка, от 9 и длиннее — до двух. Поведение настраивается на уровне индекса:

curl -X PATCH 'http://localhost:7700/indexes/products/settings/typo-tolerance' \
  -H "Authorization: Bearer $MEILI_MASTER_KEY" \
  -H 'Content-Type: application/json' \
  --data '{
    "minWordSizeForTypos": {"oneTypo": 4, "twoTypos": 8}
  }'

Синонимы полезны для брендов и сокращений, где опечатко-устойчивость не спасает — «айфон» и «iPhone» это не опечатка, а разные слова:

curl -X PATCH 'http://localhost:7700/indexes/products/settings/synonyms' \
  -H "Authorization: Bearer $MEILI_MASTER_KEY" \
  -H 'Content-Type: application/json' \
  --data '{"айфон": ["iphone"], "iphone": ["айфон"]}'

Релевантность результатов управляется правилами ранжирования — по умолчанию цепочка words, typo, proximity, attribute, sort, exactness: сначала совпадение по словам, затем количество опечаток, затем близость слов друг к другу. Порядок переопределяется под задачу, например поднять exactness выше, если важнее точное совпадение названия, чем близость слов в тексте.

Фильтруемые и сортируемые поля нужно объявить явно — без этого запрос с filter вернёт ошибку, а не пустой результат:

curl -X PUT 'http://localhost:7700/indexes/products/settings/filterable-attributes' \
  -H "Authorization: Bearer $MEILI_MASTER_KEY" \
  --data '["brand", "price"]'

После этого в поиске доступна фильтрация вида filter: "brand = Salomon AND price < 15000" — комбинация полнотекстового поиска и точных условий в одном запросе, без отдельного SQL-фильтра поверх результатов.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Нужны сами нейросети для контента?

Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.

Частые вопросы

Meilisearch может заменить Elasticsearch полностью?

Для поиска по каталогу, базе статей или CRM — да, и с меньшим порогом входа. Для аналитики логов и агрегаций уровня Kibana на сотнях миллионов документов — нет, там нужен Elasticsearch или OpenSearch.

Сколько документов выдержит один узел?

Официально движок рассчитан на миллионы документов на одном инстансе, но точный потолок зависит от RAM, диска и размера самих документов — проверяйте нагрузочным тестом на своём железе.

Нужна ли отдельная база данных вместо Meilisearch?

Нет, это не замена основной БД — отдельный поисковый индекс рядом с ней. Данные и их целостность остаются за СУБД, Meilisearch синхронизируется с ней вашим кодом.

Как обновлять документы при изменениях в основной базе?

Тем же эндпоинтом POST /indexes/{index}/documents — Meilisearch делает upsert по полю id, перезаписывая документ целиком, а не мержа по полям.

Что будет, если контейнер перезапустится во время индексации?

Незавершённые задачи попадают в очередь заново благодаря restart: unless-stopped и персистентному тому — проверьте статус через /tasks после перезапуска.

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →