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 944 additions and 0 deletions
<?php
declare(strict_types=1);
namespace IQDEV\ElasticSearch\Converter\Request\Filter\Type;
use IQDEV\ElasticSearch\Esable;
use IQDEV\ElasticSearch\Criteria\Filter\Filter;
use IQDEV\ElasticSearch\Search\BoolQuery\Terms;
class KeywordFilterType extends AbstractFilterType
{
public function __construct(
private readonly Filter $filter,
) {
$this->field = $this->filter->field()->value();
}
public function getEsable(): Esable
{
return new Terms($this->field, $this->filter->value()->value());
}
}
<?php
declare(strict_types=1);
namespace IQDEV\ElasticSearch\Converter\Request\Filter\Type;
use IQDEV\ElasticSearch\Esable;
use IQDEV\ElasticSearch\Criteria\Filter\Collection\FilterCollection;
use IQDEV\ElasticSearch\Criteria\Filter\Collection\FilterGroupCollection;
use IQDEV\ElasticSearch\Criteria\Filter\Filter;
use IQDEV\ElasticSearch\Search\BoolQuery\Stats;
class RangeFilterType extends AbstractFilterType
{
public function __construct(
private readonly FilterGroupCollection $filterGroupCollection,
) {
$this->field = $this->filterGroupCollection->first()->field()->value();
}
public function getEsable(): Esable
{
$ranges = [];
foreach ($this->filterGroupCollection as $filter) {
/** @var Filter $filter */
$value = $filter->value()->value();
$ranges[$filter->operator()->value] = $value;
}
return new Stats($this->field, $ranges);
}
public static function getFiltersByOneProperty(FilterGroupCollection $filterGroupCollection): FilterCollection
{
$rangeFilters = new FilterCollection();
$properties = [];
foreach ($filterGroupCollection as $filter) {
/** @var Filter $filter */
$properties[$filter->field()->value()][] = $filter;
}
foreach ($properties as $propertyFilters) {
$rangeFilters->add(new FilterGroupCollection($propertyFilters));
}
return $rangeFilters;
}
}
<?php
declare(strict_types=1);
namespace IQDEV\ElasticSearch\Converter\Request;
use IQDEV\ElasticSearch\Config\MappingValidator;
use IQDEV\ElasticSearch\Configuration;
use IQDEV\ElasticSearch\Converter\Request\Collection\NestedFilterCollection;
use IQDEV\ElasticSearch\Converter\Request\Collection\PropertyFilterCollection;
use IQDEV\ElasticSearch\Converter\Request\Filter\AbstractFilterQuery;
use IQDEV\ElasticSearch\Converter\Request\Filter\NestedFilter;
use IQDEV\ElasticSearch\Converter\Request\Filter\PropertyFilter;
use IQDEV\ElasticSearch\Criteria\Filter\Collection\FilterCollection;
use IQDEV\ElasticSearch\Criteria\Filter\Collection\FilterGroupCollection;
use IQDEV\ElasticSearch\Criteria\Filter\Filter;
use IQDEV\ElasticSearch\Search\BoolQuery\Query;
use IQDEV\ElasticSearch\Search\Nested;
class FilterQuery
{
private Query $query;
public function __construct(
private readonly Configuration $configuration,
private readonly FilterCollection $filterCollection,
private array $exclude = [],
) {
$this->query = new Query();
$this->convertToQuery();
}
private function convertToQuery(): void
{
[$propertyFilterCollection, $nestedFilterCollection] = $this->separatePropertyTypes($this->filterCollection);
if (false === $propertyFilterCollection->isEmpty()) {
$this->fillQuery($propertyFilterCollection, new PropertyFilter());
}
if (false === $nestedFilterCollection->isEmpty()) {
$this->fillQuery($nestedFilterCollection, new NestedFilter());
}
}
private function fillQuery(FilterCollection $filterCollection, AbstractFilterQuery $filterQuery): void
{
foreach ($filterCollection as $filterGroup) {
/** @var FilterGroupCollection $filterGroup */
$this->setQuery($filterGroup, $filterQuery);
}
}
private function setQuery(FilterGroupCollection $filterGroup, AbstractFilterQuery $filterQuery): void
{
$filters = $filterQuery->getQuery($filterGroup, $this->exclude);
if ($filters instanceof Query) {
if ($filters->isEmpty()) {
return;
}
foreach ($filters->getFilter() as $filter) {
$this->query->getFilter()->add($filter);
}
foreach ($filters->getShould() as $filter) {
$this->query->getShould()->add($filter);
}
foreach ($filters->getMustNot() as $filter) {
$this->query->getMustNot()->add($filter);
}
$this->query = $filters;
} elseif ($filters instanceof Nested) {
if ($filters->getQuery()->isEmpty()) {
return;
}
$this->query->getFilter()->add($filters);
}
}
public function getQuery(): Query
{
return $this->query;
}
private function separatePropertyTypes(FilterCollection $filterCollection): array
{
$propertyFilter = new PropertyFilterCollection();
$nestedFilter = new NestedFilterCollection();
foreach ($filterCollection as $groupFilter) {
/** @var FilterGroupCollection $groupFilter */
$propertyGroupCollection = new FilterGroupCollection();
$nestedGroupCollection = new FilterGroupCollection();
$propertyGroupCollection->setLogicOperator($groupFilter->getLogicOperator());
$nestedGroupCollection->setLogicOperator($groupFilter->getLogicOperator());
foreach ($groupFilter as $filter) {
/** @var Filter $filter */
if (true === MappingValidator::isPropertyExists($this->configuration, $filter->field()->value())) {
$propertyGroupCollection->add($filter);
} else {
$nestedGroupCollection->add($filter);
}
}
if (false === $propertyGroupCollection->isEmpty()) {
$propertyFilter->add($propertyGroupCollection);
}
if (false === $nestedGroupCollection->isEmpty()) {
$nestedFilter->add($nestedGroupCollection);
}
}
return [$propertyFilter, $nestedFilter];
}
}
<?php
declare(strict_types=1);
namespace IQDEV\ElasticSearch\Converter\Request;
use IQDEV\ElasticSearch\Search\Request;
abstract class RequestBuilder
{
public function __construct(
protected Request $request = new Request(),
) {
}
public function getRequest(): Request
{
return $this->request;
}
abstract public function setPagination(): void;
abstract public function setSort(): void;
abstract public function setSearch(): void;
abstract public function setFilter(): void;
abstract public function setAggregations(): void;
abstract public function build(): void;
}
<?php
namespace IQDEV\ElasticSearch\Converter\Result;
use Elastic\Elasticsearch\Response\Elasticsearch;
use IQDEV\ElasticSearch\Configuration;
use IQDEV\ElasticSearch\Document\Product;
use IQDEV\ElasticSearch\Facet\FacetResult;
use IQDEV\ElasticSearch\Facet\FacetType;
use IQDEV\ElasticSearch\Facet\Item\FacetItemList;
use IQDEV\ElasticSearch\Facet\Item\FacetItemRange;
use IQDEV\ElasticSearch\Facet\Item\FacetItemRangeDTO;
use IQDEV\ElasticSearch\Result;
final class EsResponseToResult
{
public function fromResponse(Elasticsearch $response, Configuration $configuration): Result
{
$catalogSearchResult = new Result();
$data = $response->asArray();
if (isset($data['hits']['hits'])) {
foreach ($data['hits']['hits'] as $hit) {
if (isset($hit['_source'])) {
try {
$product = $this->productFromArray($hit['_source']);
$catalogSearchResult->getProducts()->add($product);
} catch (\Throwable $ex) {
continue;
}
}
}
$catalogSearchResult->setTotal((int)$data['hits']['total']['value']);
}
if (isset($data['aggregations']['keyword_facet']['agg_keyword_facet_code']['buckets'])) {
$this->parseKeywordFacet($data, $catalogSearchResult);
}
if (isset($data['aggregations']['number_facet']['agg_number_facet_code']['buckets'])) {
$this->parseNumberFacet($data, $catalogSearchResult);
}
$this->parsePropertyFacet($data, $catalogSearchResult, $configuration);
return $catalogSearchResult;
}
private function productFromArray(array $data): Product
{
if (!isset($data['data']['id'])) {
throw new \RuntimeException('id is not set');
}
$id = $data['data']['id'];
$title = $data['data']['title'] ?? '';
$info = $data;
return new Product($id, $title, $info);
}
private function parseKeywordFacet(array $data, Result $catalogSearchResult)
{
$buckets = $data['aggregations']['keyword_facet']['agg_keyword_facet_code']['buckets'];
$bucketsFiltered = [];
if (isset($data['aggregations']['keyword_facet_filtered']['all_keyword_facet_filtered']['agg_keyword_facet_code']['buckets'])) {
foreach ($data['aggregations']['keyword_facet_filtered']['all_keyword_facet_filtered']['agg_keyword_facet_code']['buckets'] as $bucket) {
$bucketsFiltered[$bucket['key']] = [];
foreach ($bucket['agg_keyword_facet_value']['buckets'] as $values) {
$bucketsFiltered[$bucket['key']][$values['key']] = $values;
}
}
}
foreach ($buckets as $bucket) {
$code = $bucket['key'];
if (isset($data['aggregations']["keyword_facet_$code"]['agg_special']['agg_keyword_facet_code']['buckets'])) {
$bucketsFiltered[$code] = [];
foreach ($data['aggregations']["keyword_facet_$code"]['agg_special']['agg_keyword_facet_code']['buckets'] as $filteredBucket) {
if ($filteredBucket['key'] === $code) {
foreach ($filteredBucket['agg_keyword_facet_value']['buckets'] as $values) {
$bucketsFiltered[$code][$values['key']] = $values;
}
}
}
}
}
$bucketsFiltered = array_filter($bucketsFiltered);
foreach ($buckets as $bucket) {
$code = $bucket['key'];
$valueBucket = $bucket['agg_keyword_facet_value']['buckets'];
$facet = new FacetResult(FacetType::LIST, $code);
foreach ($valueBucket as $value) {
$count = $value['doc_count'];
if (isset($bucketsFiltered[$code][$value['key']])) {
$count = $bucketsFiltered[$code][$value['key']]['doc_count'];
} elseif (isset($bucketsFiltered[$code])) {
$count = 0;
}
$facet->products->add(
FacetItemList::create(
$value['key'],
$count,
isset($bucketsFiltered[$code][$value['key']])
)
);
}
$catalogSearchResult->getFacets()->add($facet);
}
}
private function parseNumberFacet(array $data, Result $catalogSearchResult)
{
$buckets = $data['aggregations']['number_facet']['agg_number_facet_code']['buckets'];
$bucketsFiltered = [];
if (isset($data['aggregations']['number_facet_filtered']['all_number_facet_filtered']['agg_number_facet_code']['buckets'])) {
foreach ($data['aggregations']['number_facet_filtered']['all_number_facet_filtered']['agg_number_facet_code']['buckets'] as $bucket) {
$bucketsFiltered[$bucket['key']] = $bucket['agg_number_facet_value'];
}
}
foreach ($buckets as $bucket) {
$code = $bucket['key'];
$workBucket = $bucket['agg_number_facet_value'];
$selectedBucket = !empty($bucketsFiltered[$code]) ? $bucketsFiltered[$code] : null;
$facet = new FacetResult(FacetType::RANGE, $code);
$facetItem = FacetItemRange::create(
FacetItemRangeDTO::create(
$workBucket['min'],
$workBucket['max'],
$workBucket['avg'],
$workBucket['sum']
),
$selectedBucket !== null ? FacetItemRangeDTO::create(
$selectedBucket['min'],
$selectedBucket['max'],
$selectedBucket['avg'],
$selectedBucket['sum']
) : FacetItemRangeDTO::create(),
$selectedBucket ? $selectedBucket['count'] : 0,
$selectedBucket !== null
);
$facet->products->add($facetItem);
$catalogSearchResult->getFacets()->add($facet);
}
}
private function parsePropertyFacet(array $data, Result $catalogSearchResult, Configuration $configuration)
{
$properties = array_keys($configuration->getMapping()['properties']);
foreach ($data['aggregations'] as $key => $aggs) {
if (!in_array($key, $properties, true)) {
continue;
}
$facet = new FacetResult(FacetType::LIST, $key);
$buckets = array_key_exists('buckets', $aggs) ? $aggs['buckets'] : $aggs[$key]['buckets'];
foreach ($buckets as $bucket) {
$code = $bucket['key'];
$count = $bucket['doc_count'];
$facet->products->add(
FacetItemList::create(
$code,
$count
)
);
}
$catalogSearchResult->getFacets()->add($facet);
}
}
}
<?php
declare(strict_types=1);
namespace IQDEV\ElasticSearch\Criteria\Aggs;
use IQDEV\ElasticSearch\Document\Property\Property;
class Aggs
{
public function __construct(
private readonly Property $property,
) {
}
public function getProperty(): Property
{
return $this->property;
}
}
<?php
namespace IQDEV\ElasticSearch\Criteria\Aggs;
use Ramsey\Collection\AbstractCollection;
class AggsCollection extends AbstractCollection
{
/**
* @inheritDoc
*/
public function getType(): string
{
return Aggs::class;
}
}
<?php
namespace IQDEV\ElasticSearch\Criteria;
use IQDEV\ElasticSearch\Criteria\Aggs\AggsCollection;
use IQDEV\ElasticSearch\Criteria\Filter\Collection\FilterCollection;
use IQDEV\ElasticSearch\Criteria\Order\OrderCollection;
use IQDEV\ElasticSearch\Criteria\Search\SearchCollection;
final class Criteria
{
private SearchCollection $search;
private FilterCollection $filters;
private OrderCollection $sorting;
private Pagination $pagination;
private AggsCollection $aggregations;
public function __construct()
{
$this->search = new SearchCollection();
$this->filters = new FilterCollection();
$this->sorting = new OrderCollection();
$this->pagination = new Pagination();
$this->aggregations = new AggsCollection();
}
public function getSearch(): SearchCollection
{
return $this->search;
}
public function getFilters(): FilterCollection
{
return $this->filters;
}
public function getSorting(): OrderCollection
{
return $this->sorting;
}
public function getPagination(): Pagination
{
return $this->pagination;
}
public function getAggs(): AggsCollection
{
return $this->aggregations;
}
public function __clone(): void
{
$this->search = clone $this->search;
$this->filters = clone $this->filters;
$this->sorting = clone $this->sorting;
$this->pagination = clone $this->pagination;
$this->aggregations = clone $this->aggregations;
}
}
<?php
namespace IQDEV\ElasticSearch\Criteria\Filter\Collection;
use IQDEV\ElasticSearch\Criteria\Filter\Filter;
use IQDEV\ElasticSearch\Criteria\Filter\FilterOperator;
use IQDEV\ElasticSearch\Criteria\Filter\FilterType;
use IQDEV\ElasticSearch\Criteria\Filter\LogicOperator;
use Ramsey\Collection\AbstractCollection;
/**
* @method self add(FilterGroupCollection $item)
*/
class FilterCollection extends AbstractCollection
{
/** @var LogicOperator Тип логической операции для коллекции */
protected LogicOperator $type;
/**
* @param FilterGroupCollection[] $data
*/
public function __construct(array $data = [])
{
parent::__construct($data);
$this->type = LogicOperator::AND;
}
/**
* @inheritDoc
*/
public function getType(): string
{
return FilterGroupCollection::class;
}
/**
* Установка типа логической операции
*
* @param LogicOperator $type
*
* @return $this
*/
public function setLogicalType(LogicOperator $type): self
{
$this->type = $type;
return $this;
}
/**
* Получение типа логической операции
*
* @return LogicOperator
*/
public function getLogicalType(): LogicOperator
{
return $this->type;
}
public function getFilterCollectionByType(FilterType $type): PostFilterCollection|QueryFilterCollection
{
$collection = match ($type) {
FilterType::POST => new PostFilterCollection(),
FilterType::QUERY => new QueryFilterCollection(),
};
$collection->data = array_filter(
$this->toArray(),
static fn (FilterGroupCollection $group) => $group->getFilterType() === $type
);
return $collection;
}
public function getQueryFilterCollection(): QueryFilterCollection
{
$collection = new QueryFilterCollection();
$collection->data = array_filter(
$this->toArray(),
static fn (FilterGroupCollection $group) => $group->getFilterType() === FilterType::QUERY
);
return $collection;
}
public function getPostFilterCollection(): PostFilterCollection
{
$collection = new PostFilterCollection();
$collection->data = array_filter(
$this->toArray(),
static fn (FilterGroupCollection $group) => $group->getFilterType() === FilterType::POST
);
return $collection;
}
public function getKeywordFilters(array $excludeFilter = []): FilterCollection
{
$filterCollection = new FilterCollection();
foreach ($this->data as $filterGroup) {
/** @var FilterGroupCollection $filterGroup */
$keywordFilterGroup = new FilterGroupCollection();
foreach ($filterGroup->data as $filter) {
/** @var Filter $filter */
$field = $filter->field()->value();
if (in_array($field, $excludeFilter, true)
|| in_array($filter->operator(), [
FilterOperator::LT,
FilterOperator::LTE,
FilterOperator::GT,
FilterOperator::GTE
], true)
) {
continue;
}
$keywordFilterGroup->add($filter);
}
if (false === $keywordFilterGroup->isEmpty()) {
$keywordFilterGroup->setLogicOperator($filterGroup->getLogicOperator());
$filterCollection->add($keywordFilterGroup);
}
}
return $filterCollection;
}
public function getNumberFilters(array $excludeFilter = []): FilterCollection
{
$filterCollection = new FilterCollection();
foreach ($this as $filterGroup) {
/** @var FilterGroupCollection $filterGroup */
$numberFilterGroup = new FilterGroupCollection();
foreach ($filterGroup as $filter) {
/** @var Filter $filter */
$field = $filter->field()->value();
if (in_array($field, $excludeFilter, true)) {
continue;
}
if (in_array($filter->operator(), [
FilterOperator::LT,
FilterOperator::LTE,
FilterOperator::GT,
FilterOperator::GTE
], true)) {
$numberFilterGroup->add($filter);
}
}
if (false === $numberFilterGroup->isEmpty()) {
$numberFilterGroup->setLogicOperator($filterGroup->getLogicOperator());
$filterCollection->add($numberFilterGroup);
}
}
return $filterCollection;
}
}
<?php
namespace IQDEV\ElasticSearch\Criteria\Filter\Collection;
use IQDEV\ElasticSearch\Criteria\Filter\Filter;
use IQDEV\ElasticSearch\Criteria\Filter\FilterOperator;
use IQDEV\ElasticSearch\Criteria\Filter\FilterType;
use IQDEV\ElasticSearch\Criteria\Filter\LogicOperator;
use Ramsey\Collection\AbstractCollection;
/**
* @method bool add(Filter $item)
*/
class FilterGroupCollection extends AbstractCollection
{
/** @var LogicOperator Тип логической операции для коллекции */
protected LogicOperator $logicOperator;
/** @var FilterType Тип фильтра для коллекции */
protected FilterType $filterType;
/**
* @param Filter[] $data
*/
public function __construct(array $data = [])
{
parent::__construct($data);
$this->logicOperator = LogicOperator::AND;
$this->filterType = FilterType::POST;
}
/**
* @inheritDoc
*/
public function getType(): string
{
return Filter::class;
}
/**
* Установка типа логической операции
*
* @param LogicOperator $type
*
* @return $this
*/
public function setLogicOperator(LogicOperator $type): self
{
$this->logicOperator = $type;
return $this;
}
/**
* Получение типа логической операции
*
* @return LogicOperator
*/
public function getLogicOperator(): LogicOperator
{
return $this->logicOperator;
}
/**
* Установка типа фильтрации
*
* @param FilterType $type
*
* @return $this
*/
public function setFilterType(FilterType $type): self
{
$this->filterType = $type;
return $this;
}
/**
* Получение типа фильтрации
*
* @return FilterType
*/
public function getFilterType(): FilterType
{
return $this->filterType;
}
public function getKeywordFilters(array $excludeFilter = []): FilterGroupCollection
{
return $this->getFilters(false, $excludeFilter);
}
public function getRangeFilters(array $excludeFilter = []): FilterGroupCollection
{
return $this->getFilters(true, $excludeFilter);
}
private function getFilters(bool $range = false, array $excludeFilter = []): FilterGroupCollection
{
$filterGroup = new FilterGroupCollection();
$filterGroup->setLogicOperator($this->getLogicOperator());
foreach ($this->data as $filter) {
/** @var Filter $filter */
$field = $filter->field()->value();
if (false === in_array($field, $excludeFilter, true)
&& $range === in_array($filter->operator(), [
FilterOperator::LT,
FilterOperator::LTE,
FilterOperator::GT,
FilterOperator::GTE
], true)
) {
$filterGroup->add($filter);
}
}
return $filterGroup;
}
}
<?php
namespace IQDEV\ElasticSearch\Criteria\Filter\Collection;
class PostFilterCollection extends FilterCollection
{
}
\ No newline at end of file
<?php
namespace IQDEV\ElasticSearch\Criteria\Filter\Collection;
class QueryFilterCollection extends FilterCollection
{
}
\ No newline at end of file
<?php
namespace IQDEV\ElasticSearch\Criteria\Filter;
final class Field
{
public function __construct(
protected string $value
) {
}
public function value(): string
{
return $this->value;
}
}
<?php
namespace IQDEV\ElasticSearch\Criteria\Filter;
final class Filter
{
public function __construct(
private Field $field,
private FilterOperator $operator,
private FilterValue $value
) {
}
public function field(): Field
{
return $this->field;
}
public function operator(): FilterOperator
{
return $this->operator;
}
public function value(): FilterValue
{
return $this->value;
}
}
<?php
namespace IQDEV\ElasticSearch\Criteria\Filter;
enum FilterOperator: string
{
case EQ = 'eq';
case NE = 'ne';
case GT = 'gt';
case LT = 'lt';
case GTE = 'gte';
case LTE = 'lte';
case CONTAINS = 'contains';
case NOT_CONTAINS = 'not_contains';
}
<?php
namespace IQDEV\ElasticSearch\Criteria\Filter;
enum FilterType
{
case POST;
case QUERY;
}
<?php
namespace IQDEV\ElasticSearch\Criteria\Filter;
interface FilterValue
{
public function value();
}
<?php
namespace IQDEV\ElasticSearch\Criteria\Filter;
enum LogicOperator: string
{
case AND = 'and';
case OR = 'or';
case NOT = 'not';
}
<?php
namespace IQDEV\ElasticSearch\Criteria\Filter\Value;
use IQDEV\ElasticSearch\Criteria\Filter\FilterValue;
class FilterKeyword implements FilterValue
{
/**
* @param string|string[]|bool $value
*/
public function __construct(
public string|array|bool $value
) {
}
/**
* @return string|string[]|bool
*/
public function value(): string|array|bool
{
return $this->value;
}
}
<?php
namespace IQDEV\ElasticSearch\Criteria\Filter\Value;
use IQDEV\ElasticSearch\Criteria\Filter\FilterValue;
class FilterNumber implements FilterValue
{
/**
* @param float $value
*/
public function __construct(
public float $value
) {
}
public function value(): float
{
return $this->value;
}
}