From 65de22aba85c588f614a796c0644fdacf4194763 Mon Sep 17 00:00:00 2001 From: Franck Deroche Date: Fri, 26 Mar 2021 19:16:33 +0100 Subject: [PATCH] Add spellcheck support. --- src/Plugin/GraphQL/Fields/SearchAPISearch.php | 12 + .../GraphQL/Fields/SearchAPISearch.php.orig | 377 ++++++++++++++++++ .../GraphQL/Fields/SearchAPISpellcheck.php | 43 ++ .../Fields/SearchAPISpellcheckSource.php | 31 ++ .../Fields/SearchAPISpellcheckSuggestion.php | 31 ++ .../GraphQL/Types/SearchAPISpellcheck.php | 28 ++ 6 files changed, 522 insertions(+) create mode 100644 src/Plugin/GraphQL/Fields/SearchAPISearch.php.orig create mode 100644 src/Plugin/GraphQL/Fields/SearchAPISpellcheck.php create mode 100644 src/Plugin/GraphQL/Fields/SearchAPISpellcheckSource.php create mode 100644 src/Plugin/GraphQL/Fields/SearchAPISpellcheckSuggestion.php create mode 100644 src/Plugin/GraphQL/Types/SearchAPISpellcheck.php diff --git a/src/Plugin/GraphQL/Fields/SearchAPISearch.php b/src/Plugin/GraphQL/Fields/SearchAPISearch.php index 86eb963..94f1bab 100644 --- a/src/Plugin/GraphQL/Fields/SearchAPISearch.php +++ b/src/Plugin/GraphQL/Fields/SearchAPISearch.php @@ -30,6 +30,7 @@ * "range" = "RangeInput", * "sort" = "[SortInput]", * "facets" = "[FacetInput]", + * "spellcheck" = "Boolean", * "more_like_this" = "MLTInput", * "solr_params" = "[SolrParameterInput]", * }, @@ -319,6 +320,11 @@ private function prepareSearchQuery($args) { if ($args['fulltext']) { $this->setFulltextFields($args['fulltext']); } + if ($args['spellcheck'] && $args['fulltext']) { + $this->query->setOption('search_api_spellcheck', [ + 'keys' => $args['fulltext']['keys'], + ]); + } // Adding range parameters to the query (e.g for pagination). if ($args['range']) { $this->query->range($args['range']['offset'], $args['range']['limit']); @@ -371,6 +377,12 @@ private function getSearchResponse($results) { if ($facets) { $search_response['facets'] = $facets; } + + $spellcheck = $results->getExtraData('search_api_spellcheck'); + if ($spellcheck && !empty($spellcheck['suggestions'])) { + $search_response['spellcheck'] = $spellcheck['suggestions']; + } + return $search_response; } diff --git a/src/Plugin/GraphQL/Fields/SearchAPISearch.php.orig b/src/Plugin/GraphQL/Fields/SearchAPISearch.php.orig new file mode 100644 index 0000000..86eb963 --- /dev/null +++ b/src/Plugin/GraphQL/Fields/SearchAPISearch.php.orig @@ -0,0 +1,377 @@ +entityTypeManager = $entity_type_manager; + $this->logger = $logger; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('entity_type.manager'), + $container->get('logger.factory') + ); + } + + /** + * {@inheritdoc} + */ + public function resolveValues($value, array $args, ResolveContext $context, ResolveInfo $info) { + + // Load up the index passed in argument. + $this->index = $this->entityTypeManager->getStorage('search_api_index')->load($args['index_id']); + + // Prepare the query with our arguments. + $this->prepareSearchQuery($args); + + // Execute search. + try { + $results = $this->query->execute(); + } + // Handle error, check exception type -> SearchApiException ? + catch (\Exception $exception) { + $this->logger->get('graphql_search_api')->error($exception->getMessage()); + } + + // Get search response from results. + $search_response = $this->getSearchResponse($results); + + // Add the result count to the response. + $search_response['result_count'] = $results->getResultCount(); + + // Set response type. + $search_response['type'] = 'SearchAPIResult'; + + yield $search_response; + + } + + /** + * {@inheritdoc} + */ + protected function getCacheDependencies(array $result, $value, array $args, ResolveContext $context, ResolveInfo $info) { + + $metadata = new CacheableMetadata(); + + $metadata->addCacheTags(['search_api_list:' . $this->index->id()]); + + return [$metadata]; + } + + /** + * Adds conditions to the Search API query. + * + * @conditions + * The conditions to be added. + */ + private function addConditions($conditions) { + + // Loop through conditions to add them into the query. + foreach ($conditions as $condition) { + if (empty($condition['operator'])) { + $condition['operator'] = '='; + } + if ($condition['value'] == 'NULL') { + $condition['value'] = NULL; + } + // Set the condition in the query. + $this->query->addCondition($condition['name'], $condition['value'], $condition['operator']); + } + } + + /** + * Adds a condition group to the Search API query. + * + * @condition_group + * The conditions to be added. + */ + private function addConditionGroup($condition_group_arg) { + + // Loop through the groups in the args. + foreach ($condition_group_arg['groups'] as $group) { + + // Set default conjunction and tags. + $group_conjunction = 'AND'; + $group_tags = []; + + // Set conjunction from args. + if (isset($group['conjunction'])) { + + $group_conjunction = $group['conjunction']; + } + if (isset($group['tags'])) { + $group_tags = $group['tags']; + } + + // Create a single condition group. + $condition_group = $this->query->createConditionGroup($group_conjunction, $group_tags); + + // Loop through all conditions and add them to the Group. + foreach ($group['conditions'] as $condition) { + + $condition_group->addCondition($condition['name'], $condition['value'], $condition['operator']); + } + + // Merge the single groups to the condition group. + $this->query->addConditionGroup($condition_group); + } + } + + /** + * Adds direct Solr parameters to the Search API query. + * + * @params + * The conditions to be added. + */ + private function addSolrParams($params) { + + // Loop through conditions to add them into the query. + foreach ($params as $param) { + // Set the condition in the query. + $this->query->setOption('solr_param_' . $param['parameter'], $param['value']); + } + } + + /** + * Sets fulltext fields in the Search API query. + * + * @full_text_params + * Parameters containing fulltext keywords to be used as well as optional + * fields. + */ + private function setFulltextFields($full_text_params) { + + // Check if keys is an array and if so set a conjunction. + if (is_array($full_text_params['keys'])) { + // If no conjunction was specified use OR as default. + if (!empty($full_text_params['conjunction'])) { + $full_text_params['keys']['#conjunction'] = $full_text_params['conjunction']; + } + else { + $full_text_params['keys']['#conjunction'] = 'OR'; + } + } + + // Set the keys in the query. + $this->query->keys($full_text_params['keys']); + + // Set the optional fulltext fields if specified. + if (!empty($full_text_params['fields'])) { + $this->query->setFulltextFields($full_text_params['fields']); + } + } + + /** + * Sets facets in the Search API query. + * + * @facets + * The facets to be added to the query. + */ + private function setFacets($facets) { + + // Retrieve this index server details. + $server = $this->index->getServerInstance(); + + // Check if the index server supports facets (e.g solr). + if ($server->supportsFeature('search_api_facets')) { + + $facets_array = []; + + // Loop through all the provided facets. + foreach ($facets as $facet) { + $facets_array[$facet['field']] = [ + 'field' => $facet['field'], + 'limit' => $facet['limit'], + 'operator' => $facet['operator'], + 'min_count' => $facet['min_count'], + 'missing' => $facet['missing'], + ]; + } + + // Set the facets in the query. + $this->query->setOption('search_api_facets', $facets_array); + } + } + + /** + * Sets MLT in the Search API query. + * + * @mlt_params + * The MLT params to be added to the query. + */ + private function setMlt($mlt_params) { + + // Retrieve this index server details. + $server = $this->index->getServerInstance(); + + // Check if the index server supports More Like This (e.g solr). + if ($server->supportsFeature('search_api_mlt')) { + + // Set the more like this parameters in the query. + $this->query->setOption('search_api_mlt', $mlt_params); + } + } + + /** + * Prepares the Search API query by adding all possible options. + * + * Options include conditions, language, fulltext, range, sort and facets. + * + * @args + * The arguments containing all the parameters to be loaded to the query. + */ + private function prepareSearchQuery($args) { + + // Prepare a query for the respective Search API index. + $this->query = $this->index->query(); + + // Adding query conditions if they exist. + if ($args['conditions']) { + $this->addConditions($args['conditions']); + } + // Adding query group conditions if they exist. + if ($args['condition_group']) { + $this->addConditionGroup($args['condition_group']); + } + // Adding Solr specific parameters if they exist. + if ($args['solr_params']) { + $this->addSolrParams($args['solr_params']); + } + // Restrict the search to specific languages. + if ($args['language']) { + $this->query->setLanguages($args['language']); + } + // Set fulltext search parameters in the query. + if ($args['fulltext']) { + $this->setFulltextFields($args['fulltext']); + } + // Adding range parameters to the query (e.g for pagination). + if ($args['range']) { + $this->query->range($args['range']['offset'], $args['range']['limit']); + } + // Adding sort parameters to the query. + if ($args['sort']) { + foreach ($args['sort'] as $sort) { + $this->query->sort($sort['field'], $sort['value']); + } + } + // Adding facets to the query. + if ($args['facets']) { + $this->setFacets($args['facets']); + } + // Adding more like this parameters to the query. + if ($args['more_like_this']) { + $this->setMlt($args['more_like_this']); + } + } + + /** + * Obtain a Search Response from the results returned by Search API. + * + * @results + * The Search API results to be parsed. + */ + private function getSearchResponse($results) { + + // Obtain result items. + $result_items = $results->getResultItems(); + // Initialise response array. + $search_response = []; + + // Loop through each item in the result set. + foreach ($result_items as &$item) { + // Load the response document into the search response array. + $document['item'] = $item; + $document['index_id'] = $this->index->id(); + $document['type'] = str_replace("_", "", ucwords($this->index->id() . "Doc", '_')); + + // Set the relevance score. + $document['relevance'] = $item->getScore(); + + $search_response['SearchAPIDocument'][] = $document; + } + + // Extract facets from the result set. + $facets = $results->getExtraData('search_api_facets'); + + if ($facets) { + $search_response['facets'] = $facets; + } + return $search_response; + } + +} diff --git a/src/Plugin/GraphQL/Fields/SearchAPISpellcheck.php b/src/Plugin/GraphQL/Fields/SearchAPISpellcheck.php new file mode 100644 index 0000000..42f8f54 --- /dev/null +++ b/src/Plugin/GraphQL/Fields/SearchAPISpellcheck.php @@ -0,0 +1,43 @@ + $suggestions) { + + // Prepare a facet response. + $response_suggestion = NULL; + $response_suggestion['type'] = 'SearchAPISpellcheck'; + $response_suggestion['source'] = $term; + + // Loop through the facet values and load them into the response. + foreach ($suggestions as $suggestion) { + $response_suggestion['suggestion'] = $suggestion; + yield $response_suggestion; + } + } + } + } + +} diff --git a/src/Plugin/GraphQL/Fields/SearchAPISpellcheckSource.php b/src/Plugin/GraphQL/Fields/SearchAPISpellcheckSource.php new file mode 100644 index 0000000..78a699e --- /dev/null +++ b/src/Plugin/GraphQL/Fields/SearchAPISpellcheckSource.php @@ -0,0 +1,31 @@ +