besnovatyj / yii2-cms-search
Фасад сквозного поиска по сайту для Yii2 CMS: собирает контент модулей через контракт SearchableProvider в единый каталог документов и рисует выдачу. Движок поиска (TNTSearch, Manticore, ...) подключается отдельным пакетом-ядром и переключается в настройках.
Package info
github.com/besnovatyj/besnovatyj-yii2-cms-search
Type:yii2-extension
pkg:composer/besnovatyj/yii2-cms-search
Requires
- php: >=8.4
- ext-mbstring: *
- besnovatyj/yii2-cms-contracts: ^1.0
- besnovatyj/yii2-cms-forms: ^1.0
- besnovatyj/yii2-cms-kernel: ^1.0
- yiisoft/yii2: ~2.0.0
- yiisoft/yii2-bootstrap5: ~2.0.0
Requires (Dev)
- roave/security-advisories: dev-latest
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
Единый поиск по контенту всех модулей CMS. Пакет собирает документы, хранит каталог и рисует выдачу; собственно поиск по словам выполняет ядро — отдельный пакет, подключаемый как плагин.
Как это устроено
Контентные модули Фасад (этот пакет) Ядро (отдельный пакет)
───────────────── ────────────────── ──────────────────────
Blog, Page, Shop … search_documents TNTSearch / Manticore
implements каталог документов, ←→ «слова → id + score»
SearchableProvider → ссылки, статусы, фасеты,
синонимы, вёрстка выдачи
Ключевое разделение: ядро отвечает только за текст, всё остальное — фасад. Поэтому смена движка не трогает ни контентные модули, ни шаблоны, а выдача выглядит одинаково при любом ядре.
Подключение модуля к поиску
Модуль реализует контракт Besnovatyj\Contracts\search\SearchableProvider на классе Module:
public function searchSources(): array { return [new SearchSource('blog.post', 'Статьи блога', 1.0, 'bi bi-newspaper')]; } public function searchDocuments(string $type): iterable { return match ($type) { 'blog.post' => (new PostReadRepository())->searchDocuments(), default => [], }; }
Требования к провайдеру:
- отдавать только публично доступное — черновики и снятое с публикации в общий индекс попасть не должны;
- отдавать генератор и читать записи пачками (
each(100)) — переиндексация не должна держать весь контент в памяти; - отдавать сырые поля: HTML не чистить, шорткоды не раскрывать — это делает фасад единообразно;
- ссылку задавать роутом и параметрами, а не готовым URL: короткий адрес может измениться в модуле алиасов.
Переиндексация
php yii Search/index/rebuild # полная пересборка php yii Search/index/status # состояние индекса и список источников
На боевом сервере запускать от пользователя веб-сервера, иначе процесс не прочитает секреты базы:
sudo -u www-data php yii Search/index/rebuild
Инкрементального обновления нет намеренно: полная пересборка сайта на тысячи документов занимает секунды и не имеет состояний рассинхронизации (пропущенные события, каскадные удаления мимо ActiveRecord). Кнопка пересборки есть и в админке — раздел «Поисковый индекс».
Индекс собирается рядом с рабочим и подменяется в конце, поэтому во время пересборки сайт продолжает искать по старому индексу.
Настройки
Управляются модулем yii2-cms-config, категория Search:
| Опция | Смысл |
|---|---|
| Активное ядро | какой движок обслуживает поиск |
| Запасное ядро | куда переключиться, если активное не отвечает |
| Исключённые разделы | ключи источников, не участвующие в поиске |
| Веса разделов | blog.post: 2, page.page: 1.5 |
| Синонимы | группа в строке: врач, доктор, терапевт |
| Минимальная длина запроса, размер страницы, длина фрагмента, поиск с опечатками |
Смена ядра, весов или синонимов требует пересборки индекса — стеммер и веса «запекаются» в индекс при индексации. Страница состояния предупредит об этом сама.
Что показывать в теме
echo \Besnovatyj\Search\widgets\SearchBoxWidget::widget();
Страница выдачи — /search?q=...&type=blog.post, базовый шаблон лежит в
views/frontend/search/index.php и переопределяется темой.
Ядра
| Пакет | Модуль | Движок | Инфраструктура | Морфология |
|---|---|---|---|---|
yii2-cms-search-tnt |
SearchTnt |
TNTSearch, индекс в MySQL проекта | ничего ставить не нужно | стемминг (Snowball) |
yii2-cms-search-manticore |
SearchManticore |
Manticore Search | отдельный демон + роль ansible | лемматизация |
Оба ядра — полноценные модули: включаются и выключаются менеджером модулей, имеют свои настройки в общей странице настроек. Установлены оба — задайте второй запасным: тогда падение демона перестаёт быть аварией.
Как ядро подключается к фасаду
Фасад не знает ни одного движка поимённо — ядро объявляет себя само. Модуль пакета-ядра реализует
Besnovatyj\Search\contracts\SearchEngineProvider:
public function searchEngines(): array { return [new SearchEngineDescriptor('tnt', 'TNTSearch (в базе проекта)', TntSearchEngine::class)]; }
Фасад собирает такие объявления через EngineRegistry — тем же обходом модулей с проверкой
instanceof, каким собирает поставщиков контента. Отсюда три следствия:
- список ядер в настройках равен составу включённых модулей-ядер: выключили ядро в менеджере модулей — оно исчезло и из выбора, и из выдачи;
- карты адаптеров, проверок
class_exists()и флагов «установлено» в фасаде нет — они выражали бы то же самое вторым способом; - новое ядро добавляется установкой пакета, без единой правки фасада.
Сам движок создаётся DI-контейнером, поэтому его зависимости (настройки, соединение, схема
индекса) объявляются в config/common.php пакета-ядра и внедряются через конструктор.
Готовность ядра описывают два метода контракта: isAvailable() для горячего пути (фасад решает,
работать этим ядром или уйти на запасное) и unavailableReason() для страницы состояния и консоли.
Второй нужен потому, что причины неготовности требуют разных действий: «демон не отвечает» —
чинить окружение, «индекс ещё не собран этим ядром» — нажать пересборку. Схлопнутые в один bool,
они превращаются в «ядро не отвечает» на живом демоне.
Проводка (для тех, кто будет править пакет)
Вся DI-проводка живёт в config/common.php — в секции container.singletons. Файла
config/container.php у модуля нет намеренно: он выполняется только при инициализации модуля, а
поиск вызывают отовсюду — из виджета в шапке темы, из консоли, из админки. Настройки собираются
SearchSettingsFactory из params модуля один раз за запрос и дальше передаются готовым объектом
SearchSettings; ни один класс пакета не читает Yii::$app->getModule() сам.