<?php namespace IQDEV\ElasticSearch\Converter; use IQDEV\ElasticSearch\Filter\PostFilterCollection; use IQDEV\ElasticSearch\Filter\QueryFilterCollection; 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\ElasticSearch\Criteria; use IQDEV\ElasticSearch\Document\Property\AttrType; use IQDEV\ElasticSearch\Document\Property\PropertyType; use IQDEV\ElasticSearch\Filter\Filter; use IQDEV\ElasticSearch\Filter\FilterCollection; use IQDEV\ElasticSearch\Filter\FilterGroupCollection; use IQDEV\ElasticSearch\Filter\FilterKeyword; use IQDEV\ElasticSearch\Filter\FilterNumber; use IQDEV\ElasticSearch\Filter\FilterOperator; use IQDEV\ElasticSearch\Filter\FilterType; use IQDEV\ElasticSearch\Filter\LogicOperator; use IQDEV\ElasticSearch\Order\Order; 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 = $order->orderType(); if ($order->orderBy() instanceof AttrType) { $request->getSort()->add(new OrderField($order->orderBy(), $direction)); } elseif ($order->orderBy() instanceof PropertyType) { if ($order->orderBy()->type() === PropertyType::TYPE_KEYWORD) { $request->getSort()->add(new OrderKeywordProperty($order->orderBy(), $direction)); } elseif ($order->orderBy()->type() === PropertyType::TYPE_NUMBER) { $request->getSort()->add(new OrderNumberProperty($order->orderBy(), $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]; } }