Удобная фильтрация — основа успешного интернет-магазина. Пользователи должны быстро находить нужные товары среди тысяч позиций. В MODX для этого есть два мощных инструмента: mFilter2 для фильтрации по характеристикам и mSearch2 для полнотекстового поиска. Расскажу, как настроить их для максимальной эффективности.
Обзор решений для фильтрации
mFilter2 — фильтрация по характеристикам
Возможности:
- Фильтры по TV-параметрам и опциям miniShop2
- Поддержка числовых диапазонов (цена, размер)
- Множественный выбор значений
- AJAX обновление без перезагрузки страницы
- Интеграция с pdoPage для пагинации
mSearch2 — полнотекстовый поиск
Возможности:
- Поиск по названию, описанию, артикулу
- Автодополнение запросов
- Поиск по синонимам и морфологии
- Интеграция с Elasticsearch для больших каталогов
- Поисковая аналитика
Зачем использовать оба
Связка mFilter2 + mSearch2 покрывает все потребности пользователей:
- mFilter2 — для уточнения по параметрам («красные платья размера M»)
- mSearch2 — для поиска по названию («iPhone 15 Pro»)
Установка и базовая настройка
Установка компонентов
# В менеджере MODX через Управление пакетами:
# 1. miniShop2 (если ещё не установлен)
# 2. mSearch2
# 3. mFilter2
# 4. pdoTools (зависимость)
Настройка индекса mSearch2
// Системные настройки mSearch2
'mse2_index_fields' => 'pagetitle,description,content,article,vendor.name'
'mse2_exclude_fields' => 'password,email'
'mse2_search_class' => 'msProduct'
Запуск индексации:
# В терминале сервера
php manager/index.php --action=index --class=mSearch2\\
Или через планировщик cron:
# Ежедневная переиндексация в 2:00
0 2 * * * /usr/bin/php /path/to/modx/manager/index.php --action=index --class=mSearch2
Создание каталога с фильтрацией
Основной шаблон каталога
<!DOCTYPE html>
<html>
<head>
<title>[[*pagetitle]] - каталог товаров</title>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
</head>
<body>
<div class="catalog-container">
<!-- Поиск -->
<div class="search-block">
[[!mSearch2?
&tpl=`tpl.mSearch2.row`
&limit=`12`
&outputSeparator=``
&ajaxMode=`scroll`
&showResults=`0`
]]
</div>
<!-- Фильтры -->
<div class="filters-sidebar">
[[!mFilter2?
&class=`msProduct`
&parents=`[[*id]]`
&tpl=`tpl.msProducts.row`
&limit=`12`
&ajaxMode=`default`
&filters=`vendor,price,color,size,material`
&suggestionsRadio=`vendor`
&suggestionsCheckbox=`color,size,material`
&suggestionsSlider=`price`
]]
</div>
<!-- Результаты -->
<div class="products-grid">
<div id="mse2_results">
<!-- Товары загружаются через AJAX -->
</div>
</div>
</div>
</body>
</html>
Чанки для фильтров
Фильтр по производителю (tpl.mFilter2.outer.vendor):
<div class="filter-group">
<h3>Производитель</h3>
<div class="filter-options">
[[+rows]]
</div>
</div>
Опции производителя (tpl.mFilter2.row.vendor):
<label class="filter-option">
<input type="radio" name="vendor" value="[[+value]]" [[+selected]]>
<span class="filter-label">[[+title]] ([[+num]])</span>
</label>
Фильтр-слайдер для цены (tpl.mFilter2.outer.price):
<div class="filter-group">
<h3>Цена, руб.</h3>
<div class="price-slider">
<div id="price-range"
data-min="[[+min]]"
data-max="[[+max]]"
data-value-min="[[+value_min]]"
data-value-max="[[+value_max]]">
</div>
<div class="price-inputs">
<input type="number" name="price_min" value="[[+value_min]]" placeholder="от">
<span>—</span>
<input type="number" name="price_max" value="[[+value_max]]" placeholder="до">
</div>
</div>
</div>
Множественный выбор для цветов (tpl.mFilter2.outer.color):
<div class="filter-group">
<h3>Цвет</h3>
<div class="filter-checkboxes">
[[+rows]]
</div>
</div>
Опции цветов (tpl.mFilter2.row.color):
<label class="filter-checkbox">
<input type="checkbox" name="color[]" value="[[+value]]" [[+selected]]>
<span class="color-swatch" style="background-color: [[+value]];"></span>
<span class="filter-label">[[+title]] ([[+num]])</span>
</label>
Настройка поиска mSearch2
Форма поиска с автодополнением
<!-- Чанк: tpl.mSearch2.form -->
<form class="search-form" id="mse2_form">
<div class="search-input-wrapper">
<input type="text"
name="query"
placeholder="Поиск товаров..."
value="[[+mse2_query]]"
autocomplete="off"
id="mse2_query">
<div id="mse2_suggestions" class="search-suggestions">
<!-- Автодополнение -->
</div>
</div>
<button type="submit" class="search-btn">
Найти
</button>
<div class="search-filters">
<select name="category" id="mse2_category">
<option value="">Все категории</option>
[[pdoMenu?
&parents=`[[mse2_get_parents]]`
&level=`1`
&tpl=`@INLINE <option value="[[+id]]" [[+mse2_selected]]>[[+menutitle]]</option>`
]]
</select>
<select name="sort" id="mse2_sort">
<option value="publishedon|desc">По новизне</option>
<option value="price|asc">По цене ↑</option>
<option value="price|desc">По цене ↓</option>
<option value="rank|desc">По релевантности</option>
</select>
</div>
</form>
Шаблон результатов поиска
<!-- Чанк: tpl.mSearch2.row -->
<div class="product-card" data-id="[[+id]]">
<div class="product-image">
<a href="[[+uri]]">
[[+thumb:is=``:then=`
<img src="[[+image]]" alt="[[+pagetitle]]" loading="lazy">
`:else=`
[[msGallery? &id=`[[+id]]` &tpl=`tpl.msGallery.thumb` &limit=`1`]]
`]]
</a>
[[+old_price:gt=`[[+price]]`:then=`
<div class="product-badge sale">Скидка</div>
`]]
</div>
<div class="product-info">
<h3 class="product-title">
<a href="[[+uri]]">[[+pagetitle]]</a>
</h3>
<div class="product-vendor">[[+vendor.name]]</div>
<div class="product-description">
[[+description:limit=`150`]]
</div>
<div class="product-properties">
[[+color:notempty=`<span class="prop">Цвет: [[+color]]</span>`]]
[[+size:notempty=`<span class="prop">Размер: [[+size]]</span>`]]
</div>
<div class="product-price">
[[+old_price:gt=`[[+price]]`:then=`
<span class="price-old">[[+old_price]] ₽</span>
`]]
<span class="price-current">[[+price]] ₽</span>
</div>
<div class="product-actions">
[[+count:gt=`0`:then=`
<button class="btn-cart" data-id="[[+id]]">В корзину</button>
`:else=`
<button class="btn-notify" data-id="[[+id]]">Сообщить о поступлении</button>
`]]
<button class="btn-favorite" data-id="[[+id]]">♡</button>
</div>
</div>
</div>
Продвинутая настройка фильтров
Создание кастомного фильтра по брендам
<?php
// Сниппет: CustomBrandFilter
$brands = $modx->getCollection('msVendor', ['active' => 1]);
$currentBrand = $_GET['vendor'] ?? '';
$output = '<div class="brand-filter">';
$output .= '<h3>Бренды</h3>';
foreach ($brands as $brand) {
$count = $modx->getCount('msProduct', [
'vendor' => $brand->get('id'),
'published' => 1,
'deleted' => 0
]);
if ($count > 0) {
$selected = ($currentBrand == $brand->get('id')) ? 'checked' : '';
$logo = $brand->get('logo') ? '<img src="' . $brand->get('logo') . '" alt="' . $brand->get('name') . '">' : '';
$output .= '<label class="brand-option">';
$output .= '<input type="radio" name="vendor" value="' . $brand->get('id') . '" ' . $selected . '>';
$output .= $logo;
$output .= '<span>' . $brand->get('name') . ' (' . $count . ')</span>';
$output .= '</label>';
}
}
$output .= '</div>';
return $output;
Фильтр по наличию и доставке
<?php
// Сниппет: AvailabilityFilter
$filters = [
'in_stock' => 'В наличии',
'quick_delivery' => 'Быстрая доставка',
'free_delivery' => 'Бесплатная доставка'
];
$output = '<div class="availability-filter">';
$output .= '<h3>Наличие и доставка</h3>';
foreach ($filters as $key => $label) {
$checked = !empty($_GET[$key]) ? 'checked' : '';
$output .= '<label class="availability-option">';
$output .= '<input type="checkbox" name="' . $key . '" value="1" ' . $checked . '>';
$output .= '<span>' . $label . '</span>';
$output .= '</label>';
}
$output .= '</div>';
return $output;
Интеграция с рейтингами
<?php
// Модификация mFilter2 для учёта рейтингов
// В файле core/components/mfilter2/processors/web/filter.class.php
public function addRatingFilter($c, $filter_key) {
$rating = $this->getProperty($filter_key);
if (empty($rating)) return $c;
// Подключаем таблицу отзывов
$c->leftJoin('TicketComment', 'Comments', [
'Comments.parent = msProduct.id',
'Comments.published' => 1
]);
// Группируем и фильтруем по среднему рейтингу
$c->select('AVG(Comments.rating) as avg_rating');
$c->groupby('msProduct.id');
$c->having('avg_rating >= ' . (float)$rating);
return $c;
}
JavaScript для улучшения UX
Автоматическое обновление фильтров
// assets/js/catalog-filters.js
(function($) {
'use strict';
var CatalogFilters = {
init: function() {
this.bindEvents();
this.initPriceSlider();
this.initSearch();
},
bindEvents: function() {
// Автоматическая отправка при изменении фильтров
$(document).on('change', '.filter-option input', function() {
CatalogFilters.applyFilters();
});
// Дебаунс для текстовых полей
$(document).on('input', '.price-inputs input',
CatalogFilters.debounce(function() {
CatalogFilters.applyFilters();
}, 500)
);
// Очистка фильтров
$(document).on('click', '.clear-filters', function() {
CatalogFilters.clearFilters();
});
},
applyFilters: function() {
var data = $('#mse2_form, .filters-form').serialize();
// Показываем лоадер
$('.products-grid').addClass('loading');
$.post('/assets/connectors/mfilter2/connector.php', data)
.done(function(response) {
if (response.success) {
$('.products-grid').html(response.data.output);
CatalogFilters.updateUrl(data);
CatalogFilters.updateFilterCounts(response.data.filters);
}
})
.always(function() {
$('.products-grid').removeClass('loading');
});
},
initPriceSlider: function() {
var $slider = $('#price-range');
if (!$slider.length) return;
var min = parseInt($slider.data('min'));
var max = parseInt($slider.data('max'));
var valueMin = parseInt($slider.data('value-min')) || min;
var valueMax = parseInt($slider.data('value-max')) || max;
// Используем noUiSlider для красивого слайдера
if (typeof noUiSlider !== 'undefined') {
noUiSlider.create($slider[0], {
start: [valueMin, valueMax],
connect: true,
range: {
'min': min,
'max': max
},
format: {
to: function(value) {
return Math.round(value);
},
from: function(value) {
return Number(value);
}
}
});
$slider[0].noUiSlider.on('change', function(values) {
$('input[name="price_min"]').val(values[0]);
$('input[name="price_max"]').val(values[1]);
CatalogFilters.applyFilters();
});
}
},
initSearch: function() {
var $input = $('#mse2_query');
var $suggestions = $('#mse2_suggestions');
$input.on('input', CatalogFilters.debounce(function() {
var query = $input.val().trim();
if (query.length >= 2) {
$.get('/assets/connectors/msearch2/connector.php', {
action: 'suggest',
query: query,
limit: 8
})
.done(function(response) {
if (response.success && response.data.length) {
var html = '';
response.data.forEach(function(item) {
html += '<div class="suggestion-item" data-query="' + item.query + '">';
html += item.title;
html += '</div>';
});
$suggestions.html(html).show();
} else {
$suggestions.hide();
}
});
} else {
$suggestions.hide();
}
}, 300));
// Выбор из автодополнения
$(document).on('click', '.suggestion-item', function() {
var query = $(this).data('query');
$input.val(query);
$suggestions.hide();
$('#mse2_form').submit();
});
// Скрытие подсказок при клике вне поиска
$(document).on('click', function(e) {
if (!$(e.target).closest('.search-input-wrapper').length) {
$suggestions.hide();
}
});
},
updateUrl: function(data) {
// Обновляем URL без перезагрузки страницы
var params = new URLSearchParams(data);
var newUrl = window.location.pathname + '?' + params.toString();
history.replaceState({}, '', newUrl);
},
updateFilterCounts: function(filters) {
// Обновляем количество товаров в фильтрах
Object.keys(filters).forEach(function(key) {
Object.keys(filters[key]).forEach(function(value) {
var $option = $('.filter-option input[name="' + key + '"][value="' + value + '"]');
var $label = $option.siblings('.filter-label');
if ($label.length) {
var text = $label.text().replace(/\(\d+\)/, '(' + filters[key][value] + ')');
$label.text(text);
}
});
});
},
clearFilters: function() {
$('.filter-option input').prop('checked', false);
$('.price-inputs input').val('');
this.applyFilters();
},
debounce: function(func, wait) {
var timeout;
return function executedFunction() {
var later = function() {
clearTimeout(timeout);
func.apply(this, arguments);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
};
$(document).ready(function() {
CatalogFilters.init();
});
})(jQuery);
Оптимизация производительности
Кеширование фильтров
<?php
// Плагин: FilterCache
// События: OnBeforeManagerPageInit, OnDocFormSave
switch ($modx->event->name) {
case 'OnDocFormSave':
$resource = $modx->event->params['resource'];
// Очищаем кеш фильтров при изменении товара
if ($resource instanceof msProduct) {
$modx->cacheManager->delete('filter_options_' . $resource->get('parent'));
$modx->cacheManager->delete('mse2_index');
}
break;
}
// В сниппете фильтра используем кеш
$cacheKey = 'filter_options_' . $modx->resource->get('id');
$options = $modx->cacheManager->get($cacheKey);
if (!$options) {
// Генерируем опции фильтра
$options = generateFilterOptions();
$modx->cacheManager->set($cacheKey, $options, 3600); // 1 час
}
Индексы базы данных
-- Для ускорения фильтрации по опциям
CREATE INDEX idx_ms_product_options_filter ON modx_ms2_product_options (key, value, product_id);
-- Для быстрого поиска
CREATE FULLTEXT INDEX idx_ms_products_search ON modx_ms2_products (pagetitle, description, content);
-- Для фильтра по цене
CREATE INDEX idx_ms_products_price ON modx_ms2_products (price, published, deleted);
-- Для сортировки
CREATE INDEX idx_ms_products_sort ON modx_ms2_products (publishedon, price, pagetitle);
AJAX-пагинация
// Подгрузка товаров при прокрутке
var isLoading = false;
var hasMore = true;
var currentPage = 1;
$(window).scroll(function() {
if (!isLoading && hasMore && $(window).scrollTop() + $(window).height() >= $(document).height() - 1000) {
loadMoreProducts();
}
});
function loadMoreProducts() {
if (isLoading) return;
isLoading = true;
currentPage++;
var data = $('#mse2_form').serialize() + '&page=' + currentPage;
$.post('/assets/connectors/mfilter2/connector.php', data)
.done(function(response) {
if (response.success && response.data.output.trim()) {
$('.products-grid').append(response.data.output);
} else {
hasMore = false;
}
})
.always(function() {
isLoading = false;
});
}
Заключение
Эффективная фильтрация товаров в MODX требует правильной настройки mFilter2 и mSearch2. Ключевые принципы успешной реализации:
- Продумайте структуру фильтров — не перегружайте интерфейс
- Оптимизируйте производительность — используйте кеширование и индексы
- Улучшайте UX — автодополнение, AJAX, сохранение состояния
- Тестируйте на реальных данных — производительность может отличаться
Правильно настроенная фильтрация повышает конверсию интернет-магазина на 20-40%, так как пользователи быстрее находят нужные товары. MODX даёт все инструменты для создания мощной и гибкой системы поиска.
Связанные статьи: Создание каталога товаров на MODX