Организация поиска по товарам: от MySQL до специализированных систем

Раздел: Разработка каталогов на PHP -> Функционал каталога

Поиск по каталогу на 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, отсортированы по релевантности.

Поиск по каталогу PHP - comments

En
Catalog php find (php)