first commit
This commit is contained in:
313
bundles/KupShop/CatalogElasticBundle/PreparedSectionQuery.php
Normal file
313
bundles/KupShop/CatalogElasticBundle/PreparedSectionQuery.php
Normal file
@@ -0,0 +1,313 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace KupShop\CatalogElasticBundle;
|
||||
|
||||
use Elastica\Exception\NotFoundException;
|
||||
use Elastica\Multi\ResultSet as MultiResultSet;
|
||||
use Elastica\Multi\Search as MultiSearch;
|
||||
use Elastica\Result;
|
||||
use Elastica\ResultSet;
|
||||
use KupShop\CatalogBundle\ProductList\Builder\FilteredProductListInterface;
|
||||
use KupShop\CatalogBundle\ProductList\DynamicFilterAttributes;
|
||||
use KupShop\CatalogBundle\Search\Exception\FulltextException;
|
||||
use KupShop\CatalogElasticBundle\Contracts\ElasticSectionEventDispatcher;
|
||||
use KupShop\CatalogElasticBundle\Contracts\SectionResponseInterface;
|
||||
use KupShop\CatalogElasticBundle\Dto\PreparedSectionQueryInnerState;
|
||||
use KupShop\CatalogElasticBundle\Elastica\ErrorWrap;
|
||||
use KupShop\CatalogElasticBundle\Util\ElasticFilterValues;
|
||||
use KupShop\CatalogElasticBundle\Util\ElasticQueryClient;
|
||||
use Query\Product;
|
||||
use Query\QueryBuilder;
|
||||
|
||||
class PreparedSectionQuery implements SectionResponseInterface, FilteredProductListInterface, ElasticSectionEventDispatcher
|
||||
{
|
||||
private array $listeners = [];
|
||||
private MultiResultSet $response;
|
||||
private bool $isExecuted = false;
|
||||
|
||||
private float $queryCompileTime = -1;
|
||||
private float $queryExecuteTime = -1;
|
||||
|
||||
/**
|
||||
* @param \Closure(SectionRequest): MultiSearch $queryCompiler
|
||||
*/
|
||||
public function __construct(
|
||||
private readonly SectionRequest $request,
|
||||
private readonly \Closure $queryCompiler,
|
||||
private readonly ElasticFilterValues $filterValuesCompiler,
|
||||
private readonly \ProductList $productList,
|
||||
private readonly \Filter $filter,
|
||||
private readonly ?\Pager $pager,
|
||||
private readonly DynamicFilterAttributes $dynamicFilterAttributes,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the ES search and executes it. If the request was already executed - do nothing.
|
||||
*
|
||||
* Also runs event listeners.
|
||||
*
|
||||
* @throws FulltextException
|
||||
*/
|
||||
protected function ensureExecuted(): void
|
||||
{
|
||||
if ($this->isExecuted) {
|
||||
return;
|
||||
}
|
||||
|
||||
// First execute the ES query.
|
||||
// compile the query -> create the Elasticsearch MultiSearch from \FilterParams, \Pager etc.
|
||||
// queryCompileTime = PHP execution time of the compilation of the ES query (through Elastica).
|
||||
$compileStart = microtime(true);
|
||||
|
||||
// Provide access to the internal members of this class without needing to first execute the query (access by getters on this class).
|
||||
// Some modules will need this to modify the \Pager (e.g. ProductListBanners - by adding PagerExtraItems into \Pager...).
|
||||
$innerStateRef = new PreparedSectionQueryInnerState(
|
||||
$this->productList,
|
||||
$this->filter,
|
||||
$this->pager,
|
||||
$this->dynamicFilterAttributes,
|
||||
);
|
||||
// Send an event before the search is compiled - allows modifications to \FilterParams, \Pager, order etc.
|
||||
$this->dispatchEvent(self::EVENT_BEFORE_COMPILE, $this->request, $innerStateRef);
|
||||
|
||||
// This line creates the ES search (in the form of Elastica\MultiSearch instance).
|
||||
$multiSearch = $this->createMultiSearch();
|
||||
|
||||
$this->queryCompileTime = microtime(true) - $compileStart;
|
||||
|
||||
// execute it -> actually make the request to Elasticsearch
|
||||
// queryExecuteTime = PHP execution time of the Elasticsearch (HTTP) request.
|
||||
$executeStart = microtime(true);
|
||||
|
||||
// This event should be used to add additional searches to the MultiSearch.
|
||||
// (You can also modify the existing ones, but don't do that please.)
|
||||
// SearchView: adds searches to different ES indices to search in - articles, sections, pages, ...
|
||||
$this->dispatchEvent(self::EVENT_BEFORE_EXECUTE, $multiSearch, $innerStateRef);
|
||||
|
||||
// Here the query is executed (HTTP request is sent).
|
||||
$this->response = static::checkResponse($multiSearch->search());
|
||||
$this->queryExecuteTime = microtime(true) - $executeStart;
|
||||
$this->isExecuted = true;
|
||||
|
||||
// Fill the objects.
|
||||
$productIds = $this->getProductIds();
|
||||
$this->productList->andSpec(static function (QueryBuilder $qb) use ($productIds) {
|
||||
$qb->andWhere(Product::productsIds($productIds));
|
||||
});
|
||||
|
||||
// Add products counts for filters (\Filter)
|
||||
$filterValuesCache = $this->filterValuesCompiler->getFromResponse(
|
||||
$this->response,
|
||||
$this->dynamicFilterAttributes,
|
||||
);
|
||||
$this->filter->setValuesCache($filterValuesCache);
|
||||
|
||||
// Update pager
|
||||
if ($this->pager) {
|
||||
$maxResultsAllowed = ElasticQueryClient::ELASTIC_RESULTS_LIMIT;
|
||||
|
||||
$this->pager->setTotal(min(
|
||||
$maxResultsAllowed - ($maxResultsAllowed % $this->pager->getOnPage()),
|
||||
$this->getProductsCount(),
|
||||
));
|
||||
}
|
||||
|
||||
$this->dispatchEvent(self::EVENT_AFTER_EXECUTE, $innerStateRef);
|
||||
|
||||
// Log the query timings as HTTP headers (when in development)
|
||||
$this->debugLog();
|
||||
}
|
||||
|
||||
public function createMultiSearch(): MultiSearch
|
||||
{
|
||||
return ($this->queryCompiler)($this->request);
|
||||
}
|
||||
|
||||
public function getMainResult(): ResultSet
|
||||
{
|
||||
$this->ensureExecuted();
|
||||
|
||||
return $this->response[ElasticQueryClient::$mainQueryName];
|
||||
}
|
||||
|
||||
public function getProductsCount(): int
|
||||
{
|
||||
return $this->getMainResult()->getTotalHits();
|
||||
}
|
||||
|
||||
public function getProductIds(): array
|
||||
{
|
||||
return array_filter(array_map(
|
||||
static fn (Result $doc) => $doc->getFields()['id'][0] ?? null,
|
||||
$this->getMainResult()->getResults(),
|
||||
));
|
||||
}
|
||||
|
||||
public function getResponse(): MultiResultSet
|
||||
{
|
||||
$this->ensureExecuted();
|
||||
|
||||
return $this->response;
|
||||
}
|
||||
|
||||
public function isExecuted(): bool
|
||||
{
|
||||
return $this->isExecuted;
|
||||
}
|
||||
|
||||
public function getProductList(): \ProductList
|
||||
{
|
||||
$this->ensureExecuted();
|
||||
|
||||
return $this->productList;
|
||||
}
|
||||
|
||||
public function getPager(): ?\Pager
|
||||
{
|
||||
$this->ensureExecuted();
|
||||
|
||||
return $this->pager;
|
||||
}
|
||||
|
||||
public function getFilterParams(): \FilterParams
|
||||
{
|
||||
$this->ensureExecuted();
|
||||
|
||||
return $this->filter->getFilterParams();
|
||||
}
|
||||
|
||||
public function getDynamicFilter(): \Filter
|
||||
{
|
||||
$this->ensureExecuted();
|
||||
|
||||
return $this->filter;
|
||||
}
|
||||
|
||||
public function getQueryTimes(): array
|
||||
{
|
||||
$this->ensureExecuted();
|
||||
|
||||
$separate = [];
|
||||
foreach ($this->response->getResultSets() as $name => $rs) {
|
||||
try {
|
||||
$separate[$name] = $rs->getResponse()->getEngineTime();
|
||||
} catch (NotFoundException) {
|
||||
$separate[$name] = '!NOT FOUND!';
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
$total = $this->response->getResponse()->getEngineTime();
|
||||
} catch (NotFoundException) {
|
||||
$total = '!NOT FOUND!';
|
||||
}
|
||||
|
||||
return [
|
||||
'took-total' => $total,
|
||||
'took-per-search' => $separate,
|
||||
'php-compile' => $this->queryCompileTime,
|
||||
'php-request' => $this->queryExecuteTime,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param callable(SectionRequest, PreparedSectionQueryInnerState): void $listener
|
||||
*/
|
||||
public function onBeforeCompile(callable $listener): void
|
||||
{
|
||||
if ($this->isExecuted) {
|
||||
throw new \LogicException(sprintf(
|
||||
'Cannot add a "%s" listener after the search query was already executed (doing so would have no effect).',
|
||||
self::EVENT_BEFORE_COMPILE,
|
||||
));
|
||||
}
|
||||
|
||||
$this->listeners[self::EVENT_BEFORE_COMPILE][] = $listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param callable(MultiSearch, PreparedSectionQueryInnerState): void $listener
|
||||
*/
|
||||
public function onBeforeExecute(callable $listener): void
|
||||
{
|
||||
if ($this->isExecuted) {
|
||||
throw new \LogicException(sprintf(
|
||||
'Cannot add a "%s" listener after the search query was already executed (doing so would have no effect).',
|
||||
self::EVENT_BEFORE_EXECUTE,
|
||||
));
|
||||
}
|
||||
|
||||
$this->listeners[self::EVENT_BEFORE_EXECUTE][] = $listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param callable(PreparedSectionQueryInnerState): void $listener
|
||||
*/
|
||||
public function onAfterExecute(callable $listener): void
|
||||
{
|
||||
if ($this->isExecuted) {
|
||||
throw new \LogicException(sprintf(
|
||||
'Cannot add a "%s" listener after the search query was already executed (doing so would have no effect).',
|
||||
self::EVENT_AFTER_EXECUTE,
|
||||
));
|
||||
}
|
||||
|
||||
$this->listeners[self::EVENT_AFTER_EXECUTE][] = $listener;
|
||||
}
|
||||
|
||||
protected function dispatchEvent(string $eventName, ...$args): void
|
||||
{
|
||||
if (empty($this->listeners[$eventName])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$listeners = &$this->listeners[$eventName];
|
||||
|
||||
while ($listener = array_shift($listeners)) {
|
||||
$listener(...$args);
|
||||
}
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'productList' => $this->getProductList(),
|
||||
'dynamicFilter' => $this->getDynamicFilter(),
|
||||
'filterParams' => $this->getFilterParams(),
|
||||
'pager' => $this->getPager(),
|
||||
];
|
||||
}
|
||||
|
||||
private function debugLog(): void
|
||||
{
|
||||
if (!isDevelopment() || isFunctionalTests()) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($this->getQueryTimes() as $key => $value) {
|
||||
if (is_array($value)) {
|
||||
$value = json_encode($value);
|
||||
}
|
||||
|
||||
header("X-ES-{$key}: {$value}");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether there are any errors in any of the result sets.
|
||||
* The Elastica library unfortunately does not with Multi\ResultSet :/.
|
||||
*
|
||||
* @throws FulltextException
|
||||
*/
|
||||
protected static function checkResponse(MultiResultSet $multiResultSet): MultiResultSet
|
||||
{
|
||||
if ($err = ErrorWrap::extractError($multiResultSet)) {
|
||||
getRaven()->captureException($err);
|
||||
}
|
||||
|
||||
return $multiResultSet;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user