ProductDataMapper.php 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327
  1. <?php
  2. /**
  3. * Copyright © Magento, Inc. All rights reserved.
  4. * See COPYING.txt for license details.
  5. */
  6. namespace Magento\Elasticsearch\Model\Adapter\BatchDataMapper;
  7. use Magento\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider;
  8. use Magento\Eav\Model\Entity\Attribute;
  9. use Magento\Elasticsearch\Model\Adapter\Document\Builder;
  10. use Magento\Elasticsearch\Model\Adapter\FieldMapperInterface;
  11. use Magento\Elasticsearch\Model\Adapter\BatchDataMapperInterface;
  12. use Magento\Elasticsearch\Model\Adapter\FieldType\Date as DateFieldType;
  13. use Magento\AdvancedSearch\Model\Adapter\DataMapper\AdditionalFieldsProviderInterface;
  14. use Magento\Eav\Api\Data\AttributeOptionInterface;
  15. /**
  16. * Map product index data to search engine metadata
  17. */
  18. class ProductDataMapper implements BatchDataMapperInterface
  19. {
  20. /**
  21. * @var AttributeOptionInterface[]
  22. */
  23. private $attributeOptionsCache;
  24. /**
  25. * @var Builder
  26. */
  27. private $builder;
  28. /**
  29. * @var FieldMapperInterface
  30. */
  31. private $fieldMapper;
  32. /**
  33. * @var DateFieldType
  34. */
  35. private $dateFieldType;
  36. /**
  37. * @var array
  38. */
  39. private $excludedAttributes;
  40. /**
  41. * @var AdditionalFieldsProviderInterface
  42. */
  43. private $additionalFieldsProvider;
  44. /**
  45. * @var DataProvider
  46. */
  47. private $dataProvider;
  48. /**
  49. * List of attributes which will be skipped during mapping
  50. *
  51. * @var string[]
  52. */
  53. private $defaultExcludedAttributes = [
  54. 'price',
  55. 'media_gallery',
  56. 'tier_price',
  57. 'quantity_and_stock_status',
  58. 'media_gallery',
  59. 'giftcard_amounts',
  60. ];
  61. /**
  62. * @var string[]
  63. */
  64. private $attributesExcludedFromMerge = [
  65. 'status',
  66. 'visibility',
  67. 'tax_class_id'
  68. ];
  69. /**
  70. * Construction for DocumentDataMapper
  71. *
  72. * @param Builder $builder
  73. * @param FieldMapperInterface $fieldMapper
  74. * @param DateFieldType $dateFieldType
  75. * @param AdditionalFieldsProviderInterface $additionalFieldsProvider
  76. * @param DataProvider $dataProvider
  77. * @param array $excludedAttributes
  78. */
  79. public function __construct(
  80. Builder $builder,
  81. FieldMapperInterface $fieldMapper,
  82. DateFieldType $dateFieldType,
  83. AdditionalFieldsProviderInterface $additionalFieldsProvider,
  84. DataProvider $dataProvider,
  85. array $excludedAttributes = []
  86. ) {
  87. $this->builder = $builder;
  88. $this->fieldMapper = $fieldMapper;
  89. $this->dateFieldType = $dateFieldType;
  90. $this->excludedAttributes = array_merge($this->defaultExcludedAttributes, $excludedAttributes);
  91. $this->additionalFieldsProvider = $additionalFieldsProvider;
  92. $this->dataProvider = $dataProvider;
  93. $this->attributeOptionsCache = [];
  94. }
  95. /**
  96. * Map index data for using in search engine metadata
  97. *
  98. * @param array $documentData
  99. * @param int $storeId
  100. * @param array $context
  101. * @return array
  102. */
  103. public function map(array $documentData, $storeId, array $context = [])
  104. {
  105. $documents = [];
  106. foreach ($documentData as $productId => $indexData) {
  107. $this->builder->addField('store_id', $storeId);
  108. $productIndexData = $this->convertToProductData($productId, $indexData, $storeId);
  109. foreach ($productIndexData as $attributeCode => $value) {
  110. // Prepare processing attribute info
  111. if (strpos($attributeCode, '_value') !== false) {
  112. $this->builder->addField($attributeCode, $value);
  113. continue;
  114. }
  115. $this->builder->addField(
  116. $this->fieldMapper->getFieldName(
  117. $attributeCode,
  118. $context
  119. ),
  120. $value
  121. );
  122. }
  123. $documents[$productId] = $this->builder->build();
  124. }
  125. $productIds = array_keys($documentData);
  126. foreach ($this->additionalFieldsProvider->getFields($productIds, $storeId) as $productId => $fields) {
  127. $documents[$productId] = array_merge_recursive(
  128. $documents[$productId],
  129. $this->builder->addFields($fields)->build()
  130. );
  131. }
  132. return $documents;
  133. }
  134. /**
  135. * Convert raw data retrieved from source tables to human-readable format.
  136. *
  137. * @param int $productId
  138. * @param array $indexData
  139. * @param int $storeId
  140. * @return array
  141. */
  142. private function convertToProductData(int $productId, array $indexData, int $storeId): array
  143. {
  144. $productAttributes = [];
  145. if (isset($indexData['options'])) {
  146. // cover case with "options"
  147. // see \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider::prepareProductIndex
  148. $productAttributes['options'] = $indexData['options'];
  149. unset($indexData['options']);
  150. }
  151. foreach ($indexData as $attributeId => $attributeValues) {
  152. $attribute = $this->dataProvider->getSearchableAttribute($attributeId);
  153. if (in_array($attribute->getAttributeCode(), $this->excludedAttributes, true)) {
  154. continue;
  155. }
  156. if (!\is_array($attributeValues)) {
  157. $attributeValues = [$productId => $attributeValues];
  158. }
  159. $attributeValues = $this->prepareAttributeValues($productId, $attribute, $attributeValues, $storeId);
  160. $productAttributes += $this->convertAttribute($attribute, $attributeValues);
  161. }
  162. return $productAttributes;
  163. }
  164. /**
  165. * Convert data for attribute, add {attribute_code}_value for searchable attributes, that contain actual value.
  166. *
  167. * @param Attribute $attribute
  168. * @param array $attributeValues
  169. * @return array
  170. */
  171. private function convertAttribute(Attribute $attribute, array $attributeValues): array
  172. {
  173. $productAttributes = [];
  174. $retrievedValue = $this->retrieveFieldValue($attributeValues);
  175. if ($retrievedValue) {
  176. $productAttributes[$attribute->getAttributeCode()] = $retrievedValue;
  177. if ($attribute->getIsSearchable()) {
  178. $attributeLabels = $this->getValuesLabels($attribute, $attributeValues);
  179. $retrievedLabel = $this->retrieveFieldValue($attributeLabels);
  180. if ($retrievedLabel) {
  181. $productAttributes[$attribute->getAttributeCode() . '_value'] = $retrievedLabel;
  182. }
  183. }
  184. }
  185. return $productAttributes;
  186. }
  187. /**
  188. * Prepare attribute values.
  189. *
  190. * @param int $productId
  191. * @param Attribute $attribute
  192. * @param array $attributeValues
  193. * @param int $storeId
  194. * @return array
  195. */
  196. private function prepareAttributeValues(
  197. int $productId,
  198. Attribute $attribute,
  199. array $attributeValues,
  200. int $storeId
  201. ): array {
  202. if (in_array($attribute->getAttributeCode(), $this->attributesExcludedFromMerge, true)) {
  203. $attributeValues = [
  204. $productId => $attributeValues[$productId] ?? '',
  205. ];
  206. }
  207. if ($attribute->getFrontendInput() === 'multiselect') {
  208. $attributeValues = $this->prepareMultiselectValues($attributeValues);
  209. }
  210. if ($this->isAttributeDate($attribute)) {
  211. foreach ($attributeValues as $key => $attributeValue) {
  212. $attributeValues[$key] = $this->dateFieldType->formatDate($storeId, $attributeValue);
  213. }
  214. }
  215. return $attributeValues;
  216. }
  217. /**
  218. * Prepare multiselect values.
  219. *
  220. * @param array $values
  221. * @return array
  222. */
  223. private function prepareMultiselectValues(array $values): array
  224. {
  225. return \array_merge(...\array_map(function (string $value) {
  226. return \explode(',', $value);
  227. }, $values));
  228. }
  229. /**
  230. * Is attribute date.
  231. *
  232. * @param Attribute $attribute
  233. * @return bool
  234. */
  235. private function isAttributeDate(Attribute $attribute): bool
  236. {
  237. return $attribute->getFrontendInput() === 'date'
  238. || in_array($attribute->getBackendType(), ['datetime', 'timestamp'], true);
  239. }
  240. /**
  241. * Get values labels.
  242. *
  243. * @param Attribute $attribute
  244. * @param array $attributeValues
  245. * @return array
  246. */
  247. private function getValuesLabels(Attribute $attribute, array $attributeValues): array
  248. {
  249. $attributeLabels = [];
  250. $options = $this->getAttributeOptions($attribute);
  251. if (empty($options)) {
  252. return $attributeLabels;
  253. }
  254. foreach ($options as $option) {
  255. if (\in_array($option->getValue(), $attributeValues)) {
  256. $attributeLabels[] = $option->getLabel();
  257. }
  258. }
  259. return $attributeLabels;
  260. }
  261. /**
  262. * Retrieve options for attribute
  263. *
  264. * @param Attribute $attribute
  265. * @return array
  266. */
  267. private function getAttributeOptions(Attribute $attribute): array
  268. {
  269. if (!isset($this->attributeOptionsCache[$attribute->getId()])) {
  270. $options = $attribute->getOptions() ?? [];
  271. $this->attributeOptionsCache[$attribute->getId()] = $options;
  272. }
  273. return $this->attributeOptionsCache[$attribute->getId()];
  274. }
  275. /**
  276. * Retrieve value for field. If field have only one value this method return it.
  277. * Otherwise will be returned array of these values.
  278. * Note: array of values must have index keys, not as associative array.
  279. *
  280. * @param array $values
  281. * @return array|string
  282. */
  283. private function retrieveFieldValue(array $values)
  284. {
  285. $values = \array_filter(\array_unique($values));
  286. return count($values) === 1 ? \array_shift($values) : \array_values($values);
  287. }
  288. }