-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
FEATURE: Aggregation Support (a big one :-) )
- Loading branch information
1 parent
7105277
commit 396790b
Showing
13 changed files
with
674 additions
and
93 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,113 @@ | ||
<?php | ||
declare(strict_types=1); | ||
|
||
namespace Sandstorm\LightweightElasticsearch\Query; | ||
|
||
use Neos\ContentRepository\Domain\Model\NodeInterface; | ||
use Neos\ContentRepository\Utility; | ||
use Neos\Flow\Annotations as Flow; | ||
use Flowpack\ElasticSearch\ContentRepositoryAdaptor\ElasticSearchClient; | ||
use Flowpack\ElasticSearch\Transfer\Exception\ApiException; | ||
use Neos\Eel\ProtectedContextAwareInterface; | ||
use Neos\Flow\Log\ThrowableStorageInterface; | ||
use Neos\Flow\Log\Utility\LogEnvironment; | ||
use Psr\Log\LoggerInterface; | ||
|
||
|
||
abstract class AbstractSearchRequestBuilder implements ProtectedContextAwareInterface | ||
{ | ||
|
||
/** | ||
* @Flow\Inject | ||
* @var ElasticSearchClient | ||
*/ | ||
protected $elasticSearchClient; | ||
|
||
/** | ||
* @Flow\Inject | ||
* @var ThrowableStorageInterface | ||
*/ | ||
protected $throwableStorage; | ||
|
||
/** | ||
* @Flow\Inject | ||
* @var LoggerInterface | ||
*/ | ||
protected $logger; | ||
|
||
private array $additionalIndices; | ||
|
||
/** | ||
* @var boolean | ||
*/ | ||
protected $logThisQuery = false; | ||
|
||
/** | ||
* @var string | ||
*/ | ||
protected $logMessage; | ||
|
||
/** | ||
* @var NodeInterface|null | ||
*/ | ||
protected ?NodeInterface $contextNode; | ||
|
||
public function __construct(NodeInterface $contextNode = null, array $additionalIndices = []) | ||
{ | ||
$this->contextNode = $contextNode; | ||
$this->additionalIndices = $additionalIndices; | ||
} | ||
|
||
/** | ||
* Log the current request to the Elasticsearch log for debugging after it has been executed. | ||
* | ||
* @param string $message an optional message to identify the log entry | ||
* @api | ||
*/ | ||
public function log($message = null): self | ||
{ | ||
$this->logThisQuery = true; | ||
$this->logMessage = $message; | ||
|
||
return $this; | ||
} | ||
|
||
/** | ||
* Execute the query and return the SearchResult object as result. | ||
* | ||
* You can call this method multiple times; and the request is only executed at the first time; and cached | ||
* for later use. | ||
* | ||
* @throws \Flowpack\ElasticSearch\Exception | ||
* @throws \Neos\Flow\Http\Exception | ||
*/ | ||
protected function executeInternal(array $request): array | ||
{ | ||
try { | ||
$timeBefore = microtime(true); | ||
|
||
$indexNames = $this->additionalIndices; | ||
if ($this->contextNode !== null) { | ||
$dimensionValues = $this->contextNode->getContext()->getDimensions(); | ||
$dimensionHash = Utility::sortDimensionValueArrayAndReturnDimensionsHash($dimensionValues); | ||
$indexNames[] = 'neoscr-' . $dimensionHash; | ||
} | ||
|
||
$response = $this->elasticSearchClient->request('GET', '/' . implode(',', $indexNames) . '/_search', [], $request); | ||
$timeAfterwards = microtime(true); | ||
|
||
$jsonResponse = $response->getTreatedContent(); | ||
$this->logThisQuery && $this->logger->debug(sprintf('Query Log (%s): Indexname: %s %s -- execution time: %s ms -- Number of results returned: %s -- Total Results: %s', $this->logMessage, implode(',', $indexNames), $request, (($timeAfterwards - $timeBefore) * 1000), count($jsonResponse['hits']['hits']), $jsonResponse['hits']['total']['value']), LogEnvironment::fromMethodName(__METHOD__)); | ||
return $jsonResponse; | ||
} catch (ApiException $exception) { | ||
$message = $this->throwableStorage->logThrowable($exception); | ||
$this->logger->error(sprintf('Request failed with %s', $message), LogEnvironment::fromMethodName(__METHOD__)); | ||
throw $exception; | ||
} | ||
} | ||
|
||
public function allowsCallOfMethod($methodName) | ||
{ | ||
return true; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
<?php | ||
|
||
|
||
namespace Sandstorm\LightweightElasticsearch\Query\Aggregation; | ||
|
||
use Sandstorm\LightweightElasticsearch\Query\AggregationRequestBuilder; | ||
|
||
interface AggregationBuilderInterface | ||
{ | ||
/** | ||
* Returns the Elasticsearch aggregation request part; so the part inside {"aggs": ...}. | ||
* | ||
* Is called by the framework (usually inside {@see AggregationRequestBuilder}, not by the end-user. | ||
* | ||
* @return array | ||
*/ | ||
public function buildAggregationRequest(): array; | ||
|
||
/** | ||
* Binds the aggreation response to this aggregation; effectively creating an aggregation response object | ||
* for this request. | ||
* | ||
* Is called by the framework (usually inside {@see AggregationRequestBuilder}, not by the end-user. | ||
* | ||
* @param array $aggregationResponse | ||
* @return AggregationResultInterface | ||
*/ | ||
public function bindResponse(array $aggregationResponse): AggregationResultInterface; | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
<?php | ||
declare(strict_types=1); | ||
|
||
namespace Sandstorm\LightweightElasticsearch\Query\Aggregation; | ||
|
||
/** | ||
* Marker interface for aggregation results. | ||
* | ||
* An aggregation result is always created by calling {@see AggregationBuilderInterface::bindResponse()); | ||
* and each AggregationBuilder implementation has a corresponding AggregationResult implementation. | ||
*/ | ||
interface AggregationResultInterface | ||
{ | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
<?php | ||
declare(strict_types=1); | ||
|
||
namespace Sandstorm\LightweightElasticsearch\Query\Aggregation; | ||
|
||
use Neos\Eel\ProtectedContextAwareInterface; | ||
use Neos\Flow\Annotations as Flow; | ||
|
||
/** | ||
* Placeholder for an aggregation result in case of a query error | ||
* | ||
* @Flow\Proxy(false) | ||
*/ | ||
class QueryErrorAggregationResult implements AggregationResultInterface, ProtectedContextAwareInterface | ||
{ | ||
|
||
public function isError() { | ||
return true; | ||
} | ||
|
||
public function allowsCallOfMethod($methodName) | ||
{ | ||
return true; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
<?php | ||
|
||
|
||
namespace Sandstorm\LightweightElasticsearch\Query\Aggregation; | ||
|
||
use Neos\Flow\Annotations as Flow; | ||
use Sandstorm\LightweightElasticsearch\Query\SearchQueryBuilderInterface; | ||
|
||
/** | ||
* A Terms aggregation can be used to build faceted search. | ||
* | ||
* It needs to be configured using: | ||
* - the Elasticsearch field name which should be faceted (should be of type "keyword" to have useful results) | ||
* - The selected value from the request, if any. | ||
* | ||
* The Terms Aggregation can be additionally used as search filter. | ||
* | ||
* See https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-terms-aggregation.html for the details of usage. | ||
* | ||
* @Flow\Proxy(false) | ||
*/ | ||
class TermsAggregationBuilder implements AggregationBuilderInterface, SearchQueryBuilderInterface | ||
{ | ||
private string $fieldName; | ||
/** | ||
* @var string|null the selected value, as taken from the URL parameters | ||
*/ | ||
private ?string $selectedValue; | ||
|
||
public static function create(string $fieldName, ?string $selectedValue = null): self | ||
{ | ||
return new self($fieldName, $selectedValue); | ||
} | ||
|
||
private function __construct(string $fieldName, ?string $selectedValue = null) | ||
{ | ||
$this->fieldName = $fieldName; | ||
$this->selectedValue = $selectedValue; | ||
} | ||
|
||
public function buildAggregationRequest(): array | ||
{ | ||
// This is a Terms aggregation, with the field name specified by the user. | ||
return [ | ||
'terms' => [ | ||
'field' => $this->fieldName | ||
] | ||
]; | ||
} | ||
|
||
public function bindResponse(array $aggregationResponse): AggregationResultInterface | ||
{ | ||
return TermsAggregationResult::create($aggregationResponse, $this); | ||
} | ||
|
||
public function buildQuery(): array | ||
{ | ||
// for implementing faceting, we build the restriction query here | ||
if ($this->selectedValue) { | ||
return [ | ||
'term' => [ | ||
$this->fieldName => $this->selectedValue | ||
] | ||
]; | ||
} | ||
|
||
// json_encode([]) === "[]" | ||
// json_encode(new \stdClass) === "{}" <-- we need this! | ||
return ['match_all' => new \stdClass()]; | ||
} | ||
|
||
/** | ||
* @return string|null | ||
*/ | ||
public function getSelectedValue(): ?string | ||
{ | ||
return $this->selectedValue; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
<?php | ||
declare(strict_types=1); | ||
|
||
namespace Sandstorm\LightweightElasticsearch\Query\Aggregation; | ||
|
||
use Neos\Eel\ProtectedContextAwareInterface; | ||
use Neos\Flow\Annotations as Flow; | ||
|
||
/** | ||
* | ||
* Example usage: | ||
* | ||
* ```fusion | ||
* nodeTypesFacet = Neos.Fusion:Component { | ||
* termsAggregationResult = ${searchRequest.execute().aggregation("nodeTypes")} | ||
* renderer = afx` | ||
* <Neos.Fusion:Loop items={props.termsAggregationResult.buckets} itemName="bucket"> | ||
* <Neos.Neos:NodeLink node={documentNode} addQueryString={true} arguments={props.termsAggregationResult.buildUriArgumentForFacet(bucket.key)}>{bucket.key}</Neos.Neos:NodeLink> {bucket.doc_count} | ||
* </Neos.Fusion:Loop> | ||
* ` | ||
* } | ||
* ``` | ||
* | ||
* @Flow\Proxy(false) | ||
*/ | ||
class TermsAggregationResult implements AggregationResultInterface, ProtectedContextAwareInterface | ||
{ | ||
private array $aggregationResponse; | ||
private TermsAggregationBuilder $termsAggregationBuilder; | ||
|
||
private function __construct(array $aggregationResponse, TermsAggregationBuilder $aggregationRequestBuilder) | ||
{ | ||
$this->aggregationResponse = $aggregationResponse; | ||
$this->termsAggregationBuilder = $aggregationRequestBuilder; | ||
} | ||
|
||
public static function create(array $aggregationResponse, TermsAggregationBuilder $aggregationRequestBuilder): self | ||
{ | ||
return new self($aggregationResponse, $aggregationRequestBuilder); | ||
} | ||
|
||
public function getBuckets() { | ||
return $this->aggregationResponse['buckets']; | ||
} | ||
|
||
/** | ||
* @return string|null | ||
*/ | ||
public function getSelectedValue(): ?string | ||
{ | ||
return $this->termsAggregationBuilder->getSelectedValue(); | ||
} | ||
|
||
public function allowsCallOfMethod($methodName) | ||
{ | ||
return true; | ||
} | ||
} |
Oops, something went wrong.