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; } }