Smile-SA/elasticsuite

View on GitHub
src/module-elasticsuite-core/Search/Request/Builder.php

Summary

Maintainability
C
1 day
Test Coverage
<?php
/**
 * DISCLAIMER
 *
 * Do not edit or add to this file if you wish to upgrade Smile ElasticSuite to newer
 * versions in the future.
 *
 * @category  Smile
 * @package   Smile\ElasticsuiteCore
 * @author    Aurelien FOUCRET <aurelien.foucret@smile.fr>
 * @copyright 2020 Smile
 * @license   Open Software License ("OSL") v. 3.0
 */

namespace Smile\ElasticsuiteCore\Search\Request;

use Magento\Framework\Search\Request\DimensionFactory;
use Smile\ElasticsuiteCore\Api\Search\Request\ContainerConfiguration\AggregationResolverInterface;
use Smile\ElasticsuiteCore\Search\Request\Query\Builder as QueryBuilder;
use Smile\ElasticsuiteCore\Search\Request\SortOrder\SortOrderBuilder;
use Smile\ElasticsuiteCore\Search\Request\Aggregation\AggregationBuilder;
use Smile\ElasticsuiteCore\Search\RequestInterface;
use Smile\ElasticsuiteCore\Search\RequestFactory;
use Magento\Framework\Search\Request\Dimension;
use Smile\ElasticsuiteCore\Api\Search\Request\ContainerConfigurationInterface;
use Smile\ElasticsuiteCore\Api\Search\Spellchecker\RequestInterfaceFactory as SpellcheckRequestFactory;
use Smile\ElasticsuiteCore\Api\Search\SpellcheckerInterface;

/**
 * ElasticSuite search requests builder.
 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
 *
 * @category Smile
 * @package  Smile\ElasticsuiteCore
 * @author   Aurelien FOUCRET <aurelien.foucret@smile.fr>
 */
class Builder
{
    /**
     * @var ContainerConfigurationFactory
     */
    private $containerConfigFactory;

    /**
     * @var QueryBuilder
     */
    private $queryBuilder;

    /**
     * @var SortOrderBuilder
     */
    private $sortOrderBuilder;

    /**
     * @var AggregationBuilder
     */
    private $aggregationBuilder;

    /**
     * @var RequestFactory
     */
    private $requestFactory;

    /**
     * @var SpellcheckRequestFactory
     */
    private $spellcheckRequestFactory;

    /**
     * @var SpellcheckerInterface
     */
    private $spellchecker;

    /**
     * @var DimensionFactory
     */
    private $dimensionFactory;

    /**
     * @var \Smile\ElasticsuiteCore\Api\Search\Request\ContainerConfiguration\AggregationResolverInterface
     */
    private $aggregationResolver;

    /**
     * Constructor.
     *
     * @param RequestFactory                $requestFactory           Factory used to build the request.
     * @param DimensionFactory              $dimensionFactory         Factory used to dimensions of the request.
     * @param QueryBuilder                  $queryBuilder             Builder for the query part of the request.
     * @param SortOrderBuilder              $sortOrderBuilder         Builder for the sort part of the request.
     * @param AggregationBuilder            $aggregationBuilder       Builder for the aggregation part of the request.
     * @param ContainerConfigurationFactory $containerConfigFactory   Search requests configuration.
     * @param SpellcheckRequestFactory      $spellcheckRequestFactory Spellchecking request factory.
     * @param SpellcheckerInterface         $spellchecker             Spellchecker.
     * @param AggregationResolverInterface  $aggregationResolver      Aggregation Resolver.
     */
    public function __construct(
        RequestFactory $requestFactory,
        DimensionFactory $dimensionFactory,
        QueryBuilder $queryBuilder,
        SortOrderBuilder $sortOrderBuilder,
        AggregationBuilder $aggregationBuilder,
        ContainerConfigurationFactory $containerConfigFactory,
        SpellcheckRequestFactory $spellcheckRequestFactory,
        SpellcheckerInterface $spellchecker,
        AggregationResolverInterface $aggregationResolver
    ) {
        $this->spellcheckRequestFactory = $spellcheckRequestFactory;
        $this->spellchecker             = $spellchecker;
        $this->requestFactory           = $requestFactory;
        $this->dimensionFactory         = $dimensionFactory;
        $this->queryBuilder             = $queryBuilder;
        $this->sortOrderBuilder         = $sortOrderBuilder;
        $this->aggregationBuilder       = $aggregationBuilder;
        $this->containerConfigFactory   = $containerConfigFactory;
        $this->aggregationResolver      = $aggregationResolver;
    }

