События и расширение поиска
События модуля search позволяют встроить свою логику в готовый процесс поиска. Обработчик может дополнить документ перед индексацией, построить динамический URL результата, нормализовать теги или добавить к пользователю коды доступа из другого модуля. Модуль search при этом продолжает управлять индексом и проверкой прав.
События подходят для локального изменения одного этапа. Они не заменяют синхронизацию исходных объектов с индексом и не превращают поиск в источник актуальных данных. Про полный цикл добавления, обновления и удаления документов читайте в статье Индексация собственного контента.
Модуль вызывает события через классический API и передает обработчикам отдельные позиционные аргументы. Совместимый обработчик получает эти аргументы напрямую и возвращает обычное значение. Он не получает объект Bitrix\Main\Event. Поэтому для событий модуля search нужна совместимая регистрация.
Выбрать точку расширения
События относятся к четырем этапам. Одни управляют полной переиндексацией, другие сопровождают CSearch::Index(), подготавливают запрос или форматируют результат.
Начните выбор с задачи.
-
Используйте
OnReindex, если модуль должен заново передать все свои документы после очистки индекса. -
Используйте
BeforeIndex, если одно правило должно менять каждый документ перед добавлением или обновлением. -
Используйте
OnSearchGetURL, если итоговый адрес зависит от данных, которые известны только при чтении результата. -
Выберите отдельное событие тегов, фильтров или прав, если нужно изменить только соответствующий этап.
|
Событие |
Когда срабатывает |
Параметры и результат |
|
|
При полной переиндексации перед очисткой индекса |
Без параметров. Возвращаемое значение не используется |
|
|
При полной или модульной переиндексации, когда модуль |
Параметры: Обработчик может вернуть массив документов вместо передачи через метод объекта |
|
|
В начале |
Параметры: Возвращенный массив полностью заменяет документ для следующих обработчиков и индексации. Значение другого типа не используется |
|
|
Перед обновлением основной записи существующего документа |
Параметры: числовой идентификатор индекса Возвращаемое значение не используется |
|
|
После добавления основной записи и передачи документа поисковому движку, но до сохранения связанных прав, сайтов, параметров, заголовка и тегов |
Параметры: числовой идентификатор индекса Возвращаемое значение не используется |
|
|
Перед удалением существующего документа через методы модуля |
Параметры: Возвращаемое значение не используется |
|
|
При индексации статического файла, до стандартного чтения и разбора содержимого |
Параметры: Если итоговый результат обработки — массив, он заменяет стандартный разбор файла. При результате другого типа сохраняется стандартная обработка. Любое непустое возвращаемое значение останавливает перебор обработчиков |
|
|
После разбора поисковой строки, до чтения результатов |
Параметры: Возвращенная непустая строка ( |
|
|
При чтении результата, если сохраненный URL начинается со знака |
Параметры: Возвращенная строка ( |
|
|
При вызове |
Параметры: Возвращенная строка ( |
|
|
При SQL-обработке неизвестного поля фильтра в |
Параметры: Возвращенная непустая строка ( |
|
|
При подготовке кодов доступа авторизованного пользователя |
Параметры: служебный параметр Коды из возвращенного массива добавляются к стандартным кодам |
Событие OnReindex всегда получает объект $callback и имя его метода. Обработчик может передать документы через этот метод или вернуть массив документов. Модуль воспринимает непустой идентификатор как продолжение только при пошаговой переиндексации с положительным ограничением времени.
События OnBeforeIndexUpdate, OnAfterIndexAdd и OnBeforeIndexDelete подходят для наблюдения за изменениями индекса. Они не позволяют заменить поля или отменить операцию через возвращаемое значение. Не связывайте обработчик удаления с форматом строки $where. Модуль передает условие с внутренним идентификатором, но этот формат относится к устройству модуля.
Зарегистрировать обработчик
Метод Bitrix\Main\EventManager::addEventHandlerCompatible() регистрирует обработчик на время текущего запроса. Разместите вызов в загружаемом файле проекта, например /local/php_interface/init.php. Тогда проект подпишется на событие в каждом запросе. Собственный модуль регистрирует постоянную подписку во время установки, чтобы удалить ее вместе с модулем.
Примеры используют зарегистрированный модуль vendor.docs. Разместите классы в каталоге /local/modules/vendor.docs/lib/Search/. Пространство имен Vendor\Docs\Search связывает их с каталогом /lib/Search/.
О правилах именования, пространствах имен и автозагрузке читайте в статье Архитектура модулей.
Для регистрации передайте пять параметров.
-
$fromModuleId— идентификатор модуля-источника события. Для всех событий из таблицы укажитеsearch. -
$eventType— имя события, напримерBeforeIndex. Менеджер событий не учитывает регистр, но исходное написание упрощает поиск события в документации и коде. -
$callback— вызываемый обработчик в виде замыкания или массива из имени класса и метода. -
$includeFile— необязательный путь к подключаемому файлу. Значение по умолчанию равноfalse. -
$sort— необязательный приоритет выполнения. Значение по умолчанию равно100. Меньшее значение запускает обработчик раньше.
Пример. Код регистрирует обработчики изменения документа, формирования динамического URL и собственного поля фильтра в /local/php_interface/init.php. Метод Loader::includeModule() проверяет установку vendor.docs и регистрирует пространство имен его классов. Без этого вызова автозагрузка не найдет обработчики модуля.
use Bitrix\Main\EventManager;
use Bitrix\Main\Loader;
use Vendor\Docs\Search\SearchDocumentEventHandler;
use Vendor\Docs\Search\SearchFilterEventHandler;
use Vendor\Docs\Search\SearchUrlEventHandler;
if (Loader::includeModule('vendor.docs'))
{
$eventManager = EventManager::getInstance();
$eventManager->addEventHandlerCompatible(
'search',
'BeforeIndex',
[SearchDocumentEventHandler::class, 'beforeIndex']
);
$eventManager->addEventHandlerCompatible(
'search',
'OnSearchGetURL',
[SearchUrlEventHandler::class, 'getSearchResultUrl']
);
$eventManager->addEventHandlerCompatible(
'search',
'OnSearchPrepareFilter',
[SearchFilterEventHandler::class, 'prepareFilter']
);
}
Регистрируйте постоянные обработчики в модуле, которому принадлежит код.
Изменить документ перед индексацией
Событие BeforeIndex получает документ раньше всех проверок CSearch::Index(). Оно подходит для общего правила, которое должно работать и при одиночном обновлении, и при полной переиндексации. Например, обработчик может добавить тег ко всем документам своего модуля.
В параметре $fields важны следующие поля.
-
MODULE_ID— идентификатор модуля, который передал документ. -
ITEM_ID— стабильный идентификатор исходного объекта. -
TITLE,BODYиTAGS— содержимое, которое участвует в поиске. -
SITE_ID— сайты и URL документа. -
PERMISSIONS— коды доступа к результату.
Обработчик должен вернуть весь массив. Модуль передаст его следующему обработчику, поэтому потерянное поле уже не попадет в индекс. Сначала проверьте MODULE_ID. Это ограничит правило документами нужного источника.
Пример. Обработчик добавляет тег knowledge-base к документам модуля vendor.docs. Повторный вызов не создает дубль тега.
namespace Vendor\Docs\Search;
final class SearchDocumentEventHandler
{
public static function beforeIndex(array $fields): array
{
if (($fields['MODULE_ID'] ?? '') !== 'vendor.docs')
{
return $fields;
}
$tags = array_filter(
array_map('trim', explode(',', (string)($fields['TAGS'] ?? ''))),
static fn(string $tag): bool => $tag !== ''
);
$tags[] = 'knowledge-base';
$fields['TAGS'] = implode(', ', array_unique($tags));
return $fields;
}
}
Проверьте результат обычным поиском по добавленному тегу. Пример запроса с ограничениями по сайту и правам смотрите в статье Ранжирование, теги и поисковые подсказки.
Исключить документ из индексации
Событие BeforeIndex не поддерживает специальный результат для отмены. Значение false модуль игнорирует и продолжает работу с прежним массивом. Исключайте документ до вызова индексирующего API.
-
При одиночном обновлении проверьте условие в коде синхронизации и не вызывайте
CSearch::Index(). -
В обработчике
OnReindexпропустите объект и не передавайте его в$callback. -
Если документ уже находится в индексе, удалите его через
CSearch::DeleteIndex().
Не очищайте TITLE и BODY ради отмены. Новый документ с двумя пустыми полями не попадет в индекс. Существующий документ с такими значениями модуль удалит. Это разные результаты, поэтому такой прием скрывает ошибку в данных и усложняет проверку.
События при добавлении, обновлении и удалении документа
Метод CSearch::Index() выбирает ветку после BeforeIndex. Состав событий зависит от того, существует ли документ с той же парой MODULE_ID и ITEM_ID.
CSearch::Index()
|
v
BeforeIndex
|
+— новый документ —> добавить основную запись
| |
| v
| OnAfterIndexAdd
| |
| v
| сохранить права, сайты,
| параметры, заголовок и теги
|
+— существующий документ
|
+— TITLE и BODY пусты —> OnBeforeIndexDelete —> удалить
|
+— обновить переданные сайты и параметры,
| применить непустой набор прав
|
+— дата не изменилась и bOverWrite = false
| —> обновить метку сеанса и завершить
|
+— дата изменилась или bOverWrite = true
|
v
обновить заголовок и теги
|
v
OnBeforeIndexUpdate
|
v
обновить основную запись
Событие OnBeforeIndexUpdate не срабатывает, если дата документа не изменилась и параметр $bOverWrite равен false. При этом метод уже мог обновить сайты, права и именованные параметры в основном хранилище, но еще не передал изменения внешнему движку.
Событие OnAfterIndexAdd подтверждает добавление основной записи. Связанные права, сайты, параметры, заголовок и теги модуль сохраняет после него. Не читайте в этом обработчике только что созданный документ как полностью готовый результат.
Событие OnBeforeIndexDelete срабатывает перед удалением существующей записи. Модуль не вызывает его для нового документа, который не прошел начальные проверки. Обработчик не может отменить удаление и не получает данные исходного объекта.
События при полной переиндексации
Событие OnBeforeFullReindexClear запускается один раз перед глобальной очисткой индекса. Возвращаемое значение не отменяет очистку.
Глобальная очистка через CSearch::ReIndexAll(true, ...) не вызывает OnBeforeIndexDelete для каждого удаляемого документа. Используйте OnBeforeFullReindexClear, если перед такой очисткой нужно один раз подготовить связанное состояние. При модульной переиндексации с очисткой OnBeforeIndexDelete может сработать для каждой записи. Событие также вызывают точечные и фоновые удаления.
После очистки модуль индексирует статические файлы, а затем вызывает обработчики OnReindex. Каждый документ из обработчика снова проходит через CSearch::Index() и событие BeforeIndex. Поэтому одно правило подготовки документа работает при обычной синхронизации и восстановлении индекса.
Пример реализации OnReindex смотрите в статье Индексация собственного контента. Сценарий включает регистрацию, параметры обработчика, продолжение пошагового процесса и проверку результата.
Изменить ссылки в результатах поиска
События этого этапа не меняют найденные документы. Они добавляют параметры к ссылкам или преобразуют специальный URL во время чтения результата.
Добавить параметры к ссылкам
Событие OnSearch получает исходную строку после ее разбора. При поиске по тегам параметр начинается с tags:. Непустой результат обработчика модуль добавляет к URL каждого документа через ? или &.
Обработчик возвращает готовую строку параметров без начального разделителя. Код должен сам кодировать имена и значения. Этот механизм подходит для технической метки перехода или параметра статистики.
Событие OnSearch не меняет поисковую строку, фильтр или состав выдачи. Если проект должен исправить или дополнить запрос, подготовьте поля QUERY и TAGS до вызова CSearch::Search(). Правила выполнения запроса собраны в статье Поисковые запросы через CSearch.
Построить динамический URL
Событие OnSearchGetURL запускается только для URL, который начинается со знака =. Такой маркер позволяет сохранить в документе условное значение, а окончательный адрес собрать при чтении результата.
Обработчик получает основные данные результата.
-
MODULE_ID— идентификатор модуля-источника. -
ITEM_ID— идентификатор исходного объекта. -
URL— текущее условное значение. -
SITE_ID,DIRиSERVER_NAME— данные сайта результата. -
PARAM1иPARAM2— значения области документа, если источник их заполнил.
Верните строку нового URL. Значение null сохраняет текущий адрес. Если зарегистрировано несколько обработчиков, каждый следующий получает массив с URL, который вернул предыдущий.
Пример. Документы vendor.docs хранят значение =vendor.docs:article. Обработчик строит ссылку по ITEM_ID и добавляет каталог сайта в начало пути.
Сначала передайте маркер в поле URL поискового документа. Если ассоциативный массив SITE_ID содержит отдельный URL сайта, запишите маркер в это значение. URL внутри SITE_ID имеет приоритет над общим полем URL.
$searchDocument['URL'] = '=vendor.docs:article';
Затем передайте $searchDocument в CSearch::Index().
namespace Vendor\Docs\Search;
final class SearchUrlEventHandler
{
public static function getSearchResultUrl(array $fields): ?string
{
if (
($fields['MODULE_ID'] ?? '') !== 'vendor.docs'
|| ($fields['URL'] ?? '') !== '=vendor.docs:article'
)
{
return null;
}
return sprintf(
'%s/help/article.php?id=%s',
rtrim((string)($fields['DIR'] ?? ''), '/'),
rawurlencode((string)$fields['ITEM_ID'])
);
}
}
Поиск проверяет права до чтения результата. Новый URL не расширяет доступ к документу. Целевая страница должна независимо проверить право пользователя на исходный объект. Иначе человек сможет открыть закрытый объект по прямой ссылке.
Обработчик запускается для каждой строки выдачи, при чтении данных через CSearchTitle и CSearchItem, а также при построении карты сайта модулем search. Поэтому он не должен зависеть только от контекста страницы результатов.
Изменить теги, фильтры и коды доступа
Эти события влияют на разные уровни поиска. Теги меняют представление значений, фильтры добавляют условия выборки, а коды доступа определяют видимость уже найденных документов.
Нормализовать теги
Событие OnSearchGetTag получает по одному тегу после разделения строки по запятым. Каждый обработчик возвращает значение для следующего. Пустая строка удаляет тег из результата.
Правило действует в точках вызова tags_prepare(): при индексации тегов, подготовке фильтра TAG класса CSearchTags, сборе статистики и форматировании. Метод CSearch::Search() не вызывает эту функцию для входного TAGS. Если обработчик меняет написание меток, подготовьте выбранные теги по тому же правилу до обычного поискового запроса. Делайте преобразование идемпотентным. Повторный запуск над уже нормализованным значением не должен менять его снова.
Глобальная нормализация может объединить два исходных тега в один. Перед включением обработчика проверьте существующие данные и выполните переиндексацию. Иначе старые записи и новые запросы будут использовать разные значения. О хранении и поиске тегов читайте в статье Ранжирование, теги и поисковые подсказки.
Добавить собственное поле фильтра
Событие OnSearchPrepareFilter обрабатывает поле, которое модуль search не знает. Обработчик получает SQL-псевдоним таблицы поискового индекса, имя поля и переданное значение. Он должен вернуть законченное SQL-условие без слова WHERE.
Событие работает при поиске средствами базы данных, MySQL FullText, PostgreSQL FullText и через CSearchTags. Sphinx и OpenSearch подготавливают фильтры самостоятельно и не вызывают OnSearchPrepareFilter. Не используйте собственное поле, если проект должен переключаться между этими движками без изменения кода.
Обработчик принимает три параметра.
-
$tableAliasимеет типstring. Используйте полученный SQL-псевдоним перед именем колонки и не собирайте его самостоятельно. -
$fieldимеет типstring. Модуль переводит имя неизвестного поля фильтра в верхний регистр. -
$valueимеет типmixed. Проверьте тип и допустимое значение до формирования SQL.
Возвращайте пустую строку для чужого поля или недопустимого значения. Тогда модуль сможет вызвать следующий обработчик. Первое непустое условие останавливает перебор обработчиков для этого поля.
Используйте эту точку только для контролируемого внутреннего фильтра. Получайте SQL-помощник у подключения модуля search, ограничивайте значения списком и экранируйте строку методом forSql(). Не вставляйте в SQL имя поля или значение из запроса без проверки.
Пример. Обработчик принимает поле VENDOR_SCOPE и сопоставляет его с полем PARAM1 поискового документа. Фильтр поддерживает только области article и manual.
namespace Vendor\Docs\Search;
final class SearchFilterEventHandler
{
public static function prepareFilter(
string $tableAlias,
string $field,
mixed $value
): string
{
if (
$field !== 'VENDOR_SCOPE'
|| !is_string($value)
|| !in_array($value, ['article', 'manual'], true)
)
{
return '';
}
$database = \CDatabase::GetModuleConnection('search');
$sqlHelper = $database->getConnection()->getSqlHelper();
return sprintf(
"%sPARAM1 = '%s'",
$tableAlias,
$sqlHelper->forSql($value)
);
}
}
После регистрации из первого примера передайте VENDOR_SCOPE вместе с обычными полями в массив CSearch::Search().
if (!\Bitrix\Main\Loader::includeModule('search'))
{
throw new \RuntimeException('Модуль search не установлен');
}
$search = new \CSearch();
$search->Search([
'QUERY' => 'установка',
'SITE_ID' => 's1',
'MODULE_ID' => 'vendor.docs',
'VENDOR_SCOPE' => 'manual',
]);
После вызова обработчика условие ограничит выдачу документами, у которых PARAM1 равно manual. Для переносимого решения передайте стандартное поле 'PARAM1' => 'manual' вместо VENDOR_SCOPE. Стандартные поля MODULE_ID, ITEM_ID, PARAM1, PARAM2, SITE_ID и PARAMS не требуют обработчика и ручной сборки SQL. Готовые условия смотрите в статье Поисковые запросы через CSearch.
Добавить коды доступа пользователя
Событие OnSearchCheckPermissions расширяет набор кодов авторизованного пользователя. Обработчик должен вернуть массив строк. Те же строки источник указывает в поле PERMISSIONS поискового документа.
Стандартные коды обозначают источник права.
-
AUоткрывает документ любому авторизованному пользователю. -
U<идентификатор пользователя>открывает документ одному пользователю. -
G<идентификатор группы>открывает документ участникам группы. КодG2относится в том числе к неавторизованным посетителям.
Собственный код должен совпадать в результате обработчика и в поле PERMISSIONS. Иначе проверка не свяжет пользователя с документом.
Модуль не вызывает событие для администратора и неавторизованного посетителя. Администратор видит все подходящие документы. Для посетителя без авторизации модуль проверяет только код G2.
Дополнительный код должен обозначать подтвержденное право, а не роль интерфейса. Возвращайте минимальный набор и рассчитывайте его по текущему пользователю. Лишний код может открыть документ в выдаче пользователю без нужного права. Обработчик не должен удалять стандартные коды.
Разобрать содержимое статического файла
Событие OnSearchGetFileContent позволяет заменить стандартный разбор файла при вызове CSearch::ReindexFile(). Модуль запоминает результат каждого обработчика и останавливает перебор после первого непустого значения.
В обработчик приходят два параметра.
-
$absolutePath— абсолютный путь к файлу, который модульsearchуже проверил. -
$searchSessionId— идентификатор сеанса полной переиндексации. При одиночном вызове он может быть пустым.
Верните массив из трех полей.
-
TITLE— заголовок документа. После очистки HTML он должен остаться непустым. -
CONTENT— содержимое для поляBODYпоискового документа. -
PROPERTIES— свойства страницы. Модуль может взять из них теги по настройкеpage_tag_property.
Только итоговый результат типа array заменяет стандартный разбор. Возвращайте false или null, если обработчик не подходит. Непустое значение другого типа остановит следующие обработчики, но модуль все равно прочитает файл стандартным способом.
Предотвратить рекурсию и задержки
Все события выполняются синхронно. Индексация, поиск или чтение результата продолжатся только после завершения обработчика. Поэтому обработчик должен быстро отфильтровать чужие документы и выполнить только необходимую работу.
-
Ограничьте обработку по
MODULE_ID,URLили имени поля в начале метода. -
Делайте преобразования повторяемыми. Полная переиндексация заново вызывает
BeforeIndex,OnSearchGetTagи события добавления. -
Не вызывайте
CSearch::Index()изBeforeIndex,OnBeforeIndexUpdateилиOnAfterIndexAdd. Такой вызов снова запустит события и может создать рекурсию. -
Не выполняйте сетевые запросы для каждого документа или результата. Передайте внешнее действие в очередь с уникальным ключом
MODULE_IDиITEM_ID. Такой ключ не даст создать повторное задание при повторной индексации. -
Не рассчитывайте на общую транзакцию с исходным объектом. Ошибка обработчика может прервать текущий PHP-код, но уже выполненное внешнее действие не откатится автоматически.
-
Проверяйте результат через
CSearch, а не через внутреннее хранилище. Так проверка учтет сайт, права, выбранный поисковый движок и преобразование URL.
События помогают изменить отдельный этап без копирования поисковой логики. Выбирайте самую узкую точку расширения, сохраняйте проверку прав и проверяйте обработчик как при одиночном вызове, так и при полной переиндексации.