Mapper.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459
  1. <?php
  2. /**
  3. * Copyright © Magento, Inc. All rights reserved.
  4. * See COPYING.txt for license details.
  5. */
  6. namespace Magento\Framework\Search\Adapter\Mysql;
  7. use Magento\Framework\App\ResourceConnection;
  8. use Magento\Framework\DB\Ddl\Table;
  9. use Magento\Framework\DB\Select;
  10. use Magento\Framework\Search\Adapter\Mysql\Filter\Builder;
  11. use Magento\Framework\Search\Adapter\Mysql\Query\Builder\Match;
  12. use Magento\Framework\Search\Adapter\Mysql\Query\MatchContainer;
  13. use Magento\Framework\Search\Adapter\Mysql\Query\QueryContainer;
  14. use Magento\Framework\Search\Adapter\Mysql\Query\QueryContainerFactory;
  15. use Magento\Framework\Search\EntityMetadata;
  16. use Magento\Framework\Search\Request\Query\BoolExpression as BoolQuery;
  17. use Magento\Framework\Search\Request\Query\Filter as FilterQuery;
  18. use Magento\Framework\Search\Request\Query\Match as MatchQuery;
  19. use Magento\Framework\Search\Request\QueryInterface as RequestQueryInterface;
  20. use Magento\Framework\Search\RequestInterface;
  21. /**
  22. * Mapper class. Maps library request to specific adapter dependent query
  23. *
  24. * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
  25. * @api
  26. * @deprecated 102.0.0
  27. * @see \Magento\ElasticSearch
  28. * @since 100.0.2
  29. */
  30. class Mapper
  31. {
  32. /**
  33. * @var ScoreBuilder
  34. */
  35. private $scoreBuilderFactory;
  36. /**
  37. * @var Filter\Builder
  38. */
  39. private $filterBuilder;
  40. /**
  41. * @var ConditionManager
  42. */
  43. private $conditionManager;
  44. /**
  45. * @var IndexBuilderInterface[]
  46. */
  47. private $indexProviders;
  48. /**
  49. * @var Resource
  50. */
  51. private $resource;
  52. /**
  53. * @var EntityMetadata
  54. */
  55. private $entityMetadata;
  56. /**
  57. * @var QueryContainerFactory
  58. */
  59. private $queryContainerFactory;
  60. /**
  61. * @var Query\Builder\Match
  62. */
  63. private $matchBuilder;
  64. /**
  65. * @var TemporaryStorage
  66. */
  67. private $temporaryStorage;
  68. /**
  69. * @var string
  70. */
  71. private $relevanceCalculationMethod;
  72. /**
  73. * @var TemporaryStorageFactory
  74. */
  75. private $temporaryStorageFactory;
  76. /**
  77. * @param ScoreBuilderFactory $scoreBuilderFactory
  78. * @param Builder $filterBuilder
  79. * @param ConditionManager $conditionManager
  80. * @param ResourceConnection $resource
  81. * @param EntityMetadata $entityMetadata
  82. * @param QueryContainerFactory $queryContainerFactory
  83. * @param Query\Builder\Match $matchBuilder
  84. * @param TemporaryStorageFactory $temporaryStorageFactory
  85. * @param IndexBuilderInterface[] $indexProviders
  86. * @param string $relevanceCalculationMethod
  87. *
  88. * @SuppressWarnings(PHPMD.ExcessiveParameterList)
  89. */
  90. public function __construct(
  91. ScoreBuilderFactory $scoreBuilderFactory,
  92. Builder $filterBuilder,
  93. ConditionManager $conditionManager,
  94. ResourceConnection $resource,
  95. EntityMetadata $entityMetadata,
  96. QueryContainerFactory $queryContainerFactory,
  97. Match $matchBuilder,
  98. TemporaryStorageFactory $temporaryStorageFactory,
  99. array $indexProviders,
  100. $relevanceCalculationMethod = 'SUM'
  101. ) {
  102. $this->scoreBuilderFactory = $scoreBuilderFactory;
  103. $this->filterBuilder = $filterBuilder;
  104. $this->conditionManager = $conditionManager;
  105. $this->resource = $resource;
  106. $this->entityMetadata = $entityMetadata;
  107. $this->indexProviders = $indexProviders;
  108. $this->queryContainerFactory = $queryContainerFactory;
  109. $this->matchBuilder = $matchBuilder;
  110. $this->temporaryStorage = $temporaryStorageFactory->create();
  111. $this->temporaryStorageFactory = $temporaryStorageFactory;
  112. if (!in_array($relevanceCalculationMethod, ['SUM', 'MAX'], true)) {
  113. throw new \LogicException('Unsupported relevance calculation method used. Only SUM and MAX are allowed');
  114. }
  115. $this->relevanceCalculationMethod = $relevanceCalculationMethod;
  116. }
  117. /**
  118. * Build adapter dependent query
  119. *
  120. * @param RequestInterface $request
  121. * @return Select
  122. * @throws \LogicException
  123. * @throws \Zend_Db_Exception
  124. * @throws \InvalidArgumentException
  125. */
  126. public function buildQuery(RequestInterface $request)
  127. {
  128. if (!array_key_exists($request->getIndex(), $this->indexProviders)) {
  129. throw new \LogicException('Index provider not configured');
  130. }
  131. $indexBuilder = $this->indexProviders[$request->getIndex()];
  132. $queryContainer = $this->queryContainerFactory->create(
  133. [
  134. 'indexBuilder' => $indexBuilder,
  135. 'request' => $request,
  136. ]
  137. );
  138. $select = $indexBuilder->build($request);
  139. /** @var ScoreBuilder $scoreBuilder */
  140. $scoreBuilder = $this->scoreBuilderFactory->create();
  141. $select = $this->processQuery(
  142. $scoreBuilder,
  143. $request->getQuery(),
  144. $select,
  145. BoolQuery::QUERY_CONDITION_MUST,
  146. $queryContainer
  147. );
  148. $select = $this->addDerivedQueries(
  149. $request,
  150. $queryContainer,
  151. $scoreBuilder,
  152. $select,
  153. $indexBuilder
  154. );
  155. $select->limit($request->getSize(), $request->getFrom());
  156. $select->order('relevance ' . Select::SQL_DESC)->order('entity_id ' . Select::SQL_DESC);
  157. return $select;
  158. }
  159. /**
  160. * Creates Select which wraps search result select
  161. *
  162. * It is used to group search results by entity id.
  163. *
  164. * @param Select $select
  165. * @param ScoreBuilder $scoreBuilder
  166. * @return Select
  167. */
  168. private function createAroundSelect(Select $select, ScoreBuilder $scoreBuilder)
  169. {
  170. $parentSelect = $this->getConnection()->select();
  171. $parentSelect->from(
  172. ['main_select' => $select],
  173. [
  174. $this->entityMetadata->getEntityId() => 'entity_id',
  175. 'relevance' => sprintf('%s(%s)', $this->relevanceCalculationMethod, $scoreBuilder->getScoreAlias()),
  176. ]
  177. )->group($this->entityMetadata->getEntityId());
  178. return $parentSelect;
  179. }
  180. /**
  181. * Process query
  182. *
  183. * @param ScoreBuilder $scoreBuilder
  184. * @param RequestQueryInterface $query
  185. * @param Select $select
  186. * @param string $conditionType
  187. * @param QueryContainer $queryContainer
  188. * @return Select
  189. * @throws \InvalidArgumentException
  190. */
  191. protected function processQuery(
  192. ScoreBuilder $scoreBuilder,
  193. RequestQueryInterface $query,
  194. Select $select,
  195. $conditionType,
  196. QueryContainer $queryContainer
  197. ) {
  198. switch ($query->getType()) {
  199. case RequestQueryInterface::TYPE_MATCH:
  200. /** @var MatchQuery $query */
  201. $select = $queryContainer->addMatchQuery(
  202. $select,
  203. $query,
  204. $conditionType
  205. );
  206. break;
  207. case RequestQueryInterface::TYPE_BOOL:
  208. /** @var BoolQuery $query */
  209. $select = $this->processBoolQuery($scoreBuilder, $query, $select, $queryContainer);
  210. break;
  211. case RequestQueryInterface::TYPE_FILTER:
  212. /** @var FilterQuery $query */
  213. $select = $this->processFilterQuery($scoreBuilder, $query, $select, $conditionType, $queryContainer);
  214. break;
  215. default:
  216. throw new \InvalidArgumentException(sprintf('Unknown query type \'%s\'', $query->getType()));
  217. }
  218. return $select;
  219. }
  220. /**
  221. * Process bool query
  222. *
  223. * @param ScoreBuilder $scoreBuilder
  224. * @param BoolQuery $query
  225. * @param Select $select
  226. * @param QueryContainer $queryContainer
  227. * @return Select
  228. */
  229. private function processBoolQuery(
  230. ScoreBuilder $scoreBuilder,
  231. BoolQuery $query,
  232. Select $select,
  233. QueryContainer $queryContainer
  234. ) {
  235. $scoreBuilder->startQuery();
  236. $select = $this->processBoolQueryCondition(
  237. $scoreBuilder,
  238. $query->getMust(),
  239. $select,
  240. BoolQuery::QUERY_CONDITION_MUST,
  241. $queryContainer
  242. );
  243. $select = $this->processBoolQueryCondition(
  244. $scoreBuilder,
  245. $query->getShould(),
  246. $select,
  247. BoolQuery::QUERY_CONDITION_SHOULD,
  248. $queryContainer
  249. );
  250. $select = $this->processBoolQueryCondition(
  251. $scoreBuilder,
  252. $query->getMustNot(),
  253. $select,
  254. BoolQuery::QUERY_CONDITION_NOT,
  255. $queryContainer
  256. );
  257. $scoreBuilder->endQuery($query->getBoost());
  258. return $select;
  259. }
  260. /**
  261. * Process bool query condition (must, should, must_not)
  262. *
  263. * @param ScoreBuilder $scoreBuilder
  264. * @param RequestQueryInterface[] $subQueryList
  265. * @param Select $select
  266. * @param string $conditionType
  267. * @param QueryContainer $queryContainer
  268. * @return Select
  269. */
  270. private function processBoolQueryCondition(
  271. ScoreBuilder $scoreBuilder,
  272. array $subQueryList,
  273. Select $select,
  274. $conditionType,
  275. QueryContainer $queryContainer
  276. ) {
  277. foreach ($subQueryList as $subQuery) {
  278. $select = $this->processQuery($scoreBuilder, $subQuery, $select, $conditionType, $queryContainer);
  279. }
  280. return $select;
  281. }
  282. /**
  283. * Process filter query
  284. *
  285. * @param ScoreBuilder $scoreBuilder
  286. * @param FilterQuery $query
  287. * @param Select $select
  288. * @param string $conditionType
  289. * @param QueryContainer $queryContainer
  290. * @return Select
  291. */
  292. private function processFilterQuery(
  293. ScoreBuilder $scoreBuilder,
  294. FilterQuery $query,
  295. Select $select,
  296. $conditionType,
  297. QueryContainer $queryContainer
  298. ) {
  299. $scoreBuilder->startQuery();
  300. switch ($query->getReferenceType()) {
  301. case FilterQuery::REFERENCE_QUERY:
  302. $select = $this->processQuery(
  303. $scoreBuilder,
  304. $query->getReference(),
  305. $select,
  306. $conditionType,
  307. $queryContainer
  308. );
  309. $scoreBuilder->endQuery($query->getBoost());
  310. break;
  311. case FilterQuery::REFERENCE_FILTER:
  312. $filterCondition = $this->filterBuilder->build($query->getReference(), $conditionType);
  313. if ($filterCondition) {
  314. $select->where($filterCondition);
  315. }
  316. break;
  317. }
  318. $scoreBuilder->endQuery($query->getBoost());
  319. return $select;
  320. }
  321. /**
  322. * Add match queries to select.
  323. *
  324. * @param RequestInterface $request
  325. * @param QueryContainer $queryContainer
  326. * @param ScoreBuilder $scoreBuilder
  327. * @param Select $select
  328. * @param IndexBuilderInterface $indexBuilder
  329. * @return Select
  330. * @throws \Zend_Db_Exception
  331. */
  332. private function addDerivedQueries(
  333. RequestInterface $request,
  334. QueryContainer $queryContainer,
  335. ScoreBuilder $scoreBuilder,
  336. Select $select,
  337. IndexBuilderInterface $indexBuilder
  338. ) {
  339. $matchQueries = $queryContainer->getMatchQueries();
  340. if (!$matchQueries) {
  341. $select->columns($scoreBuilder->build());
  342. $select = $this->createAroundSelect($select, $scoreBuilder);
  343. } else {
  344. $matchContainer = array_shift($matchQueries);
  345. $this->matchBuilder->build(
  346. $scoreBuilder,
  347. $select,
  348. $matchContainer->getRequest(),
  349. $matchContainer->getConditionType()
  350. );
  351. $select->columns($scoreBuilder->build());
  352. $select = $this->createAroundSelect($select, $scoreBuilder);
  353. $select = $this->addMatchQueries($request, $select, $indexBuilder, $matchQueries);
  354. }
  355. return $select;
  356. }
  357. /**
  358. * Get connection.
  359. *
  360. * @return false|\Magento\Framework\DB\Adapter\AdapterInterface
  361. */
  362. private function getConnection()
  363. {
  364. return $this->resource->getConnection();
  365. }
  366. /**
  367. * Add match queries to select.
  368. *
  369. * @param RequestInterface $request
  370. * @param Select $select
  371. * @param IndexBuilderInterface $indexBuilder
  372. * @param MatchContainer[] $matchQueries
  373. * @return Select
  374. */
  375. private function addMatchQueries(
  376. RequestInterface $request,
  377. Select $select,
  378. IndexBuilderInterface $indexBuilder,
  379. array $matchQueries
  380. ) {
  381. $queriesCount = count($matchQueries);
  382. if ($queriesCount) {
  383. $table = $this->temporaryStorage->storeDocumentsFromSelect($select);
  384. foreach ($matchQueries as $matchContainer) {
  385. $queriesCount--;
  386. $matchScoreBuilder = $this->scoreBuilderFactory->create();
  387. $matchSelect = $this->matchBuilder->build(
  388. $matchScoreBuilder,
  389. $indexBuilder->build($request),
  390. $matchContainer->getRequest(),
  391. $matchContainer->getConditionType()
  392. );
  393. $select = $this->joinPreviousResultToSelect($matchSelect, $table, $matchScoreBuilder);
  394. if ($queriesCount) {
  395. $previousResultTable = $table;
  396. $table = $this->temporaryStorage->storeDocumentsFromSelect($select);
  397. $this->getConnection()->dropTable($previousResultTable->getName());
  398. }
  399. }
  400. }
  401. return $select;
  402. }
  403. /**
  404. * Join previous result to select.
  405. *
  406. * @param Select $query
  407. * @param Table $previousResultTable
  408. * @param ScoreBuilder $scoreBuilder
  409. * @return Select
  410. * @throws \Zend_Db_Exception
  411. */
  412. private function joinPreviousResultToSelect(Select $query, Table $previousResultTable, ScoreBuilder $scoreBuilder)
  413. {
  414. $query->joinInner(
  415. ['previous_results' => $previousResultTable->getName()],
  416. 'previous_results.entity_id = search_index.entity_id',
  417. []
  418. );
  419. $scoreBuilder->addCondition('previous_results.score', false);
  420. $query->columns($scoreBuilder->build());
  421. $query = $this->createAroundSelect($query, $scoreBuilder);
  422. return $query;
  423. }
  424. }