    /**
     * Create a new search request.
     * @SuppressWarnings(PHPMD.ExcessiveParameterList)
     *
     * @param integer               $storeId        Search request store id.
     * @param string                $containerName  Search request name.
     * @param integer               $from           Search request pagination from clause.
     * @param integer               $size           Search request pagination size.
     * @param string|QueryInterface $query          Search request query.
     * @param array                 $sortOrders     Search request sort orders.
     * @param array                 $filters        Search request filters.
     * @param QueryInterface[]      $queryFilters   Search request filters prebuilt as QueryInterface.
     * @param array                 $facets         Search request facets.
     * @param bool|int|null         $trackTotalHits If total hits should be tracked.
     *
     * @return RequestInterface
     */
    public function create(
        $storeId,
        $containerName,
        $from,
        $size,
        $query = null,
        $sortOrders = [],
        $filters = [],
        $queryFilters = [],
        $facets = [],
        $trackTotalHits = null
    ) {
        $containerConfig  = $this->getRequestContainerConfiguration($storeId, $containerName);
        $containerFilters = $this->getContainerFilters($containerConfig);
        $containerAggs    = [];

        // If "track_total_hits" is explicitely true, we are just "counting" the result, and we do not care about the aggregations.
        if ($trackTotalHits !== true) {
            $containerAggs = $this->getContainerAggregations($containerConfig, $query, $filters, $queryFilters);
        }

        $facets       = array_merge($facets, $containerAggs);
        $facetFilters = array_intersect_key($filters, $facets);
        $queryFilters = array_merge($queryFilters, $containerFilters, array_diff_key($filters, $facetFilters));

        $spellingType = SpellcheckerInterface::SPELLING_TYPE_EXACT;

        if ($query && is_string($query)) {
            $spellingType = $this->getSpellingType($containerConfig, $query);
        }

        if (null === $trackTotalHits) {
            $trackTotalHits = $containerConfig->getTrackTotalHits();
        }

        $requestParams = [
            'name'         => $containerName,
            'indexName'    => $containerConfig->getIndexName(),
            'from'         => $from,
            'size'         => $size,
            'dimensions'   => $this->buildDimensions($storeId),
            'query'        => $this->queryBuilder->createQuery($containerConfig, $query, $queryFilters, $spellingType),
            'sortOrders'   => $this->sortOrderBuilder->buildSordOrders($containerConfig, $sortOrders),
            'buckets'      => $this->aggregationBuilder->buildAggregations($containerConfig, $facets, $facetFilters),
            'spellingType' => $spellingType,
            'trackTotalHits' => $trackTotalHits,
        ];

        // Use min_score only for fulltext queries.
        if ($query !== null) {
            $requestParams['minScore'] = $containerConfig->getRelevanceConfig()->getMinScore();
        }

        if (!empty($facetFilters)) {
            $requestParams['filter'] = $this->queryBuilder->createFilterQuery($containerConfig, $facetFilters);
        }

        $request = $this->requestFactory->create($requestParams);

        return $request;
    }

    /**
     * Returns search request applied to each request for a given search container.
     *
     * @param ContainerConfigurationInterface $containerConfig Search request configuration.
     *
     * @return \Smile\ElasticsuiteCore\Search\Request\QueryInterface[]
     */
    private function getContainerFilters(ContainerConfigurationInterface $containerConfig)
    {
        return $containerConfig->getFilters();
    }

    /**
     * Returns aggregations configured in the search container.
     *
     * @param ContainerConfigurationInterface $containerConfig Search request configuration.
     * @param string|QueryInterface           $query           Search Query.
     * @param array                           $filters         Search request filters.
     * @param QueryInterface[]                $queryFilters    Search request filters prebuilt as QueryInterface.
     *
     * @return array
     */
    private function getContainerAggregations(ContainerConfigurationInterface $containerConfig, $query, $filters, $queryFilters)
    {
        return $this->aggregationResolver->getContainerAggregations($containerConfig, $query, $filters, $queryFilters);
    }

    /**
     * Retireve the spelling type for a fulltext query.
     *
     * @param ContainerConfigurationInterface $containerConfig Search request configuration.
     * @param string|string[]                 $queryText       Query text.
     *
     * @return int
     */
    private function getSpellingType(ContainerConfigurationInterface $containerConfig, $queryText)
    {
        if (is_array($queryText)) {
            $queryText = implode(" ", $queryText);
        }

        $spellcheckRequestParams = [
            'index'           => $containerConfig->getIndexName(),
            'queryText'       => $queryText,
            'cutoffFrequency' => $containerConfig->getRelevanceConfig()->getCutOffFrequency(),
            'isUsingAllTokens'  => $containerConfig->getRelevanceConfig()->isUsingAllTokens(),
            'isUsingReference'  => $containerConfig->getRelevanceConfig()->isUsingReferenceAnalyzer(),
            'isUsingEdgeNgram'  => $containerConfig->getRelevanceConfig()->isUsingEdgeNgramAnalyzer(),
        ];

        $spellcheckRequest = $this->spellcheckRequestFactory->create($spellcheckRequestParams);
        $spellingType      = $this->spellchecker->getSpellingType($spellcheckRequest);

        return $spellingType;
    }

    /**
     * Load the search request configuration (index, type, mapping, ...) using the search request container name.
     *
     * @throws \LogicException Thrown when the search container is not found into the configuration.
     *
     * @param integer $storeId       Store id.
     * @param string  $containerName Search request container name.
     *
     * @return ContainerConfigurationInterface
     */
    private function getRequestContainerConfiguration($storeId, $containerName)
    {
        if ($containerName === null) {
            throw new \LogicException('Request name is not set');
        }

        $config = $this->containerConfigFactory->create(
            ['containerName' => $containerName, 'storeId' => $storeId]
        );

        if ($config === null) {
            throw new \LogicException("No configuration exists for request {$containerName}");
        }

        return $config;
    }

    /**
     * Build a dimenstion object from
     * It is quite useless since we have a per store index but required by the RequestInterface specification.
     *
     * @param integer $storeId Store id.
     *
     * @return Dimension[]
     */
    private function buildDimensions($storeId)
    {
        $dimensions = ['scope' => $this->dimensionFactory->create(['name' => 'scope', 'value' => $storeId])];

        return $dimensions;
    }
}