Поиск по сайту с PHP: от простого LIKE до полнотекстового поиска
Реализация поискового функционала на PHP
Как организовать полнотекстовый поиск с ранжированием результатов?
Наиболее эффективное решение для поиска по текстовым данным в реляционных базах данных - использование полнотекстового индекса MySQL (FULLTEXT). Этот подход обеспечивает высокую скорость, релевантность результатов и поддержку морфологии (для английского языка). Для русской морфологии рекомендуется настроить соответствующие стоп-слова и использовать плагины, например, ngram или морфологический анализатор. Однако встроенный FULLTEXT в InnoDB поддерживает минимальную морфологию.
Создание индекса:
-- Для таблицы articles с полями title и content
ALTER TABLE articles ADD FULLTEXT INDEX ft_search (title, content);Php поиск сайту (поиск по сайту на php)
Выполнение поискового запроса:
$keyword = mysqli_real_escape_string($conn, $keyword);
$sql = "SELECT id, title,
MATCH(title, content) AGAINST('$keyword' IN NATURAL LANGUAGE MODE) AS relevance
FROM articles
WHERE MATCH(title, content) AGAINST('$keyword' IN NATURAL LANGUAGE MODE)
ORDER BY relevance DESC";
$result = mysqli_query($conn, $sql);Запрос возвращает строки, отсортированные по релевантности. Можно использовать BOOLEAN MODE для точного поиска с операторами (+/-).
Типичные проблемы:
- Минимальная длина слова (по умолчанию 4 символа) - короткие слова игнорируются. Решение: изменить параметр ft_min_word_len в my.cnf.
- Стоп-слова (предлоги, союзы) исключаются из индекса. Можно отключить стоп-слова или использовать свой список.
- При частых INSERT / UPDATE производительность индекса снижается. Используется в основном для сайтов с интенсивным чтением.
- Для кириллицы требуется корректная кодировка UTF-8 (collation utf8mb4_unicode_ci).
Как выполнить простой поиск с использованием LIKE?
Оператор LIKE подходит для небольших проектов или для поиска по строгим совпадениям (например, начало строки). Недостаток - полное сканирование таблицы.
$keyword = $_GET['search'] ?? '';
$stmt = $pdo->prepare("SELECT * FROM articles WHERE title LIKE :kw OR content LIKE :kw2");
$stmt->execute([':kw' => "%$keyword%", ':kw2' => "%$keyword%"]);
$results = $stmt->fetchAll();Использование подготовленных выражений защищает от SQL-инъекций.
Проблемы: низкая производительность на больших таблицах, отсутствие ранжирования, для поиска по нескольким словам необходима дополнительная логика.
Как использовать Sphinx для высокопроизводительного поиска?
Sphinx - специализированная система полнотекстового поиска, работающая как отдельный сервис. Подходит для больших объёмов данных и сложных запросов.
Пример конфигурации sphinx.conf и PHP-код для поиска через SphinxQL:
// SphinxQL через PDO
$dsn = 'mysql:host=127.0.0.1;port=9306;charset=utf8';
$pdo = new PDO($dsn);
$keyword = $pdo->quote($keyword);
$query = "SELECT id, weight() FROM articles WHERE MATCH($keyword) LIMIT 20";
$stmt = $pdo->query($query);
$ids = $stmt->fetchAll(PDO::FETCH_COLUMN);Проблемы: необходимость установки и настройки Sphinx, двойная индексация (данные дублируются), сложность сопровождения.
Как интегрировать Elasticsearch для гибкого поиска?
Elasticsearch обеспечивает распределённый поиск и аналитику в реальном времени. Требуется установка Elasticsearch и использование клиента elasticsearch-php.
require 'vendor/autoload.php';
$client = Elastic\Elasticsearch\ClientBuilder::create()->build();
$params = [
'index' => 'articles',
'body' => [
'query' => [
'multi_match' => [
'query' => $keyword,
'fields' => ['title^2', 'content']
]
]
]
];
$response = $client->search($params);
$hits = $response['hits']['hits'];Проблемы: высокие требования к ресурсам, необходимость синхронизации данных, сложность настройки индексов.
Как организовать поиск по файлам на сервере?
Если контент хранится в файловой системе (например, статьи в .txt), можно использовать рекурсивный обход.
function searchFiles($dir, $keyword) {
$files = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir));
$results = [];
foreach ($files as $file) {
if ($file->isFile() && $file->getExtension() == 'txt') {
$content = file_get_contents($file->getPathname());
if (stripos($content, $keyword) !== false) {
$results[] = $file->getPathname();
}
}
}
return $results;
}Проблемы: низкая производительность на большом количестве файлов, отсутствие индексации, необходимость защиты от включения больших файлов в память.
Как использовать внешний поисковый API (например, Google Custom Search)?
Google Custom Search позволяет поиск по сайту без собственного индекса. Требуется API-ключ и CX.
$apiKey = 'YOUR_KEY';
$cx = 'YOUR_CX';
$query = urlencode($keyword);
$url = "https://www.googleapis.com/customsearch/v1?key=$apiKey&cx=$cx&q=$query";
$response = file_get_contents($url);
$data = json_decode($response, true);
$items = $data['items'] ?? [];Проблемы: зависимость от внешнего сервиса, ограничения на количество запросов, отсутствие полного контроля.
Как реализовать поиск с использованием регулярных выражений?
Регулярные выражения в PHP пригодны для сложных поисковых шаблонов, но только для небольших массивов данных.
$articles = ['text1', 'text2', ...];
$pattern = '/поиск/i';
$results = preg_grep($pattern, $articles);Проблемы: не индексированный поиск, низкая скорость, сложность для многословных запросов.
Расширенные примеры и нестандартные сценарии
Пример полнотекстового поиска с подсветкой ключевых слов
$keyword = 'PHP поиск';
$sql = "SELECT id, title, content,
MATCH(title, content) AGAINST('$keyword' IN BOOLEAN MODE) AS relevance
FROM articles
WHERE MATCH(title, content) AGAINST('$keyword' IN BOOLEAN MODE)
ORDER BY relevance DESC";
$result = mysqli_query($conn, $sql);
while ($row = mysqli_fetch_assoc($result)) {
$highlighted = preg_replace("/\b({$keyword})\b/i", '$1', $row['content']);
echo "$highlighted
";
}Результат: фрагменты текста с тегами <mark>.
Поиск с пагинацией и общим количеством результатов
$page = $_GET['page'] ?? 1;
$perPage = 10;
$offset = ($page - 1) * $perPage;
// сначала общее количество
$countSql = "SELECT COUNT(*) FROM articles WHERE MATCH(title, content) AGAINST(?)";
$stmt = $pdo->prepare($countSql);
$stmt->execute([$keyword]);
$total = $stmt->fetchColumn();
$sql = "SELECT id, title, MATCH(title, content) AGAINST(? IN NATURAL LANGUAGE MODE) AS relevance
FROM articles
WHERE MATCH(title, content) AGAINST(? IN NATURAL LANGUAGE MODE)
ORDER BY relevance DESC
LIMIT $offset, $perPage";
$stmt = $pdo->prepare($sql);
$stmt->execute([$keyword, $keyword]);
$results = $stmt->fetchAll();Интеграция Elasticsearch с созданием индекса и маппингом
// Создание индекса с маппингом для русского языка
$params = [
'index' => 'articles',
'body' => [
'settings' => [
'analysis' => [
'analyzer' => [
'my_russian' => [
'type' => 'standard',
'stopwords' => '_russian_'
]
]
]
],
'mappings' => [
'properties' => [
'title' => [
'type' => 'text',
'analyzer' => 'my_russian'
],
'content' => [
'type' => 'text',
'analyzer' => 'my_russian'
]
]
]
]
];
$client->indices()->create($params);Sphinx: индексация с использованием realtime index
// В sphinx.conf добавить
index articles_rt
{
type = rt
path = /var/data/articles_rt
rt_field = title
rt_field = content
rt_attr_uint = id
}
// PHP добавление документа
$sphinx = new PDO('mysql:host=127.0.0.1;port=9306');
$sphinx->exec("INSERT INTO articles_rt (id, title, content) VALUES ($id, '$title', '$content')");
// Поиск
$stmt = $sphinx->query("SELECT * FROM articles_rt WHERE MATCH('$keyword')");Поиск в нескольких таблицах с объединением через UNION
$sql = "(SELECT id, 'article' AS type, title, content FROM articles WHERE MATCH(title, content) AGAINST(?))
UNION
(SELECT id, 'page' AS type, title, content FROM pages WHERE MATCH(title, content) AGAINST(?))
ORDER BY relevance DESC
LIMIT 20";
$stmt = $pdo->prepare($sql);
$stmt->execute([$keyword, $keyword]);Обработка ошибок при подключении к Elasticsearch
try {
$client->search($params);
} catch (Elastic\Elasticsearch\Exception\ElasticsearchException $e) {
error_log("Elasticsearch error: " . $e->getMessage());
echo "Поиск временно недоступен.";
}Поиск с помощью Zend Search Lucene (устаревший, но для справки)
// Требуется библиотека Zend Framework
$index = Zend_Search_Lucene::open('/path/to/index');
$query = Zend_Search_Lucene_Search_QueryParser::parse($keyword);
$hits = $index->find($query);
foreach ($hits as $hit) {
echo $hit->title . "\n";
}