Commit a7880bb1 authored by Pavel's avatar Pavel
Browse files

Merge branch 'EscapeDependence' into 'main'

Escape dependence

See merge request piligrimov/search-es!3
parents 28ed37eb 815a674a
Loading
Loading
Loading
Loading
+3 −10
Original line number Diff line number Diff line
@@ -16,9 +16,8 @@
    "php"
  ],
  "require": {
    "php": ">=7.4",
    "ramsey/collection": "^1.2",
    "iqdev/search-dc": "1.2.*",
    "php": ">=8.1",
    "ramsey/collection": "^2",
    "elasticsearch/elasticsearch": "^8.5",
    "vlucas/phpdotenv": "^5.4.1"
  },
@@ -32,15 +31,9 @@
      "IQDEV\\ElasticSearchTests\\": "tests/"
    }
  },
  "repositories": [
    {
      "type": "vcs",
      "url": "ssh://git@gitlab.iqdev.digital:8422/piligrimov/search-dc.git"
    }
  ],
  "require-dev": {
    "phpunit/phpunit": "^9.5",
    "symfony/var-dumper": "^5.4"
    "symfony/var-dumper": "^6.3"
  },
  "scripts": {
    "tests": "php ./vendor/bin/phpunit --testdox --verbose"
+0 −458
Original line number Diff line number Diff line
<?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];
    }
}
+53 −0
Original line number Diff line number Diff line
<?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;
    }
}
+63 −0
Original line number Diff line number Diff line
<?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);
    }
}
+71 −0
Original line number Diff line number Diff line
<?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);
    }
}
Loading