phpdev.org Программисты Laravel, Yii2, Битрикс
  • Поддержка
  • Разработка
  • Кейсы
  • О компании
  • Блог
  • Контакты
  • Команда
  • RU | EN
для клиентов
Главная
Блог
Elastic Search - начало работы
03.11.2021

Elastic Search - начало работы

В статье рассмотрим как и где можно использовать ElasticSearch на сайте, сравним скорость выборки с MySQL, а так же результаты выборки.

ElasticSearch — это одна из самых популярных поисковых систем в области Big Data с широким набором функций полнотекстового поиска.
Описание всех преимуществ этого движка доступно на официальном сайте.

Для чего может использоваться:

  • Поиск на сайте.
  • Анализ большого количества данных.
  • Построение аналитических графиков.
  • Средства сбора логов и поиска по ним (в составе стека ELK — Elasticsearch + Logstash + Kibana).

ElasticSearch взаимодействует с другими сервисами и языками программирования. Подключиться к нему можно по REST API.

Преимущества:

  • Высокая скорость работы.
  • Просто настроить интеграцию с другими системами.
  • Отказоустойчивость и масштабирование. Кластер Elasticsearch автоматически реплицирует данные на все ноды.
Перед дальнейшим прочтением нужно понимать две основных термина в 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
  • телефон
  • имя
  • email
Создаем файл import.php
 

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 "%Галанцева%"

2021-10-27_12-02-25.png

В результате MySQL справился за 1.4 сек, а ElasticSearch за 0.4 сек, но давайте выполним запрос еще раз и посмотрим время результата из кэша
2021-10-27_12-11-18.png
В этом случае MySQL оказался быстрее.
Если реализовывать поиска по каталогу на сайте, то с малой долей вероятности все запросы пользователей будут одинаковы, соответственно и кэш будет иметь куда меньший вес, в этом случае лучше использовать ElasticSearch:
  • скорость поиска без учета кэша выше
  • если проиндексировать достаточное количество полей (название, цену, путь к картинке и ссылку на товар), то для страницы поиска можно вообще не обращаться к БД
  • возможность использовать поиск по похожим фразам (box → fox, black → lack, act → cat)

Заключение

ElasticSearch прекрасный инструмент для поиска, который при правильно отстроенной структуре существенно снизит нагрузку на БД, тем самым увеличит скорость сайта и поиска. 

Возврат к списку


КОНТАКТЫ

Также вы можете связаться с нами по этим контактам:

phpdev.org Программисты Laravel, Yii2, Битрикс
info@phpdev.org
г. Минск, ул. Ольшевского, д. 22, пом. 20
Карта
© 2015 – 2023 PHPDev
Мы в соцсетях:
phpdev.org Программисты Laravel, Yii2, Битрикс
меню
Поддержка Разработка Кейсы О компании Блог Контакты Команда
RU | EN
Программисты Laravel, Yii2, Битрикс
BY +375
  • BY +375
  • RU +7
  • UA +380

Нажимая кнопку «Оставить заявку», Вы даёте согласие на обработку Персональных данных.

phpdev.org Программисты Laravel, Yii2, Битрикс
Спасибо! The application has been successfully sent. We will contact you shortly