<?php namespace IQDEV\ElasticSearch\Converter; use IQDEV\ElasticSearch\Config\MappingValidator; use IQDEV\ElasticSearch\Configuration; use IQDEV\ElasticSearch\Criteria; use IQDEV\ElasticSearch\Document\Property\PropertyType; use IQDEV\ElasticSearch\Request\Filter\Collection\FilterCollection; use IQDEV\ElasticSearch\Request\Filter\Collection\FilterGroupCollection; use IQDEV\ElasticSearch\Request\Filter\Collection\PostFilterCollection; use IQDEV\ElasticSearch\Request\Filter\Collection\QueryFilterCollection; use IQDEV\ElasticSearch\Request\Filter\Filter; use IQDEV\ElasticSearch\Request\Filter\FilterOperator; use IQDEV\ElasticSearch\Request\Filter\FilterType; use IQDEV\ElasticSearch\Request\Filter\LogicOperator; use IQDEV\ElasticSearch\Request\Filter\Value\FilterKeyword; use IQDEV\ElasticSearch\Request\Filter\Value\FilterNumber; use IQDEV\ElasticSearch\Request\Order\Order; use IQDEV\ElasticSearch\Request\Search\Search; use IQDEV\ElasticSearch\Request\Search\SearchQuery; 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\Stats; use IQDEV\ElasticSearch\Search\BoolQuery\Terms; use IQDEV\ElasticSearch\Search\Nested; use IQDEV\ElasticSearch\Search\Pagination; use IQDEV\ElasticSearch\Search\Request; final class CriteriaToEsRequest { public function __construct( private readonly Configuration $configuration, ) { } public function fromCriteria(Criteria $criteria): Request { $request = new Request(); $request = $this->pagination($request, $criteria); $request = $this->order($request, $criteria); $request = $this->search($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->getPagination()->limit, $criteria->getPagination()->offset)); return $request; } private function order(Request $request, Criteria $criteria): Request { $request = clone $request; if (true === $criteria->getSorting()->isEmpty()) { return $request; } foreach ($criteria->getSorting() as $order) { /** @var Order $order */ $request->getSort()->add($order); } return $request; } private function search(Request $request, Criteria $criteria): Request { $request = clone $request; foreach ($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)); } } return $request; } private function filter(Request $request, Criteria $criteria): Request { $request = clone $request; if ($criteria->getFilters()->isEmpty()) { return $request; } $queryFilters = $criteria->getFilters()->getFilterCollectionByType(FilterType::QUERY); $postFilters = $criteria->getFilters()->getFilterCollectionByType(FilterType::POST); $this->addFilterToRequest($request, $queryFilters); $this->addPostFilterToRequest($request, $postFilters); return $request; } private function separatePropertyTypes(FilterCollection $filterCollection): array { $propertyFilter = new FilterCollection(); $nestedFilter = new FilterCollection(); 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]; } private function addFilterToRequest(Request $request, QueryFilterCollection $filterCollection): void { [$propertyFilterCollection, $nestedFilterCollection] = $this->separatePropertyTypes($filterCollection); $this->addPropertyFilterToRequest($request, $propertyFilterCollection); $this->addNestedFilterToRequest($request, $nestedFilterCollection); } private function addPostFilterToRequest(Request $request, PostFilterCollection $filterCollection): void { [$propertyFilterCollection, $nestedFilterCollection] = $this->separatePropertyTypes($filterCollection); $this->addPropertyPostFilterToRequest($request, $propertyFilterCollection); $this->addNestedPostFilterToRequest($request, $nestedFilterCollection); } private function addPropertyFilterToRequest(Request $request, FilterCollection $filterCollection): void { $keywordFilterCollection = $this->getKeywordFilter($filterCollection); foreach ($keywordFilterCollection->getFilter() as $filter) { $request->getQuery()->getFilter()->add($filter); } foreach ($keywordFilterCollection->getShould() as $should) { $request->getQuery()->getShould()->add($should); } $numberFilterCollection = $this->getNumberFilter($filterCollection); foreach ($numberFilterCollection->getFilter() as $filter) { $request->getQuery()->getFilter()->add($filter); } foreach ($numberFilterCollection->getShould() as $should) { $request->getQuery()->getShould()->add($should); } } private function addPropertyPostFilterToRequest(Request $request, FilterCollection $filterCollection): void { $keywordFilterCollection = $this->getKeywordFilter($filterCollection); foreach ($keywordFilterCollection->getFilter() as $filter) { $request->getPostFilter()->getFilter()->add($filter); } foreach ($keywordFilterCollection->getShould() as $should) { $request->getPostFilter()->getShould()->add($should); } $numberFilterCollection = $this->getNumberFilter($filterCollection); foreach ($numberFilterCollection->getFilter() as $filter) { $request->getPostFilter()->getFilter()->add($filter); } foreach ($numberFilterCollection->getShould() as $should) { $request->getPostFilter()->getShould()->add($should); } } private function addNestedFilterToRequest(Request $request, FilterCollection $filterCollection): void { $keywordFilterCollection = $this->getKeywordFilter($filterCollection); $keywordFilter = new Nested(); $keywordFilter->setPath('search_data'); if (false === $keywordFilterCollection->isEmpty()) { $keywordNestedFilterQuery = clone $keywordFilter; $keywordNestedFilterQuery->setQuery($keywordFilterCollection); $request->getQuery()->getFilter()->add($keywordNestedFilterQuery); } $numberFilterCollection = $this->getNumberFilter($filterCollection); $numberFilter = new Nested(); $numberFilter->setPath('search_data'); if (false === $numberFilterCollection->isEmpty()) { $numberNestedFilterQuery = clone $numberFilter; $numberNestedFilterQuery->setQuery($numberFilterCollection); $request->getQuery()->getFilter()->add($numberNestedFilterQuery); } } private function addNestedPostFilterToRequest(Request $request, FilterCollection $postFilter): void { $keywordFilterCollection = $this->getKeywordFilter($postFilter); $keywordFilter = new Nested(); $keywordFilter->setPath('search_data'); if (false === $keywordFilterCollection->isEmpty()) { $keywordNestedFilterQuery = clone $keywordFilter; $keywordNestedFilterQuery->setQuery($keywordFilterCollection); $request->getPostFilter()->getFilter()->add($keywordNestedFilterQuery); } $numberFilterCollection = $this->getNumberFilter($postFilter); $numberFilter = new Nested(); $numberFilter->setPath('search_data'); if (false === $numberFilterCollection->isEmpty()) { $numberNestedFilterQuery = clone $numberFilter; $numberNestedFilterQuery->setQuery($numberFilterCollection); $request->getPostFilter()->getFilter()->add($numberNestedFilterQuery); } } 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->getLogicOperator() === 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(), [FilterOperator::LT, FilterOperator::LTE], true)) { $ranges[$group][$field][$filter->operator()->value] = $value; continue; } if (in_array($filter->operator(), [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) { $isProperty = MappingValidator::isPropertyExists($this->configuration, $field); $facet = $isProperty ? new Stats($field, $range) : new FilterNumberFacet( $field, $range ); if ($iGroup === 0) { $numberFilter->getFilter()->add($facet); } else { $numberFilter->getShould()->add($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->getLogicOperator() === LogicOperator::OR; foreach ($filterGroup as $filter) { /** @var Filter $filter */ $value = $filter->value()->value(); $field = $filter->field()->value(); $isProperty = MappingValidator::isPropertyExists($this->configuration, $field); if (in_array($field, $excludeFilter, true)) { continue; } if (in_array($filter->operator(), [FilterOperator::LT, FilterOperator::LTE], true)) { continue; } if (in_array($filter->operator(), [FilterOperator::GT, FilterOperator::GTE], true)) { continue; } if (is_array($value)) { $value = array_map(static fn($v) => (string)$v, $value); } else { $value = (string)$value; } if ($should) { $keywordFilter->getShould()->add($isProperty ? new Terms($field, $value) : new FilterKeywordFacet($field, $value)); } else { $keywordFilter->getFilter()->add($isProperty ? new Terms($field, $value) : new FilterKeywordFacet($field, $value)); } } } return $keywordFilter; } private function aggs(Request $request, Criteria $criteria): Request { $request = clone $request; if ($criteria->getFilters()->isEmpty() && $criteria->getSearch()->isEmpty()) { return $request; } $request->getAggs()->add( AggsFacetTerms::create( 'keyword_facet', 'keyword_facet' ) ); $request->getAggs()->add( AggsFacetStats::create( 'number_facet', 'number_facet' ) ); $postFilters = $criteria->getFilters()->getFilterCollectionByType(FilterType::POST); $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(), [], true)) { 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->getFilter()->add($nestedFilterKeyword); } if (false === $numberFilter->isEmpty()) { $nestedFilterNumber = new Nested(); $nestedFilterNumber->setPath('search_data') ->setQuery($numberFilter); $queryKeywordFiltered->getFilter()->add($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->getFilter()->add($nestedFilterKeyword); $queryNumberFiltered->getFilter()->add($nestedFilterKeyword); } if (false === $numberFilter->isEmpty()) { $nestedFilterNumber = new Nested(); $nestedFilterNumber->setPath('search_data') ->setQuery($numberFilter); $queryKeywordFiltered->getFilter()->add($nestedFilterNumber); $queryNumberFiltered->getFilter()->add($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; } }