mirror of
https://github.com/OpenXE-org/OpenXE.git
synced 2024-12-27 23:20:28 +01:00
488 lines
15 KiB
PHP
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]);
|
||
|
}
|
||
|
}
|