Elasticsearch.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384
  1. <?php
  2. /**
  3. * Copyright © Magento, Inc. All rights reserved.
  4. * See COPYING.txt for license details.
  5. */
  6. namespace Magento\Elasticsearch\Model\Adapter;
  7. use Magento\Framework\App\ObjectManager;
  8. /**
  9. * Elasticsearch adapter
  10. * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
  11. */
  12. class Elasticsearch
  13. {
  14. /**#@+
  15. * Text flags for Elasticsearch bulk actions
  16. */
  17. const BULK_ACTION_INDEX = 'index';
  18. const BULK_ACTION_CREATE = 'create';
  19. const BULK_ACTION_DELETE = 'delete';
  20. const BULK_ACTION_UPDATE = 'update';
  21. /**#@-*/
  22. /**
  23. * Buffer for total fields limit in mapping.
  24. */
  25. private const MAPPING_TOTAL_FIELDS_BUFFER_LIMIT = 1000;
  26. /**#@-*/
  27. protected $connectionManager;
  28. /**
  29. * @var DataMapperInterface
  30. * @deprecated 100.2.0 Will be replaced with BatchDataMapperInterface
  31. */
  32. protected $documentDataMapper;
  33. /**
  34. * @var \Magento\Elasticsearch\Model\Adapter\Index\IndexNameResolver
  35. */
  36. protected $indexNameResolver;
  37. /**
  38. * @var FieldMapperInterface
  39. */
  40. protected $fieldMapper;
  41. /**
  42. * @var \Magento\Elasticsearch\Model\Config
  43. */
  44. protected $clientConfig;
  45. /**
  46. * @var \Magento\Elasticsearch\Model\Client\Elasticsearch
  47. */
  48. protected $client;
  49. /**
  50. * @var \Magento\Elasticsearch\Model\Adapter\Index\BuilderInterface
  51. */
  52. protected $indexBuilder;
  53. /**
  54. * @var \Psr\Log\LoggerInterface
  55. */
  56. protected $logger;
  57. /**
  58. * @var array
  59. */
  60. protected $preparedIndex = [];
  61. /**
  62. * @var BatchDataMapperInterface
  63. */
  64. private $batchDocumentDataMapper;
  65. /**
  66. * Constructor for Elasticsearch adapter.
  67. *
  68. * @param \Magento\Elasticsearch\SearchAdapter\ConnectionManager $connectionManager
  69. * @param DataMapperInterface $documentDataMapper
  70. * @param FieldMapperInterface $fieldMapper
  71. * @param \Magento\Elasticsearch\Model\Config $clientConfig
  72. * @param \Magento\Elasticsearch\Model\Adapter\Index\BuilderInterface $indexBuilder
  73. * @param \Psr\Log\LoggerInterface $logger
  74. * @param \Magento\Elasticsearch\Model\Adapter\Index\IndexNameResolver $indexNameResolver
  75. * @param array $options
  76. * @param BatchDataMapperInterface $batchDocumentDataMapper
  77. * @throws \Magento\Framework\Exception\LocalizedException
  78. */
  79. public function __construct(
  80. \Magento\Elasticsearch\SearchAdapter\ConnectionManager $connectionManager,
  81. DataMapperInterface $documentDataMapper,
  82. FieldMapperInterface $fieldMapper,
  83. \Magento\Elasticsearch\Model\Config $clientConfig,
  84. \Magento\Elasticsearch\Model\Adapter\Index\BuilderInterface $indexBuilder,
  85. \Psr\Log\LoggerInterface $logger,
  86. \Magento\Elasticsearch\Model\Adapter\Index\IndexNameResolver $indexNameResolver,
  87. $options = [],
  88. BatchDataMapperInterface $batchDocumentDataMapper = null
  89. ) {
  90. $this->connectionManager = $connectionManager;
  91. $this->documentDataMapper = $documentDataMapper;
  92. $this->fieldMapper = $fieldMapper;
  93. $this->clientConfig = $clientConfig;
  94. $this->indexBuilder = $indexBuilder;
  95. $this->logger = $logger;
  96. $this->indexNameResolver = $indexNameResolver;
  97. $this->batchDocumentDataMapper = $batchDocumentDataMapper ?:
  98. ObjectManager::getInstance()->get(BatchDataMapperInterface::class);
  99. try {
  100. $this->client = $this->connectionManager->getConnection($options);
  101. } catch (\Exception $e) {
  102. $this->logger->critical($e);
  103. throw new \Magento\Framework\Exception\LocalizedException(
  104. __('The search failed because of a search engine misconfiguration.')
  105. );
  106. }
  107. }
  108. /**
  109. * Retrieve Elasticsearch server status
  110. *
  111. * @return bool
  112. * @throws \Magento\Framework\Exception\LocalizedException
  113. */
  114. public function ping()
  115. {
  116. try {
  117. $response = $this->client->ping();
  118. } catch (\Exception $e) {
  119. throw new \Magento\Framework\Exception\LocalizedException(
  120. __('Could not ping search engine: %1', $e->getMessage())
  121. );
  122. }
  123. return $response;
  124. }
  125. /**
  126. * Create Elasticsearch documents by specified data
  127. *
  128. * @param array $documentData
  129. * @param int $storeId
  130. * @return array
  131. */
  132. public function prepareDocsPerStore(array $documentData, $storeId)
  133. {
  134. $documents = [];
  135. if (count($documentData)) {
  136. $documents = $this->batchDocumentDataMapper->map(
  137. $documentData,
  138. $storeId
  139. );
  140. }
  141. return $documents;
  142. }
  143. /**
  144. * Add prepared Elasticsearch documents to Elasticsearch index
  145. *
  146. * @param array $documents
  147. * @param int $storeId
  148. * @param string $mappedIndexerId
  149. * @return $this
  150. * @throws \Exception
  151. */
  152. public function addDocs(array $documents, $storeId, $mappedIndexerId)
  153. {
  154. if (count($documents)) {
  155. try {
  156. $indexName = $this->indexNameResolver->getIndexName($storeId, $mappedIndexerId, $this->preparedIndex);
  157. $bulkIndexDocuments = $this->getDocsArrayInBulkIndexFormat($documents, $indexName);
  158. $this->client->bulkQuery($bulkIndexDocuments);
  159. } catch (\Exception $e) {
  160. $this->logger->critical($e);
  161. throw $e;
  162. }
  163. }
  164. return $this;
  165. }
  166. /**
  167. * Removes all documents from Elasticsearch index
  168. *
  169. * @param int $storeId
  170. * @param string $mappedIndexerId
  171. * @return $this
  172. */
  173. public function cleanIndex($storeId, $mappedIndexerId)
  174. {
  175. $this->checkIndex($storeId, $mappedIndexerId, true);
  176. $indexName = $this->indexNameResolver->getIndexName($storeId, $mappedIndexerId, $this->preparedIndex);
  177. if ($this->client->isEmptyIndex($indexName)) {
  178. // use existing index if empty
  179. return $this;
  180. }
  181. // prepare new index name and increase version
  182. $indexPattern = $this->indexNameResolver->getIndexPattern($storeId, $mappedIndexerId);
  183. $version = (int)(str_replace($indexPattern, '', $indexName));
  184. $newIndexName = $indexPattern . ++$version;
  185. // remove index if already exists
  186. if ($this->client->indexExists($newIndexName)) {
  187. $this->client->deleteIndex($newIndexName);
  188. }
  189. // prepare new index
  190. $this->prepareIndex($storeId, $newIndexName, $mappedIndexerId);
  191. return $this;
  192. }
  193. /**
  194. * Delete documents from Elasticsearch index by Ids
  195. *
  196. * @param array $documentIds
  197. * @param int $storeId
  198. * @param string $mappedIndexerId
  199. * @return $this
  200. * @throws \Exception
  201. */
  202. public function deleteDocs(array $documentIds, $storeId, $mappedIndexerId)
  203. {
  204. try {
  205. $this->checkIndex($storeId, $mappedIndexerId, false);
  206. $indexName = $this->indexNameResolver->getIndexName($storeId, $mappedIndexerId, $this->preparedIndex);
  207. $bulkDeleteDocuments = $this->getDocsArrayInBulkIndexFormat(
  208. $documentIds,
  209. $indexName,
  210. self::BULK_ACTION_DELETE
  211. );
  212. $this->client->bulkQuery($bulkDeleteDocuments);
  213. } catch (\Exception $e) {
  214. $this->logger->critical($e);
  215. throw $e;
  216. }
  217. return $this;
  218. }
  219. /**
  220. * Reformat documents array to bulk format
  221. *
  222. * @param array $documents
  223. * @param string $indexName
  224. * @param string $action
  225. * @return array
  226. */
  227. protected function getDocsArrayInBulkIndexFormat(
  228. $documents,
  229. $indexName,
  230. $action = self::BULK_ACTION_INDEX
  231. ) {
  232. $bulkArray = [
  233. 'index' => $indexName,
  234. 'type' => $this->clientConfig->getEntityType(),
  235. 'body' => [],
  236. 'refresh' => true,
  237. ];
  238. foreach ($documents as $id => $document) {
  239. $bulkArray['body'][] = [
  240. $action => [
  241. '_id' => $id,
  242. '_type' => $this->clientConfig->getEntityType(),
  243. '_index' => $indexName
  244. ]
  245. ];
  246. if ($action == self::BULK_ACTION_INDEX) {
  247. $bulkArray['body'][] = $document;
  248. }
  249. }
  250. return $bulkArray;
  251. }
  252. /**
  253. * Checks whether Elasticsearch index and alias exists.
  254. *
  255. * @param int $storeId
  256. * @param string $mappedIndexerId
  257. * @param bool $checkAlias
  258. *
  259. * @return $this
  260. */
  261. public function checkIndex(
  262. $storeId,
  263. $mappedIndexerId,
  264. $checkAlias = true
  265. ) {
  266. // create new index for store
  267. $indexName = $this->indexNameResolver->getIndexName($storeId, $mappedIndexerId, $this->preparedIndex);
  268. if (!$this->client->indexExists($indexName)) {
  269. $this->prepareIndex($storeId, $indexName, $mappedIndexerId);
  270. }
  271. // add index to alias
  272. if ($checkAlias) {
  273. $namespace = $this->indexNameResolver->getIndexNameForAlias($storeId, $mappedIndexerId);
  274. if (!$this->client->existsAlias($namespace, $indexName)) {
  275. $this->client->updateAlias($namespace, $indexName);
  276. }
  277. }
  278. return $this;
  279. }
  280. /**
  281. * Update Elasticsearch alias for new index.
  282. *
  283. * @param int $storeId
  284. * @param string $mappedIndexerId
  285. * @return $this
  286. */
  287. public function updateAlias($storeId, $mappedIndexerId)
  288. {
  289. if (!isset($this->preparedIndex[$storeId])) {
  290. return $this;
  291. }
  292. $oldIndex = $this->indexNameResolver->getIndexFromAlias($storeId, $mappedIndexerId);
  293. if ($oldIndex == $this->preparedIndex[$storeId]) {
  294. $oldIndex = '';
  295. }
  296. $this->client->updateAlias(
  297. $this->indexNameResolver->getIndexNameForAlias($storeId, $mappedIndexerId),
  298. $this->preparedIndex[$storeId],
  299. $oldIndex
  300. );
  301. // remove obsolete index
  302. if ($oldIndex) {
  303. $this->client->deleteIndex($oldIndex);
  304. }
  305. return $this;
  306. }
  307. /**
  308. * Create new index with mapping.
  309. *
  310. * @param int $storeId
  311. * @param string $indexName
  312. * @param string $mappedIndexerId
  313. * @return $this
  314. */
  315. protected function prepareIndex($storeId, $indexName, $mappedIndexerId)
  316. {
  317. $this->indexBuilder->setStoreId($storeId);
  318. $settings = $this->indexBuilder->build();
  319. $allAttributeTypes = $this->fieldMapper->getAllAttributesTypes([
  320. 'entityType' => $mappedIndexerId,
  321. // Use store id instead of website id from context for save existing fields mapping.
  322. // In future websiteId will be eliminated due to index stored per store
  323. 'websiteId' => $storeId
  324. ]);
  325. $settings['index']['mapping']['total_fields']['limit'] = $this->getMappingTotalFieldsLimit($allAttributeTypes);
  326. $this->client->createIndex($indexName, ['settings' => $settings]);
  327. $this->client->addFieldsMapping(
  328. $allAttributeTypes,
  329. $indexName,
  330. $this->clientConfig->getEntityType()
  331. );
  332. $this->preparedIndex[$storeId] = $indexName;
  333. return $this;
  334. }
  335. /**
  336. * Get total fields limit for mapping.
  337. *
  338. * @param array $allAttributeTypes
  339. * @return int
  340. */
  341. private function getMappingTotalFieldsLimit(array $allAttributeTypes): int
  342. {
  343. return count($allAttributeTypes) + self::MAPPING_TOTAL_FIELDS_BUFFER_LIMIT;
  344. }
  345. }