ElasticSearch — это одна из самых популярных поисковых систем в области Big Data с широким набором функций полнотекстового поиска.
Описание всех преимуществ этого движка доступно на официальном сайте.
Для чего может использоваться:
- Поиск на сайте.
- Анализ большого количества данных.
- Построение аналитических графиков.
- Средства сбора логов и поиска по ним (в составе стека ELK — Elasticsearch + Logstash + Kibana).
ElasticSearch взаимодействует с другими сервисами и языками программирования. Подключиться к нему можно по REST API.
Преимущества:
- Высокая скорость работы.
- Просто настроить интеграцию с другими системами.
- Отказоустойчивость и масштабирование. Кластер Elasticsearch автоматически реплицирует данные на все ноды.
- индекс - похож на базу данных в реляционных базах данных.
- документ - единица индекса, которая имеет собственный id, похоже на строку таблицы в реляционной базе данных.
Установка
Устанавливать ElasticSearch будем через Docker, для этого соберем простой docker-compose.yml
version: '3.7'
services:
elasticsearch:
container_name: es
image: docker.elastic.co/elasticsearch/elasticsearch:7.11.0
environment:
- xpack.security.enabled=false
- "discovery.type=single-node"
ports:
- 9200:9200
networks:
default:
name: network
После запуска контейнера, переходим по адресу server_ip:9200 (далее будем использовать 127.0.0.1:9200) - в результате должны увидеть такой ответ{
"name" : "58370095bf1b",
"cluster_name" : "docker-cluster",
"cluster_uuid" : "91DO9_IuRQCXfikgPAluvA",
"version" : {
"number" : "7.11.0",
"build_flavor" : "default",
"build_type" : "docker",
"build_hash" : "8ced7813d6f16d2ef30792e2fcde3e755795ee04",
"build_date" : "2021-02-08T22:44:01.320463Z",
"build_snapshot" : false,
"lucene_version" : "8.7.0",
"minimum_wire_compatibility_version" : "6.8.0",
"minimum_index_compatibility_version" : "6.0.0-beta1"
},
"tagline" : "You Know, for Search"
}
Если увидели ответ, значит ElasticSearch готов к работе. Теперь нужно настроить php, для этого устанавливаем пакет для работы с ElascticSearch
composer require elasticsearch/elasticsearch
Импорт данных
Перед тем как начать искать что-то через ElasticSearch - это что-то нужно в него поместить, в данном случае поместим туда некоторые поля из таблице с клиентами:- id
- телефон
- имя
use Elasticsearch\ClientBuilder;
require_once $_SERVER['DOCUMENT_ROOT'] . '/vendor/autoload.php';
$client = ClientBuilder::create()
->setHosts(['127.0.0.1:9200']) // указываем, в виде массива, хост и порт сервера elasticsearch
->build();
// пропустим получение данных из MySQL и сразу перейдем к заполнению
$i = 0;
$params = ['body' => []];
foreach ($arData as $ar) {
$i++;
$params['body'][] = [
'index' => [
'_index' => 'clients', // указываем в какой индекс добавляем
'_id' => $ar['id'] // присваиваем документу id как в БД
]
];
$params['body'][] = [
'name' => $ar['name'],
'email' => $ar['email'],
'last_name' => $ar['last_name'],
'phone' => $ar['phone'],
];
if ($i == 1000) {
$i = 0;
$responses = $client->bulk($params);
$params = ['body' => []];
unset($responses);
}
}
Представим, что в $arData хранится данные записей из БД, далее их читаем и по тысяче записей отправляем в ElascticSearch в индекс clients. В качестве id документа указываем id из БД, чтобы в дальнейшем было проще обращаться к конкретным записям, если id не задан, то он будет автоматически сгенерирован.Поиск
Данные у нас уже есть, теперь можно приступить к их поиску, для этого создадим файл search.php:
use Elasticsearch\ClientBuilder;
require_once $_SERVER['DOCUMENT_ROOT'] . '/vendor/autoload.php';
if (!empty($_GET['query'])) {
$client = ClientBuilder::create()
->setHosts(['127.0.0.1:9200']) // указываем, в виде массива, хост и порт сервера elasticsearch
->build();
$params = [
'index' => 'clients', // по какому индексу ищем
'size' => 100 // количество результатов выборки
];
$params['body'] = [
'query' => [
'bool' => [
'should' => [ // should - логическое OR, must - логическое AND
// полное совпадение
[
'match' => [
'last_name' => $_GET['query']
]
],
// частичное совпадение, аналог LIKE в MySQL
[
'wildcard' => [
'last_name' => [
'value' => '*' . $_GET['query'] . '*',
"boost" => '1.0',
"rewrite" => "constant_score",
]
]
],
// поиск по похожим фразам (box → fox, black → lack, act → cat)
[
'fuzzy' => [
'last_name' => $_GET['query']
]
]
]
]
]
];
$result = $client->search($params);
}
Теперь при обращении по адресу /search.php?query=Иванов будет осуществлен поиск по фамилии, результат поиска
Array
(
[took] => 40
[timed_out] =>
[_shards] => Array
(
[total] => 1
[successful] => 1
[skipped] => 0
[failed] => 0
)
[hits] => Array
(
[total] => Array
(
[value] => 10000
[relation] => gte
)
[max_score] => 11.786083
[hits] => Array
(
[0] => Array
(
[_index] => clients
[_type] => _doc
[_id] => 1555023
[_score] => 11.786083
[_source] => Array
(
[name] => Евгений
[id] => 1555023
[email] => null@mail.ru
[last_name] => Иванов
[phone] => 1234567899
)
)
[1] => Array
(
[_index] => clients
[_type] => _doc
[_id] => 1555289
[_score] => 11.786083
[_source] => Array
(
[name] => Дмитрий
[id] => 1555289
[email] =>
[last_name] => Иванов
[phone] => 1234567890
)
)
)
)
)
В массиве ['hits']['hits'] - отображаются найденные документыДавайте добавим к нашему запросу условие в котором email не должен быть пустым
$params['body'] = [
'query' => [
'bool' => [
'must' => [
[
'bool' => [
'should' => [
// полное совпадение
[
'match' => [
'last_name' => $_GET['query']
]
],
// частичное совпадение, аналог LIKE в MySQL
[
'wildcard' => [
'last_name' => [
'value' => '*' . $_GET['query'] . '*',
"boost" => '1.0',
"rewrite" => "constant_score",
]
]
],
// поиск по похожим фразам (box → fox, black → lack, act → cat)
[
'fuzzy' => [
'last_name' => $_GET['query']
]
]
],
]
],
// проверка на пустое значение (sql: email != "")
[
'bool' => [
'must_not' => [
[
'term' => [
"email.keyword" => [
'value' => '',
'boost' => 1
]
]
]
]
]
],
// проверка на NULL (sql: email IS NOT NULL)
[
'exists' => [
'field' => 'email',
'boost' => 1
]
]
],
]
]
];
В ElasticSearch есть возможность транслировать SQL запрос в формат ElasticSearch (полезно для построения сложных запросов), для этого нужно отправить POST запрос на адрес 127.0.0.1:9200/_sql/translate и в теле указать json
{
"query": "SELECT * FROM clients WHERE (last_name LIKE '%иванов%' OR last_name='иванов') AND email IS NOT NULL"
}
В ответе будет json запроса к ElasticSearch - переписываем его на php и получаем готовый запрос
{
"size": 1000,
"query": {
"bool": {
"must": [
{
"bool": {
"should": [
{
"wildcard": {
"last_name.keyword": {
"wildcard": "*иванов*",
"boost": 1
}
}
},
{
"term": {
"last_name.keyword": {
"value": "иванов",
"boost": 1
}
}
}
],
"adjust_pure_negative": true,
"boost": 1
}
},
{
"exists": {
"field": "email",
"boost": 1
}
}
],
"adjust_pure_negative": true,
"boost": 1
}
},
"_source": {
"includes": [
"email",
"id",
"last_name",
"name",
"phone"
],
"excludes": []
},
"sort": [
{
"_doc": {
"order": "asc"
}
}
]
}
Возможные типы запросов описаны тут
Обновление данных
Если данные на сайте поменялись, то поменять их так же нужно в ElasticSearch
$params = [
'index' => 'clients',
'id' => 32, // id как в БД
'body' => [
'name' => 'Николай',
'email' => null,
'last_name' => 'Петров',
'phone' => '1234567890',
]
];
$response = $client->index($params);
Код обновления такой же, как и при добавлении, идентификатором служит поле id, которое у нас совпадает с id в БД. При обновлении важно в body указывать все поля. Удаление документа из индекса
$params = [
'index' => 'clients',
'id' => '32'
];
$response = $client->delete($params);
Скорость работы
Давайте сравним скорость поиска через ElasticSearch и через MySQL, чтобы было честно отсчет времени будем начинать с начала подключения и завершать после обработки данных, поиск осуществляем по таблице с 2 млн записейДля примера будем возьмем запрос
SELECT * FROM clients WHERE name LIKE "%Галанцева%"
В результате MySQL справился за 1.4 сек, а ElasticSearch за 0.4 сек, но давайте выполним запрос еще раз и посмотрим время результата из кэша
В этом случае MySQL оказался быстрее.
Если реализовывать поиска по каталогу на сайте, то с малой долей вероятности все запросы пользователей будут одинаковы, соответственно и кэш будет иметь куда меньший вес, в этом случае лучше использовать ElasticSearch:
- скорость поиска без учета кэша выше
- если проиндексировать достаточное количество полей (название, цену, путь к картинке и ссылку на товар), то для страницы поиска можно вообще не обращаться к БД
- возможность использовать поиск по похожим фразам (box → fox, black → lack, act → cat)