$ composer require --prefer-dist mirocow/yii2-elasticsearch
- создать класс реализующий интерфейс common\modules\elasticsearch\contracts\IndexInterface
- добавить его в настройках модуля индексации в common/config/main.php
- запустить индексацию
- Построить запрос используя построители (QueryBuilder - для самого запроса и (AggBuilder и AggregationMulti) для построения агрегации)
- Воспользоваться помошником QueryHelper, для упрощения построения запроса
- Для вывода можно воспользоваться ActiveRecod: ModelPopulate или ActiveProvider: SearchDataProvider
return [
'modules' => [
'elasticsearch' => [
'class' => mirocow\elasticsearch\Module::class,
'indexes' => [
// Содержит инструкции для создания индекса продуктов
common\repositories\indexes\ProductIndex::class => [
'class' => common\repositories\indexes\ProductIndex::class,
'index_name' => 'es_index_products',
'index_type' => 'products',
],
],
'isDebug' => true,
],
],
'bootstrap' => [
mirocow\elasticsearch\Bootstrap::class,
]
];
<?php
namespace common\repositories\repositories;
use common\models\essence\Product;
use common\repositories\exceptions\EntityNotFoundException;
final class ProductRepository implements \mirocow\elasticsearch\contracts\RepositoryInterface
{
/** @inheritdoc */
public function get(int $id)
{
$product = Product::find()
->where(['id' => $id])
->one();
if (!$product) {
throw new EntityNotFoundException('Product with id ' . $id . ' not found');
}
return $product;
}
/** @inheritdoc */
public function ids()
{
return Product::find()
->alias('product')
->select('product.id')
->asArray()
->each();
}
/** @inheritdoc */
public function count(): int
{
return (int)Product::find()
->count();
}
}
<?php
namespace common\repositories\indexes;
use common\essence\Product;
use common\repositories\exceptions\EntityNotFoundException;
use common\repositories\repositories\ProductRepository;
use mirocow\elasticsearch\components\indexes\AbstractSearchIndex;
use mirocow\elasticsearch\exceptions\SearchIndexerException;
/**
* Class ProductIndex
* @package common\repositories\indexes
*/
class ProductIndex extends AbstractSearchIndex
{
/** @var string */
public $index_name = 'index_products';
/** @var string */
public $index_type = 'products';
/** @var ProductRepository */
private $products;
/**
* ProductIndex constructor.
*/
public function __construct() {
parent::__construct();
$this->products = new ProductRepository();
}
/** @inheritdoc */
public function accepts($document)
{
return $document instanceof Product;
}
/** @inheritdoc */
public function documentIds()
{
return $this->products->ids();
}
/** @inheritdoc */
public function documentCount()
{
return $this->products->count();
}
/** @inheritdoc */
public function addDocumentById(int $documentId)
{
try {
$document = $this->products->get($documentId);
} catch (EntityNotFoundException $e) {
throw new SearchIndexerException('Product with id '.$documentId.' does not exist', 0, $e);
}
$body = [
'id' => $product->id,
'title' => [
'ru' => $productName,
'en' => $productName,
],
'attributes' => $product->attributes,
];
if($this->documentExists($document->id)){
return $this->documentUpdateById($document->id, $body);
} else {
return $this->documentCreate($document->id, $body);
}
}
/** @inheritdoc */
protected function indexConfig(): array
{
return [
'index' => $this->name(),
'body' => [
'settings' => [
'number_of_shards' => 1,
'number_of_replicas' => 0,
// https://www.elastic.co/guide/en/elasticsearch/reference/5.6/analysis-analyzers.html
// https://www.elastic.co/guide/en/elasticsearch/reference/5.6/analysis.html
'analysis' => [
'filter' => [
'_delimiter' => [
'type' => 'word_delimiter',
'generate_word_parts' => true,
'catenate_words' => true,
'catenate_numbers' => true,
'catenate_all' => true,
'split_on_case_change' => true,
'preserve_original' => true,
'split_on_numerics' => true,
'stem_english_possessive' => true // `s
],
'fulltext_index_ngram_filter' => [
'type' => 'edge_ngram',
'min_gram' => '2',
'max_gram' => '20',
],
/**
* Russian
*/
"russian_stop" => [
"type" => "stop",
"stopwords" => "_russian_",
],
"russian_keywords" => [
"type" => "keyword_marker",
"keywords" => ["пример"],
],
"russian_stemmer" => [
"type" => "stemmer",
"language" => "russian",
],
/**
* English
*/
"english_stop" => [
"type" => "stop",
"stopwords" => "_english_",
],
"english_keywords" => [
"type" => "keyword_marker",
"keywords" => ["example"],
],
"english_stemmer" => [
"type" => "stemmer",
"language" => "english",
],
"english_possessive_stemmer" => [
"type" => "stemmer",
"language" => "possessive_english",
],
],
// https://www.elastic.co/guide/en/elasticsearch/reference/5.6/analyzer.html
// https://www.elastic.co/guide/en/elasticsearch/reference/5.6/search-analyzer.html
'analyzer' => [
// victoria's, victorias, victoria
'autocomplete' => [
'type' => 'custom',
'tokenizer' => 'standard',
'filter' => [
// https://www.elastic.co/guide/en/elasticsearch/reference/5.6/analysis-standard-tokenfilter.html
'standard',
// https://www.elastic.co/guide/en/elasticsearch/reference/5.6/analysis-lowercase-tokenizer.html
'lowercase',
// https://www.elastic.co/guide/en/elasticsearch/reference/5.6/analysis-stop-tokenfilter.html
'stop',
// https://www.elastic.co/guide/en/elasticsearch/reference/5.6/analysis-asciifolding-tokenfilter.html
'asciifolding',
// https://www.elastic.co/guide/en/elasticsearch/reference/5.6/analysis-porterstem-tokenfilter.html
'porter_stem',
//'english_stemmer',
//'russian_stemmer',
'_delimiter',
],
],
'search_analyzer' => [
'type' => 'custom',
'tokenizer' => 'standard',
'filter' => [
// https://www.elastic.co/guide/en/elasticsearch/reference/5.6/analysis-standard-tokenfilter.html
'standard',
// https://www.elastic.co/guide/en/elasticsearch/reference/5.6/analysis-lowercase-tokenizer.html
'lowercase',
// https://www.elastic.co/guide/en/elasticsearch/reference/5.6/analysis-stop-tokenfilter.html
'stop',
// https://www.elastic.co/guide/en/elasticsearch/reference/5.6/analysis-asciifolding-tokenfilter.html
'asciifolding',
// https://www.elastic.co/guide/en/elasticsearch/reference/5.6/analysis-porterstem-tokenfilter.html
'porter_stem',
//'english_stemmer',
//'russian_stemmer',
],
],
'fulltext_index_analyzer_ru' => [
'filter' => [
"lowercase",
"russian_stop",
"russian_keywords",
"russian_stemmer",
"fulltext_index_ngram_filter",
],
'tokenizer' => 'standard',
],
'fulltext_index_analyzer_en' => [
'filter' => [
"english_possessive_stemmer",
"lowercase",
"english_stop",
"english_keywords",
"english_stemmer",
"fulltext_index_ngram_filter",
],
'tokenizer' => 'standard',
],
],
],
],
'mappings' => [
$this->type() => [
// Определяет базовый набор свойств для группы полей
'dynamic_templates' => [
[
'attributes' => [
'path_match' => 'attributes.*',
'mapping' => [
'index' => false,
],
],
],
],
// При индексировании поля _all все поля документа объединяются в одну большую строку независимо от типа данных.
// По умолчанию поле _all включено.
"_all" => [
"enabled" => false
],
'properties' => [
// Возвращаемые данные, не индексируются
// Заполняет модель методом populate
// Не индексируется
'attributes' => [
'properties' => [
'created_at' => [
"type" => "date",
// 2016-12-28 16:21:30
"format" => "yyyy-MM-dd HH:mm:ss||yyyy-MM-dd||epoch_millis",
],
],
],
'title' => [
'properties' => [
'en' => [
'type' => 'text',
'search_analyzer' => 'fulltext_index_analyzer_en',
'analyzer' => 'fulltext_index_analyzer_en',
//'analyzer' => 'english',
],
'ru' => [
'type' => 'text',
'search_analyzer' => 'fulltext_index_analyzer_ru',
'analyzer' => 'fulltext_index_analyzer_ru',
//'analyzer' => 'russian',
],
],
],
],
],
],
],
];
}
}
Создать пустой индекс
$ php yii elasticsearch/index/create index_name
Заполнить индекс всеми документами
$ php yii elasticsearch/index/populate index_name
Удалить индекс и все его данные
$ php yii elasticsearch/index/destroy index_name
Удалить все индексы если они существуют, создать все индексы, проиндексировать документы во всех индексах
$ php yii elasticsearch/index/rebuild
$ export PHP_IDE_CONFIG="serverName=www.skringo.ztc" && export XDEBUG_CONFIG="remote_host=192.168.1.6 idekey=xdebug" && php7.0 ./yii elasticsearch/index/create products_search
$terms = QueryHelper::terms('categories.name', 'my category');
$nested[] = QueryHelper::nested('string_facet',
QueryHelper::filter([
QueryHelper::term('string_facet.facet_name', ['value' => $id, 'boost' => 1]),
QueryHelper::term('string_facet.facet_value', ['value' => $value, 'boost' => 1]),
])
);
$filter[] = QueryHelper::should($nested);
use mirocow\elasticsearch\components\queries\QueryBuilder;
class ProductFacets extends ProductIndex
{
/**
* @param ProductSearch $model
* @return QueryBuilder
* @throws \Exception
*/
public function getQueryFascetes(ProductSearch $model)
{
$should = [];
$must = [];
$must_not = [];
$filter = [];
$_should[] = QueryHelper::multiMatch([
'title.ru^10',
'title.en^10',
'description.ru^20',
'description.en^20',
'model.name^20',
'brand.name^15',
'categories.name^10',
], $queryCorrect, 'phrase', ['operator' => 'or', 'boost' => 0.5]);
$must[] = QueryHelper::should($_should);
/** @var Aggregation $agg */
$aggBuilder = new AggBuilder();
$aggregations = new AggregationMulti;
$terms = QueryHelper::terms('categories.name', 'my category');
$must[] = $terms;
$agg = $aggBuilder->filter('categories.id', $terms)
->add($aggBuilder->terms('categories.id'));
$aggregations->add('categories', $agg);
$terms = QueryHelper::terms('brand.name', 'my brand');
$must[] = $terms;
$agg = $aggBuilder->filter('brand.id', $terms)
->add($aggBuilder->terms('brand.id'));
$aggregations->add('brands', $agg);
/** @var QueryBuilder $query */
$query = new QueryBuilder;
$query = $query
->add(QueryHelper::bool($filter, $must, $should, $must_not))
->aggregations($aggregations)
->withSource('attributes');
return $query;
}
}
use mirocow\elasticsearch\components\indexes\ModelPopulate;
final class ProductPopulate extends ModelPopulate
{
public $modelClass = Product::class;
}
use mirocow\elasticsearch\components\queries\QueryBuilder;
use mirocow\elasticsearch\components\factories\IndexerFactory;
use mirocow\elasticsearch\components\indexes\SearchDataProvider;
class ProductSearch extends Product
{
public function search ($params, $pageSize = 9)
{
$this->load($params);
/** @var ProductFacets $search */
$search = IndexerFactory::createIndex(ProductFacets::class);
/** @var QueryBuilder $query */
$query = $search->getQueryFascetes($this);
$dataProvider = new SearchDataProvider([
'modelClass' => (new ProductPopulate())->select('_source.attributes'),
'search' => $search,
'query' => $query,
'sort' => QueryHelper::sortBy(['_score' => SORT_ASC]),
'pagination' => [
'pageSize' => 10,
],
]);
}
}
use mirocow\elasticsearch\components\queries\QueryBuilder;
use mirocow\elasticsearch\components\factories\IndexerFactory;
class ProductSearch extends Product
{
public function search ($params, $pageSize = 9)
{
$this->load($params);
/** @var ProductFacets $search */
$search = IndexerFactory::createIndex(ProductFacets::class);
/** @var QueryBuilder $query */
$query = $search->getQueryFascetes($this);
$products = (new ProductPopulate())
->select('_source.attributes')
->search($query)
->result();
}
}
Отладочный поисковый запрос Если вы когда-либо задавались вопросом, почему документ не соответствует запросу, сначала проверьте сопоставление типа с помощью API-интерфейса GET, как показано здесь:
GET example6/product/_mapping
Если отображение правильное, убедитесь, что значение текстового поля проанализировано правильно. Вы можете использовать API анализа для проверки списка токенов, как показано ниже:
GET _analyze?analyzer=russian&text=Маша+любит+вареники
Если анализатор ведет себя так, как ожидалось, убедитесь, что вы используете правильный тип запроса. Например, используя совпадение вместо запроса термина и так далее.
- https://www.elastic.co/guide/en/elasticsearch/reference/5.6/analysis-analyzers.html
- https://www.elastic.co/guide/en/elasticsearch/reference/5.6/analysis.html
- https://www.elastic.co/guide/en/elasticsearch/reference/5.6/analyzer.html
- https://www.elastic.co/guide/en/elasticsearch/reference/5.6/index.html
- https://discuss.elastic.co/c/in-your-native-tongue/russian
- https://ru.stackoverflow.com/search?q=Elasticsearch
- http://qaru.site/search?query=ElasticSearch
- https://discuss.elastic.co/c/elasticsearch
Посмотреть на:
- https://github.com/inpsyde/elastic-facets
- https://github.com/ongr-io/ElasticsearchDSL/tree/master/src (https://github.com/ongr-io/ElasticsearchDSL/blob/master/docs/index.md)
- https://github.com/kiwiz/ecl ?
- https://github.com/ruflin/Elastica
- https://hotexamples.com/ru/examples/elastica.aggregation/Terms/-/php-terms-class-examples.html