Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
No results found
Show changes
Showing
with 700 additions and 94 deletions
<?php
namespace IQDEV\ElasticSearch\Facet\Item;
class FacetItemRangeDTO
{
public ?float $min = null;
public ?float $max = null;
public ?float $avg = null;
public ?float $sum = null;
public static function create(?float $min = null, ?float $max = null, ?float $avg = null, ?float $sum = null): self
{
$instance = new self();
$instance->min = $min;
$instance->max = $max;
$instance->avg = $avg;
$instance->sum = $sum;
return $instance;
}
}
\ No newline at end of file
<?php
namespace IQDEV\ElasticSearch\Facet\Item;
class FacetItemSingle extends FacetItem
{
public static function create(?string $value = null, int $count = 0, ?string $label = null): self
{
$instance = new self();
$instance->value = $value;
$instance->count = $count;
$instance->label = $label;
return $instance;
}
}
<?php
namespace IQDEV\ElasticSearch\Facet\Type;
use IQDEV\ElasticSearch\Facet\Facet;
final class BaseFacet extends Facet
{
/**
* @inheritDoc
*/
public function es(): array
{
return [
$this->property->getKey() => $this->value,
];
}
}
<?php
namespace IQDEV\ElasticSearch\Facet\Type;
use IQDEV\ElasticSearch\Facet\Facet;
final class KeywordFacet extends Facet
{
/**
* @inheritDoc
*/
public function es(): array
{
$value = is_array($this->value) ?
array_map(static fn($value) => (string) $value, $this->value) :
(string) $this->value;
return [
'facet_code' => $this->property->getKey(),
'facet_value' => $value
];
}
}
<?php
namespace IQDEV\ElasticSearch\Facet\Type;
use IQDEV\ElasticSearch\Facet\Facet;
final class NumberFacet extends Facet
{
/**
* @inheritDoc
*/
public function es(): array
{
return [
'facet_code' => $this->property->getKey(),
'facet_value' => (float) $this->value,
];
}
}
<?php
namespace IQDEV\ElasticSearch\Helper;
class ArrayHelper
{
/**
* Recursively filter an array
*
* @param array $array
* @param callable|null $callback
*
* @return array
*/
public static function array_filter_recursive(array $array, ?callable $callback = null): array
{
$array = is_callable($callback) ? array_filter($array, $callback) : array_filter($array);
foreach ($array as $key => &$value) {
if (is_array($value)) {
$value = call_user_func([__CLASS__, __FUNCTION__], $value, $callback);
if (!empty($value)) {
$value = self::array_filter_recursive($value, $callback);
} else {
unset($array[$key]);
}
}
}
return $array;
}
}
\ No newline at end of file
<?php
namespace IQDEV\ElasticSearch\Indexer;
use IQDEV\ElasticSearch\Esable;
final class AddIndex implements Index
{
public function __construct(
private string $name,
private Esable $body,
private ?string $id = null
)
{
}
public function es(): array
{
$es = [
'index' => $this->name,
'body' => $this->body->es(),
];
if ($this->id) {
$es['id'] = $this->id;
}
return $es;
}
}
<?php
namespace IQDEV\ElasticSearch\Indexer;
use IQDEV\ElasticSearch\Configuration;
use IQDEV\ElasticSearch\Document\ProductDocument;
use IQDEV\ElasticSearch\Document\Property\Property;
use IQDEV\ElasticSearch\Document\Property\PropertyType;
use IQDEV\ElasticSearch\Facet\FacetFactory;
class BaseIndexProvider implements IndexProvider
{
private ?int $size = null;
private ?int $limit = null;
public function __construct(
private array $products,
private Configuration $configuration
) {
}
public function get(): \Generator
{
foreach ($this->products as $product) {
$document = new ProductDocument(
FacetFactory::createFromProperty(new Property('category_id', PropertyType::BASE), $product['category'])
);
$document->setAdditionData($product['data'] ?? []);
foreach ($product['properties'] as $type => $values) {
foreach ($values as $key => $value) {
if ($type === 'number') {
$document->getNumberFacets()->add(
FacetFactory::createFromProperty(new Property($key, PropertyType::NUMBER), $value)
);
} else {
$document->getKeywordFacets()->add(
FacetFactory::createFromProperty(new Property($key, PropertyType::KEYWORD), $value)
);
}
}
}
$document->setSearchContent($product['name']);
yield new AddIndex(
$this->configuration->getIndexName(),
$document,
$product['id']
);
}
}
public function setBatchSize(int $size): void
{
$this->size = $size;
}
public function getBatchSize(): ?int
{
return $this->size;
}
public function setLimit(int $limit): void
{
$this->limit = $limit;
}
public function getLimit(): ?int
{
return $this->limit;
}
}
<?php
namespace IQDEV\ElasticSearch\Indexer;
use IQDEV\ElasticSearch\Esable;
final class BulkIndex implements Index
{
public function __construct(
private string $name,
private Esable $body,
private ?string $id = null
) {
}
public function es(): array
{
$es = [
[
'index' => [
'_index' => $this->name
]
]
];
if ($this->id) {
$es[0]['index']['_id'] = $this->id;
}
$es[] = $this->body->es();
return $es;
}
}
<?php
namespace IQDEV\ElasticSearch\Indexer;
final class DeleteIndex implements Index
{
public function __construct(
private string $name,
private ?string $id = null
) {
}
public function es(): array
{
$es = [
'index' => $this->name
];
if ($this->id) {
$es['id'] = $this->id;
}
return $es;
}
}
<?php
namespace IQDEV\ElasticSearch\Indexer;
use Elastic\Elasticsearch\Client;
use Elastic\Elasticsearch\Exception\ClientResponseException;
use Elastic\Elasticsearch\Exception\ContentTypeException;
use Elastic\Elasticsearch\Exception\MissingParameterException;
use Elastic\Elasticsearch\Exception\ServerResponseException;
use Elastic\Elasticsearch\Response\Elasticsearch;
use Elastic\Elasticsearch\Traits\EndpointTrait;
use IQDEV\ElasticSearch\Configuration;
use Psr\Log\LoggerInterface;
final class EsHelperEndpoint
{
use EndpointTrait;
public function __construct(
private Client $esClient,
private Configuration $configuration,
private LoggerInterface $logger
) {
}
public function isIndexExists(): bool
{
$response = $this->esClient
->indices()
->exists(
[
'index' => $this->configuration->getIndexName(),
]
);
return $response instanceof Elasticsearch && true === $response->asBool();
}
/**
* Создание индекса
*
* @return void
*
* @throws \Elastic\Elasticsearch\Exception\ClientResponseException
* @throws \Elastic\Elasticsearch\Exception\MissingParameterException
* @throws \Elastic\Elasticsearch\Exception\ServerResponseException
*/
public function create(): void
{
if (false === $this->isIndexExists()) {
$this->logger->info(sprintf('Index %s was created', $this->configuration->getIndexName()));
$this->esClient->indices()->create(
[
'index' => $this->configuration->getIndexName(),
'body' => [
'mappings' => $this->configuration->getMapping(),
'settings' => $this->configuration->getSettings(),
],
]
);
}
}
/**
* Обновление конфигурации индекса
*
* @throws ContentTypeException
*/
public function reconfigurate(): void
{
$this->esClient->sendRequest(
$this->createRequest(
'POST',
'/' . $this->encode($this->configuration->getIndexName()) . '/_close',
[
'Accept' => 'application/json',
'Content-Type' => 'application/json',
],
[],
)
);
$this->esClient->sendRequest(
$this->createRequest(
'PUT',
'/' . $this->encode($this->configuration->getIndexName()) . '/_settings',
[
'Accept' => 'application/json',
'Content-Type' => 'application/json',
],
$this->configuration->getSettings(),
)
);
$this->esClient->sendRequest(
$this->createRequest(
'PUT',
'/' . $this->encode($this->configuration->getIndexName()) . '/_mapping',
[
'Accept' => 'application/json',
'Content-Type' => 'application/json',
],
$this->configuration->getMapping(),
)
);
$this->esClient->sendRequest(
$this->createRequest(
'POST',
'/' . $this->encode($this->configuration->getIndexName()) . '/_open',
[
'Accept' => 'application/json',
'Content-Type' => 'application/json',
],
[],
)
);
$this->logger->info(sprintf('Index %s was reconfigurated', $this->configuration->getIndexName()));
}
/**
* Полная очистка документов индекса
*
* @throws ContentTypeException
*/
public function clear(): void
{
if ($this->isIndexExists()) {
$this->esClient->sendRequest(
$this->createRequest(
'POST',
'/' . $this->encode($this->configuration->getIndexName()) . '/_delete_by_query',
[
'Accept' => 'application/json',
'Content-Type' => 'application/json',
],
'{"query": {"match_all": {}}}'
)
);
$this->logger->info(sprintf('Index %s was cleared', $this->configuration->getIndexName()));
} else {
$this->logger->error(sprintf('Index %s does not exists', $this->configuration->getIndexName()));
}
}
/**
* Удаление индекса
*
* @return void
*
* @throws ClientResponseException
* @throws MissingParameterException
* @throws ServerResponseException
*/
public function delete(): void
{
if ($this->isIndexExists()) {
$response = $this->esClient
->indices()
->delete(
[
'index' => $this->configuration->getIndexName(),
]
);
if (($response instanceof Elasticsearch) && false === $response->asBool()) {
$this->logger->info(sprintf('Index %s was deleted', $this->configuration->getIndexName()));
} else {
$this->logger->error(sprintf('Index %s was not deleted', $this->configuration->getIndexName()));
}
}
}
}
\ No newline at end of file
......@@ -4,33 +4,6 @@ namespace IQDEV\ElasticSearch\Indexer;
use IQDEV\ElasticSearch\Esable;
final class Index implements Esable
interface Index extends Esable
{
private string $name;
private Esable $body;
private ?string $id;
public function __construct(
string $name,
Esable $body,
?string $id = null
) {
$this->name = $name;
$this->body = $body;
$this->id = $id;
}
public function es(): array
{
$es = [
'index' => $this->name,
'body' => $this->body->es(),
];
if ($this->id) {
$es['id'] = $this->id;
}
return $es;
}
}
......@@ -4,5 +4,42 @@ namespace IQDEV\ElasticSearch\Indexer;
interface IndexProvider
{
/**
* Итерационное получение элемнтов для обновления
*
* @return \Generator|Index[]
*/
public function get(): \Generator;
}
\ No newline at end of file
/**
* Установка размера пакета для передачив elasticsearch
*
* @param int $size
*
* @return void
*/
public function setBatchSize(int $size): void;
/**
* Получение размера пакета для передачив elasticsearch
*
* @return int|null
*/
public function getBatchSize(): ?int;
/**
* Установка лимита на количество обрабатываемых данных для индексации за один раз
*
* @param int $limit
*
* @return void
*/
public function setLimit(int $limit): void;
/**
* Получение лимита на количество обрабатываемых данных для индексации за один раз
*
* @return int|null
*/
public function getLimit(): ?int;
}
......@@ -4,40 +4,86 @@ namespace IQDEV\ElasticSearch\Indexer;
use Elastic\Elasticsearch\Client;
use IQDEV\ElasticSearch\Configuration;
use Psr\Log\LoggerInterface;
final class IndexRunner
{
private Client $esClient;
private Configuration $configuration;
private EsHelperEndpoint $helper;
public function __construct(
Client $esClient,
Configuration $configuration
) {
Client $esClient,
Configuration $configuration,
LoggerInterface $logger
)
{
$this->esClient = $esClient;
$this->configuration = $configuration;
$this->helper = new EsHelperEndpoint($esClient, $configuration, $logger);
}
public function run(IndexProvider $indexProvider)
{
if ($this->esClient->indices()->exists(['index' => $this->configuration->getIndexName()])->asBool() === false) {
$this->esClient->indices()->create(
[
'index' => $this->configuration->getIndexName(),
'body' => [
'mappings' => $this->configuration->getMapping(),
],
]
);
}
$this->helper->create();
if ($indexProvider->getBatchSize() !== null && $indexProvider->getBatchSize() > 0) {
$counter = 0;
$params = ['body' => []];
foreach ($indexProvider->get() as $index) {
if ($index instanceof DeleteIndex) {
if (!empty($params['body'])) {
$this->esClient->bulk($params);
$params = ['body' => []];
$counter = 0;
}
$this->esClient->delete($index->es());
continue;
}
if ($index instanceof UpdateIndex) {
if (!empty($params['body'])) {
$this->esClient->bulk($params);
$params = ['body' => []];
$counter = 0;
}
$this->esClient->update($index->es());
continue;
}
if (!$index instanceof BulkIndex) {
continue;
}
$esIndex = $index->es();
foreach ($esIndex as $indexItem) {
$params['body'][] = $indexItem;
}
foreach ($indexProvider->get() as $index) {
if (!$index instanceof Index) {
continue;
if (++$counter >= $indexProvider->getBatchSize()) {
$this->esClient->bulk($params);
$params = ['body' => []];
$counter = 0;
}
}
$esIndex = $index->es();
$this->esClient->index($esIndex);
if (!empty($params['body'])) {
$this->esClient->bulk($params);
}
} else {
foreach ($indexProvider->get() as $index) {
if ($index instanceof DeleteIndex) {
$this->esClient->delete($index->es());
continue;
}
if ($index instanceof AddIndex) {
$this->esClient->index($index->es());
continue;
}
if ($index instanceof UpdateIndex) {
$this->esClient->update($index->es());
continue;
}
}
}
}
}
<?php
namespace IQDEV\ElasticSearch\Indexer;
use IQDEV\ElasticSearch\Esable;
final class UpdateIndex implements Index
{
public function __construct(
private string $name,
private Esable $body,
private ?string $id = null
) {
}
public function es(): array
{
$es = [
'index' => $this->name,
'body' => [
'doc' => $this->body->es()
]
];
if ($this->id) {
$es['id'] = $this->id;
}
return $es;
}
}
<?php
namespace IQDEV\ElasticSearch;
use IQDEV\ElasticSearch\Document\ProductCollection;
use IQDEV\ElasticSearch\Facet\Collection\FacetResultCollection;
class Result
{
private ProductCollection $products;
private FacetResultCollection $facets;
private int $total = 0;
public function __construct()
{
$this->products = new ProductCollection();
$this->facets = new FacetResultCollection();
}
public function setTotal(int $total): void
{
$this->total = $total;
}
public function getTotal(): int
{
return $this->total;
}
public function getProducts(): ProductCollection
{
return $this->products;
}
public function getFacets(): FacetResultCollection
{
return $this->facets;
}
}
......@@ -12,71 +12,52 @@ final class Aggs implements Esable
private ?Query $query = null;
private ?Nested $nested = null;
private ?Terms $terms = null;
private ?ExtremumTerms $extremumTerms = null;
private string $key;
private ?Stats $stats = null;
public function __construct(string $key)
{
$this->key = $key;
public function __construct(
private string $key
) {
}
/**
* @param Aggs|null $aggs
* @return Aggs
*/
public function addAggs(?Aggs $aggs): self
public function addAggs(Aggs $aggs): self
{
if ($this->aggs === null) {
if (null === $this->aggs) {
$this->aggs = new AggsCollection();
}
$this->aggs->add($aggs);
return $this;
}
/**
* @param Query|null $query
* @return Aggs
*/
public function setQuery(?Query $query): self
{
$this->query = $query;
return $this;
}
/**
* @param Nested|null $nested
* @return Aggs
*/
public function setNested(?Nested $nested): self
{
$this->nested = $nested;
return $this;
}
/**
* @param Terms|null $terms
* @return Aggs
*/
public function setTerms(?Terms $terms): self
{
$this->terms = $terms;
return $this;
}
/**
* @param ExtremumTerms|null $terms
* @return Aggs
*/
public function setExtremumTerms(?ExtremumTerms $terms): self
public function setStats(?Stats $stats): self
{
$this->extremumTerms = $terms;
$this->stats = $stats;
return $this;
}
/**
* @return string
*/
public function getKey(): string
{
return $this->key;
......@@ -90,7 +71,7 @@ final class Aggs implements Esable
$agg['aggs'] = array_merge($agg, $this->aggs->es()['aggs']);
}
if (isset($this->query) && $this->query->isEmpty() === false) {
if ($this->query && false === $this->query->isEmpty()) {
$agg['filter'] = $this->query->es()[$this->query->getType()];
}
......@@ -101,11 +82,11 @@ final class Aggs implements Esable
if ($this->terms) {
$agg['terms'] = $this->terms->es()['terms'];
}
if ($this->extremumTerms) {
$agg = array_merge($agg, $this->extremumTerms->es());
if ($this->stats) {
$agg['stats'] = $this->stats->es()['stats'];
}
return $agg;
}
}
\ No newline at end of file
}
......@@ -3,22 +3,28 @@
namespace IQDEV\ElasticSearch\Search\Aggs;
use IQDEV\ElasticSearch\Esable;
use Ramsey\Collection\AbstractCollection;
final class AggsCollection extends AbstractCollection implements Esable
final class AggsCollection implements Esable
{
public function getType(): string
/**
* @var Aggs[]
*/
private array $aggs = [];
public function add(Aggs $aggs): self
{
return Aggs::class;
$this->aggs[] = $aggs;
return $this;
}
public function es(): array
{
$aggs = [];
foreach ($this as $agg) {
foreach ($this->aggs as $agg) {
$aggs[$agg->getKey()] = $agg->es();
}
return ['aggs' => $aggs];
}
}
\ No newline at end of file
}
<?php
namespace IQDEV\ElasticSearch\Search\Aggs;
use IQDEV\ElasticSearch\Search\Nested;
final class AggsFacetStats
{
public static function create(string $code, string $facet, string $path = 'search_data'): Aggs
{
$aggNumberFacet = new Aggs($code);
$nested = new Nested();
$nested->setPath($path . '.' . $facet);
$aggNumberFacet->setNested($nested);
$aggNumberFacetCode = new Aggs("agg_{$facet}_code");
$aggNumberFacetCode->setTerms(
(new Terms("{$path}.{$facet}.facet_code"))
->setSize(250)
);
$aggKeywordFacetValue = new Aggs("agg_{$facet}_value");
$aggKeywordFacetValue->setStats(
new Stats("{$path}.{$facet}.facet_value")
);
$aggNumberFacetCode->addAggs($aggKeywordFacetValue);
$aggNumberFacet->addAggs($aggNumberFacetCode);
return $aggNumberFacet;
}
}
......@@ -4,7 +4,7 @@ namespace IQDEV\ElasticSearch\Search\Aggs;
use IQDEV\ElasticSearch\Search\Nested;
final class AggsKeyWordFacet
final class AggsFacetTerms
{
public static function create(string $code, string $facet, string $path = 'search_data'): Aggs
{
......@@ -18,7 +18,6 @@ final class AggsKeyWordFacet
(new Terms("{$path}.{$facet}.facet_code"))
->setSize(250)
);
$aggKeywordFacetValue = new Aggs("agg_{$facet}_value");
$aggKeywordFacetValue->setTerms(
(new Terms("{$path}.{$facet}.facet_value"))
......