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/composer.json b/composer.json index 9112a44..a973e2a 100644 --- a/composer.json +++ b/composer.json @@ -23,7 +23,7 @@ "require": { "php": "^8.1", "silverstripe/framework": "^5", - "silverstripe/silverstripe-discoverer-elastic-enterprise": "^1.1", + "silverstripe/silverstripe-discoverer-elastic-enterprise": "^2", "guzzlehttp/guzzle": "^7.5" }, "require-dev": { diff --git a/src/Processors/SuggestionParamsProcessor.php b/src/Processors/SuggestionParamsProcessor.php new file mode 100644 index 0000000..0a61d30 --- /dev/null +++ b/src/Processors/SuggestionParamsProcessor.php @@ -0,0 +1,34 @@ +query = $suggestion->getQueryString(); + $suggestionParams->formatted = $suggestion->isFormatted(); + + $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..c2dfae5 --- /dev/null +++ b/src/Processors/SuggestionsProcessor.php @@ -0,0 +1,49 @@ +validateResponse($response); + + $results = $response['results'] ?? []; + + foreach ($results as $result) { + $suggestions->addSuggestion(Field::create( + $result['raw'] ?? null, + $result['snippet'] ?? null, + )); + } + } + + 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 + $results = $response['results'] ?? null; + + // Specifically checking is_array(), because an empty results array is a valid response + if (!is_array($results)) { + throw new Exception('Missing required top level fields for query suggestions: results'); + } + } + +} diff --git a/src/Service/Adaptors/QuerySuggestionAdaptor.php b/src/Service/Adaptors/QuerySuggestionAdaptor.php new file mode 100644 index 0000000..54c247f --- /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..b32e8b1 --- /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..d60605c --- /dev/null +++ b/src/Service/Requests/Params/SuggestionParams.php @@ -0,0 +1,26 @@ +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; + } + +}