Elasticsearch.php 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395
  1. <?php
  2. /**
  3. * Copyright © Magento, Inc. All rights reserved.
  4. * See COPYING.txt for license details.
  5. */
  6. namespace Magento\Elasticsearch\Elasticsearch5\Model\Client;
  7. use Magento\Framework\Exception\LocalizedException;
  8. use Magento\AdvancedSearch\Model\Client\ClientInterface;
  9. /**
  10. * Elasticsearch client
  11. */
  12. class Elasticsearch implements ClientInterface
  13. {
  14. /**
  15. * Elasticsearch Client instances
  16. *
  17. * @var \Elasticsearch\Client[]
  18. */
  19. private $client;
  20. /**
  21. * @var array
  22. */
  23. private $clientOptions;
  24. /**
  25. * @var bool
  26. */
  27. private $pingResult;
  28. /**
  29. * @var string
  30. */
  31. private $serverVersion;
  32. /**
  33. * Initialize Elasticsearch Client
  34. *
  35. * @param array $options
  36. * @param \Elasticsearch\Client|null $elasticsearchClient
  37. * @throws LocalizedException
  38. */
  39. public function __construct(
  40. $options = [],
  41. $elasticsearchClient = null
  42. ) {
  43. if (empty($options['hostname']) || ((!empty($options['enableAuth']) &&
  44. ($options['enableAuth'] == 1)) && (empty($options['username']) || empty($options['password'])))) {
  45. throw new LocalizedException(
  46. __('The search failed because of a search engine misconfiguration.')
  47. );
  48. }
  49. if (!($elasticsearchClient instanceof \Elasticsearch\Client)) {
  50. $config = $this->buildConfig($options);
  51. $elasticsearchClient = \Elasticsearch\ClientBuilder::fromConfig($config, true);
  52. }
  53. $this->client[getmypid()] = $elasticsearchClient;
  54. $this->clientOptions = $options;
  55. }
  56. /**
  57. * Get Elasticsearch Client
  58. *
  59. * @return \Elasticsearch\Client
  60. */
  61. private function getClient()
  62. {
  63. $pid = getmypid();
  64. if (!isset($this->client[$pid])) {
  65. $config = $this->buildConfig($this->clientOptions);
  66. $this->client[$pid] = \Elasticsearch\ClientBuilder::fromConfig($config, true);
  67. }
  68. return $this->client[$pid];
  69. }
  70. /**
  71. * Ping the Elasticsearch client
  72. *
  73. * @return bool
  74. */
  75. public function ping()
  76. {
  77. if ($this->pingResult === null) {
  78. $this->pingResult = $this->getClient()->ping(['client' => ['timeout' => $this->clientOptions['timeout']]]);
  79. }
  80. return $this->pingResult;
  81. }
  82. /**
  83. * Validate connection params
  84. *
  85. * @return bool
  86. */
  87. public function testConnection()
  88. {
  89. return $this->ping();
  90. }
  91. /**
  92. * Build config.
  93. *
  94. * @param array $options
  95. * @return array
  96. */
  97. private function buildConfig($options = [])
  98. {
  99. $host = preg_replace('/http[s]?:\/\//i', '', $options['hostname']);
  100. $protocol = parse_url($options['hostname'], PHP_URL_SCHEME);
  101. if (!$protocol) {
  102. $protocol = 'http';
  103. }
  104. if (!empty($options['port'])) {
  105. $host .= ':' . $options['port'];
  106. }
  107. if (!empty($options['enableAuth']) && ($options['enableAuth'] == 1)) {
  108. $host = sprintf('%s://%s:%s@%s', $protocol, $options['username'], $options['password'], $host);
  109. }
  110. $options['hosts'] = [$host];
  111. return $options;
  112. }
  113. /**
  114. * Performs bulk query over Elasticsearch index
  115. *
  116. * @param array $query
  117. * @return void
  118. */
  119. public function bulkQuery($query)
  120. {
  121. $this->getClient()->bulk($query);
  122. }
  123. /**
  124. * Creates an Elasticsearch index.
  125. *
  126. * @param string $index
  127. * @param array $settings
  128. * @return void
  129. */
  130. public function createIndex($index, $settings)
  131. {
  132. $this->getClient()->indices()->create([
  133. 'index' => $index,
  134. 'body' => $settings,
  135. ]);
  136. }
  137. /**
  138. * Delete an Elasticsearch index.
  139. *
  140. * @param string $index
  141. * @return void
  142. */
  143. public function deleteIndex($index)
  144. {
  145. $this->getClient()->indices()->delete(['index' => $index]);
  146. }
  147. /**
  148. * Check if index is empty.
  149. *
  150. * @param string $index
  151. * @return bool
  152. */
  153. public function isEmptyIndex($index)
  154. {
  155. $stats = $this->getClient()->indices()->stats(['index' => $index, 'metric' => 'docs']);
  156. if ($stats['indices'][$index]['primaries']['docs']['count'] == 0) {
  157. return true;
  158. }
  159. return false;
  160. }
  161. /**
  162. * Updates alias.
  163. *
  164. * @param string $alias
  165. * @param string $newIndex
  166. * @param string $oldIndex
  167. * @return void
  168. */
  169. public function updateAlias($alias, $newIndex, $oldIndex = '')
  170. {
  171. $params['body'] = ['actions' => []];
  172. if ($oldIndex) {
  173. $params['body']['actions'][] = ['remove' => ['alias' => $alias, 'index' => $oldIndex]];
  174. }
  175. if ($newIndex) {
  176. $params['body']['actions'][] = ['add' => ['alias' => $alias, 'index' => $newIndex]];
  177. }
  178. $this->getClient()->indices()->updateAliases($params);
  179. }
  180. /**
  181. * Checks whether Elasticsearch index exists
  182. *
  183. * @param string $index
  184. * @return bool
  185. */
  186. public function indexExists($index)
  187. {
  188. return $this->getClient()->indices()->exists(['index' => $index]);
  189. }
  190. /**
  191. * Exists alias.
  192. *
  193. * @param string $alias
  194. * @param string $index
  195. * @return bool
  196. */
  197. public function existsAlias($alias, $index = '')
  198. {
  199. $params = ['name' => $alias];
  200. if ($index) {
  201. $params['index'] = $index;
  202. }
  203. return $this->getClient()->indices()->existsAlias($params);
  204. }
  205. /**
  206. * Get alias.
  207. *
  208. * @param string $alias
  209. * @return array
  210. */
  211. public function getAlias($alias)
  212. {
  213. return $this->getClient()->indices()->getAlias(['name' => $alias]);
  214. }
  215. /**
  216. * Add mapping to Elasticsearch index
  217. *
  218. * @param array $fields
  219. * @param string $index
  220. * @param string $entityType
  221. * @return void
  222. */
  223. public function addFieldsMapping(array $fields, $index, $entityType)
  224. {
  225. $params = [
  226. 'index' => $index,
  227. 'type' => $entityType,
  228. 'body' => [
  229. $entityType => [
  230. '_all' => $this->prepareFieldInfo([
  231. 'enabled' => true,
  232. 'type' => 'text',
  233. ]),
  234. 'properties' => [],
  235. 'dynamic_templates' => [
  236. [
  237. 'price_mapping' => [
  238. 'match' => 'price_*',
  239. 'match_mapping_type' => 'string',
  240. 'mapping' => [
  241. 'type' => 'float',
  242. 'store' => true,
  243. ],
  244. ],
  245. ],
  246. [
  247. 'string_mapping' => [
  248. 'match' => '*',
  249. 'match_mapping_type' => 'string',
  250. 'mapping' => $this->prepareFieldInfo([
  251. 'type' => 'text',
  252. 'index' => false,
  253. ]),
  254. ],
  255. ],
  256. [
  257. 'position_mapping' => [
  258. 'match' => 'position_*',
  259. 'match_mapping_type' => 'string',
  260. 'mapping' => [
  261. 'type' => 'int',
  262. ],
  263. ],
  264. ],
  265. ],
  266. ],
  267. ],
  268. ];
  269. foreach ($fields as $field => $fieldInfo) {
  270. $params['body'][$entityType]['properties'][$field] = $this->prepareFieldInfo($fieldInfo);
  271. }
  272. $this->getClient()->indices()->putMapping($params);
  273. }
  274. /**
  275. * Fix backward compatibility of field definition. Allow to run both 2.x and 5.x servers.
  276. *
  277. * @param array $fieldInfo
  278. *
  279. * @return array
  280. */
  281. private function prepareFieldInfo($fieldInfo)
  282. {
  283. if (strcmp($this->getServerVersion(), '5') < 0) {
  284. if ($fieldInfo['type'] == 'keyword') {
  285. $fieldInfo['type'] = 'string';
  286. $fieldInfo['index'] = isset($fieldInfo['index']) ? $fieldInfo['index'] : 'not_analyzed';
  287. }
  288. if ($fieldInfo['type'] == 'text') {
  289. $fieldInfo['type'] = 'string';
  290. }
  291. }
  292. return $fieldInfo;
  293. }
  294. /**
  295. * Delete mapping in Elasticsearch index
  296. *
  297. * @param string $index
  298. * @param string $entityType
  299. * @return void
  300. */
  301. public function deleteMapping($index, $entityType)
  302. {
  303. $this->getClient()->indices()->deleteMapping([
  304. 'index' => $index,
  305. 'type' => $entityType,
  306. ]);
  307. }
  308. /**
  309. * Execute search by $query
  310. *
  311. * @param array $query
  312. * @return array
  313. */
  314. public function query($query)
  315. {
  316. $query = $this->prepareSearchQuery($query);
  317. return $this->getClient()->search($query);
  318. }
  319. /**
  320. * Fix backward compatibility of the search queries. Allow to run both 2.x and 5.x servers.
  321. *
  322. * @param array $query
  323. *
  324. * @return array
  325. */
  326. private function prepareSearchQuery($query)
  327. {
  328. if (strcmp($this->getServerVersion(), '5') < 0) {
  329. if (isset($query['body']) && isset($query['body']['stored_fields'])) {
  330. $query['body']['fields'] = $query['body']['stored_fields'];
  331. unset($query['body']['stored_fields']);
  332. }
  333. }
  334. return $query;
  335. }
  336. /**
  337. * Execute suggest query
  338. *
  339. * @param array $query
  340. * @return array
  341. */
  342. public function suggest($query)
  343. {
  344. return $this->getClient()->suggest($query);
  345. }
  346. /**
  347. * Retrieve ElasticSearch server current version.
  348. *
  349. * @return string
  350. */
  351. private function getServerVersion()
  352. {
  353. if ($this->serverVersion === null) {
  354. $info = $this->getClient()->info();
  355. $this->serverVersion = $info['version']['number'];
  356. }
  357. return $this->serverVersion;
  358. }
  359. }