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

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

MAATRIX

Если Meilisearch кажется слишком «магическим» — схема угадывается сама, а типы полей вылезают боком только на проде, — присмотритесь к Typesense. Тот же класс задач (быстрый опечатко-устойчивый поиск вместо тяжёлого Elasticsearch), но с обязательной типизированной схемой коллекции и упором на предсказуемость: то, что прошло валидацию при создании индекса, ведёт себя одинаково и через год. Разворачивается движок в Docker Compose буквально одним контейнером — ниже рабочий файл и разбор нюансов, которые на голом docker run не видны.

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

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

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

Чем Typesense отличается от Meilisearch и Elasticsearch

Все три решают похожую задачу — full-text поиск с ранжированием по релевантности, — но идут к ней по-разному.

ElasticsearchMeilisearchTypesense
Схема коллекциимаппинги, можно оставить динамическимине обязательна, движок угадывает типы полейобязательна, поля и типы задаются явно при создании
Язык реализации / рантаймJava, JVMRustC++
Минимальные требования по RAMот 2-4 ГБ только под процессот 0,5-1 ГБ на стартеот 0,5-1 ГБ на старте, дальше растёт с размером индекса в памяти
Опечатко-устойчивостьтребует настройки анализаторовиз коробкииз коробки, с числом опечаток по длине слова
Векторный поискесть (dense_vector)есть с недавних версийесть, включая гибридный поиск (текст + вектор в одном запросе)
Кластеризация / отказоустойчивостьзрелая, шардирование и репликацияограниченнаяRAFT-консенсус, встроен в сам движок

Ключевое практическое отличие Typesense от Meilisearch — обязательная схема. Meilisearch позволяет залить JSON и разобраться с типами полей потом; Typesense потребует заранее сказать, что price — это int32, а tags — это string[]. На старте это кажется лишней бюрократией, но на проде типизированная схема ловит проблему в момент индексации документа, а не в момент, когда пользователь получает пустую выдачу из-за того, что число внезапно приехало строкой. Если такой строгости не хочется — у нас есть отдельная статья про установку Meilisearch на Ubuntu 24.04, там компромисс сделан в другую сторону.

По ресурсам Typesense держит весь индекс в оперативной памяти — это и даёт миллисекундные ответы, и одновременно определяет требования к серверу: для каталога на десятки-сотни тысяч документов хватит 1-2 ГБ RAM, но точный расход зависит от количества полей, их размера и того, сколько из них проиндексировано — ориентируйтесь по факту, замеряя на своих данных, а не по чужим цифрам.

Структура и готовый docker-compose.yml

Каталоги на сервере:

/opt/typesense/
├── data/              # индекс, снапшоты, состояние RAFT
└── docker-compose.yml

Создайте директорию и сгенерируйте API-ключ — Typesense не имеет дефолтного ключа и без него не запустится:

mkdir -p /opt/typesense/data
cd /opt/typesense
openssl rand -hex 24

Сохраните вывод — это и будет TYPESENSE_API_KEY. Держите его отдельно от репозитория, в .env-файле рядом с compose-файлом:

nano .env
TYPESENSE_API_KEY=вставьте_сюда_сгенерированный_ключ

Сам docker-compose.yml:

services:
  typesense:
    image: typesense/typesense:27.1
    container_name: typesense
    restart: unless-stopped
    ports:
      - "127.0.0.1:8108:8108"
    volumes:
      - ./data:/data
    environment:
      - TYPESENSE_API_KEY=${TYPESENSE_API_KEY}
      - TYPESENSE_DATA_DIR=/data
      - TYPESENSE_ENABLE_CORS=true
    ulimits:
      nofile:
        soft: 65536
        hard: 65536

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

  • Версия образа зафиксирована (27.1), а не latest. Typesense между минорными версиями иногда меняет формат хранения данных на диске — обновление должно быть осознанным шагом, а не результатом случайного docker compose pull.
  • Порт 8108 пробрасывается только на 127.0.0.1, а не на все интерфейсы. Публичный доступ к API поиска — это отдельный вопрос, который закрывается реверс-прокси, а не открытым портом наружу.
  • ulimits.nofile подняты явно: при большом количестве одновременных запросов дефолтный лимит открытых файлов в контейнере может стать узким местом раньше, чем закончится CPU.
  • TYPESENSE_ENABLE_CORS=true нужен, только если поисковые запросы идут прямо из браузера через JS-клиент. Если весь поиск проксируется через ваш бэкенд, эту строку лучше убрать — меньше открытых дверей.

