├── README.md ├── composer.json └── src ├── module-elasticsuite-autocomplete ├── Controller │ └── Index │ │ └── Index.php ├── Model │ ├── Indexer │ │ ├── Category │ │ │ ├── Breadcrumbs.php │ │ │ └── Fulltext │ │ │ │ └── Datasource │ │ │ │ ├── Breadcrumbs.php │ │ │ │ └── Url.php │ │ └── Product │ │ │ └── Fulltext │ │ │ └── Datasource │ │ │ ├── AdditionalAttributes.php │ │ │ ├── ConfigurablePrice.php │ │ │ └── Url.php │ ├── Query.php │ ├── Query │ │ ├── Category.php │ │ └── Product.php │ └── Render │ │ ├── Category.php │ │ └── Product.php ├── Plugin │ └── ImproveSearch.php ├── Provider │ └── Config.php ├── ViewModel │ └── Form.php ├── composer.json ├── etc │ ├── adminhtml │ │ └── system.xml │ ├── config.xml │ ├── di.xml │ ├── frontend │ │ └── routes.xml │ └── module.xml ├── registration.php └── view │ └── frontend │ └── layout │ └── default.xml └── pub └── search.php /README.md: -------------------------------------------------------------------------------- 1 | # Magento 2 Elastisuite Autocomplete speed up 2 | 3 | Magento 2 Module to speed autocomplete search with elasticsuite 4 | 5 | ## Features 6 | 7 | This module use : 8 | - Elasticsearch response 9 | - Load the minimum magento class to display product (price and image helper) 10 | - Return only products (with additional_attributes), categories 11 | 12 | To improve speed I try two ways of routing : 13 | - Default magento 2 routing way (by declaring routes.xml and use controller) 14 | - No routing way (use a search.php file in /pub directory) 15 | 16 | ## Installation 17 | 18 | ### Composer 19 | ``` 20 | $ composer require "web200/magento-elasticsuite-autocomplete":"*" 21 | ``` 22 | 23 | ### Or Github 24 | ``` 25 | git clone git@github.com:Web200/magento-elasticsuite-autocomplete.git 26 | ``` 27 | 28 | Copy 29 | ``` 30 | $MAGENTO_ROOT/app/code/Web200/ElasticsuiteAutocomplete/pub/search.php 31 | ``` 32 | in 33 | ``` 34 | $MAGENTO_ROOT/pub/ 35 | ``` 36 | 37 | ### Nginx specification 38 | 39 | ``` 40 | # Defaut magento installation (Nginx) protect php script execution, you need to edit your virtualhost like this : 41 | location ~ ^/(index|get|static|errors/report|errors/404|errors/503|health_check)\.php$ { 42 | # => 43 | location ~ ^/(index|get|static|errors/report|errors/404|errors/503|health_check|search)\.php$ { 44 | ``` 45 | 46 | ## Use 47 | 48 | - Reindex Magento Catalog 49 | ``` 50 | php bin/magento indexer:reindex 51 | ``` 52 | 53 | If you clone the repository don't forget to copy /pub/search.php file (file is copied automatically if you use composer install) 54 | 55 | ``` 56 | # To test the module you only need to install it, and edit this file (by overriding in your theme module): 57 | # smile/elasticsuite/src/module-elasticsuite-core/view/frontend/templates/search/form.mini.phtml 58 | 59 | # Default elasticsuite module : 60 | "url":"getUrl('search/ajax/suggest'); ?>", 61 | # This Module with default magento routing : 62 | "url":"getUrl('autocomplete'); ?>", 63 | # This Module without magento routing : 64 | "url":"getFormViewModel()->getSearchUrl() ?>", 65 | ``` 66 | 67 | ## Benchmarks 68 | 69 | For benchmarking, I use Magento 2.4.2 with sample data and all cache are active. 70 | Local ubuntu with mysql / elasticsearch / apache (no docker use) 71 | 72 | Results : 73 | 74 | * Elasticsuite last version (2.10.3) : 360ms 75 | * This Module with default magento routing : 120ms 76 | * This Module without magento routing : 80ms 77 | 78 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web200/magento-elasticsuite-autocomplete", 3 | "description": "Magento 2 Module to speed autocomplete search", 4 | "require": { 5 | "php": "^7.0|^8.0" 6 | }, 7 | "type": "magento2-component", 8 | "license": [ 9 | "OSL-3.0", 10 | "AFL-3.0" 11 | ], 12 | "replace": { 13 | "web200/module-elasticsuite-autocomplete": "self.version" 14 | }, 15 | "autoload": { 16 | "files": [ 17 | "src/module-elasticsuite-autocomplete/registration.php" 18 | ], 19 | "psr-4": { 20 | "Web200\\ElasticsuiteAutocomplete\\": "src/module-elasticsuite-autocomplete" 21 | } 22 | }, 23 | "extra": { 24 | "map": [ 25 | [ 26 | "src/pub/search.php", 27 | "pub/search.php" 28 | ] 29 | ] 30 | }, 31 | "minimum-stability": "dev", 32 | "prefer-stable": true 33 | } 34 | -------------------------------------------------------------------------------- /src/module-elasticsuite-autocomplete/Controller/Index/Index.php: -------------------------------------------------------------------------------- 1 | 18 | * @copyright 2021 Web200 19 | * @license https://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) 20 | * @link https://www.web200.fr/ 21 | */ 22 | class Index implements HttpGetActionInterface 23 | { 24 | /** 25 | * Query 26 | * 27 | * @var Query $query 28 | */ 29 | protected $query; 30 | /** 31 | * Result json factory 32 | * 33 | * @var $resultJsonFactory 34 | */ 35 | protected $resultJsonFactory; 36 | 37 | /** 38 | * Index constructor. 39 | * 40 | * @param Query $query 41 | * @param JsonFactory $resultJsonFactory 42 | */ 43 | public function __construct( 44 | Query $query, 45 | JsonFactory $resultJsonFactory 46 | ) { 47 | $this->query = $query; 48 | $this->resultJsonFactory = $resultJsonFactory; 49 | } 50 | 51 | /** 52 | * Execute action based on request and return result 53 | * 54 | * @return ResultInterface|ResponseInterface 55 | */ 56 | public function execute() 57 | { 58 | $resultJson = $this->resultJsonFactory->create(); 59 | $resultJson->setData($this->query->execute()); 60 | 61 | return $resultJson; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/module-elasticsuite-autocomplete/Model/Indexer/Category/Breadcrumbs.php: -------------------------------------------------------------------------------- 1 | 18 | * @copyright 2021 Web200 19 | * @license https://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) 20 | * @link https://www.web200.fr/ 21 | */ 22 | class Breadcrumbs 23 | { 24 | /** 25 | * Cache id categories 26 | * 27 | * @var string CACHE_ID_CATEGORIES 28 | */ 29 | protected const CACHE_ID_CATEGORIES = 'elasticautocomplete_categories'; 30 | /** 31 | * Store categories 32 | * 33 | * @var string[] $storeCategories 34 | */ 35 | protected $storeCategories; 36 | /** 37 | * Cache 38 | * 39 | * @var CacheInterface $cache 40 | */ 41 | protected $cache; 42 | /** 43 | * Category collection factory 44 | * 45 | * @var CollectionFactory $categoryCollectionFactory 46 | */ 47 | protected $categoryCollectionFactory; 48 | /** 49 | * Json 50 | * 51 | * @var Json $json 52 | */ 53 | protected $json; 54 | /** 55 | * StoreManager Interface 56 | * 57 | * @var StoreManagerInterface $storeManager 58 | */ 59 | protected $storeManager; 60 | 61 | /** 62 | * Breadcrumbs constructor. 63 | * 64 | * @param StoreManagerInterface $storeManager 65 | * @param Json $json 66 | * @param CollectionFactory $categoryCollectionFactory 67 | * @param CacheInterface $cache 68 | */ 69 | public function __construct( 70 | StoreManagerInterface $storeManager, 71 | Json $json, 72 | CollectionFactory $categoryCollectionFactory, 73 | CacheInterface $cache 74 | ) { 75 | $this->categoryCollectionFactory = $categoryCollectionFactory; 76 | $this->cache = $cache; 77 | $this->json = $json; 78 | $this->storeManager = $storeManager; 79 | } 80 | 81 | /** 82 | * buildCategoryPath 83 | * 84 | * @param $categoryPath 85 | * @param int $storeId 86 | * 87 | * @return string 88 | */ 89 | public function getCategoryFromPath($categoryPath, int $storeId): string 90 | { 91 | /* first 2 categories can be ignored */ 92 | $categoryIds = array_slice(explode('/', $categoryPath), 2); 93 | $categoriesWithNames = []; 94 | $storeCategories = $this->getStoreCategories($storeId); 95 | foreach ($categoryIds as $categoryId) { 96 | if (isset($storeCategories[$categoryId])) { 97 | $categoriesWithNames[] = $storeCategories[$categoryId]['name']; 98 | } 99 | } 100 | 101 | return implode('/', $categoriesWithNames); 102 | } 103 | 104 | /** 105 | * Get store categories 106 | * 107 | * @param int $storeId 108 | * 109 | * @return string[]|mixed 110 | */ 111 | protected function getStoreCategories(int $storeId): array 112 | { 113 | if ($this->storeCategories === null) { 114 | $this->storeCategories = []; 115 | $rootCategoryId = $this->storeManager->getStore($storeId)->getRootCategoryId(); 116 | $cacheKey = self::CACHE_ID_CATEGORIES . '-' . $rootCategoryId . '-' . $storeId; 117 | $cacheCategory = $this->cache->load($cacheKey); 118 | if (!$cacheCategory) { 119 | $categories = $this->categoryCollectionFactory->create() 120 | ->setStoreId($storeId) 121 | ->addAttributeToFilter('path', array('like' => "1/{$rootCategoryId}/%")) 122 | ->addAttributeToSelect('name'); 123 | /** @var Category $categ */ 124 | foreach ($categories as $categ) { 125 | $this->storeCategories[$categ->getData('entity_id')] = [ 126 | 'name' => $categ->getData('name'), 127 | 'path' => $categ->getData('path') 128 | ]; 129 | } 130 | $cacheCategory = $this->json->serialize($this->storeCategories); 131 | $this->cache->save($cacheCategory, $cacheKey); 132 | } 133 | 134 | $this->storeCategories = $this->json->unserialize($cacheCategory, true); 135 | } 136 | 137 | return $this->storeCategories; 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/module-elasticsuite-autocomplete/Model/Indexer/Category/Fulltext/Datasource/Breadcrumbs.php: -------------------------------------------------------------------------------- 1 | 16 | * @copyright 2021 Web200 17 | * @license https://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) 18 | * @link https://www.web200.fr/ 19 | */ 20 | class Breadcrumbs implements DatasourceInterface 21 | { 22 | /** 23 | * Resource connection 24 | * 25 | * @var ResourceConnection $resource 26 | */ 27 | protected $resource; 28 | /** 29 | * categoryBreadcrumbs 30 | * 31 | * @var CategoryBreadcrumbs $categoryBreadcrumbs 32 | */ 33 | protected $categoryBreadcrumbs; 34 | 35 | /** 36 | * Breadcrumbs constructor. 37 | * 38 | * @param ResourceConnection $resource 39 | * @param CategoryBreadcrumbs $categoryBreadcrumbs 40 | */ 41 | public function __construct( 42 | ResourceConnection $resource, 43 | CategoryBreadcrumbs $categoryBreadcrumbs 44 | ) { 45 | $this->resource = $resource; 46 | $this->categoryBreadcrumbs = $categoryBreadcrumbs; 47 | } 48 | 49 | /** 50 | * Add categories data to the index data. 51 | * 52 | * {@inheritdoc} 53 | */ 54 | public function addData($storeId, array $indexData) 55 | { 56 | /** @var string[] $data */ 57 | foreach ($indexData as $categoryId => $data) { 58 | if (!isset($data['path'])) { 59 | continue; 60 | } 61 | $indexData[$categoryId]['breadcrumb'] = $this->categoryBreadcrumbs->getCategoryFromPath( 62 | $data['path'], 63 | $storeId 64 | ); 65 | } 66 | 67 | return $indexData; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/module-elasticsuite-autocomplete/Model/Indexer/Category/Fulltext/Datasource/Url.php: -------------------------------------------------------------------------------- 1 | 15 | * @copyright 2021 Web200 16 | * @license https://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) 17 | * @link https://www.web200.fr/ 18 | */ 19 | class Url implements DatasourceInterface 20 | { 21 | /** 22 | * Resource connection 23 | * 24 | * @var ResourceConnection $resource 25 | */ 26 | protected $resource; 27 | 28 | /** 29 | * Url constructor. 30 | * 31 | * @param ResourceConnection $resource 32 | */ 33 | public function __construct( 34 | ResourceConnection $resource 35 | ) { 36 | $this->resource = $resource; 37 | } 38 | 39 | /** 40 | * Add categories data to the index data. 41 | * 42 | * {@inheritdoc} 43 | */ 44 | public function addData($storeId, array $indexData) 45 | { 46 | $requestPathData = $this->getRequestPathData((int)$storeId); 47 | /** @var string[] $data */ 48 | foreach ($requestPathData as $data) { 49 | if (isset($indexData[(int)$data['entity_id']])) { 50 | $indexData[(int)$data['entity_id']]['request_path'] = $data['request_path']; 51 | } 52 | } 53 | 54 | return $indexData; 55 | } 56 | 57 | /** 58 | * Get request path data 59 | * 60 | * @param int $storeId 61 | * 62 | * @return string[] 63 | */ 64 | protected function getRequestPathData(int $storeId): array 65 | { 66 | $connection = $this->resource->getConnection(); 67 | $select = $connection->select()->from( 68 | ['url_rewrite' => $connection->getTableName('url_rewrite')], 69 | ['request_path', 'entity_id'] 70 | )->where('entity_type = ?', 'category') 71 | ->where('store_id = ?', $storeId) 72 | ->where('redirect_type = 0') 73 | ->where('metadata IS NULL'); 74 | 75 | return $connection->fetchAll($select); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/module-elasticsuite-autocomplete/Model/Indexer/Product/Fulltext/Datasource/AdditionalAttributes.php: -------------------------------------------------------------------------------- 1 | 23 | * @copyright 2021 Web200 24 | * @license https://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) 25 | * @link https://www.web200.fr/ 26 | */ 27 | class AdditionalAttributes implements DatasourceInterface 28 | { 29 | /** 30 | * Resource connection 31 | * 32 | * @var ResourceConnection $resource 33 | */ 34 | protected $resource; 35 | /** 36 | * Attribute config 37 | * 38 | * @var AttributeConfig $attributeConfig 39 | */ 40 | protected $attributeConfig; 41 | /** 42 | * Attribute repository interface 43 | * 44 | * @var AttributeRepositoryInterface $eavAttributeRepositoryInterface 45 | */ 46 | protected $eavAttributeRepositoryInterface; 47 | /** 48 | * Object manager interface 49 | * 50 | * @var ObjectManagerInterface $objectManager 51 | */ 52 | protected $objectManager; 53 | /** 54 | * Emulation 55 | * 56 | * @var Emulation $appEmulation 57 | */ 58 | protected $appEmulation; 59 | /** 60 | * Area list 61 | * 62 | * @var AreaList $areaList 63 | */ 64 | protected $areaList; 65 | 66 | /** 67 | * AdditionalAttributes constructor. 68 | * 69 | * @param Emulation $appEmulation 70 | * @param AreaList $areaList 71 | * @param ObjectManagerInterface $objectManager 72 | * @param AttributeConfig $attributeConfig 73 | * @param AttributeRepositoryInterface $eavAttributeRepositoryInterface 74 | * @param ResourceConnection $resource 75 | */ 76 | public function __construct( 77 | Emulation $appEmulation, 78 | AreaList $areaList, 79 | ObjectManagerInterface $objectManager, 80 | AttributeConfig $attributeConfig, 81 | AttributeRepositoryInterface $eavAttributeRepositoryInterface, 82 | ResourceConnection $resource 83 | ) { 84 | $this->resource = $resource; 85 | $this->attributeConfig = $attributeConfig; 86 | $this->eavAttributeRepositoryInterface = $eavAttributeRepositoryInterface; 87 | $this->objectManager = $objectManager; 88 | $this->appEmulation = $appEmulation; 89 | $this->areaList = $areaList; 90 | } 91 | 92 | /** 93 | * Add categories data to the index data. 94 | * 95 | * {@inheritdoc} 96 | */ 97 | public function addData($storeId, array $indexData) 98 | { 99 | $this->appEmulation->startEnvironmentEmulation($storeId); 100 | $area = $this->areaList->getArea(Area::AREA_FRONTEND); 101 | $area->load(Area::PART_TRANSLATE); 102 | 103 | /** @var string $attributeCode */ 104 | foreach ($this->attributeConfig->getAdditionalSelectedAttributes() as $attributeCode) { 105 | foreach (array_keys($indexData) as $productId) { 106 | $indexData[$productId][$attributeCode] = ''; 107 | } 108 | 109 | $attribute = $this->eavAttributeRepositoryInterface->get(Product::ENTITY, $attributeCode); 110 | $values = $this->getValueFromAttributes((int)$storeId, $attribute, array_keys($indexData)); 111 | if ($attribute->getSourceModel() !== null) { 112 | $values = $this->loadSourceModelValue($values, $attribute); 113 | } 114 | foreach ($values as $value) { 115 | $indexData[(int)$value['entity_id']][$attributeCode] = $value['value']; 116 | } 117 | } 118 | 119 | $this->appEmulation->stopEnvironmentEmulation(); 120 | 121 | return $indexData; 122 | } 123 | 124 | /** 125 | * Get request path data 126 | * 127 | * @param int $storeId 128 | * @param AttributeInterface $attribute 129 | * @param array $productIds 130 | * 131 | * @return string[] 132 | */ 133 | protected function getValueFromAttributes(int $storeId, AttributeInterface $attribute, array $productIds): array 134 | { 135 | if (!in_array($attribute->getBackendType(), ['datetime', 'decimal', 'int', 'varchar'])) { 136 | return []; 137 | } 138 | 139 | $connection = $this->resource->getConnection(); 140 | $tableName = $connection->getTableName('catalog_product_entity_' . $attribute->getBackendType()); 141 | $select = $connection->select()->from( 142 | ['cpe_default' => $tableName], 143 | [] 144 | )->joinLeft( 145 | ['cpe' => $tableName], 146 | 'cpe.attribute_id = cpe_default.attribute_id AND 147 | cpe.entity_id = cpe_default.entity_id AND 148 | cpe.store_id = ' . $storeId, 149 | [] 150 | )->columns([ 151 | 'value' => new \Zend_Db_Expr('IFNULL(`cpe`.`value`, cpe_default.value)'), 152 | 'entity_id' => new \Zend_Db_Expr('IFNULL(`cpe`.`entity_id`, cpe_default.entity_id)'), 153 | ]) 154 | ->where('cpe_default.store_id = 0') 155 | ->where('cpe_default.entity_id IN (?)', $productIds) 156 | ->where('cpe_default.attribute_id = ?', $attribute->getId()); 157 | 158 | return $connection->fetchAll($select); 159 | } 160 | 161 | /** 162 | * Load source model value 163 | * 164 | * @param array $values 165 | * @param AttributeInterface $attribute 166 | * 167 | * @return mixed 168 | */ 169 | protected function loadSourceModelValue(array $values, AttributeInterface $attribute): array 170 | { 171 | $sourceModel = $this->objectManager->get($attribute->getSourceModel()); 172 | $options = []; 173 | $optionsArray = $sourceModel->toOptionArray(); 174 | foreach ($optionsArray as $row) { 175 | $options[$row['value']] = $row['label']; 176 | } 177 | 178 | foreach ($values as &$value) { 179 | if (isset($options[$value['value']])) { 180 | $value['value'] = (string)$options[$value['value']]; 181 | } 182 | } 183 | 184 | return $values; 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /src/module-elasticsuite-autocomplete/Model/Indexer/Product/Fulltext/Datasource/ConfigurablePrice.php: -------------------------------------------------------------------------------- 1 | 21 | * @copyright 2021 Web200 22 | * @license https://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) 23 | * @link https://www.web200.fr/ 24 | */ 25 | class ConfigurablePrice extends Indexer implements DatasourceInterface 26 | { 27 | /** 28 | * Dimension collection factory 29 | * 30 | * @var DimensionCollectionFactory $dimensionCollectionFactory 31 | */ 32 | protected $dimensionCollectionFactory; 33 | /** 34 | * Dimensions 35 | * 36 | * @var string[] $dimensions 37 | */ 38 | protected $dimensions; 39 | /** 40 | * Dimension by website 41 | * 42 | * @var string[] $dimensionsByWebsite 43 | */ 44 | protected $dimensionsByWebsite = []; 45 | /** 46 | * Resource connection 47 | * 48 | * @var ResourceConnection $resource 49 | */ 50 | protected $resource; 51 | /** 52 | * Price table resolver 53 | * 54 | * @var PriceTableResolver $priceTableResolver 55 | */ 56 | protected $priceTableResolver; 57 | 58 | /** 59 | * ConfigurablePrice constructor. 60 | * 61 | * @param ResourceConnection $resource 62 | */ 63 | public function __construct( 64 | ResourceConnection $resource, 65 | StoreManagerInterface $storeManager, 66 | MetadataPool $metadataPool, 67 | DimensionCollectionFactory $dimensionCollectionFactory, 68 | PriceTableResolver $priceTableResolver 69 | ) { 70 | parent::__construct($resource, $storeManager, $metadataPool); 71 | 72 | $this->resource = $resource; 73 | $this->priceTableResolver = $priceTableResolver; 74 | $this->dimensionCollectionFactory = $dimensionCollectionFactory; 75 | } 76 | 77 | /** 78 | * Add categories data to the index data. 79 | * 80 | * {@inheritdoc} 81 | */ 82 | public function addData($storeId, array $indexData) 83 | { 84 | $configurableIds = []; 85 | /** @var string[] $data */ 86 | foreach ($indexData as $data) { 87 | if ($data['type_id'] !== 'configurable') { 88 | continue; 89 | } 90 | $configurableIds[] = $data['entity_id']; 91 | } 92 | 93 | if (empty($configurableIds)) { 94 | return $indexData; 95 | } 96 | 97 | 98 | $websiteId = (int)$this->getStore($storeId)->getWebsiteId(); 99 | $minFinalPriceData = $this->getMinFinalPrice($configurableIds, $websiteId, true); 100 | if (empty($minFinalPriceData)) { 101 | $minFinalPriceData = $this->getMinFinalPrice($configurableIds, $websiteId, false); 102 | } 103 | if (empty($minFinalPriceData)) { 104 | return $indexData; 105 | } 106 | 107 | foreach ($minFinalPriceData as $row) { 108 | if (!isset($indexData[$row['entity_id']]['price'])) { 109 | continue; 110 | } 111 | foreach ($indexData[$row['entity_id']]['price'] as &$priceRow) { 112 | if (isset($priceRow['customer_group_id']) && $priceRow['customer_group_id'] == $row['customer_group_id']) { 113 | $priceRow['original_price'] = $row['original_price']; 114 | break; 115 | } 116 | } 117 | } 118 | 119 | return $indexData; 120 | } 121 | 122 | /** 123 | * Get min final price 124 | * 125 | * @param array $parentIds 126 | * @param int $websiteId 127 | * @param bool $inStock 128 | * 129 | * @return array 130 | */ 131 | protected function getMinFinalPrice(array $parentIds, int $websiteId, bool $inStock): array 132 | { 133 | $connection = $this->resource->getConnection(); 134 | $select = $connection->select()->from( 135 | ['parent' => $connection->getTableName('catalog_product_entity')], 136 | [] 137 | )->join( 138 | ['link' => $connection->getTableName('catalog_product_relation')], 139 | 'link.parent_id = parent.entity_id', 140 | [] 141 | )->join( 142 | ['t' => $connection->getTableName($this->getPriceIndexDimensionsTables($websiteId))], 143 | 't.entity_id = link.child_id ', 144 | [] 145 | )->join( 146 | ['csi' => $connection->getTableName('cataloginventory_stock_item')], 147 | 'csi.product_id = link.child_id AND csi.is_in_stock=' . $inStock ? '1' : '0', 148 | [] 149 | )->columns([ 150 | new \Zend_Db_Expr('t.customer_group_id'), 151 | new \Zend_Db_Expr('parent.entity_id'), 152 | new \Zend_Db_Expr('MIN(price) as original_price') 153 | ]) 154 | ->where('parent.entity_id IN (?)', $parentIds) 155 | ->where('t.price > 0') 156 | ->group(['parent.entity_id', 't.customer_group_id']); 157 | 158 | return $connection->fetchAll($select); 159 | } 160 | 161 | /** 162 | * Return the price index tables according to the price index dimensions for the given website. 163 | * 164 | * @param integer $websiteId Website id. 165 | * 166 | * @return string 167 | */ 168 | private function getPriceIndexDimensionsTables(int $websiteId): string 169 | { 170 | $tables = []; 171 | 172 | $indexDimensions = $this->getPriceIndexDimensions($websiteId); 173 | foreach ($indexDimensions as $dimensions) { 174 | $tables[] = $this->priceTableResolver->resolve('catalog_product_index_price', $dimensions); 175 | } 176 | 177 | return $tables[0]; 178 | } 179 | 180 | /** 181 | * Return price index dimensions applicable for the given website. 182 | * 183 | * @param integer $websiteId 184 | * 185 | * @return array 186 | * @SuppressWarnings(PHPMD.ElseExpression) 187 | */ 188 | private function getPriceIndexDimensions(int $websiteId) 189 | { 190 | if (!array_key_exists($websiteId, $this->dimensionsByWebsite)) { 191 | $indexDimensions = $this->getAllPriceIndexDimensions(); 192 | 193 | $relevantDimensions = []; 194 | foreach ($indexDimensions as $dimensions) { 195 | if (array_key_exists(WebsiteDimensionProvider::DIMENSION_NAME, $dimensions)) { 196 | $websiteDimension = $dimensions[WebsiteDimensionProvider::DIMENSION_NAME]; 197 | if ((string)$websiteDimension->getValue() == $websiteId) { 198 | $relevantDimensions[] = $dimensions; 199 | } 200 | } else { 201 | $relevantDimensions[] = $dimensions; 202 | } 203 | } 204 | 205 | $this->dimensionsByWebsite[$websiteId] = $relevantDimensions; 206 | } 207 | 208 | return $this->dimensionsByWebsite[$websiteId]; 209 | } 210 | 211 | /** 212 | * Return all price index dimensions. 213 | * 214 | * @return array 215 | */ 216 | private function getAllPriceIndexDimensions() 217 | { 218 | if ($this->dimensions === null) { 219 | $this->dimensions = $this->dimensionCollectionFactory->create(); 220 | } 221 | 222 | return $this->dimensions; 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /src/module-elasticsuite-autocomplete/Model/Indexer/Product/Fulltext/Datasource/Url.php: -------------------------------------------------------------------------------- 1 | 15 | * @copyright 2021 Web200 16 | * @license https://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) 17 | * @link https://www.web200.fr/ 18 | */ 19 | class Url implements DatasourceInterface 20 | { 21 | /** 22 | * Resource connection 23 | * 24 | * @var ResourceConnection $resource 25 | */ 26 | protected $resource; 27 | 28 | /** 29 | * Url constructor. 30 | * 31 | * @param ResourceConnection $resource 32 | */ 33 | public function __construct( 34 | ResourceConnection $resource 35 | ) { 36 | $this->resource = $resource; 37 | } 38 | 39 | /** 40 | * Add categories data to the index data. 41 | * 42 | * {@inheritdoc} 43 | */ 44 | public function addData($storeId, array $indexData) 45 | { 46 | $requestPathData = $this->getRequestPathData((int)$storeId, array_keys($indexData)); 47 | /** @var string[] $data */ 48 | foreach ($requestPathData as $data) { 49 | $indexData[(int)$data['entity_id']]['request_path'] = $data['request_path']; 50 | } 51 | 52 | return $indexData; 53 | } 54 | 55 | /** 56 | * Get request path data 57 | * 58 | * @param int $storeId 59 | * @param array $productId 60 | * 61 | * @return string[] 62 | */ 63 | protected function getRequestPathData(int $storeId, array $productId): array 64 | { 65 | $connection = $this->resource->getConnection(); 66 | $select = $connection->select()->from( 67 | ['url_rewrite' => $connection->getTableName('url_rewrite')], 68 | ['request_path', 'entity_id'] 69 | )->where('entity_type = ?', 'product') 70 | ->where('entity_id IN (?)', $productId) 71 | ->where('store_id = ?', $storeId) 72 | ->where('redirect_type = 0') 73 | ->where('metadata IS NULL'); 74 | 75 | return $connection->fetchAll($select); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/module-elasticsuite-autocomplete/Model/Query.php: -------------------------------------------------------------------------------- 1 | 16 | * @copyright 2021 Web200 17 | * @license https://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) 18 | * @link https://www.web200.fr/ 19 | */ 20 | class Query 21 | { 22 | /** 23 | * Query Product 24 | * 25 | * @var QueryProduct $productQuery 26 | */ 27 | protected $productQuery; 28 | /** 29 | * Query category 30 | * 31 | * @var QueryCategory $categoryQuery 32 | */ 33 | protected $categoryQuery; 34 | /** 35 | * Config 36 | * 37 | * @var Config $config 38 | */ 39 | protected $config; 40 | 41 | /** 42 | * Query constructor. 43 | * 44 | * @param Config $config 45 | * @param QueryProduct $productQuery 46 | * @param QueryCategory $categoryQuery 47 | */ 48 | public function __construct( 49 | Config $config, 50 | QueryProduct $productQuery, 51 | QueryCategory $categoryQuery 52 | ) { 53 | $this->productQuery = $productQuery; 54 | $this->categoryQuery = $categoryQuery; 55 | $this->config = $config; 56 | } 57 | 58 | /** 59 | * Execute 60 | * 61 | * @return mixed 62 | */ 63 | public function execute() 64 | { 65 | /** @var string[] $result */ 66 | $result = []; 67 | 68 | if ($this->config->isProductActive()) { 69 | $result = array_merge($result, $this->productQuery->execute()); 70 | } 71 | if ($this->config->isCategoryActive()) { 72 | $result = array_merge($result, $this->categoryQuery->execute()); 73 | } 74 | 75 | return $result; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/module-elasticsuite-autocomplete/Model/Query/Category.php: -------------------------------------------------------------------------------- 1 | 22 | * @copyright 2021 Web200 23 | * @license https://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) 24 | * @link https://www.web200.fr/ 25 | */ 26 | class Category 27 | { 28 | /** 29 | * Client 30 | * 31 | * @var Client $client 32 | */ 33 | protected $client; 34 | /** 35 | * Mapper 36 | * 37 | * @var Mapper $requestMapper 38 | */ 39 | protected $requestMapper; 40 | /** 41 | * Request builder 42 | * 43 | * @var Builder $requestBuilder 44 | */ 45 | protected $requestBuilder; 46 | /** 47 | * Store manager interface 48 | * 49 | * @var StoreManagerInterface $storeManager 50 | */ 51 | protected $storeManager; 52 | /** 53 | * Cache 54 | * 55 | * @var CacheInterface $cache 56 | */ 57 | protected $cache; 58 | /** 59 | * Json 60 | * 61 | * @var Json $json 62 | */ 63 | protected $json; 64 | /** 65 | * Category render 66 | * 67 | * @var CategoryRender $categoryRender 68 | */ 69 | protected $categoryRender; 70 | /** 71 | * Query factory 72 | * 73 | * @var QueryFactory $queryFactory 74 | */ 75 | protected $queryFactory; 76 | /** 77 | * Config 78 | * 79 | * @var Config $config 80 | */ 81 | protected $config; 82 | 83 | /** 84 | * Category constructor. 85 | * 86 | * @param Config $config 87 | * @param Builder $requestBuilder 88 | * @param Mapper $mapper 89 | * @param CacheInterface $cache 90 | * @param StoreManagerInterface $storeManager 91 | * @param Json $json 92 | * @param CategoryRender $categoryRender 93 | * @param QueryFactory $queryFactory 94 | * @param Client $client 95 | */ 96 | public function __construct( 97 | Config $config, 98 | Builder $requestBuilder, 99 | Mapper $mapper, 100 | CacheInterface $cache, 101 | StoreManagerInterface $storeManager, 102 | Json $json, 103 | CategoryRender $categoryRender, 104 | QueryFactory $queryFactory, 105 | Client $client 106 | ) { 107 | $this->client = $client; 108 | $this->requestMapper = $mapper; 109 | $this->requestBuilder = $requestBuilder; 110 | $this->storeManager = $storeManager; 111 | $this->cache = $cache; 112 | $this->json = $json; 113 | $this->categoryRender = $categoryRender; 114 | $this->queryFactory = $queryFactory; 115 | $this->config = $config; 116 | } 117 | 118 | /** 119 | * Execute 120 | * 121 | * @return mixed 122 | */ 123 | public function execute() 124 | { 125 | return $this->parseQuery($this->query()); 126 | } 127 | 128 | /** 129 | * BuildQuery 130 | * 131 | * @return string[] 132 | */ 133 | protected function buildQuery() 134 | { 135 | /** @var int $storeId */ 136 | $storeId = $this->storeManager->getStore()->getId(); 137 | /** @var string $key */ 138 | $key = 'smile_autocomplete_category_query:' . $storeId; 139 | /** @var string $searchRequestJson */ 140 | $searchRequestJson = $this->cache->load($key); 141 | if ($searchRequestJson === false) { 142 | $request = $this->requestBuilder->create( 143 | $storeId, 144 | 'category_search_container', 145 | 0, 146 | $this->config->getCategoryAutocompleteMaxSize(), 147 | 'product', 148 | [], 149 | [], 150 | [] 151 | ); 152 | /** @var string[] $searchRequest */ 153 | $searchRequest = [ 154 | 'index' => $request->getIndex(), 155 | 'body' => $this->requestMapper->buildSearchRequest($request), 156 | ]; 157 | $this->cache->save($this->json->serialize($searchRequest), $key, ['autocomplete'], 3600); 158 | 159 | return $searchRequest; 160 | } 161 | 162 | return $this->json->unserialize($searchRequestJson); 163 | } 164 | 165 | /** 166 | * Query 167 | * 168 | * @return string[] 169 | */ 170 | protected function query(): array 171 | { 172 | /** @var string $searchString */ 173 | $searchString = $this->queryFactory->get()->getQueryText(); 174 | 175 | /** @var string[] $query */ 176 | $query = $this->buildQuery(); 177 | if (isset($query['body']['query']['bool']['must']['bool']['should'])) { 178 | foreach ($query['body']['query']['bool']['must']['bool']['should'] as &$should) { 179 | $should['multi_match']['query'] = $searchString; 180 | } 181 | } 182 | 183 | return $this->client->search($query); 184 | } 185 | 186 | /** 187 | * Parse query 188 | * 189 | * @param string[] $result 190 | * 191 | * @return string[] 192 | */ 193 | protected function parseQuery(array $result): array 194 | { 195 | if (!isset($result['hits'])) { 196 | return []; 197 | } 198 | 199 | /** @var string[] $final */ 200 | $final = []; 201 | /** @var string[] $category */ 202 | foreach ($result['hits']['hits'] as $category) { 203 | if (!isset($category['_source'])) { 204 | continue; 205 | } 206 | $final[] = $this->categoryRender->render($category); 207 | } 208 | 209 | return $final; 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /src/module-elasticsuite-autocomplete/Model/Query/Product.php: -------------------------------------------------------------------------------- 1 | 26 | * @copyright 2021 Web200 27 | * @license https://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) 28 | * @link https://www.web200.fr/ 29 | */ 30 | class Product 31 | { 32 | /** 33 | * Client 34 | * 35 | * @var Client $client 36 | */ 37 | protected $client; 38 | /** 39 | * Mapper 40 | * 41 | * @var Mapper $requestMapper 42 | */ 43 | protected $requestMapper; 44 | /** 45 | * Request builder 46 | * 47 | * @var Builder $requestBuilder 48 | */ 49 | protected $requestBuilder; 50 | /** 51 | * Store manager interface 52 | * 53 | * @var StoreManagerInterface $storeManager 54 | */ 55 | protected $storeManager; 56 | /** 57 | * Cache 58 | * 59 | * @var CacheInterface $cache 60 | */ 61 | protected $cache; 62 | /** 63 | * Json 64 | * 65 | * @var Json $json 66 | */ 67 | protected $json; 68 | /** 69 | * Product render 70 | * 71 | * @var RenderProduct $productRender 72 | */ 73 | protected $productRender; 74 | /** 75 | * Query factory 76 | * 77 | * @var QueryFactory $queryFactory 78 | */ 79 | protected $queryFactory; 80 | /** 81 | * Config 82 | * 83 | * @var Config $config 84 | */ 85 | protected $config; 86 | /** 87 | * Config 88 | * 89 | * @var TermDataProvider $termDataProvider 90 | */ 91 | protected $termDataProvider; 92 | 93 | /** 94 | * Product constructor. 95 | * 96 | * @param Config $config 97 | * @param Builder $requestBuilder 98 | * @param Mapper $mapper 99 | * @param CacheInterface $cache 100 | * @param StoreManagerInterface $storeManager 101 | * @param Json $json 102 | * @param RenderProduct $productRender 103 | * @param QueryFactory $queryFactory 104 | * @param Client $client 105 | * @param TermDataProvider $termDataProvider 106 | */ 107 | public function __construct( 108 | Config $config, 109 | Builder $requestBuilder, 110 | Mapper $mapper, 111 | CacheInterface $cache, 112 | StoreManagerInterface $storeManager, 113 | Json $json, 114 | RenderProduct $productRender, 115 | QueryFactory $queryFactory, 116 | Client $client, 117 | TermDataProvider $termDataProvider 118 | ) { 119 | $this->client = $client; 120 | $this->requestMapper = $mapper; 121 | $this->requestBuilder = $requestBuilder; 122 | $this->storeManager = $storeManager; 123 | $this->cache = $cache; 124 | $this->json = $json; 125 | $this->productRender = $productRender; 126 | $this->queryFactory = $queryFactory; 127 | $this->config = $config; 128 | $this->termDataProvider = $termDataProvider; 129 | } 130 | 131 | /** 132 | * Execute 133 | * 134 | * @return mixed 135 | */ 136 | public function execute() 137 | { 138 | return $this->parseQuery($this->query()); 139 | } 140 | 141 | /** 142 | * BuildQuery 143 | * 144 | * @return string[] 145 | * @throws NoSuchEntityException 146 | */ 147 | protected function buildQuery() 148 | { 149 | /** @var int $storeId */ 150 | $storeId = $this->storeManager->getStore()->getId(); 151 | 152 | /** @var string[] $facets */ 153 | $facets = [ 154 | ['name' => 'attribute_set_id', 'type' => BucketInterface::TYPE_TERM, 'size' => 0], 155 | ['name' => 'indexed_attributes', 'type' => BucketInterface::TYPE_TERM, 'size' => 0], 156 | ]; 157 | $request = $this->requestBuilder->create( 158 | $storeId, 159 | 'catalog_product_autocomplete', 160 | 0, 161 | $this->config->getProductAutocompleteMaxSize(), 162 | $this->getSearchTerm(), 163 | [], 164 | [], 165 | [], 166 | $facets 167 | ); 168 | /** @var string[] $searchRequest */ 169 | $searchRequest = [ 170 | 'index' => $request->getIndex(), 171 | 'body' => $this->requestMapper->buildSearchRequest($request), 172 | ]; 173 | 174 | return $searchRequest; 175 | } 176 | 177 | /** 178 | * Get search Term 179 | * 180 | * @return array|null[]|string[] 181 | */ 182 | protected function getSearchTerm() 183 | { 184 | $terms = array_map( 185 | function (TermItem $termItem) { 186 | return $termItem->getTitle(); 187 | }, 188 | $this->termDataProvider->getItems() 189 | ); 190 | 191 | if (empty($terms)) { 192 | $terms = [$this->queryFactory->get()->getQueryText()]; 193 | } 194 | 195 | return $terms; 196 | } 197 | 198 | /** 199 | * Query 200 | * 201 | * @return string[] 202 | */ 203 | protected function query(): array 204 | { 205 | return $this->client->search($this->buildQuery()); 206 | } 207 | 208 | /** 209 | * Parse query 210 | * 211 | * @return array 212 | */ 213 | protected function parseQuery(array $result): array 214 | { 215 | if (!isset($result['hits'])) { 216 | return []; 217 | } 218 | 219 | /** @var string[] $final */ 220 | $final = []; 221 | foreach ($result['hits']['hits'] as $product) { 222 | if (!isset($product['_source'])) { 223 | continue; 224 | } 225 | $final[] = $this->productRender->render($product); 226 | } 227 | 228 | return $final; 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /src/module-elasticsuite-autocomplete/Model/Render/Category.php: -------------------------------------------------------------------------------- 1 | 18 | * @copyright 2021 Web200 19 | * @license https://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) 20 | * @link https://www.web200.fr/ 21 | */ 22 | class Category 23 | { 24 | /** 25 | * Store manager 26 | * 27 | * @var StoreManagerInterface $storeManager 28 | */ 29 | protected $storeManager; 30 | /** 31 | * ScopeConfig 32 | * 33 | * @var ScopeConfigInterface $scopeConfig 34 | */ 35 | protected $scopeConfig; 36 | 37 | /** 38 | * Product constructor. 39 | * 40 | * @param ScopeConfigInterface $scopeConfig 41 | * @param StoreManagerInterface $storeManager 42 | */ 43 | public function __construct( 44 | ScopeConfigInterface $scopeConfig, 45 | StoreManagerInterface $storeManager 46 | ) { 47 | $this->storeManager = $storeManager; 48 | $this->scopeConfig = $scopeConfig; 49 | } 50 | 51 | /** 52 | * Render 53 | * 54 | * @param string[] $categoryData 55 | * 56 | * @return string[] 57 | */ 58 | public function render(array $categoryData) 59 | { 60 | /** @var string[] $category */ 61 | $category = []; 62 | 63 | $category['type'] = 'category'; 64 | $category['title'] = $this->getFirstResult($categoryData['_source']['name']); 65 | $category['url'] = $this->generateCategoryUrl($categoryData); 66 | $category['breadcrumb'] = explode('/', $categoryData['_source']['breadcrumb']); 67 | 68 | return $category; 69 | } 70 | 71 | /** 72 | * Get first result 73 | * 74 | * @param $result 75 | * 76 | * @return mixed 77 | */ 78 | protected function getFirstResult($result) 79 | { 80 | if (is_array($result)) { 81 | $result = $result[0]; 82 | } 83 | 84 | return $result; 85 | } 86 | 87 | /** 88 | * Generate category url 89 | * 90 | * @param array $categoryData 91 | * 92 | * @return string 93 | */ 94 | protected function generateCategoryUrl(array $categoryData): string 95 | { 96 | if (isset($productData['_source']['request_path'])) { 97 | $requestPath = $this->getFirstResult($categoryData['_source']['request_path']); 98 | } else { 99 | $suffix = (string)$this->scopeConfig->getValue(CategoryUrlPathGenerator::XML_PATH_CATEGORY_URL_SUFFIX, ScopeInterface::SCOPE_STORE); 100 | $requestPath = $this->getFirstResult($categoryData['_source']['url_key']) . $suffix; 101 | } 102 | 103 | return $this->storeManager->getStore()->getBaseUrl(UrlInterface::URL_TYPE_LINK) . $this->getFirstResult($requestPath); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/module-elasticsuite-autocomplete/Model/Render/Product.php: -------------------------------------------------------------------------------- 1 | 23 | * @copyright 2021 Web200 24 | * @license https://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) 25 | * @link https://www.web200.fr/ 26 | */ 27 | class Product 28 | { 29 | /** 30 | * Store manager 31 | * 32 | * @var StoreManagerInterface $storeManager 33 | */ 34 | protected $storeManager; 35 | /** 36 | * Image helper 37 | * 38 | * @var $imageHelper 39 | */ 40 | protected $imageHelper; 41 | /** 42 | * Product factory 43 | * 44 | * @var ProductFactory $productFactory 45 | */ 46 | protected $productFactory; 47 | /** 48 | * Customer session 49 | * 50 | * @var Session $customerSession 51 | */ 52 | protected $customerSession; 53 | /** 54 | * Attribute config 55 | * 56 | * @var AttributeConfig 57 | */ 58 | protected $attributeConfig; 59 | /** 60 | * Data 61 | * 62 | * @var Data $priceHelper 63 | */ 64 | protected $priceHelper; 65 | /** 66 | * scopeConfig 67 | * 68 | * @var ScopeConfigInterface $scopeConfig 69 | */ 70 | protected $scopeConfig; 71 | 72 | /** 73 | * Product constructor. 74 | * 75 | * @param AttributeConfig $attributeConfig 76 | * @param ScopeConfigInterface $scopeConfig 77 | * @param StoreManagerInterface $storeManager 78 | * @param Data $priceHelper 79 | * @param Image $imageHelper 80 | * @param Session $customerSession 81 | * @param ProductFactory $productFactory 82 | */ 83 | public function __construct( 84 | AttributeConfig $attributeConfig, 85 | ScopeConfigInterface $scopeConfig, 86 | StoreManagerInterface $storeManager, 87 | Data $priceHelper, 88 | Image $imageHelper, 89 | Session $customerSession, 90 | ProductFactory $productFactory 91 | ) { 92 | $this->storeManager = $storeManager; 93 | $this->imageHelper = $imageHelper; 94 | $this->productFactory = $productFactory; 95 | $this->customerSession = $customerSession; 96 | $this->attributeConfig = $attributeConfig; 97 | $this->priceHelper = $priceHelper; 98 | $this->scopeConfig = $scopeConfig; 99 | } 100 | 101 | /** 102 | * Render 103 | * 104 | * @param string[] $productData 105 | * 106 | * @return string[] 107 | */ 108 | public function render(array $productData) 109 | { 110 | /** @var string[] $product */ 111 | $product = []; 112 | $product['type'] = 'product'; 113 | $product['type_id'] = $productData['_source']['type_id']; 114 | $product['title'] = $productData['_source']['name'][0]; 115 | $product['url'] = $this->generateProductUrl($productData); 116 | $product['sku'] = $this->getFirstResult($productData['_source']['sku']); 117 | $product['image'] = $this->generateImageUrl($this->getFirstResult($productData['_source']['image'])); 118 | list($product['regular_price_value'], $product['price_value'], $product['promotion_percentage']) = $this->getPriceValue($productData); 119 | if ($product['price_value'] <= 0) { 120 | $product['price_value'] = $this->getPrice($product, 'regular_price_value'); 121 | } 122 | $product['price'] = $this->getPrice($product, 'price_value'); 123 | $product['regular_price'] = $this->getPrice($product, 'regular_price_value'); 124 | 125 | $additionalAttributes = $this->attributeConfig->getAdditionalSelectedAttributes(); 126 | foreach ($additionalAttributes as $key) { 127 | if (isset($productData['_source'][$key])) { 128 | $product[$key] = $productData['_source'][$key]; 129 | } 130 | } 131 | 132 | return $product; 133 | } 134 | 135 | /** 136 | * Get price 137 | * 138 | * @param array $productData 139 | * 140 | * @return mixed 141 | */ 142 | protected function getPriceValue(array $productData) 143 | { 144 | if (!isset($productData['_source']['price'])) { 145 | return ['', '', '']; 146 | } 147 | 148 | /** @var string[] $prices */ 149 | $prices = []; 150 | foreach ($productData['_source']['price'] as $price) { 151 | $prices[$price['customer_group_id']] = $price; 152 | } 153 | 154 | /** @var int $customerGroupId */ 155 | $customerGroupId = $this->customerSession->getCustomerGroupId(); 156 | if ($customerGroupId >= 0 && isset($prices[$customerGroupId])) { 157 | $regularPrice = $prices[$customerGroupId]['original_price']; 158 | $finalPrice = $prices[$customerGroupId]['price']; 159 | if ($finalPrice <= 0) { 160 | $finalPrice = $regularPrice; 161 | } 162 | $promotion = 0; 163 | if ($regularPrice != $finalPrice && $regularPrice > 0) { 164 | $promotion = round(100 - ($finalPrice * 100 / $regularPrice), 2); 165 | } 166 | 167 | return [ 168 | $regularPrice, 169 | $finalPrice, 170 | $promotion 171 | ]; 172 | } 173 | 174 | return ['', '', '']; 175 | } 176 | 177 | /** 178 | * Get price 179 | * 180 | * @param string[] $product 181 | * @param string $key 182 | * 183 | * @return string 184 | */ 185 | protected function getPrice(array $product, string $key): string 186 | { 187 | $price = $this->priceHelper->currency($product[$key]); 188 | 189 | if ($product['type_id'] === 'configurable') { 190 | return __('As low as') . ' ' . $price; 191 | } 192 | 193 | return $price; 194 | } 195 | 196 | /** 197 | * Get first result 198 | * 199 | * @param $result 200 | * 201 | * @return mixed 202 | */ 203 | protected function getFirstResult($result) 204 | { 205 | if (is_array($result)) { 206 | $result = $result[0]; 207 | } 208 | 209 | return $result; 210 | } 211 | 212 | /** 213 | * Generate product url 214 | * 215 | * @param array $productData 216 | * 217 | * @return string 218 | */ 219 | protected function generateProductUrl(array $productData): string 220 | { 221 | if (isset($productData['_source']['request_path'])) { 222 | $requestPath = $this->getFirstResult($productData['_source']['request_path']); 223 | } else { 224 | $suffix = (string)$this->scopeConfig->getValue(ProductUrlPathGenerator::XML_PATH_PRODUCT_URL_SUFFIX, ScopeInterface::SCOPE_STORE); 225 | $requestPath = $this->getFirstResult($productData['_source']['url_key']) . $suffix; 226 | } 227 | 228 | return $this->storeManager->getStore()->getBaseUrl(UrlInterface::URL_TYPE_LINK) . $requestPath; 229 | } 230 | 231 | /** 232 | * Generate image url 233 | * 234 | * @param null|string $imagePath 235 | * 236 | * @return string 237 | */ 238 | protected function generateImageUrl(?string $imagePath): string 239 | { 240 | if ($imagePath === null) { 241 | return ''; 242 | } 243 | 244 | $product = $this->productFactory->create(); 245 | 246 | return $this->imageHelper->init($product, 'smile_elasticsuite_autocomplete_product_image') 247 | ->setImageFile($imagePath) 248 | ->getUrl(); 249 | } 250 | } 251 | -------------------------------------------------------------------------------- /src/module-elasticsuite-autocomplete/Plugin/ImproveSearch.php: -------------------------------------------------------------------------------- 1 | 22 | * @copyright 2022 Web200 23 | * @license https://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) 24 | * @link https://www.web200.fr/ 25 | */ 26 | class ImproveSearch 27 | { 28 | /** 29 | * @var QueryFactory 30 | */ 31 | protected $queryFactory; 32 | /** 33 | * @var FieldFilterInterface[] 34 | */ 35 | protected $fieldFilters; 36 | /** 37 | * @var Config $config 38 | */ 39 | protected $config; 40 | 41 | /** 42 | * Constructor. 43 | * 44 | * @param Config $config 45 | * @param QueryFactory $queryFactory Query factory (used to build subqueries. 46 | * @param FieldFilterInterface[] $fieldFilters Field filters models. 47 | */ 48 | public function __construct( 49 | Config $config, 50 | QueryFactory $queryFactory, 51 | array $fieldFilters = [] 52 | ) { 53 | $this->queryFactory = $queryFactory; 54 | $this->fieldFilters = $fieldFilters; 55 | $this->config = $config; 56 | } 57 | 58 | /** 59 | * @param QueryBuilder $subject 60 | * @param QueryInterface $result 61 | * @param ContainerConfigurationInterface $containerConfig 62 | * @param string $queryText 63 | * @param string $spellingType 64 | * @param float|int $boost 65 | * 66 | * @return QueryInterface 67 | */ 68 | public function afterCreate( 69 | QueryBuilder $subject, 70 | QueryInterface $result, 71 | ContainerConfigurationInterface $containerConfig, 72 | $queryText, 73 | $spellingType, 74 | $boost = 1 75 | ): QueryInterface { 76 | 77 | if ($this->config->isImproveSearchActive()) { 78 | return $this->create($containerConfig, $queryText, $spellingType); 79 | } 80 | 81 | return $result; 82 | } 83 | 84 | /** 85 | * Create the fulltext search query. 86 | * 87 | * @param ContainerConfigurationInterface $containerConfig Search request container configuration. 88 | * @param string $queryText The text query. 89 | * @param string $spellingType The type of spellchecked applied. 90 | * @param float $boost Boost of the created query. 91 | * 92 | * @return QueryInterface 93 | */ 94 | public function create(ContainerConfigurationInterface $containerConfig, $queryText, $spellingType, $boost = 1) 95 | { 96 | $query = null; 97 | 98 | $fuzzySpellingTypes = [ 99 | SpellcheckerInterface::SPELLING_TYPE_FUZZY, 100 | SpellcheckerInterface::SPELLING_TYPE_MOST_FUZZY 101 | ]; 102 | 103 | if (is_array($queryText)) { 104 | $queries = []; 105 | foreach ($queryText as $currentQueryText) { 106 | $queries[] = $this->create($containerConfig, $currentQueryText, $spellingType); 107 | } 108 | $query = $this->queryFactory->create(QueryInterface::TYPE_BOOL, ['should' => $queries, 'boost' => $boost]); 109 | } elseif ($spellingType == SpellcheckerInterface::SPELLING_TYPE_PURE_STOPWORDS) { 110 | $query = $this->getPureStopwordsQuery($containerConfig, $queryText, $boost); 111 | } elseif (in_array($spellingType, $fuzzySpellingTypes)) { 112 | $query = $this->getSpellcheckedQuery($containerConfig, $queryText, $spellingType, $boost); 113 | } 114 | 115 | if ($query === null) { 116 | $queries = []; 117 | $queries[] = $this->getWeightedSearchQuery($containerConfig, $queryText); 118 | $queries[] = $this->getPhrasePrefixSearchQuery($containerConfig, $queryText); 119 | $queries[] = $this->getFuzzyQuery($containerConfig, $queryText); 120 | 121 | $query = $this->queryFactory->create(QueryInterface::TYPE_BOOL, ['should' => $queries, 'boost' => $boost]); 122 | } 123 | 124 | return $query; 125 | } 126 | 127 | /** 128 | * Provides a weighted search query (multi match) using mapping field configuration. 129 | * 130 | * @param ContainerConfigurationInterface $containerConfig Search request container configuration. 131 | * @param string $queryText The text query. 132 | * 133 | * @return QueryInterface 134 | */ 135 | private function getWeightedSearchQuery(ContainerConfigurationInterface $containerConfig, $queryText) 136 | { 137 | $relevanceConfig = $containerConfig->getRelevanceConfig(); 138 | $phraseMatchBoost = $relevanceConfig->getPhraseMatchBoost(); 139 | $defaultSearchField = MappingInterface::DEFAULT_SEARCH_FIELD; 140 | $searchableFieldFilter = $this->fieldFilters['searchableFieldFilter']; 141 | $sortableAnalyzer = FieldInterface::ANALYZER_SORTABLE; 142 | $phraseAnalyzer = FieldInterface::ANALYZER_WHITESPACE; 143 | 144 | if (is_string($queryText) && str_word_count($queryText) > 1) { 145 | $phraseAnalyzer = FieldInterface::ANALYZER_SHINGLE; 146 | } 147 | 148 | $searchFields = array_merge( 149 | $this->getWeightedFields($containerConfig, null, $searchableFieldFilter, $defaultSearchField), 150 | $this->getWeightedFields( 151 | $containerConfig, 152 | $phraseAnalyzer, 153 | $searchableFieldFilter, 154 | $defaultSearchField, 155 | $phraseMatchBoost 156 | ), 157 | $this->getWeightedFields( 158 | $containerConfig, 159 | $sortableAnalyzer, 160 | $searchableFieldFilter, 161 | null, 162 | 2 * $phraseMatchBoost 163 | ) 164 | ); 165 | 166 | $queryParams = [ 167 | 'fields' => $searchFields, 168 | 'queryText' => $queryText, 169 | 'minimumShouldMatch' => 1, 170 | 'cutoffFrequency' => $relevanceConfig->getCutOffFrequency(), 171 | 'tieBreaker' => $relevanceConfig->getTieBreaker(), 172 | ]; 173 | 174 | return $this->queryFactory->create(QueryInterface::TYPE_MULTIMATCH, $queryParams); 175 | } 176 | 177 | /** 178 | * Provides a weighted search query (multi match) using mapping field configuration. 179 | * 180 | * @param ContainerConfigurationInterface $containerConfig Search request container configuration. 181 | * @param string $queryText The text query. 182 | * 183 | * @return QueryInterface 184 | */ 185 | private function getPhrasePrefixSearchQuery(ContainerConfigurationInterface $containerConfig, $queryText) 186 | { 187 | $defaultSearchField = MappingInterface::DEFAULT_SEARCH_FIELD; 188 | $searchableFieldFilter = $this->fieldFilters['searchableFieldFilter']; 189 | 190 | $searchFields = array_merge( 191 | $this->getWeightedFields($containerConfig, null, $searchableFieldFilter, $defaultSearchField), 192 | ); 193 | 194 | $queryParams = [ 195 | 'fields' => $searchFields, 196 | 'queryText' => $queryText, 197 | 'type' => 'phrase_prefix' 198 | ]; 199 | 200 | return $this->queryFactory->create(QueryInterface::TYPE_MULTIMATCH, $queryParams); 201 | } 202 | 203 | /** 204 | * Build a query when the fulltext search query contains only stopwords. 205 | * 206 | * @param ContainerConfigurationInterface $containerConfig Search request container configuration. 207 | * @param string $queryText The text query. 208 | * @param float $boost Boost of the created query. 209 | * 210 | * @return QueryInterface 211 | */ 212 | private function getPureStopwordsQuery(ContainerConfigurationInterface $containerConfig, $queryText, $boost) 213 | { 214 | $relevanceConfig = $containerConfig->getRelevanceConfig(); 215 | 216 | $analyzer = FieldInterface::ANALYZER_WHITESPACE; 217 | if (is_string($queryText) && str_word_count($queryText) > 1) { 218 | $analyzer = FieldInterface::ANALYZER_SHINGLE; 219 | } 220 | 221 | $defaultSearchField = MappingInterface::DEFAULT_SEARCH_FIELD; 222 | $searchableFieldFilter = $this->fieldFilters['searchableFieldFilter']; 223 | 224 | $searchFields = $this->getWeightedFields( 225 | $containerConfig, 226 | $analyzer, 227 | $searchableFieldFilter, 228 | $defaultSearchField 229 | ); 230 | 231 | $queryParams = [ 232 | 'fields' => $searchFields, 233 | 'queryText' => $queryText, 234 | 'minimumShouldMatch' => "100%", 235 | 'tieBreaker' => $relevanceConfig->getTieBreaker(), 236 | 'boost' => $boost, 237 | ]; 238 | 239 | return $this->queryFactory->create(QueryInterface::TYPE_MULTIMATCH, $queryParams); 240 | } 241 | 242 | /** 243 | * Spellcheked query building. 244 | * 245 | * @param ContainerConfigurationInterface $containerConfig Search request container configuration. 246 | * @param string $queryText The text query. 247 | * @param string $spellingType The type of spellchecked applied. 248 | * @param float $boost Boost of the created query. 249 | * 250 | * @return QueryInterface 251 | */ 252 | private function getSpellcheckedQuery( 253 | ContainerConfigurationInterface $containerConfig, 254 | $queryText, 255 | $spellingType, 256 | $boost 257 | ) { 258 | $query = null; 259 | 260 | $relevanceConfig = $containerConfig->getRelevanceConfig(); 261 | $queryClauses = []; 262 | 263 | if ($relevanceConfig->isFuzzinessEnabled()) { 264 | $queryClauses[] = $this->getFuzzyQuery($containerConfig, $queryText); 265 | } 266 | 267 | if ($relevanceConfig->isPhoneticSearchEnabled()) { 268 | $queryClauses[] = $this->getPhoneticQuery($containerConfig, $queryText); 269 | } 270 | 271 | if (!empty($queryClauses)) { 272 | $queryParams = ['should' => $queryClauses, 'boost' => $boost]; 273 | 274 | if ($spellingType == SpellcheckerInterface::SPELLING_TYPE_MOST_FUZZY) { 275 | $queryParams['must'] = [$this->getWeightedSearchQuery($containerConfig, $queryText)]; 276 | } 277 | 278 | $query = $this->queryFactory->create(QueryInterface::TYPE_BOOL, $queryParams); 279 | } 280 | 281 | return $query; 282 | } 283 | 284 | /** 285 | * Fuzzy query part. 286 | * 287 | * @param ContainerConfigurationInterface $containerConfig Search request container configuration. 288 | * @param string $queryText The text query. 289 | * 290 | * @return QueryInterface 291 | */ 292 | private function getFuzzyQuery(ContainerConfigurationInterface $containerConfig, $queryText) 293 | { 294 | $relevanceConfig = $containerConfig->getRelevanceConfig(); 295 | $phraseMatchBoost = $relevanceConfig->getPhraseMatchBoost(); 296 | 297 | $defaultSearchField = MappingInterface::DEFAULT_SPELLING_FIELD; 298 | 299 | $standardAnalyzer = FieldInterface::ANALYZER_WHITESPACE; 300 | $phraseAnalyzer = FieldInterface::ANALYZER_WHITESPACE; 301 | if (is_string($queryText) && str_word_count($queryText) > 1) { 302 | $phraseAnalyzer = FieldInterface::ANALYZER_SHINGLE; 303 | } 304 | 305 | $fuzzyFieldFilter = $this->fieldFilters['fuzzyFieldFilter']; 306 | 307 | $searchFields = array_merge( 308 | $this->getWeightedFields($containerConfig, $standardAnalyzer, $fuzzyFieldFilter, $defaultSearchField), 309 | $this->getWeightedFields( 310 | $containerConfig, 311 | $phraseAnalyzer, 312 | $fuzzyFieldFilter, 313 | $defaultSearchField, 314 | $phraseMatchBoost 315 | ) 316 | ); 317 | 318 | $queryParams = [ 319 | 'fields' => $searchFields, 320 | 'queryText' => $queryText, 321 | 'minimumShouldMatch' => "100%", 322 | 'tieBreaker' => $relevanceConfig->getTieBreaker(), 323 | 'fuzzinessConfig' => $relevanceConfig->getFuzzinessConfiguration(), 324 | 'cutoffFrequency' => $relevanceConfig->getCutoffFrequency(), 325 | ]; 326 | 327 | return $this->queryFactory->create(QueryInterface::TYPE_MULTIMATCH, $queryParams); 328 | } 329 | 330 | /** 331 | * Phonentic query part. 332 | * 333 | * @param ContainerConfigurationInterface $containerConfig Search request container configuration. 334 | * @param string $queryText The text query. 335 | * 336 | * @return QueryInterface 337 | */ 338 | private function getPhoneticQuery(ContainerConfigurationInterface $containerConfig, $queryText) 339 | { 340 | $relevanceConfig = $containerConfig->getRelevanceConfig(); 341 | $analyzer = FieldInterface::ANALYZER_PHONETIC; 342 | $defaultSearchField = MappingInterface::DEFAULT_SPELLING_FIELD; 343 | $fuzzyFieldFilter = $this->fieldFilters['fuzzyFieldFilter']; 344 | 345 | $searchFields = $this->getWeightedFields($containerConfig, $analyzer, $fuzzyFieldFilter, $defaultSearchField); 346 | 347 | $queryParams = [ 348 | 'fields' => $searchFields, 349 | 'queryText' => $queryText, 350 | 'minimumShouldMatch' => "100%", 351 | 'tieBreaker' => $relevanceConfig->getTieBreaker(), 352 | 'cutoffFrequency' => $relevanceConfig->getCutoffFrequency(), 353 | ]; 354 | 355 | return $this->queryFactory->create(QueryInterface::TYPE_MULTIMATCH, $queryParams); 356 | } 357 | 358 | /** 359 | * Build an array of weighted fields to be searched with the ability to apply a filter callback method and a default field. 360 | * 361 | * @param ContainerConfigurationInterface $containerConfig Search request container config. 362 | * @param null $analyzer Target analyzer. 363 | * @param FieldFilterInterface|null $fieldFilter Field filter. 364 | * @param string|null $defaultField Default search field. 365 | * @param integer $boost Additional boost applied to the fields (multiplicative). 366 | * 367 | * @return array 368 | */ 369 | private function getWeightedFields( 370 | ContainerConfigurationInterface $containerConfig, 371 | $analyzer = null, 372 | FieldFilterInterface $fieldFilter = null, 373 | $defaultField = null, 374 | $boost = 1 375 | ) { 376 | 377 | $mapping = $containerConfig->getMapping(); 378 | 379 | return $mapping->getWeightedSearchProperties($analyzer, $defaultField, $boost, $fieldFilter); 380 | } 381 | } 382 | -------------------------------------------------------------------------------- /src/module-elasticsuite-autocomplete/Provider/Config.php: -------------------------------------------------------------------------------- 1 | 17 | * @copyright 2021 Web200 18 | * @license https://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) 19 | * @link https://www.web200.fr/ 20 | */ 21 | class Config 22 | { 23 | public const CATEGORY_ACTIVE = 'smile_elasticsuite_autocomplete_settings/category_autocomplete/is_active'; 24 | public const PRODUCT_ACTIVE = 'smile_elasticsuite_autocomplete_settings/product_autocomplete/is_active'; 25 | public const IMPROVE_SEARCH_ACTIVE = 'smile_elasticsuite_autocomplete_settings/product_autocomplete/improve_search_active'; 26 | public const PRODUCT_MAX_SIZE = 'smile_elasticsuite_autocomplete_settings/product_autocomplete/max_size'; 27 | public const CATEGORY_MAX_SIZE = 'smile_elasticsuite_autocomplete_settings/category_autocomplete/max_size'; 28 | /** 29 | * Scope config interface 30 | * 31 | * @var ScopeConfigInterface $scopeConfig 32 | */ 33 | protected $scopeConfig; 34 | 35 | /** 36 | * Query constructor. 37 | * 38 | * @param ScopeConfigInterface $scopeConfig 39 | */ 40 | public function __construct( 41 | ScopeConfigInterface $scopeConfig 42 | ) { 43 | $this->scopeConfig = $scopeConfig; 44 | } 45 | 46 | /** 47 | * Is category active 48 | * 49 | * @param null $store 50 | * 51 | * @return bool 52 | */ 53 | public function isCategoryActive($store = null): bool 54 | { 55 | return (bool)$this->scopeConfig->getValue( 56 | self::CATEGORY_ACTIVE, 57 | ScopeInterface::SCOPE_STORES, 58 | $store 59 | ); 60 | } 61 | 62 | /** 63 | * Is product active 64 | * 65 | * @param null $store 66 | * 67 | * @return bool 68 | */ 69 | public function isProductActive($store = null): bool 70 | { 71 | return (bool)$this->scopeConfig->getValue( 72 | self::PRODUCT_ACTIVE, 73 | ScopeInterface::SCOPE_STORES, 74 | $store 75 | ); 76 | } 77 | 78 | /** 79 | * Is improve search active 80 | * 81 | * @param null $store 82 | * 83 | * @return bool 84 | */ 85 | public function isImproveSearchActive($store = null): bool 86 | { 87 | return (bool)$this->scopeConfig->getValue( 88 | self::IMPROVE_SEARCH_ACTIVE, 89 | ScopeInterface::SCOPE_STORES, 90 | $store 91 | ); 92 | } 93 | 94 | /** 95 | * Get product autocomplete max size 96 | * 97 | * @param null $store 98 | * 99 | * @return int|null 100 | */ 101 | public function getProductAutocompleteMaxSize($store = null): ?int 102 | { 103 | return (int)$this->scopeConfig->getValue( 104 | self::PRODUCT_MAX_SIZE, 105 | ScopeInterface::SCOPE_STORES, 106 | $store 107 | ); 108 | } 109 | 110 | /** 111 | * Get category autocomplete max size 112 | * 113 | * @param null $store 114 | * 115 | * @return int|null 116 | */ 117 | public function getCategoryAutocompleteMaxSize($store = null): ?int 118 | { 119 | return (int)$this->scopeConfig->getValue( 120 | self::CATEGORY_MAX_SIZE, 121 | ScopeInterface::SCOPE_STORES, 122 | $store 123 | ); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/module-elasticsuite-autocomplete/ViewModel/Form.php: -------------------------------------------------------------------------------- 1 | storeManager = $storeManager; 54 | $this->urlBuilder = $urlBuilder; 55 | $this->scopeConfig = $scopeConfig; 56 | } 57 | 58 | /** 59 | * Get search url 60 | * 61 | * @return string 62 | * @throws NoSuchEntityException 63 | */ 64 | public function getSearchUrl(): string 65 | { 66 | $useStoreInUrl = (bool)$this->scopeConfig->getValue(Store::XML_PATH_STORE_IN_URL, ScopeInterface::SCOPE_STORE); 67 | if (!$useStoreInUrl) { 68 | return $this->urlBuilder->getUrl('', ['_direct' => 'search.php']); 69 | } 70 | 71 | return '/search.php?store_code=' . $this->storeManager->getStore()->getCode(); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/module-elasticsuite-autocomplete/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web200/magento-elasticsuite-autocomplete", 3 | "description": "Magento 2 Module to speed autocomplete search", 4 | "require": { 5 | "php": "^7.0" 6 | }, 7 | "type": "magento2-module", 8 | "license": [ 9 | "OSL-3.0", 10 | "AFL-3.0" 11 | ], 12 | "autoload": { 13 | "files": [ 14 | "registration.php" 15 | ], 16 | "psr-4": { 17 | "Web200\\ElasticsuiteAutocomplete\\": "" 18 | } 19 | }, 20 | "extra": { 21 | "map": [ 22 | [ 23 | "pub/search.php", 24 | "pub/search.php" 25 | ] 26 | ] 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/module-elasticsuite-autocomplete/etc/adminhtml/system.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 | 7 | 8 | 9 | Magento\Config\Model\Config\Source\Yesno 10 | 11 | 12 | 13 | Magento\Config\Model\Config\Source\Yesno 14 | Add prefix and fuzziness search for queries. 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | Magento\Config\Model\Config\Source\Yesno 23 | 24 | 25 |
26 |
27 |
28 | -------------------------------------------------------------------------------- /src/module-elasticsuite-autocomplete/etc/config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 1 7 | 8 | 9 | 1 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/module-elasticsuite-autocomplete/etc/di.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Web200\ElasticsuiteAutocomplete\Model\Indexer\Product\Fulltext\Datasource\Url 8 | Web200\ElasticsuiteAutocomplete\Model\Indexer\Product\Fulltext\Datasource\ConfigurablePrice 9 | Web200\ElasticsuiteAutocomplete\Model\Indexer\Product\Fulltext\Datasource\AdditionalAttributes 10 | 11 | 12 | Web200\ElasticsuiteAutocomplete\Model\Indexer\Category\Fulltext\Datasource\Url 13 | Web200\ElasticsuiteAutocomplete\Model\Indexer\Category\Fulltext\Datasource\Breadcrumbs 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | Smile\ElasticsuiteCore\Search\Request\Query\Fulltext\SearchableFieldFilter 27 | Smile\ElasticsuiteCore\Search\Request\Query\Fulltext\FuzzyFieldFilter 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/module-elasticsuite-autocomplete/etc/frontend/routes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/module-elasticsuite-autocomplete/etc/module.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/module-elasticsuite-autocomplete/registration.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Web200\ElasticsuiteAutocomplete\ViewModel\Form 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/pub/search.php: -------------------------------------------------------------------------------- 1 | [DirectoryList::URL_PATH => ''], 14 | DirectoryList::MEDIA => [DirectoryList::URL_PATH => 'media'], 15 | DirectoryList::STATIC_VIEW => [DirectoryList::URL_PATH => 'static'], 16 | DirectoryList::UPLOAD => [DirectoryList::URL_PATH => 'media/upload'], 17 | ] 18 | ); 19 | 20 | $params[StoreManager::PARAM_RUN_CODE] = $_SERVER[StoreManager::PARAM_RUN_CODE] ?? null; 21 | if (isset($_REQUEST['store_code'])) { 22 | $params[StoreManager::PARAM_RUN_CODE] = $_REQUEST['store_code']; 23 | } 24 | $bootstrap = Bootstrap::create(BP, $params); 25 | 26 | $obj = $bootstrap->getObjectManager(); 27 | $obj->get('Magento\Framework\App\State')->setAreaCode('frontend'); 28 | $obj->get('Magento\Framework\View\DesignLoader')->load(Area::PART_DESIGN); 29 | 30 | header('Content-Type: application/json'); 31 | echo json_encode($obj->get('Web200\ElasticsuiteAutocomplete\Model\Query')->execute()); 32 | --------------------------------------------------------------------------------