search_api_meilisearch-1.0.x-dev/src/Plugin/search_api/processor/Synonyms.php
src/Plugin/search_api/processor/Synonyms.php
<?php
namespace Drupal\search_api_meilisearch\Plugin\search_api\processor;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\PluginFormInterface;
use Drupal\search_api\IndexInterface;
use Drupal\search_api\Processor\FieldsProcessorPluginBase;
use Drupal\search_api\SearchApiException;
use Drupal\search_api_meilisearch\Api\MeilisearchApiException;
use Drupal\search_api_meilisearch\Api\MeilisearchApiServiceInterface;
use Drupal\search_api_meilisearch\Plugin\search_api\backend\SearchApiMeilisearchBackend;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Add synonyms to the Meilisearch index.
*
* @SearchApiProcessor(
* id = "search_api_meilisearch_synonyms",
* label = @Translation("Synonyms"),
* description = @Translation("If multiples words have an equivalent meaning in your dataset, you can decide to create a synonym list for these words. "),
* stages = {
* "preprocess_index" = 0,
* },
* locked = false,
* hidden = false,
* )
*/
final class Synonyms extends FieldsProcessorPluginBase implements PluginFormInterface {
/**
* Delimiter that separates synonyms in a group.
*
* @var string
*/
const SYNONYM_DELIMITER = ',';
/**
* Delimiter that determines the synonym relationship.
*
* @var string
*/
const GROUP_DELIMITER = ':';
/**
* The logger to use for logging messages.
*
* @var \Psr\Log\LoggerInterface
*/
protected LoggerInterface $logger;
/**
* The config factory service.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected ConfigFactoryInterface $configFactory;
/**
* Meilisearch service for making API calls.
*
* @var \Drupal\search_api_meilisearch\Api\MeilisearchApiServiceInterface
*/
protected MeilisearchApiServiceInterface $meiliService;
/**
* The construct method for the Meilisearch back-end plugin.
*
* @param array $configuration
* Configuration array.
* @param string $plugin_id
* The plugin id.
* @param mixed $plugin_definition
* A plugin definition.
* @param \Drupal\search_api_meilisearch\Api\MeilisearchApiServiceInterface $meiliService
* A MeilisearchApiService instance.
* @param \Drupal\Core\Config\ConfigFactoryInterface $configFactory
* A ConfigFactory instance.
* @param \Psr\Log\LoggerInterface $logger
* A LoggerInterface instance.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, MeilisearchApiServiceInterface $meiliService, ConfigFactoryInterface $configFactory, LoggerInterface $logger) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->meiliService = $meiliService;
$this->configFactory = $configFactory;
$this->logger = $logger;
$this->setConnection();
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): self {
return new self(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('search_api_meilisearch.api'),
$container->get('config.factory'),
$container->get('logger.channel.search_api_meilisearch')
);
}
/**
* Sets the connection information in the service.
*/
protected function setConnection() {
try {
$server = $this->getIndex()->getServerInstance();
if (isset($server)) {
$config = $server->getBackendConfig();
if (isset($config['meilisearch_host_address']) && $config['meilisearch_host_port']) {
$connectionUrl = $config['meilisearch_host_address'] . ':' . $config['meilisearch_host_port'];
$this->meiliService->setUrl($connectionUrl);
if (isset($config['meilisearch_master_key'])) {
$masterKey = $config['meilisearch_master_key'];
$this->meiliService->setMasterKey($masterKey);
}
}
}
}
catch (SearchApiException $e) {
return;
}
}
/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
$configuration = parent::defaultConfiguration();
$configuration['synonyms'] = [];
unset($configuration['all_fields']);
unset($configuration['fields']);
return $configuration;
}
/**
* {@inheritdoc}
*/
public static function supportsIndex(IndexInterface $index): bool {
try {
$server = $index->getServerInstance();
return $server && $server->getBackend() instanceof SearchApiMeilisearchBackend;
}
catch (SearchApiException $e) {
return FALSE;
}
}
/**
* {@inheritdoc}
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state): array {
$synonymsConfig = $this->configuration['synonyms'];
$value = '';
/**
* @var string $phrase
* @var string[] $synonyms
*/
foreach ($synonymsConfig as $phrase => $synonyms) {
if (empty(trim($phrase)) || empty($synonyms)) {
continue;
}
$value .= $phrase . self::GROUP_DELIMITER . ' ' . implode(self::SYNONYM_DELIMITER . ' ', $synonyms) . "\n";
}
$form['synonyms'] = [
'#type' => 'textarea',
'#title' => $this->t('Synonyms'),
'#description' => $this->t('List all the synonyms for the word after the colon and separate them with a comma. <br>Example: <br>beautiful: attractive, pretty <br>attractive: beautiful, pretty<br>united states: us'),
'#default_value' => $value,
];
return $form;
}
/**
* {@inheritdoc}
*/
public function validateConfigurationForm(array &$form, FormStateInterface $form_state) {
try {
$this->meiliService->connection();
}
catch (MeilisearchApiException $e) {
$this->logger->error($e->getMessage());
switch ($e->getCode()) {
case(403):
$form_state->setErrorByName('server', $this->t('The master key is not correct.'));
break;
case(0):
$form_state->setErrorByName('server', $this->t('The connection to the server could not be established.'));
break;
}
}
$separateSynonyms = $this->cleanSynonyms($form_state->getValue('synonyms'));
foreach ($separateSynonyms as $synonym) {
$match = [];
$matches = preg_match('/[^.\s\W](?=\S)[a-zA-Z0-9 ]*[^.\s\W]\s?:\s?[a-zA-Z0-9 ,]*[^\W]/', $synonym, $match);
if (empty($match) || $matches === 0) {
$form_state->setErrorByName($synonym, $this->t('The format of "@synonym" synonym is not valid.', ['@synonym' => $synonym]));
}
}
}
/**
* {@inheritdoc}
*/
public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
$synonyms = $this->prepareSynonyms($form_state->getValue('synonyms'));
$indexName = $this->getIndex()->id();
try {
if (empty($synonyms)) {
$resetSynonyms = $this->meiliService->resetSynonyms($indexName);
$task = $this->meiliService->waitForUpdate($resetSynonyms['taskUid']);
if ($task['status'] === 'failed') {
throw new MeilisearchApiException($task['error']['message']);
}
$this->messenger()->addStatus($this->t('Synonyms were successfully deleted.'));
}
else {
$updateSynonyms = $this->meiliService->updateSynonyms($indexName, $synonyms);
$task = $this->meiliService->waitForUpdate($updateSynonyms['taskUid']);
if ($task['status'] === 'failed') {
throw new MeilisearchApiException($task['error']['message']);
}
$this->messenger()->addStatus($this->t('Synonyms were successfully updated.'));
}
$form_state->setValue('synonyms', $synonyms);
parent::submitConfigurationForm($form, $form_state);
}
catch (MeilisearchApiException $e) {
$this->logger->warning($e->getMessage());
$this->messenger()->addWarning($this->t('@message', ['@message' => $e->getMessage()]));
}
}
/**
* Processes the synonyms list and prepares it for the Meilisearch.
*
* Synonyms need to be transformed from a string of format e.g.
* beautiful:attractive, pretty
* hardworking:diligent
* to an associate array of unique words as keys and synonyms as values.
*
* @param string $synonymsString
* User's input of synonyms.
*
* @return array
* Processed synonyms from the users input.
*/
private function prepareSynonyms(string $synonymsString): array {
$synonyms = [];
$separateSynonyms = $this->cleanSynonyms($synonymsString);
foreach ($separateSynonyms as $synonym) {
[$synonymGroupName, $synonymGroupValues] = explode(self::GROUP_DELIMITER, $synonym);
$synonymValues = explode(self::SYNONYM_DELIMITER, $synonymGroupValues);
$synonymValues = array_map('trim', $synonymValues);
$synonyms[strtolower($synonymGroupName)] = array_map('strtolower', $synonymValues);
}
return $synonyms;
}
/**
* Removes empty lines and whitespaces from the synonyms string.
*
* @param string $synonymsString
* User's input of synonyms.
*
* @return array
* Cleaned synonyms array.
*/
private function cleanSynonyms(string $synonymsString): array {
$separateSynonyms = preg_split('/\r\n|\r|\n/', $synonymsString);
$separateSynonyms = array_map('trim', $separateSynonyms);
return array_filter($separateSynonyms, 'strlen');
}
}
