src/Elasticsearch/Framework/DataAbstractionLayer/ElasticsearchEntitySearcher.php line 38

  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(private readonly Client $client, private readonly EntitySearcherInterface $decorated, private readonly ElasticsearchHelper $helper, private readonly CriteriaParser $criteriaParser, private readonly AbstractElasticsearchSearchHydrator $hydrator, private readonly EventDispatcherInterface $eventDispatcher)
  28.     {
  29.     }
  30.     public function search(EntityDefinition $definitionCriteria $criteriaContext $context): IdSearchResult
  31.     {
  32.         if (!$this->helper->allowSearch($definition$context$criteria)) {
  33.             return $this->decorated->search($definition$criteria$context);
  34.         }
  35.         if ($criteria->getLimit() === 0) {
  36.             return new IdSearchResult(0, [], $criteria$context);
  37.         }
  38.         $search $this->createSearch($criteria$definition$context);
  39.         $this->eventDispatcher->dispatch(
  40.             new ElasticsearchEntitySearcherSearchEvent(
  41.                 $search,
  42.                 $definition,
  43.                 $criteria,
  44.                 $context
  45.             )
  46.         );
  47.         $search $this->convertSearch($criteria$definition$context$search);
  48.         try {
  49.             $result $this->client->search([
  50.                 'index' => $this->helper->getIndexName($definition$context->getLanguageId()),
  51.                 'track_total_hits' => true,
  52.                 'body' => $search,
  53.             ]);
  54.         } catch (\Throwable $e) {
  55.             $this->helper->logAndThrowException($e);
  56.             return $this->decorated->search($definition$criteria$context);
  57.         }
  58.         $result $this->hydrator->hydrate($definition$criteria$context$result);
  59.         $result->addState(self::RESULT_STATE);
  60.         return $result;
  61.     }
  62.     private function createSearch(Criteria $criteriaEntityDefinition $definitionContext $context): Search
  63.     {
  64.         $search = new Search();
  65.         $this->helper->handleIds($definition$criteria$search$context);
  66.         $this->helper->addFilters($definition$criteria$search$context);
  67.         $this->helper->addPostFilters($definition$criteria$search$context);
  68.         $this->helper->addQueries($definition$criteria$search$context);
  69.         $this->helper->addSortings($definition$criteria$search$context);
  70.         $this->helper->addTerm($criteria$search$context$definition);
  71.         $search->setSize(self::MAX_LIMIT);
  72.         $limit $criteria->getLimit();
  73.         if ($limit !== null) {
  74.             $search->setSize($limit);
  75.         }
  76.         $search->setFrom((int) $criteria->getOffset());
  77.         return $search;
  78.     }
  79.     /**
  80.      * @return array<string, mixed>
  81.      */
  82.     private function convertSearch(Criteria $criteriaEntityDefinition $definitionContext $contextSearch $search): array
  83.     {
  84.         if (!$criteria->getGroupFields()) {
  85.             return $search->toArray();
  86.         }
  87.         $aggregation $this->buildTotalCountAggregation($criteria$definition$context);
  88.         $search->addAggregation($aggregation);
  89.         $array $search->toArray();
  90.         $array['collapse'] = $this->parseGrouping($criteria->getGroupFields(), $definition$context);
  91.         return $array;
  92.     }
  93.     /**
  94.      * @param FieldGrouping[] $groupings
  95.      *
  96.      * @return array{field: string, inner_hits?: array{name: string}}
  97.      */
  98.     private function parseGrouping(array $groupingsEntityDefinition $definitionContext $context): array
  99.     {
  100.         /** @var FieldGrouping $grouping */
  101.         $grouping array_shift($groupings);
  102.         $accessor $this->criteriaParser->buildAccessor($definition$grouping->getField(), $context);
  103.         if (empty($groupings)) {
  104.             return ['field' => $accessor];
  105.         }
  106.         return [
  107.             'field' => $accessor,
  108.             'inner_hits' => [
  109.                 'name' => 'inner',
  110.                 'collapse' => $this->parseGrouping($groupings$definition$context),
  111.             ],
  112.         ];
  113.     }
  114.     private function buildTotalCountAggregation(Criteria $criteriaEntityDefinition $definitionContext $context): AbstractAggregation
  115.     {
  116.         $groupings $criteria->getGroupFields();
  117.         if (\count($groupings) === 1) {
  118.             $first array_shift($groupings);
  119.             $accessor $this->criteriaParser->buildAccessor($definition$first->getField(), $context);
  120.             $aggregation = new CardinalityAggregation('total-count');
  121.             $aggregation->setField($accessor);
  122.             return $this->addPostFilterAggregation($criteria$definition$context$aggregation);
  123.         }
  124.         $fields = [];
  125.         foreach ($groupings as $grouping) {
  126.             $accessor $this->criteriaParser->buildAccessor($definition$grouping->getField(), $context);
  127.             $fields[] = sprintf(
  128.                 '
  129.                 if (doc[\'%s\'].size()==0) {
  130.                     value = value + \'empty\';
  131.                 } else {
  132.                     value = value + doc[\'%s\'].value;
  133.                 }',
  134.                 $accessor,
  135.                 $accessor
  136.             );
  137.         }
  138.         $script '
  139.             def value = \'\';
  140.             ' implode(' '$fields) . '
  141.             return value;
  142.         ';
  143.         $aggregation = new CardinalityAggregation('total-count');
  144.         $aggregation->setScript($script);
  145.         return $this->addPostFilterAggregation($criteria$definition$context$aggregation);
  146.     }
  147.     private function addPostFilterAggregation(Criteria $criteriaEntityDefinition $definitionContext $contextCardinalityAggregation $aggregation): AbstractAggregation
  148.     {
  149.         if (!$criteria->getPostFilters()) {
  150.             return $aggregation;
  151.         }
  152.         $query $this->criteriaParser->parseFilter(
  153.             new MultiFilter(MultiFilter::CONNECTION_AND$criteria->getPostFilters()),
  154.             $definition,
  155.             $definition->getEntityName(),
  156.             $context
  157.         );
  158.         $filterAgg = new FilterAggregation('total-filtered-count'$query);
  159.         $filterAgg->addAggregation($aggregation);
  160.         return $filterAgg;
  161.     }
  162. }