Skip to content
Snippets Groups Projects
Commit fb4b8faf authored by Adam Bramley's avatar Adam Bramley Committed by Kim Pepper
Browse files

Issue #3271337 by acbramley, larowlan: Add native support for n-gram and edge n-gram

parent 9b8557e8
Branches
Tags 1.0.0-alpha9
No related merge requests found
Pipeline #5205 failed
Showing
with 416 additions and 3 deletions
......@@ -4,6 +4,10 @@ services:
class: Drupal\search_api_opensearch\Connector\ConnectorPluginManager
parent: default_plugin_manager
plugin.manager.search_api_opensearch.analyser:
class: Drupal\search_api_opensearch\Analyser\AnalyserManager
parent: default_plugin_manager
logger.channel.search_api_opensearch:
parent: logger.channel_base
arguments: [ 'search_api_opensearch' ]
......@@ -54,3 +58,4 @@ services:
- '@search_api.fields_helper'
- '@search_api_opensearch.field_mapper'
- '@logger.channel.search_api_opensearch'
- '@plugin.manager.search_api_opensearch.analyser'
<?php
declare(strict_types=1);
namespace Drupal\search_api_opensearch\Analyser;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\PluginBase;
/**
* Defines a base class for analyser plugins.
*/
abstract class AnalyserBase extends PluginBase implements AnalyserInterface {
/**
* {@inheritdoc}
*/
public function getLabel(): string {
return $this->getPluginDefinition()['label'];
}
/**
* {@inheritdoc}
*/
public function getSettings(): array {
return [];
}
/**
* {@inheritdoc}
*/
public function getConfiguration(): array {
return $this->configuration;
}
/**
* {@inheritdoc}
*/
public function setConfiguration(array $configuration): self {
$this->configuration = $configuration;
return $this;
}
/**
* {@inheritdoc}
*/
public function defaultConfiguration(): array {
return [];
}
/**
* {@inheritdoc}
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state): array {
return [];
}
/**
* {@inheritdoc}
*/
public function validateConfigurationForm(array &$form, FormStateInterface $form_state): void {
// Nil op.
}
/**
* {@inheritdoc}
*/
public function submitConfigurationForm(array &$form, FormStateInterface $form_state): void {
// Nil op.
}
/**
* {@inheritdoc}
*/
public function getPluginId(): string {
return $this->pluginId;
}
/**
* {@inheritdoc}
*/
public function getPluginDefinition(): array {
return $this->pluginDefinition;
}
}
\ No newline at end of file
<?php
declare(strict_types=1);
namespace Drupal\search_api_opensearch\Analyser;
use Drupal\Component\Plugin\ConfigurableInterface;
use Drupal\Component\Plugin\PluginInspectionInterface;
use Drupal\Core\Plugin\PluginFormInterface;
use OpenSearch\Client;
/**
* Defines an interface for analyser plugins.
*/
interface AnalyserInterface extends PluginFormInterface, ConfigurableInterface, PluginInspectionInterface {
/**
* Gets the analyser label.
*
* @return string
* The label.
*/
public function getLabel(): string;
/**
* Gets the analyser settings.
*
* @return array
* Analyser settings.
*/
public function getSettings(): array;
}
\ No newline at end of file
<?php
declare(strict_types=1);
namespace Drupal\search_api_opensearch\Analyser;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Plugin\DefaultPluginManager;
use Drupal\search_api_opensearch\Annotation\OpenSearchAnalyser;
/**
* Defines a plugin manager for analyser plugins.
*/
final class AnalyserManager extends DefaultPluginManager {
/**
* Constructs a AnalyserManager object.
*
* @param \Traversable $namespaces
* An object that implements \Traversable which contains the root paths
* keyed by the corresponding namespace to look for plugin implementations.
* @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
* Cache backend instance to use.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler to invoke the alter hook with.
*/
public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler) {
$this->alterInfo('opensearch_analyser_info');
$this->setCacheBackend($cache_backend, 'opensearch_analyser_plugins');
parent::__construct('Plugin/OpenSearch/Analyser', $namespaces, $module_handler, AnalyserInterface::class, OpenSearchAnalyser::class);
}
}
\ No newline at end of file
<?php
declare(strict_types=1);
namespace Drupal\search_api_opensearch\Annotation;
use Drupal\Component\Annotation\Plugin;
use Drupal\Core\Annotation\Translation;
/**
* Defines an annotation for open search analyser plugins.
*
* @Annotation
*/
final class OpenSearchAnalyser extends Plugin {
/**
* Plugin ID.
*/
public string $id;
/**
* Plugin label.
*/
public string|Translation $label;
}
\ No newline at end of file
<?php
declare(strict_types=1);
namespace Drupal\search_api_opensearch\Plugin\OpenSearch\Analyser;
use Drupal\search_api_opensearch\Analyser\AnalyserBase;
use Drupal\search_api_opensearch\Annotation\OpenSearchAnalyser;
/**
* Defines an Edge N-gram analyser.
*
* @OpenSearchAnalyser(
* id = \Drupal\search_api_opensearch\Plugin\OpenSearch\Analyser\EdgeNgram::PLUGIN_ID,
* label = @Translation("Edge N-gram analyzer"),
* )
*/
final class EdgeNgram extends AnalyserBase {
/**
* The plugin ID.
*/
public const PLUGIN_ID = 'edge_ngram_analyzer';
/**
* The filter ID.
*/
public const FILTER_ID = 'edge_ngram_filter';
/**
* {@inheritdoc}
*/
public function getSettings(): array {
return [
'analysis' => [
'filter' => [
self::FILTER_ID => [
'type' => 'edge_ngram',
'min_gram' => 1,
'max_gram' => 20,
],
],
'analyzer' => [
self::PLUGIN_ID => [
'type' => 'custom',
'tokenizer' => 'standard',
'filter' => ['lowercase', 'asciifolding', self::FILTER_ID],
],
],
],
];
}
}
<?php
declare(strict_types=1);
namespace Drupal\search_api_opensearch\Plugin\OpenSearch\Analyser;
use Drupal\search_api_opensearch\Analyser\AnalyserBase;
use Drupal\search_api_opensearch\Annotation\OpenSearchAnalyser;
/**
* Defines an N-gram analyser.
*
* @OpenSearchAnalyser(
* id = \Drupal\search_api_opensearch\Plugin\OpenSearch\Analyser\Ngram::PLUGIN_ID,
* label = @Translation("N-gram analyzer"),
* )
*/
final class Ngram extends AnalyserBase {
/**
* The plugin ID.
*/
public const PLUGIN_ID = 'ngram_analyzer';
/**
* The filter ID.
*/
public const FILTER_ID = 'ngram_filter';
/**
* {@inheritdoc}
*/
public function getSettings(): array {
return [
'analysis' => [
'filter' => [
self::FILTER_ID => [
'type' => 'ngram',
'min_gram' => 1,
'max_gram' => 20,
],
],
'analyzer' => [
self::PLUGIN_ID => [
'type' => 'custom',
'tokenizer' => 'standard',
'filter' => ['lowercase', 'asciifolding', self::FILTER_ID],
],
],
],
];
}
}
......@@ -445,4 +445,11 @@ class OpenSearchBackend extends BackendPluginBase implements PluginFormInterface
return $vars;
}
/**
* {@inheritdoc}
*/
public function supportsDataType($type) {
return parent::supportsDataType($type) || str_starts_with($type, 'search_api_opensearch_');
}
}
<?php
declare(strict_types=1);
namespace Drupal\search_api_opensearch\Plugin\search_api\data_type;
use Drupal\search_api\Plugin\search_api\data_type\TextDataType;
/**
* Defines a class for n-gram data type.
*
* @SearchApiDataType(
* id = "search_api_opensearch_edge_ngram",
* label = @Translation("Edge N-gram"),
* description = @Translation("Edge ngram"),
* fallback_type = "text",
* )
*/
final class EdgeNgramDataType extends TextDataType {
}
<?php
declare(strict_types=1);
namespace Drupal\search_api_opensearch\Plugin\search_api\data_type;
use Drupal\search_api\Plugin\search_api\data_type\TextDataType;
/**
* Defines a class for n-gram data type.
*
* @SearchApiDataType(
* id = "search_api_opensearch_ngram",
* label = @Translation("N-gram"),
* description = @Translation("ngram"),
* fallback_type = "text",
* )
*/
final class NgramDataType extends TextDataType {
}
\ No newline at end of file
......@@ -2,6 +2,7 @@
namespace Drupal\search_api_opensearch\SearchAPI;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\DependencyInjection\DependencySerializationTrait;
use Drupal\Core\Utility\Error;
use Drupal\search_api\IndexInterface;
......@@ -9,6 +10,8 @@ use Drupal\search_api\Query\QueryInterface;
use Drupal\search_api\Query\ResultSetInterface;
use Drupal\search_api\SearchApiException;
use Drupal\search_api\Utility\FieldsHelperInterface;
use Drupal\search_api_opensearch\Analyser\AnalyserInterface;
use Drupal\search_api_opensearch\Analyser\AnalyserManager;
use Drupal\search_api_opensearch\SearchAPI\Query\QueryParamBuilder;
use Drupal\search_api_opensearch\SearchAPI\Query\QueryResultParser;
use OpenSearch\Client;
......@@ -72,6 +75,11 @@ class BackendClient implements BackendClientInterface {
*/
protected $logger;
/**
* Client settings.
*/
protected array $settings = [];
/**
* Constructs a new BackendClient.
*
......@@ -91,8 +99,10 @@ class BackendClient implements BackendClientInterface {
* The OpenSearch client.
* @param array $settings
* The settings.
* @param \Drupal\search_api_opensearch\Analyser\AnalyserManager $analyserManager
* Analyser manager.
*/
public function __construct(QueryParamBuilder $queryParamBuilder, QueryResultParser $resultParser, IndexParamBuilder $indexParamBuilder, FieldsHelperInterface $fieldsHelper, FieldMapper $fieldParamsBuilder, LoggerInterface $logger, Client $client, array $settings) {
public function __construct(QueryParamBuilder $queryParamBuilder, QueryResultParser $resultParser, IndexParamBuilder $indexParamBuilder, FieldsHelperInterface $fieldsHelper, FieldMapper $fieldParamsBuilder, LoggerInterface $logger, Client $client, array $settings, protected AnalyserManager $analyserManager) {
$this->indexParamBuilder = $indexParamBuilder;
$this->queryParamBuilder = $queryParamBuilder;
$this->resultParser = $resultParser;
......@@ -246,6 +256,7 @@ class BackendClient implements BackendClientInterface {
$this->client->indices()->create([
'index' => $indexId,
]);
$this->updateSettings($index);
$this->updateFieldMapping($index);
}
catch (OpenSearchException $e) {
......@@ -334,4 +345,46 @@ class BackendClient implements BackendClientInterface {
return $vars;
}
/**
* Updates index settings.
*
* @param \Drupal\search_api\IndexInterface $index_param
* Index.
*/
public function updateSettings(IndexInterface $index_param): void {
$indexId = $this->getIndexId($index_param);
$params = $this->fieldParamsBuilder->mapFieldParams($indexId, $index_param);
$analysers = array_reduce($params['body']['properties'], function (array $carry, array $field_definition) {
if (isset($field_definition['analyzer'])) {
$carry[$field_definition['analyzer']] = $field_definition['analyzer_settings'] ?? [];
}
return $carry;
}, []);
$settings = [];
foreach ($analysers as $analyser_id => $configuration) {
$analyser = $this->analyserManager->createInstance($analyser_id, $configuration);
assert($analyser instanceof AnalyserInterface);
$settings = NestedArray::mergeDeep($settings, $analyser->getSettings());
}
if (!$settings) {
// Nothing to push.
return;
}
try {
$index_param = [
'index' => $indexId,
];
$this->client->indices()->close($index_param);
$this->client->indices()->putSettings($index_param + [
'body' => $settings,
]);
}
catch (OpenSearchException $e) {
throw new SearchApiException(sprintf('An error occurred updating settings for index %s.', $indexId), 0, $e);
}
finally {
$this->client->indices()->open($index_param);
}
}
}
......@@ -2,6 +2,7 @@
namespace Drupal\search_api_opensearch\SearchAPI;
use Drupal\search_api_opensearch\Analyser\AnalyserManager;
use Drupal\search_api_opensearch\SearchAPI\Query\QueryParamBuilder;
use Drupal\search_api_opensearch\SearchAPI\Query\QueryResultParser;
use Drupal\search_api\Utility\FieldsHelperInterface;
......@@ -73,8 +74,10 @@ class BackendClientFactory {
* The field mapper.
* @param \Psr\Log\LoggerInterface $logger
* The logger.
* @param \Drupal\search_api_opensearch\Analyser\AnalyserManager $analyserManager
* Analyser manager.
*/
public function __construct(QueryParamBuilder $queryParamBuilder, QueryResultParser $resultParser, IndexParamBuilder $itemParamBuilder, FieldsHelperInterface $fieldsHelper, FieldMapper $fieldParamsBuilder, LoggerInterface $logger) {
public function __construct(QueryParamBuilder $queryParamBuilder, QueryResultParser $resultParser, IndexParamBuilder $itemParamBuilder, FieldsHelperInterface $fieldsHelper, FieldMapper $fieldParamsBuilder, LoggerInterface $logger, protected AnalyserManager $analyserManager) {
$this->itemParamBuilder = $itemParamBuilder;
$this->queryParamBuilder = $queryParamBuilder;
$this->resultParser = $resultParser;
......@@ -104,6 +107,7 @@ class BackendClientFactory {
$this->logger,
$client,
$settings,
$this->analyserManager,
);
}
......
......@@ -6,6 +6,8 @@ use Drupal\search_api\IndexInterface;
use Drupal\search_api\Item\FieldInterface;
use Drupal\search_api\Utility\FieldsHelperInterface;
use Drupal\search_api_opensearch\Event\FieldMappingEvent;
use Drupal\search_api_opensearch\Plugin\OpenSearch\Analyser\EdgeNgram;
use Drupal\search_api_opensearch\Plugin\OpenSearch\Analyser\Ngram;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
/**
......@@ -136,6 +138,18 @@ class FieldMapper {
],
],
],
'search_api_opensearch_ngram' => [
'type' => 'text',
'index' => TRUE,
'boost' => $field->getBoost(),
'analyzer' => Ngram::PLUGIN_ID,
],
'search_api_opensearch_edge_ngram' => [
'type' => 'text',
'index' => TRUE,
'boost' => $field->getBoost(),
'analyzer' => EdgeNgram::PLUGIN_ID,
],
'uri', 'string', 'token' => ['type' => 'keyword'],
'integer', 'duration' => ['type' => 'integer'],
'boolean' => ['type' => 'boolean'],
......
......@@ -116,7 +116,7 @@ class IndexParamBuilder {
foreach ($field->getValues() as $value) {
$values[] = match ($field_type) {
'string' => (string) $value,
'text' => $value->toText(),
'text', 'search_api_opensearch_ngram', 'search_api_opensearch_edge_ngram' => $value->toText(),
'boolean' => (boolean) $value,
default => $value,
};
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment