Skip to content
CriteriaToEsRequest.php 16.4 KiB
Newer Older
Pavel's avatar
Pavel committed
<?php

namespace IQDEV\ElasticSearch\Converter;

use IQDEV\ElasticSearch\Filter\PostFilterCollection;
use IQDEV\ElasticSearch\Filter\QueryFilterCollection;
Pavel's avatar
Pavel committed
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;
Pavel's avatar
Pavel committed
use IQDEV\Search\Filter\FilterCollection;
use IQDEV\Search\Filter\FilterGroupCollection;
use IQDEV\Search\Filter\FilterKeyword;
use IQDEV\Search\Filter\FilterNumber;
Pavel's avatar
Pavel committed
use IQDEV\Search\Filter\FilterOperator;
use IQDEV\Search\Filter\FilterType;
Pavel's avatar
Pavel committed
use IQDEV\Search\Filter\LogicOperator;
Pavel's avatar
Pavel committed
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;
        }

Pavel's avatar
Pavel committed
        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;
Pavel's avatar
Pavel committed
                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()) {
Pavel's avatar
Pavel committed
            $keywordNestedFilter = new Nested();
            $keywordNestedFilter->setPath('search_data');

            if (false === $keywordQueryFilter->isEmpty()) {
                $keywordNestedFilterQuery = clone $keywordNestedFilter;
                $keywordNestedFilterQuery->setQuery($keywordQueryFilter);
                $request->getQuery()->filter($keywordNestedFilterQuery);
            }
Pavel's avatar
Pavel committed

            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()) {
Pavel's avatar
Pavel committed
            $numberNestedFilter = new Nested();
            $numberNestedFilter->setPath('search_data');

            if (false === $numberQueryFilter->isEmpty()) {
                $numberNestedFilterQuery = clone $numberNestedFilter;
                $numberNestedFilterQuery->setQuery($numberQueryFilter);
                $request->getQuery()->filter($numberNestedFilterQuery);
            }
Pavel's avatar
Pavel committed

            if (false === $numberPostFilter->isEmpty()) {
                $numberNestedFilterPost = clone $numberNestedFilter;
                $numberNestedFilterPost->setQuery($numberPostFilter);
                $request->getPostFilter()->filter($numberNestedFilterPost);
            }
Pavel's avatar
Pavel committed
        }

        return $request;
    }

    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;
            }
Pavel's avatar
Pavel committed
            $group = $filterGroup->getLogicalType()->value() === LogicOperator::OR ? count($ranges) + 1 : 0;
            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

Pavel's avatar
Pavel committed
                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;
                }
Pavel's avatar
Pavel committed
            }
        }

        if (false === empty($ranges)) {
Pavel's avatar
Pavel committed
            foreach ($ranges as $iGroup => $group) {
                foreach ($group as $field => $range) {
                    $facet = new FilterNumberFacet(
Pavel's avatar
Pavel committed
                        $field,
                        $range
Pavel's avatar
Pavel committed
                    );

                    if ($iGroup === 0) {
                        $numberFilter->filter($facet);
                    } else {
                        $numberFilter->should($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 */
Pavel's avatar
Pavel committed
            if ($filterGroup->isEmpty()) {
                continue;
            }
            $should = $filterGroup->getLogicalType()->value() === LogicOperator::OR;
Pavel's avatar
Pavel committed
            foreach ($filterGroup as $filter) {
                /** @var Filter $filter */
                $value = $filter->value()->value();
                $field = $filter->field()->value();

                if (in_array($field, $excludeFilter, true)) {
                    continue;
                }
Pavel's avatar
Pavel committed

Pavel's avatar
Pavel committed
                if (in_array($filter->operator()->value(), [FilterOperator::LT, FilterOperator::LTE], true)) {
                    continue;
                }
Pavel's avatar
Pavel committed

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

Pavel's avatar
Pavel committed
                if ('search' === $field) {
                    continue;
                }
Pavel's avatar
Pavel committed

Pavel's avatar
Pavel committed
                if ('category_id' === $field) {
                    continue;
                }
Pavel's avatar
Pavel committed

Pavel's avatar
Pavel committed
                if (is_array($value)) {
                    $value = array_map(static fn($v) => (string)$v, $value);
                } else {
                    $value = (string)$value;
                }
Pavel's avatar
Pavel committed

Pavel's avatar
Pavel committed
                if ($should) {
                    $keywordFilter->should(new FilterKeywordFacet($field, $value));
                } else {
                    $keywordFilter->filter(new FilterKeywordFacet($field, $value));
                }
Pavel's avatar
Pavel committed
            }
        }

        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());

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()->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);
                    }
Pavel's avatar
Pavel committed

                    if (false === $numberFilter->isEmpty()) {
                        $nestedFilterNumber = new Nested();
                        $nestedFilterNumber->setPath('search_data')
                            ->setQuery($numberFilter);
                        $queryKeywordFiltered->filter($nestedFilterNumber);
                    }
Pavel's avatar
Pavel committed

                    if ($queryKeywordFiltered->isEmpty() === false) {
                        $aggsFiltered->setQuery($queryKeywordFiltered);
                    } else {
                        $aggsFiltered->setNested((new Nested())->setPath('search_data'));
                    }
Pavel's avatar
Pavel committed

                    $request->getAggs()->add($aggsFiltered);
                }
