Создание поиска по товарам в каталоге: от простого запроса до обработки результатов
Реализация поиска в каталоге на PHP
Полнотекстовый поиск MySQL (FULLTEXT) - эффективное решение для средних каталогов
Основной рекомендуемый способ организации поиска в каталоге товаров или статей - использование полнотекстового индекса MySQL с оператором MATCH AGAINST. Этот подход позволяет выполнять быстрый поиск по текстовым полям, учитывая релевантность результатов, игнорируя стоп-слова и поддерживая естественный язык.
Как реализовать полнотекстовый поиск в MySQL для каталога PHP?
Для начала необходимо создать полнотекстовый индекс на колонке (или нескольких колонках) таблицы. Например, таблица products с полями name и description:
CREATE TABLE products (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255),
description TEXT,
price DECIMAL(10,2),
FULLTEXT (name, description)
) ENGINE=InnoDB;Search tags php tag (поиск по тегам в php)
Далее в PHP-скрипте выполняется запрос с подготовленным выражением:
$search = $_GET['q'] ?? '';
$stmt = $pdo->prepare("SELECT *, MATCH(name, description) AGAINST(:query IN NATURAL LANGUAGE MODE) AS relevance
FROM products
WHERE MATCH(name, description) AGAINST(:query IN NATURAL LANGUAGE MODE)
ORDER BY relevance DESC
LIMIT 20");
$stmt->execute(['query' => $search]);
$results = $stmt->fetchAll();Search topic php (поиск по теме в php)
При выводе результатов можно подсветить совпадения функцией highlight (реализация приведена ниже).
Типичная ошибка: забыть создать индекс или использовать неправильный движок (MyISAM по умолчанию поддерживает FULLTEXT, InnoDB с MySQL 5.6+ тоже). Если индекс отсутствует, запрос будет выполняться как обычное сканирование таблицы.
Проблема с кириллицей: для корректной работы с русским языком необходимо установить правильную кодировку сортировки (например, utf8mb4_unicode_ci) и задать минимальную длину слова (по умолчанию 4 символа). Короткие слова игнорируются. Для изменения минимальной длины нужно настроить параметр ft_min_word_len в MySQL.
Вариант 1: Оператор LIKE для простого поиска
Как выполнить поиск по частичному совпадению с помощью LIKE в PHP?
Оператор LIKE подходит для маленьких каталогов (до нескольких тысяч записей) и когда не требуется высокая производительность. Пример:
$search = $_GET['q'] ?? '';
$stmt = $pdo->prepare("SELECT * FROM products WHERE name LIKE :query OR description LIKE :query2");
$likeQuery = '%' . $search . '%';
$stmt->execute(['query' => $likeQuery, 'query2' => $likeQuery]);Search type php id type (тип поиска по id в php)
Недостатки: медленный на больших таблицах, не учитывает релевантность, не работает со стоп-словами, подвержен SQL-инъекциям при неправильном использовании (но подготовленные запросы решают эту проблему).
Результат: список товаров, где в названии или описании встречается искомая строка.
Search php view (вид поиска в php)
Вариант 2: Регулярные выражения REGEXP
Когда и как применять REGEXP для гибкого поиска в MySQL?
REGEXP позволяет искать по сложным шаблонам, например, товары, название которых начинается с определенной буквы или содержит цифры. Пример поиска названий, начинающихся на "Ноут":
$stmt = $pdo->prepare("SELECT * FROM products WHERE name REGEXP :pattern");
$stmt->execute(['pattern' => '^Ноут']);Catalog php search (поиск в каталоге php)
Но REGEXP не использует индексы, поэтому подходит только для маленьких таблиц или фильтрации уже выбранного набора.
Вариант 3: Самостоятельная разбивка запроса и несколько условий
Как реализовать поиск по каждому слову запроса отдельно на стороне PHP?
Пользователь может ввести несколько слов, и нужно найти товары, содержащие все эти слова (или любое из них). Пример для случая "все слова":
$search = $_GET['q'] ?? '';
$words = preg_split('/\s+/', $search);
$conditions = [];
$params = [];
foreach ($words as $i => $word) {
$conditions[] = "(name LIKE :word{$i} OR description LIKE :desc{$i})";
$params["word{$i}"] = '%' . $word . '%';
$params["desc{$i}"] = '%' . $word . '%';
}
$where = implode(' AND ', $conditions);
$stmt = $pdo->prepare("SELECT * FROM products WHERE {$where}");
$stmt->execute($params);Index php act search (действие поиска в php)
Этот вариант гибкий, но запрос может стать длинным, а производительность падает с ростом числа слов.
Вариант 4: Интеграция внешнего поискового движка (Elasticsearch, Sphinx)
В каких случаях стоит использовать Elasticsearch вместо встроенного поиска MySQL?
Для очень больших каталогов (сотни тысяч и миллионы записей) или при необходимости сложной фильтрации, фасетного поиска, автодополнения - подключается отдельный сервис. Пример отправки запроса через HTTP-клиент Guzzle:
use GuzzleHttp\Client;
$client = new Client(['base_uri' => 'http://localhost:9200']);
$response = $client->post('/products/_search', [
'json' => [
'query' => [
'multi_match' => [
'query' => $search,
'fields' => ['name', 'description']
]
]
]
]);
$body = json_decode($response->getBody(), true);
$results = array_map(function($hit) { return $hit['_source']; }, $body['hits']['hits']);
Такой подход требует настройки индексации данных из MySQL в Elasticsearch (Logstash, Beats или вручную).
Общие проблемы и способы их решения
SQL-инъекции: всегда использовать подготовленные запросы или PDO. Никогда не конкатенировать пользовательский ввод напрямую.
Производительность: для таблиц более 100 тысяч записей LIKE и REGEXP становятся неприемлемо медленными. Необходимо либо использовать FULLTEXT индекс, либо внешние инструменты. Также важно иметь правильные индексы на поля, участвующие в поиске.
Релевантность: в FULLTEXT поиске по умолчанию используется бинарный режим, учитываются только слова длиннее 3-4 символов. Для русского языка может потребоваться настройка стемминга или использование внешнего анализатора.
Пагинация: при больших результатах используйте LIMIT и OFFSET, но для полнотекстового поиска лучше применять курсорную пагинацию (по последнему ID), если важна производительность на больших страницах.
Кодировка: убедитесь, что таблицы и соединение используют utf8mb4, иначе кириллица может не распознаваться.
Расширенные примеры реализации поиска
Пример 1. Полнотекстовый поиск с пагинацией и подсчетом общего количества
PHP-код с использованием PDO и передачей параметров LIMIT, OFFSET:
$page = max(1, (int)($_GET['page'] ?? 1));
$perPage = 10;
$offset = ($page - 1) * $perPage;
$search = $_GET['q'] ?? '';
// Подсчет результатов
$countStmt = $pdo->prepare("SELECT COUNT(*) FROM products WHERE MATCH(name, description) AGAINST(:query IN NATURAL LANGUAGE MODE)");
$countStmt->execute(['query' => $search]);
$total = $countStmt->fetchColumn();
$totalPages = ceil($total / $perPage);
// Выборка с пагинацией
$stmt = $pdo->prepare("SELECT *, MATCH(name, description) AGAINST(:query IN NATURAL LANGUAGE MODE) AS rel
FROM products
WHERE MATCH(name, description) AGAINST(:query IN NATURAL LANGUAGE MODE)
ORDER BY rel DESC
LIMIT :limit OFFSET :offset");
$stmt->bindValue(':query', $search, PDO::PARAM_STR);
$stmt->bindValue(':limit', $perPage, PDO::PARAM_INT);
$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
$stmt->execute();
$results = $stmt->fetchAll();
// Вывод
foreach ($results as $row) {
echo "" . highlight($row['name'], $search) . " - " . $row['price'] . " руб.
";
}
echo "Страница $page из $totalPages";
Результат (вывод):
Товар: Ноутбук HP - 45000 руб. Товар: Ноутбук Lenovo - 38000 руб. Страница 1 из 3
Пример 2. Функция подсветки совпадений
function highlight($text, $search) {
$words = preg_split('/\s+/', $search);
foreach ($words as $word) {
$text = preg_replace('/(' . preg_quote($word, '/') . ')/iu', '$1', $text);
}
return $text;
}
Использование: echo highlight($row['name'], $search);
Пример 3. Автодополнение (автокомплит) с помощью AJAX и FULLTEXT
PHP endpoint (autocomplete.php):
$search = $_GET['term'] ?? '';
$stmt = $pdo->prepare("SELECT DISTINCT name FROM products WHERE MATCH(name) AGAINST(:query IN BOOLEAN MODE) LIMIT 10");
$stmt->execute(['query' => $search . '*']);
$names = $stmt->fetchAll(PDO::FETCH_COLUMN);
echo json_encode($names);
JavaScript обрабатывает ответ и выводит подсказки. Результат:
["Ноутбук HP", "Ноутбук Lenovo", "Ноутбук Acer"]
Пример 4. Использование Sphinx Search для больших каталогов
Конфигурация Sphinx (sphinx.conf) может выглядеть так:
source products_source
{
type = mysql
sql_host = localhost
sql_user = user
sql_pass = pass
sql_db = catalog
sql_query = SELECT id, name, description FROM products
sql_attr_uint = id
}
index products_index
{
source = products_source
path = /var/data/sphinx/products
charset_table = 0..9, A..Z->a..z, a..z, U+410..U+44F->U+430..U+44F, U+430..U+44F
min_word_len = 2
}
В PHP через API Sphinx (используя pecl/sphinx):
$s = new SphinxClient();
$s->SetServer('localhost', 9312);
$s->SetMatchMode(SPH_MATCH_EXTENDED2);
$result = $s->Query($search, 'products_index');
if ($result && $result['matches']) {
$ids = array_keys($result['matches']);
$stmt = $pdo->prepare("SELECT * FROM products WHERE id IN (" . implode(',', $ids) . ")");
$stmt->execute();
$products = $stmt->fetchAll();
}
Это позволяет обрабатывать миллионы записей с высокой скоростью.