Откройте поиск на своём сайте и введите название любого товара из каталога. Всё нашлось? Тогда эта статья вам, скорее всего, не нужна — но сохраните её на случай, когда перестанет. А если вы увидели пустую страницу, «ничего не найдено» или список, в котором товаров нет вообще, — вы ровно там, где был наш клиент. При этом поиск по всему сайту работал исправно. Это самое коварное в такой поломке: система отвечает «всё в порядке», разработка отчитывается «поиск работает», а покупатель в это время уходит без товара. Ниже — разбор реального инцидента на интернет-магазине стройматериалов: два независимых дефекта, один протухший кэш, цифры, SQL, PHP, Docker, поисковый сервер и чек-лист, который можно повторить. Без «почистите кэш и попробуйте ещё раз».

Прежде чем что-то делать. Всё, что ниже, затрагивает поисковый индекс и настройки БД. Сделайте дамп базы и убедитесь, что умеете его развернуть. Операции с
b_search_contentна большом каталоге могут занять минуты и на это время заметно нагрузить диск.
Содержание
- Часть 1. Как на самом деле работает поиск в Битриксе
- Часть 2. Как выглядела поломка: сайт ищет, каталог молчит
- Часть 3. Почему страница отдаёт старые данные: протухший кэш Redis и цифра 2081
- Часть 4. Дефект первый: инфоблок «Регионы», съедавший лимит выдачи
- Часть 5. Дефект второй: почему пустой arrFILTER ищет везде
- Часть 6. Чек-лист внедрения: четыре шага и один костыль
- Часть 7. Поиск по артикулу: отдельная боль, отдельное решение
- Часть 8. Поисковый сервер и Docker: когда штатного индекса уже мало
- Часть 9. SEO-побочка: 2081 тонкая страница и краулинговый бюджет
- Часть 10. Семь правил, которые мы вынесли из этого кейса
- Часть 11. Поиск починили. Трафика больше не стало
- Часть 12. Первая гипотеза: битая страница категории
- Часть 13. Вторая гипотеза: плохая позиция
- Часть 14. Поворотный момент: сверка с рыночной медианой
- Часть 15. Побочная находка: карусель товаров и сломанный YML
- Часть 16. Что осталось после разбора
- FAQ
- Вывод
Часть 1. Как на самом деле работает поиск в Битриксе
Прежде чем что-то чинить, нужно принять неочевидную вещь. Она объясняет примерно половину всех обращений с формулировкой «поиск битрикс не работает».
Штатный поиск 1С-Битрикс не «спрашивает» таблицу товаров. Он работает с собственным хранилищем, которое индексатор наполняет заранее. Товар может прекрасно лежать в каталоге — опубликован, с ценой, с остатком, с фотографиями — и при этом не находиться поиском. Не потому что он «плохой», а потому что его нет в индексе.
Технически это группа таблиц с префиксом b_search_content. Для понимания выдачи важны четыре:
| Таблица | Что хранит |
|---|---|
b_search_content |
Сами документы. TITLE — заголовок, BODY — текст, MODULE_ID — источник (значение iblock означает «из инфоблока»), PARAM2 — ID инфоблока |
b_search_content_stem |
Словоформы. Здесь живёт морфология: механизм, превращающий «кирпича», «кирпичи» и «кирпичом» в одну основу |
b_search_content_site |
Привязка документа к сайту. На многосайтовости это критично |
b_search_content_right |
Права доступа. Важно, если поиск не показывает товары из закрытых групп |
Отсюда главное следствие: ограничение области поиска — это всегда работа с полем PARAM2. Не с каталогом, не с разделами, не с категориями. Инфоблоки, которых вы в выдаче не ждёте, попадают туда так же легко, как каталог.
Как работает ранжирование (и почему «мусор» побеждает товары)
Релевантность считается по совокупности: частота слова в документе, его позиция внутри текста, вес поля и редкость словоформы во всём индексе. TITLE весит больше, чем BODY — но это справедливо только при сравнимых объёмах текста.
На практике получается так: страница, упомянувшая «кирпич» сорок раз среди ссылок и подписей, обходит карточку товара, где это слово стоит в заголовке один раз. Не потому что она релевантнее по смыслу — потому что она больше.
Правило, которое стоит запомнить: «мусор» в результатах поиска — это обычно вопрос объёма, а не смысла. Если выдача выглядит странно, считайте не релевантность документов, а их размер.
И ещё одна мысль, которая на этом проекте оказалась ключевой. Поисковый индекс — это данные, которые меняются в обход правок контента. Переиндексация не меняет ни страницу, ни элемент инфоблока. Она меняет содержимое модуля поиска — а ни один стандартный механизм сброса кэша Битрикса такого события не видит. Запомните эту фразу: через одну часть она выстрелит.
Три вопроса, которые стоит задать своему поиску прямо сейчас
- Что вообще лежит в индексе? Какие инфоблоки и в каких объёмах. Ответ на этот вопрос снимает большинство загадок.
- Совпадает ли выдача с содержимым индекса? Если счётчики и пагинация не бьются — вы смотрите на кэш, а не на поиск.
- Участвует ли в поиске то, что не должно? Регионы, справочники, служебные списки, страницы-заглушки.
Настройка поиска битрикс: с чего начинать диагностику
Если у вас «поиск не находит товары» — не начинайте с переиндексации. Начните с просмотра того, что уже лежит в индексе, и в каких объёмах. Этот скрипт мы теперь запускаем на любом подозрительном проекте первым. Скопируйте его в корень сайта, откройте в браузере, потом обязательно удалите:
<?php
require_once($_SERVER["DOCUMENT_ROOT"] . "/bitrix/modules/main/include/prolog_before.php");
global $DB;
$rs = $DB->Query("
SELECT PARAM2, COUNT(*) AS CNT
FROM b_search_content
WHERE MODULE_ID = 'iblock'
GROUP BY PARAM2
ORDER BY CNT DESC
");
while ($ar = $rs->Fetch()) {
echo "iblock " . (int)$ar["PARAM2"] . " -> " . (int)$ar["CNT"] . " документов\n";
}
Если в выводе рядом с каталогом стоит инфоблок, которого вы в выдаче не ждёте, — причина, скорее всего, уже найдена. В нашем случае список выглядел абсолютно логично для человека, знающего структуру сайта, и абсолютно неожиданно для того, кто смотрит только на страницу поиска.
Часть 2. Как выглядела поломка: сайт ищет, каталог молчит
Входные данные: интернет-магазин стройматериалов — несколько тысяч товаров, региональность, отдельная страница каталога со своим поиском и общесайтовый поиск в шапке.
| Адрес | Что происходило |
|---|---|
/search/?q=кирпич |
Страница отдавала выдачу, которая не билась с содержимым индекса: счётчики не совпадали, число страниц пагинации не соответствовало количеству документов |
/catalog/?q=Кирпич |
Выдача формально есть, но товаров в ней практически нет |
Обе страницы построены на одном поисковом модуле и работают с одним индексом. Значит, дело не в поиске как таковом — иначе ломалось бы везде одинаково. Ломались два разных слоя поверх общего индекса.
Именно поэтому первая версия «битый индекс, надо переиндексировать» была тупиковой. Чтобы её опровергнуть, хватило одной проверки: индекс оказался цел, цифры в нём корректные — а страница показывала не то, что в индексе.
Чтобы у вас была шкала: вот как выглядел результат до и после починки.
| Метрика | До | После |
|---|---|---|
| Документов по запросу «кирпич» | выдача не совпадала с индексом | 2081, 42 страницы по 50 |
| Товаров в первых 200 позициях каталога | около 7 | около 157 |
| Позиций, занятых служебным инфоблоком | ~193 из 200 | 0 |
| Страниц выдачи в индексе внешних ПС | тысячи тонких дублей | закрыты через noindex |
Часть 3. Почему страница отдаёт старые данные: протухший кэш Redis и цифра 2081
Логичная гипотеза номер два звучала так: у страницы поиска есть кэш, и он устарел. Гипотеза подтвердилась — но не полностью, и в этом вся суть.
Проблема сидела в кэше компонента. Запись в Redis осталась от предыдущего состояния поиска и не инвалидировалась. Причина ровно та, о которой мы говорили в первой части: переиндексация поиска меняет содержимое поискового модуля, а не страницы и не элементы инфоблоков. Для механизмов сброса кэша Битрикса это событие не существует.
После принудительной инвалидации страница начала отдавать 2081 документ, 42 страницы по 50. Цифры наконец совпали с реальным содержимым индекса — и на момент замера это было корректное состояние поиска по слову «кирпич».
Как проверить, в кэше ли дело
Самый быстрый способ — временно отключить кэш у компонента поиска и сравнить выдачу. В шаблоне компонента (search.php внутри шаблона) это делается так:
// Отладочный режим: кэш выключен, выдача всегда свежая
"CACHE_TYPE" => "N",
"CACHE_TIME" => 0,
Если при CACHE_TYPE => "N" выдача корректная, а при штатном значении — нет, диагноз подтверждён. После проверки обязательно верните параметр на место, иначе получите полный поисковый запрос на каждый хит:
"CACHE_TYPE" => $arParams["CACHE_TYPE"],
Отдельно проверьте, не включён ли управляемый кэш с большим TTL и не «залипла» ли конкретная запись. Сброс — через админку Настройки → Настройки продукта → Автокеширование → Очистить файлы кэша, либо через Redis:
# Посмотреть, какие ключи вообще есть
redis-cli --scan --pattern "*bitrix*" | head -50
# Удалить ключи конкретного сайта (шаблон префикса смотрите в настройках)
redis-cli --scan --pattern "bitrix:s1:*search*" | xargs -r redis-cli del
Осторожно с
FLUSHALL. На боевом сайте в Redis живут не только кэши — там могут быть сессии, очереди, счётчики. Полная очистка выкинет всех пользователей и может уронить фоновые процессы. Чистите по маске и точечно.
Что отсюда следует практически
Результаты поиска нельзя кэшировать теми же механизмами, что обычные страницы: они зависят от данных, которые меняются вне контентных правок. Разумных вариантов два:
- Не кэшировать поисковый компонент вообще. Поиск по сайту — обычно не тот трафик, ради которого стоит жертвовать актуальностью. Это честный и самый простой путь.
- Держать короткий TTL и явную инвалидацию в момент переиндексации.
Отдельный вывод про отладку: никогда не проверяйте гипотезу о поиске на протухшем кэше. Мы потеряли на этом время и едва не наступили на те же грабли второй раз — уже после правок индекса, когда корректный фикс «не сработал» только потому, что в Redis лежала старая выдача. Порядок такой: сначала правка, потом сброс кэша, потом проверка. Не наоборот.
И здесь важно не остановиться на достигнутом. Кэш починен, /search/ работает. Но каталог по-прежнему не находил товары — значит, у него была своя причина. Две поломки, никак не связанные между собой.
Часть 4. Дефект первый: инфоблок «Регионы», съедавший лимит выдачи
Вот это — главная находка кейса. Читайте внимательно, если у вас на сайте есть региональность, справочники или любой служебный инфоблок.
На сайте работает региональность Аспро. Данные о ней хранятся в отдельном инфоблоке — в нашей конфигурации ID=1 (aspro_lite_regionality). Этот инфоблок тоже был проиндексирован. А его документы содержат в BODY ссылки на разделы каталога по каждому городу — то есть слово «кирпич» встречается там десятки раз: в названиях разделов, подписях, ссылках, хлебных крошках, alt-ах картинок.
Дальше сработала арифметика. Поисковый движок отдаёт ограниченное число позиций — в нашем случае 200. Расклад получился такой:
- из 200 доступных позиций лимита около 193 занимали документы инфоблока регионов;
- товары из инфоблоков 11 и 12 в эти 200 позиций практически не попадали;
- пользователь видел выдачу, где товаров нет — потому что их отрезало до показа.
Подчеркнём то, что легко упустить: проблема была не в сортировке. Если бы дело было в сортировке, товары стояли бы ниже — но существовали. Здесь товары в лимит не попадали вовсе. Поиск до них не доходил физически.
Длинная служебная страница, много раз упомянувшая ключевое слово, вытесняет товары не потому, что релевантнее, а потому что занимает место в ограниченной выдаче. Для интернет-магазина это означает прямую потерю денег: покупатель ищет «купить кирпич оптом», видит выдачу без кирпича и уходит. Ни одна метрика при этом не падает — реклама-то работает, клики-то идут.
Как это увидеть своими глазами
Скрипт из первой части показывает объёмы по инфоблокам. Дополнительно полезно посмотреть, какие именно документы инфоблока-нарушителя попали в индекс и как выглядит их BODY:
SELECT ID, TITLE, LEFT(BODY, 300) AS BODY_HEAD
FROM b_search_content
WHERE MODULE_ID = 'iblock' AND PARAM2 = '1'
LIMIT 20;
Если в BODY_HEAD вы видите длинный список ссылок на разделы — диагноз подтверждён. Этот документ не поисковая страница товара, это навигационный индекс, который случайно попал в поисковую выдачу.
Важно: это не только про регионы
Наш случай — иллюстрация, а не единственный сценарий. Тот же дефект дают:
| Что проиндексировано лишнего | Почему занимает выдачу |
|---|---|
| Региональность (Аспро, собственные модули) | Десятки ссылок на разделы по каждому городу |
| Справочники и классификаторы | Длинные списки значений, в которых встречаются товарные слова |
| Служебные мнемоники SEO (автогенерация meta) | Содержат целевые запросы сразу пачкой |
| Устаревшие/архивные инфоблоки | Остались с прошлых версий сайта, но индексация включена |
| Списки отзывов, вопросов, тегов | Словарь товаров упоминается в каждом элементе |
Проверьте это сегодня. Запустите скрипт подсчёта по
PARAM2и посмотрите, сколько инфоблоков у вас в индексе. Если в списке больше одного-двух — с высокой вероятностью часть вашей выдачи сейчас занимают не товары. Это не теория: такой дефект годами живёт на сайтах, потому что поиск «вроде работает».
Часть 5. Дефект второй: почему пустой arrFILTER ищет везде
Логичный следующий шаг — ограничить область поиска. У компонента bitrix:search.page для этого есть параметр arrFILTER, и есть отдельные ключи под конкретные типы источников, например arrFILTER_iblock_aspro_lite_catalog. Соблазн — попробовать исключить регионы «от противного».
Не сработало. Переданный пустой массив:
"arrFILTER_iblock_aspro_lite_regionality" => array()
Битрикс трактует не как «ни одного инфоблока», а как «область не ограничена». Компонент видит пустой массив и не применяет ограничение вообще. Мы проверили это экспериментально на боевой конфигурации: программно исключить регионы через фильтр не получилось именно по этой причине, а не из-за ошибки в коде.
Рабочий обходной путь — только белый список
Не пытайтесь перечислить, где искать не надо. Перечислите, где надо:
"arrFILTER" => array("iblock_aspro_lite_catalog"),
"arrFILTER_iblock_aspro_lite_catalog" => array(11, 12),
Оговоримся: поведение arrFILTER на пустом значении стоит перепроверять на своей версии модуля поиска — в разных релизах нюансы отличаются. Но принцип универсален: белый список надёжнее чёрного всегда, когда API не даёт гарантий на пустое значение. Мы применяем это правило не только в поиске.
Границы применимости
Белый список лечит каталоговый компонент — страницу /catalog/?q=…. Общесайтовый /search/ регионы всё равно покажет, потому что там область поиска другая. Поэтому одного фильтра недостаточно: нужен слой на уровне индекса — отключение индексации. Это уже часть чек-листа.
Второе ограничение: фильтр не умеет исключать частично. Вы не можете сказать «искать в этом инфоблоке, но не в таких-то его разделах». Если товарные данные и мусор лежат в одном инфоблоке — белый список не спасёт, придётся чистить индекс или идти на внешний поисковый сервер (об этом в Части 8).
Часть 6. Чек-лист внедрения: четыре шага и один костыль
Порядок важен. Если поменять шаги местами, вы получите выдачу, которая «всё ещё не работает», и будете искать причину заново.
Шаг 1. Выключить индексацию служебного инфоблока
Через админку: Контент → Инфоблоки → Регионы → вкладка «Поиск» → снять «Элементы участвуют в поиске» → Сохранить.
То же самое напрямую в БД. Если разделы инфоблока тоже индексировались, верните и INDEX_SECTION в исходное значение:
-- Выключаем индексацию элементов инфоблока регионов
UPDATE b_iblock
SET INDEX_ELEMENT = 'N',
INDEX_SECTION = 'N'
WHERE ID = 1;
Проверить, что применилось:
SELECT ID, NAME, INDEX_ELEMENT, INDEX_SECTION
FROM b_iblock
WHERE ID = 1;
Шаг 2. Удалить уже проиндексированные документы
Здесь мы сами сначала сделали неправильно. Прямой DELETE по головной таблице чистит только её и оставляет сирот в связанных таблицах индекса:
-- ТАК ДЕЛАТЬ НЕ НАДО: чистит одну таблицу из нескольких
DELETE c FROM b_search_content c
JOIN b_search_content_site s ON s.SEARCH_CONTENT_ID = c.ID
WHERE c.MODULE_ID = 'iblock' AND c.PARAM2 = '1' AND s.SITE_ID = 's1';
Правильно — удалять через API поиска, тогда чистятся все связанные таблицы:
<?php
require_once($_SERVER["DOCUMENT_ROOT"] . "/bitrix/modules/main/include/prolog_before.php");
$rs = CIBlockElement::GetList(
array(),
array("IBLOCK_ID" => 1),
false,
false,
array("ID")
);
while ($ar = $rs->Fetch()) {
$indexId = CSearch::GetIndexID("iblock", $ar["ID"], 1, "s1");
if ($indexId) {
CSearch::DeleteIndexByID($indexId);
}
}
echo "Готово\n";
Перед этим полезно один раз посмотреть, из каких таблиц вообще состоит поисковый индекс в вашей версии модуля, и после чистки убедиться, что сирот не осталось:
SHOW TABLES LIKE 'b_search_content%';
-- Контроль: сколько документов инфоблока 1 осталось в индексе
SELECT COUNT(*) AS LEFT_OVER
FROM b_search_content
WHERE MODULE_ID = 'iblock' AND PARAM2 = '1';
Если элементов много (десятки тысяч), удаление через API — это тысячи отдельных запросов. Разбейте процесс по шагам, добавьте вывод прогресса и запускайте через CLI, а не через браузер, иначе упрётесь в
max_execution_timeна середине и получите частично почищенный индекс.
Шаг 3. Осторожно с лимитом выдачи
Соблазн после шагов 1–2 — поднять PAGE_RESULT_COUNT до 2000, чтобы отдавать все совпадения (в нашем случае их было порядка 596). Не спешите.
Сначала проверьте, что этот параметр у вас ограничивает именно сырую выдачу движка, а не размер страницы пагинации. Если второе — вы получите одну страницу на 2000 товаров, что плохо и для пользователя, и для внешней выдачи: такая страница станет ещё и каталогом-гигантом, конкурирующим с основной категорией.
Если первое — поднимайте до разумного значения, но отдавайте себе отчёт: это компенсирующий костыль, а не лечение. После шагов 1–2 в первых 200 позициях уже будет порядок около 157 товаров, и необходимость в поднятии лимита может отпасть совсем. Попробуйте сначала без него.
Шаг 4. Откатить отладочные правки и сбросить кэш ещё раз
В процессе отладки в catalog/main/search.php остался временный патч, который отключал кэш. Его нужно вернуть на место, иначе вы будете гонять полный поисковый запрос на каждый хит:
// Было при отладке
// "CACHE_TYPE" => "N",
// Вернуть как было
"CACHE_TYPE" => $arParams["CACHE_TYPE"],
И сразу после этого — сбросить кэш поисковых страниц ещё раз. Redis всё ещё хранит старую выдачу, и на протухшем кэше корректный фикс будет выглядеть как «ничего не изменилось». Мы на эти грабли едва не наступили повторно.
Проверка финального результата — по тем же трём адресам и с теми же цифрами, что и в начале:
-- 1. В индексе нет документов служебного инфоблока
SELECT COUNT(*) FROM b_search_content WHERE MODULE_ID='iblock' AND PARAM2='1';
-- 2. Товары в индексе есть и в нормальном объёме
SELECT COUNT(*) FROM b_search_content WHERE MODULE_ID='iblock' AND PARAM2 IN ('11','12');
-- 3. Поиск по контрольному слову отдаёт товары
-- Проверяем в браузере: /search/?q=кирпич и /catalog/?q=Кирпич
Сводная таблица: симптом → слой → причина
| Симптом | Слой | Причина |
|---|---|---|
| Счётчики и пагинация не бьются с содержимым индекса | Кэш компонента (Redis) | Реиндексация не инвалидирует кэш страницы поиска |
| В выдаче нет товаров, хотя поиск «сработал» | Поисковый индекс | Служебный инфоблок занял лимит выдачи (~193 из 200) |
| Исключение инфоблока через arrFILTER не действует | Компонент поиска | Пустой массив = «область не ограничена», а не «ничего» |
| Товар не находится по артикулу | Индекс + токенизация | Свойство не индексируется или код режется морфологией |
| Товары находятся, но не те / много мусора | Ранжирование | Крупные документы перевешивают короткие карточки |
Часть 7. Поиск по артикулу: отдельная боль, отдельное решение
Требование «искать товары по NAME и по артикулу» — это третья задача, независимая от первых двух, и решается она не в компоненте, а в индексе и на уровне запроса. Смешивать её с историей про регионы нельзя: причины разные, инструменты разные.
Это одна из самых частых жалоб в интернет-магазинах вообще, и в Битриксе особенно — потому что поведение штатного поиска здесь неочевидно даже для опытных разработчиков.
Проверить, попадает ли артикул в индекс вообще
Артикул обычно хранится как свойство элемента и индексируется только при включённом флаге «Участвует в поиске» (в БД — SEARCHABLE):
SELECT ID, NAME, CODE, SEARCHABLE, MULTIPLE
FROM b_iblock_property
WHERE IBLOCK_ID IN (11, 12)
AND (CODE LIKE '%ARTIC%' OR NAME LIKE '%Артикул%');
| Поле | Что означает |
|---|---|
SEARCHABLE = 'Y' |
Свойство участвует в поиске. Если 'N' — артикул в индекс не попадает вообще |
MULTIPLE = 'Y' |
Множественное свойство. Индексируется, но проверять надо каждое значение отдельно |
CODE |
Символьный код — именно его удобно искать в коде компонента, а не название свойства |
Если SEARCHABLE = 'N' — включить и запустить полную переиндексацию: Настройки → Поиск → Переиндексация, предварительно сняв галку «переиндексировать только изменённые». Частичная переиндексация новое поле для старых элементов не подхватит — это отдельная ловушка, на которой теряют часы.
-- Программная полная переиндексация (осторожно: долго и нагружает сервер)
<?php
require_once($_SERVER["DOCUMENT_ROOT"] . "/bitrix/modules/main/include/prolog_before.php");
@set_time_limit(0);
CSearch::DeleteIndexTotal();
CSearch::RebuildIndex();
Не запускайте полную переиндексацию через браузер. Только CLI. На каталоге в несколько тысяч товаров это может занять от нескольких минут до десятка, и обрыв по таймауту оставит индекс в половинчатом состоянии. Запускайте ночью или в окно низкой нагрузки.
Почему «поиск по артикулу не работает» даже когда он индексируется
Штатный полнотекстовый поиск токенизирует строку и стеммит слова. Для кодов вида А-12345, SKU_00219, BR-45/2 дефис, слэш и подчёркивание работают как разделители. Код распадается на короткие токены, которые либо не находятся, либо находятся слишком широко — вместо одной карточки покупатель получает десяток.
Проверить, что реально лежит в индексе по конкретному товару, можно так:
SELECT sc.ID, sc.TITLE, sc.BODY
FROM b_search_content sc
WHERE sc.MODULE_ID = 'iblock'
AND sc.PARAM2 IN ('11', '12')
AND sc.TITLE LIKE '%фрагмент_названия%'
LIMIT 5;
Посмотрите содержимое BODY: если артикула там нет как отдельного слова — вопрос не в компоненте, а в индексации свойства. Если артикул есть, но разбит на куски — вопрос в токенизации.
Рабочее решение: точный поиск в обход индекса
Надёжный путь — сначала прямой запрос к свойству артикула через CIBlockElement::GetList, и только если там пусто — идти в полнотекстовый поиск. Это несколько строк в начале поискового компонента, и они снимают целый класс жалоб:
<?php
$q = trim($_GET["q"]);
// Шаг 1. Пробуем найти точное совпадение по артикулу
$articleHits = array();
if ($q !== "") {
$rs = CIBlockElement::GetList(
array(),
array(
"IBLOCK_ID" => array(11, 12),
"ACTIVE" => "Y",
"PROPERTY_ARTICLE" => $q, // символьный код свойства артикула
),
false,
false,
array("ID", "NAME")
);
while ($ar = $rs->Fetch()) {
$articleHits[] = $ar;
}
}
// Шаг 2. Если точных совпадений нет — идём в полнотекстовый поиск
if (empty($articleHits)) {
// ... штатный вызов компонента bitrix:search.page
}
Важные нюансы, которые легко упустить:
- Точное совпадение ≠ регистронезависимое. Фильтр по свойству в Битриксе сравнивает значение как есть. Нормализуйте ввод и значение свойства (
toUpperCase, обрезка пробелов и дефисов) перед сравнением, иначе «а-12345» не найдёт «А-12345». - Нормализуйте сам запрос. Пользователи вставляют артикулы с пробелами, кавычками, точкой на конце и невидимыми символами из Excel. Одна строка очистки экономит десятки обращений в поддержку.
- Частичное совпадение — отдельный сценарий. Если нужно искать по фрагменту артикула,
PROPERTY_ARTICLE => $qне поможет: используйте"%".$q."%", но помните, что этоLIKE '%...%'без индекса и на большом каталоге будет тормозить. Для больших каталогов правильнее внешний поисковый сервер. - Не сломайте остальной поиск. Найденное по артикулу должно встраиваться в общую выдачу, а не заменять её — иначе поиск по названию перестанет работать.
Хорошая проверка на будущее. Добавьте в интерфейс подсказку: «ищет по названию и артикулу». Большая часть жалоб «поиск не находит товар» — это на самом деле жалобы на то, что поиск не ищет там, где пользователь ожидал. Явное обещание в интерфейсе снимает половину претензий ещё до того, как они превратятся в тикет.
Часть 8. Поисковый сервер и Docker: когда штатного индекса уже мало
Возникает закономерный вопрос: а нельзя ли решить всё это радикально — вынести поиск на отдельный движок? Можно. Но сначала — честная оговорка, ради которой эта часть вообще написана.
Внешний поисковый сервер не лечит дефекты из Частей 4 и 5. Если у вас служебный инфоблок съедает выдачу, Sphinx покажет его ровно так же — или даже охотнее, потому что ранжирует по своим правилам. Сначала пройдите чек-лист из Части 6. Внешний движок — это следующий шаг, а не замена диагностике.
Когда переходить на внешний поиск
Признаки, при которых это оправдано:
- Каталог вырос и штатный индекс не справляется. Поиск стал заметно тормозить, выдача отдаётся секундами, а переиндексация занимает часы.
- Релевантность не лечится настройками. Вы уже прошли чек-лист, почистили индекс, включили индексацию свойств — а выдача всё равно «не та».
- Нужны фасеты и фильтры внутри выдачи. Подсказки по мере ввода, «похожие товары», фильтр результатов по цене и разделу прямо на странице поиска.
- Артикулы и SKU со спецсимволами. Нужно искать по
А-12345/2как по целому токену, а не как по набору обрывков. - Нужна управляемая релевантность. Ручные веса полей, продвижение отдельных товаров, исключение конкретных документов — штатным модулем это не настраивается.
- Поиск — точка входа в магазин. Если по внутреннему поиску идёт значимая доля заказов, его качество становится деньгами, а не удобством.
Три варианта: сравнение без маркетинга
| Решение | Что даёт | Чем платите | Когда брать |
|---|---|---|---|
| Штатный индекс Битрикса | Работает из коробки, нет отдельного сервиса, нечему падать | Ограниченное управление релевантностью, морфология «как есть», скорость на больших каталогах | Каталог до десятков тысяч позиций, обычные требования |
| Sphinx / Manticore | Скорость, свои веса, своя токенизация, управление через конфиг | Отдельный сервис, свой конфиг, обслуживание индекса, ещё одна точка отказа | Каталог от десятков тысяч, нужна точность и контроль |
| OpenSearch / Elasticsearch | Фасеты, агрегации, автодополнение, богатая экосистема | Тяжело по ресурсам (JVM, память), сложнее в эксплуатации | Нужны фасеты и аналитика, есть ресурсы сервера |
Важно про совместимость. Битрикс умеет работать со Sphinx как с внешним движком полнотекстового поиска — это настраивается в модуле поиска. Поддержка OpenSearch/Elasticsearch появляется в новых релизах модуля; проверьте состав настроек в своей версии, прежде чем планировать миграцию. Если поддержки нет — часть задач решается сторонними модулями с маркетплейса, но это уже отдельная интеграция со своими рисками.
Почему логично смотреть в сторону Manticore
Sphinx Search — тот самый движок, с которым Битрикс исторически интегрируется, — в своём классическом виде давно не развивается. Его живой форк Manticore Search совместим по протоколу и во многом по синтаксису конфига, поэтому с точки зрения сайта подмена происходит почти незаметно:
| Порт | Протокол | Для чего |
|---|---|---|
9312 |
SphinxAPI | Родной протокол, которым пользуется модуль поиска Битрикса |
9306 |
MySQL-протокол | Удобно для отладки: подключаетесь любым MySQL-клиентом и делаете запросы руками |
9308 |
HTTP/JSON | Интеграции, проверки, свой поиск в приложении |
Последний пункт недооценивают, а он спасает время: возможность подключиться к поисковому движку обычным mysql -h127.0.0.1 -P9306 и увидеть, что реально лежит в индексе, — это тот же приём, что мы применяли к b_search_content в первой части, только теперь без SQL к боевой базе сайта.
Оговорка по версиям. В свежих мажорных релизах Manticore режим работы через конфиг постепенно вытесняется RT-режимом с индексами, создаваемыми командами. Сверяйтесь с документацией вашей версии — синтаксис и наличие plain-режима отличаются между ветками. Ниже схема для режима с конфигом, потому что именно он ближе всего к тому, как устроена интеграция с Битриксом.
Установка через Docker
Почему Docker, а не установка пакетом. Причины практические, и они важнее «модности»:
- Изоляция. Поисковый сервер не тащит за собой системные зависимости на боевую машину с сайтом.
- Воспроизводимость. Версия движка зафиксирована в образе. Обновились — можно откатиться на прежний тег за минуту.
- Предсказуемость. Конфиг, данные и порты описаны в одном файле, который лежит в репозитории рядом с проектом.
- Эксперименты без риска. Поднять движок, залить индекс, сравнить выдачу со штатной — и удалить, если не подошло.
docker-compose.yml
version: "3.8"
services:
manticore:
image: manticoresearch/manticore:6
container_name: manticore
restart: always
# Порты слушаем ТОЛЬКО на localhost.
# Наружу поисковый движок выставлять нельзя — там нет аутентификации.
ports:
- "127.0.0.1:9312:9312"
- "127.0.0.1:9306:9306"
- "127.0.0.1:9308:9308"
ulimits:
nproc: 65535
nofile:
soft: 65535
hard: 65535
# Жёсткий лимит памяти. Поисковый движок без него умеет съесть всю ОЗУ.
mem_limit: 2g
volumes:
- ./manticore/manticore.conf:/etc/manticoresearch/manticore.conf:ro
- ./manticore/data:/var/lib/manticore
- ./manticore/log:/var/log/manticore
environment:
- EXTRA_FLAGS=--config /etc/manticoresearch/manticore.conf
healthcheck:
test: ["CMD", "mysql", "-h127.0.0.1", "-P9306", "-e", "SHOW STATUS"]
interval: 30s
timeout: 10s
retries: 3
start_period: 30s
Разберём то, что здесь неочевидно, — именно эти строки обычно и создают проблемы:
| Строка | Зачем |
|---|---|
127.0.0.1:9312:9312 |
Привязка к localhost. Если написать просто 9312:9312, движок окажется доступен из интернета без пароля. Это классическая и очень опасная ошибка |
ulimits: nofile |
Поисковый движок держит много соединений и файловых дескрипторов. Без поднятия лимитов он начинает падать под нагрузкой с невнятными ошибками |
mem_limit |
Ограничение памяти. Особенно важно, если на сервере живёт ещё и сайт с базой |
Отдельный том /var/lib/manticore |
Индекс — это данные. Без постоянного тома он исчезнет вместе с контейнером, и вы будете переиндексировать каталог заново |
start_period |
Движку нужно время на загрузку. Без этой паузы healthcheck объявит контейнер мёртвым на старте |
Если ваш образ не подхватывает
EXTRA_FLAGS— не полагайтесь на переменную, просто примонтируйте конфиг по тому пути, который читает ваша версия образа, и проверьте это в логах. Пути внутри образа между версиями отличались, поэтому сверяйтесь с документацией образа, а не с этой статьёй.
Подготовка каталогов и запуск
# Каталоги для конфига, данных и логов
mkdir -p ./manticore/data ./manticore/log
# Конфиг должен быть доступен на чтение контейнеру
touch ./manticore/manticore.conf
# Запуск
docker compose up -d
# Смотрим логи старта
docker compose logs -f manticore
# Проверяем, что движок отвечает
docker compose exec manticore mysql -h127.0.0.1 -P9306 -e "SHOW STATUS"
Схема конфига: из чего он состоит
В модуле поиска Битрикса есть генератор конфигурации — он собирает файл под структуру вашей базы, и это правильный путь: не писать конфиг руками, а сгенерировать и далее править. Ниже — схема, чтобы вы понимали, из чего он состоит и что там безопасно менять.
# Источник данных: как движок забирает документы из MySQL сайта
source bitrix_source
{
type = mysql
sql_host = host.docker.internal # адрес MySQL, если БД не в этом же compose
sql_user = <db_user>
sql_pass = <db_pass>
sql_db = <db_name>
sql_port = 3306
sql_query_pre = SET NAMES utf8
# Основной запрос по поисковому индексу Битрикса
sql_query = \
SELECT ID, MODULE_ID, ITEM_ID, PARAM2, TITLE, BODY \
FROM b_search_content
# Числовые атрибуты: по ним потом фильтруем область поиска
sql_attr_uint = PARAM2 # ID инфоблока — тот самый фильтр
sql_attr_uint = ITEM_ID
sql_attr_string = MODULE_ID
# Текстовые поля, которые участвуют в поиске и выводятся в результатах
sql_field_string = TITLE
sql_field_string = BODY
}
# Сам индекс: где лежат данные и как обрабатывать текст
index bitrix_index
{
source = bitrix_source
path = /var/lib/manticore/bitrix_index
# Морфология. Для русского — stem_enru или russian_stemmer
morphology = stem_enru
min_word_len = 2
min_infix_len = 2
# ГЛАВНОЕ для артикулов: какие символы считать разделителями
# Убрав дефис и слэш, вы получите поиск по "А-12345" как по целому токену
charset_table = 0..9, A..Z->a..z, a..z, \
U+410..U+42F->U+430..U+44F, U+430..U+44F, \
U+401->U+451, U+451
}
# Служба поиска: порты и лимиты
searchd
{
listen = 9312
listen = 9306:mysql41
log = /var/log/manticore/searchd.log
query_log = /var/log/manticore/query.log
pid_file = /var/run/manticore/searchd.pid
}
Обратите внимание на charset_table. Это ровно то место, где решается проблема из Части 7: если дефис и слэш не перечислены как разделители, артикул А-12345 становится единым токеном, и поиск по нему работает точно. Задача, которую штатным индексом приходилось решать обходным кодом на CIBlockElement::GetList, здесь решается строчкой в конфиге.
Построение индекса
# Первичная полная индексация
docker compose exec manticore indexer --config /etc/manticoresearch/manticore.conf --all
# Инкрементальное обновление (по расписанию, например раз в 15 минут)
docker compose exec manticore indexer --config /etc/manticoresearch/manticore.conf --rotate bitrix_index
Подключение к Битриксу
Дальше — в административной части сайта, в модуле поиска: выбирается внешний движок, указываются хост и порт, а также имя индекса. Если движок живёт на том же сервере в контейнере, для сайта это 127.0.0.1 и порт 9312. Если контейнеры в своей сети — по имени сервиса, которое вы задали в compose.
Порядок такой:
- Настроить подключение в модуле поиска и переключить движок на внешний.
- Запустить полную индексацию — и в Битриксе, и построение индекса в самом движке.
- Сравнить выдачу: контрольные слова и артикулы. Обязательно с теми же цифрами, что вы снимали до миграции.
- Настроить регулярное обновление индекса — через агент Битрикса или через cron с
--rotate. - Проверить, что область поиска по-прежнему ограничена каталогом. Смена движка не отменяет дефектов из Частей 4 и 5.
Самая частая ошибка при миграции: переключить движок и забыть про область поиска. Внешний движок с готовым индексом обслуживает весь поиск, включая служебные инфоблоки. Если вы не отфильтровали их раньше, после миграции они никуда не денутся — а иногда и поднимутся выше, потому что их текст теперь получил правильный стемминг.
Риски, о которых надо подумать заранее
- Новая точка отказа. Упал поисковый сервис — поиск лежит. Нужен
restart: always(у нас есть), healthcheck и, желательно, откат на штатный поиск, если движок не ответил. - Рассинхрон индекса. Между переиндексацией в Битриксе и построением индекса в движке есть окно. Если это заметно клиенту, порядок операций нужно отладить.
- Двойное потребление памяти. Движку нужна своя память, и если он на том же сервере, что и сайт, лимиты обязательны для обоих.
- Персистентность. Тома с данными — в бэкап. Восстановить индекс можно переиндексацией, но это часы и нагрузка.
- Реквизиты БД в конфиге. В конфиг попадают логин и пароль от базы. Файл не должен лежать в публичном репозитории или в веб-доступе.
Готовьтесь честно: миграция на внешний поисковый сервер — это не «установить Docker и всё заработает». Это новый сервис, новые регламенты обновления индекса, новый мониторинг и новая ответственность. Мы советуем идти на это, когда возможности штатного поиска исчерпаны, а не когда хочется «современно». В нашем кейсе ни дефект с инфоблоком, ни кэш, ни артикулы внешнего движка не требовали — хватило наведения порядка в штатном индексе.
Три ловушки, в которые мы лично попали при подъёме OpenSearch
Чек-лист выше — это то, что нужно сделать правильно с первого раза. А вот три вещи, на которых мы реально споткнулись, разворачивая движок в боевом проекте, — держите их в голове, они не очевидны из документации.
Ловушка первая: переиндексация тихо обрывается на первой тысяче.
В коде модуля iblock Битрикса, в методе OnSearchReindex, стоит защитный LIMIT 1000 на каждый проход по инфоблоку — это оговорено прямо в комментарии к коду: «For MySQL, we have to solve client out of memory problem by limiting the query». Логика рассчитана на то, что вызывающий код передаёт состояние $NS и вызывает CSearch::ReIndexAll() в цикле, пока функция не вернёт финальное число, а не массив-продолжение.
Если вызвать ReIndexAll($bFull, $max_execution_time) с $max_execution_time = 0 один раз — функция отработает без ошибок, отрапортует «готово», но по факту обработает только первую партию из 1000 элементов на инфоблок. Мы поймали это на каталоге в 18 000+ товаров: скрипт отчитался об успехе, а в поисковом индексе оказалось 96 документов вместо тысяч.
Рабочий цикл выглядит так:
$NS = [];
$step = 0;
do {
$step++;
$result = CSearch::ReIndexAll(true, 25, $NS); // 25 сек на шаг
if (is_array($result)) {
$NS = $result; // продолжение: MODULE, ID, CNT
} else {
break; // $result — финальное число обработанных документов
}
} while (true);
Признак, что вы наступили на эту грабли: количество документов в поисковом индексе после «успешной» переиндексации подозрительно круглое (ровно 1000, 2000 и т. д. на инфоблок) и не растёт при повторных запусках без явного цикла.
Ловушка вторая: индекс выглядит пустым, хотя данные реально записались.
После переиндексации через OpenSearch счётчик документов в _cat/indices может застыть на месте на десятки минут, хотя store.size при этом растёт. Причина не в записи — она идёт нормально, — а в том, что статистика docs.count обновляется только при refresh индекса, а не в реальном времени при каждой вставке. Если во время массовой индексации refresh_interval не форсируется, счётчик просто не успевает обновляться, и создаётся полное ощущение, что процесс завис или ничего не пишет.
Диагностика — не гадать, а форсировать refresh и смотреть заново:
curl -s -k -u admin:PASS -X POST "https://localhost:9200/INDEX_NAME/_refresh"
curl -s -k -u admin:PASS "https://localhost:9200/_cat/indices?v"
Если после этого счётчик резко подскакивает до ожидаемого значения — паники не было, был просто lag статистики.
Ловушка третья: боевой пароль в открытом виде в публичном файле.
Она не про OpenSearch напрямую, но случилась в том же проекте и стоит того, чтобы её здесь упомянуть, потому что она типична для «старых» боевых проектов, куда заходишь чинить одно, а находишь рядом совсем другое. При ревизии корня сайта (то, чем мы занялись параллельно с настройкой поиска) обнаружился десяток служебных PHP-скриптов, оставшихся с прошлых доработок, один из которых содержал захардкоженный пароль от боевой базы данных в открытом виде — и был доступен по прямому URL любому, кто знал имя файла.
Практический вывод: пока чините поиск, не поленитесь одной командой посмотреть, что ещё лежит в корне сайта:
grep -rl "password\|PASS\|pwd" /var/www/*/data/www/*/*.php 2>/dev/null | grep -v bitrix/
Любой файл, который явно не часть ядра или темы (test_*.php, check_*.php, debug_*.php, скрипты с говорящими названиями вроде convert_all.php) и при этом содержит креды — кандидат на немедленный перенос в недоступную снаружи папку, даже если сейчас «работает и не трогай». Разовые диагностические скрипты, оставленные в вебруте, — одна из самых частых причин утечек на проектах, которые никто не аудировал годами.
Как на самом деле выглядел подъём движка: два эпизода, которые стоило бы предвидеть
Чек-лист по Docker в начале Части 8 — это уже отфильтрованная, правильная версия. На практике мы прошли её не с первого раза, и оба отступления от правильного пути стоят отдельного разбора — потому что оба выглядят как мелочь, пока не случаются.
Эпизод первый: контейнер без тома — это бомба замедленного действия.
Первый запуск движка выглядел рабочим: контейнер поднялся, API отвечал, каталог проиндексировался. На этом этапе мы обнаружили, что порт движка по ошибке смотрел наружу на всех интерфейсах — -p 9200:9200 вместо -p 127.0.0.1:9200:9200. Дыра очевидная: движок в базовой поставке из коробки не имеет включённой аутентификации при таких настройках, и любой, кто знает IP сервера, мог достучаться до всего поискового индекса напрямую.
Исправление напрашивалось само — остановить контейнер, пересоздать с правильным биндингом на localhost:
docker stop opensearch
docker rm opensearch
docker run -d --name opensearch --restart unless-stopped \
-p 127.0.0.1:9200:9200 -p 127.0.0.1:9600:9600 \
-e "discovery.type=single-node" \
-e "OPENSEARCH_INITIAL_ADMIN_PASSWORD=..." \
opensearchproject/opensearch:2.15.0
Проблема в том, что docker run без явного флага -v не создаёт постоянное хранилище — данные живут внутри слоя контейнера. docker rm стирает их безвозвратно вместе с контейнером. Результат: весь только что построенный индекс (порядка 40 минут работы на каталоге в несколько тысяч товаров) пропал, и пришлось переиндексировать заново.
Правильный порядок — сразу с volume, до первого боевого запуска:
docker volume create opensearch-data
docker run -d --name opensearch --restart unless-stopped \
-p 127.0.0.1:9200:9200 -p 127.0.0.1:9600:9600 \
-v opensearch-data:/usr/share/opensearch/data \
-e "discovery.type=single-node" \
-e "OPENSEARCH_INITIAL_ADMIN_PASSWORD=..." \
-e "OPENSEARCH_JAVA_OPTS=-Xms768m -Xmx768m" \
opensearchproject/opensearch:2.15.0
Отдельно к OPENSEARCH_JAVA_OPTS: значение в 768 МБ — не универсальная рекомендация, а следствие конкретных условий сервера (5,8 ГБ RAM, из которых часть уже занята другими сервисами, а своп подъеден почти под завязку). На более свободном сервере heap стоит поднимать до 2 ГБ и выше; на утеснённом — держите его явно ограниченным, иначе движок без задумчивости заберёт всю доступную память и уронит остальные сервисы на той же машине.
Практический вывод: docker volume create и -v в команде запуска — не опциональный шаг «сделаем потом, если понадобится», а обязательный первый шаг перед любой боевой индексацией. Проверить, что volume реально подключился, можно одной командой сразу после старта:
docker inspect opensearch --format '{{json .Mounts}}'
Пустой массив [] в ответе — сигнал остановиться и не индексировать, пока не подключите постоянное хранилище.
Эпизод второй: поиск находит товары, но не там, где нужно.
После того как индекс встал на место, поиск по названию заработал — но релевантность выдачи была не идеальной: товары, у которых слово запроса встречалось один раз в заголовке, проигрывали товарам, где то же слово несколько раз мелькало в описании или характеристиках. Штатный запрос к движку искал совпадения только в поле body (объединённом тексте описания и свойств), полностью игнорируя title.
Патч — не в конфигурации, а в самом PHP-коде модуля поиска, в месте, где строится тело запроса к движку:
// Было: поиск только по body
$query['query']['bool']['must'][] = [
'match' => [
'body' => ['query' => $strQuery],
],
];
// Стало: title и body ищутся параллельно, но title весит в 5 раз больше
$query['query']['bool']['must'][] = [
'bool' => [
'should' => [
['match' => ['title' => ['query' => $strQuery, 'boost' => 5]]],
['match' => ['body' => ['query' => $strQuery, 'boost' => 1]]],
],
'minimum_should_match' => 1,
],
];
Стоит держать в уме, что это правка файла внутри модуля ядра Битрикса — она не переживёт обновление модуля search и должна быть задокументирована отдельно (или вынесена в перекрывающий файл, если версия Битрикса это поддерживает), иначе через полгода кто-то обновит модуль и относительность снова просядет без видимой причины.
Ещё одна деталь, которая обнаружилась при этом же патче и достойна отдельного пункта в чек-листе: поле title в автоматически сгенерированной схеме индекса получает базовый анализатор без русской морфологии, тогда как body — анализатор russian с полноценным стеммингом. Для точных однословных совпадений разницы почти нет, но при многословных запросах с изменением окончаний («кирпичи» вместо «кирпич») это может ощутимо повлиять на то, что находится в заголовках, а что нет. Стоит явно проверить схему индекса перед боевым запуском, а не полагаться на дефолт.
Покажу 10 ошибок, из-за которых ваш сайт не растёт
Экспресс-аудит за 1 рабочий день — техника, видимость против конкурентов и GEO.
Получить экспресс-аудитЧасть 9. SEO-побочка: 2081 тонкая страница и краулинговый бюджет
Отдельная часть этой истории, которую обычно не замечают, пока не откроют панель вебмастера.
Внутренний поиск, отдающий тысячи страниц по одному слову, — это не только проблема навигации. 2081 документ по запросу «кирпич» означает 42 страницы пагинации, доступные внешним роботам. Плюс столько же по каждому другому слову — а их тысячи. Что это даёт:
- Тонкие дублирующие страницы. Выдача по параметру
qпочти не отличается от страниц категорий, но конкурирует с ними за один и тот же смысл. Внешний поисковик выбирает «лучшую» из двух похожих страниц — и не всегда это категория. - Слив краулингового бюджета. Робот обходит бесконечные комбинации параметров вместо реальных карточек товаров. Для магазина с тысячами SKU это означает, что новые товары индексируются неделями.
- Риск soft 404. Выдача без результатов, отдающая
200с пустым содержимым. Поисковик считает такую страницу низкокачественной, а накопление таких страниц бьёт по всему домену. - Раздутый отчёт по страницам в индексе. По нему потом невозможно оценить реальное состояние: непонятно, где товары, а где выдача по слову «и».
Что закрывать в первую очередь
- Страницы результатов поиска —
/search/?q=…и/catalog/?q=…. - Параметры сортировки и фильтров —
?sort=,?PRICE_FROM=,?PAGEN_1=. На магазинах это даёт дублей на порядок больше, чем поиск. Если бюджет ограничен, я бы начал именно с них. - Выдача без результатов — должна отдавать
404, а не200. - Пагинация — корректный
canonical, чтобы страницы «2», «3», «4» не собирали вес себе.
Важная тонкость: Disallow и noindex несовместимы
Здесь ломается большинство попыток. Если закрыть /search? через Disallow в robots.txt, робот не сможет прочитать noindex на самой странице — и она может остаться в индексе как «неизвестная». Логика такая:
| Хотите | Как делать |
|---|---|
| Убрать из индекса совсем | noindex на странице + разрешить обход, чтобы робот этот noindex увидел |
| Не тратить бюджет на обход параметров | Disallow в robots.txt, но тогда noindex не сработает — полагайтесь на исключение по обходу |
| И то и другое | Disallow для параметров пагинации и сортировок, noindex только для страниц, которые робот должен обойти и выкинуть |
Для Яндекса есть третий инструмент, который часто решает задачу элегантнее — директива Clean-param. Она говорит роботу склеивать страницы с разными значениями параметра в одну: не нужно ни закрывать, ни плодить noindex.
# robots.txt
User-agent: *
Disallow: /search?
Disallow: /*?q=
Disallow: /*sort=
Disallow: /*PAGEN_1=
Clean-param: q /search?
Синтаксис Clean-param и поддерживаемые шаблоны проверяйте по актуальной документации Яндекса — там были изменения, и от версии зависит, какие формы он понимает. Google параметрами не управляет давно, для него работают canonical, noindex и запрет обхода.
Что ещё:
- Пустая страница поиска (
/search/без параметров) остаётся открытой — она имеет смысл как точка входа и как посадочная со ссылок. - Карту сайта проверить на то, чтобы страницы выдачи туда не попадали вообще. Генераторы иногда вытаскивают всё подряд.
- Внешние ссылки на поиск — если на
/search?q=ведут ссылки с других сайтов,Disallowне мешает такой ссылке существовать, но снимает необходимость её показывать.
Как измерить эффект
Открытой метрики «краулинговый бюджет» не существует, но косвенно его видно по нескольким изменениям, которые стоит отследить в течение месяца после правок:
- Общее число страниц в индексе в Яндекс.Вебмастере и Google Search Console — должно уменьшаться за счёт выдачи и параметров, а не за счёт товаров.
- Скорость индексации новых товаров. Добавьте десяток новых позиций и посмотрите, за сколько они попадут в индекс. До правок и после — разница обычно заметная.
- Доля обходов параметров. В отчётах по обходу видно, куда робот тратит запросы.
- Позиции категорий. После ухода из выдачи дублей категории обычно подрастают — не сразу, за 3–6 недель.
И отдельно — про стоимость этой поломки. Тонкие страницы поиска годами выглядели как «нормальные» страницы сайта: их видел робот, они отдавали
200, они были в индексе. Проблема не в том, что они существовали. Проблема в том, что пока они тратили бюджет, реальные карточки товаров ждали своей очереди — а магазин в это время платил за рекламу, ведущую на плохо проиндексированные страницы.
Часть 10. Семь правил, которые мы вынесли из этого кейса
- Поиск — это не элемент инфоблока, а отдельное хранилище. Всё, что попало в индекс, участвует в выдаче. Служебные инфоблоки надо отключать от индексации осознанно и сразу при внедрении, а не когда о них вспомнят через два года.
- Ранжировать может не тот, кто релевантнее. Большой
BODYвесит больше смысла. Если выдача выглядит как мусор — считайте не релевантность документов, а их размер. - Пустой фильтр ≠ пустая область поиска. Пустой массив означает «ограничений нет». Белые списки надёжнее чёрных всегда, когда API не даёт гарантий на пустое значение.
- Кэш поиска — отдельная сущность. Он зависит от данных, которые меняются вне контентных правок. Не проверяйте гипотезы на протухшем кэше и не забывайте чистить его после правок индекса.
- Прямой SQL по файлам индекса — риск сирот. Удаляйте документы через API поиска, а не только из головной таблицы.
- Внешний поисковый сервер не лечит неразобранные дефекты. Он добавляет слой, а не убирает причину. Сначала диагностика, потом движок.
- Внутренний поиск — это SEO-фактор, а не только функция интерфейса. Тысячи страниц выдачи потребляют краулинговый бюджет и конкурируют с категориями. Их место — за
noindex, а не в индексе внешних поисковиков.
Часть 11. Поиск починили. Трафика больше не стало
Здесь начинается вторая половина этой истории, и она куда менее очевидная, чем первая.
Через некоторое время после того, как поиск на сайте клиента заработал как надо, встал следующий вопрос: раз с сайтом теперь всё в порядке технически, почему рост позиций в поиске не превращается в рост заявок? Клиент выгрузил данные из Яндекс.Вебмастера за три месяца — и цифры не складывались в понятную картину.
Показы по сайту росли: +10,3% за период. Клики при этом падали: −10,8%. Общий CTR планомерно снижался — с 2,21% в конце июня до 1,1–1,2% в начале сентября. Формально сайт «рос» в глазах Яндекса. По факту — терял людей.
Дальше — история о том, как легко в такой ситуации продиагностировать не ту проблему, и что помогло этого не сделать.
Часть 12. Первая гипотеза: битая страница категории
Флагманский товар магазина — тротуарная плитка. Больше всего показов из всех категорий сайта, и при этом катастрофический CTR: 5028 показов, 5 кликов, 0,1%. Естественная первая гипотеза — со страницей что-то не так технически.
Проверка боевого URL напрямую вернула 404. Логичный вывод — страница сломана, Яндекс показывает мёртвую ссылку, отсюда и нулевые клики.
Вывод оказался неверным, и вот почему это важно. Повторная проверка тем же запросом, но от имени YandexBot в User-Agent, вернула честные 200 OK. Прямой curl с самого сервера — тоже 200. 404 отдавался только внешним запросам с определённых источников — похоже на антибот-фильтр или гео-блокировку на уровне хостинга, не имеющую отношения к реальной доступности страницы для пользователей и поискового робота.
Практический вывод из этого шага: если диагностируете низкий CTR, никогда не делайте вывод о доступности страницы по одному внешнему запросу. Проверяйте минимум тремя способами: с сервера напрямую, с внешнего IP, и — обязательно — с подменой User-Agent на официального поискового бота. Разные результаты этих трёх проверок — это и есть диагноз, а не сам факт одной неудачной проверки.
Часть 13. Вторая гипотеза: плохая позиция
Ладно, страница жива. Тогда, возможно, дело в позиции — CTR в 0,1% совместим с положением где-то на 30–50-й позиции выдачи, где кликов физически почти не бывает ни у кого.
Проверка реальной позиции по данным того же Вебмастера показала другое: средняя позиция страницы по этому запросу — 8–11, то есть верх второй-низ первой страницы выдачи. Для такой позиции 0,1% CTR — это аномалия, а не норма: на топ-10 в среднем CTR должен измеряться единицами процентов, не долями.
Живой скриншот выдачи по гео-запросу подтвердил: страница на 3-й позиции органики, с рейтингом 5,0★ и 50 отзывами — визуально лучше конкурентов на тех же позициях. И тем не менее почти без кликов.
Часть 14. Поворотный момент: сверка с рыночной медианой
На этом месте естественно было бы удариться в частности — переписывать сниппет, добавлять микроразметку Product, разбираться с рейтингом. Прежде чем тратить на это время, стоило сделать одну простую вещь, которую легко пропустить: сравнить показатель не с абстрактным «хорошо было бы больше», а с реальной медианой по нише у прямых конкурентов.
Отчёт Яндекс.Маркета по средним показателям рынка для подкатегории «Тротуарная плитка, бордюр» дал неожиданный результат:
- CTR сайта в этой подкатегории: 1,00%
- Медиана рынка по той же подкатегории: 0,19%
Сайт клиента опережал медианного конкурента в этой нише в пять раз. То, что казалось главной проблемой всего анализа, на поверку оказалось одной из сильных сторон сайта — просто исходные цифры (показы/клики по широким общероссийским запросам без гео-фильтра) создавали обманчивую картину, потому что включали массу нерелевантных показов пользователям за пределами региона обслуживания.
Настоящая проблема нашлась в другом месте того же отчёта: категория «Инструменты» — 208 показов, 1 клик, CTR 0,48% против медианы рынка 2,50%. Отставание в пять раз, и, в отличие от тротуарной плитки, статистически значимое — это не шум на малых числах, а системная слабость в конкретной товарной группе.
Практический вывод, который стоит вынести за пределы этого конкретного случая: низкий CTR по общим запросам без региональной привязки — это не диагноз, а сырые данные. Прежде чем чинить конкретную страницу, сравните её показатели с медианой по нише у конкурентов на той же площадке (в случае Яндекса — отчёт «Средние показатели по рынку» в кабинете Яндекс.Маркета). Без этого шага легко потратить бюджет на то, что и так работает, и пропустить то, что реально проседает.
Часть 15. Побочная находка: карусель товаров и сломанный YML
В процессе разбора всплыла ещё одна деталь, объясняющая часть картины. В верхней, самой заметной зоне поисковой выдачи — карусели «Товары» с ценами и кнопкой «В магазин», которая показывается ещё до органических результатов, — сайт клиента отсутствовал вовсе. Конкуренты в этой зоне были представлены, причём один — дважды.
Причина оказалась чисто технической: YML-фид, который питает эту карусель через Яндекс.Маркет, был настроен до переезда сайта на новый VPS и не был переподключён после миграции. Сам файл генерировался корректно — структура валидная, цены и остатки актуальные, — но кабинет Яндекс.Маркета его больше не забирал, потому что после смены сервера никто не переуказал актуальный URL фида в личном кабинете.
Это отдельный класс проблем: технически всё работает, но связь между системами обрывается на организационном шаге, который легко забыть при миграции инфраструктуры. Если у вас недавно менялся хостинг или домен фида — карусель товаров стоит проверить в первую очередь, это одна из самых частых причин внезапного исчезновения из самой заметной зоны выдачи.
Часть 16. Что осталось после разбора
Собранная картина оказалась куда более спокойной, чем выглядела на старте:
- Флагманская категория (тротуарная плитка) работает лучше рынка — трогать не нужно;
- Категория «Инструменты» — реальная точка роста, с понятным числовым разрывом от медианы;
- Карусель товаров — техническая недоработка после переезда, чинится переподключением фида, без переписывания контента;
- Общее падение CTR по сайту — следствие суммы этих факторов, а не одной большой поломки.
Если бы диагностика остановилась на первой правдоподобной гипотезе — «страница тротуарной плитки битая» или «сайт плохо ранжируется по флагманскому товару», — правки ушли бы туда, где всё и так было в порядке, а «Инструменты» и сломанный фид остались бы незамеченными. Сверка с рыночной медианой заняла один отчёт и пятнадцать минут — и полностью изменила приоритеты работы.
FAQ
Почему поиск в Битриксе не находит товары, хотя переиндексация прошла?
Самая частая причина — товар есть в индексе, но до него не доходит выдача: лимит позиций занят другими документами. В нашем кейсе так вели себя документы служебного инфоблока регионов — около 193 позиций из 200. Проверьте, какие инфоблоки вообще наполняют индекс (скрипт из Части 1), и отключите индексацию лишних. Вторая по частоте причина — протухший кэш компонента: страница отдаёт старую выдачу, а вы ищете причину в индексе.
Как сделать поиск по артикулу в Битриксе?
Три шага, по порядку. Первое — включить у свойства «Артикул» флаг «Участвует в поиске» (в БД SEARCHABLE = 'Y') и убедиться, что он реально включён, а не «когда-то был». Второе — выполнить полную переиндексацию: частичная новое поле для старых элементов не подхватит, и это самая частая причина, почему «включил, а не ищет». Третье — для артикулов со спецсимволами (дефис, слэш, подчёркивание) добавить точный поиск по свойству в обход полнотекстового индекса: морфология режет такие коды на куски, и штатный поиск по ним принципиально ненадёжен.
Почему пустой arrFILTER не исключает инфоблок из поиска?
Потому что пустой массив компонент читает как «область поиска не ограничена», а не как «искать негде». Логика «исключу регионы, передав пустой фильтр» здесь не работает — проверено экспериментально. Исключать инфоблоки нужно другим способом: снять у них индексацию и вычистить уже проиндексированные документы. А область поиска ограничивать только белым списком — явно перечислять, где искать.
Почему страница поиска отдаёт старую выдачу?
Потому что результаты поиска лежат в кэше компонента, а переиндексация для Битрикса не является событием изменения контента: она меняет содержимое поискового модуля, а не страницы и не элементы инфоблоков. Штатные механизмы сброса кэша этого не видят. Либо не кэшируйте поисковый компонент вообще, либо держите короткий TTL с явной инвалидацией при переиндексации. И не проверяйте гипотезы на протухшем кэше — сначала сброс, потом выводы.
Чем заменить штатный поиск — Sphinx, Manticore или OpenSearch?
Если выбираете впервые — Manticore Search. Это живой форк Sphinx, совместимый с SphinxAPI, поэтому модуль поиска Битрикса видит его как обычный Sphinx, а вы получаете реалтайм-индексы и готовый Docker-образ. Sphinx классический остаётся рабочим вариантом, но развивается медленно. OpenSearch — когда нужны фасеты, агрегации и автодополнение и вы готовы платить за это ресурсами сервера: JVM и от 4 ГБ памяти на однонодовую установку. Но сначала пройдите чек-лист из Части 6 — внешний движок не лечит неразобранную причину.
Как настроить поиск в Битриксе, чтобы он искал только по каталогу?
Двумя слоями, и оба обязательны. На уровне компонента — белый список вместо чёрного: явно указываете инфоблок каталога и его ID, а не пытаетесь исключить лишнее. На уровне индекса — отключаете индексацию всех служебных инфоблоков и чистите то, что уже попало в b_search_content. Только фильтра недостаточно: общесайтовый поиск в шапке пойдёт по другому компоненту и покажет лишнее.
Должны ли страницы поиска индексироваться?
Страницы с параметром запроса — нет. Это динамические выборки, которые плодят тысячи почти одинаковых адресов, конкурируют с категориями и расходуют краулинговый бюджет. Пустую страницу поиска без параметров при этом стоит оставить открытой: она работает как точка входа для пользователя. И помните про несовместимость: если закрыть адрес через Disallow, робот не увидит на нём noindex — выберите что-то одно.
Сколько времени занимает такая диагностика?
В нашем случае от первой жалобы до внедрения фикса — около двух рабочих дней, из которых большая часть ушла на две ложные версии: «битый индекс» и «дело в сортировке». Если идти сразу по чек-листу из Части 6, диагностика занимает несколько часов. Основное время съедает не поиск причины, а чистка индекса и переиндексация нормального объёма.
Почему после починки поиска трафик не вырос?
Потому что поиск и SEO-трафик — две разные подсистемы, которые чинятся отдельно. В нашем кейсе после починки поиска показы выросли на 10,3%, а клики упали на 10,8% — и это не противоречие, а разные слои проблемы. Низкий CTR по общим запросам без гео-привязки часто оказывается не слабостью, а особенностью: сверка с медианой рынка показала, что флагманская категория работает в пять раз лучше конкурентов. Реальная точка роста нашлась в другой товарной группе — но только после того, как мы перестали чинить то, что и так работало.
Как понять, что категория действительно проседает, а не просто выглядит слабо?
Сравните её CTR с медианой по нише у конкурентов — в Яндексе это отчёт «Средние показатели по рынку» в кабинете Яндекс.Маркета. Абстрактное «хорошо было бы больше» здесь не работает: на малых числах показов разница в один клик даёт видимость катастрофы. В нашем случае у «Тротуарной плитки» было 5028 показов и 5 кликов (0,1%), что выглядело провалом, — а медиана рынка в этой подкатегории оказалась 0,19%. Настоящий провал был в «Инструментах»: 0,48% против 2,50% при 208 показах. Разница в пять раз, и она статистически значима.
Вывод
Самое неприятное в этой истории не то, что поиск по каталогу не находил товары. А то, что система при этом сообщала, что всё в порядке. Общесайтовый поиск работал. Индекс был цел. Страница отдавала 200. Ни одна метрика не падала — реклама шла, клики были, корзины наполнялись, просто чуть медленнее обычного.
Поломка состояла из трёх независимых частей, и ни одна из них не была «поиском» в том смысле, в котором о нём думают:
- Кэш компонента в Redis, который не инвалидировался после переиндексации, потому что для Битрикса переиндексация не является изменением контента.
- Служебный инфоблок «Регионы», чьи документы заняли около 193 позиций из 200 в лимите выдачи — и физически не пустили туда товары. Не отсортировали ниже. Не пустили вообще.
- Параметр
arrFILTER, у которого пустой массив означает «везде», а не «нигде», — из-за чего самый логичный контрход не сработал.
Отсюда вывод, который стоит вынести за пределы Битрикса. Поиск в интернет-магазине — это не функция, а витрина. Покупатель, который не нашёл товар, не пишет в поддержку «у вас поиск сломан». Он уходит к конкуренту и, скорее всего, не возвращается. Вы не увидите этого ни в отчёте об ошибках, ни в метрике отказов — потому что технически всё работало.
Чек-лист, который стоит пройти на своём сайте прямо сейчас
- Посчитать, что лежит в индексе по инфоблокам — скрипт на 12 строк из Части 1. Если в списке есть что-то, кроме каталога и полезных разделов, — вы уже знаете первое место, где теряете выдачу.
- Ввести в поиск название товара из середины каталога. Не популярного, а обычного. Именно на таких позициях вылезает дефект с лимитом.
- Ввести артикул — с дефисом, если он в дефисе. Если не нашло, проверьте
SEARCHABLEу свойства и вспомните, что морфология режет спецсимволы. - Сравнить выдачу со счётчиком документов. Не бьются — вы смотрите на кэш.
- Проверить
/search?q=и/catalog?sort=в панели вебмастера. Если они в индексе — часть краулингового бюджета уходит не туда.
Пять проверок, полчаса времени. В нашем случае они бы сэкономили два дня работы и, что важнее, несколько месяцев потерянных продаж.
Но и это ещё не конец истории. Когда поиск заработал, выяснилось, что техническая исправность сайта и рост трафика — не одно и то же. Показы выросли, клики упали, и потребовался отдельный разбор, чтобы понять: флагманская категория работает лучше рынка, а реальная точка роста — в другой товарной группе и в сломанном YML-фиде, который перестал забираться после переезда на новый сервер. Урок второй половины кейса простой: после починки инфраструктуры не спешите считать, что рост случится сам. Сверьте показатели с медианой по нише, проверьте, не отвалились ли внешние интеграции при миграции, и только потом решайте, что чинить дальше.
Материал основан на реальном проекте: интернет-магазин стройматериалов на 1С-Битрикс с региональностью Аспро, каталогом в двух инфоблоках и протухшим кэшем компонента в Redis. Все цифры — с момента замера, все команды — из боевой работы. Если у вас похожая ситуация и поиск «вроде работает, но не находит», начните с диагностического скрипта в Части 1 — он отвечает на половину вопросов за один запуск.
Автор: Дмитрий И. — SEO-специалист.
Покажу 10 ошибок, из-за которых ваш сайт не растёт
Экспресс-аудит за 1 рабочий день — техника, видимость против конкурентов и GEO.
Получить экспресс-аудит

