Skip to content
Snippets Groups Projects
CriteriaToEsRequest.php 18.3 KiB
Newer Older
Pavel's avatar
Pavel committed
<?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;
Pavel's avatar
Pavel committed
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;
Pavel's avatar
Pavel committed
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,
    ) {
    }

Pavel's avatar
Pavel committed
    public function fromCriteria(Criteria $criteria): Request
    {
        $request = new Request();
Pavel's avatar
Pavel committed
        $request = $this->pagination($request, $criteria);
        $request = $this->order($request, $criteria);
        $request = $this->search($request, $criteria);
Pavel's avatar
Pavel committed
        $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));
Pavel's avatar
Pavel committed

        return $request;
    }

    private function order(Request $request, Criteria $criteria): Request
    {
        $request = clone $request;

        if (true === $criteria->getSorting()->isEmpty()) {
Pavel's avatar
Pavel committed
            return $request;
        }

        foreach ($criteria->getSorting() as $order) {
Pavel's avatar
Pavel committed
            /** @var Order $order */
            $request->getSort()->add($order);
Pavel's avatar
Pavel committed
        }

        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;
    }

Pavel's avatar
Pavel committed
    private function filter(Request $request, Criteria $criteria): Request
    {
        $request = clone $request;
        if ($criteria->getFilters()->isEmpty()) {
Pavel's avatar
Pavel committed
            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);
Pavel's avatar
Pavel committed
                }
Pavel's avatar
Pavel committed
            }

            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);
Pavel's avatar
Pavel committed
        }
    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
Pavel's avatar
Pavel committed
    {
        $numberFilter = new Query();

        if ($filterCollection->isEmpty()) {
            return $numberFilter;
        }
Pavel's avatar
Pavel committed
        $ranges = [];

        foreach ($filterCollection as $filterGroup) {
Pavel's avatar
Pavel committed
            /** @var FilterGroupCollection $filterGroup */
            if ($filterGroup->isEmpty()) {
Pavel's avatar
Pavel committed
                continue;
            }
            $group = $filterGroup->getLogicOperator() === LogicOperator::OR ? count($ranges) + 1 : 0;
Pavel's avatar
Pavel committed
            if (!isset($ranges[$group])) {
                $ranges[$group] = [];
Pavel's avatar
Pavel committed
            }
Pavel's avatar
Pavel committed
            foreach ($filterGroup as $filter) {
                /** @var Filter $filter */
                $value = $filter->value()->value();
                $field = $filter->field()->value();
Pavel's avatar
Pavel committed
                if (in_array($field, $excludeFilter, true)) {
                    continue;
                }
                if (in_array($filter->operator(), [FilterOperator::LT, FilterOperator::LTE], true)) {
                    $ranges[$group][$field][$filter->operator()->value] = $value;
Pavel's avatar
Pavel committed
                    continue;
                }

                if (in_array($filter->operator(), [FilterOperator::GT, FilterOperator::GTE], true)) {
                    $ranges[$group][$field][$filter->operator()->value] = $value;
Pavel's avatar
Pavel committed
                }
Pavel's avatar
Pavel committed
            }
        }

        if (false === empty($ranges)) {
Pavel's avatar
Pavel committed
            foreach ($ranges as $iGroup => $group) {
                foreach ($group as $field => $range) {
                    $isProperty = MappingValidator::isPropertyExists($this->configuration, $field);
                    $facet = $isProperty ? new Stats($field, $range) : new FilterNumberFacet(
Pavel's avatar
Pavel committed
                        $field,
                        $range
Pavel's avatar
Pavel committed
                    );

                    if ($iGroup === 0) {
                        $numberFilter->getFilter()->add($facet);
Pavel's avatar
Pavel committed
                    } else {
                        $numberFilter->getShould()->add($facet);
Pavel's avatar
Pavel committed
            }
        }

        return $numberFilter;
    }

    private function getKeywordFilter(FilterCollection $filterCollection, array $excludeFilter = []): Query
Pavel's avatar
Pavel committed
    {
        $keywordFilter = new Query();

        if ($filterCollection->isEmpty()) {
            return $keywordFilter;
        }

        foreach ($filterCollection as $filterGroup) {
Pavel's avatar
Pavel committed
            /** @var FilterGroupCollection $filterGroup */
            if ($filterGroup->isEmpty()) {
                continue;
            }
            $should = $filterGroup->getLogicOperator() === LogicOperator::OR;
Pavel's avatar
Pavel committed
            foreach ($filterGroup as $filter) {
                /** @var Filter $filter */
                $value = $filter->value()->value();
                $field = $filter->field()->value();
                $isProperty = MappingValidator::isPropertyExists($this->configuration, $field);
Pavel's avatar
Pavel committed

                if (in_array($field, $excludeFilter, true)) {
                    continue;
                }
                if (in_array($filter->operator(), [FilterOperator::LT, FilterOperator::LTE], true)) {
Pavel's avatar
Pavel committed
                    continue;
                }
                if (in_array($filter->operator(), [FilterOperator::GT, FilterOperator::GTE], true)) {
Pavel's avatar
Pavel committed
                    continue;
                }
Pavel's avatar
Pavel committed
                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));
Pavel's avatar
Pavel committed
            }
        }

        return $keywordFilter;
    }

    private function aggs(Request $request, Criteria $criteria): Request
    {
        $request = clone $request;

        if ($criteria->getFilters()->isEmpty() && $criteria->getSearch()->isEmpty()) {
Pavel's avatar
Pavel committed
            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);
Pavel's avatar
Pavel committed
        $getKey = static fn(string $type, string $name): string => sprintf('%s_facet_%s', $type, $name);
        foreach ($postFilters as $filterGroup) {
Pavel's avatar
Pavel committed
            /** @var FilterGroupCollection $filterGroup */
            foreach ($filterGroup as $filter) {
                /** @var Filter $filter */
                $field = $filter->field()->value();

                if ($filter->value() instanceof FilterNumber) {
Pavel's avatar
Pavel committed
                    continue;
                }

                if (in_array($filter->operator(), [], true)) {
Pavel's avatar
Pavel committed
                    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);
                }
Pavel's avatar
Pavel committed
            }
        $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;
Pavel's avatar
Pavel committed
}