Запуск:

docker compose up -d
docker compose logs -f typesense

В логах должна появиться строка Peer refresh succeeded (даже в single-node режиме Typesense поднимает внутренний RAFT-узел) и Setting log level of RAFT library to... — это нормально, не ошибка. Проверка живости:

curl http://127.0.0.1:8108/health

Ответ {"ok":true} означает, что контейнер поднялся и слушает.

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

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

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

Типизированная схема: создание коллекции

В отличие от Meilisearch, здесь нельзя просто залить документы — сначала описывается коллекция с явными типами полей:

curl -X POST 'http://127.0.0.1:8108/collections' \
  -H "X-TYPESENSE-API-KEY: ваш_ключ" \
  -H 'Content-Type: application/json' \
  --data-binary '{
    "name": "products",
    "fields": [
      {"name": "name", "type": "string"},
      {"name": "category", "type": "string", "facet": true},
      {"name": "price", "type": "int32"},
      {"name": "tags", "type": "string[]", "facet": true, "optional": true},
      {"name": "in_stock", "type": "bool"}
    ],
    "default_sorting_field": "price"
  }'

facet: true включает возможность фасетной фильтрации по полю (например, счётчики товаров по категориям в интерфейсе), optional: true разрешает документам не содержать это поле вовсе — без пометки Typesense потребует его в каждом документе и отклонит запись без него. default_sorting_field задаёт числовое поле, по которому результаты сортируются, если явный порядок в запросе не указан — обычно это что-то вроде рейтинга или популярности, не обязательно цена, здесь для примера.

Попытка залить документ с полем не того типа (строка в price вместо числа) вернёт ошибку валидации сразу при индексации — это и есть та самая предсказуемость, ради которой стоит выбирать Typesense вместо схемы, которую движок угадывает сам.

Индексация документов

Документы заливаются пакетом через JSONL (по документу на строку), что заметно быстрее поштучных запросов на больших объёмах:

curl -X POST 'http://127.0.0.1:8108/collections/products/documents/import?action=upsert' \
  -H "X-TYPESENSE-API-KEY: ваш_ключ" \
  -H 'Content-Type: text/plain' \
  --data-binary $'{"id":"1","name":"Смартфон Айфон 15","category":"Электроника","price":79990,"tags":["новинка","хит"],"in_stock":true}\n{"id":"2","name":"Кроссовки беговые","category":"Обувь","price":6990,"in_stock":true}'

action=upsert означает, что документ с тем же id перезапишется, а не задублируется — удобно для повторной синхронизации из основной БД. Если источник данных уже лежит в PostgreSQL, посмотрите статью про установку PostgreSQL на Ubuntu 24.04 — оттуда документы выгружаются периодическим скриптом (cron или очередь на изменение записи) и заливаются в Typesense тем же батч-запросом.

Поиск с опечаткой в слове «айфонн»:

curl -G 'http://127.0.0.1:8108/collections/products/documents/search' \
  -H "X-TYPESENSE-API-KEY: ваш_ключ" \
  --data-urlencode 'q=айфонн' \
  --data-urlencode 'query_by=name,category' \
  --data-urlencode 'filter_by=in_stock:true' \
  --data-urlencode 'sort_by=price:desc'

query_by явно перечисляет поля для полнотекстового поиска — в отличие от Meilisearch, где searchableAttributes настраиваются один раз на индекс, здесь список задаётся прямо в каждом запросе, что даёт гибкость (разный набор полей под разные сценарии поиска без пересоздания коллекции).

