Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fallback pseudo search engine #1

Draft
wants to merge 10 commits into
base: develop
Choose a base branch
from
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,19 @@ Install
1. Execute `composer require adriendupuis/ezplatform-standard;`
1. Add to config/bundles.php: `AdrienDupuis\EzPlatformStandardBundle\AdrienDupuisEzPlatformStandardBundle::class => ['all' => true],`

Features
--------

### Fallback Search Engine

The fallback search engine is a wrapper receiving a list of search engines.
* Before searching, it loops on this list and execute the search on the first healthy search engine.
* For indexing, two configurable cases:
- Index on every healthy engine; Skip unavailable ones.
- Skip whole index from all engines if one is unhealthy.

TODO
----

- Twig functions and filters
- Enhance Fallback Search Engine indexation scenarios
7 changes: 7 additions & 0 deletions src/bundle/AdrienDupuisEzPlatformStandardBundle.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,15 @@

namespace AdrienDupuis\EzPlatformStandardBundle;

use Symfony\Component\DependencyInjection\Compiler\PassConfig;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Bundle\Bundle;

class AdrienDupuisEzPlatformStandardBundle extends Bundle
{
public function build(ContainerBuilder $container)
{
parent::build($container);
$container->addCompilerPass(new FallbackSearchEngine\CompilerPass(), PassConfig::TYPE_OPTIMIZE);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

namespace AdrienDupuis\EzPlatformStandardBundle\DependencyInjection;

use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Extension\Extension;
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;

class AdrienDupuisEzPlatformStandardExtension extends Extension
{
public function load(array $configs, ContainerBuilder $container): void
{
$loader = new YamlFileLoader(
$container,
new FileLocator(__DIR__.'/../Resources/config')
);
$loader->load('services.yaml');
}
}
36 changes: 36 additions & 0 deletions src/bundle/FallbackSearchEngine/CompilerPass.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

namespace AdrienDupuis\EzPlatformStandardBundle\FallbackSearchEngine;

use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;

class CompilerPass implements CompilerPassInterface
{
public const SEARCH_ENGINE_FALLBACK_HANDLER = 'ezplatform.search_engine.fallback.handler';
public const SEARCH_ENGINE_PING_SERVICE_TAG = 'ezplatform.search_engine.ping';

/**
* Registers all found search engines to the SearchEngineFactory.
*
* @throws \LogicException
*/
public function process(ContainerBuilder $container)
{
$searchEngineFallbackHandlerDefinition = $container->getDefinition(self::SEARCH_ENGINE_FALLBACK_HANDLER);
$searchEnginePingServices = $container->findTaggedServiceIds(self::SEARCH_ENGINE_PING_SERVICE_TAG);

foreach ($searchEnginePingServices as $serviceId => $attributes) {
foreach ($attributes as $attribute) {
$searchEngineFallbackHandlerDefinition->addMethodCall(
'registerSearchEnginePingService',
[
new Reference($serviceId),
$attribute['alias'],
]
);
}
}
}
}
231 changes: 231 additions & 0 deletions src/bundle/FallbackSearchEngine/Handler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
<?php

namespace AdrienDupuis\EzPlatformStandardBundle\FallbackSearchEngine;

use AdrienDupuis\EzPlatformStandardBundle\FallbackSearchEngine\Ping\PingInterface;
use eZ\Bundle\EzPublishCoreBundle\ApiLoader\Exception\InvalidSearchEngine;
use eZ\Bundle\EzPublishCoreBundle\ApiLoader\SearchEngineFactory;
use eZ\Publish\API\Repository\Values\Content\LocationQuery;
use eZ\Publish\API\Repository\Values\Content\Query;
use eZ\Publish\API\Repository\Values\Content\Query\Criterion;
use eZ\Publish\API\Repository\Values\Content\Search\SearchResult;
use eZ\Publish\Core\Base\Exceptions\InvalidArgumentException;
use eZ\Publish\SPI\Persistence\Content;
use eZ\Publish\SPI\Persistence\Content\Location;
use eZ\Publish\SPI\Search\VersatileHandler;
use Psr\Log\LoggerInterface;

class Handler implements VersatileHandler
{
const ALIAS = 'fallback';

/** @var string[] */
private $searchEngineAliases;

/** @var SearchEngineFactory */
private $searchEngineFactory;

/** @var LoggerInterface */
private $logger;

/** @var PingInterface[] */
private $searchEnginePingServices;

/** @var VersatileHandler */
private $innerSearchEngine;

public function __construct(
array $searchEngineAliases,
SearchEngineFactory $searchEngineFactory,
LoggerInterface $logger = null
) {
$this->searchEngineAliases = $searchEngineAliases;
$this->searchEngineFactory = $searchEngineFactory;
$this->logger = $logger;

if (0 === count($searchEngineAliases)) {
throw new InvalidArgumentException('$searchEngineAliases', 'The fallback search engine chain\'s alias list can\'t be empty.');
} elseif (1 === count($searchEngineAliases)) {
$this->logger->notice('The fallback search engine chain\'s alias list must contain more than one alias to be useful.');
}
if (in_array(self::ALIAS, $searchEngineAliases)) {
throw new InvalidArgumentException('$searchEngineAliases', 'The fallback search engine chain can\'t contain the fallback handler\'s alias itself.');
}
}

public function registerSearchEnginePingService(PingInterface $searchEnginePingService, string $searchEngineAlias)
{
$this->searchEnginePingServices[$searchEngineAlias] = $searchEnginePingService;
}

public function getSearchEnginePingService(string $searchEngineAlias): PingInterface
{
if (!array_key_exists($searchEngineAlias, $this->searchEnginePingServices)) {
throw new InvalidSearchEngine("Search engine '{$searchEngineAlias}' has no ping service. Could not find any service tagged with 'ezplatform.search_engine.ping' with alias '{$searchEngineAlias}'.");
}

return $this->searchEnginePingServices[$searchEngineAlias];
}

public function setInnerSearchEngine()
{
$this->innerSearchEngine = $this->getAvailableSearchEngine();
}

public function getInnerSearchEngine(): ?VersatileHandler
{
if (is_null($this->innerSearchEngine)) {
$this->setInnerSearchEngine();
}

return $this->innerSearchEngine;
}

/**
* Get search engine by alias/identifier.
*/
private function getSearchEngine(string $alias): VersatileHandler
{
$searchEngines = $this->searchEngineFactory->getSearchEngines();
if (array_key_exists($alias, $searchEngines)) {
return $searchEngines[$alias];
}
throw new InvalidSearchEngine("Invalid search engine '{$alias}'. Could not find any service tagged with 'ezplatform.search_engine' with alias '{$alias}'.");
}

/**
* Check if a search engine is responding.
*/
public function isAvailable(string $alias): bool
{
return $this->getSearchEnginePingService($alias)->ping();
}

/**
* Get first responding search engine.
*/
public function getAvailableSearchEngine(): ?VersatileHandler
{
foreach ($this->searchEngineAliases as $index => $alias) {
if ($this->isAvailable($alias)) {
if ($index && !is_null($this->logger)) {
$this->logger->notice("Use '{$alias}' search service as substitute.");
}

return $this->getSearchEngine($alias);
} elseif (!is_null($this->logger)) {
$this->logger->warning(($index ? 'Alternative' : 'Main')." search service '{$alias}' is not available.");
}
}
if (!is_null($this->logger)) {
$this->logger->error('No search service available.');
}

return null;
}

private function getEmptySearchResult(): SearchResult
{
return new SearchResult([
'time' => 0,
'totalCount' => 0,
'searchHits' => [],
]);
}

/**
* Check that all search engines are responding.
*/
public function allSearchEngineAreAvailable(): bool
{
foreach ($this->searchEngineAliases as $alias) {
if (!$this->isAvailable($alias)) {
return false;
}
}

return true;
}

/* Search */

public function supports(int $capabilityFlag): bool
{
return $this->getInnerSearchEngine()->supports($capabilityFlag);
}

/**
* @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException
*/
public function findContent(Query $query, array $languageFilter = []): SearchResult
{
if (empty($this->getInnerSearchEngine())) {
return $this->getEmptySearchResult();
}

return $this->getInnerSearchEngine()->findContent($query, $languageFilter);
}

public function findSingle(Criterion $filter, array $languageFilter = [])
{
return $this->getInnerSearchEngine()->findSingle($query, $languageFilter);
}

public function findLocations(LocationQuery $query, array $languageFilter = []): SearchResult
{
if (empty($this->getInnerSearchEngine())) {
return $this->getEmptySearchResult();
}

return $this->getInnerSearchEngine()->findLocations($query, $languageFilter);
}

public function suggest($prefix, $fieldPaths = [], $limit = 10, Criterion $filter = null)
{
return $this->getInnerSearchEngine()->suggest($prefix, $fieldPaths, $limit, $filter);
}

/* Index */

public function deleteTranslation(int $contentId, string $languageCode): void
{
foreach ($this->searchEngineAliases as $alias) {
$this->getSearchEngine($alias)->deleteTranslation($contentId, $languageCode);
}
}

public function indexContent(Content $content)
{
foreach ($this->searchEngineAliases as $alias) {
$this->getSearchEngine($alias)->indexContent($content);
}
}

public function deleteContent($contentId, $versionId = null)
{
foreach ($this->searchEngineAliases as $alias) {
$this->getSearchEngine($alias)->deleteContent($contentId, $versionId);
}
}

public function indexLocation(Location $location)
{
foreach ($this->searchEngineAliases as $alias) {
$this->getSearchEngine($alias)->indexContent($location);
}
}

public function deleteLocation($locationId, $contentId)
{
foreach ($this->searchEngineAliases as $alias) {
$this->getSearchEngine($alias)->deleteLocation($locationId, $contentId);
}
}

public function purgeIndex()
{
foreach ($this->searchEngineAliases as $alias) {
$this->getSearchEngine($alias)->purgeIndex();
}
}
}
Loading