Pavel's avatar
Pavel committed
            }
Pavel's avatar
Pavel committed

            $keywordFilter = $this->getKeywordFilter($postFilters);
            $numberFilter  = $this->getNumberFilter($postFilters);
Pavel's avatar
Pavel committed

Pavel's avatar
Pavel committed
            $aggsKeywordFiltered = new Aggs('keyword_facet_filtered');
            $aggsKeywordFiltered->addAggs(
Pavel's avatar
Pavel committed
                AggsFacetTerms::create(
Pavel's avatar
Pavel committed
                    'all_keyword_facet_filtered',
Pavel's avatar
Pavel committed
                    'keyword_facet'
                )
            );
            $queryKeywordFiltered = new Query();
Pavel's avatar
Pavel committed

            $aggsNumberFiltered = new Aggs('number_facet_filtered');
            $aggsNumberFiltered->addAggs(
                AggsFacetStats::create(
                    'all_number_facet_filtered',
                    'number_facet'
                )
            );
            $queryNumberFiltered = new Query();

Pavel's avatar
Pavel committed
            if (false === $keywordFilter->isEmpty()) {
                $nestedFilterKeyword = new Nested();
                $nestedFilterKeyword->setPath('search_data')
                    ->setQuery($keywordFilter);
Pavel's avatar
Pavel committed

Pavel's avatar
Pavel committed
                $queryKeywordFiltered->filter($nestedFilterKeyword);
Pavel's avatar
Pavel committed
                $queryNumberFiltered->filter($nestedFilterKeyword);
Pavel's avatar
Pavel committed
            }

            if (false === $numberFilter->isEmpty()) {
                $nestedFilterNumber = new Nested();
                $nestedFilterNumber->setPath('search_data')
                    ->setQuery($numberFilter);
Pavel's avatar
Pavel committed

Pavel's avatar
Pavel committed
                $queryKeywordFiltered->filter($nestedFilterNumber);
Pavel's avatar
Pavel committed
                $queryNumberFiltered->filter($nestedFilterNumber);
Pavel's avatar
Pavel committed
            if (false === $queryKeywordFiltered->isEmpty()) {
                $aggsKeywordFiltered->setQuery($queryKeywordFiltered);
Pavel's avatar
Pavel committed
            } else {
Pavel's avatar
Pavel committed
                $aggsKeywordFiltered->setNested((new Nested())->setPath('search_data'));
Pavel's avatar
Pavel committed
            if (false === $queryNumberFiltered->isEmpty()) {
                $aggsNumberFiltered->setQuery($queryNumberFiltered);
            } else {
                $aggsNumberFiltered->setNested((new Nested())->setPath('search_data'));
            }
Pavel's avatar
Pavel committed

Pavel's avatar
Pavel committed
            $request->getAggs()
                ->add($aggsKeywordFiltered)
                ->add($aggsNumberFiltered);
Pavel's avatar
Pavel committed

        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];
    }
Pavel's avatar
Pavel committed
}