Реверс-прокси, ключи доступа и фаервол

Ключ, который вы сгенерировали в .env, — это admin key с полным доступом, включая удаление коллекций. Отдавать его во фронтенд нельзя. Создайте отдельный scoped-ключ только на поиск:

curl -X POST 'http://127.0.0.1:8108/keys' \
  -H "X-TYPESENSE-API-KEY: ваш_admin_ключ" \
  -H 'Content-Type: application/json' \
  --data-binary '{
    "description": "Search-only key for frontend",
    "actions": ["documents:search"],
    "collections": ["products"]
  }'

Такой ключ безопасно встраивать в клиентский JavaScript — он не даёт ни записи, ни удаления, только чтение через поиск.

Наружу API нужно отдавать через HTTPS. Если реверс-прокси ещё не настроен, разберитесь с Caddy с автоматическим SSL на Ubuntu 24.04 — конфиг для Caddyfile такой же лаконичный, как для любого другого сервиса за прокси:

search.example.com {
    reverse_proxy 127.0.0.1:8108
}

Порт 8108 при этом наружу не открывается вообще — доступ к нему только с localhost, через который и работает прокси. Базовую настройку фаервола ufw на сервере, включая то, какие порты стоит держать закрытыми по умолчанию, разбирали в статье про установку UFW на Ubuntu 24.04.

Резервное копирование снапшотом

У Typesense есть встроенный механизм снапшотов — он консистентен на момент снятия и не требует останавливать сервис:

mkdir -p /opt/typesense/snapshots
curl -X POST 'http://127.0.0.1:8108/operations/snapshot?snapshot_path=/data/../snapshots' \
  -H "X-TYPESENSE-API-KEY: ваш_admin_ключ"

Путь снапшота указывается относительно того, что видит контейнер, поэтому проще заранее добавить отдельный volume под снапшоты в docker-compose.yml:

    volumes:
      - ./data:/data
      - ./snapshots:/snapshots

и снимать снапшот в /snapshots вместо трюка с ../. Файлы снапшота дальше нужно забирать на отдельное хранилище — если на сервере уже развёрнут бэкап-пайплайн, туда же можно подключить и Typesense, например через restic в Docker Compose, направив его на каталог /opt/typesense/snapshots.

Восстановление из снапшота — это остановка контейнера, замена содержимого ./data файлами из снапшота и повторный запуск:

docker compose down
rm -rf /opt/typesense/data/*
cp -r /opt/typesense/snapshots/<дата_снапшота>/* /opt/typesense/data/
docker compose up -d

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

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

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

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

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

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

Чем именно типизированная схема Typesense лучше «угадывания» типов в Meilisearch?

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

Нужен ли отдельный сервер под Typesense или можно ставить рядом с приложением?

Для небольших и средних каталогов вполне можно держать в одном Docker Compose стеке с остальными сервисами — движок компактный. Разносить стоит, когда индекс большой и начинает заметно конкурировать за память с основным приложением.

Поддерживает ли Typesense кластеризацию для отказоустойчивости?

Да, встроенный RAFT-консенсус позволяет поднять несколько узлов (обычно нечётное число, 3 или 5) через переменную TYPESENSE_NODES со списком адресов — но это отдельная настройка поверх одноконтейнерного варианта из этой статьи, и для большинства проектов одного узла со снапшотами достаточно.

Что делать, если после обновления образа Typesense не стартует и падает с ошибкой в логах про формат данных?

Скорее всего, между версиями изменился формат хранения — не обновляйтесь через latest вслепую, фиксируйте версию в compose-файле и перед мажорным апдейтом снимайте снапшот, чтобы было куда откатиться.

Можно ли использовать Typesense для векторного или гибридного поиска, а не только текстового?

Да, начиная с версий, где добавлена поддержка embedding-полей — поле объявляется с типом float[] фиксированной размерности, и запрос может комбинировать текстовый и векторный поиск одновременно. Это отдельная тема, требующая своего пайплайна генерации эмбеддингов, здесь не разбиралась.

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

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

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