Организация поиска по товарам: от MySQL до специализированных систем
Поиск по каталогу на PHP: обзор решений
Поиск по каталогу - одна из ключевых функций интернет-магазина или любого справочника. В зависимости от объёма данных, требований к скорости и точности, можно выбрать подходящий инструмент. В этой части рассмотрены несколько подходов с примерами кода на PHP и SQL, а также указаны типичные проблемы и способы их решения.
Как добиться быстрого поиска по каталогу с поддержкой логических операторов, фраз и исключений?
Полнотекстовый поиск MySQL с режимом BOOLEAN MODE
Этот метод использует встроенный полнотекстовый индекс MySQL и позволяет задавать операторы +, -, * (звёздочка для усечения), а также фразы в кавычках. Подходит для большинства каталогов с тысячами записей, не требует установки дополнительного ПО.
Для работы необходимо создать индекс FULLTEXT на полях, по которым будет выполняться поиск (например, name, description). Пример SQL:
ALTER TABLE products ADD FULLTEXT INDEX ft_search (name, description);Catalog php find (поиск по каталогу php)
Запрос на PHP с использованием PDO:
$search = '+ноутбук -acer "игровой"';
$query = "SELECT * FROM products WHERE MATCH(name, description) AGAINST(:search IN BOOLEAN MODE)";
$stmt = $pdo->prepare($query);
$stmt->execute(['search' => $search]);
$results = $stmt->fetchAll();
Типичные проблемы:
- Специальные символы (+, -, *) должны экранироваться при использовании пользовательского ввода. Иначе запрос может быть некорректным или вызвать синтаксическую ошибку.
- Минимальная длина слова для индексации (по умолчанию 4 символа). Для коротких слов (например, "3G") поиск не сработает - нужно изменить параметр ft_min_word_len.
- Стоп-слова (по умолчанию "и", "в", "на") игнорируются. Для русского языка может потребоваться отключить стоп-слова или настроить свой список.
Решение: экранировать ввод через функцию, проверяющую наличие спецсимволов, и добавлять перед ними обратную косую черту. Для коротких слов увеличить ft_min_word_len в my.cnf.
Цель: получить релевантные результаты с возможностью точного управления поиском без внешних сервисов. Используется для типовых каталогов с объёмом до 100 000 записей.
Как сделать простой поиск по одному полю с частичным совпадением?
Поиск с оператором LIKE
Самый простой вариант, который не требует создания индексов. Подходит для поиска по одному полю (например, только по названию) при небольшом числе записей (до нескольких сотен).
$search = '%ноут%';
$query = "SELECT * FROM products WHERE name LIKE :search";
$stmt = $pdo->prepare($query);
$stmt->execute(['search' => $search]);
$results = $stmt->fetchAll();
Проблемы:
- Оператор LIKE не использует индексы, если шаблон начинается с '%' - это приводит к полному сканированию таблицы и медленной работе при больших объёмах.
- Не учитывается релевантность: результаты показываются в произвольном порядке (обычно по первичному ключу).
- Не поддерживается морфология - поиск слова "ноутбуки" не найдёт запись со словом "ноутбук".
Решение: использовать только для фильтрации по префиксу (без '%' в начале) или в комбинации с другими условиями. Для больших каталогов переходить на полнотекстовый поиск.
Цель: быстрый прототип или административный интерфейс для маленькой базы.
Как получить результаты, отсортированные по релевантности, без специального синтаксиса запросов?
Полнотекстовый поиск в естественно-языковом режиме
Этот режим автоматически рассчитывает релевантность на основе частоты терминов в документе. Пользователь может вводить слова без операторов. Индекс FULLTEXT также требуется.
$search = 'ноутбук игровой';
$query = "SELECT *, MATCH(name, description) AGAINST(:search AS NATURAL LANGUAGE MODE) AS relevance FROM products WHERE MATCH(name, description) AGAINST(:search IN NATURAL LANGUAGE MODE) ORDER BY relevance DESC";
$stmt = $pdo->prepare($query);
$stmt->execute(['search' => $search]);
$results = $stmt->fetchAll();
Проблемы:
- Игнорируются стоп-слова (русские предлоги).
- Слова короче минимальной длины не учитываются.
- Все введённые слова считаются обязательными (AND) - если хотя бы одно слово отсутствует, запись не будет выбрана.
- Нельзя исключить слово или искать фразу целиком.
Решение: если нужна гибкость, лучше использовать BOOLEAN MODE. Естественно-языковой режим подходит, когда пользователь вводит одно-два слова и важна сортировка по релевантности.
Цель: простой поиск с сортировкой по релевантности для каталогов, где пользователи обычно вводят 1–3 слова.
Как организовать поиск с высокой производительностью, морфологией и фасетной навигацией?
Интеграция с Elasticsearch
Elasticsearch - полнотекстовая поисковая система на основе Lucene. Позволяет задавать анализаторы для русского языка, поддерживает синонимы, ранжирование, автодополнение. Требуется установка сервера Elasticsearch и библиотеки для PHP (например, elasticsearch/elasticsearch).
Пример индексации товаров:
$client = ClientBuilder::create()->build();
$params = [
'index' => 'catalog',
'id' => $productId,
'body' => [
'name' => $name,
'description' => $description,
'price' => $price
]
];
$response = $client->index($params);
Поисковый запрос:
$search = 'ноутбук для игр';
$params = [
'index' => 'catalog',
'body' => [
'query' => [
'multi_match' => [
'query' => $search,
'fields' => ['name^3', 'description']
]
]
]
];
$response = $client->search($params);
Проблемы:
- Требуется поддерживать отдельный сервер (Java) и синхронизировать данные.
- Сложность настройки анализаторов и маппингов.
- Потребление оперативной памяти.
Решение: использовать Elasticsearch для крупных проектов (миллионы записей) или когда нужны сложные возможности (поиск по близости, синонимы). Для небольших каталогов достаточно MySQL.
Цель: промышленный поиск с морфологией, подсветкой результатов, фасетной фильтрацией и высокой нагрузкой.
Расширенные примеры программного кода
Пример 1: Полнотекстовый поиск BOOLEAN MODE с экранированием и пагинацией
Функция для экранирования специальных символов полнотекстового поиска MySQL:
function escapeFulltext($string) {
$specialChars = ['+', '-', '>', '<', '(', ')', '~', '*', '"', '@'];
foreach ($specialChars as $char) {
$string = str_replace($char, '\\' . $char, $string);
}
return $string;
}
$userQuery = '+ноутбук -acer "игровой"';
$search = escapeFulltext($userQuery);
$page = isset($_GET['page']) ? (int)$_GET['page'] : 1;
$perPage = 10;
$offset = ($page - 1) * $perPage;
$query = "SELECT SQL_CALC_FOUND_ROWS * FROM products WHERE MATCH(name, description) AGAINST(:search IN BOOLEAN MODE) LIMIT :offset, :perPage";
$stmt = $pdo->prepare($query);
$stmt->execute([
'search' => $search,
'offset' => $offset,
'perPage' => $perPage
]);
$results = $stmt->fetchAll();
// Получение общего количества
$totalStmt = $pdo->query("SELECT FOUND_ROWS()");
$totalRows = $totalStmt->fetchColumn();
Результат:
Найдено 12 товаров. Страница 1 из 2.
Пример 2: Поиск с учётом нескольких таблиц (связь товар-категория)
Пусть есть таблицы products и categories, необходимо искать одновременно по имени товара и имени категории.
$search = 'ноутбук';
$query = "SELECT p.*, c.name AS category_name
FROM products p
JOIN categories c ON p.category_id = c.id
WHERE MATCH(p.name, p.description) AGAINST(:search IN BOOLEAN MODE)
OR MATCH(c.name) AGAINST(:search IN BOOLEAN MODE)
ORDER BY MATCH(p.name) AGAINST(:search2 IN BOOLEAN MODE) DESC";
$stmt = $pdo->prepare($query);
$stmt->execute([
'search' => $search,
'search2' => $search
]);
$results = $stmt->fetchAll();
Результат:
Товар "Ноутбук Lenovo" (категория "Компьютеры") - релевантность по имени высокая. Товар "Мышь" (категория "Ноутбуки и аксессуары") - найдена по категории.
Пример 3: Интеграция с Elasticsearch - полная индексация и поиск с русской морфологией
Установка анализатора для русского языка через маппинг:
$params = [
'index' => 'catalog',
'body' => [
'settings' => [
'analysis' => [
'analyzer' => [
'russian_analyzer' => [
'tokenizer' => 'standard',
'filter' => ['lowercase', 'russian_morphology']
]
]
]
],
'mappings' => [
'properties' => [
'name' => [
'type' => 'text',
'analyzer' => 'russian_analyzer'
]
]
]
]
];
$client->indices()->create($params);
Поисковый запрос с подсветкой результатов:
$params = [
'index' => 'catalog',
'body' => [
'query' => [
'multi_match' => [
'query' => 'ноутбуках',
'fields' => ['name^3', 'description']
]
],
'highlight' => [
'fields' => [
'name' => new \stdClass(),
'description' => new \stdClass()
]
]
]
];
$response = $client->search($params);
Результат (после обработки):
Найдено 5 товаров. Название: ... <em>Ноутбук</em> Lenovo ... Описание: ... подходит для <em>игр</em> ...
Пример 4: Использование Sphinx Search как альтернатива
Sphinx - ещё один полнотекстовый движок, простой в настройке. Пример конфигурации sphinx.conf:
source src1 {
type = mysql
sql_host = localhost
sql_user = root
sql_pass =
sql_db = catalog
sql_query = SELECT id, name, description FROM products
}
index idx1 {
source = src1
path = /var/data/sphinx/catalog
min_word_len = 2
charset_table = 0..9, A..Z->a..z, a..z, U+410..U+44F->U+430..U+44F, U+401->U+451
}
searchd {
listen = 9312
log = /var/log/sphinx/searchd.log
}
Запрос из PHP через SphinxQL:
$sphinx = new PDO('mysql:host=127.0.0.1;port=9306');
$query = "SELECT id, WEIGHT() AS w FROM idx1 WHERE MATCH(:search) ORDER BY w DESC LIMIT 10";
$stmt = $sphinx->prepare($query);
$stmt->execute(['search' => 'ноутбук* игровой']);
$ids = $stmt->fetchAll(PDO::FETCH_COLUMN, 0);
// Затем из MySQL выбрать товары по этим id
if ($ids) {
$placeholders = implode(',', array_fill(0, count($ids), '?'));
$mysqlQuery = "SELECT * FROM products WHERE id IN ($placeholders)";
$stmt = $pdo->prepare($mysqlQuery);
$stmt->execute($ids);
$products = $stmt->fetchAll();
}
Результат:
Товары, полученные от Sphinx, отсортированы по релевантности.