RequestGenerator.php 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256
  1. <?php
  2. /**
  3. * Copyright © Magento, Inc. All rights reserved.
  4. * See COPYING.txt for license details.
  5. */
  6. namespace Magento\CatalogSearch\Model\Search;
  7. use Magento\Catalog\Api\Data\EavAttributeInterface;
  8. use Magento\Catalog\Model\Entity\Attribute;
  9. use Magento\Catalog\Model\ResourceModel\Product\Attribute\CollectionFactory;
  10. use Magento\CatalogSearch\Model\Search\RequestGenerator\GeneratorResolver;
  11. use Magento\Framework\App\ObjectManager;
  12. use Magento\Framework\Search\Request\FilterInterface;
  13. use Magento\Framework\Search\Request\QueryInterface;
  14. /**
  15. * Catalog search request generator.
  16. *
  17. * @api
  18. * @since 100.0.2
  19. */
  20. class RequestGenerator
  21. {
  22. /** Filter name suffix */
  23. const FILTER_SUFFIX = '_filter';
  24. /** Bucket name suffix */
  25. const BUCKET_SUFFIX = '_bucket';
  26. /**
  27. * @var CollectionFactory
  28. */
  29. private $productAttributeCollectionFactory;
  30. /**
  31. * @var GeneratorResolver
  32. */
  33. private $generatorResolver;
  34. /**
  35. * @param CollectionFactory $productAttributeCollectionFactory
  36. * @param GeneratorResolver $generatorResolver
  37. */
  38. public function __construct(
  39. CollectionFactory $productAttributeCollectionFactory,
  40. GeneratorResolver $generatorResolver = null
  41. ) {
  42. $this->productAttributeCollectionFactory = $productAttributeCollectionFactory;
  43. $this->generatorResolver = $generatorResolver
  44. ?: ObjectManager::getInstance()->get(GeneratorResolver::class);
  45. }
  46. /**
  47. * Generate dynamic fields requests
  48. *
  49. * @return array
  50. */
  51. public function generate()
  52. {
  53. $requests = [];
  54. $requests['catalog_view_container'] =
  55. $this->generateRequest(EavAttributeInterface::IS_FILTERABLE, 'catalog_view_container', false);
  56. $requests['quick_search_container'] =
  57. $this->generateRequest(EavAttributeInterface::IS_FILTERABLE_IN_SEARCH, 'quick_search_container', true);
  58. $requests['advanced_search_container'] = $this->generateAdvancedSearchRequest();
  59. return $requests;
  60. }
  61. /**
  62. * Generate search request
  63. *
  64. * @param string $attributeType
  65. * @param string $container
  66. * @param bool $useFulltext
  67. * @return array
  68. */
  69. private function generateRequest($attributeType, $container, $useFulltext)
  70. {
  71. $request = [];
  72. foreach ($this->getSearchableAttributes() as $attribute) {
  73. if ($attribute->getData($attributeType)) {
  74. if (!in_array($attribute->getAttributeCode(), ['price', 'category_ids'], true)) {
  75. $queryName = $attribute->getAttributeCode() . '_query';
  76. $request['queries'][$container]['queryReference'][] = [
  77. 'clause' => 'must',
  78. 'ref' => $queryName,
  79. ];
  80. $filterName = $attribute->getAttributeCode() . self::FILTER_SUFFIX;
  81. $request['queries'][$queryName] = [
  82. 'name' => $queryName,
  83. 'type' => QueryInterface::TYPE_FILTER,
  84. 'filterReference' => [
  85. [
  86. 'clause' => 'must',
  87. 'ref' => $filterName,
  88. ]
  89. ],
  90. ];
  91. $bucketName = $attribute->getAttributeCode() . self::BUCKET_SUFFIX;
  92. $generator = $this->generatorResolver->getGeneratorForType($attribute->getBackendType());
  93. $request['filters'][$filterName] = $generator->getFilterData($attribute, $filterName);
  94. $request['aggregations'][$bucketName] = $generator->getAggregationData($attribute, $bucketName);
  95. }
  96. }
  97. /** @var $attribute Attribute */
  98. if (!$attribute->getIsSearchable() || in_array($attribute->getAttributeCode(), ['price', 'sku'], true)) {
  99. // Some fields have their own specific handlers
  100. continue;
  101. }
  102. $request = $this->processPriceAttribute($useFulltext, $attribute, $request);
  103. }
  104. return $request;
  105. }
  106. /**
  107. * Retrieve searchable attributes
  108. *
  109. * @return \Magento\Catalog\Model\ResourceModel\Product\Attribute\Collection
  110. */
  111. protected function getSearchableAttributes()
  112. {
  113. /** @var \Magento\Catalog\Model\ResourceModel\Product\Attribute\Collection $productAttributes */
  114. $productAttributes = $this->productAttributeCollectionFactory->create();
  115. $productAttributes->addFieldToFilter(
  116. ['is_searchable', 'is_visible_in_advanced_search', 'is_filterable', 'is_filterable_in_search'],
  117. [1, 1, [1, 2], 1]
  118. );
  119. return $productAttributes;
  120. }
  121. /**
  122. * Generate advanced search request
  123. *
  124. * @return array
  125. * @SuppressWarnings(PHPMD.CyclomaticComplexity)
  126. */
  127. private function generateAdvancedSearchRequest()
  128. {
  129. $request = [];
  130. foreach ($this->getSearchableAttributes() as $attribute) {
  131. /** @var $attribute Attribute */
  132. if (!$attribute->getIsVisibleInAdvancedSearch()) {
  133. continue;
  134. }
  135. if (in_array($attribute->getAttributeCode(), ['price', 'sku'])) {
  136. //same fields have special semantics
  137. continue;
  138. }
  139. $queryName = $attribute->getAttributeCode() . '_query';
  140. $request['queries']['advanced_search_container']['queryReference'][] = [
  141. 'clause' => 'must',
  142. 'ref' => $queryName,
  143. ];
  144. switch ($attribute->getBackendType()) {
  145. case 'static':
  146. break;
  147. case 'text':
  148. case 'varchar':
  149. if ($attribute->getFrontendInput() === 'multiselect') {
  150. $filterName = $attribute->getAttributeCode() . self::FILTER_SUFFIX;
  151. $request['queries'][$queryName] = [
  152. 'name' => $queryName,
  153. 'type' => QueryInterface::TYPE_FILTER,
  154. 'filterReference' => [
  155. [
  156. 'ref' => $filterName,
  157. ],
  158. ],
  159. ];
  160. $request['filters'][$filterName] = [
  161. 'type' => FilterInterface::TYPE_TERM,
  162. 'name' => $filterName,
  163. 'field' => $attribute->getAttributeCode(),
  164. 'value' => '$' . $attribute->getAttributeCode() . '$',
  165. ];
  166. } else {
  167. $request['queries'][$queryName] = [
  168. 'name' => $queryName,
  169. 'type' => 'matchQuery',
  170. 'value' => '$' . $attribute->getAttributeCode() . '$',
  171. 'match' => [
  172. [
  173. 'field' => $attribute->getAttributeCode(),
  174. 'boost' => $attribute->getSearchWeight() ?: 1,
  175. ],
  176. ],
  177. ];
  178. }
  179. break;
  180. case 'decimal':
  181. case 'datetime':
  182. case 'date':
  183. $filterName = $attribute->getAttributeCode() . self::FILTER_SUFFIX;
  184. $request['queries'][$queryName] = [
  185. 'name' => $queryName,
  186. 'type' => QueryInterface::TYPE_FILTER,
  187. 'filterReference' => [
  188. [
  189. 'ref' => $filterName,
  190. ],
  191. ],
  192. ];
  193. $request['filters'][$filterName] = [
  194. 'field' => $attribute->getAttributeCode(),
  195. 'name' => $filterName,
  196. 'type' => FilterInterface::TYPE_RANGE,
  197. 'from' => '$' . $attribute->getAttributeCode() . '.from$',
  198. 'to' => '$' . $attribute->getAttributeCode() . '.to$',
  199. ];
  200. break;
  201. default:
  202. $filterName = $attribute->getAttributeCode() . self::FILTER_SUFFIX;
  203. $request['queries'][$queryName] = [
  204. 'name' => $queryName,
  205. 'type' => QueryInterface::TYPE_FILTER,
  206. 'filterReference' => [
  207. [
  208. 'ref' => $filterName,
  209. ],
  210. ],
  211. ];
  212. $request['filters'][$filterName] = [
  213. 'type' => FilterInterface::TYPE_TERM,
  214. 'name' => $filterName,
  215. 'field' => $attribute->getAttributeCode(),
  216. 'value' => '$' . $attribute->getAttributeCode() . '$',
  217. ];
  218. }
  219. }
  220. return $request;
  221. }
  222. /**
  223. * Modify request for price attribute.
  224. *
  225. * @param bool $useFulltext
  226. * @param Attribute $attribute
  227. * @param array $request
  228. * @return array
  229. */
  230. private function processPriceAttribute($useFulltext, $attribute, $request)
  231. {
  232. // Match search by custom price attribute isn't supported
  233. if ($useFulltext && $attribute->getFrontendInput() !== 'price') {
  234. $request['queries']['search']['match'][] = [
  235. 'field' => $attribute->getAttributeCode(),
  236. 'boost' => $attribute->getSearchWeight() ?: 1,
  237. ];
  238. }
  239. return $request;
  240. }
  241. }