OpenXE/classes/Modules/SuperSearch/SuperSearchIndexer.php
2021-05-21 08:49:41 +02:00

488 lines
15 KiB
PHP

<?php
namespace Xentral\Modules\SuperSearch;
use DateTimeImmutable;
use DateTimeInterface;
use Exception;
use Xentral\Components\Database\Database;
use Xentral\Components\Database\Exception\DatabaseExceptionInterface;
use Xentral\Modules\SuperSearch\Exception\InvalidReturnValueException;
use Xentral\Modules\SuperSearch\Exception\ProviderIncompatibleException;
use Xentral\Modules\SuperSearch\Exception\ProviderMissingException;
use Xentral\Modules\SuperSearch\SearchIndex\Data\IndexIdentifier;
use Xentral\Modules\SuperSearch\SearchIndex\Data\IndexItem;
use Xentral\Modules\SuperSearch\SearchIndex\Provider\BulkIndexProviderInterface;
use Xentral\Modules\SuperSearch\SearchIndex\Provider\DiffIndexProviderInterface;
use Xentral\Modules\SuperSearch\SearchIndex\Provider\FullIndexProviderInterface;
use Xentral\Modules\SuperSearch\SearchIndex\Provider\ItemIndexProviderInterface;
use Xentral\Modules\SuperSearch\SearchIndex\Provider\SearchIndexProviderInterface;
final class SuperSearchIndexer
{
/** @var Database $db */
private $db;
/** @var array|SearchIndexProviderInterface[] $provider */
private $provider;
/**
* @param Database $database
* @param SearchIndexProviderInterface[]|array $provider Nur aktive Provider übergeben!
*/
public function __construct(Database $database, array $provider = [])
{
$this->db = $database;
$this->provider = $provider;
}
/**
* Gibt Meta-Information zu den Providern zurück; nur von aktiven Providern
*
* @return array
*/
public function getProviderMetaData()
{
$meta = [];
foreach ($this->provider as $provider) {
$meta[] = [
'name' => $provider->getIndexName(),
'title' => $provider->getIndexTitle(),
'module' => $provider->getModuleName(),
];
}
return $meta;
}
/**
* Tatsächliche Index-Größe ermitteln (gruppiert nach Index) (nur von aktiven Providern)
*
* @return array
*/
public function getProviderIndexSizesCurrent()
{
$sql =
'SELECT sig.name, COUNT(sii.id) AS `index_size`
FROM `supersearch_index_group` AS `sig`
LEFT JOIN `supersearch_index_item` AS `sii` ON sig.name = sii.index_name AND sii.outdated = 0
WHERE sig.active = 1
GROUP BY sig.name';
$indexSizes = $this->db->fetchPairs($sql);
// Fehlende Indexe ergänzen, falls Provider registriert ist aber noch nie gelaufen ist
foreach ($this->provider as $provider) {
$indexName = $provider->getIndexName();
if (!isset($indexSizes[$indexName])) {
$indexSizes[$indexName] = null;
}
}
ksort($indexSizes);
return $indexSizes;
}
/**
* Potentielle Index-Größe ermitteln (gruppiert nach Index) (nur von aktiven Providern)
*
* @return array
*/
public function getProviderIndexSizesPotential()
{
$indexSizes = [];
foreach ($this->provider as $provider) {
// Möglich Index-Größe beim Provider erfragen
$indexSizePotential = null;
if ($provider instanceof BulkIndexProviderInterface) {
$indexSizePotential = $provider->getTotalCount();
}
$indexSizes[$provider->getIndexName()] = $indexSizePotential;
}
ksort($indexSizes);
return $indexSizes;
}
/**
* @param string $name Index-Name
*
* @throws ProviderMissingException
* @throws InvalidReturnValueException
*
* @return void
*/
public function updateIndexFull($name)
{
/** @var FullIndexProviderInterface $provider */
$provider = $this->tryGetProviderByIndexName($name);
if ($provider === null) {
throw new ProviderMissingException(sprintf('Provider for index "%s" is missing', $name));
}
if ($provider instanceof BulkIndexProviderInterface) {
/** @var BulkIndexProviderInterface $provider */
$totalCount = $provider->getTotalCount();
if ($totalCount < 0) {
throw new InvalidReturnValueException(sprintf(
'Method %s::getTotalCount() returned an invalid value "%s". Total count must be a positive number.',
get_class($provider),
$totalCount
));
}
$itemsPerStep = $provider->getBulkSize();
$currentOffset = 0;
// Alle Index-Einträge als "veraltet" markieren
$this->markIndexAsOutdated($name);
do {
$items = $provider->getBulkItems($currentOffset, $itemsPerStep);
$this->db->beginTransaction();
foreach ($items as $item) {
$this->saveItem($item);
}
unset($items);
$this->db->commit();
$currentOffset += $itemsPerStep;
} while ($currentOffset < $totalCount);
// Full-Update-Zeitpunkt aktualisieren
$this->updateLastFullUpdateTime($name);
// Diff-Update-Zeitpunkt ebenfalls aktualisieren > Nächstes Diff-Update dann ab diesem Zeitpunkt
$this->updateLastDiffUpdateTime($name);
// Als "veraltet" markierte Index-Einträge löschen
$this->deleteOutdatedIndexItems($name);
}
if (
$provider instanceof FullIndexProviderInterface &&
!$provider instanceof BulkIndexProviderInterface
) {
$this->markIndexAsOutdated($name);
$items = $provider->getAllItems();
$this->db->beginTransaction();
foreach ($items as $item) {
$this->saveItem($item);
}
$this->db->commit();
$this->updateLastFullUpdateTime($name);
$this->deleteOutdatedIndexItems($name);
}
}
/**
* @param string $name Index-Name
* @param DateTimeInterface $since
*
* @throws ProviderMissingException
*
* @return void
*/
public function updateIndexSince($name, DateTimeInterface $since)
{
/** @var DiffIndexProviderInterface $provider */
$provider = $this->tryGetProviderByIndexName($name);
if ($provider === null) {
throw new ProviderMissingException(sprintf('Provider for index "%s" is missing', $name));
}
if (!$provider instanceof DiffIndexProviderInterface) {
return;
}
$items = $provider->getItemsSince($since);
$this->db->beginTransaction();
foreach ($items as $item) {
$this->saveItem($item);
}
$this->db->commit();
$this->updateLastDiffUpdateTime($name);
}
/**
* @param string $name
*
* @return DateTimeInterface|null
*/
public function getLastFullIndexTime($name)
{
$dateString = $this->db->fetchValue(
'SELECT sig.last_full_update FROM `supersearch_index_group` AS `sig` WHERE sig.name = :index_name',
['index_name' => (string)$name]
);
try {
if (!empty($dateString)) {
return new DateTimeImmutable($dateString);
}
} catch (Exception $exception) {
// nope - return null
}
return null;
}
/**
* @param string $name
*
* @return DateTimeInterface|null
*/
public function getLastDiffIndexTime($name)
{
$dateString = $this->db->fetchValue(
'SELECT sig.last_diff_update FROM `supersearch_index_group` AS `sig` WHERE sig.name = :index_name',
['index_name' => (string)$name]
);
try {
if (!empty($dateString)) {
return new DateTimeImmutable($dateString);
}
} catch (Exception $exception) {
// nope - return null
}
return null;
}
/**
* @param IndexIdentifier $identifier
*
* @return void
*/
public function deleteIndexItem(IndexIdentifier $identifier)
{
$sql =
'DELETE FROM `supersearch_index_item`
WHERE `index_name` = :index_name AND `index_id` = :index_id
LIMIT 1';
$bindValues = [
'index_name' => $identifier->getName(),
'index_id' => $identifier->getId(),
];
$this->db->perform($sql, $bindValues);
}
/**
* @param IndexIdentifier $identifier
*
* @throws ProviderMissingException
* @throws ProviderIncompatibleException
*
* @return void
*/
public function updateIndexItem(IndexIdentifier $identifier)
{
/** @var ItemIndexProviderInterface $provider */
$provider = $this->tryGetProviderByIndexName($identifier->getName());
if ($provider === null) {
throw new ProviderMissingException(sprintf(
'Provider for index "%s" is missing.', $identifier->getName()
));
}
if (!$provider instanceof ItemIndexProviderInterface) {
throw new ProviderIncompatibleException(sprintf(
'Provider for index "%s" is incompatible. Provider %s does not implement %s.',
$identifier->getName(),
get_class($provider),
ItemIndexProviderInterface::class
));
}
$item = $provider->getItem($identifier);
if ($item === null) {
return;
}
$this->saveItem($item);
}
/**
* Updates or creates an index item
*
* @param IndexItem $item
*
* @throws ProviderMissingException
*
* @return void
*/
private function saveItem(IndexItem $item)
{
$existingItemId = (int)$this->db->fetchValue(
'SELECT sii.id
FROM `supersearch_index_item` AS `sii`
WHERE sii.index_name = :index_name AND sii.index_id = :index_id',
[
'index_name' => $item->identifier->getName(),
'index_id' => $item->identifier->getId(),
]
);
if ($existingItemId > 0) {
$this->updateItem($item);
} else {
$this->createItem($item);
}
}
/**
* @param IndexItem $item
*
* @throws DatabaseExceptionInterface
*
* @return void
*/
private function updateItem(IndexItem $item)
{
$searchWords = '';
if (!empty($item->data->getWords())) {
$searchWords = implode(' | ', $item->data->getWords());
}
$additionalInfos = null;
if (!empty($item->data->getAdditionalInfos())) {
$additionalInfos = implode(' ## ', $item->data->getAdditionalInfos());
}
$sql =
'UPDATE `supersearch_index_item`
SET
`project_id` = :project_id,
`title` = :title,
`subtitle` = :subtitle,
`additional_infos` = :additional_infos,
`link` = :link,
`search_words` = :search_words,
`created_at` = `created_at`,
`updated_at` = NOW(),
`outdated` = 0
WHERE `index_name` = :index_name AND `index_id` = :index_id
LIMIT 1';
$bindValues = [
'index_name' => $item->identifier->getName(),
'index_id' => $item->identifier->getId(),
'project_id' => $item->data->getProjectId(),
'title' => $item->data->getTitle(),
'link' => $item->data->getLink(),
'subtitle' => $item->data->getSubTitle(),
'search_words' => $searchWords,
'additional_infos' => $additionalInfos,
];
$this->db->perform($sql, $bindValues);
}
/**
* @param IndexItem $item
*
* @return void
*/
private function createItem(IndexItem $item)
{
$searchWords = '';
if (!empty($item->data->getWords())) {
$searchWords = implode(' | ', $item->data->getWords());
}
$additionalInfos = null;
if (!empty($item->data->getAdditionalInfos())) {
$additionalInfos = implode(' ## ', $item->data->getAdditionalInfos());
}
$sql =
'INSERT INTO `supersearch_index_item`
(`index_name`, `index_id`, `project_id`, `title`, `subtitle`, `additional_infos`,
`link`, `search_words`, `outdated`, `created_at`, `updated_at`)
VALUES
(:index_name, :index_id, :project_id, :title, :subtitle, :additional_infos,
:link, :search_words, 0, NOW(), NULL)';
$bindValues = [
'index_name' => $item->identifier->getName(),
'index_id' => $item->identifier->getId(),
'project_id' => $item->data->getProjectId(),
'title' => $item->data->getTitle(),
'link' => $item->data->getLink(),
'subtitle' => $item->data->getSubTitle(),
'search_words' => $searchWords,
'additional_infos' => $additionalInfos,
];
$this->db->perform($sql, $bindValues);
}
/**
* Alle Einträge als veraltet markieren
*
* Beim Update/Insert eines Eintrags wird dieser wieder auf ungelöscht gestellt bevor er wirklich gelöscht wird.
*
* @param string $indexName
*
* @return void
*/
private function markIndexAsOutdated($indexName)
{
$sql = 'UPDATE `supersearch_index_item` SET `outdated` = 1 WHERE `index_name` = :index_name';
$this->db->perform($sql, ['index_name' => (string)$indexName]);
}
/**
* @param string $indexName
*
* @return void
*/
private function deleteOutdatedIndexItems($indexName)
{
$sql = 'DELETE FROM `supersearch_index_item` WHERE `index_name` = :index_name AND `outdated` = 1';
$this->db->perform($sql, ['index_name' => (string)$indexName]);
}
/**
* @param string $name
*
* @return SearchIndexProviderInterface|null
*/
private function tryGetProviderByIndexName($name)
{
foreach ($this->provider as $provider) {
if ($name === $provider->getIndexName()) {
return $provider;
}
}
return null;
}
/**
* @param string $name
*
* @return void
*/
private function updateLastFullUpdateTime($name)
{
$sql = 'UPDATE `supersearch_index_group` SET `last_full_update` = NOW() WHERE `name` = :index_name LIMIT 1';
$this->db->perform($sql, ['index_name' => (string)$name]);
}
/**
* @param string $name
*
* @return void
*/
private function updateLastDiffUpdateTime($name)
{
$sql = 'UPDATE `supersearch_index_group` SET `last_diff_update` = NOW() WHERE `name` = :index_name LIMIT 1';
$this->db->perform($sql, ['index_name' => (string)$name]);
}
}