diff --git a/src/module-elasticsuite-core/Api/Healthcheck/CheckInterface.php b/src/module-elasticsuite-core/Api/Healthcheck/CheckInterface.php
new file mode 100644
index 000000000..b1194c760
--- /dev/null
+++ b/src/module-elasticsuite-core/Api/Healthcheck/CheckInterface.php
@@ -0,0 +1,51 @@
+
+ * @copyright 2024 Smile
+ * @license Open Software License ("OSL") v. 3.0
+ */
+
+namespace Smile\ElasticsuiteCore\Api\Healthcheck;
+
+/**
+ * Health CheckInterface.
+ */
+interface CheckInterface
+{
+ /**
+ * Retrieve the unique identifier for the health check.
+ *
+ * @return string
+ */
+ public function getIdentifier(): string;
+
+ /**
+ * Retrieve the description of the health check.
+ *
+ * @return string
+ */
+ public function getDescription(): string;
+
+ /**
+ * Retrieve the status of the health check.
+ * Expected values: 'success', 'warning'.
+ *
+ * @return string
+ */
+ public function getStatus(): string;
+
+ /**
+ * Retrieve the sort order for the health check, which determines
+ * the display order of checks in the admin panel.
+ *
+ * @return int
+ */
+ public function getSortOrder(): int;
+}
diff --git a/src/module-elasticsuite-core/Block/Adminhtml/Healthcheck/Healthcheck.php b/src/module-elasticsuite-core/Block/Adminhtml/Healthcheck/Healthcheck.php
new file mode 100644
index 000000000..d9dae75a0
--- /dev/null
+++ b/src/module-elasticsuite-core/Block/Adminhtml/Healthcheck/Healthcheck.php
@@ -0,0 +1,61 @@
+
+ * @copyright 2024 Smile
+ * @license Open Software License ("OSL") v. 3.0
+ */
+
+namespace Smile\ElasticsuiteCore\Block\Adminhtml\Healthcheck;
+
+use Magento\Backend\Block\Template;
+use Magento\Backend\Block\Template\Context;
+use Smile\ElasticsuiteCore\Api\Healthcheck\CheckInterface;
+use Smile\ElasticsuiteCore\Healthcheck\HealthcheckList;
+
+/**
+ * Class Healthcheck.
+ *
+ * Block class for displaying Elasticsuite health checks in the Magento Admin panel.
+ */
+class Healthcheck extends Template
+{
+ /**
+ * HealthcheckList instance to manage and retrieve health checks.
+ *
+ * @var HealthcheckList
+ */
+ private $healthcheckList;
+
+ /**
+ * Constructor.
+ *
+ * @param Context $context Magento context object for backend blocks.
+ * @param HealthcheckList $healthcheckList The health check list object providing health check data.
+ * @param array $data Additional block data.
+ */
+ public function __construct(Context $context, HealthcheckList $healthcheckList, array $data = [])
+ {
+ parent::__construct($context, $data);
+ $this->healthcheckList = $healthcheckList;
+ }
+
+ /**
+ * Retrieve all health checks.
+ *
+ * Provides an array of health check instances, each implementing the CheckInterface,
+ * sorted by their specified order.
+ *
+ * @return CheckInterface[]
+ */
+ public function getHealthchecks(): array
+ {
+ return $this->healthcheckList->getChecks();
+ }
+}
diff --git a/src/module-elasticsuite-core/Controller/Adminhtml/Healthcheck/Index.php b/src/module-elasticsuite-core/Controller/Adminhtml/Healthcheck/Index.php
new file mode 100644
index 000000000..9fffa54a9
--- /dev/null
+++ b/src/module-elasticsuite-core/Controller/Adminhtml/Healthcheck/Index.php
@@ -0,0 +1,42 @@
+
+ * @copyright 2024 Smile
+ * @license Open Software License ("OSL") v. 3.0
+ */
+
+namespace Smile\ElasticsuiteCore\Controller\Adminhtml\Healthcheck;
+
+use Magento\Framework\App\Action\HttpGetActionInterface;
+use Magento\Framework\Controller\ResultFactory;
+use Magento\Framework\View\Result\Page;
+use Smile\ElasticsuiteIndices\Controller\Adminhtml\AbstractAction;
+
+/**
+ * Class Index.
+ */
+class Index extends AbstractAction implements HttpGetActionInterface
+{
+ /**
+ * @inheritdoc
+ *
+ * @return Page
+ */
+ public function execute(): Page
+ {
+ $breadMain = __('Healthcheck');
+
+ /** @var Page $resultPage */
+ $resultPage = $this->resultFactory->create(ResultFactory::TYPE_PAGE);
+ $resultPage->getConfig()->getTitle()->prepend($breadMain);
+
+ return $resultPage;
+ }
+}
diff --git a/src/module-elasticsuite-core/Healthcheck/GhostIndicesCheck.php b/src/module-elasticsuite-core/Healthcheck/GhostIndicesCheck.php
new file mode 100644
index 000000000..9e9133dee
--- /dev/null
+++ b/src/module-elasticsuite-core/Healthcheck/GhostIndicesCheck.php
@@ -0,0 +1,164 @@
+
+ * @copyright 2024 Smile
+ * @license Open Software License ("OSL") v. 3.0
+ */
+
+namespace Smile\ElasticsuiteCore\Healthcheck;
+
+use Exception;
+use Magento\Framework\UrlInterface;
+use Smile\ElasticsuiteCore\Api\Healthcheck\CheckInterface;
+use Smile\ElasticsuiteIndices\Model\IndexStatsProvider;
+
+/**
+ * Class GhostIndicesCheck.
+ *
+ * Health check to identify any ghost indices in the Elasticsearch cluster.
+ */
+class GhostIndicesCheck implements CheckInterface
+{
+ /**
+ * Route to Elasticsuite -> Indices page.
+ */
+ private const ROUTE_ELASTICSUITE_INDICES = 'smile_elasticsuite_indices';
+
+ public const GHOST_STATUS = 'ghost';
+
+ /**
+ * @var IndexStatsProvider
+ */
+ private $indexStatsProvider;
+
+ /**
+ * @var UrlInterface
+ */
+ private $urlBuilder;
+
+ /**
+ * Constructor.
+ *
+ * @param IndexStatsProvider $indexStatsProvider Index stats provider.
+ * @param UrlInterface $urlBuilder URL builder.
+ */
+ public function __construct(
+ IndexStatsProvider $indexStatsProvider,
+ UrlInterface $urlBuilder
+ ) {
+ $this->indexStatsProvider = $indexStatsProvider;
+ $this->urlBuilder = $urlBuilder;
+ }
+
+ /**
+ * Retrieve the unique identifier for this health check.
+ *
+ * @return string
+ */
+ public function getIdentifier(): string
+ {
+ return 'ghost_indices_check';
+ }
+
+ /**
+ * Retrieve a brief description of this health check.
+ *
+ * @return string
+ * @throws Exception
+ */
+ public function getDescription(): string
+ {
+ $ghostCount = $this->getNumberOfGhostIndices();
+
+ if ($ghostCount > 0) {
+ // Description when ghost indices are found.
+ // @codingStandardsIgnoreStart
+ return __(
+ 'You have %1 ghost indices. Ghost indices have a footprint on your Elasticsearch cluster health. '
+ . 'You should consider removing them.
'
+ . 'Click here to go to the Elasticsuite Indices page to take appropriate actions.',
+ $ghostCount,
+ $this->getElasticsuiteIndicesUrl()
+ );
+ // @codingStandardsIgnoreEnd
+ }
+
+ // Description when no ghost indices are found.
+ return __('There are no ghost indexes in your Elasticsearch cluster. No action is required at this time.');
+ }
+
+ /**
+ * Retrieve the status of this health check.
+ * Returns 'warning' if ghost indices are found, otherwise 'success'.
+ *
+ * @return string
+ * @throws Exception
+ */
+ public function getStatus(): string
+ {
+ return $this->hasGhostIndices() ? 'warning' : 'success';
+ }
+
+ /**
+ * Retrieve the sort order for this health check.
+ *
+ * @return int
+ */
+ public function getSortOrder(): int
+ {
+ return 10; // Adjust as necessary.
+ }
+
+ /**
+ * Checks if there are any ghost indices.
+ *
+ * @return bool
+ * @throws Exception
+ */
+ private function hasGhostIndices(): bool
+ {
+ return $this->getNumberOfGhostIndices() > 0;
+ }
+
+ /**
+ * Get number of ghost Elasticsuite indices.
+ *
+ * @return int
+ * @throws Exception
+ */
+ private function getNumberOfGhostIndices(): int
+ {
+ $ghostIndices = 0;
+ $elasticsuiteIndices = $this->indexStatsProvider->getElasticSuiteIndices();
+
+ if ($elasticsuiteIndices !== null) {
+ foreach ($elasticsuiteIndices as $indexName => $indexAlias) {
+ $indexData = $this->indexStatsProvider->indexStats($indexName, $indexAlias);
+
+ if (array_key_exists('index_status', $indexData)
+ && $indexData['index_status'] === self::GHOST_STATUS) {
+ $ghostIndices++;
+ }
+ }
+ }
+
+ return $ghostIndices;
+ }
+
+ /**
+ * Retrieve a URL to the Elasticsuite Indices page for more information.
+ *
+ * @return string
+ */
+ public function getElasticsuiteIndicesUrl(): string
+ {
+ return $this->urlBuilder->getUrl(self::ROUTE_ELASTICSUITE_INDICES);
+ }
+}
diff --git a/src/module-elasticsuite-core/Healthcheck/HealthcheckList.php b/src/module-elasticsuite-core/Healthcheck/HealthcheckList.php
new file mode 100644
index 000000000..587f99b26
--- /dev/null
+++ b/src/module-elasticsuite-core/Healthcheck/HealthcheckList.php
@@ -0,0 +1,58 @@
+
+ * @copyright 2024 Smile
+ * @license Open Software License ("OSL") v. 3.0
+ */
+
+namespace Smile\ElasticsuiteCore\Healthcheck;
+
+use Smile\ElasticsuiteCore\Api\Healthcheck\CheckInterface;
+
+/**
+ * Class HealthcheckList.
+ *
+ * Manages a list of health checks for the Elasticsuite module.
+ */
+class HealthcheckList
+{
+ /**
+ * Array of health checks implementing the CheckInterface.
+ *
+ * @var CheckInterface[]
+ */
+ private $checks;
+
+ /**
+ * Constructor.
+ *
+ * @param CheckInterface[] $checks Array of health checks to be managed by this list.
+ */
+ public function __construct(array $checks = [])
+ {
+ $this->checks = $checks;
+ }
+
+ /**
+ * Retrieve all health checks, sorted by their sort order.
+ *
+ * Sorts the checks based on the value returned by each check's `getSortOrder` method.
+ *
+ * @return CheckInterface[] Array of health checks sorted by order.
+ */
+ public function getChecks(): array
+ {
+ usort($this->checks, function (CheckInterface $a, CheckInterface $b) {
+ return $a->getSortOrder() <=> $b->getSortOrder();
+ });
+
+ return $this->checks;
+ }
+}
diff --git a/src/module-elasticsuite-core/Healthcheck/ReplicasConfigCheck.php b/src/module-elasticsuite-core/Healthcheck/ReplicasConfigCheck.php
new file mode 100644
index 000000000..434e517d1
--- /dev/null
+++ b/src/module-elasticsuite-core/Healthcheck/ReplicasConfigCheck.php
@@ -0,0 +1,167 @@
+
+ * @copyright 2024 Smile
+ * @license Open Software License ("OSL") v. 3.0
+ */
+
+namespace Smile\ElasticsuiteCore\Healthcheck;
+
+use Magento\Framework\UrlInterface;
+use Smile\ElasticsuiteCore\Api\Healthcheck\CheckInterface;
+use Smile\ElasticsuiteCore\Helper\IndexSettings as IndexSettingsHelper;
+use Smile\ElasticsuiteCore\Client\Client;
+
+/**
+ * Class ReplicasConfigCheck.
+ *
+ * Health check for replicas misconfiguration in Elasticsuite.
+ */
+class ReplicasConfigCheck implements CheckInterface
+{
+ /**
+ * Route to Stores -> Configuration section.
+ */
+ private const ROUTE_SYSTEM_CONFIG = 'adminhtml/system_config/edit';
+
+ /**
+ * Anchor for Stores -> Configuration -> ELASTICSUITE -> Base Settings -> Indices Settings.
+ */
+ private const ANCHOR_ES_INDICES_SETTINGS_PATH = 'smile_elasticsuite_core_base_settings_indices_settings-link';
+
+ /**
+ * URL for Elasticsuite Indices Settings Wiki page.
+ */
+ private const ES_INDICES_SETTINGS_WIKI_PAGE = 'https://github.com/Smile-SA/elasticsuite/wiki/ModuleInstall#indices-settings';
+
+ public const WARNING_STATUS = 'warning';
+
+ /**
+ * @var IndexSettingsHelper
+ */
+ protected $helper;
+
+ /**
+ * @var Client
+ */
+ protected $client;
+
+ /**
+ * @var UrlInterface
+ */
+ private $urlBuilder;
+
+ /**
+ * Constructor.
+ *
+ * @param IndexSettingsHelper $indexSettingHelper Index settings helper.
+ * @param Client $client Elasticsearch client.
+ * @param UrlInterface $urlBuilder URL builder.
+ */
+ public function __construct(
+ IndexSettingsHelper $indexSettingHelper,
+ Client $client,
+ UrlInterface $urlBuilder
+ ) {
+ $this->helper = $indexSettingHelper;
+ $this->client = $client;
+ $this->urlBuilder = $urlBuilder;
+ }
+
+ /**
+ * Retrieve the unique identifier for this health check.
+ *
+ * @return string
+ */
+ public function getIdentifier(): string
+ {
+ return 'replicas_config_check';
+ }
+
+ /**
+ * Retrieve a dynamic description for this health check based on its status.
+ *
+ * @return string
+ */
+ public function getDescription(): string
+ {
+ $status = $this->getStatus();
+
+ if ($status === self::WARNING_STATUS) {
+ // Description when the replicas configuration is incorrect.
+ // @codingStandardsIgnoreStart
+ return __(
+ 'The number of replicas configured for Elasticsuite is incorrect. '
+ . 'You can\'t use %1 replicas since there is only %2 nodes in your Elasticsearch cluster.
'
+ . 'Click here to go to the Elasticsuite Basic Settings page and change your Number of Replicas per Index parameter according to our Wiki page.',
+ $this->helper->getNumberOfReplicas(),
+ $this->getNumberOfNodes(),
+ $this->getElasticsuiteConfigUrl(),
+ self::ES_INDICES_SETTINGS_WIKI_PAGE
+ );
+ // @codingStandardsIgnoreEnd
+ }
+
+ // Description when the replicas configuration is optimized.
+ return __('The number of replicas is properly configured for the Elasticsearch cluster. No action is required at this time.');
+ }
+
+ /**
+ * Retrieve the status of this health check.
+ *
+ * @return string
+ */
+ public function getStatus(): string
+ {
+ $numberOfReplicas = $this->helper->getNumberOfReplicas();
+ $numberOfNodes = $this->getNumberOfNodes();
+
+ if ($numberOfReplicas > 1 && $numberOfReplicas > ($numberOfNodes - 1)) {
+ return 'warning';
+ }
+
+ return 'success';
+ }
+
+ /**
+ * Retrieve the sort order for this health check.
+ *
+ * @return int
+ */
+ public function getSortOrder(): int
+ {
+ return 20; // Adjust as necessary.
+ }
+
+ /**
+ * Get the number of nodes in the Elasticsearch cluster.
+ *
+ * @return int
+ */
+ private function getNumberOfNodes(): int
+ {
+ $nodeInfo = $this->client->nodes()->info()['_nodes'] ?? [];
+
+ return $nodeInfo['total'] ?? 0;
+ }
+
+ /**
+ * Get URL to the admin Elasticsuite Configuration page.
+ *
+ * @return string
+ */
+ private function getElasticsuiteConfigUrl(): string
+ {
+ return $this->urlBuilder->getUrl(
+ self::ROUTE_SYSTEM_CONFIG,
+ ['section' => 'smile_elasticsuite_core_base_settings', '_fragment' => self::ANCHOR_ES_INDICES_SETTINGS_PATH]
+ );
+ }
+}
diff --git a/src/module-elasticsuite-core/Healthcheck/ShardsConfigCheck.php b/src/module-elasticsuite-core/Healthcheck/ShardsConfigCheck.php
new file mode 100644
index 000000000..66e04fe9f
--- /dev/null
+++ b/src/module-elasticsuite-core/Healthcheck/ShardsConfigCheck.php
@@ -0,0 +1,192 @@
+
+ * @copyright 2024 Smile
+ * @license Open Software License ("OSL") v. 3.0
+ */
+
+namespace Smile\ElasticsuiteCore\Healthcheck;
+
+use Exception;
+use Magento\Framework\UrlInterface;
+use Smile\ElasticsuiteCore\Api\Healthcheck\CheckInterface;
+use Smile\ElasticsuiteCore\Helper\IndexSettings as IndexSettingsHelper;
+use Smile\ElasticsuiteIndices\Model\IndexStatsProvider;
+
+/**
+ * Class ShardsConfigCheck.
+ *
+ * Checks for shard misconfigurations in the Elasticsearch cluster.
+ */
+class ShardsConfigCheck implements CheckInterface
+{
+ /**
+ * Route to Stores -> Configuration section.
+ */
+ private const ROUTE_SYSTEM_CONFIG = 'adminhtml/system_config/edit';
+
+ /**
+ * Anchor for Stores -> Configuration -> ELASTICSUITE -> Base Settings -> Indices Settings.
+ */
+ private const ANCHOR_ES_INDICES_SETTINGS_PATH = 'smile_elasticsuite_core_base_settings_indices_settings-link';
+
+ /**
+ * URL for Elasticsuite Indices Settings Wiki page.
+ */
+ private const ES_INDICES_SETTINGS_WIKI_PAGE = 'https://github.com/Smile-SA/elasticsuite/wiki/ModuleInstall#indices-settings';
+
+ public const WARNING_STATUS = 'warning';
+
+ public const UNDEFINED_SIZE = 'undefined';
+
+ /**
+ * @var IndexSettingsHelper
+ */
+ private $indexSettingsHelper;
+
+ /**
+ * @var IndexStatsProvider
+ */
+ private $indexStatsProvider;
+
+ /**
+ * @var UrlInterface
+ */
+ private $urlBuilder;
+
+ /**
+ * Constructor.
+ *
+ * @param IndexSettingsHelper $indexSettingsHelper Index settings helper.
+ * @param IndexStatsProvider $indexStatsProvider Index stats provider.
+ * @param UrlInterface $urlBuilder URL builder.
+ */
+ public function __construct(
+ IndexSettingsHelper $indexSettingsHelper,
+ IndexStatsProvider $indexStatsProvider,
+ UrlInterface $urlBuilder
+ ) {
+ $this->indexSettingsHelper = $indexSettingsHelper;
+ $this->indexStatsProvider = $indexStatsProvider;
+ $this->urlBuilder = $urlBuilder;
+ }
+
+ /**
+ * Retrieve the unique identifier for this health check.
+ *
+ * @return string
+ */
+ public function getIdentifier(): string
+ {
+ return 'shards_config_check';
+ }
+
+ /**
+ * Retrieve a dynamic description for this health check based on its status.
+ *
+ * @return string
+ * @throws Exception
+ */
+ public function getDescription(): string
+ {
+ $numberOfShards = $this->indexSettingsHelper->getNumberOfShards();
+ $maxIndexSize = $this->getMaxIndexSize();
+ $status = $this->getStatus();
+
+ if ($status === self::WARNING_STATUS) {
+ // Description when the shard's configuration is incorrect.
+ // @codingStandardsIgnoreStart
+ return __(
+ 'The number of shards configured for Elasticsuite is incorrect. '
+ . 'You don\'t need to use %1 shards since your biggest Elasticsuite index is only %2.
'
+ . 'Click here to go to the Elasticsuite Basic Settings page and change your Number of Shards per Index parameter according to our Wiki page.',
+ $numberOfShards,
+ $maxIndexSize['human_size'],
+ $this->getElasticsuiteConfigUrl(),
+ self::ES_INDICES_SETTINGS_WIKI_PAGE
+ );
+ // @codingStandardsIgnoreEnd
+ }
+
+ // Description when the shard's configuration is optimized.
+ return __('The number of shards is properly configured for the Elasticsearch cluster. No action is required at this time.');
+ }
+
+ /**
+ * Retrieve the status of this health check.
+ *
+ * @return string
+ * @throws Exception
+ */
+ public function getStatus(): string
+ {
+ $numberOfShards = $this->indexSettingsHelper->getNumberOfShards();
+ $maxIndexSize = $this->getMaxIndexSize();
+
+ if ($numberOfShards > 1 && $maxIndexSize && $maxIndexSize['size_in_bytes'] < 10737418240) {
+ return 'warning';
+ }
+
+ return 'success';
+ }
+
+ /**
+ * Retrieve the sort order for this health check.
+ *
+ * @return int
+ */
+ public function getSortOrder(): int
+ {
+ return 30; // Adjust as necessary.
+ }
+
+ /**
+ * Get size of the largest Elasticsuite index.
+ *
+ * @return array|false
+ * @throws Exception
+ */
+ private function getMaxIndexSize()
+ {
+ $elasticsuiteIndices = $this->indexStatsProvider->getElasticSuiteIndices();
+ $indexSizes = [];
+
+ foreach ($elasticsuiteIndices as $indexName => $indexAlias) {
+ $indexData = $this->indexStatsProvider->indexStats($indexName, $indexAlias);
+
+ if (array_key_exists('size', $indexData) && array_key_exists('size_in_bytes', $indexData)
+ && $indexData['size_in_bytes'] !== self::UNDEFINED_SIZE) {
+ $indexSizes[] = ['human_size' => $indexData['size'], 'size_in_bytes' => $indexData['size_in_bytes']];
+ }
+ }
+
+ if (!empty($indexSizes)) {
+ $indexSizesInBytes = array_column($indexSizes, "size_in_bytes");
+ array_multisort($indexSizesInBytes, SORT_DESC, $indexSizes);
+
+ return current($indexSizes);
+ }
+
+ return false;
+ }
+
+ /**
+ * Get URL to the Elasticsuite Configuration page.
+ *
+ * @return string
+ */
+ private function getElasticsuiteConfigUrl(): string
+ {
+ return $this->urlBuilder->getUrl(
+ self::ROUTE_SYSTEM_CONFIG,
+ ['section' => 'smile_elasticsuite_core_base_settings', '_fragment' => self::ANCHOR_ES_INDICES_SETTINGS_PATH]
+ );
+ }
+}
diff --git a/src/module-elasticsuite-core/Model/System/Message/GenericWarningAboutClusterMisconfig.php b/src/module-elasticsuite-core/Model/System/Message/GenericWarningAboutClusterMisconfig.php
new file mode 100644
index 000000000..b3d9bbc7f
--- /dev/null
+++ b/src/module-elasticsuite-core/Model/System/Message/GenericWarningAboutClusterMisconfig.php
@@ -0,0 +1 @@
+
= __('Identifier') ?> | += __('Description') ?> | += __('Status') ?> | +
---|---|---|
= $check->getIdentifier() ?> | += $check->getDescription() ?> | ++ + = $check->getStatus() ?> + + | +