Skip to content

Commit

Permalink
Discoverer v2 support. Spelling suggestions feature
Browse files Browse the repository at this point in the history
  • Loading branch information
chrispenny committed Oct 31, 2024
1 parent c9b9f5c commit 4cb0d58
Show file tree
Hide file tree
Showing 14 changed files with 299 additions and 40 deletions.
2 changes: 1 addition & 1 deletion .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true

[{*.yml}]
[*.yml]
indent_size = 2
indent_style = space

Expand Down
26 changes: 26 additions & 0 deletions _config/adaptors.yml
Original file line number Diff line number Diff line change
@@ -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'
29 changes: 0 additions & 29 deletions _config/config.yml

This file was deleted.

12 changes: 12 additions & 0 deletions _config/factory.yml
Original file line number Diff line number Diff line change
@@ -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'
10 changes: 10 additions & 0 deletions _config/requests.yml
Original file line number Diff line number Diff line change
@@ -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
33 changes: 33 additions & 0 deletions src/Processors/SuggestionParamsProcessor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

namespace SilverStripe\DiscovererBifrost\Processors;

use SilverStripe\Core\Injector\Injectable;
use SilverStripe\Discoverer\Query\Suggestion;
use SilverStripe\DiscovererBifrost\Service\Requests\Params\SuggestionParams;

class SuggestionParamsProcessor
{

use Injectable;

public function getQueryParams(Suggestion $suggestion): SuggestionParams
{
$suggestionParams = SuggestionParams::create();
$suggestionParams->query = $suggestion->getQueryString();

$limit = $suggestion->getLimit();
$fields = $suggestion->getFields();

if ($limit) {
$suggestionParams->size = $limit;
}

if ($fields) {
$suggestionParams->fields = $fields;
}

return $suggestionParams;
}

}
57 changes: 57 additions & 0 deletions src/Processors/SuggestionsProcessor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<?php

namespace SilverStripe\DiscovererBifrost\Processors;

use Exception;
use SilverStripe\Core\Injector\Injectable;
use SilverStripe\Discoverer\Service\Results\Suggestions;

class SuggestionsProcessor
{

use Injectable;

/**
* @throws Exception
*/
public function getProcessedSuggestions(Suggestions $suggestions, array $response): void
{
// Check that we have all critical fields in our Elastic response
$this->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)
));
}
}

}
53 changes: 53 additions & 0 deletions src/Service/Adaptors/QuerySuggestionAdaptor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?php

namespace SilverStripe\DiscovererBifrost\Service\Adaptors;

use Elastic\EnterpriseSearch\Exception\ClientErrorResponseException;
use Elastic\EnterpriseSearch\Response\Response;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\Discoverer\Query\Suggestion;
use SilverStripe\Discoverer\Service\Interfaces\QuerySuggestionAdaptor as QuerySuggestionAdaptorInterface;
use SilverStripe\Discoverer\Service\Results\Suggestions;
use SilverStripe\DiscovererBifrost\Processors\SuggestionParamsProcessor;
use SilverStripe\DiscovererBifrost\Processors\SuggestionsProcessor;
use SilverStripe\DiscovererBifrost\Service\Requests\QuerySuggestion;
use SilverStripe\DiscovererElasticEnterprise\Service\Adaptors\BaseAdaptor;
use Throwable;

class QuerySuggestionAdaptor extends BaseAdaptor implements QuerySuggestionAdaptorInterface
{

public function process(Suggestion $suggestion, string $indexName): Suggestions
{
// Instantiate our Suggestions class with empty data. This will still be returned if there is an Exception
// during communication with Elastic (so that the page doesn't seriously break)
$suggestions = Suggestions::create();

try {
$engine = $this->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;
}
}

}
53 changes: 53 additions & 0 deletions src/Service/Adaptors/SpellingSuggestionAdaptor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?php

namespace SilverStripe\DiscovererBifrost\Service\Adaptors;

use Elastic\EnterpriseSearch\Exception\ClientErrorResponseException;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\Discoverer\Query\Suggestion;
use SilverStripe\Discoverer\Service\Interfaces\SpellingSuggestionAdaptor as SpellingSuggestionAdaptorInterface;
use SilverStripe\Discoverer\Service\Results\Suggestions;
use SilverStripe\DiscovererBifrost\Processors\SuggestionParamsProcessor;
use SilverStripe\DiscovererBifrost\Processors\SuggestionsProcessor;
use SilverStripe\DiscovererBifrost\Service\Requests\SpellingSuggestion;
use SilverStripe\DiscovererElasticEnterprise\Service\Adaptors\BaseAdaptor;
use Throwable;
use Elastic\EnterpriseSearch\Response\Response;

class SpellingSuggestionAdaptor extends BaseAdaptor implements SpellingSuggestionAdaptorInterface
{

public function process(Suggestion $suggestion, string $indexName): Suggestions
{
// Instantiate our Suggestions class with empty data. This will still be returned if there is an Exception
// during communication with Elastic (so that the page doesn't seriously break)
$suggestions = Suggestions::create();

try {
$engine = $this->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;
}
}

}
4 changes: 2 additions & 2 deletions src/Service/Requests/ClickPost.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
18 changes: 18 additions & 0 deletions src/Service/Requests/Params/SuggestionParams.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

namespace SilverStripe\DiscovererBifrost\Service\Requests\Params;

use SilverStripe\Core\Injector\Injectable;

class SuggestionParams
{

use Injectable;

public ?string $query = null;

public array $fields = [];

public ?int $size = null;

}
16 changes: 10 additions & 6 deletions src/Service/Requests/QuerySuggestion.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,21 @@

namespace SilverStripe\DiscovererBifrost\Service\Requests;

use Elastic\EnterpriseSearch\AppSearch\Request\QuerySuggestion as AppSearchQuerySuggestion;
use Elastic\EnterpriseSearch\AppSearch\Schema\QuerySuggestionRequest;
use Elastic\EnterpriseSearch\Request\Request;
use SilverStripe\Core\Injector\Injectable;
use SilverStripe\DiscovererBifrost\Service\Requests\Params\SuggestionParams;

class QuerySuggestion extends AppSearchQuerySuggestion
class QuerySuggestion extends Request
{

public function __construct(string $engineName, ?QuerySuggestionRequest $query_suggestion_request = null)
{
parent::__construct($engineName, $query_suggestion_request);
use Injectable;

public function __construct(string $engineName, SuggestionParams $params)
{
$this->method = 'POST';
$this->path = sprintf('/api/v1/%s/query_suggestion', $engineName);
$this->headers['Content-Type'] = 'application/json';
$this->body = $params;
}

}
4 changes: 2 additions & 2 deletions src/Service/Requests/Search.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
22 changes: 22 additions & 0 deletions src/Service/Requests/SpellingSuggestion.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

namespace SilverStripe\DiscovererBifrost\Service\Requests;

use Elastic\EnterpriseSearch\Request\Request;
use SilverStripe\Core\Injector\Injectable;
use SilverStripe\DiscovererBifrost\Service\Requests\Params\SuggestionParams;

class SpellingSuggestion extends Request
{

use Injectable;

public function __construct(string $engineName, SuggestionParams $params)
{
$this->method = 'POST';
$this->path = sprintf('/api/v1/%s/spelling_suggestion', $engineName);
$this->headers['Content-Type'] = 'application/json';
$this->body = $params;
}

}

0 comments on commit 4cb0d58

Please sign in to comment.