From 4cb0d58296a16af9711dd6506a80538755295ae3 Mon Sep 17 00:00:00 2001 From: Chris Penny Date: Tue, 29 Oct 2024 15:01:27 +1300 Subject: [PATCH] Discoverer v2 support. Spelling suggestions feature --- .editorconfig | 2 +- _config/adaptors.yml | 26 +++++++++ _config/config.yml | 29 ---------- _config/factory.yml | 12 ++++ _config/requests.yml | 10 ++++ src/Processors/SuggestionParamsProcessor.php | 33 +++++++++++ src/Processors/SuggestionsProcessor.php | 57 +++++++++++++++++++ .../Adaptors/QuerySuggestionAdaptor.php | 53 +++++++++++++++++ .../Adaptors/SpellingSuggestionAdaptor.php | 53 +++++++++++++++++ src/Service/Requests/ClickPost.php | 4 +- .../Requests/Params/SuggestionParams.php | 18 ++++++ src/Service/Requests/QuerySuggestion.php | 16 ++++-- src/Service/Requests/Search.php | 4 +- src/Service/Requests/SpellingSuggestion.php | 22 +++++++ 14 files changed, 299 insertions(+), 40 deletions(-) create mode 100644 _config/adaptors.yml delete mode 100644 _config/config.yml create mode 100644 _config/factory.yml create mode 100644 _config/requests.yml create mode 100644 src/Processors/SuggestionParamsProcessor.php create mode 100644 src/Processors/SuggestionsProcessor.php create mode 100644 src/Service/Adaptors/QuerySuggestionAdaptor.php create mode 100644 src/Service/Adaptors/SpellingSuggestionAdaptor.php create mode 100644 src/Service/Requests/Params/SuggestionParams.php create mode 100644 src/Service/Requests/SpellingSuggestion.php diff --git a/.editorconfig b/.editorconfig index 9d24928..0741b16 100644 --- a/.editorconfig +++ b/.editorconfig @@ -10,7 +10,7 @@ indent_style = space insert_final_newline = true trim_trailing_whitespace = true -[{*.yml}] +[*.yml] indent_size = 2 indent_style = space diff --git a/_config/adaptors.yml b/_config/adaptors.yml new file mode 100644 index 0000000..5488b96 --- /dev/null +++ b/_config/adaptors.yml @@ -0,0 +1,26 @@ +--- +Name: discoverer-bifrost-adaptors +After: discoverer-adaptors +Only: + envvarset: 'BIFROST_QUERY_API_KEY' +--- +SilverStripe\Core\Injector\Injector: + # Adaptors provided by this module + SilverStripe\Discoverer\Service\Interfaces\QuerySuggestionAdaptor: + class: SilverStripe\DiscovererBifrost\Service\Adaptors\QuerySuggestionAdaptor + SilverStripe\Discoverer\Service\Interfaces\SpellingSuggestionAdaptor: + class: SilverStripe\DiscovererBifrost\Service\Adaptors\SpellingSuggestionAdaptor + # Adaptors provided by the ElasticEnterprise dependency + SilverStripe\Discoverer\Query\Facet\FacetAdaptor: + class: SilverStripe\DiscovererElasticEnterprise\Query\Facet\FacetAdaptor + SilverStripe\Discoverer\Query\Filter\CriteriaAdaptor: + class: SilverStripe\DiscovererElasticEnterprise\Query\Filter\CriteriaAdaptor + SilverStripe\Discoverer\Query\Filter\CriterionAdaptor: + class: SilverStripe\DiscovererElasticEnterprise\Query\Filter\CriterionAdaptor + SilverStripe\Discoverer\Service\Interfaces\ProcessAnalyticsAdaptor: + class: SilverStripe\DiscovererElasticEnterprise\Service\Adaptors\ProcessAnalyticsAdaptor + SilverStripe\Discoverer\Service\Interfaces\SearchAdaptor: + class: SilverStripe\DiscovererElasticEnterprise\Service\Adaptors\SearchAdaptor + +SilverStripe\DiscovererElasticEnterprise\Service\Adaptors\BaseAdaptor: + prefix_env_var: 'BIFROST_ENGINE_PREFIX' diff --git a/_config/config.yml b/_config/config.yml deleted file mode 100644 index 162f49b..0000000 --- a/_config/config.yml +++ /dev/null @@ -1,29 +0,0 @@ ---- -Name: discoverer-bifrost -Only: - envvarset: 'BIFROST_QUERY_API_KEY' ---- -SilverStripe\Core\Injector\Injector: - SilverStripe\Discoverer\Query\Facet\FacetAdaptor: - class: SilverStripe\DiscovererElasticEnterprise\Query\Facet\FacetAdaptor - SilverStripe\Discoverer\Query\Filter\CriteriaAdaptor: - class: SilverStripe\DiscovererElasticEnterprise\Query\Filter\CriteriaAdaptor - SilverStripe\Discoverer\Query\Filter\CriterionAdaptor: - class: SilverStripe\DiscovererElasticEnterprise\Query\Filter\CriterionAdaptor - SilverStripe\Discoverer\Service\SearchServiceAdaptor: - class: SilverStripe\DiscovererElasticEnterprise\Service\SearchServiceAdaptor - Elastic\EnterpriseSearch\Client.searchClient: - factory: SilverStripe\DiscovererBifrost\Service\ClientFactory - constructor: - host: '`BIFROST_ENDPOINT`' - token: '`BIFROST_QUERY_API_KEY`' - http_client: '%$GuzzleHttp\Client' - Elastic\EnterpriseSearch\AppSearch\Request\Search: - class: SilverStripe\DiscovererBifrost\Service\Requests\Search - Elastic\EnterpriseSearch\AppSearch\Request\LogClickthrough: - class: SilverStripe\DiscovererBifrost\Service\Requests\ClickPost - Elastic\EnterpriseSearch\AppSearch\Request\QuerySuggestion: - class: SilverStripe\DiscovererBifrost\Service\Requests\QuerySuggestion - -SilverStripe\DiscovererElasticEnterprise\Service\SearchServiceAdaptor: - prefix_env_var: 'BIFROST_ENGINE_PREFIX' diff --git a/_config/factory.yml b/_config/factory.yml new file mode 100644 index 0000000..5020887 --- /dev/null +++ b/_config/factory.yml @@ -0,0 +1,12 @@ +--- +Name: discoverer-bifrost-factory +Only: + envvarset: 'BIFROST_QUERY_API_KEY' +--- +SilverStripe\Core\Injector\Injector: + Elastic\EnterpriseSearch\Client.searchClient: + factory: SilverStripe\DiscovererBifrost\Service\ClientFactory + constructor: + host: '`BIFROST_ENDPOINT`' + token: '`BIFROST_QUERY_API_KEY`' + http_client: '%$GuzzleHttp\Client' diff --git a/_config/requests.yml b/_config/requests.yml new file mode 100644 index 0000000..916cd51 --- /dev/null +++ b/_config/requests.yml @@ -0,0 +1,10 @@ +--- +Name: discoverer-bifrost-requests +Only: + envvarset: 'BIFROST_QUERY_API_KEY' +--- +SilverStripe\Core\Injector\Injector: + Elastic\EnterpriseSearch\AppSearch\Request\Search: + class: SilverStripe\DiscovererBifrost\Service\Requests\Search + Elastic\EnterpriseSearch\AppSearch\Request\LogClickthrough: + class: SilverStripe\DiscovererBifrost\Service\Requests\ClickPost diff --git a/src/Processors/SuggestionParamsProcessor.php b/src/Processors/SuggestionParamsProcessor.php new file mode 100644 index 0000000..202b5f8 --- /dev/null +++ b/src/Processors/SuggestionParamsProcessor.php @@ -0,0 +1,33 @@ +query = $suggestion->getQueryString(); + + $limit = $suggestion->getLimit(); + $fields = $suggestion->getFields(); + + if ($limit) { + $suggestionParams->size = $limit; + } + + if ($fields) { + $suggestionParams->fields = $fields; + } + + return $suggestionParams; + } + +} diff --git a/src/Processors/SuggestionsProcessor.php b/src/Processors/SuggestionsProcessor.php new file mode 100644 index 0000000..aaff78f --- /dev/null +++ b/src/Processors/SuggestionsProcessor.php @@ -0,0 +1,57 @@ +validateResponse($response); + + $suggestions->setSuggestions($response['results'] ?? []); + } + + private function validateResponse(array $response): void + { + // If any errors are present, then let's throw and track what they were + if (array_key_exists('errors', $response)) { + throw new Exception(sprintf('Elastic response contained errors: %s', json_encode($response['errors']))); + } + + // The top level fields that we expect to receive from Elastic for each search + $meta = $response['meta'] ?? null; + $results = $response['results'] ?? null; + // Check if any required fields are missing + $missingTopLevelFields = []; + + // Basic falsy check is fine here. An empty `meta` would still be an error + if (!$meta) { + $missingTopLevelFields[] = 'meta'; + } + + // Specifically checking is_array(), because an empty results array is a valid response + if (!is_array($results)) { + $missingTopLevelFields[] = 'results'; + } + + // We were missing one or more required top level fields + if ($missingTopLevelFields) { + throw new Exception(sprintf( + 'Missing required top level fields for query suggestions: %s', + implode(', ', $missingTopLevelFields) + )); + } + } + +} diff --git a/src/Service/Adaptors/QuerySuggestionAdaptor.php b/src/Service/Adaptors/QuerySuggestionAdaptor.php new file mode 100644 index 0000000..c51de56 --- /dev/null +++ b/src/Service/Adaptors/QuerySuggestionAdaptor.php @@ -0,0 +1,53 @@ +environmentizeIndex($indexName); + $params = SuggestionParamsProcessor::singleton()->getQueryParams($suggestion); + $request = QuerySuggestion::create($engine, $params); + + $transportResponse = $this->getClient()->appSearch()->getTransport()->sendRequest($request->getRequest()); + $response = Injector::inst()->create(Response::class, $transportResponse); + + SuggestionsProcessor::singleton()->getProcessedSuggestions($suggestions, $response->asArray()); + // If we got this far, then the request was a success + $suggestions->setSuccess(true); + } catch (ClientErrorResponseException $e) { + $errors = (string)$e->getResponse()->getBody(); + // Log the error without breaking the page + $this->getLogger()->error(sprintf('Bifrost error: %s', $errors), ['bifrost' => $e]); + // Our request was not a success + $suggestions->setSuccess(false); + } catch (Throwable $e) { + // Log the error without breaking the page + $this->getLogger()->error(sprintf('Bifrost error: %s', $e->getMessage()), ['bifrost' => $e]); + // Our request was not a success + $suggestions->setSuccess(false); + } finally { + return $suggestions; + } + } + +} diff --git a/src/Service/Adaptors/SpellingSuggestionAdaptor.php b/src/Service/Adaptors/SpellingSuggestionAdaptor.php new file mode 100644 index 0000000..537b4de --- /dev/null +++ b/src/Service/Adaptors/SpellingSuggestionAdaptor.php @@ -0,0 +1,53 @@ +environmentizeIndex($indexName); + $params = SuggestionParamsProcessor::singleton()->getQueryParams($suggestion); + $request = SpellingSuggestion::create($engine, $params); + + $transportResponse = $this->getClient()->appSearch()->getTransport()->sendRequest($request->getRequest()); + $response = Injector::inst()->create(Response::class, $transportResponse); + + SuggestionsProcessor::singleton()->getProcessedSuggestions($suggestions, $response->asArray()); + // If we got this far, then the request was a success + $suggestions->setSuccess(true); + } catch (ClientErrorResponseException $e) { + $errors = (string)$e->getResponse()->getBody(); + // Log the error without breaking the page + $this->getLogger()->error(sprintf('Bifrost error: %s', $errors), ['bifrost' => $e]); + // Our request was not a success + $suggestions->setSuccess(false); + } catch (Throwable $e) { + // Log the error without breaking the page + $this->getLogger()->error(sprintf('Bifrost error: %s', $e->getMessage()), ['bifrost' => $e]); + // Our request was not a success + $suggestions->setSuccess(false); + } finally { + return $suggestions; + } + } + +} diff --git a/src/Service/Requests/ClickPost.php b/src/Service/Requests/ClickPost.php index 5c8fbe8..7b11c4f 100644 --- a/src/Service/Requests/ClickPost.php +++ b/src/Service/Requests/ClickPost.php @@ -8,9 +8,9 @@ class ClickPost extends AppSearchLogClickthrough { - public function __construct(string $engineName, ?ClickParams $click_params = null) + public function __construct(string $engineName, ?ClickParams $params = null) { - parent::__construct($engineName, $click_params); + parent::__construct($engineName, $params); $this->path = sprintf('/api/v1/%s/click', $engineName); } diff --git a/src/Service/Requests/Params/SuggestionParams.php b/src/Service/Requests/Params/SuggestionParams.php new file mode 100644 index 0000000..32a9761 --- /dev/null +++ b/src/Service/Requests/Params/SuggestionParams.php @@ -0,0 +1,18 @@ +method = 'POST'; $this->path = sprintf('/api/v1/%s/query_suggestion', $engineName); + $this->headers['Content-Type'] = 'application/json'; + $this->body = $params; } } diff --git a/src/Service/Requests/Search.php b/src/Service/Requests/Search.php index cf5dcfc..a36362b 100644 --- a/src/Service/Requests/Search.php +++ b/src/Service/Requests/Search.php @@ -8,9 +8,9 @@ class Search extends AppSearchSearch { - public function __construct(string $engineName, ?SearchRequestParams $search_request_params = null) + public function __construct(string $engineName, ?SearchRequestParams $params = null) { - parent::__construct($engineName, $search_request_params); + parent::__construct($engineName, $params); $this->path = sprintf('/api/v1/%s/search', $engineName); } diff --git a/src/Service/Requests/SpellingSuggestion.php b/src/Service/Requests/SpellingSuggestion.php new file mode 100644 index 0000000..23bf38d --- /dev/null +++ b/src/Service/Requests/SpellingSuggestion.php @@ -0,0 +1,22 @@ +method = 'POST'; + $this->path = sprintf('/api/v1/%s/spelling_suggestion', $engineName); + $this->headers['Content-Type'] = 'application/json'; + $this->body = $params; + } + +}