Создание индекса ElasticSearch - vorst.ru

ElasticSearch


Полнотекстовый поиск

Определение модели и действия контроллера для создания индекса ElasticSearch для организации полнотекстового поиска.

ElasticSearch

Полнотекстовый поиск

На почту пришла рассылка и я записался на вебинар по Elasticsearch, но когда подошел срок, стало жалко времени на просмотр видео. Просто почитал документацию. Прошла пара недель и тут один пишет:

Нужно сделать поиск слов в таблице "articles" по столбцу "content". В результатах поиска сначала выводить те, которые содержат наибольшее количество слов в статье и далее по мере уменьшения.

Да ... Мысль летит и пальцы не поспевают набирать текст, пропуская слова. Тем не менее, задача есть, касается полнотекстового поиска и самое время использовать ElasticSearch.


Что мы имеем? Есть таблица с заголовками, статьями и прочими полями. Нужен индекс, который позволит проводить поиск по содержимому. Искать надо слово или набор слов, причем в любом количестве и быстро. Для подобной задачи подходит ElasticSearch и начать нужно с установки Java, ElasticSearch (который требует версии PHP >= 7.0) и расширения yii2-elasticsearch.

Model

Индекс нужно создавать для модели, содержащей статьи. Допустим это модель Post. В общем каталоге для моделей, то есть в папке common/models создадим подкаталог elastic и определим в нем класс модели для индекса (yii2-elasticsearch doc, https://habr.com/post/280488/).

<?php
namespace common\models\elastic;
use yii\elasticsearch\ActiveRecord;
class Post extends ActiveRecord
{
  // DB name
  public static function index() 
  {  
    return 'blog'; 
  } 
  // Table name
  public static function type() 
  {
    return 'post';  
  }
  public function attributes()
  {
    return ['id', 'title', 'content'];
  }
  public function rules() 
  {   
    return [
      [$this->attributes(), 'safe']  
    ];
  }
  // Table definition
  public static function mapping()
  {
    return [
      static::type() => [
        'properties' => [
          'id' => ['type' => 'long'],
          'title' => ['type' => 'text'],
          'content' => ['type' => 'text'],
        ]
      ],
    ];
  }
  public static function updateMapping()
  {
    $db = static::getDb();
    $command = $db->createCommand();
    $command->setMapping(static::index(), static::type(), static::mapping());
  }
  // Fill in by common/models/Post model
  public function fill($model, $setPrimaryKey = true) {
    if($setPrimaryKey)
      $this->primaryKey = $model->id;
    $this->attributes = [
      'title' => $model->title,
      'content'  => $model->content,
    ];
  }
  public static function createIndex()
  {
    $db = static::getDb();
    $command = $db->createCommand();
    if($command->indexExists(static::index()))
      $command->deleteIndex(static::index());
    $command->createIndex(static::index(), [
      'settings' => [
        'analysis' => [
          'filter' => [
            'ru_stop' => [
              'type' => 'stop',
              'stopwords' => '_russian_',
            ],
            'ru_stemmer' => [
              'type' => 'stemmer',
              'language' => 'russian',
            ],
          ],
          'analyzer' => [
            'default' => [
              'char_filter' => [
                'html_strip',
              ],
              'tokenizer' => 'standard',
              'filter' => [
                'lowercase',
                'ru_stop',
                'ru_stemmer',
              ],
            ],
          ],
        ],
      ],
      'mappings' => static::mapping(),
    ]);
  }
}

Полей в индексе может быть больше. Собственно, можно указать все поля, где целесообразно вести поиск. В "боевой" версии я добавил еще excerpt.

При создании индекса используется анализатор. Я просто скопировал его готовым из примера. Очевидно, что он требует к себе большего внимания, но результата можно достичь и копипастом. А результат классный - поиск ведется не только по полному совпадению слова, но и по корню.

Controller

Для выполнения поиска нужно предварительно сформировать индекс. Делается это просто. Перебираются все записи основной модели и сохраняются в индексе.

Контроллер можно определить консольным - для начала работы удобно формировать индекс из консоли.

Кроме того, при изменении записей в основной модели, индекс требует периодического полного обновления. Эту процедуру тоже удобно проводить из консоли, через cron.

<?php
namespace console\controllers;
use yii\console\Controller;
use common\models\Post;
use common\models\elastic\Post as ElasticPost;
class ElasticController extends Controller {
  public function actionCreateIndex() {
    ElasticPost::createIndex();
    foreach(Post::find()->where([
      'status' => Post::STATUS_PUBLISHED
    ])->all() as $post) {
      $elastic = new ElasticPost();
      $elastic->fill($post);
      $elastic->save();
    }
    \Yii::info('The ElasticSearch index was created ('. 
      ElasticPost::index() .'/'. ElasticPost::type() .').', __METHOD__);
  }
}

Log message

Оператор Yii::info() записывает сообщение в лог. Лог может быть не только файлом, но и email ящиком. В этом случае, после формирования индекса, можно получать извещение на нужный ящик.

Параметр categories в console/config.php определяет, что на ящик нужно отправлять только сообщения начинающиеся на указанную строку. В свою очередь строка задается в __METHOD__. Параметр logVars нужно обязательно обнулить, чтобы не получать кучу не нужной информации.

<?php
$params = array_merge(
    require(__DIR__ . '/../../common/config/params.php'),
    require(__DIR__ . '/../../common/config/params-local.php'),
    require(__DIR__ . '/params.php'),
    require(__DIR__ . '/params-local.php')
);
$mailer = require(__DIR__ . '/../../common/config/mailer.php');
return [
  'id' => 'app-console',
  'basePath' => dirname(__DIR__),
  'bootstrap' => ['log'],
  'controllerNamespace' => 'console\controllers',
  'components' => [
    'urlManager' => [
      'class' => 'yii\web\UrlManager',
        'enablePrettyUrl' => true,
        'showScriptName' => false,
        'baseUrl' => 'http://localhost/console',
    ],
    'mailer' => $mailer,
    'log' => [
      'targets' => [
        [
          'class' => 'yii\log\FileTarget',
          'levels' => ['error', 'warning'],
        ],
        [
          'class' => 'yii\log\EmailTarget',
          'levels' => ['info'],
          'categories' => ['console\controllers\ElasticController*'],
          'logVars' => [],
          'message' => [
            'from' => ['admin@sample.ru'],
            'to' => ['admin@sample.ru'],
            'subject' => 'ElasticSearch',
          ],
        ],
      ],
    ],
  ],
  'params' => $params,
];

Создание индекса ElasticSearch

Переходим в корень приложения, запускаем и ждем письма. Не забудьте определить mailer.

>php yii elastic/create-index

Проверим, как работает индекс.

curl -XGET "localhost:9200/blog/post/_search?pretty" -d'
{
  "_source": ["title"],
  "query": {
    "match": {
      "content": "history"
    }
  }
}'

Заключение

Для использования сформированного индекса нужен интерфейс во frontend, который позволит вводить слова поиска и учитывать полученное условие при выдаче результатов. Но об этом в следующей статье.

Оставьте комментарий

Только авторизованные посетители могут оставлять комментарии. Пожалуйста, войдите или пройдите регистрацию.