vendor/shopware/elasticsearch/Framework/DataAbstractionLayer/ElasticsearchEntitySearcher.php line 44

  1. <?php declare(strict_types=1);
  2. namespace Shopware\Elasticsearch\Framework\DataAbstractionLayer;
  3. use OpenSearch\Client;
  4. use OpenSearchDSL\Aggregation\AbstractAggregation;
  5. use OpenSearchDSL\Aggregation\Bucketing\FilterAggregation;
  6. use OpenSearchDSL\Aggregation\Metric\CardinalityAggregation;
  7. use OpenSearchDSL\Search;
  8. use Shopware\Core\Framework\Context;
  9. use Shopware\Core\Framework\DataAbstractionLayer\EntityDefinition;
  10. use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
  11. use Shopware\Core\Framework\DataAbstractionLayer\Search\EntitySearcherInterface;
  12. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\MultiFilter;
  13. use Shopware\Core\Framework\DataAbstractionLayer\Search\Grouping\FieldGrouping;
  14. use Shopware\Core\Framework\DataAbstractionLayer\Search\IdSearchResult;
  15. use Shopware\Core\Framework\Log\Package;
  16. use Shopware\Elasticsearch\Framework\DataAbstractionLayer\Event\ElasticsearchEntitySearcherSearchEvent;
  17. use Shopware\Elasticsearch\Framework\ElasticsearchHelper;
  18. use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
  19. #[Package('core')]
  20. class ElasticsearchEntitySearcher implements EntitySearcherInterface
  21. {
  22.     final public const MAX_LIMIT 10000;
  23.     final public const RESULT_STATE 'loaded-by-elastic';
  24.     /**
  25.      * @internal
  26.      */
  27.     public function __construct(
  28.         private readonly Client $client,
  29.         private readonly EntitySearcherInterface $decorated,
  30.         private readonly ElasticsearchHelper $helper,
  31.         private readonly CriteriaParser $criteriaParser,
  32.         private readonly AbstractElasticsearchSearchHydrator $hydrator,
  33.         private readonly EventDispatcherInterface $eventDispatcher
  34.     ) {
  35.     }
  36.     public function search(EntityDefinition $definitionCriteria $criteriaContext $context): IdSearchResult
  37.     {
  38.         if (!$this->helper->allowSearch($definition$context$criteria)) {
  39.             return $this->decorated->search($definition$criteria$context);
  40.         }
  41.         if ($criteria->getLimit() === 0) {
  42.             return new IdSearchResult(0, [], $criteria$context);
  43.         }
  44.         $search $this->createSearch($criteria$definition$context);
  45.         $this->eventDispatcher->dispatch(
  46.             new ElasticsearchEntitySearcherSearchEvent(
  47.                 $search,
  48.                 $definition,
  49.                 $criteria,
  50.                 $context
  51.             )
  52.         );
  53.         $search $this->convertSearch($criteria$definition$context$search);
  54.         try {
  55.             $result $this->client->search([
  56.                 'index' => $this->helper->getIndexName($definition$context->getLanguageId()),
  57.                 'track_total_hits' => true,
  58.                 'body' => $search,
  59.             ]);
  60.         } catch (\Throwable $e) {
  61.             $this->helper->logAndThrowException($e);
  62.             return $this->decorated->search($definition$criteria$context);
  63.         }
  64.         $result $this->hydrator->hydrate($definition$criteria$context$result);
  65.         $result->addState(self::RESULT_STATE);
  66.         return $result;
  67.     }
  68.     private function createSearch(Criteria $criteriaEntityDefinition $definitionContext $context): Search
  69.     {
  70.         $search = new Search();
  71.         $this->helper->handleIds($definition$criteria$search$context);
  72.         $this->helper->addFilters($definition$criteria$search$context);
  73.         $this->helper->addPostFilters($definition$criteria$search$context);
  74.         $this->helper->addQueries($definition$criteria$search$context);
  75.         $this->helper->addSortings($definition$criteria$search$context);
  76.         $this->helper->addTerm($criteria$search$context$definition);
  77.         $search->setSize(self::MAX_LIMIT);
  78.         $limit $criteria->getLimit();
  79.         if ($limit !== null) {
  80.             $search->setSize($limit);
  81.         }
  82.         $search->setFrom((int) $criteria->getOffset());
  83.         return $search;
  84.     }
  85.     /**
  86.      * @return array<string, mixed>
  87.      */
  88.     private function convertSearch(Criteria $criteriaEntityDefinition $definitionContext $contextSearch $search): array
  89.     {
  90.         if (!$criteria->getGroupFields()) {
  91.             return $search->toArray();
  92.         }
  93.         $aggregation $this->buildTotalCountAggregation($criteria$definition$context);
  94.         $search->addAggregation($aggregation);
  95.         $array $search->toArray();
  96.         $array['collapse'] = $this->parseGrouping($criteria->getGroupFields(), $definition$context);
  97.         return $array;
  98.     }
  99.     /**
  100.      * @param FieldGrouping[] $groupings
  101.      *
  102.      * @return array{field: string, inner_hits?: array{name: string}}
  103.      */
  104.     private function parseGrouping(array $groupingsEntityDefinition $definitionContext $context): array
  105.     {
  106.         /** @var FieldGrouping $grouping */
  107.         $grouping array_shift($groupings);
  108.         $accessor $this->criteriaParser->buildAccessor($definition$grouping->getField(), $context);
  109.         if (empty($groupings)) {
  110.             return ['field' => $accessor];
  111.         }
  112.         return [
  113.             'field' => $accessor,
  114.             'inner_hits' => [
  115.                 'name' => 'inner',
  116.                 'collapse' => $this->parseGrouping($groupings$definition$context),
  117.             ],
  118.         ];
  119.     }
  120.     private function buildTotalCountAggregation(Criteria $criteriaEntityDefinition $definitionContext $context): AbstractAggregation
  121.     {
  122.         $groupings $criteria->getGroupFields();
  123.         if (\count($groupings) === 1) {
  124.             $first array_shift($groupings);
  125.             $accessor $this->criteriaParser->buildAccessor($definition$first->getField(), $context);
  126.             $aggregation = new CardinalityAggregation('total-count');
  127.             $aggregation->setField($accessor);
  128.             return $this->addPostFilterAggregation($criteria$definition$context$aggregation);
  129.         }
  130.         $fields = [];
  131.         foreach ($groupings as $grouping) {
  132.             $accessor $this->criteriaParser->buildAccessor($definition$grouping->getField(), $context);
  133.             $fields[] = sprintf(
  134.                 '
  135.                 if (doc[\'%s\'].size()==0) {
  136.                     value = value + \'empty\';
  137.                 } else {
  138.                     value = value + doc[\'%s\'].value;
  139.                 }',
  140.                 $accessor,
  141.                 $accessor
  142.             );
  143.         }
  144.         $script '
  145.             def value = \'\';
  146.             ' implode(' '$fields) . '
  147.             return value;
  148.         ';
  149.         $aggregation = new CardinalityAggregation('total-count');
  150.         $aggregation->setScript($script);
  151.         return $this->addPostFilterAggregation($criteria$definition$context$aggregation);
  152.     }
  153.     private function addPostFilterAggregation(Criteria $criteriaEntityDefinition $definitionContext $contextCardinalityAggregation $aggregation): AbstractAggregation
  154.     {
  155.         if (!$criteria->getPostFilters()) {
  156.             return $aggregation;
  157.         }
  158.         $query $this->criteriaParser->parseFilter(
  159.             new MultiFilter(MultiFilter::CONNECTION_AND$criteria->getPostFilters()),
  160.             $definition,
  161.             $definition->getEntityName(),
  162.             $context
  163.         );
  164.         $filterAgg = new FilterAggregation('total-filtered-count'$query);
  165.         $filterAgg->addAggregation($aggregation);
  166.         return $filterAgg;
  167.     }
  168. }