scoreBuilderFactory = $scoreBuilderFactory; $this->filterBuilder = $filterBuilder; $this->conditionManager = $conditionManager; $this->resource = $resource; $this->entityMetadata = $entityMetadata; $this->indexProviders = $indexProviders; $this->queryContainerFactory = $queryContainerFactory; $this->matchBuilder = $matchBuilder; $this->temporaryStorage = $temporaryStorageFactory->create(); $this->temporaryStorageFactory = $temporaryStorageFactory; if (!in_array($relevanceCalculationMethod, ['SUM', 'MAX'], true)) { throw new \LogicException('Unsupported relevance calculation method used. Only SUM and MAX are allowed'); } $this->relevanceCalculationMethod = $relevanceCalculationMethod; } /** * Build adapter dependent query * * @param RequestInterface $request * @return Select * @throws \LogicException * @throws \Zend_Db_Exception * @throws \InvalidArgumentException */ public function buildQuery(RequestInterface $request) { if (!array_key_exists($request->getIndex(), $this->indexProviders)) { throw new \LogicException('Index provider not configured'); } $indexBuilder = $this->indexProviders[$request->getIndex()]; $queryContainer = $this->queryContainerFactory->create( [ 'indexBuilder' => $indexBuilder, 'request' => $request, ] ); $select = $indexBuilder->build($request); /** @var ScoreBuilder $scoreBuilder */ $scoreBuilder = $this->scoreBuilderFactory->create(); $select = $this->processQuery( $scoreBuilder, $request->getQuery(), $select, BoolQuery::QUERY_CONDITION_MUST, $queryContainer ); $select = $this->addDerivedQueries( $request, $queryContainer, $scoreBuilder, $select, $indexBuilder ); $select->limit($request->getSize(), $request->getFrom()); $select->order('relevance ' . Select::SQL_DESC)->order('entity_id ' . Select::SQL_DESC); return $select; } /** * Creates Select which wraps search result select * * It is used to group search results by entity id. * * @param Select $select * @param ScoreBuilder $scoreBuilder * @return Select */ private function createAroundSelect(Select $select, ScoreBuilder $scoreBuilder) { $parentSelect = $this->getConnection()->select(); $parentSelect->from( ['main_select' => $select], [ $this->entityMetadata->getEntityId() => 'entity_id', 'relevance' => sprintf('%s(%s)', $this->relevanceCalculationMethod, $scoreBuilder->getScoreAlias()), ] )->group($this->entityMetadata->getEntityId()); return $parentSelect; } /** * Process query * * @param ScoreBuilder $scoreBuilder * @param RequestQueryInterface $query * @param Select $select * @param string $conditionType * @param QueryContainer $queryContainer * @return Select * @throws \InvalidArgumentException */ protected function processQuery( ScoreBuilder $scoreBuilder, RequestQueryInterface $query, Select $select, $conditionType, QueryContainer $queryContainer ) { switch ($query->getType()) { case RequestQueryInterface::TYPE_MATCH: /** @var MatchQuery $query */ $select = $queryContainer->addMatchQuery( $select, $query, $conditionType ); break; case RequestQueryInterface::TYPE_BOOL: /** @var BoolQuery $query */ $select = $this->processBoolQuery($scoreBuilder, $query, $select, $queryContainer); break; case RequestQueryInterface::TYPE_FILTER: /** @var FilterQuery $query */ $select = $this->processFilterQuery($scoreBuilder, $query, $select, $conditionType, $queryContainer); break; default: throw new \InvalidArgumentException(sprintf('Unknown query type \'%s\'', $query->getType())); } return $select; } /** * Process bool query * * @param ScoreBuilder $scoreBuilder * @param BoolQuery $query * @param Select $select * @param QueryContainer $queryContainer * @return Select */ private function processBoolQuery( ScoreBuilder $scoreBuilder, BoolQuery $query, Select $select, QueryContainer $queryContainer ) { $scoreBuilder->startQuery(); $select = $this->processBoolQueryCondition( $scoreBuilder, $query->getMust(), $select, BoolQuery::QUERY_CONDITION_MUST, $queryContainer ); $select = $this->processBoolQueryCondition( $scoreBuilder, $query->getShould(), $select, BoolQuery::QUERY_CONDITION_SHOULD, $queryContainer ); $select = $this->processBoolQueryCondition( $scoreBuilder, $query->getMustNot(), $select, BoolQuery::QUERY_CONDITION_NOT, $queryContainer ); $scoreBuilder->endQuery($query->getBoost()); return $select; } /** * Process bool query condition (must, should, must_not) * * @param ScoreBuilder $scoreBuilder * @param RequestQueryInterface[] $subQueryList * @param Select $select * @param string $conditionType * @param QueryContainer $queryContainer * @return Select */ private function processBoolQueryCondition( ScoreBuilder $scoreBuilder, array $subQueryList, Select $select, $conditionType, QueryContainer $queryContainer ) { foreach ($subQueryList as $subQuery) { $select = $this->processQuery($scoreBuilder, $subQuery, $select, $conditionType, $queryContainer); } return $select; } /** * Process filter query * * @param ScoreBuilder $scoreBuilder * @param FilterQuery $query * @param Select $select * @param string $conditionType * @param QueryContainer $queryContainer * @return Select */ private function processFilterQuery( ScoreBuilder $scoreBuilder, FilterQuery $query, Select $select, $conditionType, QueryContainer $queryContainer ) { $scoreBuilder->startQuery(); switch ($query->getReferenceType()) { case FilterQuery::REFERENCE_QUERY: $select = $this->processQuery( $scoreBuilder, $query->getReference(), $select, $conditionType, $queryContainer ); $scoreBuilder->endQuery($query->getBoost()); break; case FilterQuery::REFERENCE_FILTER: $filterCondition = $this->filterBuilder->build($query->getReference(), $conditionType); if ($filterCondition) { $select->where($filterCondition); } break; } $scoreBuilder->endQuery($query->getBoost()); return $select; } /** * Add match queries to select. * * @param RequestInterface $request * @param QueryContainer $queryContainer * @param ScoreBuilder $scoreBuilder * @param Select $select * @param IndexBuilderInterface $indexBuilder * @return Select * @throws \Zend_Db_Exception */ private function addDerivedQueries( RequestInterface $request, QueryContainer $queryContainer, ScoreBuilder $scoreBuilder, Select $select, IndexBuilderInterface $indexBuilder ) { $matchQueries = $queryContainer->getMatchQueries(); if (!$matchQueries) { $select->columns($scoreBuilder->build()); $select = $this->createAroundSelect($select, $scoreBuilder); } else { $matchContainer = array_shift($matchQueries); $this->matchBuilder->build( $scoreBuilder, $select, $matchContainer->getRequest(), $matchContainer->getConditionType() ); $select->columns($scoreBuilder->build()); $select = $this->createAroundSelect($select, $scoreBuilder); $select = $this->addMatchQueries($request, $select, $indexBuilder, $matchQueries); } return $select; } /** * Get connection. * * @return false|\Magento\Framework\DB\Adapter\AdapterInterface */ private function getConnection() { return $this->resource->getConnection(); } /** * Add match queries to select. * * @param RequestInterface $request * @param Select $select * @param IndexBuilderInterface $indexBuilder * @param MatchContainer[] $matchQueries * @return Select */ private function addMatchQueries( RequestInterface $request, Select $select, IndexBuilderInterface $indexBuilder, array $matchQueries ) { $queriesCount = count($matchQueries); if ($queriesCount) { $table = $this->temporaryStorage->storeDocumentsFromSelect($select); foreach ($matchQueries as $matchContainer) { $queriesCount--; $matchScoreBuilder = $this->scoreBuilderFactory->create(); $matchSelect = $this->matchBuilder->build( $matchScoreBuilder, $indexBuilder->build($request), $matchContainer->getRequest(), $matchContainer->getConditionType() ); $select = $this->joinPreviousResultToSelect($matchSelect, $table, $matchScoreBuilder); if ($queriesCount) { $previousResultTable = $table; $table = $this->temporaryStorage->storeDocumentsFromSelect($select); $this->getConnection()->dropTable($previousResultTable->getName()); } } } return $select; } /** * Join previous result to select. * * @param Select $query * @param Table $previousResultTable * @param ScoreBuilder $scoreBuilder * @return Select * @throws \Zend_Db_Exception */ private function joinPreviousResultToSelect(Select $query, Table $previousResultTable, ScoreBuilder $scoreBuilder) { $query->joinInner( ['previous_results' => $previousResultTable->getName()], 'previous_results.entity_id = search_index.entity_id', [] ); $scoreBuilder->addCondition('previous_results.score', false); $query->columns($scoreBuilder->build()); $query = $this->createAroundSelect($query, $scoreBuilder); return $query; } }