Suggestions.php 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275
  1. <?php
  2. /**
  3. * Copyright © Magento, Inc. All rights reserved.
  4. * See COPYING.txt for license details.
  5. */
  6. namespace Magento\Elasticsearch6\Model\DataProvider;
  7. use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProviderInterface;
  8. use Magento\Store\Model\ScopeInterface;
  9. use Magento\Search\Model\QueryInterface;
  10. use Magento\AdvancedSearch\Model\SuggestedQueriesInterface;
  11. use Magento\Elasticsearch\Model\Config;
  12. use Magento\Elasticsearch\SearchAdapter\ConnectionManager;
  13. use Magento\Search\Model\QueryResultFactory;
  14. use Magento\Framework\App\Config\ScopeConfigInterface;
  15. use Magento\Elasticsearch\SearchAdapter\SearchIndexNameResolver;
  16. use Magento\Store\Model\StoreManagerInterface as StoreManager;
  17. /**
  18. * Class Suggestions
  19. */
  20. class Suggestions implements SuggestedQueriesInterface
  21. {
  22. /**
  23. * @var Config
  24. */
  25. private $config;
  26. /**
  27. * @var QueryResultFactory
  28. */
  29. private $queryResultFactory;
  30. /**
  31. * @var ConnectionManager
  32. */
  33. private $connectionManager;
  34. /**
  35. * @var ScopeConfigInterface
  36. */
  37. private $scopeConfig;
  38. /**
  39. * @var SearchIndexNameResolver
  40. */
  41. private $searchIndexNameResolver;
  42. /**
  43. * @var StoreManager
  44. */
  45. private $storeManager;
  46. /**
  47. * @var FieldProviderInterface
  48. */
  49. private $fieldProvider;
  50. /**
  51. * Suggestions constructor.
  52. *
  53. * @param ScopeConfigInterface $scopeConfig
  54. * @param Config $config
  55. * @param QueryResultFactory $queryResultFactory
  56. * @param ConnectionManager $connectionManager
  57. * @param SearchIndexNameResolver $searchIndexNameResolver
  58. * @param StoreManager $storeManager
  59. * @param FieldProviderInterface $fieldProvider
  60. */
  61. public function __construct(
  62. ScopeConfigInterface $scopeConfig,
  63. Config $config,
  64. QueryResultFactory $queryResultFactory,
  65. ConnectionManager $connectionManager,
  66. SearchIndexNameResolver $searchIndexNameResolver,
  67. StoreManager $storeManager,
  68. FieldProviderInterface $fieldProvider
  69. ) {
  70. $this->queryResultFactory = $queryResultFactory;
  71. $this->connectionManager = $connectionManager;
  72. $this->scopeConfig = $scopeConfig;
  73. $this->config = $config;
  74. $this->searchIndexNameResolver = $searchIndexNameResolver;
  75. $this->storeManager = $storeManager;
  76. $this->fieldProvider = $fieldProvider;
  77. }
  78. /**
  79. * @inheritdoc
  80. */
  81. public function getItems(QueryInterface $query)
  82. {
  83. $result = [];
  84. if ($this->isSuggestionsAllowed()) {
  85. $isResultsCountEnabled = $this->isResultsCountEnabled();
  86. foreach ($this->getSuggestions($query) as $suggestion) {
  87. $count = null;
  88. if ($isResultsCountEnabled) {
  89. $count = isset($suggestion['freq']) ? $suggestion['freq'] : null;
  90. }
  91. $result[] = $this->queryResultFactory->create(
  92. [
  93. 'queryText' => $suggestion['text'],
  94. 'resultsCount' => $count,
  95. ]
  96. );
  97. }
  98. }
  99. return $result;
  100. }
  101. /**
  102. * @inheritdoc
  103. */
  104. public function isResultsCountEnabled()
  105. {
  106. return $this->scopeConfig->isSetFlag(
  107. SuggestedQueriesInterface::SEARCH_SUGGESTION_COUNT_RESULTS_ENABLED,
  108. ScopeInterface::SCOPE_STORE
  109. );
  110. }
  111. /**
  112. * Get Suggestions
  113. *
  114. * @param QueryInterface $query
  115. *
  116. * @return array
  117. * @throws \Magento\Framework\Exception\NoSuchEntityException
  118. */
  119. private function getSuggestions(QueryInterface $query)
  120. {
  121. $suggestions = [];
  122. $searchSuggestionsCount = $this->getSearchSuggestionsCount();
  123. $searchQuery = $this->initQuery($query);
  124. $searchQuery = $this->addSuggestFields($searchQuery, $searchSuggestionsCount);
  125. $result = $this->fetchQuery($searchQuery);
  126. if (is_array($result)) {
  127. foreach ($result['suggest'] ?? [] as $suggest) {
  128. foreach ($suggest as $token) {
  129. foreach ($token['options'] ?? [] as $key => $suggestion) {
  130. $suggestions[$suggestion['score'] . '_' . $key] = $suggestion;
  131. }
  132. }
  133. }
  134. ksort($suggestions);
  135. $texts = array_unique(array_column($suggestions, 'text'));
  136. $suggestions = array_slice(
  137. array_intersect_key(array_values($suggestions), $texts),
  138. 0,
  139. $searchSuggestionsCount
  140. );
  141. }
  142. return $suggestions;
  143. }
  144. /**
  145. * Init Search Query
  146. *
  147. * @param string $query
  148. *
  149. * @return array
  150. * @throws \Magento\Framework\Exception\NoSuchEntityException
  151. */
  152. private function initQuery($query)
  153. {
  154. $searchQuery = [
  155. 'index' => $this->searchIndexNameResolver->getIndexName(
  156. $this->storeManager->getStore()->getId(),
  157. Config::ELASTICSEARCH_TYPE_DEFAULT
  158. ),
  159. 'type' => Config::ELASTICSEARCH_TYPE_DEFAULT,
  160. 'body' => [
  161. 'suggest' => [
  162. 'text' => $query->getQueryText()
  163. ]
  164. ],
  165. ];
  166. return $searchQuery;
  167. }
  168. /**
  169. * Build Suggest on searchable fields.
  170. *
  171. * @param array $searchQuery
  172. * @param int $searchSuggestionsCount
  173. *
  174. * @return array
  175. */
  176. private function addSuggestFields($searchQuery, $searchSuggestionsCount)
  177. {
  178. $fields = $this->getSuggestFields();
  179. foreach ($fields as $field) {
  180. $searchQuery['body']['suggest']['phrase_' . $field] = [
  181. 'phrase' => [
  182. 'field' => $field,
  183. 'analyzer' => 'standard',
  184. 'size' => $searchSuggestionsCount,
  185. 'max_errors' => 1,
  186. 'direct_generator' => [
  187. [
  188. 'field' => $field,
  189. 'min_word_length' => 3,
  190. 'min_doc_freq' => 1,
  191. ]
  192. ],
  193. ],
  194. ];
  195. }
  196. return $searchQuery;
  197. }
  198. /**
  199. * Get fields to build suggest query on.
  200. *
  201. * @return array
  202. */
  203. private function getSuggestFields()
  204. {
  205. $fields = array_filter($this->fieldProvider->getFields(), function ($field) {
  206. return (($field['type'] ?? null) === 'text') && (($field['index'] ?? null) !== false);
  207. });
  208. return array_keys($fields);
  209. }
  210. /**
  211. * Fetch Query
  212. *
  213. * @param array $query
  214. * @return array
  215. */
  216. private function fetchQuery(array $query)
  217. {
  218. return $this->connectionManager->getConnection()->query($query);
  219. }
  220. /**
  221. * Get search suggestions Max Count from config
  222. *
  223. * @return int
  224. */
  225. private function getSearchSuggestionsCount()
  226. {
  227. return (int) $this->scopeConfig->getValue(
  228. SuggestedQueriesInterface::SEARCH_SUGGESTION_COUNT,
  229. ScopeInterface::SCOPE_STORE
  230. );
  231. }
  232. /**
  233. * Is Search Suggestions Allowed
  234. *
  235. * @return bool
  236. */
  237. private function isSuggestionsAllowed()
  238. {
  239. $isSuggestionsEnabled = $this->scopeConfig->isSetFlag(
  240. SuggestedQueriesInterface::SEARCH_SUGGESTION_ENABLED,
  241. ScopeInterface::SCOPE_STORE
  242. );
  243. $isEnabled = $this->config->isElasticsearchEnabled();
  244. $isSuggestionsAllowed = ($isEnabled && $isSuggestionsEnabled);
  245. return $isSuggestionsAllowed;
  246. }
  247. }