Meilisearch: быстрый поиск, фильтры и PHP — живая мастерская
Собираем поиск по каталогу с исправлением опечаток, фильтрами и подсветкой. Мини-мастерская, 200 учебных товаров, настоящий PHP-проект с Meilisearch и размещение на Linux VPS.

Что такое Meilisearch и какую задачу он решает
Meilisearch — отдельный поисковый движок с HTTP API. Вы передаёте ему документы: товары, статьи, инструкции, карточки услуг или задачи. Приложение отправляет запрос и получает подходящие записи, их порядок, подсветку совпадений и распределение по фильтрам. Для посетителя это выглядит как привычная строка поиска, которая помогает найти нужное даже по началу слова или с небольшой опечаткой.
Инструмент особенно интересен там, где человек не знает точное название. Покупатель пишет «гарнитура», сотрудник ищет инструкцию по нескольким словам, менеджер вспоминает часть названия услуги. Хороший результат в таких случаях зависит от состава документов, настроек релевантности и понятного интерфейса. Простая установка движка не заменяет эту работу, но даёт готовые механизмы, из которых её можно собрать.
В материале используется Meilisearch 1.56.1 Community Edition и самостоятельный PHP-проект. Платный аккаунт и облачный сервис для запуска не нужны. Community Edition распространяется под MIT; репозиторий Meilisearch также содержит компоненты Enterprise под BUSL, поэтому называть абсолютно весь репозиторий «только MIT» некорректно. Функции Enterprise в нашей мастерской не используются. Версия закреплена в Docker-конфигурации. См. официальное описание редакций и релиз 1.56.1.
Когда нужен поисковик, а когда хватает базы данных
Если приложение ищет заказ по точному номеру, обычный индекс базы данных часто полностью решает задачу. PostgreSQL и другие СУБД также умеют полнотекстовый поиск: выбирать отдельный сервис только ради наличия поисковой строки не стоит. Дополнительный движок приносит свои расходы: синхронизацию документов, мониторинг, резервные копии и обновления.
Meilisearch имеет смысл рассматривать, когда одновременно нужны поиск во время ввода, терпимость к опечаткам, ранжирование по нескольким признакам, подсветка и удобные фасеты. Например, в каталоге посетитель начинает с общего запроса, затем оставляет нужный бренд, ограничивает цену и смотрит только доступные товары. В административной панели такая же цепочка помогает найти статью, шаблон письма или внутренний документ.
При этом основная база остаётся источником истины. Остатки, права пользователя, актуальную стоимость и возможность оформить заказ сервер проверяет по рабочим данным. Поисковый индекс — подготовленное представление для быстрого подбора кандидатов. Такая схема позволяет перестроить поиск из базы и не превращает временное отставание индекса в финансовую или бизнес-ошибку.
Как связаны браузер, PHP и Meilisearch
В нашем проекте браузер обращается к api/search.php на том же сайте. PHP проверяет параметры, собирает разрешённый запрос и отправляет его во внутренний Meilisearch. Результат возвращается JSON, после чего JavaScript обновляет карточки и счётчики. Отдельный процесс подготовки создаёт индекс, применяет настройки и загружает учебные товары.
Посетитель → HTML + JavaScript → PHP /api/search.php
↓ search-only key
Meilisearch /indexes/products/search
Администратор → setup → настройки + документы + отдельный ключ поиска
Веб-контейнер получает только ключ чтения конкретного индекса. Секрет для управления ключами доступен служебному процессу подготовки и самому движку. Этот раздел важен и для понимания установки: Meilisearch не является PHP-библиотекой, которая начинает работать после подключения одного файла. Это самостоятельный сервис. PHP здесь выступает клиентом его API и границей между пользовательским запросом и внутренней инфраструктурой.
Такой подход легко перенести в Laravel, Symfony или собственное приложение. Меняется способ получения текущего пользователя и разрешённых фильтров, а общая цепочка остаётся прежней. Для публичного товарного каталога можно строить и прямую клиентскую интеграцию с ограниченным ключом, но пример специально использует серверный посредник: его проще разобрать, ограничить и адаптировать к закрытым данным.
Мини-мастерская: что можно попробовать прямо здесь
Внутри страницы доступны 200 синтетических товаров, поисковая строка, категории, бренды, наличие, ограничение цены и сортировка. Восемь кнопок показывают разные задачи: начало слова, опечатку, поиск по тегам, набор фильтров, цену, фасеты, подсветку и пустой результат. Под выдачей находятся вкладки запроса, ответа и рабочего PHP-кода. Параметры можно менять, а исходник — копировать или скачивать.
На сайте предусмотрены два честно обозначенных режима. Собственный ввод работает локально по JSON: буквальные вхождения слов, фильтры и сортировка. Кнопки готовых сценариев показывают заранее полученные ответы настоящего Meilisearch. Это позволяет увидеть исправление опечатки, не выдавая локальную фильтрацию за работу серверного движка. У записи сохранена первая страница; остальные доступны в проекте с сервером.
Скачанный архив запускает третий режим: тот же интерфейс действительно обращается к PHP и Meilisearch. Там произвольный запрос проверяется настоящим движком, работает серверная пагинация и можно менять индекс. Над результатом всегда указано, откуда он получен. Время обработки в записанном ответе относится к тому измерению, а не к скорости вашего браузера или будущего VPS.
Внутри встроенной мастерской можно прокручивать страницу. На телефоне удобнее открыть её на весь экран.
Подготовка документов и настройки индекса
Каждый товар — самостоятельный JSON-документ со стабильным id. Название, описание, бренд и теги помогают искать; числовая цена и логическое наличие используются для фильтрации. В учебных данных цена хранится целым количеством рублей. В настоящем магазине для точных расчётов обычно удобнее хранить целые минимальные денежные единицы и отдельно учитывать валюту.
{
"id": 101,
"sku": "LAB-0101",
"title": "Настольная лампа Рабочая 100",
"category": "Свет",
"brand": "Lumen",
"description": "Освещение рабочего стола",
"price": 2300,
"inStock": false,
"rating": 3.5,
"tags": ["свет", "лампа", "стол"]
}
В bin/setup.php явно заданы поля поиска, фильтрации, сортировки и выдачи. Это делает поведение проекта воспроизводимым. Например, price разрешена для фильтрации и сортировки, а tags участвует в текстовом поиске. Порядок searchableAttributes выражает приоритет полей при соответствующем правиле ранжирования. При добавлении собственного свойства нужно решить, какую задачу оно выполняет, а не автоматически делать доступными все поля базы.
Документ для поиска полезно собирать отдельно от ORM-модели. В нём обычно не нужны служебные заметки, себестоимость, токены или внутренние контакты. Название категории можно продублировать ради поиска, а стабильный идентификатор сохранить для фильтра. Такой небольшой слой преобразования облегчает переименование полей и последующую переиндексацию. Базовая модель данных описана в документации о документах Meilisearch.
1. Поиск по началу слова: результат во время ввода
Выберите сценарий «Поиск по началу слова». Запрос клав возвращает 20 клавиатур, хотя полное слово ещё не введено. Для товарного каталога это сокращает путь до результата: посетителю не обязательно допечатывать название и нажимать отдельную кнопку. В справочнике такую же механику можно использовать при подборе услуги или статьи помощи.
{"q":"клав","page":1,"hitsPerPage":12}
Meilisearch применяет префиксный поиск к последнему слову запроса. Это не поиск произвольной подстроки в любой позиции: начало слова и его середина — разные случаи. Если пользователь регулярно вводит часть артикула, следует отдельно проверить этот сценарий и подготовить соответствующие поля. Правило описано в документации prefix search.
В интерфейсе между вводом и запросом стоит небольшая задержка, чтобы не отправлять обращение на каждое мгновенное изменение строки. После смены запроса страница сбрасывается на первую. Иначе человек мог бы искать новый товар, оставаясь на пятой странице предыдущей выдачи, и ошибочно решить, что ничего нет. На своём сервере попробуйте клав, затем более конкретное слово и сравните не только число карточек, но и их порядок.
2. Опечатка: помочь человеку, не скрывая его запрос
Сценарий «Опечатка в запросе» использует механическя: в слове пропущена буква. Сохранённый ответ настоящего движка содержит 20 подходящих клавиатур. Локальная буквальная фильтрация такого результата не обещает: если изменить запись и выполнить её в локальном режиме, можно получить ноль совпадений. Именно эта разница показывает, зачем нужен отдельный поисковик.
{"q":"механическя","page":1,"hitsPerPage":12}
Допуск опечаток зависит от длины слова и настроек. Это не универсальное исправление любого текста, транслитерации или неправильной раскладки. Для коротких кодов, серийных номеров и артикулов излишняя нечёткость иногда вредна: близкие обозначения могут относиться к разным товарам. Такие поля стоит тестировать отдельно и при необходимости исключать из обработки опечаток. Подробности есть в описании typo tolerance.
Для проверки качества соберите реальные запросы пользователей без персональных данных: корректные, с пропуском буквы, с перестановкой, с редким названием. Заранее отметьте ожидаемый товар. Затем сравнивайте первые результаты после каждой смены настроек. Большое количество совпадений само по себе не доказывает качество: нужная запись должна находиться в понятном месте, а интерфейс — сохранять введённый человеком запрос.
3. Поиск по тегам: пользователь описывает назначение
В сценарии «Поиск по тегам» запрос гарнитура находит 20 товаров категории наушников. Здесь слово помогает описать назначение, хотя в названии карточки может стоять другое выражение. Это полезно для каталога оборудования, базы знаний и внутренних шаблонов: разработчик может связать профессиональное название с привычными словами пользователей.
{"q":"гарнитура","page":1,"hitsPerPage":12}
В нашем индексе tags явно включены в поля поиска. Это обычные подготовленные данные; пример не использует нейросеть и не обещает семантическое понимание произвольного запроса. Для начала достаточно аккуратного словаря тегов и качественных описаний. Теги лучше поддерживать как контролируемый набор, иначе со временем появятся почти одинаковые варианты и случайные служебные слова.
Применение в интерфейсе может быть шире товарной карточки. Инструкция «Восстановление доступа» получает теги «пароль» и «вход», шаблон договора — тип сделки, задача — название системы. При этом один и тот же термин иногда означает разные вещи. Проверяйте выдачу на своём материале, а расширение словаря обсуждайте с людьми, которые действительно пользуются поиском. Вкладка ответа помогает понять, какое содержимое документа сделало запись доступной для запроса.
4. Несколько фильтров: подходящий товар должен быть доступен
Нажмите «Фильтры вместе»: остаются клавиатуры в наличии стоимостью до 8000 рублей. На учебном наборе это восемь записей. Текстовый запрос здесь пустой, потому что задача сформулирована условиями. Это полноценный способ навигации: посетитель может сначала определить бюджет и категорию, а потом уточнить название.
{
"q":"",
"filter":["category = \"Клавиатуры\"","inStock = true","price <= 8000"],
"page":1,
"hitsPerPage":12
}
Сравните фильтр и сортировку. Фильтр исключает неподходящие документы. Сортировка меняет порядок уже отобранных. «Сначала дешевле» не означает «не дороже 8000», а наличие поля цены ещё не делает его разрешённым фильтром. Соответствующие свойства предварительно перечислены в настройках индекса.
PHP принимает отдельные параметры интерфейса, а затем сам собирает выражения. Категория и бренд проверяются по разрешённым значениям, цена — как ограниченное целое число. Браузер не получает возможность передать произвольный filter или выбрать чужой индекс. В прикладном интерфейсе активные условия показаны отдельными кнопками: можно убрать только цену, сохранив категорию и наличие. Это особенно помогает на телефоне, где все поля фильтра не всегда помещаются рядом с результатом.
5. Сортировка: почему цена и релевантность могут спорить
Сценарий «Цена по возрастанию» показывает каталог от дешёвых товаров к дорогим. Переключите порядок и сравните первые карточки. Сортировку выполняет сервер до разбиения на страницы: перестановка только двенадцати уже загруженных товаров дала бы неверное представление о всём каталоге.
{"q":"","sort":["price:asc"],"page":1,"hitsPerPage":12}
В проекте есть осознанная настройка: sort стоит первым в rankingRules. Поэтому при выбранной пользователем сортировке цена имеет приоритет над текстовой релевантностью. Это решение учебного каталога, а не утверждение о стандартном порядке правил Meilisearch. Если оставить сортировку после текстовых правил, более релевантные группы могут идти раньше более дешёвых товаров. Механизм последовательного применения правил описан в документации bucket sort.
'rankingRules' => [
'sort', 'words', 'typo', 'proximity', 'attribute', 'exactness'
]
Для магазина такое поведение нужно согласовать с обещанием интерфейса. Кнопка «Сначала дешевле» обычно воспринимается как глобальный порядок цены. В поиске по документации важнее может быть точность совпадения. Сначала определите ожидаемый результат на примерах, затем выбирайте правила. Смена порядка влияет сразу на весь индекс, поэтому проверять её только одним красивым запросом недостаточно.
6. Фасеты: показать, как распределились результаты
Выберите сценарий «Бренд и категория». Запрос монитор вместе с брендом Pixel возвращает десять товаров. В списках фильтров отображаются счётчики текущей выдачи. Фасета отвечает на вопрос «какие варианты и в каком количестве присутствуют среди совпадений», а фильтр — «какое условие уже применено».
{
"q":"монитор",
"filter":["brand = \"Pixel\""],
"facets":["category","brand"],
"page":1,
"hitsPerPage":12
}
Счётчики относятся ко всему найденному набору, а не только к карточкам первой страницы. В мастерской значения, отсутствующие в ответе, показываются с нулём. Это помогает увидеть последствия ограничения, но не означает, что категория вообще исчезла из каталога. Например, после выбора Pixel другие бренды получают нулевой счётчик именно в текущей комбинации условий.
В большом магазине часто нужен другой интерфейс: рядом с выбранным брендом показывать, сколько товаров добавится при переключении на соседний. Для этого требуется отдельно продумать расчёт фасет без собственного ограничения, иногда несколькими запросами. Наш пример показывает простую модель текущей выдачи и не выдаёт её за такую расширенную навигацию. Стабильные идентификаторы фильтров полезны, если названия могут меняться; этот приём разобран в официальном руководстве по фасетам на ID.
7. Подсветка: объяснить причину совпадения
В сценарии «Подсветка совпадения» запрос лампа возвращает 20 товаров. В заголовках выделяется найденное слово. Такое небольшое оформление помогает быстро просмотреть список: человек видит связь между запросом и карточкой, особенно когда названия длинные или похожие.
{
"q":"лампа",
"attributesToHighlight":["title","description"],
"highlightPreTag":"[[[MEILI_HL]]]",
"highlightPostTag":"[[[/MEILI_HL]]]"
}
Meilisearch возвращает подготовленные значения в _formatted. В нашей мастерской вместо HTML-тегов используются специальные маркеры. JavaScript разбирает только эти маркеры, создаёт mark, а остальное добавляет текстовыми узлами. Даже если документ содержит строку, похожую на тег изображения или скрипт, она остаётся текстом.
Это важная граница для интеграции с CMS. Поисковый движок индексирует переданные данные, но не становится автоматическим очистителем произвольного HTML. Нельзя без проверки присваивать весь _formatted.title в innerHTML, особенно если карточки редактируют внешние пользователи. Отдельно решите, нужен ли в выдаче полный текст описания или короткий фрагмент. Поля форматирования и параметры запроса перечислены в официальном Search API.
8. Пустая выдача и ошибка сервера — разные состояния
Сценарий «Пустой результат» отправляет запрос о несуществующей детали космолёта. Движок успешно отвечает, но совпадений нет. Интерфейс предлагает изменить запрос или снять ограничения. В рабочем продукте можно дополнительно показать популярные категории или ссылку на обращение в поддержку, сохранив введённый текст.
Если же PHP не может обратиться к движку, показывается ошибка и кнопка повторения. Нельзя заменять сетевую проблему сообщением «Ничего не найдено»: человек начнёт менять правильный запрос, хотя каталог просто недоступен. В проекте некорректные параметры получают HTTP 422, временная серверная проблема — HTTP 503, а корректный поиск без совпадений — HTTP 200 с пустым hits.
Проверяйте также промежуточное состояние. Пока запрос выполняется, интерфейс сообщает об ожидании. В браузере предусмотрен предел ожидания, а более старый ответ не может перезаписать новый запрос. Копирование исходников на этот момент заблокировано, чтобы видимый код и скачиваемые параметры не расходились. Это пригодится в любом интерфейсе с живым поиском: медленная сеть не должна превращать последовательный ввод в случайную выдачу.
PHP-обработчик: короткий запрос с понятными ограничениями
Вкладка PHP собирает исполняемый пример для корня скачанного проекта. Он подключает src/bootstrap.php, проверяет входные параметры через Search::parameters, читает ключ из серверного файла и обращается к фиксированному индексу products. В архиве используется небольшой клиент на PHP cURL: так хорошо видны HTTP-метод, путь, JSON и обработка ошибок.
<?php
declare(strict_types=1);
require __DIR__ . '/src/bootstrap.php';
$params = Aen\Meili\Search::parameters([
'q' => 'монитор',
'brand' => 'Pixel',
'sort' => 'price:asc',
'page' => 1,
]);
$keyFile = getenv('MEILI_SEARCH_KEY_FILE') ?: '/run/search/search.key';
$key = trim((string) file_get_contents($keyFile));
$client = new Aen\Meili\Client(
getenv('MEILI_HOST') ?: 'http://meili:7700', $key
);
$result = $client->request('POST', '/indexes/products/search', $params);
echo json_encode($result, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR);
После запуска стенда по инструкции ниже сохраните скачанный файл meilisearch-search.php в корне проекта. Выполните две команды из этого каталога: они одинаковы для PowerShell и Bash. Код выполнится внутри PHP-контейнера, где доступны имя meili и файл ключа. Запуск этого файла обычным PHP на хосте без настройки окружения не даст такого соединения.
docker compose cp ./meilisearch-search.php web:/var/www/app/meilisearch-search.php
docker compose exec -T web php meilisearch-search.php
Клиент ограничивает время подключения, полное время запроса и объём ответа. Он не следует перенаправлениям, а веб-обработчик не принимает произвольный адрес сервера. При исключении посетитель получает понятное сообщение и идентификатор ошибки; подробности подключения и секреты не выводятся в страницу.
Для большого приложения можно выбрать официальный PHP SDK, который устанавливается через Composer. Это отдельный вариант интеграции: наш архив не требует установки SDK и не скрывает за ним сетевой вызов. Независимо от клиента сохраняются те же обязанности: разрешённые параметры, проверка прав, тайм-ауты и аккуратная обработка неуспешного ответа.
Асинхронные задачи: принятие запроса ещё не означает готовность
Создание индекса, изменение настроек и загрузка документов выполняются через задачи. Ответ с taskUid означает, что операция принята, но использовать обновлённую выдачу ещё может быть рано. Нужно дождаться успешного завершения или разобраться с ошибкой. Статус конкретной задачи читается через /tasks/{uid}; структура ответа описана в Get task.
$pending = $admin->request('POST', '/indexes/products/documents', $documents);
$finished = $admin->wait($pending['taskUid']);
// wait возвращает результат только после succeeded;
// failed, canceled и превышение времени ожидания обрабатываются отдельно.
Наш setup последовательно ожидает создание индекса, применение настроек и импорт. В конце он проверяет число документов и сообщает о готовности. Поэтому после успешной команды есть конкретный результат — индекс из 200 товаров, а не только факт отправки HTTP-запроса.
В боевом приложении такие операции лучше выполнять из очереди или служебного задания. Если ожидание оборвалось, сначала проверьте исходную задачу: тайм-аут клиента не доказывает отмену операции на сервере. Повторная отправка без разбора может создать лишнюю нагрузку. Для больших объёмов полезны пакетная загрузка, журнал результата и отдельный контроль задержки между изменением данных и их появлением в поиске.
Как поддерживать индекс в актуальном состоянии
Учебный setup загружает фиксированный каталог. Для собственного сервиса потребуется договор между основной базой и индексом: какие записи публикуются, когда обновляются и как удаляются. Стабильный идентификатор позволяет обновлять конкретный документ без создания новой карточки при каждом сохранении.
Простой вариант для небольшого каталога — периодическое служебное задание, которое выгружает изменённые записи. Для более частых обновлений подходит очередь: приложение сохраняет данные, фиксирует событие изменения, а worker отправляет документ в Meilisearch. Отдельно обрабатываются удаление, снятие с публикации и изменение видимости. Только добавлять новые документы недостаточно: закрытая вчера статья не должна оставаться доступной в поисковой копии.
Предусмотрите возможность полной перестройки из исходной базы. Во время крупного изменения схемы удобно собрать новый индекс, проверить контрольные запросы и только потом переключать приложение. Не меняйте одновременно структуру документов, правила ранжирования и код интерфейса без промежуточной проверки: иначе будет сложно понять причину ухудшения результата.
Если поиск недоступен, остальная система по возможности продолжает работать: открытие известной карточки, оформление существующего заказа или работа с прямой ссылкой не обязаны зависеть от поискового сервиса. Конкретный резервный сценарий выбирается по продукту. Иногда достаточно честного уведомления, иногда полезен упрощённый поиск по основной базе.
Ключи, приватные документы и серверная проверка доступа
В стенде разделены три полномочия. Master key нужен для управления ключами. Административный API key применяется для подготовки индекса. Веб-обработчик использует отдельный ключ с действием search только для products. Секреты не включены в ZIP и создаются при установке. Общие принципы разделения описаны в руководстве по master key и API keys.
Ограниченный search key не равен авторизации конечного пользователя. Если индекс содержит записи разных компаний, сервер должен учитывать текущую организацию независимо от фильтра, присланного браузером. Нельзя доверять полю tenantId только потому, что его выбрали в интерфейсе. Для прямого клиентского поиска предусмотрены tenant tokens, но правила их выдачи и изоляцию тоже проектирует приложение.
В архиве находятся вымышленные публичные товары. Перед заменой их реальными документами определите, какие поля разрешено показывать и кому. Дамп индекса может содержать и данные, и служебные сведения: это резервная копия с ограниченным доступом, а не файл для каталога загрузок сайта.
На VPS сам Meilisearch остаётся во внутренней сети Docker. Посетители обращаются к PHP через HTTPS. Учебную установку дополнительно закрываем паролем на reverse proxy; для публичного магазина затем понадобится собственная модель доступа и ограничения нагрузки. Не публикуйте административный порт движка только ради удобства проверки — для обслуживания есть SSH и служебные контейнеры.
Интерфейс, мобильная версия и скорость на реальных данных
Живой поиск состоит не только из скорости API. Пользователь должен видеть применённые условия, уметь снять одно ограничение и понимать, идёт ли запрос. В мастерской фильтры остаются доступны после неудачи, а кнопка повтора появляется при ошибке. Смена категории или строки поиска возвращает первую страницу. На узком экране поля и карточки выстраиваются в колонку, а длинный JSON прокручивается внутри блока кода.
Подсказки не должны отбирать фокус при каждом введённом символе. У кнопок есть заметное состояние выбора, сообщение результата озвучивается через aria-live, вкладки исходников переключаются клавиатурой. Режим работы вынесен над интерфейсом и повторяется у выдачи. Это особенно важно здесь, где рядом существуют локальная модель, сохранённая запись и серверный ответ.
Скорость измеряйте на своём размере документов и характерных запросах. processingTimeMs описывает работу движка, но не весь путь до появления карточек: есть сеть, PHP, передача JSON и отрисовка. Двести учебных товаров показывают механику, а не доказывают производительность на миллионах документов. Проверяйте одновременно индексирование и поиск, используемые фильтры, память, диск и очередь задач.
Начните с набора контрольных запросов и понятной цели: какой документ должен быть в первых результатах, сколько времени пользователь ждёт, что происходит при ошибке. Только после этого меняйте параметры и сравнивайте измерения. Такой подход полезнее абстрактного обещания «мгновенно»: он связывает технические настройки с конкретной работой человека в интерфейсе.
Windows: полный первый запуск
Нужны Docker Desktop с Linux containers и Python 3. Откройте PowerShell в новом каталоге. Не распаковывайте проект поверх существующего приложения.
Invoke-WebRequest 'https://ae-nekrasov.ru/wp-content/plugins/aen-meilisearch-workshop/meilisearch-project.zip' -OutFile meilisearch-project.zip
Get-FileHash .\meilisearch-project.zip -Algorithm SHA256
Expand-Archive .\meilisearch-project.zip -DestinationPath .\meilisearch-lab
Set-Location .\meilisearch-lab\meilisearch-project
docker compose version
py -3 scripts/init.py
docker compose config --quiet
docker compose build web setup
docker compose up -d --wait meili
docker compose run --rm setup
docker compose up -d --no-build --wait web
Start-Process 'http://127.0.0.1:8105/'
$query=[uri]::EscapeDataString('механическя')
$result=Invoke-RestMethod "http://127.0.0.1:8105/api/search.php?q=$query"
$result.totalHits
$result.hits | Select-Object title,price
Setup должен вывести ready: true, 200 документов и завершённые задачи. Запрос с опечаткой даёт 20 результатов, на странице показаны первые 12. Ключ создаётся в secrets/master.key: скрипт сохраняет существующий ключ и не печатает его. На Windows ограничьте доступ к папке своей учётной записью средствами NTFS — POSIX-режим 0600 не заменяет ACL. Не отправляйте secrets вместе с исходниками.
Для повторного старта: docker compose up -d --wait. Для остановки: docker compose stop. Команда docker compose down удалит контейнеры и сеть, но сохранит named volumes. Не добавляйте -v, если данные нужны. При конфликте порта проверьте Get-NetTCPConnection -LocalPort 8105,17705 -ErrorAction SilentlyContinue, не завершайте неизвестные процессы.
Linux: локальный стенд
Для уже установленного Docker Engine с Compose plugin и Python 3 используйте Bash. Если Docker требует привилегий, добавляйте sudo к Docker-командам; Python-скрипт, вызывающий Docker, тоже запускайте через sudo. Группа docker даёт практически root-доступ: не выдавайте его автоматически ради примера.
curl -fL https://ae-nekrasov.ru/wp-content/plugins/aen-meilisearch-workshop/meilisearch-project.zip -o meilisearch-project.zip
sha256sum meilisearch-project.zip
unzip meilisearch-project.zip -d meilisearch-lab
cd meilisearch-lab/meilisearch-project
python3 scripts/init.py
docker compose config --quiet
docker compose build web setup
docker compose up -d --wait meili
docker compose run --rm setup
docker compose up -d --no-build --wait web
curl --fail --get http://127.0.0.1:8105/api/search.php --data-urlencode 'q=механическя'
curl --fail --get http://127.0.0.1:8105/api/search.php --data-urlencode 'category=Клавиатуры' --data-urlencode 'inStock=1' --data-urlencode 'maxPrice=8000'
docker compose logs --tail 60 web meili
Первый запрос возвращает totalHits: 20, второй — 8. Frontend доступен только на 127.0.0.1:8105, engine — на 127.0.0.1:17705. Это доступ с текущего компьютера, а не публикация в интернете. Не заменяйте loopback на 0.0.0.0 для обхода серверной настройки. Конфликты портов проверяются через sudo ss -lntp. Если Docker исчерпал пул сетей, проверьте свою адресацию и docker network ls; глобальный prune для этого проекта не нужен.
Linux VPS: Docker, домен и секреты
Результат — приватный каталог по HTTPS на облачном VPS или выделенном сервере. Используется самостоятельный deploy/compose.server.yaml; его нельзя объединять с локальным YAML, иначе можно сохранить лишние опубликованные порты. Наружу выходит только Caddy на 80/443. У PHP и Meilisearch нет host-портов.
Рецепт рассчитан на новую Ubuntu 24.04 LTS с SSH и sudo, без существующего сайта на 80/443. Сначала проверьте cat /etc/os-release, docker version, sudo ss -lntp. Если Docker или reverse proxy уже работают, сохраните их конфигурацию и интегрируйте сервис отдельно. Следующие команды используют официальный apt-репозиторий Docker.
sudo apt-get update
sudo apt-get install -y ca-certificates curl unzip python3
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
docker_arch=$(dpkg --print-architecture)
sudo tee /etc/apt/sources.list.d/docker.sources >/dev/null <<EOF
Types: deb
URIs: https://download.docker.com/linux/ubuntu
Suites: noble
Components: stable
Architectures: $docker_arch
Signed-By: /etc/apt/keyrings/docker.asc
EOF
sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
sudo systemctl enable --now docker
sudo docker compose version
Тот же рецепт с проверкой ОС и отказом менять существующую установку: scripts/install-docker-ubuntu.sh. Источник: официальная инструкция Docker. Создайте A-запись search.your-domain.tld на сервер; AAAA нужна только при работающем IPv6. Разрешите TCP 80/443 на firewall хостинга и сохраните SSH-доступ. Docker-публикации могут обходить привычные правила UFW: особенности Docker и firewall. Порты 8105 и 17705 на сервере не открываются.
sudo install -d -m 0755 /opt/aen-meili/releases/v1
sudo curl -fL https://ae-nekrasov.ru/wp-content/plugins/aen-meilisearch-workshop/meilisearch-project.zip -o /opt/aen-meili/releases/v1/source.zip
sudo unzip /opt/aen-meili/releases/v1/source.zip -d /opt/aen-meili/releases/v1
sudo ln -s /opt/aen-meili/releases/v1/meilisearch-project /opt/aen-meili/current
cd /opt/aen-meili/current
sudo python3 scripts/init.py --directory /etc/aen-meili
sudo install -m 0600 deploy/server.env.example /etc/aen-meili/server.env
sudo docker run --rm -it caddy:2.11.9-alpine caddy hash-password
sudoedit /etc/aen-meili/server.env
Можно передать свой архив через scp ./meilisearch-project.zip user@server:~/meilisearch-project.zip из PowerShell или Bash. Проверьте источник и хеш, распакуйте новый релиз. Каталоги кода в /opt должны быть проходимы (0755), секретный /etc/aen-meili — 0700; env и master.key — 0600. PHP не записывает код, данные остаются в volumes.
В env задайте настоящий домен, пользователя и bcrypt-хеш. Хеш оставьте в одинарных кавычках, сохранив все $; пароль не передавайте аргументом shell. MASTER_KEY_FILE указывает на /etc/aen-meili/master.key. Пустые MEILI_VOLUME и SEARCH_KEY_VOLUME означают тома с префиксом DEPLOY_PROJECT: разные проекты не подключат случайно один индекс. Заполняйте их только для явного восстановления. Не копируйте env-шаблон поверх заполненных настроек.
HTTPS, запуск и подключение к PHP
Сначала создаются индекс и scoped search key, затем запускаются PHP и Caddy. Первый setup загружает учебные товары. При повторе он обновляет документы с теми же ID и настройки, но не удаляет посторонние документы. Для восстановленного или уже рабочего каталога выполняйте только bin/provision-key.php, чтобы не повторять seed.
cd /opt/aen-meili/current
sudo docker compose --env-file /etc/aen-meili/server.env -f deploy/compose.server.yaml config --quiet
sudo docker compose --env-file /etc/aen-meili/server.env -f deploy/compose.server.yaml build web setup
sudo docker compose --env-file /etc/aen-meili/server.env -f deploy/compose.server.yaml up -d --wait meili
sudo docker compose --env-file /etc/aen-meili/server.env -f deploy/compose.server.yaml run --rm setup
sudo docker compose --env-file /etc/aen-meili/server.env -f deploy/compose.server.yaml run --rm --no-deps caddy caddy validate --config /etc/caddy/Caddyfile --adapter caddyfile
sudo docker compose --env-file /etc/aen-meili/server.env -f deploy/compose.server.yaml up -d --no-build --wait web caddy
curl -I http://search.your-domain.tld/
curl -I https://search.your-domain.tld/
curl --fail --user reader --get https://search.your-domain.tld/api/search.php --data-urlencode 'q=механическя'
Ожидаются HTTP→HTTPS, 401 без пароля и 20 товаров после интерактивного ввода пароля. Не добавляйте -k: проверьте DNS, время, 80/443 и журналы Caddy. Локальный тест не подтверждает выдачу публичного сертификата на ваш домен. Все внешние маршруты, включая API, проходят basic_auth; Authorization удаляется перед PHP. Общий пароль подходит для частного стенда, но не заменяет аккаунты, роли и tenant-права.
Для публичного каталога перенесите поиск в своё приложение, добавьте ограничение частоты и наблюдение за нагрузкой. Не публикуйте master key в JavaScript. Сервер должен определять разрешённую область данных сам: фильтр от браузера не является авторизацией.
Наш клиент использует PHP cURL и не требует Composer. В существующий PHP/FPM-проект перенесите Client, Search и проверенный endpoint, задайте MEILI_HOST и путь к read-only файлу search key. PHP 8.3 Ubuntu 24.04 подходит используемому синтаксису; отдельный FPM deployment здесь не выполнялся, проверен контейнер PHP 8.5.12. Если выбираете официальный SDK, установите его через Composer по документации и зафиксируйте собственный lock. Этот пример не выдаёт SDK за установленную зависимость.
sudo docker compose --env-file /etc/aen-meili/server.env -f deploy/compose.server.yaml ps
sudo docker compose --env-file /etc/aen-meili/server.env -f deploy/compose.server.yaml logs --tail 100 web meili caddy
sudo journalctl -u docker --since '30 minutes ago' --no-pager
PHP возвращает 422 для неверных параметров и 503 при недоступности движка, с request ID без ключей и тела запроса в журнале. Access logs Caddy содержат строку поиска: не передавайте там секреты и ограничьте доступ к журналам. PHP healthcheck подтверждает процесс, а не готовность поиска; дополнительно проверяйте настоящий API. Health-порт Caddy 8081 и его admin API не опубликованы на хосте.
Резервная копия и восстановление
Dump сохраняет переносимое представление документов, настроек и API-ключей. Snapshot предназначен для той же версии движка. Оба запроса асинхронны: backup ждёт succeeded и только потом копирует файл, master key, серверный env и manifest с SHA-256. Таймаут или failed не выдаются за готовую копию. Это не атомарная копия бизнес-базы и индекса вместе. Подробности: официальные dumps и snapshots.
py -3 scripts/backup.py --kind dump --output .\backups
py -3 scripts/backup.py --kind snapshot --output .\backups
py -3 scripts/restore.py --backup .\backups\TIMESTAMP --volume aen-meili-restore-check-01
sudo install -d -m 0700 /var/backups/aen-meili
sudo python3 scripts/backup.py --env /etc/aen-meili/server.env --kind dump --output /var/backups/aen-meili
sudo python3 scripts/backup.py --env /etc/aen-meili/server.env --kind snapshot --output /var/backups/aen-meili
sudo python3 scripts/restore.py --backup /var/backups/aen-meili/TIMESTAMP --volume aen-meili-restore-check-01
Замените TIMESTAMP фактическим именем каталога из вывода backup. Restore проверяет хеши и отказывается от существующего тома. Затем создаёт новый и импортирует данные в контейнере без сети и host-портов. Проверяются версия и количество документов; helper останавливается и удаляется даже при ошибке, новый том сохраняется для осмотра. Snapshot допускает только исходную версию. Для dump можно указать проверенный совместимый образ через --image getmeili/meilisearch:vX.Y.Z, предварительно изучив migration guide; будущий тег нельзя угадывать.
Копии содержат ключи и индексированные данные. Держите их вне public, с ограниченным доступом и отдельной защищённой копией вне сервера. Скрипт не сохраняет основную БД, исходники, Docker images и Caddy TLS volumes. Релизы и бизнес-базу сохраняйте отдельно. Caddy может перевыпустить сертификаты при рабочем DNS и сети, но это не обещание мгновенного восстановления. Ручная копия открытой LMDB-директории не заменяет API dump/snapshot.
Переключение, обновление и возврат
Проверенное восстановление не переключает приложение. Сохраните текущие env и master key, остановите своё приложение, задайте MEILI_VOLUME нового тома и новое имя SEARCH_KEY_VOLUME. Возьмите master key из той же копии. Не перезаписывайте домен и project name старым env без проверки. После запуска engine опубликуйте scoped key без учебного seed.
cd /opt/aen-meili/current
sudo cp -a /etc/aen-meili/server.env /etc/aen-meili/server.env.before-restore
sudo cp -a /etc/aen-meili/master.key /etc/aen-meili/master.key.before-restore
sudo docker compose --env-file /etc/aen-meili/server.env -f deploy/compose.server.yaml stop caddy web meili
sudo install -m 0600 /var/backups/aen-meili/TIMESTAMP/master.key /etc/aen-meili/master.key
sudoedit /etc/aen-meili/server.env
# Set MEILI_VOLUME=aen-meili-restore-check-01 and SEARCH_KEY_VOLUME=aen-meili-search-restored-01.
sudo docker compose --env-file /etc/aen-meili/server.env -f deploy/compose.server.yaml config --quiet
sudo docker compose --env-file /etc/aen-meili/server.env -f deploy/compose.server.yaml up -d --force-recreate --wait meili
sudo docker compose --env-file /etc/aen-meili/server.env -f deploy/compose.server.yaml run --rm setup php bin/provision-key.php
sudo docker compose --env-file /etc/aen-meili/server.env -f deploy/compose.server.yaml up -d --force-recreate --wait web caddy
curl --fail --user reader --get https://search.your-domain.tld/api/search.php --data-urlencode 'q=клав'
При смене версии укажите в env именно образ, которым проверялся импорт. Для возврата остановите caddy/web/meili, восстановите оба файла before-restore, запустите engine, выполните provision-key, затем web/caddy теми же командами. Старый том оставлен, но изменения после переключения сами в него не попадут — повторите синхронизацию из основной БД. Два движка одновременно на одном томе запускать нельзя.
PHP/JS-релиз обновляйте отдельно от движка. Распакуйте проверенный архив в /opt/aen-meili/releases/v2, сохраните env, смените APP_RELEASE=v2, перейдите в новый каталог, выполните config –quiet, build web setup, caddy validate и up -d –no-build –wait web caddy. После проверки переключите симлинк: sudo ln -sfn /opt/aen-meili/releases/v2/meilisearch-project /opt/aen-meili/current. Сохраняйте DEPLOY_PROJECT, тома, secret path и версию Meilisearch. Не повторяйте seed при обновлении интерфейса.
Для возврата PHP-релиза используйте старый каталог и прежний APP_RELEASE; сохранённый образ не пересобирайте, выполните up -d –no-build –wait web caddy и верните симлинк после проверки. Это возврат кода, а не изменившихся данных. Обновление движка сначала проверяйте через dump в новом томе. Официальная инструкция обновления также описывает –upgrade-db, но обновление на месте не атомарно: старый образ нельзя запускать на уже преобразованной базе.
Что входит в исходники и как адаптировать проект
В архиве находятся HTML5/Bootstrap 5 интерфейс, JavaScript без обязательного фреймворка, 200 учебных документов, PHP-клиент и обработчик, скрипты подготовки и резервного копирования, Docker-конфигурации и подробный README. Настоящий поиск выполняется Meilisearch в отдельном контейнере. Для интерфейса не нужны сборка Node.js или внешний SaaS.
Начните адаптацию с данных и правил доступа. Замените каталог, определите стабильный идентификатор, обновите разрешённые категории и бренды в Search.php, затем настройте поля индекса. После успешного setup проверьте обычный запрос, опечатку, пустую выдачу и комбинацию фильтров. Если меняете поля карточки, согласуйте displayedAttributes и вывод JavaScript.
В проекте проверены реальные запросы к Meilisearch, сценарии интерфейса и восстановление учебных данных из резервной копии. При этом вам всё равно понадобится приёмка своего набора документов и своего сервера. Доступность домена, DNS, ограничения провайдера, реальные права пользователей и нагрузка зависят от места установки. Учебные цены и товары не являются коммерческим предложением.
Документация и следующие шаги
Для самостоятельного продолжения пригодятся официальная документация Meilisearch, справочник Search API, руководство по Docker и раздел обновления версий. По интеграции PHP есть официальный SDK; его можно выбрать вместо небольшого HTTP-клиента из мастерской.
Следующий полезный шаг — загрузить небольшой фрагмент собственного каталога и составить десяток реальных задач поиска. Сравните формулировки пользователей с полями документов, настройте фильтры и проверьте порядок результатов. Затем добавьте синхронизацию и наблюдение за задачами. Так мастерская превращается в основу конкретного сервиса, а каждая настройка получает понятную причину и проверяемый результат.


