Skip to content
Commits on Source (11)
<?php
namespace IQDEV\ElasticSearch\Converter;
use IQDEV\ElasticSearch\Filter\PostFilterCollection;
use IQDEV\ElasticSearch\Filter\QueryFilterCollection;
use IQDEV\ElasticSearch\Order\OrderAscType;
use IQDEV\ElasticSearch\Order\OrderDescType;
use IQDEV\ElasticSearch\Order\OrderField;
use IQDEV\ElasticSearch\Order\OrderKeywordProperty;
use IQDEV\ElasticSearch\Order\OrderNumberProperty;
use IQDEV\ElasticSearch\Search\Aggs\Aggs;
use IQDEV\ElasticSearch\Search\Aggs\AggsFacetStats;
use IQDEV\ElasticSearch\Search\Aggs\AggsFacetTerms;
use IQDEV\ElasticSearch\Search\BoolQuery\FilterKeywordFacet;
use IQDEV\ElasticSearch\Search\BoolQuery\FilterNumberFacet;
use IQDEV\ElasticSearch\Search\BoolQuery\Query;
use IQDEV\ElasticSearch\Search\BoolQuery\Terms;
use IQDEV\ElasticSearch\Search\Nested;
use IQDEV\ElasticSearch\Search\Pagination;
use IQDEV\ElasticSearch\Search\Request;
use IQDEV\Search\Criteria;
use IQDEV\Search\Document\Property\AttrType;
use IQDEV\Search\Document\Property\PropertyType;
use IQDEV\Search\FIlter\Filter;
use IQDEV\Search\Filter\FilterCollection;
use IQDEV\Search\Filter\FilterGroupCollection;
use IQDEV\Search\Filter\FilterKeyword;
use IQDEV\Search\Filter\FilterNumber;
use IQDEV\Search\Filter\FilterOperator;
use IQDEV\Search\Filter\FilterType;
use IQDEV\Search\Filter\LogicOperator;
use IQDEV\Search\Order\Order;
use IQDEV\Search\Order\OrderAscType as SOrderAscType;
use IQDEV\Search\Order\OrderDescType as SOrderDescType;
final class CriteriaToEsRequest
{
public function fromCriteria(Criteria $criteria): Request
{
$request = new Request();
$request = $this->pagination($request, $criteria);
$request = $this->order($request, $criteria);
$request = $this->filter($request, $criteria);
$request = $this->aggs($request, $criteria);
return $request;
}
private function pagination(Request $request, Criteria $criteria): Request
{
$request = clone $request;
$request->setPagination(new Pagination($criteria->pagination()->limit, $criteria->pagination()->offset));
return $request;
}
private function order(Request $request, Criteria $criteria): Request
{
$request = clone $request;
if (true === $criteria->sorting()->isEmpty()) {
return $request;
}
foreach ($criteria->sorting() as $order) {
/** @var Order $order */
$direction = null;
if ($order->orderType() instanceof SOrderAscType) {
$direction = new OrderAscType();
}
if ($order->orderType() instanceof SOrderDescType) {
$direction = new OrderDescType();
}
if ($order->orderBy() instanceof AttrType) {
$request->getSort()->add(new OrderField($order->orderBy()->key(), $direction));
} elseif ($order->orderBy() instanceof PropertyType) {
if ($order->orderBy()->type() === PropertyType::TYPE_KEYWORD) {
$request->getSort()->add(new OrderKeywordProperty($order->orderBy()->key(), $direction));
} elseif ($order->orderBy()->type() === PropertyType::TYPE_NUMBER) {
$request->getSort()->add(new OrderNumberProperty($order->orderBy()->key(), $direction));
}
}
}
return $request;
}
private function filter(Request $request, Criteria $criteria): Request
{
$request = clone $request;
if ($criteria->filters()->isEmpty()) {
return $request;
}
foreach ($criteria->filters() as $filterGroup) {
/** @var FilterGroupCollection $filterGroup */
foreach ($filterGroup as $filter) {
/** @var Filter $filter */
$value = $filter->value()->value();
$field = $filter->field()->value();
if ('search' === $field) {
if ($filter->operator()->value() === FilterOperator::CONTAINS) {
$request->addMatch(
'suggest_search_content',
[
'query' => $value,
],
);
} else {
$request->addMatch(
'full_search_content',
[
'query' => $value,
],
);
}
continue;
}
if ('category_id' === $field) {
$request->getQuery()->must(
new Terms('category_id', $filter->value()->value())
);
continue;
}
}
}
[$queryFilters, $postFilters] = $this->groupFilters($criteria->filters());
$keywordQueryFilter = $this->getKeywordFilter($queryFilters);
$keywordPostFilter = $this->getKeywordFilter($postFilters);
if (false === $keywordQueryFilter->isEmpty() || false === $keywordPostFilter->isEmpty()) {
$keywordNestedFilter = new Nested();
$keywordNestedFilter->setPath('search_data');
if (false === $keywordQueryFilter->isEmpty()) {
$keywordNestedFilterQuery = clone $keywordNestedFilter;
$keywordNestedFilterQuery->setQuery($keywordQueryFilter);
$request->getQuery()->filter($keywordNestedFilterQuery);
}
if (false === $keywordPostFilter->isEmpty()) {
$keywordNestedFilterPost = clone $keywordNestedFilter;
$keywordNestedFilterPost->setQuery($keywordPostFilter);
$request->getPostFilter()->filter($keywordNestedFilterPost);
}
}
$numberQueryFilter = $this->getNumberFilter($queryFilters);
$numberPostFilter = $this->getNumberFilter($postFilters);
if (false === $numberQueryFilter->isEmpty() || false === $numberPostFilter->isEmpty()) {
$numberNestedFilter = new Nested();
$numberNestedFilter->setPath('search_data');
if (false === $numberQueryFilter->isEmpty()) {
$numberNestedFilterQuery = clone $numberNestedFilter;
$numberNestedFilterQuery->setQuery($numberQueryFilter);
$request->getQuery()->filter($numberNestedFilterQuery);
}
if (false === $numberPostFilter->isEmpty()) {
$numberNestedFilterPost = clone $numberNestedFilter;
$numberNestedFilterPost->setQuery($numberPostFilter);
$request->getPostFilter()->filter($numberNestedFilterPost);
}
}
return $request;
}
private function getNumberFilter(FilterCollection $filterCollection, array $excludeFilter = []): Query
{
$numberFilter = new Query();
if ($filterCollection->isEmpty()) {
return $numberFilter;
}
$ranges = [];
foreach ($filterCollection as $filterGroup) {
/** @var FilterGroupCollection $filterGroup */
if ($filterGroup->isEmpty()) {
continue;
}
$group = $filterGroup->getLogicalType()->value() === LogicOperator::OR ? count($ranges) + 1 : 0;
if (!isset($ranges[$group])) {
$ranges[$group] = [];
}
foreach ($filterGroup as $filter) {
/** @var Filter $filter */
$value = $filter->value()->value();
$field = $filter->field()->value();
if (in_array($field, $excludeFilter, true)) {
continue;
}
if (in_array($filter->operator()->value(), [FilterOperator::LT, FilterOperator::LTE], true)) {
$ranges[$group][$field][$filter->operator()->value()] = $value;
continue;
}
if (in_array($filter->operator()->value(), [FilterOperator::GT, FilterOperator::GTE], true)) {
$ranges[$group][$field][$filter->operator()->value()] = $value;
}
}
}
if (false === empty($ranges)) {
foreach ($ranges as $iGroup => $group) {
foreach ($group as $field => $range) {
$facet = new FilterNumberFacet(
$field,
$range
);
if ($iGroup === 0) {
$numberFilter->filter($facet);
} else {
$numberFilter->should($facet);
}
}
}
}
return $numberFilter;
}
private function getKeywordFilter(FilterCollection $filterCollection, array $excludeFilter = []): Query
{
$keywordFilter = new Query();
if ($filterCollection->isEmpty()) {
return $keywordFilter;
}
foreach ($filterCollection as $filterGroup) {
/** @var FilterGroupCollection $filterGroup */
if ($filterGroup->isEmpty()) {
continue;
}
$should = $filterGroup->getLogicalType()->value() === LogicOperator::OR;
foreach ($filterGroup as $filter) {
/** @var Filter $filter */
$value = $filter->value()->value();
$field = $filter->field()->value();
if (in_array($field, $excludeFilter, true)) {
continue;
}
if (in_array($filter->operator()->value(), [FilterOperator::LT, FilterOperator::LTE], true)) {
continue;
}
if (in_array($filter->operator()->value(), [FilterOperator::GT, FilterOperator::GTE], true)) {
continue;
}
if ('search' === $field) {
continue;
}
if ('category_id' === $field) {
continue;
}
if (is_array($value)) {
$value = array_map(static fn($v) => (string)$v, $value);
} else {
$value = (string)$value;
}
if ($should) {
$keywordFilter->should(new FilterKeywordFacet($field, $value));
} else {
$keywordFilter->filter(new FilterKeywordFacet($field, $value));
}
}
}
return $keywordFilter;
}
private function aggs(Request $request, Criteria $criteria): Request
{
$request = clone $request;
if ($criteria->filters()->isEmpty()) {
return $request;
}
$request->getAggs()->add(
AggsFacetTerms::create(
'keyword_facet',
'keyword_facet'
)
);
$request->getAggs()->add(
AggsFacetStats::create(
'number_facet',
'number_facet'
)
);
[$queryFilters, $postFilters] = $this->groupFilters($criteria->filters());
$getKey = static fn(string $type, string $name): string => sprintf('%s_facet_%s', $type, $name);
foreach ($postFilters as $filterGroup) {
/** @var FilterGroupCollection $filterGroup */
foreach ($filterGroup as $filter) {
/** @var Filter $filter */
$field = $filter->field()->value();
if ($filter->value() instanceof FilterNumber) {
continue;
}
if (in_array($filter->operator()->value(), [], true)) {
continue;
}
if ('search' === $field) {
continue;
}
if ('category_id' === $field) {
continue;
}
if ($filter->value() instanceof FilterKeyword) {
$aggsFiltered = new Aggs($getKey('keyword', $field));
$aggsFiltered->addAggs(
AggsFacetTerms::create(
'agg_special',
'keyword_facet'
)
);
$queryKeywordFiltered = new Query();
$keywordFilter = $this->getKeywordFilter($postFilters, [$field]);
$numberFilter = $this->getNumberFilter($postFilters);
if (false === $keywordFilter->isEmpty()) {
$nestedFilterKeyword = new Nested();
$nestedFilterKeyword->setPath('search_data')
->setQuery($keywordFilter);
$queryKeywordFiltered->filter($nestedFilterKeyword);
}
if (false === $numberFilter->isEmpty()) {
$nestedFilterNumber = new Nested();
$nestedFilterNumber->setPath('search_data')
->setQuery($numberFilter);
$queryKeywordFiltered->filter($nestedFilterNumber);
}
if ($queryKeywordFiltered->isEmpty() === false) {
$aggsFiltered->setQuery($queryKeywordFiltered);
} else {
$aggsFiltered->setNested((new Nested())->setPath('search_data'));
}
$request->getAggs()->add($aggsFiltered);
}
}
}
$keywordFilter = $this->getKeywordFilter($postFilters);
$numberFilter = $this->getNumberFilter($postFilters);
$aggsKeywordFiltered = new Aggs('keyword_facet_filtered');
$aggsKeywordFiltered->addAggs(
AggsFacetTerms::create(
'all_keyword_facet_filtered',
'keyword_facet'
)
);
$queryKeywordFiltered = new Query();
$aggsNumberFiltered = new Aggs('number_facet_filtered');
$aggsNumberFiltered->addAggs(
AggsFacetStats::create(
'all_number_facet_filtered',
'number_facet'
)
);
$queryNumberFiltered = new Query();
if (false === $keywordFilter->isEmpty()) {
$nestedFilterKeyword = new Nested();
$nestedFilterKeyword->setPath('search_data')
->setQuery($keywordFilter);
$queryKeywordFiltered->filter($nestedFilterKeyword);
$queryNumberFiltered->filter($nestedFilterKeyword);
}
if (false === $numberFilter->isEmpty()) {
$nestedFilterNumber = new Nested();
$nestedFilterNumber->setPath('search_data')
->setQuery($numberFilter);
$queryKeywordFiltered->filter($nestedFilterNumber);
$queryNumberFiltered->filter($nestedFilterNumber);
}
if (false === $queryKeywordFiltered->isEmpty()) {
$aggsKeywordFiltered->setQuery($queryKeywordFiltered);
} else {
$aggsKeywordFiltered->setNested((new Nested())->setPath('search_data'));
}
if (false === $queryNumberFiltered->isEmpty()) {
$aggsNumberFiltered->setQuery($queryNumberFiltered);
} else {
$aggsNumberFiltered->setNested((new Nested())->setPath('search_data'));
}
$request->getAggs()
->add($aggsKeywordFiltered)
->add($aggsNumberFiltered);
return $request;
}
/**
* @param FilterCollection $filters
* @return FilterCollection[]
*/
private function groupFilters(FilterCollection $filters): array
{
$queryFilters = new QueryFilterCollection();
$postFilters = new PostFilterCollection();
foreach ($filters as $filterGroup) {
/** @var FilterGroupCollection $filterGroup */
if ($filterGroup->isEmpty()) {
continue;
}
switch ($filterGroup->getFilterType()->value()) {
case FilterType::QUERY:
$queryFilters->add($filterGroup);
break;
case FilterType::POST:
$postFilters->add($filterGroup);
break;
}
}
return [$queryFilters, $postFilters];
}
}
<?php
declare(strict_types=1);
namespace IQDEV\ElasticSearch\Converter\Request\Aggregation;
use IQDEV\ElasticSearch\Configuration;
use IQDEV\ElasticSearch\Criteria\Criteria;
use IQDEV\ElasticSearch\Criteria\Filter\FilterType;
use IQDEV\ElasticSearch\Search\Aggs\AggsCollection;
use IQDEV\ElasticSearch\Search\Aggs\AggsFacetStats;
use IQDEV\ElasticSearch\Search\Aggs\AggsFacetTerms;
class Aggregation
{
public function __construct(
private readonly Configuration $configuration,
private readonly Criteria $criteria,
private readonly AggsCollection $aggregations = new AggsCollection(),
) {
$this->convertToQuery();
}
public function convertToQuery(): void
{
$this->aggregations->add(
AggsFacetTerms::create(
'keyword_facet',
'keyword_facet'
)
);
$this->aggregations->add(
AggsFacetStats::create(
'number_facet',
'number_facet'
)
);
$postFilterCollection = $this->criteria->getFilters()->getFilterCollectionByType(FilterType::POST);
$filterAggregation = new FilterAggregation($this->configuration, $postFilterCollection);
$filterAggregation->updateRequestAggregation($this->aggregations);
$fullAggregation = new FullAggregation($this->configuration, $postFilterCollection);
$fullAggregation->updateRequestAggregation($this->aggregations);
}
public function getAggregation(): AggsCollection
{
return $this->aggregations;
}
}
<?php
declare(strict_types=1);
namespace IQDEV\ElasticSearch\Converter\Request\Aggregation;
use IQDEV\ElasticSearch\Configuration;
use IQDEV\ElasticSearch\Converter\Request\FilterQuery;
use IQDEV\ElasticSearch\Criteria\Filter\Collection\FilterCollection;
use IQDEV\ElasticSearch\Criteria\Filter\Collection\FilterGroupCollection;
use IQDEV\ElasticSearch\Criteria\Filter\Filter;
use IQDEV\ElasticSearch\Search\Aggs\Aggs;
use IQDEV\ElasticSearch\Search\Aggs\AggsCollection;
use IQDEV\ElasticSearch\Search\Aggs\AggsFacetTerms;
use IQDEV\ElasticSearch\Search\Nested;
class FilterAggregation
{
private ?Aggs $aggregation = null;
public function __construct(
private readonly Configuration $configuration,
private readonly FilterCollection $filterCollection,
) {
}
public function updateRequestAggregation(AggsCollection $original): void
{
foreach ($this->filterCollection as $filterGroup) {
/** @var FilterGroupCollection $filterGroup */
foreach ($filterGroup as $filter) {
/** @var Filter $filter */
$field = $filter->field()->value();
$aggregation = new Aggs($this->getKey('keyword', $field));
$aggregation->addAggs(
AggsFacetTerms::create(
'agg_special',
'keyword_facet'
)
);
$queryFilterBuilder = new FilterQuery($this->configuration, $this->filterCollection, [$field]);
$query = $queryFilterBuilder->getQuery();
if (false === $query->isEmpty()) {
$aggregation->setQuery($query);
} else {
$aggregation->setNested((new Nested())->setPath('search_data'));
}
$original->add($aggregation);
}
}
}
private function getKey(string $type, string $name): string
{
return sprintf('%s_facet_%s', $type, $name);
}
}
<?php
declare(strict_types=1);
namespace IQDEV\ElasticSearch\Converter\Request\Aggregation;
use IQDEV\ElasticSearch\Configuration;
use IQDEV\ElasticSearch\Converter\Request\FilterQuery;
use IQDEV\ElasticSearch\Criteria\Filter\Collection\FilterCollection;
use IQDEV\ElasticSearch\Search\Aggs\Aggs;
use IQDEV\ElasticSearch\Search\Aggs\AggsCollection;
use IQDEV\ElasticSearch\Search\Aggs\AggsFacetStats;
use IQDEV\ElasticSearch\Search\Aggs\AggsFacetTerms;
use IQDEV\ElasticSearch\Search\Nested;
class FullAggregation
{
public function __construct(
private readonly Configuration $configuration,
private readonly FilterCollection $filterCollection,
) {
}
public function updateRequestAggregation(AggsCollection $original): void
{
$keywordQuery = (new FilterQuery($this->configuration, $this->filterCollection->getKeywordFilters()))->getQuery();
$numberQuery = (new FilterQuery($this->configuration, $this->filterCollection->getNumberFilters()))->getQuery();
$aggsKeywordFiltered = new Aggs('keyword_facet_filtered');
$aggsKeywordFiltered->addAggs(
AggsFacetTerms::create(
'all_keyword_facet_filtered',
'keyword_facet'
)
);
$aggsNumberFiltered = new Aggs('number_facet_filtered');
$aggsNumberFiltered->addAggs(
AggsFacetStats::create(
'all_number_facet_filtered',
'number_facet'
)
);
if (false === $keywordQuery->getFilter()->isEmpty()) {
foreach ($keywordQuery->getFilter() as $item) {
$numberQuery->getFilter()->add($item);
}
}
if (false === $numberQuery->getFilter()->isEmpty()) {
foreach ($numberQuery->getFilter() as $item) {
$keywordQuery->getFilter()->add($item);
}
}
if (false === $keywordQuery->getFilter()->isEmpty()) {
$aggsKeywordFiltered->setQuery($keywordQuery);
} else {
$aggsKeywordFiltered->setNested((new Nested())->setPath('search_data'));
}
if (false === $numberQuery->getFilter()->isEmpty()) {
$aggsNumberFiltered->setQuery($numberQuery);
} else {
$aggsNumberFiltered->setNested((new Nested())->setPath('search_data'));
}
$original->add($aggsKeywordFiltered)->add($aggsNumberFiltered);
}
}
<?php
declare(strict_types=1);
namespace IQDEV\ElasticSearch\Converter\Request\Collection;
use IQDEV\ElasticSearch\Criteria\Filter\Collection\FilterCollection;
class NestedFilterCollection extends FilterCollection
{
}
<?php
declare(strict_types=1);
namespace IQDEV\ElasticSearch\Converter\Request\Collection;
use IQDEV\ElasticSearch\Criteria\Filter\Collection\FilterCollection;
class PropertyFilterCollection extends FilterCollection
{
}
<?php
declare(strict_types=1);
namespace IQDEV\ElasticSearch\Converter\Request;
use IQDEV\ElasticSearch\Configuration;
use IQDEV\ElasticSearch\Converter\Request\Aggregation\Aggregation;
use IQDEV\ElasticSearch\Criteria\Criteria;
use IQDEV\ElasticSearch\Criteria\Filter\Collection\PostFilterCollection;
use IQDEV\ElasticSearch\Criteria\Filter\Collection\QueryFilterCollection;
use IQDEV\ElasticSearch\Criteria\Search\Search;
use IQDEV\ElasticSearch\Criteria\Search\SearchQuery;
use IQDEV\ElasticSearch\Document\Property\PropertyType;
use IQDEV\ElasticSearch\Search\Pagination;
class CriteriaRequestBuilder extends RequestBuilder
{
public function __construct(
private readonly Configuration $configuration,
private readonly Criteria $criteria
) {
parent::__construct();
}
public function build(): void
{
$this->setPagination();
if (false === $this->criteria->getSorting()->isEmpty()) {
$this->setSort();
}
if (false === $this->criteria->getSearch()->isEmpty()) {
$this->setSearch();
}
if (false === $this->criteria->getFilters()->isEmpty()) {
$this->setFilter();
}
if (false === $this->criteria->getSearch()->isEmpty() || false === $this->criteria->getFilters()->isEmpty()) {
$this->setAggregations();
}
}
public function setPagination(): void
{
$request = clone $this->request;
$request->setPagination(new Pagination(
$this->criteria->getPagination()->limit,
$this->criteria->getPagination()->offset
));
$this->request = $request;
}
public function setSort(): void
{
$request = clone $this->request;
foreach ($this->criteria->getSorting() as $sort) {
$request->getSort()->add($sort);
}
$this->request = $request;
}
public function setSearch(): void
{
$request = clone $this->request;
foreach ($this->criteria->getSearch() as $search) {
/** @var Search $search */
$searchQuery = new SearchQuery($search);
if ($search->getProperty()->getType() === PropertyType::TEXT) {
$request->getQueryMatch()->add($searchQuery->toQueryMatch());
} else {
$request->getQuery()->getMust()->add($searchQuery->toMust($this->configuration));
}
}
$this->request = $request;
}
public function setFilter(): void
{
$queryFilterCollection = $this->criteria->getFilters()->getQueryFilterCollection();
if (false === $queryFilterCollection->isEmpty()) {
$this->setQueryFilter($queryFilterCollection);
}
$postFilterCollection = $this->criteria->getFilters()->getPostFilterCollection();
if (false === $postFilterCollection->isEmpty()) {
$this->setPostFilter($postFilterCollection);
}
}
private function setQueryFilter(QueryFilterCollection $filterCollection): void
{
$request = clone $this->request;
$filterQuery = new FilterQuery($this->configuration, $filterCollection);
$request->getQuery()->modify($filterQuery->getQuery());
$this->request = $request;
}
private function setPostFilter(PostFilterCollection $filterCollection): void
{
$request = clone $this->request;
$filterQuery = new FilterQuery($this->configuration, $filterCollection);
$request->getPostFilter()->modify($filterQuery->getQuery());
$this->request = $request;
}
public function setAggregations(): void
{
$request = clone $this->request;
$aggregation = new Aggregation($this->configuration, $this->criteria);
$request->setAggs($aggregation->getAggregation());
$this->request = $request;
}
}
<?php
declare(strict_types=1);
namespace IQDEV\ElasticSearch\Converter\Request;
use IQDEV\ElasticSearch\Configuration;
use IQDEV\ElasticSearch\Criteria\Criteria;
use IQDEV\ElasticSearch\Search\Request;
final class CriteriaToRequest
{
public function __construct(
private readonly Configuration $configuration,
) {
}
public function fromCriteria(Criteria $criteria): Request
{
$builder = new CriteriaRequestBuilder($this->configuration, $criteria);
$builder->build();
return $builder->getRequest();
}
}
<?php
declare(strict_types=1);
namespace IQDEV\ElasticSearch\Converter\Request\Filter;
use IQDEV\ElasticSearch\Esable;
use IQDEV\ElasticSearch\Criteria\Filter\Collection\FilterGroupCollection;
use IQDEV\ElasticSearch\Criteria\Filter\LogicOperator;
use IQDEV\ElasticSearch\Search\BoolQuery\Query;
use IQDEV\ElasticSearch\Search\Nested;
abstract class AbstractFilterQuery
{
protected Query $query;
public function __construct() {
$this->query = new Query();
}
protected function setFilterByLogic(Query $query, LogicOperator $logicOperator, Esable $filter): void
{
match ($logicOperator) {
LogicOperator::AND => $query->getFilter()->add($filter),
LogicOperator::OR => $query->getShould()->add($filter),
LogicOperator::NOT => $query->getMustNot()->add($filter),
};
}
abstract public function getQuery(FilterGroupCollection $filterGroupCollection, array $exclude): Query|Nested;
}
<?php
declare(strict_types=1);
namespace IQDEV\ElasticSearch\Converter\Request\Filter;
use IQDEV\ElasticSearch\Converter\Request\Filter\Type\KeywordFilterType;
use IQDEV\ElasticSearch\Converter\Request\Filter\Type\RangeFilterType;
use IQDEV\ElasticSearch\Criteria\Filter\Collection\FilterGroupCollection;
use IQDEV\ElasticSearch\Criteria\Filter\Filter;
use IQDEV\ElasticSearch\Search\BoolQuery\Query;
use IQDEV\ElasticSearch\Search\BoolQuery\Terms;
use IQDEV\ElasticSearch\Search\Nested;
class NestedFilter extends AbstractFilterQuery
{
public const NESTED_RANGE_PATH = 'search_data.number_facet';
public const NESTED_KEYWORD_PATH = 'search_data.keyword_facet';
public function getQuery(FilterGroupCollection $filterGroupCollection, array $exclude): Nested
{
$query = new Query();
$keywordFiltersGroup = $filterGroupCollection->getKeywordFilters($exclude);
foreach ($keywordFiltersGroup as $keywordFilter) {
/** @var Filter $keywordFilter */
$esableFilter = new KeywordFilterType($keywordFilter);
$this->setFilterByLogic($query, $filterGroupCollection->getLogicOperator(), $this->getNested($esableFilter, self::NESTED_KEYWORD_PATH));
}
$rangeFilterGroup = $filterGroupCollection->getRangeFilters($exclude);
$rangesFilter = RangeFilterType::getFiltersByOneProperty($rangeFilterGroup);
foreach ($rangesFilter as $filterGroup) {
/** @var FilterGroupCollection $filterGroup */
$esableFilter = new RangeFilterType($filterGroup);
$this->setFilterByLogic($query, $filterGroupCollection->getLogicOperator(), $this->getNested($esableFilter, self::NESTED_RANGE_PATH));
}
return (new Nested())
->setPath('search_data')
->setQuery($query);
}
private function getNested(RangeFilterType|KeywordFilterType $filter, string $path): Nested
{
$cloneFilter = clone $filter;
$nested = new Nested();
$query = new Query();
$query->getFilter()->add(new Terms($path . '.facet_code', $cloneFilter->getField()));
$cloneFilter->setField($path . '.facet_value');
$query->getFilter()->add($cloneFilter->getEsable());
$nested
->setPath($path)
->setQuery($query);
return $nested;
}
}
<?php
declare(strict_types=1);
namespace IQDEV\ElasticSearch\Converter\Request\Filter;
use IQDEV\ElasticSearch\Converter\Request\Filter\Type\KeywordFilterType;
use IQDEV\ElasticSearch\Converter\Request\Filter\Type\RangeFilterType;
use IQDEV\ElasticSearch\Criteria\Filter\Collection\FilterGroupCollection;
use IQDEV\ElasticSearch\Criteria\Filter\Filter;
use IQDEV\ElasticSearch\Search\BoolQuery\Query;
class PropertyFilter extends AbstractFilterQuery
{
public function getQuery(FilterGroupCollection $filterGroupCollection, array $exclude = []): Query
{
$query = new Query();
$keywordFiltersGroup = $filterGroupCollection->getKeywordFilters($exclude);
foreach ($keywordFiltersGroup as $keywordFilter) {
/** @var Filter $keywordFilter */
$esableFilter = new KeywordFilterType($keywordFilter);
$this->setFilterByLogic($query, $filterGroupCollection->getLogicOperator(), $esableFilter->getEsable());
}
$rangeFilterGroup = $filterGroupCollection->getRangeFilters($exclude);
$rangesFilter = RangeFilterType::getFiltersByOneProperty($rangeFilterGroup);
foreach ($rangesFilter as $filterGroup) {
/** @var FilterGroupCollection $filterGroup */
$esableFilter = new RangeFilterType($filterGroup);
$this->setFilterByLogic($query, $filterGroupCollection->getLogicOperator(), $esableFilter->getEsable());
}
return $query;
}
}
<?php
declare(strict_types=1);
namespace IQDEV\ElasticSearch\Converter\Request\Filter\Type;
class AbstractFilterType
{
protected string $field = '';
public function getField(): string
{
return $this->field;
}
public function setField(string $field): self
{
$this->field = $field;
return $this;
}
}
<?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;
namespace IQDEV\ElasticSearch\Converter\Result;
use Elastic\Elasticsearch\Response\Elasticsearch;
use IQDEV\Search\Document\Product;
use IQDEV\Search\Facet\Facet;
use IQDEV\Search\Facet\Item\FacetItemList;
use IQDEV\Search\Facet\Item\FacetItemRange;
use IQDEV\Search\Facet\Item\FacetItemRangeDTO;
use IQDEV\Search\Facet\Type\FacetListType;
use IQDEV\Search\Facet\Type\FacetRangeType;
use IQDEV\Search\Result;
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
{
......@@ -91,7 +90,7 @@ final class EsResponseToResult
$code = $bucket['key'];
$valueBucket = $bucket['agg_keyword_facet_value']['buckets'];
$facet = new Facet(new FacetListType(), $code);
$facet = new FacetResult(FacetType::LIST, $code);
foreach ($valueBucket as $value) {
$count = 0;
......@@ -127,9 +126,9 @@ final class EsResponseToResult
foreach ($buckets as $bucket) {
$code = $bucket['key'];
$workBucket = $bucket['agg_number_facet_value'];
$selectedBuket = !empty($bucketsFiltered[$code]) ? $bucketsFiltered[$code] : null;
$selectedBucket = !empty($bucketsFiltered[$code]) ? $bucketsFiltered[$code] : null;
$facet = new Facet(new FacetRangeType(), $code);
$facet = new FacetResult(FacetType::RANGE, $code);
$facetItem = FacetItemRange::create(
FacetItemRangeDTO::create(
$workBucket['min'],
......@@ -137,14 +136,14 @@ final class EsResponseToResult
$workBucket['avg'],
$workBucket['sum']
),
$selectedBuket !== null ? FacetItemRangeDTO::create(
$selectedBuket['min'],
$selectedBuket['max'],
$selectedBuket['avg'],
$selectedBuket['sum']
$selectedBucket !== null ? FacetItemRangeDTO::create(
$selectedBucket['min'],
$selectedBucket['max'],
$selectedBucket['avg'],
$selectedBucket['sum']
) : FacetItemRangeDTO::create(),
$selectedBuket ? $selectedBuket['count'] : 0,
$selectedBuket !== null
$selectedBucket ? $selectedBucket['count'] : 0,
$selectedBucket !== null
);
$facet->products->add($facetItem);
......
<?php
namespace IQDEV\ElasticSearch\Criteria;
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;
public function __construct()
{
$this->search = new SearchCollection();
$this->filters = new FilterCollection();
$this->sorting = new OrderCollection();
$this->pagination = new Pagination();
}
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 __clone(): void
{
$this->search = clone $this->search;
$this->filters = clone $this->filters;
$this->sorting = clone $this->sorting;
$this->pagination = clone $this->pagination;
}
}
<?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;
}
}