diff --git a/Adapter/Client.php b/Adapter/Client.php index 8355e6e..54dd82e 100644 --- a/Adapter/Client.php +++ b/Adapter/Client.php @@ -2,6 +2,7 @@ namespace Develo\Typesense\Adapter; +use Algolia\AlgoliaSearch\Helper\ConfigHelper as AlgoliaConfigHelper; use Typesense\Client as TypeSenseClient; use Develo\Typesense\Services\ConfigService; use Algolia\AlgoliaSearch\Helper\Data as AlgoliaHelper; @@ -16,20 +17,18 @@ */ class Client { + private ?array $facets = null; + /** * @var ConfigService */ private ConfigService $configService; - /** - * @var TypeSenseClient|null - */ private ?TypeSenseClient $typeSenseClient = null; - /** - * $var AlgoliaHelper - */ - private $algoliaHelper; + private AlgoliaHelper $algoliaHelper; + + private AlgoliaConfigHelper $configHelper; /** * Initialise Typesense Client with Magento config @@ -40,11 +39,13 @@ class Client */ public function __construct( ConfigService $configService, - AlgoliaHelper $algoliaHelper + AlgoliaHelper $algoliaHelper, + AlgoliaConfigHelper $configHelper ) { $this->configService = $configService; $this->algoliaHelper = $algoliaHelper; + $this->configHelper = $configHelper; } /** @@ -63,6 +64,7 @@ public function deleteIndex(string $indexName): array */ public function addData($indexName, $data) { + $facets = []; foreach ($data as &$item) { $item['id'] = (string)$item['objectID']; $item['objectID'] = (string)$item['objectID']; @@ -83,7 +85,14 @@ public function addData($indexName, $data) $price['default'] = number_format($price['default'], 2); } + + foreach ($facets as $facet) { + if (isset($item[$facet]) && !is_array($item[$facet])) { + $item[$facet] = [strval($item[$facet])]; + } + } } + $indexName = rtrim($indexName, "_tmp"); return $this->getTypesenseClient()->collections[$indexName]->getDocuments()->import($data, ['action' => 'upsert']); } @@ -140,6 +149,18 @@ public function getTypesenseClient(): TypeSenseClient return $this->typeSenseClient; } + private function getFacets() + { + if (!is_array($this->facets)) { + $this->facets = []; + foreach ($this->configHelper->getFacets() as $facet) { + $this->facets[] = $facet['attribute']; + } + } + + return $this->facets; + } + } diff --git a/Helper/ConfigChangeHelper.php b/Helper/ConfigChangeHelper.php index 633e501..270e888 100644 --- a/Helper/ConfigChangeHelper.php +++ b/Helper/ConfigChangeHelper.php @@ -103,6 +103,9 @@ public function __construct( */ public function setCollectionConfig() { + if (!$this->configService->isTypeSenseEnabled()) { + return $this; + } $facets = []; @@ -128,18 +131,20 @@ public function setCollectionConfig() $indexName = $index["indexName"] . "_{$indexToCreate}"; - if (!isset($existingCollections[$indexName])) { + if (isset($existingCollections[$indexName])) { + $this->typesenseClient->collections[$indexName]->delete(); + unset($existingCollections[$indexName]); + } + - $this->typeSenseCollecitons->create( - [ - 'name' => $indexName, - 'enable_nested_fields' => true, - 'fields' => $fields - ] - ); + $this->typeSenseCollecitons->create( + [ + 'name' => $indexName, + 'enable_nested_fields' => true, + 'fields' => $fields + ] + ); - continue; - } } } @@ -174,7 +179,8 @@ private function getMagentoIndexes() return $indexNames; } - public function getFields(array $facets, array $sortingAttributes, string $index) : array { + public function getFields(array $facets, array $sortingAttributes, string $index): array + { switch ($index) { case 'products': $attributes = $this->algoliaConfigHelper->getProductAdditionalAttributes(); @@ -186,6 +192,16 @@ public function getFields(array $facets, array $sortingAttributes, string $index ['name' => 'visibility_catalog', 'type' => 'int64', 'facet' => true] ]; + // The hierarchal menu widget expects 10 levels of category. + for ($i = 0; $i < 10; $i++) { + $defaultAttributes[] = [ + 'name' => 'categories.level' . $i, + 'type' => 'string[]', + 'facet' => true, + 'optional' => true + ]; + } + break; case 'categories': $attributes = $this->algoliaConfigHelper->getCategoryAdditionalAttributes(); @@ -217,21 +233,8 @@ public function getFields(array $facets, array $sortingAttributes, string $index $attributeCollection = $this->attributeRepository->getList($entityTypeCode, $searchCriteria->create()); - $backendTypes = [ - 'datetime' => 'string', - 'decimal' => 'float', - 'int' => 'int64', - 'static' => 'string', - 'text' => 'string', - 'varchar' => 'string' - ]; - $fields = []; foreach ($attributeCollection->getItems() as $attribute) { - if (!isset($backendTypes[$attribute->getBackendType()]) || !$attribute->getIsRequired()) { - continue; - } - if ($attribute->getAttributeCode() === 'price') { $fields[] = [ 'name' => $attribute->getAttributeCode(), @@ -258,12 +261,18 @@ public function getFields(array $facets, array $sortingAttributes, string $index continue; } + $isFacet = in_array($attribute->getAttributeCode(), $facets); + + if (!$isFacet) { + continue; + } + $fields[] = [ 'name' => $attribute->getAttributeCode(), - 'type' => $backendTypes[$attribute->getBackendType()], - 'facet' => in_array($attribute->getAttributeCode(), $facets), - 'sort' => in_array($attribute->getAttributeCode(), $sortingAttributes) && - in_array($backendTypes[$attribute->getBackendType()], self::SORTABLE_ATTRIBUTES), + 'type' => 'string[]', + 'facet' => $isFacet, + 'sort' => false, + 'optional' => !$attribute->getIsRequired() ]; } @@ -274,7 +283,7 @@ public function getFields(array $facets, array $sortingAttributes, string $index return array_values($fields); } - public function getSearchableAttributes(string $index = self::INDEX_PRODUCTS) : string + public function getSearchableAttributes(string $index = self::INDEX_PRODUCTS): string { $attributes = []; foreach ($this->getFields([], [], $index) as $field) { diff --git a/Plugin/Algolia/AlgoliaSearch/Model/SaveSettings.php b/Plugin/Algolia/AlgoliaSearch/Model/SaveSettings.php new file mode 100644 index 0000000..b1b02d0 --- /dev/null +++ b/Plugin/Algolia/AlgoliaSearch/Model/SaveSettings.php @@ -0,0 +1,41 @@ +configService = $configService; + } + + public function aroundExecute( + \Algolia\AlgoliaSearch\Model\Observer\SaveSettings $subject, + \Closure $proceed + ) { + if($this->configService->isIndexModeTypeSenseOnly()){ + return; + } + + $result = $proceed(); + + return $result; + } +} diff --git a/README.md b/README.md index 53913fc..d0410ba 100644 --- a/README.md +++ b/README.md @@ -1,115 +1,87 @@ - - +# Magento 2 Typesense Search Integration Module - -
-
-

Magento 2 Typesense Adapter Module

+This module integrates the Typesense search engine with Magento, providing faster and more accurate search results for your customers. -

-
-

This is currently just a proof of concept!

-
- It's an Adapter Client for the main Algolia Magento 2 module. -
- We let the existing Open Source Algolia module handle indexing, queues etc, when the data is ready to be indexed it's pushed Typesense. -
- Why reinvent the wheel? -

-
+## Installation +### Composer Installation +You can install the module via Composer. Run the following command in your Magento 2 root directory: - -
- Table of Contents -
    -
  1. - Getting Started - -
  2. -
  3. Roadmap
  4. -
  5. Contributing
  6. -
  7. License
  8. -
  9. Contact
  10. -
  11. Acknowledgments
  12. -
-
+``` +composer require develodesign/magento2-module-typesense +``` - -## Getting Started +### Copying the Module -Composer install this module, it will include the Algolia module as a dependancy. +Alternatively, you can copy the module files to the `app/code/Develo/Typesense` directory in your Magento 2 installation. -### Installation +``` +php bin/magento module:enable Develo_Typesense +php bin/magento setup:upgrade +php bin/magento setup:di:compile +bin/magento setup:static-content:deploy +``` - ```shell - composer require develodesign/magento2-module-typesense - ``` - - Add Typesene Configuration - - System Config -> Type Sense -> Settings -> General +That's it! The develodesign/magento2-module-typesense module is now installed on your Magento 2 store. -

(back to top)

+## Configuration +System > Configuration > General > Typesense Search: - -## Roadmap +- "Enabled": A yes/no field to enable or disable the Typesense adapter. +- "Cloud ID": A text field to enter the Typesense cloud ID. +- "Admin API Key": A secret key to enter the Typesense admin API key. +- "Search Only Key": A public key to enter the Typesense search only key. +- "Nodes": A text field to enter the Typesense nodes. +- "Port": A text field to enter the Typesense port number. +- "Path": A text field to enter the Typesense path. +- "Protocol": A dropdown field to select the communication protocol. +- "Index Method": A dropdown field to select where the data should be indexed. -- [x] Create Basic module and Config -- [x] Index Product Data -- [ ] Index Categories -- [ ] Admin Facets -- [ ] Loads More .... +These options allow users to configure the Typesense adapter module and customize its behavior according to their needs. -

(back to top)

+***After enabling the Typesense module, if a user makes any changes to the configuration, the module will need to drop and rebuild the collections. As a result, the user will need to perform a full Magento reindex after making any configuration changes. This is important to keep in mind to ensure that the search results are accurate and up-to-date.*** +Note that users also need to configure the Algolia module to fit you requirements. However, live credentials are not needed as our module acts as an adapter. - -## Contributing +The Typesense module uses the Algolia settings, so users should configure Algolia as they normally would. It's important to note that if you set a facet, you must also set it in the product attribute section. -Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are **greatly appreciated**. +For more information on customizing the Algolia module, please refer to the following links: -If you have a suggestion that would make this better, please fork the repo and create a pull request. You can also simply open an issue with the tag "enhancement". -Don't forget to give the project a star! Thanks again! +- [Customizing Autocomplete Menu](https://www.algolia.com/doc/integration/magento-2/customize/autocomplete-menu/) +- [Customizing Instant Search Page](https://www.algolia.com/doc/integration/magento-2/customize/instant-search-page/) +- [Customizing Custom Front-end Events](https://www.algolia.com/doc/integration/magento-2/customize/custom-front-end-events/) -1. Fork the Project -2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`) -3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`) -4. Push to the Branch (`git push origin feature/AmazingFeature`) -5. Open a Pull Request +## Documentation -

(back to top)

+For more information about Typesense, check out their [official documentation](https://typesense.org/docs/). +You can also check out [Algolia's Magento 2 module](https://github.com/algolia/algoliasearch-magento-2). +## Contributors - -## License +| Name | Email | Twitter | +| -------------- | ---------------------------------------- | ----------------------------- | +| Luke Collymore | [luke@develodesign.co.uk](mailto:luke@develodesign.co.uk) | [@lukecollymore](https://twitter.com/lukecollymore) | +| Nathan McBride | [nathan@brideo.co.uk](mailto:nathan@brideo.co.uk) | [@brideoweb](https://twitter.com/brideoweb) | -Distributed under the NU General Public License. See `LICENSE.txt` for more information. -

(back to top)

+### How to Contribute +Contributions are always welcome. If you have any suggestions or find any issues, please create a GitHub issue or fork the repository and submit a pull request. +Here's how to contribute: - -## Contact -Luke Collymore - [@lukecollymore](https://twitter.com/lukecollymore) - luke@develodesign.co.uk +1. Fork the project. +2. Create your feature branch (`git checkout -b feature/AmazingFeature`). +3. Commit your changes (`git commit -m 'Add some AmazingFeature'`). +4. Push to the branch (`git push origin feature/AmazingFeature`). +5. Open a pull request. -@Nathan McBride - nathan@brideo.co.uk - -Project Link: [https://github.com/develodesign/magento2-module-typesense](https://github.com/develodesign/magento2-module-typesense) - -

(back to top)

- - - ## Acknowledgments + Algolia for creating a great product indexing and search configuration module + * [Algolia Open Source Module](https://github.com/algolia/algoliasearch-magento-2) -* [Best-README-Template](https://github.com/othneildrew/Best-README-Template) -

(back to top)

diff --git a/Services/ConfigService.php b/Services/ConfigService.php index a060ef6..b9ef61c 100644 --- a/Services/ConfigService.php +++ b/Services/ConfigService.php @@ -32,7 +32,7 @@ class ConfigService * @var EncryptorInterface $encryptor */ protected EncryptorInterface $encryptor; - + /** * @param EncryptorInterface $encryptor * @param ScopeConfigInterface $scopeConfig @@ -121,13 +121,24 @@ public function getIndexMethod(): ?string } /** - * Check if Typesense Index Mode is TypesenseOnly + * @return bool */ public function isIndexModeTypeSenseOnly(){ - $indexMethod = $this->getIndexMethod(); - if( $indexMethod == TypeSenseIndexMethod::METHOD_TYPESENSE ){ - return true; - } - return false; + return $this->getIndexMethod() === TypeSenseIndexMethod::METHOD_TYPESENSE; + } + + /** + * @return bool + */ + public function isIndexModeBoth(){ + return $this->getIndexMethod() === TypeSenseIndexMethod::METHOD_BOTH; + } + + /** + * @return bool + */ + public function isTypeSenseEnabled() + { + return $this->isEnabled() && ($this->isIndexModeTypeSenseOnly() || $this->isIndexModeBoth()); } } diff --git a/ViewModel/Adminhtml/Configuration.php b/ViewModel/Adminhtml/Configuration.php new file mode 100644 index 0000000..d27506a --- /dev/null +++ b/ViewModel/Adminhtml/Configuration.php @@ -0,0 +1,32 @@ +configService = $configService; + } + + /** + * @return bool + */ + public function isTypeSenseEnabled() + { + return $this->configService->isTypeSenseEnabled(); + } +} diff --git a/ViewModel/Form.php b/ViewModel/Form.php index 0f5e29a..225eeeb 100644 --- a/ViewModel/Form.php +++ b/ViewModel/Form.php @@ -63,6 +63,7 @@ public function getJsConfig(): string public function getAutocompleteScripts() { return $this->json->serialize( [ + $this->getAssetUrl('Hyva_AlgoliaSearch::js/internals/algoliaBundle.min.js'), $this->getAssetUrl('Hyva_AlgoliaSearch::js/internals/autocomplete-js.js'), $this->getAssetUrl('Hyva_AlgoliaSearch::js/internals/algoliasearch.js'), $this->getAssetUrl('Hyva_AlgoliaSearch::js/internals/algoliasearch-query-suggestion-plugin.js') diff --git a/etc/adminhtml/di.xml b/etc/adminhtml/di.xml index 95e348a..4ba2dd6 100644 --- a/etc/adminhtml/di.xml +++ b/etc/adminhtml/di.xml @@ -12,4 +12,8 @@ - \ No newline at end of file + + + + + diff --git a/etc/adminhtml/events.xml b/etc/adminhtml/events.xml index ff83349..7c2fb04 100644 --- a/etc/adminhtml/events.xml +++ b/etc/adminhtml/events.xml @@ -1,6 +1,22 @@ - + - \ No newline at end of file + + + + + + + + + + + + + + + + diff --git a/view/adminhtml/layout/adminhtml_system_config_edit.xml b/view/adminhtml/layout/adminhtml_system_config_edit.xml new file mode 100644 index 0000000..5a79ba3 --- /dev/null +++ b/view/adminhtml/layout/adminhtml_system_config_edit.xml @@ -0,0 +1,11 @@ + + + + + + + Develo\Typesense\ViewModel\Adminhtml\Configuration + + + + diff --git a/view/adminhtml/templates/configuration.phtml b/view/adminhtml/templates/configuration.phtml new file mode 100644 index 0000000..6222727 --- /dev/null +++ b/view/adminhtml/templates/configuration.phtml @@ -0,0 +1,66 @@ +getData('view_model'); +if (!$viewModel instanceof Configuration) { + return ''; +} + +if (!$viewModel->isTypeSenseEnabled()) { + return ''; +} + +?> + +